//go:build link

package main

// Read source files and RFC and errata files, and cross-link them.

// todo: also cross-reference typescript and possibly other files. switch from go parser to just reading the source as text.

import (
	"bytes"
	"flag"
	"fmt"
	"go/parser"
	"go/token"
	"log"
	"os"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
)

func usage() {
	log.Println("usage: link ../*.go ../*/*.go")
	flag.PrintDefaults()
	os.Exit(2)
}

func main() {
	log.SetFlags(0)
	flag.Usage = usage
	flag.Parse()
	args := flag.Args()
	if len(args) == 0 {
		usage()
	}

	type ref struct {
		srcpath   string
		srclineno int
		dstpath   string
		dstlineno int
		dstisrfc  bool
		dstrfc    string // e.g. "5322" or "6376-eid4810"
		comment   string // e.g. "todo" or "todo spec"
	}

	// RFC-file to RFC-line to references to list of file+line (possibly RFCs).
	rfcLineSources := map[string]map[int][]ref{}

	// Source-file to source-line to references of RFCs.
	sourceLineRFCs := map[string]map[int][]ref{}

	re := regexp.MustCompile(`((../)*)rfc/([0-9]{4,5})(-eid([1-9][0-9]*))?(:([1-9][0-9]*))?`)

	addRef := func(m map[string]map[int][]ref, rfc string, lineno int, r ref) {
		lineRefs := m[rfc]
		if lineRefs == nil {
			lineRefs = map[int][]ref{}
			m[rfc] = lineRefs
		}
		lineRefs[lineno] = append(lineRefs[lineno], r)
	}

	// Parse all .go files on the cli, assumed to be relative to current dir.
	fset := token.NewFileSet()
	for _, arg := range args {
		f, err := parser.ParseFile(fset, arg, nil, parser.ParseComments|parser.SkipObjectResolution)
		if err != nil {
			log.Fatalf("parse file %q: %s", arg, err)
		}
		for _, cg := range f.Comments {
			for _, c := range cg.List {
				lines := strings.Split(c.Text, "\n")
				for i, line := range lines {
					matches := re.FindAllStringSubmatch(line, -1)
					if len(matches) == 0 {
						continue
					}

					var comment string
					if strings.HasPrefix(line, "// todo") {
						s, _, have := strings.Cut(strings.TrimPrefix(line, "// "), ":")
						if have {
							comment = s
						} else {
							comment = "todo"
						}
					}

					srcpath := arg
					srclineno := fset.Position(c.Pos()).Line + i
					dir := filepath.Dir(srcpath)
					for _, m := range matches {
						pre := m[1]
						rfc := m[3]
						eid := m[5]
						lineStr := m[7]
						if eid != "" && lineStr != "" {
							log.Fatalf("%s:%d: cannot reference both errata (eid %q) to specified line number", srcpath, srclineno, eid)
						}
						var dstlineno int
						if lineStr != "" {
							v, err := strconv.ParseInt(lineStr, 10, 32)
							if err != nil {
								log.Fatalf("%s:%d: bad linenumber %q: %v", srcpath, srclineno, lineStr, err)
							}
							dstlineno = int(v)
						}
						if dstlineno <= 0 {
							dstlineno = 1
						}
						if eid != "" {
							rfc += "-eid" + eid
						}
						dstpath := filepath.Join(dir, pre+"rfc", rfc)
						if _, err := os.Stat(dstpath); err != nil {
							log.Fatalf("%s:%d: references %s: %v", srcpath, srclineno, dstpath, err)
						}
						r := ref{srcpath, srclineno, dstpath, dstlineno, true, rfc, comment}
						addRef(sourceLineRFCs, r.srcpath, r.srclineno, r)
						addRef(rfcLineSources, r.dstrfc, r.dstlineno, ref{r.dstrfc, r.dstlineno, r.srcpath, r.srclineno, false, "", comment})
					}
				}
			}
		}
	}

	files, err := os.ReadDir(".")
	if err != nil {
		log.Fatalf("readdir: %v", err)
	}
	for _, de := range files {
		name := de.Name()
		isrfc := isRFC(name)
		iserrata := isErrata(name)
		if !isrfc && !iserrata {
			continue
		}
		oldBuf, err := os.ReadFile(name)
		if err != nil {
			log.Fatalf("readdir: %v", err)
		}
		old := string(oldBuf)
		b := &bytes.Buffer{}
		lineRefs := rfcLineSources[name]
		lines := strings.Split(old, "\n")
		if len(lines) > 0 && lines[len(lines)-1] == "" {
			lines = lines[:len(lines)-1]
		}
		for i, line := range lines {
			if !(iserrata && i > 0) && len(line) > 80 {
				line = strings.TrimRight(line[:80], " ")
			}
			refs := lineRefs[i+1]
			if len(refs) > 0 {
				line = fmt.Sprintf("%-80s", line)

				// Lookup source files for rfc:line, so we can cross-link the rfcs.
				done := map[string]bool{}
				for _, r := range refs {
					for _, xr := range sourceLineRFCs[r.dstpath][r.dstlineno] {
						sref := fmt.Sprintf(" %s:%d", xr.dstrfc, xr.dstlineno)
						if xr.dstrfc == name && xr.dstlineno == i+1 || done[sref] {
							continue
						}
						line += sref
						done[sref] = true
					}
				}

				// Add link from rfc to source code.
				for _, r := range refs {
					comment := r.comment
					if comment != "" {
						comment += ": "
					}
					line += fmt.Sprintf(" %s%s:%d", comment, r.dstpath, r.dstlineno)
				}
			}
			line += "\n"
			b.WriteString(line)
		}
		newBuf := b.Bytes()
		if !bytes.Equal(oldBuf, newBuf) {
			if err := os.WriteFile(name, newBuf, 0660); err != nil {
				log.Printf("writefile %q: %s", name, err)
			}
			log.Print(name)
		}
	}
}

func isRFC(name string) bool {
	if len(name) < 4 || len(name) > 5 {
		return false
	}
	for _, c := range name {
		if c < '0' || c > '9' {
			return false
		}
	}
	return true
}

func isErrata(name string) bool {
	t := strings.Split(name, "-")
	return len(t) == 2 && isRFC(t[0]) && strings.HasPrefix(t[1], "eid")
}