package conf

import (
	"encoding"
	"fmt"
	"reflect"
	"strconv"
	"strings"
	"time"
	"unicode"
)

// Field maintains information about a field in the configuration struct.
type Field struct {
	Name    string
	FlagKey []string
	EnvKey  []string
	Field   reflect.Value
	Options FieldOptions

	// Important for flag parsing or any other source where
	// booleans might be treated specially.
	BoolField bool
}

// FieldOptions maintain flag options for a given field.
type FieldOptions struct {
	Help          string
	DefaultVal    string
	EnvName       string
	FlagName      string
	ShortFlagChar rune
	Noprint       bool
	Required      bool
	Mask          bool
}

// extractFields uses reflection to examine the struct and generate the keys.
func extractFields(prefix []string, target interface{}) ([]Field, error) {
	if prefix == nil {
		prefix = []string{}
	}
	s := reflect.ValueOf(target)

	if s.Kind() != reflect.Ptr {
		return nil, ErrInvalidStruct
	}
	s = s.Elem()
	if s.Kind() != reflect.Struct {
		return nil, ErrInvalidStruct
	}
	targetType := s.Type()

	var fields []Field

	for i := 0; i < s.NumField(); i++ {
		f := s.Field(i)
		structField := targetType.Field(i)

		// Get the conf tags associated with this item (if any).
		fieldTags := structField.Tag.Get("conf")

		// If it's ignored or can't be set, move on.
		if !f.CanSet() || fieldTags == "-" {
			continue
		}

		fieldName := structField.Name

		// Get and options.  TODO: Need more.
		fieldOpts, err := parseTag(fieldTags)
		if err != nil {
			return nil, fmt.Errorf("conf: error parsing tags for field %s: %s", fieldName, err)
		}

		// Generate the field key. This could be ignored.
		fieldKey := append(prefix, camelSplit(fieldName)...)

		// Drill down through pointers until we bottom out at type or nil.
		for f.Kind() == reflect.Ptr {
			if f.IsNil() {

				// It's not a struct so leave it alone.
				if f.Type().Elem().Kind() != reflect.Struct {
					break
				}

				// It is a struct so zero it out.
				f.Set(reflect.New(f.Type().Elem()))
			}
			f = f.Elem()
		}

		switch {

		// If we've found a struct, drill down, appending fields as we go.
		case f.Kind() == reflect.Struct:

			// Skip if it can deserialize itself.
			if setterFrom(f) == nil && textUnmarshaler(f) == nil && binaryUnmarshaler(f) == nil {

				// Prefix for any subkeys is the fieldKey, unless it's
				// anonymous, then it's just the prefix so far.
				innerPrefix := fieldKey
				if structField.Anonymous {
					innerPrefix = prefix
				}

				embeddedPtr := f.Addr().Interface()
				innerFields, err := extractFields(innerPrefix, embeddedPtr)
				if err != nil {
					return nil, err
				}
				fields = append(fields, innerFields...)
			}
		default:
			envKey := fieldKey
			if fieldOpts.EnvName != "" {
				envKey = strings.Split(fieldOpts.EnvName, "_")
			}

			flagKey := fieldKey
			if fieldOpts.FlagName != "" {
				flagKey = strings.Split(fieldOpts.FlagName, "-")
			}

			fld := Field{
				Name:      fieldName,
				EnvKey:    envKey,
				FlagKey:   flagKey,
				Field:     f,
				Options:   fieldOpts,
				BoolField: f.Kind() == reflect.Bool,
			}
			fields = append(fields, fld)
		}
	}

	return fields, nil
}

func parseTag(tagStr string) (FieldOptions, error) {
	var f FieldOptions
	if tagStr == "" {
		return f, nil
	}

	tagParts := strings.Split(tagStr, ",")
	for _, tagPart := range tagParts {
		vals := strings.SplitN(tagPart, ":", 2)
		tagProp := vals[0]

		switch len(vals) {
		case 1:
			switch tagProp {
			case "noprint":
				f.Noprint = true
			case "required":
				f.Required = true
			case "mask":
				f.Mask = true
			}
		case 2:
			tagPropVal := strings.TrimSpace(vals[1])
			if tagPropVal == "" {
				return f, fmt.Errorf("tag %q missing a value", tagProp)
			}
			switch tagProp {
			case "short":
				if len([]rune(tagPropVal)) != 1 {
					return f, fmt.Errorf("short value must be a single rune, got %q", tagPropVal)
				}
				f.ShortFlagChar = []rune(tagPropVal)[0]
			case "default":
				f.DefaultVal = tagPropVal
			case "env":
				f.EnvName = tagPropVal
			case "flag":
				f.FlagName = tagPropVal
			case "help":
				f.Help = tagPropVal
			}
		default:
			// TODO: Do we check for integrity issues here?
		}
	}

	// Perform a sanity check.
	switch {
	case f.Required && f.DefaultVal != "":
		return f, fmt.Errorf("cannot set both `required` and `default`")
	}

	return f, nil
}

// camelSplit takes a string based on camel case and splits it.
func camelSplit(src string) []string {
	if src == "" {
		return []string{}
	}
	if len(src) < 2 {
		return []string{src}
	}

	runes := []rune(src)

	lastClass := charClass(runes[0])
	lastIdx := 0
	out := []string{}

	// Split into fields based on class of unicode character.
	for i, r := range runes {
		class := charClass(r)

		// If the class has transitioned.
		if class != lastClass {

			// If going from uppercase to lowercase, we want to retain the last
			// uppercase letter for names like FOOBar, which should split to
			// FOO Bar.
			switch {
			case lastClass == classUpper && class != classNumber:
				if i-lastIdx > 1 {
					out = append(out, string(runes[lastIdx:i-1]))
					lastIdx = i - 1
				}
			default:
				out = append(out, string(runes[lastIdx:i]))
				lastIdx = i
			}
		}

		if i == len(runes)-1 {
			out = append(out, string(runes[lastIdx:]))
		}
		lastClass = class
	}

	return out
}

func processField(value string, field reflect.Value) error {
	typ := field.Type()

	// Look for a Set method.
	setter := setterFrom(field)
	if setter != nil {
		return setter.Set(value)
	}

	if t := textUnmarshaler(field); t != nil {
		return t.UnmarshalText([]byte(value))
	}

	if b := binaryUnmarshaler(field); b != nil {
		return b.UnmarshalBinary([]byte(value))
	}

	if typ.Kind() == reflect.Ptr {
		typ = typ.Elem()
		if field.IsNil() {
			field.Set(reflect.New(typ))
		}
		field = field.Elem()
	}

	switch typ.Kind() {
	case reflect.String:
		field.SetString(value)
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		var (
			val int64
			err error
		)
		if field.Kind() == reflect.Int64 && typ.PkgPath() == "time" && typ.Name() == "Duration" {
			var d time.Duration
			d, err = time.ParseDuration(value)
			val = int64(d)
		} else {
			val, err = strconv.ParseInt(value, 0, typ.Bits())
		}
		if err != nil {
			return err
		}

		field.SetInt(val)
	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
		val, err := strconv.ParseUint(value, 0, typ.Bits())
		if err != nil {
			return err
		}
		field.SetUint(val)
	case reflect.Bool:
		val, err := strconv.ParseBool(value)
		if err != nil {
			return err
		}
		field.SetBool(val)
	case reflect.Float32, reflect.Float64:
		val, err := strconv.ParseFloat(value, typ.Bits())
		if err != nil {
			return err
		}
		field.SetFloat(val)
	case reflect.Slice:
		vals := strings.Split(value, ";")
		sl := reflect.MakeSlice(typ, len(vals), len(vals))
		for i, val := range vals {
			err := processField(val, sl.Index(i))
			if err != nil {
				return err
			}
		}
		field.Set(sl)
	case reflect.Map:
		mp := reflect.MakeMap(typ)
		if len(strings.TrimSpace(value)) != 0 {
			pairs := strings.Split(value, ";")
			for _, pair := range pairs {
				kvpair := strings.Split(pair, ":")
				if len(kvpair) != 2 {
					return fmt.Errorf("invalid map item: %q", pair)
				}
				k := reflect.New(typ.Key()).Elem()
				err := processField(kvpair[0], k)
				if err != nil {
					return err
				}
				v := reflect.New(typ.Elem()).Elem()
				err = processField(kvpair[1], v)
				if err != nil {
					return err
				}
				mp.SetMapIndex(k, v)
			}
		}
		field.Set(mp)
	}
	return nil
}

func interfaceFrom(field reflect.Value, fn func(interface{}, *bool)) {

	// It may be impossible for a struct field to fail this check.
	if !field.CanInterface() {
		return
	}

	var ok bool
	fn(field.Interface(), &ok)
	if !ok && field.CanAddr() {
		fn(field.Addr().Interface(), &ok)
	}
}

// Setter is implemented by types can self-deserialize values.
// Any type that implements flag.Value also implements Setter.
type Setter interface {
	Set(value string) error
}

func setterFrom(field reflect.Value) (s Setter) {
	interfaceFrom(field, func(v interface{}, ok *bool) { s, *ok = v.(Setter) })
	return s
}

func textUnmarshaler(field reflect.Value) (t encoding.TextUnmarshaler) {
	interfaceFrom(field, func(v interface{}, ok *bool) { t, *ok = v.(encoding.TextUnmarshaler) })
	return t
}

func binaryUnmarshaler(field reflect.Value) (b encoding.BinaryUnmarshaler) {
	interfaceFrom(field, func(v interface{}, ok *bool) { b, *ok = v.(encoding.BinaryUnmarshaler) })
	return b
}

const (
	classLower int = iota
	classUpper
	classNumber
	classOther
)

func charClass(r rune) int {
	switch {
	case unicode.IsLower(r):
		return classLower
	case unicode.IsUpper(r):
		return classUpper
	case unicode.IsDigit(r):
		return classNumber
	}
	return classOther
}
