package mathutil

import (
	"reflect"
	"strconv"
	"strings"
	"time"

	"github.com/gookit/goutil/comdef"
	"github.com/gookit/goutil/internal/checkfn"
	"github.com/gookit/goutil/internal/comfunc"
)

// ConvOption convert options
type ConvOption[T any] struct {
	// if ture: value is nil, will return convert error;
	// if false(default): value is nil, will convert to zero value
	NilAsFail bool
	// HandlePtr auto convert ptr type(int,float,string) value. eg: *int to int
	// 	- if true: will use real type try convert. default is false
	//	- NOTE: current T type's ptr is default support.
	HandlePtr bool
	// StrictMode for convert value. default is false
	//
	// TRUE:
	//  - to int: string, float will return error
	StrictMode bool
	// set custom fallback convert func for not supported type.
	UserConvFn ToTypeFunc[T]
}

// NewConvOption create a new ConvOption
func NewConvOption[T any](optFns ...ConvOptionFn[T]) *ConvOption[T] {
	opt := &ConvOption[T]{}
	opt.WithOption(optFns...)
	return opt
}

// WithOption set convert option
func (opt *ConvOption[T]) WithOption(optFns ...ConvOptionFn[T]) {
	for _, fn := range optFns {
		if fn != nil {
			fn(opt)
		}
	}
}

// ConvOptionFn convert option func
type ConvOptionFn[T any] func(opt *ConvOption[T])

// WithNilAsFail set ConvOption.NilAsFail option
//
// Example:
//
//	ToIntWithFunc(val, mathutil.WithNilAsFail[int])
func WithNilAsFail[T any](opt *ConvOption[T]) { opt.NilAsFail = true }

// WithHandlePtr set ConvOption.HandlePtr option
func WithHandlePtr[T any](opt *ConvOption[T]) { opt.HandlePtr = true }

// WithStrictMode set ConvOption.StrictMode option
func WithStrictMode[T any](opt *ConvOption[T]) { opt.StrictMode = true }

// WithUserConvFn set ConvOption.UserConvFn option
func WithUserConvFn[T any](fn ToTypeFunc[T]) ConvOptionFn[T] {
	return func(opt *ConvOption[T]) {
		opt.UserConvFn = fn
	}
}

/*************************************************************
 * region Strict to int/uint
 *************************************************************/

// StrictInt check the given value is an integer(intX,uintX), return the int64 value and true if success
func StrictInt(val any) (int64, bool) {
	switch tVal := val.(type) {
	case int:
		return int64(tVal), true
	case int8:
		return int64(tVal), true
	case int16:
		return int64(tVal), true
	case int32:
		return int64(tVal), true
	case int64:
		return tVal, true
	case uint:
		return int64(tVal), true
	case uint8:
		return int64(tVal), true
	case uint16:
		return int64(tVal), true
	case uint32:
		return int64(tVal), true
	case uint64:
		return int64(tVal), true
	case uintptr:
		return int64(tVal), true
	default:
		return 0, false
	}
}

// StrictUint strict check value is integer(intX,uintX) and convert to uint64.
func StrictUint(val any) (uint64, bool) {
	switch tVal := val.(type) {
	case int:
		return uint64(tVal), true
	case int8:
		return uint64(tVal), true
	case int16:
		return uint64(tVal), true
	case int32:
		return uint64(tVal), true
	case int64:
		return uint64(tVal), true
	case uint:
		return uint64(tVal), true
	case uint8:
		return uint64(tVal), true
	case uint16:
		return uint64(tVal), true
	case uint32:
		return uint64(tVal), true
	case uint64:
		return tVal, true
	case uintptr:
		return uint64(tVal), true
	default:
		return 0, false
	}
}

/*************************************************************
 * region convert to float64
 *************************************************************/

// QuietFloat convert value to float64, will ignore error. alias of SafeFloat
func QuietFloat(in any) float64 { return SafeFloat(in) }

// SafeFloat convert value to float64, will ignore error
func SafeFloat(in any) float64 {
	val, _ := ToFloatWith(in)
	return val
}

// FloatOrPanic convert value to float64, will panic on error
func FloatOrPanic(in any) float64 { return MustFloat(in) }

// MustFloat convert value to float64, will panic on error
func MustFloat(in any) float64 {
	val, err := ToFloatWith(in)
	if err != nil {
		panic(err)
	}
	return val
}

// FloatOrDefault convert value to float64, will return default value on error
func FloatOrDefault(in any, defVal float64) float64 { return FloatOr(in, defVal) }

// FloatOr convert value to float64, will return default value on error
func FloatOr(in any, defVal float64) float64 {
	val, err := ToFloatWith(in)
	if err != nil {
		return defVal
	}
	return val
}

// Float convert value to float64, return error on failed
func Float(in any) (float64, error) { return ToFloatWith(in) }

// FloatOrErr convert value to float64, return error on failed
func FloatOrErr(in any) (float64, error) { return ToFloatWith(in) }

// ToFloat convert value to float64, return error on failed
func ToFloat(in any) (float64, error) { return ToFloatWith(in) }

// ToFloatWith try to convert value to float64. can with some option func, more see ConvOption.
func ToFloatWith(in any, optFns ...ConvOptionFn[float64]) (f64 float64, err error) {
	opt := NewConvOption(optFns...)
	if !opt.NilAsFail && in == nil {
		return 0, nil
	}

	switch tVal := in.(type) {
	case string:
		f64, err = strconv.ParseFloat(strings.TrimSpace(tVal), 64)
	case int:
		f64 = float64(tVal)
	case int8:
		f64 = float64(tVal)
	case int16:
		f64 = float64(tVal)
	case int32:
		f64 = float64(tVal)
	case int64:
		f64 = float64(tVal)
	case uint:
		f64 = float64(tVal)
	case uint8:
		f64 = float64(tVal)
	case uint16:
		f64 = float64(tVal)
	case uint32:
		f64 = float64(tVal)
	case uint64:
		f64 = float64(tVal)
	case float32:
		f64 = float64(tVal)
	case float64:
		f64 = tVal
	case *float64: // default support float64 ptr type
		f64 = *tVal
	case time.Duration:
		f64 = float64(tVal)
	case comdef.Float64able: // eg: json.Number
		f64, err = tVal.Float64()
	default:
		if opt.HandlePtr {
			if rv := reflect.ValueOf(in); rv.Kind() == reflect.Pointer {
				rv = rv.Elem()
				if checkfn.IsSimpleKind(rv.Kind()) {
					return ToFloatWith(rv.Interface(), optFns...)
				}
			}
		}

		if opt.UserConvFn != nil {
			f64, err = opt.UserConvFn(in)
		} else {
			err = comdef.ErrConvType
		}
	}
	return
}

/*************************************************************
 * region intX/floatX to string
 *************************************************************/

// MustString convert intX/floatX value to string, will panic on error
func MustString(val any) string {
	str, err := ToStringWith(val)
	if err != nil {
		panic(err)
	}
	return str
}

// StringOrPanic convert intX/floatX value to string, will panic on error
func StringOrPanic(val any) string { return MustString(val) }

// StringOrDefault convert intX/floatX value to string, will return default value on error
func StringOrDefault(val any, defVal string) string { return StringOr(val, defVal) }

// StringOr convert intX/floatX value to string, will return default value on error
func StringOr(val any, defVal string) string {
	str, err := ToStringWith(val)
	if err != nil {
		return defVal
	}
	return str
}

// ToString convert intX/floatX value to string, return error on failed
func ToString(val any) (string, error) { return ToStringWith(val) }

// StringOrErr convert intX/floatX value to string, return error on failed
func StringOrErr(val any) (string, error) { return ToStringWith(val) }

// QuietString convert intX/floatX value to string, other type convert by fmt.Sprint
func QuietString(val any) string { return SafeString(val) }

// String convert intX/floatX value to string, other type convert by fmt.Sprint
func String(val any) string {
	str, _ := TryToString(val, false)
	return str
}

// SafeString convert intX/floatX value to string, other type convert by fmt.Sprint
func SafeString(val any) string {
	str, _ := TryToString(val, false)
	return str
}

// TryToString try convert intX/floatX value to string
//
// if defaultAsErr is False, will use fmt.Sprint convert other type
func TryToString(val any, defaultAsErr bool) (string, error) {
	var optFn comfunc.ConvOptionFn
	if !defaultAsErr {
		optFn = comfunc.WithUserConvFn(comfunc.StrBySprintFn)
	}
	return ToStringWith(val, optFn)
}

// ToStringWith try to convert value to string. can with some option func, more see comfunc.ConvOption.
func ToStringWith(in any, optFns ...comfunc.ConvOptionFn) (string, error) {
	return comfunc.ToStringWith(in, optFns...)
}
