package codegen

import (
	"embed"
	"strings"
	"text/template"

	"git.savin.nyc/alex/go-winrt/internal/winmd"
)

type genDataFile struct {
	Filename string
	Data     genData
}

type genData struct {
	Package    string
	Imports    []string
	Classes    []*genClass
	Enums      []*genEnum
	Interfaces []*genInterface
	Structs    []*genStruct
	Delegates  []*genDelegate
}

func (g *genData) ComputeImports(typeDef *winmd.TypeDef) {
	// gather all imports
	imports := make([]*genImport, 0)
	if g.Classes != nil {
		for _, c := range g.Classes {
			imports = append(imports, c.GetRequiredImports()...)
		}
	}
	if g.Interfaces != nil {
		for _, i := range g.Interfaces {
			imports = append(imports, i.GetRequiredImports()...)
		}
	}

	for _, i := range imports {
		if typeDef.TypeNamespace != i.Namespace {
			g.Imports = append(g.Imports, i.ToGoImport())
		}
	}
}

type genInterface struct {
	Name      string
	GUID      string
	Signature string
	Funcs     []*genFunc
}

func (g *genInterface) GetRequiredImports() []*genImport {
	imports := make([]*genImport, 0)
	for _, f := range g.Funcs {
		imports = append(imports, f.RequiresImports...)
	}
	return imports
}

type genClass struct {
	Name                string
	Signature           string
	RequiresImports     []*genImport
	FullyQualifiedName  string
	ImplInterfaces      []*genInterface
	ExclusiveInterfaces []*genInterface
	HasEmptyConstructor bool
	IsAbstract          bool
}

func (g *genClass) GetRequiredImports() []*genImport {
	imports := make([]*genImport, 0)
	if g.RequiresImports != nil {
		imports = append(imports, g.RequiresImports...)
	}
	if g.ExclusiveInterfaces != nil {
		for _, i := range g.ExclusiveInterfaces {
			imports = append(imports, i.GetRequiredImports()...)
		}
	}

	return imports
}

type genDelegate struct {
	Name        string
	GUID        string
	Signature   string
	InParams    []*genParam
	ReturnParam *genParam // this may be nil
}

type genEnum struct {
	Name      string
	Type      string
	Signature string
	Values    []*genEnumValue
}
type genEnumValue struct {
	Name  string
	Value string
}

type genFunc struct {
	Name            string
	RequiresImports []*genImport
	Implement       bool
	FuncOwner       string
	InParams        []*genParam
	ReturnParams    []*genParam // this may be empty

	// ExclusiveTo is the name of the class that this function is exclusive to.
	// The funcion will be called statically using the RoGetActivationFactory function.
	ExclusiveTo        string
	RequiresActivation bool

	InheritedFrom winmd.QualifiedID
}

type genImport struct {
	Namespace, Name string
}

func (i genImport) ToGoImport() string {
	if !strings.Contains(i.Namespace, ".") && i.Namespace != "Windows" {
		// This is probably a built-in package
		return i.Namespace
	}

	folder := typeToFolder(i.Namespace, i.Name)
	return "git.savin.nyc/alex/go-winrt/" + folder
}

// some of the variables are not public to avoid using them
// by mistake in the code.
type genDefaultValue struct {
	value       string
	isPrimitive bool
}

// some of the variables are not public to avoid using them
// by mistake in the code.
type genParamType struct {
	namespace string
	name      string

	IsPointer          bool
	IsGeneric          bool
	IsArray            bool
	IsPrimitive        bool
	IsEnum             bool
	UnderlyingEnumType string

	defaultValue genDefaultValue
}

// some of the variables are not public to avoid using them
// by mistake in the code.
type genParam struct {
	callerPackage string

	varName string

	Type *genParamType

	IsOut bool
}

func (g *genParam) GoVarName() string {
	return typeNameToGoName(g.varName, true) // assume all are public
}

func (g *genParam) GoTypeName() string {
	if g.Type.IsPrimitive {
		return g.Type.name
	}

	name := typeNameToGoName(g.Type.name, true) // assume all are public

	pkg := typePackage(g.Type.namespace, g.Type.name)
	if g.callerPackage != pkg {
		name = pkg + "." + name
	}

	return name
}

func (g *genParam) GoDefaultValue() string {
	if g.Type.defaultValue.isPrimitive {
		return g.Type.defaultValue.value
	}

	pkg := typePackage(g.Type.namespace, g.Type.name)
	if g.callerPackage != pkg {
		return pkg + "." + g.Type.defaultValue.value
	}

	return g.Type.defaultValue.value
}

type genStruct struct {
	Name      string
	Signature string
	Fields    []*genParam
}

//go:embed templates/*
var templatesFS embed.FS

func getTemplates() (*template.Template, error) {
	return template.New("").
		Funcs(funcs()).
		ParseFS(templatesFS, "templates/*")
}

func funcs() template.FuncMap {
	return template.FuncMap{
		"funcName": funcName,
		"concat": func(a, b []*genParam) []*genParam {
			return append(a, b...)
		},
		"toLower": func(s string) string {
			return strings.ToLower(s[:1]) + s[1:]
		},
	}
}

// funcName is used to generate the name of a function.
func funcName(m genFunc) string {
	// There are some special prefixes applied to methods that we need to replace
	replacer := strings.NewReplacer(
		"get_", "Get",
		"put_", "Set",
		"add_", "Add",
		"remove_", "Remove",
	)
	name := replacer.Replace(m.Name)

	// Add a prefix to static methods to include the owner class of the method.
	// This is necessary to avoid conflicts with method names within the same package.
	// Static methods are those that are exclusive to a class and require activation.
	prefix := ""
	if m.ExclusiveTo != "" && m.RequiresActivation {
		nsAndName := strings.Split(m.ExclusiveTo, ".")
		prefix = typeNameToGoName(nsAndName[len(nsAndName)-1], true)
	}

	return prefix + name
}

func typeToFolder(ns, name string) string {
	fullName := ns
	return strings.ToLower(strings.Replace(fullName, ".", "/", -1))
}

func typePackage(ns, name string) string {
	sns := strings.Split(ns, ".")
	return strings.ToLower(sns[len(sns)-1])
}

func enumName(typeName string, enumName string) string {
	return typeName + enumName
}

func typeDefGoName(typeName string, public bool) string {
	name := typeName

	if isParameterizedName(typeName) {
		name = strings.Split(name, "`")[0]
	}

	if !public {
		name = strings.ToLower(name[0:1]) + name[1:]
	}
	return name
}

func isParameterizedName(typeName string) bool {
	// parameterized types contain a '`' followed by the amount of generic parameters in their name.
	return strings.Contains(typeName, "`")
}

func typeFilename(typeName string) string {
	// public boolean is not relevant, we are going to lower everything
	goname := typeDefGoName(typeName, true)
	return strings.ToLower(goname)
}

// removes Go reserved words from param names
func cleanReservedWords(name string) string {
	switch name {
	case "type":
		return "mType"
	}
	return name
}

func typeNameToGoName(typeName string, public bool) string {
	name := typeName

	if isParameterizedName(typeName) {
		name = strings.Split(name, "`")[0]
	}

	if !public {
		name = strings.ToLower(name[0:1]) + name[1:]
	}
	return name
}
