// Copyright (c) 2024, The Garble Authors.
// See LICENSE for licensing information.

//go:build ignore

// This is a program used with `go generate`, so it handles errors via panic.
package main

import (
	"bytes"
	"cmp"
	"fmt"
	"go/format"
	"go/version"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"slices"
	"strings"
	"text/template"
)

var goVersions = []string{"go1.25.0"}

var tmplTables = template.Must(template.New("").Parse(`
// Code generated by scripts/gen_go_std_tables.go; DO NOT EDIT.

// Generated from Go versions {{ .GoVersions }}.

package main

// runtimeAndDeps contains the runtime package and all of its transitive dependencies
// as reported by 'go list -deps'.
var runtimeAndDeps = map[string]bool{
{{- range $path := .RuntimeAndDeps }}
	"{{ $path.String }}": true, // {{ $path.GoVersionLang }}
{{- end }}
}

// runtimeAndLinknamed contains the runtime package and all the packages
// which it points to via //go:linkname directives.
// We need to track these as some are not imported as transitive dependencies,
// and we need to load these to properly obfuscate the linkname target names.
//
// Note that runtimeAndLinknamed may contain duplicates with runtimeAndDeps.
// This is on purpose; some packages are in runtimeAndDeps via 'go list -deps'
// but not transitively imported on some platforms, even though they are used
// from the runtime package via //go:linkname directives on those platforms.
// To make sure we have coverage on all platforms, we allow duplicates.
var runtimeAndLinknamed = []string{
{{- range $path := .RuntimeAndLinknamed }}
	"{{ $path.String }}", // {{ $path.GoVersionLang }}
{{- end }}
	// The net package linknames to the runtime, not the other way around.
	"net",
	// The testing package uses a //go:linkname directive pointing to testing/synctest,
	// but it doesn't import the package, presumably to avoid an import cycle.
	"testing/synctest",
}

var compilerIntrinsics = map[string]map[string]bool{
{{- range $intr := .CompilerIntrinsics }}
	"{{ $intr.Path }}": {
{{- range $name := $intr.Names }}
		"{{ $name.String }}": true, // {{ $name.GoVersionLang }}
{{- end }}
	},
{{- end }}
}

var reflectSkipPkg = map[string]bool{
	"fmt": true,
}
`[1:]))

type tmplData struct {
	GoVersions          []string
	RuntimeAndDeps      []versionedString
	RuntimeAndLinknamed []versionedString
	CompilerIntrinsics  []tmplIntrinsic
}

type tmplIntrinsic struct {
	Path  string
	Names []versionedString
}

func (t tmplIntrinsic) Compare(t2 tmplIntrinsic) int {
	return cmp.Compare(t.Path, t2.Path)
}

func (t tmplIntrinsic) Equal(t2 tmplIntrinsic) bool {
	return t.Compare(t2) == 0
}

type versionedString struct {
	String        string
	GoVersionLang string
}

func (v versionedString) Compare(v2 versionedString) int {
	if c := cmp.Compare(v.String, v2.String); c != 0 {
		return c
	}
	// Negated so that newer Go versions go first.
	return -cmp.Compare(v.GoVersionLang, v2.GoVersionLang)
}

func (v versionedString) Equal(v2 versionedString) bool {
	// Note that we do equality based on String alone,
	// because we only need one String entry with the latest version.
	return v.String == v2.String
}

func cmdGo(goVersion string, args ...string) versionedString {
	cmd := exec.Command("go", args...)
	cmd.Env = append(cmd.Environ(), "GOTOOLCHAIN="+goVersion)
	out, err := cmd.Output()
	if err != nil {
		panic(err)
	}
	return versionedString{
		String:        string(bytes.TrimSpace(out)), // no trailing newline
		GoVersionLang: version.Lang(goVersion),
	}
}

func readFile(path string) string {
	data, err := os.ReadFile(path)
	if err != nil {
		panic(err)
	}
	return string(data)
}

func lines(vs versionedString) []versionedString {
	split := strings.Split(vs.String, "\n")
	var versioned []versionedString
	for _, s := range split {
		versioned = append(versioned, versionedString{
			String:        s,
			GoVersionLang: vs.GoVersionLang,
		})
	}
	return versioned
}

var rxLinkname = regexp.MustCompile(`^//go:linkname .* ([^.]*)\.[^.]*$`)
var rxIntrinsic = regexp.MustCompile(`\b(addF|alias)\("([^"]*)", "([^"]*)",`)

func main() {
	var runtimeAndDeps []versionedString
	for _, goVersion := range goVersions {
		runtimeAndDeps = append(runtimeAndDeps, lines(cmdGo(goVersion, "list", "-deps", "runtime"))...)
	}
	slices.SortFunc(runtimeAndDeps, versionedString.Compare)
	runtimeAndDeps = slices.CompactFunc(runtimeAndDeps, versionedString.Equal)

	var goroots []versionedString
	for _, goVersion := range goVersions {
		goroots = append(goroots, cmdGo(goVersion, "env", "GOROOT"))
	}

	// All packages that the runtime linknames to, except runtime and its dependencies.
	// This resulting list is what we need to "go list" when obfuscating the runtime,
	// as they are the packages that we may be missing.
	var runtimeAndLinknamed []versionedString
	for _, goroot := range goroots {
		runtimeGoFiles, err := filepath.Glob(filepath.Join(goroot.String, "src", "runtime", "*.go"))
		if err != nil {
			panic(err)
		}
		for _, goFile := range runtimeGoFiles {
			for line := range strings.SplitSeq(readFile(goFile), "\n") {
				m := rxLinkname.FindStringSubmatch(line)
				if m == nil {
					continue
				}
				path := m[1]
				switch path {
				case "main", "runtime/metrics_test":
					continue
				}
				runtimeAndLinknamed = append(runtimeAndLinknamed, versionedString{
					String:        path,
					GoVersionLang: goroot.GoVersionLang,
				})
			}
		}
	}
	slices.SortFunc(runtimeAndLinknamed, versionedString.Compare)
	runtimeAndLinknamed = slices.CompactFunc(runtimeAndLinknamed, versionedString.Equal)

	compilerIntrinsicsIndexByPath := make(map[string]int)
	var compilerIntrinsics []tmplIntrinsic
	for _, goroot := range goroots {
		for line := range strings.SplitSeq(readFile(filepath.Join(
			goroot.String, "src", "cmd", "compile", "internal", "ssagen", "intrinsics.go",
		)), "\n") {
			m := rxIntrinsic.FindStringSubmatch(line)
			if m == nil {
				continue
			}
			path, name := m[2], m[3]
			vs := versionedString{
				String:        name,
				GoVersionLang: goroot.GoVersionLang,
			}
			if i := compilerIntrinsicsIndexByPath[path]; i == 0 {
				compilerIntrinsicsIndexByPath[path] = len(compilerIntrinsics)
				compilerIntrinsics = append(compilerIntrinsics, tmplIntrinsic{
					Path:  path,
					Names: []versionedString{vs},
				})
			} else {
				compilerIntrinsics[i].Names = append(compilerIntrinsics[i].Names, vs)
			}
		}
	}
	slices.SortFunc(compilerIntrinsics, tmplIntrinsic.Compare)
	compilerIntrinsics = slices.CompactFunc(compilerIntrinsics, tmplIntrinsic.Equal)
	for path := range compilerIntrinsics {
		intr := &compilerIntrinsics[path]
		slices.SortFunc(intr.Names, versionedString.Compare)
		intr.Names = slices.CompactFunc(intr.Names, versionedString.Equal)
	}

	var buf bytes.Buffer
	if err := tmplTables.Execute(&buf, tmplData{
		GoVersions:          goVersions,
		RuntimeAndDeps:      runtimeAndDeps,
		RuntimeAndLinknamed: runtimeAndLinknamed,
		CompilerIntrinsics:  compilerIntrinsics,
	}); err != nil {
		panic(err)
	}
	out := buf.Bytes()
	formatted, err := format.Source(out)
	if err != nil {
		fmt.Println(string(out))
		panic(err)
	}

	if err := os.WriteFile("go_std_tables.go", formatted, 0o666); err != nil {
		panic(err)
	}
}
