package hero

import (
	"net"
	"reflect"

	"github.com/kataras/iris/v12/context"
)

func valueOf(v interface{}) reflect.Value {
	if val, ok := v.(reflect.Value); ok {
		// check if it's already a reflect.Value.
		return val
	}

	return reflect.ValueOf(v)
}

// indirectType returns the value of a pointer-type "typ".
// If "typ" is a pointer, array, chan, map or slice it returns its Elem,
// otherwise returns the "typ" as it is.
func indirectType(typ reflect.Type) reflect.Type {
	switch typ.Kind() {
	case reflect.Ptr, reflect.Array, reflect.Chan, reflect.Map, reflect.Slice:
		return typ.Elem()
	}
	return typ
}

func goodVal(v reflect.Value) bool {
	switch v.Kind() {
	case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.Interface, reflect.Slice:
		if v.IsNil() {
			return false
		}
	}

	return v.IsValid()
}

func isFunc(kindable interface{ Kind() reflect.Kind }) bool {
	return kindable.Kind() == reflect.Func
}

var (
	inputTyp = reflect.TypeOf((*Input)(nil))
	errTyp   = reflect.TypeOf((*error)(nil)).Elem()

	ipTyp = reflect.TypeOf(net.IP{})
)

// isError returns true if "typ" is type of `error`.
func isError(typ reflect.Type) bool {
	return typ.Implements(errTyp)
}

func toError(v reflect.Value) error {
	if v.IsNil() {
		return nil
	}

	return v.Interface().(error)
}

var contextType = reflect.TypeOf((*context.Context)(nil))

// isContext returns true if the "typ" is a type of Context.
func isContext(typ reflect.Type) bool {
	return typ == contextType
}

var errorHandlerTyp = reflect.TypeOf((*ErrorHandler)(nil)).Elem()

func isErrorHandler(typ reflect.Type) bool {
	return typ.Implements(errorHandlerTyp)
}

var emptyValue reflect.Value

func equalTypes(binding reflect.Type, input reflect.Type) bool {
	if binding == input {
		return true
	}

	// fmt.Printf("got: %s expected: %s\n", got.String(), expected.String())
	// if accepts an interface, check if the given "got" type does
	// implement this "expected" user handler's input argument.
	if input.Kind() == reflect.Interface {
		// fmt.Printf("expected interface = %s and got to set on the arg is: %s\n", binding.String(), input.String())
		// return input.Implements(binding)
		return binding.AssignableTo(input)
	}

	// dependency: func(...) interface{} { return "string" }
	// expected input: string.
	if binding.Kind() == reflect.Interface {
		return input.AssignableTo(binding)
	}

	return false
}

func structFieldIgnored(f reflect.StructField) bool {
	if !f.Anonymous {
		return true // if not anonymous(embedded), ignore it.
	}

	if s := f.Tag.Get("ignore"); s == "true" {
		return true
	}

	if s := f.Tag.Get("stateless"); s == "true" {
		return true
	}

	return false
}

// all except non-zero.
func lookupFields(elem reflect.Value, skipUnexported bool, onlyZeros bool, parentIndex []int) (fields []reflect.StructField, stateless int) {
	// Note: embedded pointers are not supported.
	// elem = reflect.Indirect(elem)
	elemTyp := elem.Type()
	for i, n := 0, elem.NumField(); i < n; i++ {
		field := elemTyp.Field(i)
		fieldValue := elem.Field(i)

		// embed any fields from other structs.
		if indirectType(field.Type).Kind() == reflect.Struct {
			if structFieldIgnored(field) {
				stateless++ // don't skip the loop yet, e.g. iris.Context.
			} else {
				structFields, statelessN := lookupFields(fieldValue, skipUnexported, onlyZeros, append(parentIndex, i))
				stateless += statelessN
				fields = append(fields, structFields...)
				continue
			}
		}

		if onlyZeros && !isZero(fieldValue) {
			continue
		}

		// skip unexported fields here.
		if isExported := field.PkgPath == ""; skipUnexported && !isExported {
			continue
		}

		index := []int{i}
		if len(parentIndex) > 0 {
			index = append(parentIndex, i)
		}

		tmp := make([]int, len(index))
		copy(tmp, index)
		field.Index = tmp

		fields = append(fields, field)
	}

	return
}

func lookupNonZeroFieldValues(elem reflect.Value) (nonZeroFields []reflect.StructField) {
	fields, _ := lookupFields(elem, true, false, nil)
	for _, f := range fields {
		if fieldVal := elem.FieldByIndex(f.Index); goodVal(fieldVal) && !isZero(fieldVal) {
			/* && f.Type.Kind() == reflect.Ptr &&*/
			nonZeroFields = append(nonZeroFields, f)
		}
	}

	return
}

// isZero returns true if a value is nil.
// Remember; fields to be checked should be exported otherwise it returns false.
// Notes for users:
// Boolean's zero value is false, even if not set-ed.
// UintXX are not zero on 0 because they are pointers to.
func isZero(v reflect.Value) bool {
	// switch v.Kind() {
	// case reflect.Struct:
	// 	zero := true
	// 	for i := 0; i < v.NumField(); i++ {
	// 		f := v.Field(i)
	// 		if f.Type().PkgPath() != "" {
	// 			continue // unexported.
	// 		}
	// 		zero = zero && isZero(f)
	// 	}

	// 	if typ := v.Type(); typ != nil && v.IsValid() {
	// 		f, ok := typ.MethodByName("IsZero")
	// 		// if not found
	// 		// if has input arguments (1 is for the value receiver, so > 1 for the actual input args)
	// 		// if output argument is not boolean
	// 		// then skip this IsZero user-defined function.
	// 		if !ok || f.Type.NumIn() > 1 || f.Type.NumOut() != 1 && f.Type.Out(0).Kind() != reflect.Bool {
	// 			return zero
	// 		}

	// 		method := v.Method(f.Index)
	// 		// no needed check but:
	// 		if method.IsValid() && !method.IsNil() {
	// 			// it shouldn't panic here.
	// 			zero = method.Call([]reflect.Value{})[0].Interface().(bool)
	// 		}
	// 	}

	// 	return zero
	// case reflect.Func, reflect.Map, reflect.Slice:
	// 	return v.IsNil()
	// case reflect.Array:
	// 	zero := true
	// 	for i := 0; i < v.Len(); i++ {
	// 		zero = zero && isZero(v.Index(i))
	// 	}
	// 	return zero
	// }
	// if not any special type then use the reflect's .Zero
	// usually for fields, but remember if it's boolean and it's false
	// then it's zero, even if set-ed.

	if !v.CanInterface() {
		// if can't interface, i.e return value from unexported field or method then return false
		return false
	}

	if v.Type() == ipTyp {
		return len(v.Interface().(net.IP)) == 0
	}

	// zero := reflect.Zero(v.Type())
	// return v.Interface() == zero.Interface()

	return v.IsZero()
}

// IsNil same as `reflect.IsNil` but a bit safer to use, returns false if not a correct type.
func isNil(v reflect.Value) bool {
	k := v.Kind()
	switch k {
	case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.UnsafePointer, reflect.Interface, reflect.Slice:
		return v.IsNil()
	default:
		return false
	}
}
