package bfcodegen

import (
	"bytes"
	_ "embed"
	"errors"
	"fmt"
	"go/format"
	"gogs.mikescher.com/BlackForestBytes/goext"
	"gogs.mikescher.com/BlackForestBytes/goext/cryptext"
	"gogs.mikescher.com/BlackForestBytes/goext/langext"
	"gogs.mikescher.com/BlackForestBytes/goext/rext"
	"io"
	"os"
	"path"
	"path/filepath"
	"regexp"
	"strings"
	"text/template"
)

type IDDef struct {
	File         string
	FileRelative string
	Name         string
}

type IDGenOptions struct {
	DebugOutput *bool
}

var rexIDPackage = rext.W(regexp.MustCompile(`^package\s+(?P<name>[A-Za-z0-9_]+)\s*$`))

var rexIDDef = rext.W(regexp.MustCompile(`^\s*type\s+(?P<name>[A-Za-z0-9_]+)\s+string\s*//\s*(@id:type).*$`))

var rexIDChecksumConst = rext.W(regexp.MustCompile(`const ChecksumIDGenerator = "(?P<cs>[A-Za-z0-9_]*)"`))

//go:embed id-generate.template
var templateIDGenerateText string

func GenerateIDSpecs(sourceDir string, destFile string, opt IDGenOptions) error {

	debugOutput := langext.Coalesce(opt.DebugOutput, false)

	files, err := os.ReadDir(sourceDir)
	if err != nil {
		return err
	}

	oldChecksum := "N/A"
	if _, err := os.Stat(destFile); !os.IsNotExist(err) {
		content, err := os.ReadFile(destFile)
		if err != nil {
			return err
		}
		if m, ok := rexIDChecksumConst.MatchFirst(string(content)); ok {
			oldChecksum = m.GroupByName("cs").Value()
		}
	}

	files = langext.ArrFilter(files, func(v os.DirEntry) bool { return v.Name() != path.Base(destFile) })
	files = langext.ArrFilter(files, func(v os.DirEntry) bool { return strings.HasSuffix(v.Name(), ".go") })
	files = langext.ArrFilter(files, func(v os.DirEntry) bool { return !strings.HasSuffix(v.Name(), "_gen.go") })
	langext.SortBy(files, func(v os.DirEntry) string { return v.Name() })

	newChecksumStr := goext.GoextVersion
	for _, f := range files {
		content, err := os.ReadFile(path.Join(sourceDir, f.Name()))
		if err != nil {
			return err
		}
		newChecksumStr += "\n" + f.Name() + "\t" + cryptext.BytesSha256(content)
	}

	newChecksum := cryptext.BytesSha256([]byte(newChecksumStr))

	if newChecksum != oldChecksum {
		fmt.Printf("[IDGenerate] Checksum has changed ( %s -> %s ), will generate new file\n\n", oldChecksum, newChecksum)
	} else {
		fmt.Printf("[IDGenerate] Checksum unchanged ( %s ), nothing to do\n", oldChecksum)
		return nil
	}

	allIDs := make([]IDDef, 0)

	pkgname := ""

	for _, f := range files {
		if debugOutput {
			fmt.Printf("========= %s =========\n\n", f.Name())
		}

		fileIDs, pn, err := processIDFile(sourceDir, path.Join(sourceDir, f.Name()), debugOutput)
		if err != nil {
			return err
		}

		if debugOutput {
			fmt.Printf("\n")
		}

		allIDs = append(allIDs, fileIDs...)

		if pn != "" {
			pkgname = pn
		}
	}

	if pkgname == "" {
		return errors.New("no package name found in any file")
	}

	fdata, err := format.Source([]byte(fmtIDOutput(newChecksum, allIDs, pkgname)))
	if err != nil {
		return err
	}

	err = os.WriteFile(destFile, fdata, 0o755)
	if err != nil {
		return err
	}

	return nil
}

func processIDFile(basedir string, fn string, debugOutput bool) ([]IDDef, string, error) {
	file, err := os.Open(fn)
	if err != nil {
		return nil, "", err
	}

	defer func() { _ = file.Close() }()

	bin, err := io.ReadAll(file)
	if err != nil {
		return nil, "", err
	}

	lines := strings.Split(string(bin), "\n")

	ids := make([]IDDef, 0)

	pkgname := ""

	for i, line := range lines {
		if i == 0 && strings.HasPrefix(line, "// Code generated by") {
			break
		}

		if match, ok := rexIDPackage.MatchFirst(line); i == 0 && ok {
			pkgname = match.GroupByName("name").Value()
			continue
		}

		if match, ok := rexIDDef.MatchFirst(line); ok {

			rfp, err := filepath.Rel(basedir, fn)
			if err != nil {
				return nil, "", err
			}

			def := IDDef{
				File:         fn,
				FileRelative: rfp,
				Name:         match.GroupByName("name").Value(),
			}

			if debugOutput {
				fmt.Printf("Found ID definition { '%s' }\n", def.Name)
			}

			ids = append(ids, def)
		}
	}

	return ids, pkgname, nil
}

func fmtIDOutput(cs string, ids []IDDef, pkgname string) string {
	templ := template.Must(template.New("id-generate").Parse(templateIDGenerateText))

	buffer := bytes.Buffer{}

	anyDef := langext.ArrFirstOrNil(ids, func(def IDDef) bool { return def.Name == "AnyID" || def.Name == "AnyId" })

	err := templ.Execute(&buffer, langext.H{
		"PkgName":      pkgname,
		"Checksum":     cs,
		"GoextVersion": goext.GoextVersion,
		"IDs":          ids,
		"AnyDef":       anyDef,
	})
	if err != nil {
		panic(err)
	}

	return buffer.String()
}