package utils

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

func Map2Struct(vals map[string]interface{}, dst interface{}) (err error) {
	return Map2StructByTag(vals, dst, "json")
}

func Map2StructByTag(vals map[string]interface{}, dst interface{}, structTag string) (err error) {
	defer func() {
		e := recover()
		if e != nil {
			if v, ok := e.(error); ok {
				err = fmt.Errorf("Panic: %v", v.Error())
			} else {
				err = fmt.Errorf("Panic: %v", e)
			}
		}
	}()

	pt := reflect.TypeOf(dst)
	pv := reflect.ValueOf(dst)

	if pv.Kind() != reflect.Ptr || pv.Elem().Kind() != reflect.Struct {
		return fmt.Errorf("not a pointer of struct")
	}

	var f reflect.StructField
	var ft reflect.Type
	var fv reflect.Value

	for i := 0; i < pt.Elem().NumField(); i++ {
		f = pt.Elem().Field(i)
		fv = pv.Elem().Field(i)
		ft = f.Type

		if f.Anonymous || !fv.CanSet() {
			continue
		}

		tag := f.Tag.Get(structTag)

		name, option := parseTag(tag)

		if name == "-" {
			continue
		}

		if name == "" {
			name = strings.ToLower(f.Name)
		}
		val, ok := vals[name]

		if !ok {
			if option == "required" {
				return fmt.Errorf("'%v' not found", name)
			}
			if len(option) != 0 {
				val = option // default value
			} else {
				//fv.Set(reflect.Zero(ft)) // TODO set zero value or just ignore it?
				continue
			}
		}

		// convert or set value to field
		vv := reflect.ValueOf(val)
		vt := reflect.TypeOf(val)

		if vt.Kind() != reflect.String {
			// try to assign and convert
			if vt.AssignableTo(ft) {
				fv.Set(vv)
				continue
			}

			if vt.ConvertibleTo(ft) {
				fv.Set(vv.Convert(ft))
				continue
			}

			return fmt.Errorf("value type not match: field=%v(%v) value=%v(%v)", f.Name, ft.Kind(), val, vt.Kind())
		}
		s := strings.TrimSpace(vv.String())
		if len(s) == 0 && option == "required" {
			return fmt.Errorf("value of required argument can't not be empty")
		}
		fk := ft.Kind()

		// convert string to value
		if fk == reflect.Ptr && ft.Elem().Kind() == reflect.String {
			fv.Set(reflect.ValueOf(&s))
			continue
		}
		if fk == reflect.Ptr || fk == reflect.Struct {
			err = convertJsonValue(s, name, fv)
		} else if fk == reflect.Slice {
			err = convertSlice(s, f.Name, ft, fv)
		} else {
			err = convertValue(fk, s, f.Name, fv)
		}

		if err != nil {
			return err
		}
		continue
	}

	return nil
}

func Struct2Map(s interface{}) map[string]interface{} {
	return Struct2MapByTag(s, "json")
}
func Struct2MapByTag(s interface{}, tagName string) map[string]interface{} {
	t := reflect.TypeOf(s)
	v := reflect.ValueOf(s)

	if v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct {
		t = t.Elem()
		v = v.Elem()
	}

	if v.Kind() != reflect.Struct {
		return nil
	}

	m := make(map[string]interface{})

	for i := 0; i < t.NumField(); i++ {
		fv := v.Field(i)
		ft := t.Field(i)

		if !fv.CanInterface() {
			continue
		}

		if ft.PkgPath != "" { // unexported
			continue
		}

		var name string
		var option string
		tag := ft.Tag.Get(tagName)
		if tag != "" {
			ts := strings.Split(tag, ",")
			if len(ts) == 1 {
				name = ts[0]
			} else if len(ts) > 1 {
				name = ts[0]
				option = ts[1]
			}
			if name == "-" {
				continue // skip this field
			}
			if name == "" {
				name = strings.ToLower(ft.Name)
			}
			if option == "omitempty" {
				if isEmpty(&fv) {
					continue // skip empty field
				}
			}
		} else {
			name = strings.ToLower(ft.Name)
		}

		if ft.Anonymous && fv.Kind() == reflect.Ptr && fv.IsNil() {
			continue
		}
		if (ft.Anonymous && fv.Kind() == reflect.Struct) ||
			(ft.Anonymous && fv.Kind() == reflect.Ptr && fv.Elem().Kind() == reflect.Struct) {

			// embedded struct
			embedded := Struct2MapByTag(fv.Interface(), tagName)
			for embName, embValue := range embedded {
				m[embName] = embValue
			}
		} else if option == "string" {
			kind := fv.Kind()
			if kind == reflect.Int || kind == reflect.Int8 || kind == reflect.Int16 || kind == reflect.Int32 || kind == reflect.Int64 {
				m[name] = strconv.FormatInt(fv.Int(), 10)
			} else if kind == reflect.Uint || kind == reflect.Uint8 || kind == reflect.Uint16 || kind == reflect.Uint32 || kind == reflect.Uint64 {
				m[name] = strconv.FormatUint(fv.Uint(), 10)
			} else if kind == reflect.Float32 || kind == reflect.Float64 {
				m[name] = strconv.FormatFloat(fv.Float(), 'f', 2, 64)
			} else {
				m[name] = fv.Interface()
			}
		} else {
			m[name] = fv.Interface()
		}
	}

	return m
}

func isEmpty(v *reflect.Value) bool {
	k := v.Kind()
	if k == reflect.Bool {
		return v.Bool() == false
	} else if reflect.Int < k && k < reflect.Int64 {
		return v.Int() == 0
	} else if reflect.Uint < k && k < reflect.Uintptr {
		return v.Uint() == 0
	} else if k == reflect.Float32 || k == reflect.Float64 {
		return v.Float() == 0
	} else if k == reflect.Array || k == reflect.Map || k == reflect.Slice || k == reflect.String {
		return v.Len() == 0
	} else if k == reflect.Interface || k == reflect.Ptr {
		return v.IsNil()
	}
	return false
}

func convertSlice(s string, name string, ft reflect.Type, fv reflect.Value) error {
	var err error
	et := ft.Elem()

	if et.Kind() == reflect.Ptr || et.Kind() == reflect.Struct {
		return convertJsonValue(s, name, fv)
	}

	ss := strings.Split(s, ",")

	if len(s) == 0 || len(ss) == 0 {
		return nil
	}

	fs := reflect.MakeSlice(ft, 0, len(ss))

	for _, si := range ss {
		ev := reflect.New(et).Elem()

		err = convertValue(et.Kind(), si, name, ev)
		if err != nil {
			return err
		}
		fs = reflect.Append(fs, ev)
	}

	fv.Set(fs)

	return nil
}

func convertJsonValue(s string, name string, fv reflect.Value) error {
	var err error
	d := StringToSlice(s)

	if fv.Kind() == reflect.Ptr {
		if fv.IsNil() {
			fv.Set(reflect.New(fv.Type().Elem()))
		}
	} else {
		fv = fv.Addr()
	}

	err = json.Unmarshal(d, fv.Interface())

	if err != nil {
		return fmt.Errorf("invalid json '%v': %v, %v", name, err.Error(), s)
	}

	return nil
}

func convertValue(kind reflect.Kind, s string, name string, fv reflect.Value) error {
	if !fv.CanAddr() {
		return fmt.Errorf("can not addr: %v", name)
	}

	if kind == reflect.String {
		fv.SetString(s)
		return nil
	}

	if kind == reflect.Bool {
		switch s {
		case "true":
			fv.SetBool(true)
		case "false":
			fv.SetBool(false)
		case "1":
			fv.SetBool(true)
		case "0":
			fv.SetBool(false)
		default:
			return fmt.Errorf("invalid bool: %v value=%v", name, s)
		}
		return nil
	}

	if reflect.Int <= kind && kind <= reflect.Int64 {
		i, err := strconv.ParseInt(s, 10, 64)
		if err != nil {
			return fmt.Errorf("invalid int: %v value=%v", name, s)
		}
		fv.SetInt(i)

	} else if reflect.Uint <= kind && kind <= reflect.Uint64 {
		i, err := strconv.ParseUint(s, 10, 64)
		if err != nil {
			return fmt.Errorf("invalid int: %v value=%v", name, s)
		}
		fv.SetUint(i)

	} else if reflect.Float32 == kind || kind == reflect.Float64 {
		i, err := strconv.ParseFloat(s, 64)

		if err != nil {
			return fmt.Errorf("invalid float: %v value=%v", name, s)
		}

		fv.SetFloat(i)
	} else {
		// not support or just ignore it?
		// return fmt.Errorf("type not support: field=%v(%v) value=%v(%v)", name, ft.Kind(), val, vt.Kind())
	}
	return nil
}

func parseTag(tag string) (string, string) {
	tags := strings.Split(tag, ",")

	if len(tags) <= 0 {
		return "", ""
	}

	if len(tags) == 1 {
		return tags[0], ""
	}

	return tags[0], tags[1]
}
