package reflectext

import (
	"reflect"
)

var reflectBasicTypes = map[reflect.Kind]reflect.Type{
	reflect.Bool:       reflect.TypeOf(false),
	reflect.Int:        reflect.TypeOf(int(0)),
	reflect.Int8:       reflect.TypeOf(int8(0)),
	reflect.Int16:      reflect.TypeOf(int16(0)),
	reflect.Int32:      reflect.TypeOf(int32(0)),
	reflect.Int64:      reflect.TypeOf(int64(0)),
	reflect.Uint:       reflect.TypeOf(uint(0)),
	reflect.Uint8:      reflect.TypeOf(uint8(0)),
	reflect.Uint16:     reflect.TypeOf(uint16(0)),
	reflect.Uint32:     reflect.TypeOf(uint32(0)),
	reflect.Uint64:     reflect.TypeOf(uint64(0)),
	reflect.Uintptr:    reflect.TypeOf(uintptr(0)),
	reflect.Float32:    reflect.TypeOf(float32(0)),
	reflect.Float64:    reflect.TypeOf(float64(0)),
	reflect.Complex64:  reflect.TypeOf(complex64(0)),
	reflect.Complex128: reflect.TypeOf(complex128(0)),
	reflect.String:     reflect.TypeOf(""),
}

// Underlying returns the underlying type of t (without type alias)
//
// https://github.com/golang/go/issues/39574#issuecomment-655664772
func Underlying(t reflect.Type) (ret reflect.Type) {
	if t.Name() == "" {
		// t is an unnamed type. the underlying type is t itself
		return t
	}
	kind := t.Kind()
	if ret = reflectBasicTypes[kind]; ret != nil {
		return ret
	}
	switch kind {
	case reflect.Array:
		ret = reflect.ArrayOf(t.Len(), t.Elem())
	case reflect.Chan:
		ret = reflect.ChanOf(t.ChanDir(), t.Elem())
	case reflect.Map:
		ret = reflect.MapOf(t.Key(), t.Elem())
	case reflect.Func:
		nIn := t.NumIn()
		nOut := t.NumOut()
		in := make([]reflect.Type, nIn)
		out := make([]reflect.Type, nOut)
		for i := 0; i < nIn; i++ {
			in[i] = t.In(i)
		}
		for i := 0; i < nOut; i++ {
			out[i] = t.Out(i)
		}
		ret = reflect.FuncOf(in, out, t.IsVariadic())
	case reflect.Interface:
		// not supported
	case reflect.Ptr:
		ret = reflect.PtrTo(t.Elem())
	case reflect.Slice:
		ret = reflect.SliceOf(t.Elem())
	case reflect.Struct:
		// only partially supported: embedded fields
		// and unexported fields may cause panic in reflect.StructOf()
		defer func() {
			// if a panic happens, return t unmodified
			if recover() != nil && ret == nil {
				ret = t
			}
		}()
		n := t.NumField()
		fields := make([]reflect.StructField, n)
		for i := 0; i < n; i++ {
			fields[i] = t.Field(i)
		}
		ret = reflect.StructOf(fields)
	}
	return ret
}

// TryCast works similar to `v2, ok := v.(T)`
// Except it works through type alias'
func TryCast[T any](v any) (T, bool) {

	underlying := Underlying(reflect.TypeOf(v))

	def := *new(T)

	if underlying != Underlying(reflect.TypeOf(def)) {
		return def, false
	}

	r1 := reflect.ValueOf(v)

	if !r1.CanConvert(underlying) {
		return def, false
	}

	r2 := r1.Convert(underlying)

	r3 := r2.Interface()

	r4, ok := r3.(T)
	if !ok {
		return def, false
	}

	return r4, true
}

func TryCastType(v any, dest reflect.Type) (any, bool) {

	underlying := Underlying(reflect.TypeOf(v))

	if underlying != Underlying(dest) {
		return nil, false
	}

	r1 := reflect.ValueOf(v)

	if !r1.CanConvert(underlying) {
		return nil, false
	}

	r2 := r1.Convert(underlying)

	if !r2.CanConvert(dest) {
		return nil, false
	}

	r4 := r2.Convert(dest)

	return r4.Interface(), true
}
