// Copyright 2022 schukai GmbH
// SPDX-License-Identifier: AGPL-3.0

package pathfinder

import (
	"fmt"
	"reflect"
	"strconv"
	"strings"
)

// This function sets the value of a field in a struct, given a path to the field.
func SetValue[D any](obj D, keyWithDots string, newValue any) error {

	keySlice := strings.Split(keyWithDots, ".")
	v := reflect.ValueOf(obj)

	for _, key := range keySlice[0 : len(keySlice)-1] {
		for v.Kind() != reflect.Ptr {
			if v.Kind() == reflect.Invalid {
				return newInvalidPathError(keyWithDots)
			}
			v = v.Addr()
		}

		if v.Kind() != reflect.Ptr {
			return newUnsupportedTypePathError(keyWithDots, v.Type())
		}

		elem := v.Elem()
		if elem.Kind() != reflect.Struct {
			return newUnsupportedTypePathError(keyWithDots, v.Type())
		}

		v = elem.FieldByName(key)

	}

	if v.Kind() == reflect.Invalid {
		return newInvalidPathError(keyWithDots)
	}

	for v.Kind() == reflect.Ptr {
		v = v.Elem()
	}

	// non-supporter type at the top of the path
	if v.Kind() != reflect.Struct {
		return newUnsupportedTypeAtTopOfPathError(keyWithDots, v.Type())
	}

	v = v.FieldByName(keySlice[len(keySlice)-1])
	if !v.IsValid() {
		return newInvalidPathError(keyWithDots)
	}

	if !v.CanSet() {
		return newCannotSetError(keyWithDots)
	}

	switch v.Kind() {
	case reflect.Ptr:
		if newValue == nil {
			v.Set(reflect.Zero(v.Type()))
		} else {
			v.Set(reflect.ValueOf(&newValue))
		}
		return nil
	}

	newValueType := reflect.TypeOf(newValue)
	if newValueType == nil {
		return newUnsupportedTypePathError(keyWithDots, v.Type())
	}

	newValueKind := reflect.TypeOf(newValue).Kind()

	switch v.Kind() {
	case reflect.String:
		if newValueKind == reflect.String {
			v.SetString(newValue.(string))
		} else {
			v.SetString(fmt.Sprintf("%v", newValue))
		}

	case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:

		if newValueKind == reflect.Int {
			v.SetInt(int64(newValue.(int)))
		} else {
			s, err := strconv.ParseInt(newValue.(string), 10, 64)
			if err != nil {
				return err
			}
			v.SetInt(s)
		}

	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:

		if newValueKind == reflect.Int {
			v.SetUint(uint64(newValue.(int)))
		} else {
			s, err := strconv.ParseInt(newValue.(string), 10, 64)
			if err != nil {
				return err
			}
			v.SetUint(uint64(s))
		}

	case reflect.Bool:

		if newValueKind == reflect.Bool {
			v.SetBool(newValue.(bool))
		} else {
			b, err := strconv.ParseBool(newValue.(string))
			if err != nil {
				return err
			}

			v.SetBool(b)
		}

	case reflect.Float64, reflect.Float32:

		if newValueKind == reflect.Float64 {
			v.SetFloat(newValue.(float64))
		} else {
			s, err := strconv.ParseFloat(newValue.(string), 64)
			if err != nil {
				return err
			}

			v.SetFloat(s)
		}

	case reflect.Slice, reflect.Array:

		if newValueKind == reflect.Ptr {
			newValue = reflect.ValueOf(newValue).Elem().Interface()
			v.Set(reflect.ValueOf(newValue))
		} else if newValueKind == reflect.Slice {
			v.Set(reflect.ValueOf(newValue))
		} else {
			return newUnsupportedTypePathError(keyWithDots, v.Type())
		}

	default:
		return newInvalidTypeForPathError(keyWithDots, v.Type().String(), newValueKind.String())
	}

	return nil

}