package flatmap

import (
	"fmt"
	"reflect"
)

// Flatten takes an object and returns a flat map of the object. The keys of the
// map is the path of the field names until a primitive field is reached and the
// value is a string representation of the terminal field.
func Flatten(obj interface{}, filter []string, primitiveOnly bool) map[string]string {
	flat := make(map[string]string)
	v := reflect.ValueOf(obj)
	if !v.IsValid() {
		return nil
	}

	flatten("", v, primitiveOnly, false, flat)
	for _, f := range filter {
		delete(flat, f)
	}
	return flat
}

// flatten recursively calls itself to create a flatmap representation of the
// passed value. The results are stored into the output map and the keys are
// the fields prepended with the passed prefix.
// XXX: A current restriction is that maps only support string keys.
func flatten(prefix string, v reflect.Value, primitiveOnly, enteredStruct bool, output map[string]string) {
	switch v.Kind() {
	case reflect.Bool:
		output[prefix] = fmt.Sprintf("%v", v.Bool())
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		output[prefix] = fmt.Sprintf("%v", v.Int())
	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
		output[prefix] = fmt.Sprintf("%v", v.Uint())
	case reflect.Float32, reflect.Float64:
		output[prefix] = fmt.Sprintf("%v", v.Float())
	case reflect.Complex64, reflect.Complex128:
		output[prefix] = fmt.Sprintf("%v", v.Complex())
	case reflect.String:
		output[prefix] = fmt.Sprintf("%v", v.String())
	case reflect.Invalid:
		output[prefix] = "nil"
	case reflect.Ptr:
		if primitiveOnly && enteredStruct {
			return
		}

		e := v.Elem()
		if !e.IsValid() {
			output[prefix] = "nil"
		}
		flatten(prefix, e, primitiveOnly, enteredStruct, output)
	case reflect.Map:
		for _, k := range v.MapKeys() {
			if k.Kind() == reflect.Interface {
				k = k.Elem()
			}

			if k.Kind() != reflect.String {
				panic(fmt.Sprintf("%q: map key is not string: %s", prefix, k))
			}

			flatten(getSubKeyPrefix(prefix, k.String()), v.MapIndex(k), primitiveOnly, enteredStruct, output)
		}
	case reflect.Struct:
		if primitiveOnly && enteredStruct {
			return
		}
		enteredStruct = true

		t := v.Type()
		for i := 0; i < v.NumField(); i++ {
			name := t.Field(i).Name
			val := v.Field(i)
			if val.Kind() == reflect.Interface && !val.IsNil() {
				val = val.Elem()
			}

			flatten(getSubPrefix(prefix, name), val, primitiveOnly, enteredStruct, output)
		}
	case reflect.Interface:
		if primitiveOnly {
			return
		}

		e := v.Elem()
		if !e.IsValid() {
			output[prefix] = "nil"
			return
		}
		flatten(prefix, e, primitiveOnly, enteredStruct, output)
	case reflect.Array, reflect.Slice:
		if primitiveOnly {
			return
		}

		if v.Kind() == reflect.Slice && v.IsNil() {
			output[prefix] = "nil"
			return
		}
		for i := 0; i < v.Len(); i++ {
			flatten(fmt.Sprintf("%s[%d]", prefix, i), v.Index(i), primitiveOnly, enteredStruct, output)
		}
	default:
		panic(fmt.Sprintf("prefix %q; unsupported type %v", prefix, v.Kind()))
	}
}

// getSubPrefix takes the current prefix and the next subfield and returns an
// appropriate prefix.
func getSubPrefix(curPrefix, subField string) string {
	if curPrefix != "" {
		return fmt.Sprintf("%s.%s", curPrefix, subField)
	}
	return subField
}

// getSubKeyPrefix takes the current prefix and the next subfield and returns an
// appropriate prefix for a map field.
func getSubKeyPrefix(curPrefix, subField string) string {
	if curPrefix != "" {
		return fmt.Sprintf("%s[%s]", curPrefix, subField)
	}
	return subField
}
