// +build mktemplates
// Build tag so that users who run `go get zombiezen.com/go/capnproto2/...` don't install this command.
// cd internal/cmd/mktemplates && go build -tags=mktemplates

// mktemplates is a command to regenerate capnpc-go/templates.go.
package main

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

func main() {
	if len(os.Args) != 3 {
		fmt.Fprintln(os.Stderr, "usage: mktemplates OUT DIR")
		os.Exit(64)
	}
	dir := os.Args[2]
	names, err := listdir(dir)
	if err != nil {
		fatalln(err)
	}
	ts := make([]template, len(names))
	for i, name := range names {
		src, err := ioutil.ReadFile(filepath.Join(dir, name))
		if err != nil {
			fatalf("reading template %s: %v", name, err)
		}
		compiled, err := compileTemplate(name, string(src))
		if err != nil {
			fatalf("compiling template %s: %v", name, err)
		}
		ts[i] = template{
			name:    name,
			content: compiled,
		}
	}
	genbuf := new(bytes.Buffer)
	err = generateGo(genbuf, os.Args, ts)
	if err != nil {
		fatalln("generating code:", err)
	}
	code, err := format.Source(genbuf.Bytes())
	if err != nil {
		fatalln("formatting code:", err)
	}
	outname := os.Args[1]
	out, err := os.Create(outname)
	if err != nil {
		fatalf("opening destination %s: %v", outname, err)
	}
	_, err = out.Write(code)
	cerr := out.Close()
	if err != nil {
		fatalf("write to %s: %v", outname, err)
	}
	if cerr != nil {
		fatalln(err)
	}
}

func compileTemplate(name, src string) (string, error) {
	tset, err := parse.Parse(name, src, "{{", "}}", funcStubs)
	if err != nil {
		return "", err
	}
	return tset[name].Root.String(), nil
}

func generateGo(w io.Writer, args []string, ts []template) error {
	src := new(bytes.Buffer)
	for _, t := range ts {
		fmt.Fprintf(src, "{{define %q}}", t.name)
		src.WriteString(t.content)
		src.WriteString("{{end}}")
	}

	// TODO(light): collect errors
	fmt.Fprintln(w, "// Code generated from templates directory. DO NOT EDIT.")
	fmt.Fprintln(w)
	fmt.Fprintln(w, "//go:generate", strings.Join(args, " "))
	fmt.Fprintln(w)
	fmt.Fprintln(w, "package main")
	fmt.Fprintln(w, "import (")
	fmt.Fprintln(w, "\t\"strings\"")
	fmt.Fprintln(w, "\t\"text/template\"")
	fmt.Fprintln(w, ")")
	fmt.Fprintln(w, "var templates = template.Must(template.New(\"\").Funcs(template.FuncMap{")
	fmt.Fprintln(w, "\t\"title\": strings.Title,")
	fmt.Fprintf(w, "}).Parse(\n\t%q))\n", src.Bytes())
	for _, t := range ts {
		if strings.HasPrefix(t.name, "_") {
			continue
		}
		fmt.Fprintf(w, "func render%s(r renderer, p %sParams) error {\n\treturn r.Render(%[2]q, p)\n}\n", strings.Title(t.name), t.name)
	}
	return nil
}

type template struct {
	name    string
	content string
}

func listdir(name string) ([]string, error) {
	f, err := os.Open(name)
	if err != nil {
		return nil, err
	}
	defer f.Close()
	names, err := f.Readdirnames(-1)
	if err != nil {
		return nil, err
	}
	n := 0
	for _, name := range names {
		if !strings.HasPrefix(name, ".") {
			names[n] = name
			n++
		}
	}
	names = names[:n]
	sort.Strings(names)
	return names, nil
}

func fatalln(args ...interface{}) {
	var buf bytes.Buffer
	buf.WriteString("mktemplates: ")
	fmt.Fprintln(&buf, args...)
	os.Stderr.Write(buf.Bytes())
	os.Exit(1)
}

func fatalf(format string, args ...interface{}) {
	var buf bytes.Buffer
	buf.WriteString("mktemplates: ")
	fmt.Fprintf(&buf, format, args...)
	if !bytes.HasSuffix(buf.Bytes(), []byte{'\n'}) {
		buf.Write([]byte{'\n'})
	}
	os.Stderr.Write(buf.Bytes())
	os.Exit(1)
}

var funcStubs = map[string]interface{}{
	// Built-ins
	"and":      variadicBoolStub,
	"call":     func(interface{}, ...interface{}) (interface{}, error) { return nil, nil },
	"eq":       func(arg0 interface{}, args ...interface{}) (bool, error) { return false, nil },
	"ge":       cmpStub,
	"gt":       cmpStub,
	"html":     escaperStub,
	"index":    func(interface{}, ...interface{}) (interface{}, error) { return nil, nil },
	"js":       escaperStub,
	"le":       cmpStub,
	"len":      func(interface{}) (int, error) { return 0, nil },
	"lt":       cmpStub,
	"ne":       cmpStub,
	"not":      func(interface{}) bool { return false },
	"or":       variadicBoolStub,
	"print":    fmt.Sprint,
	"printf":   fmt.Sprintf,
	"println":  fmt.Sprintln,
	"urlquery": escaperStub,

	// App-specific
	"title": strings.Title,
}

func variadicBoolStub(arg0 interface{}, args ...interface{}) interface{} {
	return arg0
}

func cmpStub(interface{}, interface{}) (bool, error) {
	return false, nil
}

func escaperStub(...interface{}) string {
	return ""
}

func importStub() string {
	return ""
}
