package main

import (
	"bytes"
	"flag"
	"fmt"
	"go/format"
	"os"
	"path/filepath"
	"slices"
	"strings"
	"text/template"
)

var tmpl = template.Must(template.New("").Funcs(template.FuncMap{ //nolint:gochecknoglobals
	"base": filepath.Base,
	"camel": func(filename string) string {
		parts := strings.FieldsFunc(filename, func(r rune) bool {
			return r == '-' || r == '_' || r == ' ' || r == '.' // split by common delimiters
		})

		var sb strings.Builder

		for _, p := range parts {
			if len(p) > 0 {
				sb.WriteString(strings.ToUpper(p[:1]) + p[1:])
			}
		}

		return sb.String()
	},
	"noExt": func(filename string) string {
		if before, _, ok := strings.Cut(filename, "."); ok {
			return before
		}

		return filename
	},
}).Parse(`// Code generated by go:generate. DO NOT EDIT.

package templates

import _ "embed"

// Template content is loaded from the corresponding HTML files at compile time via go:embed.
var (
{{- range .FileNames }}
	//go:embed {{ . }}
	HTMLTemplate{{ noExt . | base | camel }} string
{{ end }}
)

// HTMLTemplateName* constants hold the canonical name of each built-in HTML template (filename without extension).
const (
{{- range .FileNames }}
	HTMLTemplateName{{ noExt . | base | camel }} = "{{ noExt . | base }}"
{{- end }}
)

// BuiltInHTML returns a new map of all built-in HTML templates keyed by their canonical name.
// The map itself is freshly allocated on each call, so adding or removing keys is safe.
func BuiltInHTML() map[string]string {
	return map[string]string{
		{{- range .FileNames }}
		HTMLTemplateName{{ noExt . | base | camel }}: HTMLTemplate{{ noExt . | base | camel }},
		{{- end }}
	}
}
`))

func main() {
	var outFile, srcDir string

	flag.StringVar(&outFile, "out", "./embed_html.go", "output file path")
	flag.StringVar(&srcDir, "src", ".", "directory to scan for HTML templates")
	flag.Parse()

	files, rErr := os.ReadDir(srcDir)
	exitIfErr(rErr, "read source directory")

	fileNames := make([]string, 0, len(files))

	for _, file := range files {
		if name := file.Name(); file.IsDir() ||
			(!strings.HasSuffix(name, ".html") && !strings.HasSuffix(name, ".htm")) ||
			strings.HasPrefix(name, ".") {
			continue // skip non-HTML files, hidden files, and directories
		}

		if info, infoErr := file.Info(); infoErr != nil {
			exitIfErr(infoErr, "stat file")
		} else if info.Size() == 0 {
			continue // skip empty files
		}

		fileNames = append(fileNames, filepath.Join(srcDir, file.Name()))
	}

	if len(fileNames) == 0 {
		exitIfErr(fmt.Errorf("no HTML files found in %q", srcDir))
	}

	slices.Sort(fileNames) // sort file names for deterministic output

	var buf bytes.Buffer
	exitIfErr(tmpl.Execute(&buf, struct{ FileNames []string }{fileNames}), "execute template")

	src, err := format.Source(buf.Bytes())
	if err != nil {
		_, _ = fmt.Fprintln(os.Stderr, buf.String()) // print unformatted source for debugging

		exitIfErr(err, "format generated source")
	}

	exitIfErr(os.WriteFile(outFile, src, 0o644), "write output file") //nolint:mnd,gosec
}

// exitIfErr prints the error message to stderr and exits with status code 1 if err is not nil.
func exitIfErr(err error, act ...string) {
	if err != nil {
		if len(act) > 0 {
			_, _ = fmt.Fprintf(os.Stderr, "Error: %s: %v\n", act[0], err)
		} else {
			_, _ = fmt.Fprintln(os.Stderr, "Error:", err)
		}

		os.Exit(1)
	}
}
