package bfcodegen

import (
	"errors"
	"fmt"
	"gogs.mikescher.com/BlackForestBytes/goext"
	"gogs.mikescher.com/BlackForestBytes/goext/cmdext"
	"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"
	"time"
)

type EnumDefVal struct {
	VarName     string
	Value       string
	Description *string
}

type EnumDef struct {
	File         string
	FileRelative string
	EnumTypeName string
	Type         string
	Values       []EnumDefVal
}

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

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

var rexValueDef = rext.W(regexp.MustCompile("^\\s*(?P<name>[A-Za-z0-9_]+)\\s+(?P<type>[A-Za-z0-9_]+)\\s*=\\s*(?P<value>(\"[A-Za-z0-9_:]+\"|[0-9]+))\\s*(//(?P<descr>.*))?.*$"))

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

func GenerateEnumSpecs(sourceDir string, destFile string) error {

	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 := rexChecksumConst.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") })
	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("[EnumGenerate] Checksum has changed ( %s -> %s ), will generate new file\n\n", oldChecksum, newChecksum)
	} else {
		fmt.Printf("[EnumGenerate] Checksum unchanged ( %s ), nothing to do\n", oldChecksum)
		return nil
	}

	allEnums := make([]EnumDef, 0)

	pkgname := ""

	for _, f := range files {
		fmt.Printf("========= %s =========\n\n", f.Name())
		fileEnums, pn, err := processFile(sourceDir, path.Join(sourceDir, f.Name()))
		if err != nil {
			return err
		}

		fmt.Printf("\n")

		allEnums = append(allEnums, fileEnums...)

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

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

	err = os.WriteFile(destFile, []byte(fmtOutput(newChecksum, allEnums, pkgname)), 0o755)
	if err != nil {
		return err
	}

	res, err := cmdext.RunCommand("go", []string{"fmt", destFile}, langext.Ptr(2*time.Second))
	if err != nil {
		return err
	}

	if res.CommandTimedOut {
		fmt.Println(res.StdCombined)
		return errors.New("go fmt timed out")
	}
	if res.ExitCode != 0 {
		fmt.Println(res.StdCombined)
		return errors.New("go fmt did not succeed")
	}

	return nil
}

func processFile(basedir string, fn string) ([]EnumDef, 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")

	enums := make([]EnumDef, 0)

	pkgname := ""

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

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

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

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

			def := EnumDef{
				File:         fn,
				FileRelative: rfp,
				EnumTypeName: match.GroupByName("name").Value(),
				Type:         match.GroupByName("type").Value(),
				Values:       make([]EnumDefVal, 0),
			}
			enums = append(enums, def)
			fmt.Printf("Found enum definition { '%s' -> '%s' }\n", def.EnumTypeName, def.Type)
		}

		if match, ok := rexValueDef.MatchFirst(line); ok {
			typename := match.GroupByName("type").Value()
			def := EnumDefVal{
				VarName:     match.GroupByName("name").Value(),
				Value:       match.GroupByName("value").Value(),
				Description: match.GroupByNameOrEmpty("descr").ValueOrNil(),
			}

			found := false
			for i, v := range enums {
				if v.EnumTypeName == typename {
					enums[i].Values = append(enums[i].Values, def)
					found = true
					if def.Description != nil {
						fmt.Printf("Found enum value [%s] for '%s'  ('%s')\n", def.Value, def.VarName, *def.Description)
					} else {
						fmt.Printf("Found enum value [%s] for '%s'\n", def.Value, def.VarName)
					}
					break
				}
			}
			if !found {
				fmt.Printf("Found non-enum value [%s] for '%s' ( looks like enum value, but no matching @enum:type )\n", def.Value, def.VarName)
			}
		}
	}

	return enums, pkgname, nil
}

func fmtOutput(cs string, enums []EnumDef, pkgname string) string {
	str := "// Code generated by enum-generate.go DO NOT EDIT.\n"
	str += "\n"
	str += "package " + pkgname + "\n"
	str += "\n"

	str += "import \"gogs.mikescher.com/BlackForestBytes/goext/langext\"" + "\n"
	str += "\n"

	str += "const ChecksumGenerator = \"" + cs + "\"" + "\n"
	str += "\n"

	str += "type Enum interface {" + "\n"
	str += "    Valid() bool" + "\n"
	str += "    ValuesAny() []any" + "\n"
	str += "    ValuesMeta() []EnumMetaValue" + "\n"
	str += "    VarName() string" + "\n"
	str += "}" + "\n"
	str += "" + "\n"

	str += "type StringEnum interface {" + "\n"
	str += "    Enum" + "\n"
	str += "    String() string" + "\n"
	str += "}" + "\n"
	str += "" + "\n"

	str += "type DescriptionEnum interface {" + "\n"
	str += "    Enum" + "\n"
	str += "    Description() string" + "\n"
	str += "}" + "\n"
	str += "\n"

	str += "type EnumMetaValue struct {" + "\n"
	str += "    VarName     string  `json:\"varName\"`" + "\n"
	str += "    Value       any     `json:\"value\"`" + "\n"
	str += "    Description *string `json:\"description\"`" + "\n"
	str += "}" + "\n"
	str += "\n"

	for _, enumdef := range enums {

		hasDescr := langext.ArrAll(enumdef.Values, func(val EnumDefVal) bool { return val.Description != nil })
		hasStr := enumdef.Type == "string"

		str += "// ================================ " + enumdef.EnumTypeName + " ================================" + "\n"
		str += "//" + "\n"
		str += "// File:       " + enumdef.FileRelative + "\n"
		str += "// StringEnum: " + langext.Conditional(hasStr, "true", "false") + "\n"
		str += "// DescrEnum:  " + langext.Conditional(hasDescr, "true", "false") + "\n"
		str += "//" + "\n"
		str += "" + "\n"

		str += "var __" + enumdef.EnumTypeName + "Values = []" + enumdef.EnumTypeName + "{" + "\n"
		for _, v := range enumdef.Values {
			str += "    " + v.VarName + "," + "\n"
		}
		str += "}" + "\n"
		str += "" + "\n"

		if hasDescr {
			str += "var __" + enumdef.EnumTypeName + "Descriptions = map[" + enumdef.EnumTypeName + "]string{" + "\n"
			for _, v := range enumdef.Values {
				str += "    " + v.VarName + ": \"" + strings.TrimSpace(*v.Description) + "\"," + "\n"
			}
			str += "}" + "\n"
			str += "" + "\n"
		}

		str += "var __" + enumdef.EnumTypeName + "Varnames = map[" + enumdef.EnumTypeName + "]string{" + "\n"
		for _, v := range enumdef.Values {
			str += "    " + v.VarName + ": \"" + v.VarName + "\"," + "\n"
		}
		str += "}" + "\n"
		str += "" + "\n"

		str += "func (e " + enumdef.EnumTypeName + ") Valid() bool {" + "\n"
		str += "    return langext.InArray(e, __" + enumdef.EnumTypeName + "Values)" + "\n"
		str += "}" + "\n"
		str += "" + "\n"

		str += "func (e " + enumdef.EnumTypeName + ") Values() []" + enumdef.EnumTypeName + " {" + "\n"
		str += "    return __" + enumdef.EnumTypeName + "Values" + "\n"
		str += "}" + "\n"
		str += "" + "\n"

		str += "func (e " + enumdef.EnumTypeName + ") ValuesAny() []any {" + "\n"
		str += "    return langext.ArrCastToAny(__" + enumdef.EnumTypeName + "Values)" + "\n"
		str += "}" + "\n"
		str += "" + "\n"

		str += "func (e " + enumdef.EnumTypeName + ") ValuesMeta() []EnumMetaValue {" + "\n"
		str += "    return []EnumMetaValue{" + "\n"
		for _, v := range enumdef.Values {
			if hasDescr {
				str += "        " + fmt.Sprintf("EnumMetaValue{VarName: \"%s\", Value: %s, Description: langext.Ptr(\"%s\")},", v.VarName, v.VarName, strings.TrimSpace(*v.Description)) + "\n"
			} else {
				str += "        " + fmt.Sprintf("EnumMetaValue{VarName: \"%s\", Value: %s, Description: nil},", v.VarName, v.VarName) + "\n"
			}
		}
		str += "    }" + "\n"
		str += "}" + "\n"
		str += "" + "\n"

		if hasStr {
			str += "func (e " + enumdef.EnumTypeName + ") String() string {" + "\n"
			str += "    return string(e)" + "\n"
			str += "}" + "\n"
			str += "" + "\n"
		}

		if hasDescr {
			str += "func (e " + enumdef.EnumTypeName + ") Description() string {" + "\n"
			str += "    if d, ok := __" + enumdef.EnumTypeName + "Descriptions[e]; ok {" + "\n"
			str += "        return d" + "\n"
			str += "    }" + "\n"
			str += "    return \"\"" + "\n"
			str += "}" + "\n"
			str += "" + "\n"
		}

		str += "func (e " + enumdef.EnumTypeName + ") VarName() string {" + "\n"
		str += "    if d, ok := __" + enumdef.EnumTypeName + "Varnames[e]; ok {" + "\n"
		str += "        return d" + "\n"
		str += "    }" + "\n"
		str += "    return \"\"" + "\n"
		str += "}" + "\n"
		str += "" + "\n"

		str += "func Parse" + enumdef.EnumTypeName + "(vv string) (" + enumdef.EnumTypeName + ", bool) {" + "\n"
		str += "    for _, ev := range __" + enumdef.EnumTypeName + "Values {" + "\n"
		str += "        if string(ev) == vv {" + "\n"
		str += "            return ev, true" + "\n"
		str += "        }" + "\n"
		str += "    }" + "\n"
		str += "    return \"\", false" + "\n"
		str += "}" + "\n"
		str += "" + "\n"

		str += "func " + enumdef.EnumTypeName + "Values() []" + enumdef.EnumTypeName + " {" + "\n"
		str += "    return __" + enumdef.EnumTypeName + "Values" + "\n"
		str += "}" + "\n"
		str += "" + "\n"

		str += "func " + enumdef.EnumTypeName + "ValuesMeta() []EnumMetaValue {" + "\n"
		str += "    return []EnumMetaValue{" + "\n"
		for _, v := range enumdef.Values {
			if hasDescr {
				str += "        " + fmt.Sprintf("EnumMetaValue{VarName: \"%s\", Value: %s, Description: langext.Ptr(\"%s\")},", v.VarName, v.VarName, strings.TrimSpace(*v.Description)) + "\n"
			} else {
				str += "        " + fmt.Sprintf("EnumMetaValue{VarName: \"%s\", Value: %s, Description: nil},", v.VarName, v.VarName) + "\n"
			}
		}
		str += "    }" + "\n"
		str += "}" + "\n"
		str += "" + "\n"

	}

	return str
}
