package structify

import (
	"encoding"
	"fmt"
	"github.com/pkg/errors"
	"golang.org/x/exp/constraints"
	"reflect"
	"strconv"
	"strings"
	"unsafe"
)

// structBranch represents either a leaf or a subTree.
type structBranch struct {
	// field specifies the struct field index.
	field int
	// leaf specifies the map key to parse the struct field from.
	leaf string
	// subTree specifies the struct field's inner tree.
	subTree []structBranch
}

type MapStructifier = func(map[string]interface{}) (interface{}, error)

// MakeMapStructifier builds a function which parses a map's string values into a new struct of type t
// and returns a pointer to it. tag specifies which tag connects struct fields to map keys.
// MakeMapStructifier panics if it detects an unsupported type (suitable for usage in init() or global vars).
func MakeMapStructifier(t reflect.Type, tag string, initer func(any)) MapStructifier {
	tree := buildStructTree(t, tag)

	return func(kv map[string]interface{}) (interface{}, error) {
		vPtr := reflect.New(t)
		ptr := vPtr.Interface()
		if initer != nil {
			initer(ptr)
		}
		vPtrElem := vPtr.Elem()
		err := errors.Wrapf(structifyMapByTree(kv, tree, vPtrElem, vPtrElem, new([]int)), "can't structify map %#v by tree %#v", kv, tree)

		return ptr, err
	}
}

// buildStructTree assembles a tree which represents the struct t based on tag.
func buildStructTree(t reflect.Type, tag string) []structBranch {
	var tree []structBranch
	numFields := t.NumField()

	for i := 0; i < numFields; i++ {
		if field := t.Field(i); field.PkgPath == "" {
			switch tagValue := field.Tag.Get(tag); tagValue {
			case "", "-":
			case ",inline":
				if subTree := buildStructTree(field.Type, tag); subTree != nil {
					tree = append(tree, structBranch{i, "", subTree})
				}
			default:
				// If parseString doesn't support *T, it'll panic.
				_ = parseString("", reflect.New(field.Type).Interface())

				tree = append(tree, structBranch{i, tagValue, nil})
			}
		}
	}

	return tree
}

// structifyMapByTree parses src's string values into the struct dest according to tree's specification.
func structifyMapByTree(src map[string]interface{}, tree []structBranch, dest, root reflect.Value, stack *[]int) error {
	*stack = append(*stack, 0)
	defer func() {
		*stack = (*stack)[:len(*stack)-1]
	}()

	for _, branch := range tree {
		(*stack)[len(*stack)-1] = branch.field

		if branch.subTree == nil {
			if v, ok := src[branch.leaf]; ok {
				if vs, ok := v.(string); ok {
					if err := parseString(vs, dest.Field(branch.field).Addr().Interface()); err != nil {
						rt := root.Type()
						typ := rt
						var path []string

						for _, i := range *stack {
							f := typ.Field(i)
							path = append(path, f.Name)
							typ = f.Type
						}

						return errors.Wrapf(err, "can't parse %s into the %s %s#%s: %s",
							branch.leaf, typ.Name(), rt.Name(), strings.Join(path, "."), vs)
					}
				}
			}
		} else if err := structifyMapByTree(src, branch.subTree, dest.Field(branch.field), root, stack); err != nil {
			return err
		}
	}

	return nil
}

// parseString parses src into *dest.
func parseString(src string, dest interface{}) error {
	switch ptr := dest.(type) {
	case encoding.TextUnmarshaler:
		return ptr.UnmarshalText([]byte(src))
	case *string:
		*ptr = src
		return nil
	case **string:
		*ptr = &src
		return nil
	case *uint8:
		return parseUint(src, ptr)
	case *uint16:
		return parseUint(src, ptr)
	case *uint32:
		return parseUint(src, ptr)
	case *uint64:
		return parseUint(src, ptr)
	case *int8:
		return parseInt(src, ptr)
	case *int16:
		return parseInt(src, ptr)
	case *int32:
		return parseInt(src, ptr)
	case *int64:
		return parseInt(src, ptr)
	case *float32:
		return parseFloat(src, ptr)
	case *float64:
		return parseFloat(src, ptr)
	default:
		panic(fmt.Sprintf("unsupported type: %T", dest))
	}
}

// parseUint parses src into *dest.
func parseUint[T constraints.Unsigned](src string, dest *T) error {
	i, err := strconv.ParseUint(src, 10, bitSizeOf[T]())
	if err == nil {
		*dest = T(i)
	}

	return err
}

// parseInt parses src into *dest.
func parseInt[T constraints.Signed](src string, dest *T) error {
	i, err := strconv.ParseInt(src, 10, bitSizeOf[T]())
	if err == nil {
		*dest = T(i)
	}

	return err
}

// parseFloat parses src into *dest.
func parseFloat[T constraints.Float](src string, dest *T) error {
	f, err := strconv.ParseFloat(src, bitSizeOf[T]())
	if err == nil {
		*dest = T(f)
	}

	return err
}

func bitSizeOf[T any]() int {
	var x T
	return int(unsafe.Sizeof(x) * 8)
}
