package encoding

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

func convertStruct(i interface{}, t reflect.Type, pointer bool) (reflect.Value, error) {
	m, ok := i.(map[string]interface{})
	if !ok {
		return zeroValue, fmt.Errorf("Cannot convert %T to struct", i)
	}

	out := reflect.New(t)

	err := Unstringify(m, out.Interface())
	if err != nil {
		return zeroValue, err
	}

	if !pointer {
		out = out.Elem()
	}

	return out, nil
}

func convertSlice(i interface{}, t reflect.Type, pointer bool) (reflect.Value, error) {
	s, ok := i.([]interface{})
	if !ok {
		return zeroValue, fmt.Errorf("Cannot convert %T to slice", i)
	}

	out := reflect.New(t)
	out.Elem().Set(reflect.MakeSlice(t, len(s), len(s)))

	for j, v := range s {
		val, err := convertType(t.Elem(), v)
		if err != nil {
			return zeroValue, err
		}

		out.Elem().Index(j).Set(val)
	}

	if !pointer {
		out = out.Elem()
	}

	return out, nil
}

func convertMap(i interface{}, t reflect.Type, pointer bool) (reflect.Value, error) {
	m, ok := i.(map[string]interface{})
	if !ok {
		return zeroValue, fmt.Errorf("Cannot convert %T to map with string keys", i)
	}

	out := reflect.New(t)
	out.Elem().Set(reflect.MakeMap(t))

	for k, v := range m {
		val, err := convertType(t.Elem(), v)
		if err != nil {
			return zeroValue, err
		}

		out.Elem().SetMapIndex(reflect.ValueOf(k), val)
	}

	if !pointer {
		out = out.Elem()
	}

	return out, nil
}

func convertString(i interface{}, pointer bool) (reflect.Value, error) {
	s, ok := i.(string)

	if !ok {
		return zeroValue, fmt.Errorf("Cannot convert %T to string", i)
	}

	if pointer {
		return reflect.ValueOf(&s), nil
	}

	return reflect.ValueOf(s), nil
}

func convertBool(i interface{}, pointer bool) (reflect.Value, error) {
	var b bool
	var err error

	switch v := i.(type) {
	case bool:
		b = v

	case string:
		b, err = strconv.ParseBool(v)
		if err != nil {
			return zeroValue, err
		}

	default:
		return zeroValue, fmt.Errorf("Cannot convert %T to bool", i)
	}

	if pointer {
		return reflect.ValueOf(&b), nil
	}

	return reflect.ValueOf(b), nil
}

func convertInt(i interface{}, pointer bool) (reflect.Value, error) {
	var n int

	switch v := i.(type) {
	case int:
		n = v

	case float64:
		n = int(v)

	case string:
		n64, err := strconv.ParseInt(v, 0, 32)
		if err != nil {
			return zeroValue, err
		}

		n = int(n64)

	default:
		return zeroValue, fmt.Errorf("Cannot convert %T to bool", i)
	}

	if pointer {
		return reflect.ValueOf(&n), nil
	}

	return reflect.ValueOf(n), nil
}

func convertFloat64(i interface{}, pointer bool) (reflect.Value, error) {
	var f float64
	var err error

	switch v := i.(type) {
	case float64:
		f = v

	case int:
		f = float64(v)

	case string:
		f, err = strconv.ParseFloat(v, 64)
		if err != nil {
			return zeroValue, err
		}

	default:
		return zeroValue, fmt.Errorf("Cannot convert %T to bool", i)
	}

	if pointer {
		return reflect.ValueOf(&f), nil
	}

	return reflect.ValueOf(f), nil
}

func convertType(t reflect.Type, i interface{}) (reflect.Value, error) {
	pointer := false
	if t.Kind() == reflect.Ptr {
		pointer = true
		t = t.Elem()
	}

	switch t.Kind() {
	case reflect.Struct:
		return convertStruct(i, t, pointer)

	case reflect.Slice:
		return convertSlice(i, t, pointer)

	case reflect.Map:
		return convertMap(i, t, pointer)

	case reflect.String:
		return convertString(i, pointer)

	case reflect.Bool:
		return convertBool(i, pointer)

	case reflect.Int:
		return convertInt(i, pointer)

	case reflect.Float64:
		return convertFloat64(i, pointer)

	default:
		return zeroValue, fmt.Errorf("Unsupported type %v", t)
	}
}

// Unstringify takes a stringified representation of a value
// and populates it into the supplied interface
func Unstringify(data map[string]interface{}, v interface{}) error {
	t := reflect.TypeOf(v).Elem()

	val := reflect.ValueOf(v).Elem()

	for i := 0; i < t.NumField(); i++ {
		f := t.Field(i)

		jsonName := f.Name
		jsonTag := strings.Split(f.Tag.Get("json"), ",")
		if len(jsonTag) > 0 && jsonTag[0] != "" {
			jsonName = jsonTag[0]
		}

		if value, ok := data[jsonName]; ok {
			newValue, err := convertType(f.Type, value)
			if err != nil {
				return err
			}

			val.FieldByName(f.Name).Set(newValue)
		}
	}

	return nil
}
