// 24 may 2014
package main

import (
	"fmt"
	"os"
	"strings"
	"go/token"
	"go/ast"
	"go/parser"
	"sort"
	"io/ioutil"
	"path/filepath"
	"os/exec"
)

func getPackage(path string) (pkg *ast.Package) {
	fileset := token.NewFileSet()		// parser.ParseDir() actually writes to this; not sure why it doesn't return one instead
	filter := func(i os.FileInfo) bool {
		return strings.HasSuffix(i.Name(), "_windows.go")
	}
	pkgs, err := parser.ParseDir(fileset, path, filter, parser.AllErrors)
	if err != nil {
		panic(err)
	}
	if len(pkgs) != 1 {
		panic("more than one package found")
	}
	for k, _ := range pkgs {		// get the sole key
		pkg = pkgs[k]
	}
	return pkg
}

type walker struct {
	desired	func(string) bool
}

var known = map[string]string{}
var unknown = map[string]struct{}{}

func (w *walker) Visit(node ast.Node) ast.Visitor {
	if n, ok := node.(*ast.Ident); ok {
		if w.desired(n.Name) {
			if n.Obj != nil {
				delete(unknown, n.Name)
				kind := n.Obj.Kind.String()
				if known[n.Name] != "" && known[n.Name] != kind {
					panic(n.Name + "(" + kind + ") already known to be a " + known[n.Name])
				}
				known[n.Name] = kind
			} else if _, ok := known[n.Name]; !ok {		// only if not known
				unknown[n.Name] = struct{}{}
			}
		}
	}
	return w
}

func gatherNames(pkg *ast.Package) {
	desired := func(name string) bool {
		if strings.HasPrefix(name, "_") && len(name) > 1 {
			return !strings.ContainsAny(name,
				"abcdefghijklmnopqrstuvwxyz")
		}
		return false
	}
	for _, f := range pkg.Files {
		for _, d := range f.Decls {
			ast.Walk(&walker{desired}, d)
		}
	}
}

// some constants confuse cgo into thinking they're external symbols for some reason
// fortunately all these constants are pointers
// TODO debug cgo
var hacknames = map[string]string{
	"_INVALID_HANDLE_VALUE":		"x_INVALID_HANDLE_VALUE",
	"_NULL":						"x_NULL",
	"_IDI_APPLICATION":			"x_IDI_APPLICATION",
	"_IDC_ARROW":				"x_IDC_ARROW",
	"_HWND_MESSAGE":			"x_HWND_MESSAGE",
}

func hacknamesPreamble() string {
	if len(hacknames) == 0 {
		return ""
	}
	// keep sorted for git
	hn := make([]string, 0, len(hacknames))
	for origname, _ := range hacknames {
		hn = append(hn, origname)
	}
	sort.Strings(hn)
	s := "// /* because cgo has issues with these */\n"
	s += "// #include <stdint.h>\n"
	for _, origname := range hn {
		s += "// uintptr_t " + hacknames[origname] + " = (uintptr_t) (" +
			origname[1:] + ");\n"		// strip leading _
	}
	return s
}

func preamble(pkg string) string {
	return "// autogenerated by windowsconstgen; do not edit\n" +
		"package " + pkg + "\n\n"		// two newlines to please go fmt
}

// for backwards compatibiilty reasons, Windows defines GetWindowLongPtr()/SetWindowLongPtr() as a macro which expands to GetWindowLong()/SetWindowLong() on 32-bit systems
// we'll just simulate that here
var gwlpNames = map[string]string{
	"386":		"etWindowLongW",
	"amd64":		"etWindowLongPtrW",
}

func printConst(f *os.File, goconst string, winconst string) {
	fmt.Fprintf(f, "	fmt.Println(\"const %s =\", C.%s)\n", goconst, winconst)
}

func printBlankLine(f *os.File) {
	fmt.Fprintf(f, "	fmt.Println()\n")
}

func printGWLPName(f *os.File, which string, char string, targetarch string) {
	fmt.Fprintf(f, "	fmt.Println(\"var %s = user32.NewProc(\\\"%s\\\")\")\n",
		which, char + gwlpNames[targetarch])
}

func main() {
	if len(os.Args) < 3 {
		panic("usage: " + os.Args[0] + " path goarch [go-command-options...]")
	}
	pkgpath := os.Args[1]
	targetarch := os.Args[2]
	if _, ok := gwlpNames[targetarch]; !ok {
		panic("unknown target windows/" + targetarch)
	}
	goopts := os.Args[3:]		// valid if len(os.Args) == 3; in that case this will just be a slice of length zero

	pkg := getPackage(pkgpath)
	gatherNames(pkg)

	// if we still have some known, I didn't clean things up completely
	knowns := ""
	for ident, kind := range known {
		if kind != "var" && kind != "const" {
			continue
		}
		knowns += "\n" + ident + " (" + kind + ")"
	}
	if knowns != "" {
		panic("error: the following are still known!" + knowns)		// has a newline already
	}

	// keep sorted for git
	consts := make([]string, 0, len(unknown))
	for ident, _ := range unknown {
		if hackname, ok := hacknames[ident]; ok {
			consts = append(consts, hackname)
			continue
		}
		consts = append(consts, ident)
	}
	sort.Strings(consts)

	// thanks to james4k in irc.freenode.net/#go-nuts
	tmpdir, err := ioutil.TempDir("", "windowsconstgen")
	if err != nil {
		panic(err)
	}
	genoutname := filepath.Join(tmpdir, "gen.go")
	f, err := os.Create(genoutname)
	if err != nil {
		panic(err)
	}
	fmt.Fprintf(f, "%s" +
		"import \"fmt\"\n" +
		"// #include <windows.h>\n" +
		"// #include <commctrl.h>\n" +
		"%s" +
		"import \"C\"\n" +
		"func main() {\n" +
		"	fmt.Print(%q)\n",
		preamble("main"), hacknamesPreamble(), preamble("ui"))
	for _, ident := range consts {
		if ident[0] == 'x' {
			// hack name; strip the leading x (but not the _ after it) from the constant name but keep the value name unchanged
			printConst(f, ident[1:], ident)
			continue
		}
		// not a hack name; strip the leading _ from the value name but keep the constant name unchanged
		printConst(f, ident, ident[1:])
	}
	printBlankLine(f)		// to please go fmt
	// and now for _getWindowLongPtr/_setWindowLongPtr
	printGWLPName(f, "_getWindowLongPtr", "G", targetarch)
	printGWLPName(f, "_setWindowLongPtr", "S", targetarch)
	fmt.Fprintf(f, "}\n")
	f.Close()

	cmd := exec.Command("go", "run")
	cmd.Args = append(cmd.Args, goopts...)		// valid if len(goopts) == 0; in that case this will just be a no-op
	cmd.Args = append(cmd.Args, genoutname)
	f, err = os.Create(filepath.Join(pkgpath, "zconstants_windows_" + targetarch + ".go"))
	if err != nil {
		panic(err)
	}
	defer f.Close()
	cmd.Stdout = f
	cmd.Stderr = os.Stderr
	// we need to preserve the environment EXCEPT FOR the variables we're overriding
	// thanks to raggi and smw in irc.freenode.net/#go-nuts
	for _, ev := range os.Environ() {
		if strings.HasPrefix(ev, "GOOS=") ||
			strings.HasPrefix(ev, "GOARCH=") ||
			strings.HasPrefix(ev, "CGO_ENABLED=") {
			continue
		}
		cmd.Env = append(cmd.Env, ev)
	}
	cmd.Env = append(cmd.Env,
		"GOOS=windows",
		"GOARCH=" + targetarch,
		"CGO_ENABLED=1")		// needed as it's not set by default in cross-compiles
	err = cmd.Run()
	if err != nil {
		// TODO find a way to get the exit code
		os.Exit(1)
	}

	// TODO remove the temporary directory
}