package binding

import (
	"fmt"
	"reflect"
	"runtime"
	"strings"
)

// isStructPtr returns true if the value given is a
// pointer to a struct
func isStructPtr(value interface{}) bool {
	return reflect.ValueOf(value).Kind() == reflect.Ptr &&
		reflect.ValueOf(value).Elem().Kind() == reflect.Struct
}

// isFunction returns true if the given value is a function
func isFunction(value interface{}) bool {
	return reflect.ValueOf(value).Kind() == reflect.Func
}

// isStruct returns true if the value given is a struct
func isStruct(value interface{}) bool {
	return reflect.ValueOf(value).Kind() == reflect.Struct
}

func normalizeStructName(name string) string {
	return strings.ReplaceAll(
		strings.ReplaceAll(
			strings.ReplaceAll(
				strings.ReplaceAll(
					name,
					",",
					"-",
				),
				"*",
				"",
			),
			"]",
			"__",
		),
		"[",
		"__",
	)
}

func (b *Bindings) getMethods(value interface{}) ([]*BoundMethod, error) {
	// Create result placeholder
	var result []*BoundMethod

	// Check type
	if !isStructPtr(value) {

		if isStruct(value) {
			name := reflect.ValueOf(value).Type().Name()
			return nil, fmt.Errorf("%s is a struct, not a pointer to a struct", name)
		}

		if isFunction(value) {
			name := runtime.FuncForPC(reflect.ValueOf(value).Pointer()).Name()
			return nil, fmt.Errorf("%s is a function, not a pointer to a struct. Wails v2 has deprecated the binding of functions. Please wrap your functions up in a struct and bind a pointer to that struct.", name)
		}

		return nil, fmt.Errorf("not a pointer to a struct.")
	}

	// Process Struct
	structType := reflect.TypeOf(value)
	structValue := reflect.ValueOf(value)
	structName := structType.Elem().Name()
	structNameNormalized := normalizeStructName(structName)
	pkgPath := strings.TrimSuffix(structType.Elem().String(), fmt.Sprintf(".%s", structName))

	// Process Methods
	for i := 0; i < structType.NumMethod(); i++ {
		methodDef := structType.Method(i)
		methodName := methodDef.Name
		method := structValue.MethodByName(methodName)

		methodReflectName := runtime.FuncForPC(methodDef.Func.Pointer()).Name()
		if b.exemptions.Contains(methodReflectName) {
			continue
		}

		// Create new method
		boundMethod := &BoundMethod{
			Path: &BoundedMethodPath{
				Package: pkgPath,
				Struct:  structNameNormalized,
				Name:    methodName,
			},
			Inputs:   nil,
			Outputs:  nil,
			Comments: "",
			Method:   method,
		}

		// Iterate inputs
		methodType := method.Type()
		inputParamCount := methodType.NumIn()
		var inputs []*Parameter
		for inputIndex := 0; inputIndex < inputParamCount; inputIndex++ {
			input := methodType.In(inputIndex)
			thisParam := newParameter("", input)

			thisInput := input

			if thisInput.Kind() == reflect.Slice {
				thisInput = thisInput.Elem()
			}

			// Process struct pointer params
			if thisInput.Kind() == reflect.Ptr {
				if thisInput.Elem().Kind() == reflect.Struct {
					typ := thisInput.Elem()
					a := reflect.New(typ)
					s := reflect.Indirect(a).Interface()
					name := typ.Name()
					packageName := getPackageName(thisInput.String())
					b.AddStructToGenerateTS(packageName, name, s)
				}
			}

			// Process struct params
			if thisInput.Kind() == reflect.Struct {
				a := reflect.New(thisInput)
				s := reflect.Indirect(a).Interface()
				name := thisInput.Name()
				packageName := getPackageName(thisInput.String())
				b.AddStructToGenerateTS(packageName, name, s)
			}

			inputs = append(inputs, thisParam)
		}

		boundMethod.Inputs = inputs

		// Iterate outputs
		// TODO: Determine what to do about limiting return types
		//       especially around errors.
		outputParamCount := methodType.NumOut()
		var outputs []*Parameter
		for outputIndex := 0; outputIndex < outputParamCount; outputIndex++ {
			output := methodType.Out(outputIndex)
			thisParam := newParameter("", output)

			thisOutput := output

			if thisOutput.Kind() == reflect.Slice {
				thisOutput = thisOutput.Elem()
			}

			// Process struct pointer params
			if thisOutput.Kind() == reflect.Ptr {
				if thisOutput.Elem().Kind() == reflect.Struct {
					typ := thisOutput.Elem()
					a := reflect.New(typ)
					s := reflect.Indirect(a).Interface()
					name := typ.Name()
					packageName := getPackageName(thisOutput.String())
					b.AddStructToGenerateTS(packageName, name, s)
				}
			}

			// Process struct params
			if thisOutput.Kind() == reflect.Struct {
				a := reflect.New(thisOutput)
				s := reflect.Indirect(a).Interface()
				name := thisOutput.Name()
				packageName := getPackageName(thisOutput.String())
				b.AddStructToGenerateTS(packageName, name, s)
			}

			outputs = append(outputs, thisParam)
		}
		boundMethod.Outputs = outputs

		// Save method in result
		result = append(result, boundMethod)

	}
	return result, nil
}

func getPackageName(in string) string {
	result := strings.Split(in, ".")[0]
	result = strings.ReplaceAll(result, "[]", "")
	result = strings.ReplaceAll(result, "*", "")
	return result
}

func getSplitReturn(in string) (string, string) {
	result := strings.SplitN(in, ".", 2)
	return result[0], result[1]
}

func hasElements(typ reflect.Type) bool {
	kind := typ.Kind()
	return kind == reflect.Ptr || kind == reflect.Array || kind == reflect.Slice || kind == reflect.Map
}
