// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2022 The Ebitengine Authors

//go:build ignore

package main

import (
	"bytes"
	"fmt"
	"go/format"
	"log"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"text/template"
)

var templateSymbols = template.Must(template.New("symbols").Funcs(funcs).Parse(
	`// Code generated by 'go generate' with gen.go. DO NOT EDIT.

// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2022 The Ebitengine Authors

//go:build {{.Tag}}

package fakecgo

{{ imports .Symbols }}

{{- range $di := .DynamicImports }}
{{- range $sym := $di.Symbols }}
//go:cgo_import_dynamic purego_{{ $sym }} {{ $sym }} "{{ $di.Name }}"
{{- end }}
{{- end }}

{{ range .Symbols -}}
//go:nosplit
//go:norace
func {{.Name}}(
{{- range .Args -}}
	{{- if .Name -}}
		{{.Name}} {{.Type}},
	{{- end -}}
{{- end -}}) {{.Return}} {
	{{- if .Return -}}
		{{- if eq .Return "unsafe.Pointer" -}}
			ret :=
		{{- else -}}
			return {{.Return}}(
		{{- end -}}
	{{- end -}}
call5({{.Name}}ABI0,
{{- range .Args}}
	{{- if .Name -}}
		{{- if hasPrefix .Type "*" -}}
			uintptr(unsafe.Pointer({{.Name}})),
		{{- else -}}
			uintptr({{.Name}}),
		{{- end -}}
	{{- else -}}
		0,
	{{- end -}}
{{- end -}}
	) {{/* end of call5 */}}
{{- if .Return -}}
	{{- if eq .Return "unsafe.Pointer"}}
		// this indirection is to avoid go vet complaining about possible misuse of unsafe.Pointer
		return *(*unsafe.Pointer)(unsafe.Pointer(&ret))
	{{- else -}}
		) {{/* end of cast */}}
	{{- end -}}
{{- end}}
}

{{end}}
{{- range .Symbols }}
//go:linkname _{{.Name}} _{{.Name}}
var _{{.Name}} uint8
var {{.Name}}ABI0 = uintptr(unsafe.Pointer(&_{{.Name}}))
{{ end }}
`))

var templateTrampolinesStubs = template.Must(template.New("trampolines").Parse(
	`// Code generated by 'go generate' with gen.go. DO NOT EDIT.

// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2022 The Ebitengine Authors

//go:build {{.Tag}}

#include "textflag.h"

// these stubs are here because it is not possible to go:linkname directly the C functions
{{ range .Symbols }}
TEXT _{{.Name}}(SB), NOSPLIT|NOFRAME, $0-0
	JMP purego_{{.Name}}(SB)
{{ end -}}
`))

type Arg struct {
	Name string
	Type string
}

type Symbol struct {
	Name   string
	Args   [5]Arg
	Return string
	GOOSes []string // empty means all GOOSes
}

var (
	libcSymbols = []Symbol{
		{"malloc", [5]Arg{{"size", "uintptr"}}, "unsafe.Pointer", nil},
		{"free", [5]Arg{{"ptr", "unsafe.Pointer"}}, "", nil},
		{"setenv", [5]Arg{{"name", "*byte"}, {"value", "*byte"}, {"overwrite", "int32"}}, "int32", nil},
		{"unsetenv", [5]Arg{{"name", "*byte"}}, "int32", nil},
		{"sigfillset", [5]Arg{{"set", "*sigset_t"}}, "int32", nil},
		{"nanosleep", [5]Arg{{"ts", "*syscall.Timespec"}, {"rem", "*syscall.Timespec"}}, "int32", nil},
		{"abort", [5]Arg{}, "", nil},
		{"sigaltstack", [5]Arg{{"ss", "*stack_t"}, {"old_ss", "*stack_t"}}, "int32", []string{"netbsd"}},
	}
	pthreadSymbols = []Symbol{
		{"pthread_attr_init", [5]Arg{{"attr", "*pthread_attr_t"}}, "int32", nil},
		{"pthread_create", [5]Arg{{"thread", "*pthread_t"}, {"attr", "*pthread_attr_t"}, {"start", "unsafe.Pointer"}, {"arg", "unsafe.Pointer"}}, "int32", nil},
		{"pthread_detach", [5]Arg{{"thread", "pthread_t"}}, "int32", nil},
		{"pthread_sigmask", [5]Arg{{"how", "sighow"}, {"ign", "*sigset_t"}, {"oset", "*sigset_t"}}, "int32", nil},
		{"pthread_self", [5]Arg{}, "pthread_t", []string{"darwin"}},
		{"pthread_get_stacksize_np", [5]Arg{{"thread", "pthread_t"}}, "size_t", []string{"darwin"}},
		{"pthread_attr_getstacksize", [5]Arg{{"attr", "*pthread_attr_t"}, {"stacksize", "*size_t"}}, "int32", []string{"linux", "freebsd", "netbsd"}},
		{"pthread_attr_setstacksize", [5]Arg{{"attr", "*pthread_attr_t"}, {"size", "size_t"}}, "int32", []string{"darwin"}},
		{"pthread_attr_destroy", [5]Arg{{"attr", "*pthread_attr_t"}}, "int32", []string{"linux", "freebsd", "netbsd"}},
		{"pthread_mutex_lock", [5]Arg{{"mutex", "*pthread_mutex_t"}}, "int32", nil},
		{"pthread_mutex_unlock", [5]Arg{{"mutex", "*pthread_mutex_t"}}, "int32", nil},
		{"pthread_cond_broadcast", [5]Arg{{"cond", "*pthread_cond_t"}}, "int32", nil},
		{"pthread_setspecific", [5]Arg{{"key", "pthread_key_t"}, {"value", "unsafe.Pointer"}}, "int32", nil},
	}
)

var funcs = map[string]any{
	"hasPrefix": strings.HasPrefix,
	"imports":   imports,
}

var GOOSes = []string{"darwin", "freebsd", "linux", "netbsd"}

func imports(symbols []Symbol) string {
	if len(symbols) == 0 {
		return ""
	}
	imports := make(map[string]struct{})
	imports["unsafe"] = struct{}{}
	for _, s := range symbols {
		for _, arg := range s.Args {
			if pkg, _, found := strings.Cut(arg.Type, "."); found {
				pkg = strings.TrimPrefix(pkg, "*")
				imports[pkg] = struct{}{}
			}
		}
	}
	if len(imports) == 1 {
		return `import "unsafe"`
	}
	result := make([]string, 0, len(imports))
	for imp := range imports {
		result = append(result, imp)
	}
	sort.Strings(result)
	var sb strings.Builder
	sb.WriteString("import (")
	for _, imp := range result {
		sb.WriteString("\n\t\"")
		sb.WriteString(imp)
		sb.WriteString("\"")
	}
	sb.WriteString("\n)")
	return sb.String()
}

// filterSymbols filters the symbols by GOOS.
// If goos is empty, it returns symbols that are not specific to any GOOS.
func filterSymbols(symbols []Symbol, goos string) []Symbol {
	filtered := make([]Symbol, 0, len(symbols))
	for _, s := range symbols {
		if (s.GOOSes == nil && goos == "") || contains(s.GOOSes, goos) {
			filtered = append(filtered, s)
		}
	}
	return filtered
}

func contains(slice []string, item string) bool {
	for _, s := range slice {
		if s == item {
			return true
		}
	}
	return false
}

func execute(t *template.Template, path string, data any) error {
	var buf bytes.Buffer
	if err := t.Execute(&buf, data); err != nil {
		return err
	}
	if filepath.Ext(path) == ".go" {
		source, err := format.Source(buf.Bytes())
		if err != nil {
			return err
		}
		buf.Reset()
		buf.Write(source)
	}
	return os.WriteFile(path, buf.Bytes(), 0644)
}

func symbolsNames(symbols []Symbol) []string {
	names := make([]string, len(symbols))
	for i, s := range symbols {
		names[i] = s.Name
	}
	return names
}

type dynamicImport struct {
	Name    string
	Symbols []string
}

type fileContent struct {
	Tag            string
	Symbols        []Symbol
	DynamicImports []dynamicImport
}

func run() error {
	allSymbols := append(append([]Symbol{}, libcSymbols...), pthreadSymbols...)
	goosAgnosticContent := fileContent{
		Symbols: filterSymbols(allSymbols, ""),
		Tag:     "!cgo && (" + strings.Join(GOOSes, " || ") + ")",
	}
	if err := execute(templateSymbols, "zsymbols.go", goosAgnosticContent); err != nil {
		return err
	}
	if err := execute(templateTrampolinesStubs, "ztrampolines_stubs.s", goosAgnosticContent); err != nil {
		return err
	}
	goosAgnosticLibCSymbols := filterSymbols(libcSymbols, "")
	goosAgnosticPthreadSymbols := filterSymbols(pthreadSymbols, "")
	for _, goos := range GOOSes {
		goosSymbols := filterSymbols(allSymbols, goos)
		var libcSO, pthreadSO string
		switch goos {
		case "darwin":
			libcSO = "/usr/lib/libSystem.B.dylib"
			pthreadSO = "/usr/lib/libSystem.B.dylib"
		case "freebsd":
			libcSO = "libc.so.7"
			pthreadSO = "libpthread.so"
		case "linux":
			libcSO = "libc.so.6"
			pthreadSO = "libpthread.so.0"
		case "netbsd":
			libcSO = "libc.so"
			pthreadSO = "libpthread.so"
		default:
			return fmt.Errorf("unsupported OS: %s", goos)
		}
		located := fileContent{
			DynamicImports: []dynamicImport{
				{libcSO, symbolsNames(append(goosAgnosticLibCSymbols, filterSymbols(libcSymbols, goos)...))},
				{pthreadSO, symbolsNames(append(goosAgnosticPthreadSymbols, filterSymbols(pthreadSymbols, goos)...))},
			},
			Symbols: goosSymbols,
			Tag:     "!cgo",
		}
		if err := execute(templateSymbols, fmt.Sprintf("zsymbols_%s.go", goos), located); err != nil {
			return err
		}
		if len(goosSymbols) != 0 {
			if err := execute(templateTrampolinesStubs, fmt.Sprintf("ztrampolines_%s.s", goos), located); err != nil {
				return err
			}
		}
	}
	return nil
}

func main() {
	if err := run(); err != nil {
		log.Fatal(err)
	}
}
