package reflectext

import (
	"reflect"
	"strings"
)

// GetMapPath returns the value deep inside a hierahically nested map structure
// eg:
// x := langext.H{"K1": langext.H{"K2": 665}}
// GetMapPath[int](x, "K1.K2") == 665
func GetMapPath[TData any](mapval any, path string) (TData, bool) {
	var ok bool

	split := strings.Split(path, ".")

	for i, key := range split {

		if i < len(split)-1 {
			mapval, ok = GetMapField[any](mapval, key)
			if !ok {
				return *new(TData), false
			}
		} else {
			return GetMapField[TData](mapval, key)
		}
	}

	return *new(TData), false
}

// GetMapField gets the value of a map, without knowing the actual types (mapval is any)
// eg:
// x := langext.H{"K1": 665}
// GetMapPath[int](x, "K1") == 665
//
// works with aliased types and autom. dereferences pointes
func GetMapField[TData any, TKey comparable](mapval any, key TKey) (TData, bool) {

	rval := reflect.ValueOf(mapval)

	for rval.Kind() == reflect.Ptr && !rval.IsNil() {
		rval = rval.Elem()
	}

	if rval.Kind() != reflect.Map {
		return *new(TData), false // mapval is not a map
	}

	kval := reflect.ValueOf(key)

	if !kval.Type().AssignableTo(rval.Type().Key()) {
		return *new(TData), false // key cannot index mapval
	}

	eval := rval.MapIndex(kval)
	if !eval.IsValid() {
		return *new(TData), false // key does not exist in mapval
	}

	destType := reflect.TypeOf(new(TData)).Elem()

	if eval.Type() == destType {
		return eval.Interface().(TData), true
	}

	if eval.CanConvert(destType) && !preventConvert(eval.Type(), destType) {
		return eval.Convert(destType).Interface().(TData), true
	}

	if (eval.Kind() == reflect.Ptr || eval.Kind() == reflect.Interface) && eval.IsNil() && destType.Kind() == reflect.Ptr {
		return *new(TData), false // special case: mapval[key] is nil
	}

	for (eval.Kind() == reflect.Ptr || eval.Kind() == reflect.Interface) && !eval.IsNil() {
		eval = eval.Elem()

		if eval.Type() == destType {
			return eval.Interface().(TData), true
		}

		if eval.CanConvert(destType) && !preventConvert(eval.Type(), destType) {
			return eval.Convert(destType).Interface().(TData), true
		}
	}

	return *new(TData), false // mapval[key] is not of type TData
}

func preventConvert(t1 reflect.Type, t2 reflect.Type) bool {
	if t1.Kind() == reflect.String && t1.Kind() != reflect.String {
		return true
	}
	if t2.Kind() == reflect.String && t1.Kind() != reflect.String {
		return true
	}
	return false
}