//go:build ignore

package main

import (
	"bytes"
	"fmt"
	"go/ast"
	"go/build"
	"go/doc"
	"go/format"
	"go/parser"
	"go/token"
	"io/fs"
	"os"
	"sort"
	"strings"
	"text/template"
	"unicode"

	"github.com/iancoleman/strcase"
)

type Param struct {
	Name      string
	Field     string
	Hidden    bool
	Type      string
	Constant  string
	Kebab     string
	Docstring string
}

func trim(s string) string {
	return strings.TrimFunc(s, unicode.IsSpace)
}

func normalizeComment(comment *ast.Comment) (lines []string) {
	text := comment.Text

	if text[1] == '/' {
		return []string{trim(text[2:])}
	}

	lines = strings.Split(text, "\n")
	for i, line := range lines {
		switch i {
		case 0:
			// Remove /*
			if len(line) == 2 {
				line = ""
				continue
			}
			line = line[2:]
		case len(lines) - 1:
			if len(line) == 2 {
				continue
			}
			// Remove */
			line = line[:len(line)-2]
		}

		lines[i] = trim(line)
	}
	return
}

const DEFAULTS_TEMPLATE = `// Code generated by gen.go; DO NOT EDIT.
package params

import (
    "fmt"

    "github.com/cfoust/cy/pkg/janet"
    "github.com/cfoust/cy/pkg/style"
)

const (
{{range .}}{{.Constant}} = "{{.Kebab}}"
{{end}}
)

{{range .}}
func (p *Parameters) {{.Name}}() {{.Type}} {
	value, ok := p.Get({{.Constant}})
	if !ok {
		return defaults.{{.Field}}
	}

	realValue, ok := value.({{.Type}})
	if !ok {
		return defaults.{{.Field}}
	}

	return realValue
}

func (p *Parameters) Set{{.Name}}(value {{.Type}}) {
	p.set({{.Constant}}, value)
}
{{end}}

func (p *Parameters) isDefault(key string) bool {
	switch key {
{{range .}}case {{.Constant}}:
		return true
{{end}}
	}
	return false
}

func (p *Parameters) getDefault(key string) (value interface{}, ok bool) {
	switch key {
{{range .}}case {{.Constant}}:
		return defaults.{{.Field}}, true
{{end}}
	}
	return nil, false
}

func (p *Parameters) setDefault(key string, value interface{}) error {
		janetValue, janetOk := value.(*janet.Value)
	switch key {
{{range .}}case {{.Constant}}:
		if !janetOk {
			realValue, ok := value.({{.Type}})
			if !ok {
			    return fmt.Errorf("invalid value for {{.Constant}}, should be {{.Type}}")
			}
			p.set(key, realValue)
			return nil
		}

{{if .Hidden}}
		return fmt.Errorf(":{{.Kebab}} is a protected parameter")
{{else}}
		var translated {{.Type}}
		err := janetValue.Unmarshal(&translated)
		if err != nil {
				janetValue.Free()
				return fmt.Errorf("invalid value for :{{.Kebab}}: %s", err)
		}
		p.set(key, translated)
		return nil
{{end}}
{{end}}
    }
    return nil
}

func init() {
	_defaultParams = []DefaultParam{
{{range .}}{{if not .Hidden}}		{
	Name: "{{.Kebab}}",
	Docstring: "{{.Docstring}}",
	Default: defaults.{{.Field}},
},
{{end}}{{end}}
	}
}

`

func getTypeName(expr ast.Expr) string {
	switch typeNode := expr.(type) {
	case *ast.StarExpr:
		return "*" + getTypeName(typeNode.X)
	case *ast.Ident:
		return typeNode.Name
	case *ast.ArrayType:
		return "[]" + getTypeName(typeNode.Elt)
	case *ast.SelectorExpr:
		return getTypeName(typeNode.X) + "." + getTypeName(typeNode.Sel)
	}

	panic("unsupported node: " + fmt.Sprintf("%+T", expr))
}

func main() {
	pkg, err := build.ImportDir(".", build.ImportComment)
	if err != nil {
		panic(err)
	}

	fset := token.NewFileSet()
	include := func(info fs.FileInfo) bool {
		return info.Name() == "defaults.go"
	}
	pkgs, err := parser.ParseDir(fset, pkg.Dir, include, parser.ParseComments)
	if err != nil {
		panic(err)
	}

	var params []Param

	docPkg := doc.New(pkgs["params"], pkg.ImportPath, doc.AllDecls)
	for _, typ := range docPkg.Types {
		if typ.Name != "defaultParams" {
			continue
		}

		for _, spec := range typ.Decl.Specs {
			typeSpec, ok := spec.(*ast.TypeSpec)
			if !ok {
				continue
			}

			structType, ok := typeSpec.Type.(*ast.StructType)
			if !ok {
				continue
			}

			for _, field := range structType.Fields.List {
				name := field.Names[0].Name
				type_ := getTypeName(field.Type)

				var docs []string
				if field.Doc != nil {
					for _, comment := range field.Doc.List {
						docs = append(docs, normalizeComment(comment)...)
					}
				}

				firstChar := rune(name[0])
				original := name
				hidden := unicode.IsLower(firstChar)
				if hidden {
					name = string(unicode.ToUpper(firstChar)) + name[1:]
				}

				kebab := strcase.ToKebab(name)
				if hidden {
					kebab = "---" + kebab
				}

				docstring := strings.Join(docs, "\\n")
				docstring = strings.ReplaceAll(
					docstring,
					`"`,
					`\"`,
				)

				params = append(params, Param{
					Name:      name,
					Field:     original,
					Hidden:    hidden,
					Kebab:     kebab,
					Type:      type_,
					Constant:  "Param" + name,
					Docstring: docstring,
				})
			}
		}
	}

	sort.SliceStable(params, func(i, j int) bool {
		return params[i].Name < params[j].Name
	})

	tmpl, err := template.
		New("defaults").
		Parse(DEFAULTS_TEMPLATE)
	if err != nil {
		panic(err)
	}

	output := bytes.Buffer{}
	err = tmpl.Execute(&output, params)
	if err != nil {
		panic(err)
	}

	formatted, err := format.Source(output.Bytes())
	if err != nil {
		panic(err)
	}

	f, err := os.Create("defaults_gen.go")
	if err != nil {
		panic(err)
	}
	f.Write(formatted)
}
