package binding

import (
	"bufio"
	"bytes"
	"fmt"
	"os"
	"path/filepath"
	"reflect"
	"runtime"
	"sort"
	"strings"

	"github.com/wailsapp/wails/v2/internal/typescriptify"

	"github.com/leaanthony/slicer"
	"github.com/wailsapp/wails/v2/internal/logger"
)

type Bindings struct {
	db         *DB
	logger     logger.CustomLogger
	exemptions slicer.StringSlicer

	structsToGenerateTS map[string]map[string]interface{}
	enumsToGenerateTS   map[string]map[string]interface{}
	tsPrefix            string
	tsSuffix            string
	tsInterface         bool
	obfuscate           bool
}

// NewBindings returns a new Bindings object
func NewBindings(logger *logger.Logger, structPointersToBind []interface{}, exemptions []interface{}, obfuscate bool, enumsToBind []interface{}) *Bindings {
	result := &Bindings{
		db:                  newDB(),
		logger:              logger.CustomLogger("Bindings"),
		structsToGenerateTS: make(map[string]map[string]interface{}),
		enumsToGenerateTS:   make(map[string]map[string]interface{}),
		obfuscate:           obfuscate,
	}

	for _, exemption := range exemptions {
		if exemption == nil {
			continue
		}
		name := runtime.FuncForPC(reflect.ValueOf(exemption).Pointer()).Name()
		// Yuk yuk yuk! Is there a better way?
		name = strings.TrimSuffix(name, "-fm")
		result.exemptions.Add(name)
	}

	for _, enum := range enumsToBind {
		result.AddEnumToGenerateTS(enum)
	}

	// Add the structs to bind
	for _, ptr := range structPointersToBind {
		err := result.Add(ptr)
		if err != nil {
			logger.Fatal("Error during binding: " + err.Error())
		}
	}

	return result
}

// Add the given struct methods to the Bindings
func (b *Bindings) Add(structPtr interface{}) error {
	methods, err := b.getMethods(structPtr)
	if err != nil {
		return fmt.Errorf("cannot bind value to app: %s", err.Error())
	}

	for _, method := range methods {
		splitName := strings.Split(method.Name, ".")
		packageName := splitName[0]
		structName := splitName[1]
		methodName := splitName[2]

		// Add it as a regular method
		b.db.AddMethod(packageName, structName, methodName, method)
	}
	return nil
}

func (b *Bindings) DB() *DB {
	return b.db
}

func (b *Bindings) ToJSON() (string, error) {
	return b.db.ToJSON()
}

func (b *Bindings) GenerateModels() ([]byte, error) {
	models := map[string]string{}
	var seen slicer.StringSlicer
	var seenEnumsPackages slicer.StringSlicer
	allStructNames := b.getAllStructNames()
	allStructNames.Sort()
	allEnumNames := b.getAllEnumNames()
	allEnumNames.Sort()
	for packageName, structsToGenerate := range b.structsToGenerateTS {
		thisPackageCode := ""
		w := typescriptify.New()
		w.WithPrefix(b.tsPrefix)
		w.WithSuffix(b.tsSuffix)
		w.WithInterface(b.tsInterface)
		w.Namespace = packageName
		w.WithBackupDir("")
		w.KnownStructs = allStructNames
		w.KnownEnums = allEnumNames
		// sort the structs
		var structNames []string
		for structName := range structsToGenerate {
			structNames = append(structNames, structName)
		}
		sort.Strings(structNames)
		for _, structName := range structNames {
			fqstructname := packageName + "." + structName
			if seen.Contains(fqstructname) {
				continue
			}
			structInterface := structsToGenerate[structName]
			w.Add(structInterface)
		}

		// if we have enums for this package, add them as well
		var enums, enumsExist = b.enumsToGenerateTS[packageName]
		if enumsExist {
			for enumName, enum := range enums {
				fqemumname := packageName + "." + enumName
				if seen.Contains(fqemumname) {
					continue
				}
				w.AddEnum(enum)
			}
			seenEnumsPackages.Add(packageName)
		}

		str, err := w.Convert(nil)
		if err != nil {
			return nil, err
		}
		thisPackageCode += str
		seen.AddSlice(w.GetGeneratedStructs())
		models[packageName] = thisPackageCode
	}

	// Add outstanding enums to the models that were not in packages with structs
	for packageName, enumsToGenerate := range b.enumsToGenerateTS {
		if seenEnumsPackages.Contains(packageName) {
			continue
		}

		thisPackageCode := ""
		w := typescriptify.New()
		w.WithPrefix(b.tsPrefix)
		w.WithSuffix(b.tsSuffix)
		w.WithInterface(b.tsInterface)
		w.Namespace = packageName
		w.WithBackupDir("")

		for enumName, enum := range enumsToGenerate {
			fqemumname := packageName + "." + enumName
			if seen.Contains(fqemumname) {
				continue
			}
			w.AddEnum(enum)
		}
		str, err := w.Convert(nil)
		if err != nil {
			return nil, err
		}
		thisPackageCode += str
		models[packageName] = thisPackageCode
	}

	// Sort the package names first to make the output deterministic
	sortedPackageNames := make([]string, 0)
	for packageName := range models {
		sortedPackageNames = append(sortedPackageNames, packageName)
	}
	sort.Strings(sortedPackageNames)

	var modelsData bytes.Buffer
	for _, packageName := range sortedPackageNames {
		modelData := models[packageName]
		if strings.TrimSpace(modelData) == "" {
			continue
		}
		modelsData.WriteString("export namespace " + packageName + " {\n")
		sc := bufio.NewScanner(strings.NewReader(modelData))
		for sc.Scan() {
			modelsData.WriteString("\t" + sc.Text() + "\n")
		}
		modelsData.WriteString("\n}\n\n")
	}
	return modelsData.Bytes(), nil
}

func (b *Bindings) WriteModels(modelsDir string) error {
	modelsData, err := b.GenerateModels()
	if err != nil {
		return err
	}
	// Don't write if we don't have anything
	if len(modelsData) == 0 {
		return nil
	}

	filename := filepath.Join(modelsDir, "models.ts")
	err = os.WriteFile(filename, modelsData, 0o755)
	if err != nil {
		return err
	}

	return nil
}

func (b *Bindings) AddEnumToGenerateTS(e interface{}) {
	enumType := reflect.TypeOf(e)

	var packageName string
	var enumName string
	// enums should be represented as array of all possible values
	if hasElements(enumType) {
		enum := enumType.Elem()
		// simple enum represented by struct with Value/TSName fields
		if enum.Kind() == reflect.Struct {
			_, tsNamePresented := enum.FieldByName("TSName")
			enumT, valuePresented := enum.FieldByName("Value")
			if tsNamePresented && valuePresented {
				packageName = getPackageName(enumT.Type.String())
				enumName = enumT.Type.Name()
			} else {
				return
			}
			// otherwise expecting implementation with TSName() https://github.com/tkrajina/typescriptify-golang-structs#enums-with-tsname
		} else {
			packageName = getPackageName(enumType.Elem().String())
			enumName = enumType.Elem().Name()
		}
		if b.enumsToGenerateTS[packageName] == nil {
			b.enumsToGenerateTS[packageName] = make(map[string]interface{})
		}
		if b.enumsToGenerateTS[packageName][enumName] != nil {
			return
		}
		b.enumsToGenerateTS[packageName][enumName] = e
	}
}

func (b *Bindings) AddStructToGenerateTS(packageName string, structName string, s interface{}) {
	if b.structsToGenerateTS[packageName] == nil {
		b.structsToGenerateTS[packageName] = make(map[string]interface{})
	}
	if b.structsToGenerateTS[packageName][structName] != nil {
		return
	}
	b.structsToGenerateTS[packageName][structName] = s

	// Iterate this struct and add any struct field references
	structType := reflect.TypeOf(s)
	for hasElements(structType) {
		structType = structType.Elem()
	}

	for i := 0; i < structType.NumField(); i++ {
		field := structType.Field(i)
		if field.Anonymous || !field.IsExported() {
			continue
		}
		kind := field.Type.Kind()
		if kind == reflect.Struct {
			fqname := field.Type.String()
			sNameSplit := strings.SplitN(fqname, ".", 2)
			if len(sNameSplit) < 2 {
				continue
			}
			sName := sNameSplit[1]
			pName := getPackageName(fqname)
			a := reflect.New(field.Type)
			if b.hasExportedJSONFields(field.Type) {
				s := reflect.Indirect(a).Interface()
				b.AddStructToGenerateTS(pName, sName, s)
			}
		} else {
			fType := field.Type
			for hasElements(fType) {
				fType = fType.Elem()
			}
			if fType.Kind() == reflect.Struct {
				fqname := fType.String()
				sNameSplit := strings.SplitN(fqname, ".", 2)
				if len(sNameSplit) < 2 {
					continue
				}
				sName := sNameSplit[1]
				pName := getPackageName(fqname)
				a := reflect.New(fType)
				if b.hasExportedJSONFields(fType) {
					s := reflect.Indirect(a).Interface()
					b.AddStructToGenerateTS(pName, sName, s)
				}
			}
		}
	}
}

func (b *Bindings) SetTsPrefix(prefix string) *Bindings {
	b.tsPrefix = prefix
	return b
}

func (b *Bindings) SetTsSuffix(postfix string) *Bindings {
	b.tsSuffix = postfix
	return b
}

func (b *Bindings) SetOutputType(outputType string) *Bindings {
	if outputType == "interfaces" {
		b.tsInterface = true
	}
	return b
}

func (b *Bindings) getAllStructNames() *slicer.StringSlicer {
	var result slicer.StringSlicer
	for packageName, structsToGenerate := range b.structsToGenerateTS {
		for structName := range structsToGenerate {
			result.Add(packageName + "." + structName)
		}
	}
	return &result
}

func (b *Bindings) getAllEnumNames() *slicer.StringSlicer {
	var result slicer.StringSlicer
	for packageName, enumsToGenerate := range b.enumsToGenerateTS {
		for enumName := range enumsToGenerate {
			result.Add(packageName + "." + enumName)
		}
	}
	return &result
}

func (b *Bindings) hasExportedJSONFields(typeOf reflect.Type) bool {
	for i := 0; i < typeOf.NumField(); i++ {
		jsonFieldName := ""
		f := typeOf.Field(i)
		// function, complex, and channel types cannot be json-encoded
		if f.Type.Kind() == reflect.Chan ||
			f.Type.Kind() == reflect.Func ||
			f.Type.Kind() == reflect.UnsafePointer ||
			f.Type.Kind() == reflect.Complex128 ||
			f.Type.Kind() == reflect.Complex64 {
			continue
		}
		jsonTag, hasTag := f.Tag.Lookup("json")
		if !hasTag && f.IsExported() {
			return true
		}
		if len(jsonTag) == 0 {
			continue
		}
		jsonTagParts := strings.Split(jsonTag, ",")
		if len(jsonTagParts) > 0 {
			jsonFieldName = jsonTagParts[0]
		}
		for _, t := range jsonTagParts {
			if t == "-" {
				continue
			}
		}
		if jsonFieldName != "" {
			return true
		}
	}
	return false
}
