package stalecucumber

import "io"
import "reflect"
import "fmt"
import "errors"
import "strings"
import "math/big"
import "bytes"

const PICKLE_TAG = "pickle"

type UnpackingError struct {
	Source      interface{}
	Destination reflect.Value
	Err         error
}

/*
This type is returned when a call to From() fails.
Setting "AllowMissingFields" and "AllowMismatchedFields"
on the result of "UnpackInto" controls if this error is
returned or not.
*/
func (ue UnpackingError) Error() string {
	var dv string
	var dt string
	k := ue.Destination.Kind()
	switch k {
	case reflect.Ptr:
		dt = fmt.Sprintf("%s", ue.Destination.Type().Elem())
		if !ue.Destination.IsNil() {
			dv = fmt.Sprintf("%v", ue.Destination.Elem().Interface())
		} else {
			dv = "nil"

		}
	case reflect.Invalid:
		dv = "invalid"
		dt = dv
	default:
		dv = fmt.Sprintf("%v", ue.Destination.Interface())
		dt = fmt.Sprintf("%s", ue.Destination.Type())
	}

	return fmt.Sprintf("Error unpacking %v(%T) into %s(%s):%v",
		ue.Source,
		ue.Source,
		dv,
		dt,
		ue.Err)
}

var ErrNilPointer = errors.New("Destination cannot be a nil pointer")
var ErrNotPointer = errors.New("Destination must be a pointer type")
var ErrTargetTypeNotPointer = errors.New("Target type must be a pointer to unpack this value")
var ErrTargetTypeOverflow = errors.New("Value overflows target type")
var ErrTargetTypeMismatch = errors.New("Target type does not match source type")

type unpacker struct {
	dest                  reflect.Value
	AllowMissingFields    bool
	AllowMismatchedFields bool
}

func UnpackInto(dest interface{}) unpacker {
	return unpacker{dest: reflect.ValueOf(dest),
		AllowMissingFields:    true,
		AllowMismatchedFields: false}
}

func (u unpacker) From(srcI interface{}, err error) error {
	//Check if an error occurred
	if err != nil {
		return err
	}

	return u.from(srcI)
}

func (u unpacker) from(srcI interface{}) error {

	//Get the value of the destination
	v := u.dest

	//The destination must always be a pointer
	if v.Kind() != reflect.Ptr {
		return UnpackingError{Source: srcI,
			Destination: u.dest,
			Err:         ErrNotPointer}
	}

	//The destination can never be nil
	if v.IsNil() {
		return UnpackingError{Source: srcI,
			Destination: u.dest,
			Err:         ErrNilPointer}

	}

	//Indirect the destination. This gets the actual
	//value pointed at
	vIndirect := v
	for vIndirect.Kind() == reflect.Ptr {
		if vIndirect.IsNil() {
			vIndirect.Set(reflect.New(vIndirect.Type().Elem()))
		}
		vIndirect = vIndirect.Elem()
	}

	//Check the input against known types
	switch s := srcI.(type) {
	default:
		return UnpackingError{Source: srcI,
			Destination: u.dest,
			Err:         errors.New("Unknown source type")}
	case PickleNone:
		vElem := v.Elem()
		for vElem.Kind() == reflect.Ptr {
			next := vElem.Elem()
			if next.Kind() == reflect.Ptr {
				vElem = next
				continue
			}

			if vElem.CanSet() {
				vElem.Set(reflect.Zero(vElem.Type()))
				return nil
			}
		}

		return UnpackingError{Source: srcI,
			Destination: u.dest,
			Err:         ErrTargetTypeNotPointer}

	case int64:
		switch vIndirect.Kind() {
		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int64, reflect.Int32:
			if vIndirect.OverflowInt(s) {
				return UnpackingError{Source: srcI,
					Destination: u.dest,
					Err:         ErrTargetTypeOverflow}
			}
			vIndirect.SetInt(s)
			return nil
		case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
			if s < 0 || vIndirect.OverflowUint(uint64(s)) {
				return UnpackingError{Source: srcI,
					Destination: u.dest,
					Err:         ErrTargetTypeOverflow}
			}

			vIndirect.SetUint(uint64(s))
			return nil
		}

		dstBig, ok := vIndirect.Addr().Interface().(*big.Int)
		if ok {
			dstBig.SetInt64(s)
			return nil
		}

	case string:
		switch vIndirect.Kind() {
		case reflect.String:
			vIndirect.SetString(s)
			return nil
		}
	case bool:
		switch vIndirect.Kind() {
		case reflect.Bool:
			vIndirect.SetBool(s)
			return nil
		}
	case float64:
		switch vIndirect.Kind() {
		case reflect.Float32, reflect.Float64:
			vIndirect.SetFloat(s)
			return nil
		}
	case *big.Int:
		dstBig, ok := vIndirect.Addr().Interface().(*big.Int)
		if ok {
			dstBig.Set(s)
			return nil
		}

		if vi, err := Int(srcI, nil); err == nil {
			return unpacker{dest: v,
				AllowMismatchedFields: u.AllowMismatchedFields,
				AllowMissingFields:    u.AllowMissingFields}.From(vi, nil)
		}

	case io.Reader:		
		var readerType = reflect.TypeOf((*io.Reader)(nil)).Elem()
		// Check for exact match
		if vIndirect.Kind() == reflect.Interface && vIndirect.Type().Implements(readerType) { 
			vIndirect.Set(reflect.ValueOf(s))
			return nil
		}

		// Handle converting io.Reader to byte buffer if the caller passes that in
		var byteBufferType = reflect.TypeOf((*bytes.Buffer)(nil)).Elem()
		if vIndirect.Type() == byteBufferType {
			buf := &bytes.Buffer{}
			_, err := io.Copy(buf, s)
			if err != nil {
				return err
			}

			vIndirect.Set(reflect.ValueOf(*buf))
			return nil
		}		
		
	case []interface{}:
		//Check that the destination is a slice
		if vIndirect.Kind() != reflect.Slice {
			return UnpackingError{Source: s,
				Destination: u.dest,
				Err:         fmt.Errorf("Cannot unpack slice into destination")}
		}
		//Check for exact type match
		if vIndirect.Type().Elem().Kind() == reflect.Interface {
			vIndirect.Set(reflect.ValueOf(s))
			return nil
		}

		//Build the value using reflection
		var replacement reflect.Value
		if vIndirect.IsNil() || vIndirect.Len() < len(s) {
			replacement = reflect.MakeSlice(vIndirect.Type(),
				len(s), len(s))
		} else {
			replacement = vIndirect.Slice(0, len(s))
		}

		for i, srcV := range s {
			dstV := replacement.Index(i)

			//Recurse to set the value
			err := unpacker{dest: dstV.Addr(),
				AllowMissingFields:    u.AllowMissingFields,
				AllowMismatchedFields: u.AllowMismatchedFields}.
				From(srcV, nil)
			if err != nil {
				return err
			}
		}
		vIndirect.Set(replacement)
		return nil

	case map[interface{}]interface{}:
		//Check to see if the field is exactly
		//of the type
		if vIndirect.Kind() == reflect.Map {
			dstT := vIndirect.Type()
			if dstT.Key().Kind() == reflect.Interface &&
				dstT.Elem().Kind() == reflect.Interface {
				vIndirect.Set(reflect.ValueOf(s))
				return nil
			}
		}

		var src map[string]interface{}
		var err error
		src, err = DictString(srcI, err)
		if err != nil {
			return UnpackingError{Source: srcI,
				Destination: u.dest,
				Err:         fmt.Errorf("Cannot unpack source into struct")}
		}

		if vIndirect.Kind() != reflect.Struct {
			return UnpackingError{Source: src,
				Destination: u.dest,
				Err:         fmt.Errorf("Cannot unpack into %v", v.Kind().String())}

		}

		var fieldByTag map[string]int
		vIndirectType := reflect.TypeOf(vIndirect.Interface())
		numFields := vIndirectType.NumField()

		for i := 0; i != numFields; i++ {
			fv := vIndirectType.Field(i)
			tag := fv.Tag.Get(PICKLE_TAG)

			if len(tag) != 0 {
				if fieldByTag == nil {
					fieldByTag = make(map[string]int)
				}
				fieldByTag[tag] = i
			}
		}

		for k, kv := range src {
			var fv reflect.Value

			if fieldIndex, ok := fieldByTag[k]; ok {
				fv = vIndirect.Field(fieldIndex)
			} else {
				//Try the name verbatim. This catches
				//embedded fields as well
				fv = vIndirect.FieldByName(k)

				if !fv.IsValid() {
					//Capitalize the first character. Structs
					//do not export fields with a lower case
					//first character
					capk := strings.ToUpper(k[0:1]) + k[1:]
					fv = vIndirect.FieldByName(capk)
				}
			}

			if !fv.IsValid() || !fv.CanSet() {
				if !u.AllowMissingFields {
					return UnpackingError{Source: src,
						Destination: u.dest,
						Err:         fmt.Errorf("Cannot find field for key %q", k)}
				}
				continue
			}

			err := unpacker{dest: fv.Addr(),
				AllowMismatchedFields: u.AllowMismatchedFields,
				AllowMissingFields:    u.AllowMissingFields}.from(kv)

			if err != nil {
				if u.AllowMismatchedFields {
					if unpackingError, ok := err.(UnpackingError); ok {
						switch unpackingError.Err {
						case ErrTargetTypeOverflow,
							ErrTargetTypeNotPointer,
							ErrTargetTypeMismatch:

							fv.Set(reflect.Zero(fv.Type()))
							continue
						}

					}
				}
				return err
			}
		}

		return nil
	}

	return UnpackingError{Source: srcI,
		Destination: u.dest,
		Err:         ErrTargetTypeMismatch}
}
