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

//go:build ignore

package main

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

const templateSymbols = `// 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 !cgo && (darwin || freebsd || linux)

package fakecgo

import (
	"syscall"
	"unsafe"
)

// setg_trampoline calls setg with the G provided
func setg_trampoline(setg uintptr, G uintptr)

// call5 takes fn the C function and 5 arguments and calls the function with those arguments
func call5(fn, a1, a2, a3, a4, a5 uintptr) uintptr

{{ range . -}}
//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 . }}
//go:linkname _{{.Name}} _{{.Name}}
var _{{.Name}} uint8
var {{.Name}}ABI0 = uintptr(unsafe.Pointer(&_{{.Name}}))
{{ end }}
`

const templateTrampolinesStubs = `// 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 !cgo && (darwin || freebsd || linux)

#include "textflag.h"

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

const templateSymbolsGoos = `// 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 !cgo

package fakecgo

{{- range $location := . }}
{{- range .Symbols }}
//go:cgo_import_dynamic purego_{{ .Name }} {{ .Name }} "{{ $location.SharedObject }}"
{{- end }}
{{- end }}
`

type Arg struct {
	Name string
	Type string
}

type Symbol struct {
	Name   string
	Args   [5]Arg
	Return string
}

type LocatedSymbols struct {
	SharedObject string
	Symbols      []Symbol
}

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

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

func run() error {
	t, err := template.New("symbol.go").Funcs(funcs).Parse(templateSymbols)
	if err != nil {
		return err
	}
	f, err := os.Create("symbols.go")
	defer f.Close()
	if err != nil {
		return err
	}
	allSymbols := append(append([]Symbol{}, libcSymbols...), pthreadSymbols...)
	buf := new(bytes.Buffer)
	if err := t.Execute(buf, allSymbols); err != nil {
		return err
	}
	source, err := format.Source(buf.Bytes())
	if err != nil {
		return err
	}
	if _, err = f.Write(source); err != nil {
		return err
	}
	t, err = template.New("trampolines_stubs.s").Funcs(funcs).Parse(templateTrampolinesStubs)
	if err != nil {
		return err
	}
	f, err = os.Create("trampolines_stubs.s")
	defer f.Close()
	if err != nil {
		return err
	}
	if err := t.Execute(f, allSymbols); err != nil {
		return err
	}
	t, err = template.New("symbols_goos.go").Parse(templateSymbolsGoos)
	if err != nil {
		return err
	}
	for _, goos := range []string{"darwin", "linux", "freebsd"} {
		f, err = os.Create(fmt.Sprintf("symbols_%s.go", goos))
		defer f.Close()
		if err != nil {
			return err
		}
		b := &bytes.Buffer{}
		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"
		default:
			return fmt.Errorf("unsupported OS: %s", goos)
		}
		located := []LocatedSymbols{
			{SharedObject: libcSO, Symbols: libcSymbols},
			{SharedObject: pthreadSO, Symbols: pthreadSymbols},
		}
		if err = t.Execute(b, located); err != nil {
			return err
		}
		var src []byte
		src, err = format.Source(b.Bytes())
		if err != nil {
			return err
		}
		if _, err = f.Write(src); err != nil {
			return err
		}
	}
	return nil
}

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