package js

import (
	"encoding"
	"encoding/json"
	"fmt"
	"reflect"
	"strconv"
	"strings"
	"time"

	"sirherobrine23.com.br/Sirherobrine23/napi-go"
)

const propertiesTagName = "napi"

// Convert go types to valid NAPI, if not conpatible return Undefined.
func ValueOf(env napi.EnvType, value any) (napiValue napi.ValueType, err error) {
	return valueOf(env, reflect.ValueOf(value))
}

// Convert NAPI value to Go values
func ValueFrom(napiValue napi.ValueType, v any) error {
	ptr := reflect.ValueOf(v)
	if ptr.Kind() != reflect.Pointer {
		return fmt.Errorf("require point to convert napi value to go value")
	}
	return valueFrom(napiValue, ptr.Elem())
}

func valueOf(env napi.EnvType, ptr reflect.Value) (napiValue napi.ValueType, err error) {
	defer func(err *error) {
		if err2 := recover(); err2 != nil {
			switch v := err2.(type) {
			case error:
				*err = v
			default:
				*err = fmt.Errorf("panic recover: %s", err2)
			}
		}
	}(&err)

	ptrType := ptr.Type()
	if ptrType.ConvertibleTo(reflect.TypeFor[napi.ValueType]()) {
		if ptr.IsValid() {
			return ptr.Interface().(napi.ValueType), nil
		}
		return nil, nil
	} else if !ptr.IsValid() {
		return env.Undefined()
	} else if !ptr.IsZero() && ptr.CanInterface() { // Marshalers
		switch v := ptr.Interface().(type) {
		case time.Time:
			return napi.CreateDate(env, v)
		case encoding.TextMarshaler:
			data, err := v.MarshalText()
			if err != nil {
				return nil, err
			}
			return napi.CreateString(env, string(data))
		case json.Marshaler:
			var pointData any
			data, err := v.MarshalJSON()
			if err != nil {
				return nil, err
			} else if err = json.Unmarshal(data, &pointData); err != nil {
				return nil, err
			}
			return ValueOf(env, pointData)
		}
	}

	switch ptrType.Kind() {
	case reflect.Pointer:
		return valueOf(env, ptr.Elem())
	case reflect.String:
		return napi.CreateString(env, ptr.String())
	case reflect.Bool:
		return napi.CreateBoolean(env, ptr.Bool())
	case reflect.Int, reflect.Uint, reflect.Int32, reflect.Uint32, reflect.Float32, reflect.Int8, reflect.Uint8, reflect.Int16, reflect.Uint16:
		return napi.CreateNumber(env, ptr.Int())
	case reflect.Float64:
		return napi.CreateNumber(env, ptr.Float())
	case reflect.Int64, reflect.Uint64:
		return napi.CreateBigint(env, ptr.Int())
	case reflect.Func:
		return funcOf(env, ptr)
	case reflect.Slice, reflect.Array:
		arr, err := napi.CreateArray(env, ptr.Len())
		if err != nil {
			return nil, err
		}
		for index := range ptr.Len() {
			value, err := valueOf(env, ptr.Index(index))
			if err != nil {
				return arr, err
			} else if err = arr.Set(index, value); err != nil {
				return arr, err
			}
		}
		return arr, nil
	case reflect.Struct:
		obj, err := napi.CreateObject(env)
		if err != nil {
			return nil, err
		}

		for keyIndex := range ptrType.NumField() {
			field, fieldType := ptr.Field(keyIndex), ptrType.Field(keyIndex)
			if !fieldType.IsExported() || fieldType.Tag.Get(propertiesTagName) == "-" {
				continue
			}

			value, err := valueOf(env, field)
			if err != nil {
				return obj, err
			}

			typeof, err := value.Type()
			if err != nil {
				return nil, err
			}

			keyNamed := fieldType.Name
			if strings.Count(fieldType.Tag.Get(propertiesTagName), ",") > 0 {
				fields := strings.SplitN(fieldType.Tag.Get(propertiesTagName), ",", 2)
				keyNamed = fields[0]
				switch fields[1] {
				case "omitempty":
					switch typeof {
					case napi.TypeUndefined, napi.TypeNull, napi.TypeUnkown:
						continue
					case napi.TypeString:
						str, err := napi.ToString(value).Utf8Value()
						if err != nil {
							return nil, err
						} else if str == "" {
							continue
						}
					}
				case "omitzero":
					switch typeof {
					case napi.TypeUndefined, napi.TypeNull, napi.TypeUnkown:
						continue
					case napi.TypeDate:
						value, err := napi.ToDate(value).Time()
						if err != nil {
							return nil, err
						} else if value.Unix() == 0 {
							continue
						}
					case napi.TypeBigInt:
						value, err := napi.ToBigint(value).Int64()
						if err != nil {
							return nil, err
						} else if value == 0 {
							continue
						}
					case napi.TypeNumber:
						value, err := napi.ToNumber(value).Int()
						if err != nil {
							return nil, err
						} else if value == 0 {
							continue
						}
					case napi.TypeArray:
						value, err := napi.ToArray(value).Length()
						if err != nil {
							return nil, err
						} else if value == 0 {
							continue
						}
					}
				}
			}
			if err = obj.Set(keyNamed, value); err != nil {
				return obj, err
			}
		}

		return obj, nil
	case reflect.Map:
		obj, err := napi.CreateObject(env)
		if err != nil {
			return nil, err
		}
		for ptrKey, ptrValue := range ptr.Seq2() {
			key, err := valueOf(env, ptrKey)
			if err != nil {
				return nil, err
			}
			value, err := valueOf(env, ptrValue)
			if err != nil {
				return nil, err
			} else if err = obj.SetWithValue(key, value); err != nil {
				return nil, err
			}
		}
		return obj, nil
	case reflect.Interface:
		if ptr.IsValid() {
			if ptr.IsNil() {
				return env.Null()
			} else if ptr.CanInterface() {
				return valueOf(env, reflect.ValueOf(ptr.Interface()))
			}
		}
	}
	return env.Undefined()
}

// Convert javascript value to go typed value
func valueFrom(jsValue napi.ValueType, ptr reflect.Value) error {
	typeOf, err := jsValue.Type()
	if err != nil {
		return err
	}

	ptrType := ptr.Type()
	if ptrType.ConvertibleTo(reflect.TypeFor[napi.ValueType]()) {
		switch typeOf {
		case napi.TypeUndefined:
			und, err := jsValue.Env().Undefined()
			if err != nil {
				return err
			}
			ptr.Set(reflect.ValueOf(und))
		case napi.TypeNull:
			null, err := jsValue.Env().Null()
			if err != nil {
				return err
			}
			ptr.Set(reflect.ValueOf(null))
		case napi.TypeBoolean:
			ptr.Set(reflect.ValueOf(napi.ToBoolean(jsValue)))
		case napi.TypeNumber:
			ptr.Set(reflect.ValueOf(napi.ToNumber(jsValue)))
		case napi.TypeBigInt:
			ptr.Set(reflect.ValueOf(napi.ToBigint(jsValue)))
		case napi.TypeString:
			ptr.Set(reflect.ValueOf(napi.ToString(jsValue)))
		case napi.TypeSymbol:
			// ptr.Set(reflect.ValueOf(napi.ToString(jsValue)))
		case napi.TypeObject:
			ptr.Set(reflect.ValueOf(napi.ToObject(jsValue)))
		case napi.TypeFunction:
			ptr.Set(reflect.ValueOf(napi.ToFunction(jsValue)))
		case napi.TypeExternal:
			// ptr.Set(reflect.ValueOf(napi.ToFunction(jsValue)))
		case napi.TypeTypedArray:
			// ptr.Set(reflect.ValueOf(napi.ToFunction(jsValue)))
		case napi.TypePromise:
			// ptr.Set(reflect.ValueOf(napi.ToFunction(jsValue)))
		case napi.TypeDataView:
			// ptr.Set(reflect.ValueOf(napi.ToFunction(jsValue)))
		case napi.TypeBuffer:
			ptr.Set(reflect.ValueOf(napi.ToBuffer(jsValue)))
		case napi.TypeDate:
			ptr.Set(reflect.ValueOf(napi.ToDate(jsValue)))
		case napi.TypeArray:
			ptr.Set(reflect.ValueOf(napi.ToArray(jsValue)))
		case napi.TypeArrayBuffer:
			// ptr.Set(reflect.ValueOf(napi.ToArray(jsValue)))
		case napi.TypeError:
			ptr.Set(reflect.ValueOf(napi.ToError(jsValue)))
		}
		return nil
	}

	switch ptrType.Kind() {
	case reflect.Pointer:
		return valueFrom(jsValue, ptr.Elem())
	case reflect.Interface:
		// Check if is any and can set
		if ptr.CanSet() && ptrType == reflect.TypeFor[any]() {
			switch typeOf {
			case napi.TypeNull, napi.TypeUndefined, napi.TypeUnkown:
				ptr.Set(reflect.Zero(ptrType))
				return nil
			case napi.TypeBoolean:
				valueOf, err := napi.ToBoolean(jsValue).Value()
				if err != nil {
					return err
				}
				ptr.Set(reflect.ValueOf(valueOf))
			case napi.TypeNumber:
				numberValue, err := napi.ToNumber(jsValue).Float()
				if err != nil {
					return err
				}
				ptr.Set(reflect.ValueOf(numberValue))
			case napi.TypeBigInt:
				numberValue, err := napi.ToBigint(jsValue).Int64()
				if err != nil {
					return err
				}
				ptr.Set(reflect.ValueOf(numberValue))
			case napi.TypeString:
				str, err := napi.ToString(jsValue).Utf8Value()
				if err != nil {
					return err
				}
				ptr.Set(reflect.ValueOf(str))
			case napi.TypeDate:
				timeDate, err := napi.ToDate(jsValue).Time()
				if err != nil {
					return err
				}
				ptr.Set(reflect.ValueOf(timeDate))
			case napi.TypeArray:
				napiArray := napi.ToArray(jsValue)
				size, err := napiArray.Length()
				if err != nil {
					return err
				}
				value := reflect.MakeSlice(reflect.SliceOf(ptrType), size, size)
				for index := range size {
					napiValue, err := napiArray.Get(index)
					if err != nil {
						return err
					} else if err = valueFrom(napiValue, value.Index(index)); err != nil {
						return err
					}
				}
				ptr.Set(value)
			case napi.TypeBuffer:
				buff, err := napi.ToBuffer(jsValue).Data()
				if err != nil {
					return err
				}
				ptr.Set(reflect.ValueOf(buff))
			case napi.TypeObject:
				obj := napi.ToObject(jsValue)
				goMap := reflect.MakeMap(reflect.MapOf(reflect.TypeFor[string](), reflect.TypeFor[any]()))
				for keyName, value := range obj.Seq() {
					valueOf := reflect.New(reflect.TypeFor[any]())
					if err := valueFrom(value, valueOf); err != nil {
						return err
					}
					goMap.SetMapIndex(reflect.ValueOf(keyName), valueOf)
				}
				ptr.Set(goMap)
			case napi.TypeFunction:
				ptr.Set(reflect.ValueOf(napi.ToFunction(jsValue)))
			}
			return nil
		}
		return fmt.Errorf("cannot set value, returned %s", typeOf)
	}

	switch typeOf {
	case napi.TypeNull, napi.TypeUndefined, napi.TypeUnkown:
		switch ptrType.Kind() {
		case reflect.Interface, reflect.Pointer:
			ptr.Set(reflect.Zero(ptrType))
			return nil
		default:
			return fmt.Errorf("cannot set value, returned %s", typeOf)
		}
	case napi.TypeBoolean:
		switch ptr.Kind() {
		case reflect.Bool:
			valueOf, err := napi.ToBoolean(jsValue).Value()
			if err != nil {
				return err
			}
			ptr.SetBool(valueOf)
		default:
			return fmt.Errorf("cannot set boolean value to %s", ptr.Kind())
		}
	case napi.TypeNumber:
		switch ptrType.Kind() {
		case reflect.Float32, reflect.Float64:
			floatValue, err := napi.ToNumber(jsValue).Float()
			if err != nil {
				return err
			}
			ptr.SetFloat(floatValue)
			return nil
		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
			numberValue, err := napi.ToNumber(jsValue).Int()
			if err != nil {
				return err
			}
			ptr.SetInt(numberValue)
			return nil
		case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
			numberValue, err := napi.ToNumber(jsValue).Int()
			if err != nil {
				return err
			}
			ptr.SetUint(uint64(numberValue))
			return nil
		default:
			return fmt.Errorf("cannot set number value to %s", ptr.Kind())
		}
	case napi.TypeBigInt:
		switch ptrType.Kind() {
		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
			numberValue, err := napi.ToNumber(jsValue).Int()
			if err != nil {
				return err
			}
			ptr.SetInt(numberValue)
			return nil
		case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
			numberValue, err := napi.ToNumber(jsValue).Int()
			if err != nil {
				return err
			}
			ptr.SetUint(uint64(numberValue))
			return nil
		default:
			return fmt.Errorf("cannot set number value to %s", ptr.Kind())
		}
	case napi.TypeString:
		switch ptr.Kind() {
		case reflect.String:
		default:
			return fmt.Errorf("cannot set string to %s", ptr.Kind())
		}
		str, err := napi.ToString(jsValue).Utf8Value()
		if err != nil {
			return err
		}
		ptr.Set(reflect.ValueOf(str))
		return nil
	case napi.TypeDate:
		switch ptrType.Kind() {
		case reflect.Struct:
			if ptrType == reflect.TypeFor[time.Time]() {
				break
			}
			fallthrough
		default:
			return fmt.Errorf("cannot set Date to %s", ptr.Kind())
		}
		timeDate, err := napi.ToDate(jsValue).Time()
		if err != nil {
			return err
		}
		ptr.Set(reflect.ValueOf(timeDate))
		return nil
	case napi.TypeArray:
		napiArray := napi.ToArray(jsValue)
		size, err := napiArray.Length()
		if err != nil {
			return err
		}

		switch ptr.Kind() {
		case reflect.Slice:
			value := reflect.MakeSlice(ptrType, size, size)
			for index := range size {
				napiValue, err := napiArray.Get(index)
				if err != nil {
					return err
				} else if err = valueFrom(napiValue, value.Index(index)); err != nil {
					return err
				}
			}
			ptr.Set(value)
			return nil
		case reflect.Array:
			value := reflect.New(ptrType)
			for index := range min(size, value.Len()) {
				napiValue, err := napiArray.Get(index)
				if err != nil {
					return err
				} else if err = valueFrom(napiValue, value.Index(index)); err != nil {
					return err
				}
			}
			ptr.Set(value)
			return nil
		default:
			return fmt.Errorf("cannot set Array to %s", ptr.Kind())
		}
	case napi.TypeBuffer:
		switch ptr.Kind() {
		case reflect.Slice:
			if ptrType == reflect.TypeFor[[]byte]() {
				break
			}
			fallthrough
		default:
			return fmt.Errorf("cannot set Buffer to %s", ptr.Kind())
		}
		buff, err := napi.ToBuffer(jsValue).Data()
		if err != nil {
			return err
		}
		ptr.SetBytes(buff)
		return nil
	case napi.TypeObject:
		obj := napi.ToObject(jsValue)
		switch ptr.Kind() {
		case reflect.Struct:
			ptr.Set(reflect.New(ptrType).Elem())
			for keyIndex := range ptrType.NumField() {
				field, fieldType := ptr.Field(keyIndex), ptrType.Field(keyIndex)
				if !fieldType.IsExported() || fieldType.Tag.Get(propertiesTagName) == "-" {
					continue
				}

				keyName, omitEmpty, omitZero := fieldType.Name, false, false
				if strings.Count(fieldType.Tag.Get(propertiesTagName), ",") > 0 {
					fields := strings.SplitN(fieldType.Tag.Get(propertiesTagName), ",", 2)
					keyName = fields[0]
					switch fields[1] {
					case "omitempty":
						omitEmpty = true
					case "omitzero":
						omitZero = true
					}
				} else {
					omitEmpty, omitZero = true, true
				}

				if ok, err := obj.Has(keyName); err != nil {
					return err
				} else if !ok && !(omitEmpty || omitZero) {
					return fmt.Errorf("cannot set %s to %s", keyName, ptr.Kind())
				}

				value, err := obj.Get(keyName)
				if err != nil {
					return err
				}

				valueTypeof, _ := value.Type()
				if omitEmpty || omitZero {
					switch valueTypeof {
					case napi.TypeUndefined, napi.TypeNull, napi.TypeUnkown:
						continue
					case napi.TypeString:
						if str, _ := napi.ToString(value).Utf8Value(); str == "" {
							continue
						}
					case napi.TypeDate:
						if timeDate, _ := napi.ToDate(value).Time(); timeDate.Unix() == 0 {
							continue
						}
					case napi.TypeBigInt:
						if numberValue, _ := napi.ToBigint(value).Int64(); numberValue == 0 {
							continue
						}
					case napi.TypeNumber:
						if numberValue, _ := napi.ToNumber(value).Int(); numberValue == 0 {
							continue
						}
					case napi.TypeArray:
						if size, _ := napi.ToArray(value).Length(); size == 0 {
							continue
						}
					}
				}

				valueOf := reflect.New(fieldType.Type).Elem()
				if err := valueFrom(value, valueOf); err != nil {
					return err
				}
				field.Set(valueOf)
			}
			return nil
		case reflect.Map:
			// Check if key is string, bool, int*, uint*, float*, else return error
			switch ptrType.Key().Kind() {
			case reflect.String:
			case reflect.Bool:
			case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
			case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
			case reflect.Float32, reflect.Float64:
			default:
				return fmt.Errorf("cannot set Object to %s", ptr.Kind())
			}

			goMap := reflect.MakeMap(ptrType)
			for keyName, value := range obj.Seq() {
				keySetValue := reflect.New(ptrType.Key()).Elem()
				switch ptrType.Key().Kind() {
				case reflect.String:
					keySetValue.SetString(keyName)
				case reflect.Bool:
					boolV, err := strconv.ParseBool(keyName)
					if err != nil {
						return err
					}
					keySetValue.SetBool(boolV)
				case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
					intV, err := strconv.ParseInt(keyName, 10, 64)
					if err != nil {
						return err
					}
					keySetValue.SetInt(intV)
				case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
					intV, err := strconv.ParseUint(keyName, 10, 64)
					if err != nil {
						return err
					}
					keySetValue.SetUint(intV)
				case reflect.Float32, reflect.Float64:
					floatV, err := strconv.ParseFloat(keyName, 64)
					if err != nil {
						return err
					}
					keySetValue.SetFloat(floatV)
				}

				valueOf := reflect.New(ptrType.Elem()).Elem()
				if err := valueFrom(value, valueOf); err != nil {
					return err
				}
				goMap.SetMapIndex(keySetValue, valueOf)
			}
			ptr.Set(goMap)
			return nil
		default:
			return fmt.Errorf("cannot set Object to %s", ptr.Kind())
		}
	default:
		println(typeOf.String())
	}

	return nil
}