package struc

import (
	"encoding/binary"
	"fmt"
	"io"
	"reflect"
	"strings"
)

type Fields []*Field

func (f Fields) SetByteOrder(order binary.ByteOrder) {
	for _, field := range f {
		if field != nil {
			field.Order = order
		}
	}
}

func (f Fields) String() string {
	fields := make([]string, len(f))
	for i, field := range f {
		if field != nil {
			fields[i] = field.String()
		}
	}
	return "{" + strings.Join(fields, ", ") + "}"
}

func (f Fields) Sizeof(val reflect.Value, options *Options) int {
	for val.Kind() == reflect.Ptr {
		val = val.Elem()
	}
	size := 0
	for i, field := range f {
		if field != nil {
			size += field.Size(val.Field(i), options)
		}
	}
	return size
}

func (f Fields) sizefrom(val reflect.Value, index []int) int {
	field := val.FieldByIndex(index)
	switch field.Kind() {
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		return int(field.Int())
	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
		n := int(field.Uint())
		// all the builtin array length types are native int
		// so this guards against weird truncation
		if n < 0 {
			return 0
		}
		return n
	default:
		name := val.Type().FieldByIndex(index).Name
		panic(fmt.Sprintf("sizeof field %T.%s not an integer type", val.Interface(), name))
	}
}

func (f Fields) Pack(buf []byte, val reflect.Value, options *Options) (int, error) {
	for val.Kind() == reflect.Ptr {
		val = val.Elem()
	}
	pos := 0
	for i, field := range f {
		if field == nil {
			continue
		}
		v := val.Field(i)
		length := field.Len
		if field.Sizefrom != nil {
			length = f.sizefrom(val, field.Sizefrom)
		}
		if length <= 0 && field.Slice {
			length = v.Len()
		}
		if field.Sizeof != nil {
			length := val.FieldByIndex(field.Sizeof).Len()
			switch field.kind {
			case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
				// allocating a new int here has fewer side effects (doesn't update the original struct)
				// but it's a wasteful allocation
				// the old method might work if we just cast the temporary int/uint to the target type
				v = reflect.New(v.Type()).Elem()
				v.SetInt(int64(length))
			case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
				v = reflect.New(v.Type()).Elem()
				v.SetUint(uint64(length))
			default:
				panic(fmt.Sprintf("sizeof field is not int or uint type: %s, %s", field.Name, v.Type()))
			}
		}
		if n, err := field.Pack(buf[pos:], v, length, options); err != nil {
			return n, err
		} else {
			pos += n
		}
	}
	return pos, nil
}

func (f Fields) Unpack(r io.Reader, val reflect.Value, options *Options) error {
	for val.Kind() == reflect.Ptr {
		val = val.Elem()
	}
	var tmp [8]byte
	var buf []byte
	for i, field := range f {
		if field == nil {
			continue
		}
		v := val.Field(i)
		length := field.Len
		if field.Sizefrom != nil {
			length = f.sizefrom(val, field.Sizefrom)
		}
		if v.Kind() == reflect.Ptr && !v.Elem().IsValid() {
			v.Set(reflect.New(v.Type().Elem()))
		}
		if field.Type == Struct {
			if field.Slice {
				vals := v
				if !field.Array {
					vals = reflect.MakeSlice(v.Type(), length, length)
				}
				for i := 0; i < length; i++ {
					v := vals.Index(i)
					fields, err := parseFields(v)
					if err != nil {
						return err
					}
					if err := fields.Unpack(r, v, options); err != nil {
						return err
					}
				}
				if !field.Array {
					v.Set(vals)
				}
			} else {
				// TODO: DRY (we repeat the inner loop above)
				fields, err := parseFields(v)
				if err != nil {
					return err
				}
				if err := fields.Unpack(r, v, options); err != nil {
					return err
				}
			}
			continue
		} else {
			typ := field.Type.Resolve(options)
			if typ == CustomType {
				if err := v.Addr().Interface().(Custom).Unpack(r, length, options); err != nil {
					return err
				}
			} else {
				size := length * field.Type.Resolve(options).Size()
				if size < 8 {
					buf = tmp[:size]
				} else {
					buf = make([]byte, size)
				}
				if _, err := io.ReadFull(r, buf); err != nil {
					return err
				}
				err := field.Unpack(buf[:size], v, length, options)
				if err != nil {
					return err
				}
			}
		}
	}
	return nil
}
