//go:build linux

package main

import (
	"debug/elf"
	"flag"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"runtime"
	"strings"
)

var (
	destFlag    string
	packageFlag string
)

func main() {
	flag.Usage = func() {
		fmt.Fprint(os.Stderr, `Usage: memexec-gen PATH

Environment variables:
  LD_LIBRARY_PATH
  	list of directories where to search libraries, see 'man 7 ld.so'

Options:
`)
		flag.PrintDefaults()
	}
	flag.StringVar(&destFlag, "dest", "",
		"`path` where binaries and go file are written to, defaults to PATH basename",
	)
	flag.StringVar(&packageFlag, "package", "",
		"generated package `name`, defaults to PATH basename",
	)
	flag.Parse()
	if flag.NArg() != 1 {
		flag.Usage()
		os.Exit(2)
	}
	if err := run(flag.Arg(0)); err != nil {
		fmt.Fprintf(os.Stderr, "error: %s\n", err)
		os.Exit(127)
	}
}

func run(bin string) error {
	base := filepath.Base(bin)
	if destFlag == "" {
		destFlag = base
	}
	if packageFlag == "" {
		packageFlag = base
	}

	err := os.MkdirAll(destFlag, 0o755)
	if err != nil && !os.IsExist(err) {
		return err
	}
	output, err = os.OpenFile(
		filepath.Join(destFlag, "memexec.go"),
		os.O_CREATE|os.O_WRONLY|os.O_TRUNC,
		0o644,
	)
	if err != nil {
		return err
	}

	libs := map[string]string{}
	if err := ldd(libs, bin, func(name string) (string, error) {
		return name, nil
	}); err != nil {
		return err
	}
	for name, path := range libs {
		src, err := os.Open(path)
		if err != nil {
			return err
		}
		if name == bin {
			name = base
		}
		dst, err := os.OpenFile(
			filepath.Join(destFlag, name),
			os.O_CREATE|os.O_WRONLY|os.O_TRUNC,
			0o755,
		)
		if err != nil {
			return err
		}
		if _, err := io.Copy(dst, src); err != nil {
			return err
		}
		if err := dst.Close(); err != nil {
			return err
		}
		if err := src.Close(); err != nil {
			return err
		}
	}

	out("// Code generated by memexec-gen. DO NOT EDIT.")
	out("package ", packageFlag)
	out()
	out("import (")
	out(`	_ "embed"`)
	out(`	"os"`)
	out(`	"os/exec"`)
	out(`	"path/filepath"`)
	out()
	out(`	"github.com/amenzhinsky/go-memexec"`)
	out(")")
	out()

	for name := range libs {
		if name == bin {
			name = base
		}
		out("//go:embed ", name)
		out("var ", goName(name), " []byte")
		out()
	}

	out("func New(opts ...memexec.Option) (*memexec.Exec, error) {")
	out(`	temp, err := os.MkdirTemp("", "")`)
	out("	if err != nil {")
	out("		return nil, err")
	out("	}")
	for name := range libs {
		if name == bin {
			continue
		}
		out(`	if err := os.WriteFile(filepath.Join(temp, "`, name, `"), `, goName(name), ", 0o755); err != nil {")
		out("		return nil, err")
		out("	}")
	}
	out("	return memexec.New(", goName(base), ",  append([]memexec.Option{")
	out("		memexec.WithPrepare(func(exe *exec.Cmd) {")
	out(`			exe.Env = append(exe.Env, "LD_LIBRARY_PATH="+temp)`)
	out("		}),")
	out("		memexec.WithCleanup(func() error {")
	out("			return os.RemoveAll(temp)")
	out("		}),")
	out("	}, opts...)...)")
	out("}")

	return nil
}

var output io.Writer

func out(strs ...string) {
	for _, str := range strs {
		if _, err := fmt.Fprint(output, str); err != nil {
			panic(err)
		}
	}
	if _, err := fmt.Fprintln(output); err != nil {
		panic(err)
	}
}

func goName(name string) string {
	name = strings.ReplaceAll(name, ".", "_")
	name = strings.ReplaceAll(name, "-", "_")
	return "memexec_" + name
}

func ldd(
	deps map[string]string, name string, look func(name string) (string, error),
) error {
	path, err := look(name)
	if err != nil {
		return err
	}
	deps[name] = path
	e, err := elf.Open(path)
	if err != nil {
		return err
	}
	l, err := e.ImportedLibraries()
	if err != nil {
		return err
	}
	rpaths, err := e.DynString(elf.DT_RPATH)
	if err != nil {
		return err
	}
	runpaths, err := e.DynString(elf.DT_RUNPATH)
	if err != nil {
		return err
	}
	if err := e.Close(); err != nil {
		return err
	}
	for _, lib := range l {
		if _, ok := deps[lib]; ok {
			continue
		}
		if err := ldd(deps, lib, func(name string) (string, error) {
			return lookLibrary(name, rpaths, runpaths)
		}); err != nil {
			return err
		}
	}
	return nil
}

var ldPaths = []string{"/lib", "/usr/local/lib", "/usr/lib"}

func init() {
	// TODO: os.ReadFile(fmt.Sprintf("/etc/ld-musl-$(ARCH).path"))
	switch runtime.GOARCH {
	case "amd64":
		ldPaths = append(ldPaths,
			"/lib64",
			"/usr/lib64",
			"/usr/local/lib/x86_64-linux-gnu",
			"/lib/x86_64-linux-gnu",
			"/usr/lib/x86_64-linux-gnu",
		)
	case "arm64":
		ldPaths = append(ldPaths,
			"/usr/local/lib/aarch64-linux-gnu",
			"/lib/aarch64-linux-gnu",
			"/usr/lib/aarch64-linux-gnu",
		)
	}
	if s := os.Getenv("LD_LIBRARY_PATH"); s != "" {
		ldPaths = append(ldPaths, filepath.SplitList(s)...)
	}
}

func lookLibrary(name string, rpaths, runpaths []string) (string, error) {
	for _, dir := range append(rpaths, append(ldPaths, runpaths...)...) {
		path := filepath.Join(dir, name)
		stat, err := os.Stat(path)
		if err != nil {
			if os.IsNotExist(err) {
				continue
			}
			return "", err
		}
		if !stat.IsDir() {
			return path, nil
		}
	}
	return "", fmt.Errorf("dynamic library %s not found", name)
}
