package plist

import (
	"encoding"
	"fmt"
	"reflect"
	"runtime"
	"time"
)

type incompatibleDecodeTypeError struct {
	dest reflect.Type
	src  string // type name (from cfValue)
}

func (u *incompatibleDecodeTypeError) Error() string {
	return fmt.Sprintf("plist: type mismatch: tried to decode plist type `%v' into value of type `%v'", u.src, u.dest)
}

var (
	plistUnmarshalerType = reflect.TypeOf((*Unmarshaler)(nil)).Elem()
	textUnmarshalerType  = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
	uidType              = reflect.TypeOf(UID(0))
)

func isEmptyInterface(v reflect.Value) bool {
	return v.Kind() == reflect.Interface && v.NumMethod() == 0
}

func (p *Decoder) unmarshalPlistInterface(pval cfValue, unmarshalable Unmarshaler) {
	err := unmarshalable.UnmarshalPlist(func(i interface{}) (err error) {
		defer func() {
			if r := recover(); r != nil {
				if _, ok := r.(runtime.Error); ok {
					panic(r)
				}
				err = r.(error)
			}
		}()
		p.unmarshal(pval, reflect.ValueOf(i))
		return
	})

	if err != nil {
		panic(err)
	}
}

func (p *Decoder) unmarshalTextInterface(pval cfString, unmarshalable encoding.TextUnmarshaler) {
	err := unmarshalable.UnmarshalText([]byte(pval))
	if err != nil {
		panic(err)
	}
}

func (p *Decoder) unmarshalTime(pval cfDate, val reflect.Value) {
	val.Set(reflect.ValueOf(time.Time(pval)))
}

func (p *Decoder) unmarshalLaxString(s string, val reflect.Value) {
	switch val.Kind() {
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		i := mustParseInt(s, 10, 64)
		val.SetInt(i)
		return
	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
		i := mustParseUint(s, 10, 64)
		val.SetUint(i)
		return
	case reflect.Float32, reflect.Float64:
		f := mustParseFloat(s, 64)
		val.SetFloat(f)
		return
	case reflect.Bool:
		b := mustParseBool(s)
		val.SetBool(b)
		return
	case reflect.Struct:
		if val.Type() == timeType {
			t, err := time.Parse(textPlistTimeLayout, s)
			if err != nil {
				panic(err)
			}
			val.Set(reflect.ValueOf(t.In(time.UTC)))
			return
		}
		fallthrough
	default:
		panic(&incompatibleDecodeTypeError{val.Type(), "string"})
	}
}

func (p *Decoder) unmarshal(pval cfValue, val reflect.Value) {
	if pval == nil {
		return
	}

	if val.Kind() == reflect.Ptr {
		if val.IsNil() {
			val.Set(reflect.New(val.Type().Elem()))
		}
		val = val.Elem()
	}

	if isEmptyInterface(val) {
		v := p.valueInterface(pval)
		val.Set(reflect.ValueOf(v))
		return
	}

	incompatibleTypeError := &incompatibleDecodeTypeError{val.Type(), pval.typeName()}

	// time.Time implements TextMarshaler, but we need to parse it as RFC3339
	if date, ok := pval.(cfDate); ok {
		if val.Type() == timeType {
			p.unmarshalTime(date, val)
			return
		}
		panic(incompatibleTypeError)
	}

	if receiver, can := implementsInterface(val, plistUnmarshalerType); can {
		p.unmarshalPlistInterface(pval, receiver.(Unmarshaler))
		return
	}

	if val.Type() != timeType {
		if receiver, can := implementsInterface(val, textUnmarshalerType); can {
			if str, ok := pval.(cfString); ok {
				p.unmarshalTextInterface(str, receiver.(encoding.TextUnmarshaler))
			} else {
				panic(incompatibleTypeError)
			}
			return
		}
	}

	typ := val.Type()

	switch pval := pval.(type) {
	case cfString:
		if val.Kind() == reflect.String {
			val.SetString(string(pval))
			return
		}
		if p.lax {
			p.unmarshalLaxString(string(pval), val)
			return
		}

		panic(incompatibleTypeError)
	case *cfNumber:
		switch val.Kind() {
		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
			val.SetInt(int64(pval.value))
		case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
			val.SetUint(pval.value)
		default:
			panic(incompatibleTypeError)
		}
	case *cfReal:
		if val.Kind() == reflect.Float32 || val.Kind() == reflect.Float64 {
			// TODO: Consider warning on a downcast (storing a 64-bit value in a 32-bit reflect)
			val.SetFloat(pval.value)
		} else {
			panic(incompatibleTypeError)
		}
	case cfBoolean:
		if val.Kind() == reflect.Bool {
			val.SetBool(bool(pval))
		} else {
			panic(incompatibleTypeError)
		}
	case cfData:
		if val.Kind() == reflect.Slice && typ.Elem().Kind() == reflect.Uint8 {
			val.SetBytes([]byte(pval))
		} else {
			panic(incompatibleTypeError)
		}
	case cfUID:
		if val.Type() == uidType {
			val.SetUint(uint64(pval))
		} else {
			switch val.Kind() {
			case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
				val.SetInt(int64(pval))
			case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
				val.SetUint(uint64(pval))
			default:
				panic(incompatibleTypeError)
			}
		}
	case *cfArray:
		p.unmarshalArray(pval, val)
	case *cfDictionary:
		p.unmarshalDictionary(pval, val)
	}
}

func (p *Decoder) unmarshalArray(a *cfArray, val reflect.Value) {
	var n int
	if val.Kind() == reflect.Slice {
		// Slice of element values.
		// Grow slice.
		cnt := len(a.values) + val.Len()
		if cnt >= val.Cap() {
			ncap := 2 * cnt
			if ncap < 4 {
				ncap = 4
			}
			new := reflect.MakeSlice(val.Type(), val.Len(), ncap)
			reflect.Copy(new, val)
			val.Set(new)
		}
		n = val.Len()
		val.SetLen(cnt)
	} else if val.Kind() == reflect.Array {
		if len(a.values) > val.Cap() {
			panic(fmt.Errorf("plist: attempted to unmarshal %d values into an array of size %d", len(a.values), val.Cap()))
		}
	} else {
		panic(&incompatibleDecodeTypeError{val.Type(), a.typeName()})
	}

	// Recur to read element into slice.
	for _, sval := range a.values {
		p.unmarshal(sval, val.Index(n))
		n++
	}
	return
}

func (p *Decoder) unmarshalDictionary(dict *cfDictionary, val reflect.Value) {
	typ := val.Type()
	switch val.Kind() {
	case reflect.Struct:
		tinfo, err := getTypeInfo(typ)
		if err != nil {
			panic(err)
		}

		entries := make(map[string]cfValue, len(dict.keys))
		for i, k := range dict.keys {
			sval := dict.values[i]
			entries[k] = sval
		}

		for _, finfo := range tinfo.fields {
			p.unmarshal(entries[finfo.name], finfo.value(val))
		}
	case reflect.Map:
		if val.IsNil() {
			val.Set(reflect.MakeMap(typ))
		}

		for i, k := range dict.keys {
			sval := dict.values[i]

			keyv := reflect.ValueOf(k).Convert(typ.Key())
			mapElem := val.MapIndex(keyv)
			if !mapElem.IsValid() {
				mapElem = reflect.New(typ.Elem()).Elem()
			}

			p.unmarshal(sval, mapElem)
			val.SetMapIndex(keyv, mapElem)
		}
	default:
		panic(&incompatibleDecodeTypeError{typ, dict.typeName()})
	}
}

/* *Interface is modelled after encoding/json */
func (p *Decoder) valueInterface(pval cfValue) interface{} {
	switch pval := pval.(type) {
	case cfString:
		return string(pval)
	case *cfNumber:
		if pval.signed {
			return int64(pval.value)
		}
		return pval.value
	case *cfReal:
		if pval.wide {
			return pval.value
		} else {
			return float32(pval.value)
		}
	case cfBoolean:
		return bool(pval)
	case *cfArray:
		return p.arrayInterface(pval)
	case *cfDictionary:
		return p.dictionaryInterface(pval)
	case cfData:
		return []byte(pval)
	case cfDate:
		return time.Time(pval)
	case cfUID:
		return UID(pval)
	}
	return nil
}

func (p *Decoder) arrayInterface(a *cfArray) []interface{} {
	out := make([]interface{}, len(a.values))
	for i, subv := range a.values {
		out[i] = p.valueInterface(subv)
	}
	return out
}

func (p *Decoder) dictionaryInterface(dict *cfDictionary) map[string]interface{} {
	out := make(map[string]interface{})
	for i, k := range dict.keys {
		subv := dict.values[i]
		out[k] = p.valueInterface(subv)
	}
	return out
}
