package exif

import (
	"errors"
	"fmt"
	"strconv"
	"strings"

	"encoding/binary"

	"github.com/dsoprea/go-logging"
)

type TagTypePrimitive uint16

func (typeType TagTypePrimitive) String() string {
	return TypeNames[typeType]
}

func (tagType TagTypePrimitive) Size() int {
	if tagType == TypeByte {
		return 1
	} else if tagType == TypeAscii || tagType == TypeAsciiNoNul {
		return 1
	} else if tagType == TypeShort {
		return 2
	} else if tagType == TypeLong {
		return 4
	} else if tagType == TypeRational {
		return 8
	} else if tagType == TypeSignedLong {
		return 4
	} else if tagType == TypeSignedRational {
		return 8
	} else {
		log.Panicf("can not determine tag-value size for type (%d): [%s]", tagType, TypeNames[tagType])

		// Never called.
		return 0
	}
}

const (
	TypeByte           TagTypePrimitive = 1
	TypeAscii          TagTypePrimitive = 2
	TypeShort          TagTypePrimitive = 3
	TypeLong           TagTypePrimitive = 4
	TypeRational       TagTypePrimitive = 5
	TypeUndefined      TagTypePrimitive = 7
	TypeSignedLong     TagTypePrimitive = 9
	TypeSignedRational TagTypePrimitive = 10

	// TypeAsciiNoNul is just a pseudo-type, for our own purposes.
	TypeAsciiNoNul TagTypePrimitive = 0xf0
)

var (
	typeLogger = log.NewLogger("exif.type")
)

var (
	// TODO(dustin): Rename TypeNames() to typeNames() and add getter.
	TypeNames = map[TagTypePrimitive]string{
		TypeByte:           "BYTE",
		TypeAscii:          "ASCII",
		TypeShort:          "SHORT",
		TypeLong:           "LONG",
		TypeRational:       "RATIONAL",
		TypeUndefined:      "UNDEFINED",
		TypeSignedLong:     "SLONG",
		TypeSignedRational: "SRATIONAL",

		TypeAsciiNoNul: "_ASCII_NO_NUL",
	}

	TypeNamesR = map[string]TagTypePrimitive{}
)

var (
	// ErrNotEnoughData is used when there isn't enough data to accomodate what
	// we're trying to parse (sizeof(type) * unit_count).
	ErrNotEnoughData = errors.New("not enough data for type")

	// ErrWrongType is used when we try to parse anything other than the
	// current type.
	ErrWrongType = errors.New("wrong type, can not parse")

	// ErrUnhandledUnknownTag is used when we try to parse a tag that's
	// recorded as an "unknown" type but not a documented tag (therefore
	// leaving us not knowning how to read it).
	ErrUnhandledUnknownTypedTag = errors.New("not a standard unknown-typed tag")
)

type Rational struct {
	Numerator   uint32
	Denominator uint32
}

type SignedRational struct {
	Numerator   int32
	Denominator int32
}

func TagTypeSize(tagType TagTypePrimitive) int {

	// DEPRECATED(dustin): `(TagTypePrimitive).Size()` should be used, directly.

	return tagType.Size()
}

// Format returns a stringified value for the given bytes. Automatically
// calculates count based on type size.
func Format(rawBytes []byte, tagType TagTypePrimitive, justFirst bool, byteOrder binary.ByteOrder) (value string, err error) {
	defer func() {
		if state := recover(); state != nil {
			err = log.Wrap(state.(error))
		}
	}()

	// TODO(dustin): !! Add tests

	typeSize := tagType.Size()

	if len(rawBytes)%typeSize != 0 {
		log.Panicf("byte-count (%d) does not align for [%s] type with a size of (%d) bytes", len(rawBytes), TypeNames[tagType], typeSize)
	}

	// unitCount is the calculated unit-count. This should equal the original
	// value from the tag (pre-resolution).
	unitCount := uint32(len(rawBytes) / typeSize)

	// Truncate the items if it's not bytes or a string and we just want the first.

	valueSuffix := ""
	if justFirst == true && unitCount > 1 && tagType != TypeByte && tagType != TypeAscii && tagType != TypeAsciiNoNul {
		unitCount = 1
		valueSuffix = "..."
	}

	if tagType == TypeByte {
		items, err := parser.ParseBytes(rawBytes, unitCount)
		log.PanicIf(err)

		return DumpBytesToString(items), nil
	} else if tagType == TypeAscii {
		phrase, err := parser.ParseAscii(rawBytes, unitCount)
		log.PanicIf(err)

		return phrase, nil
	} else if tagType == TypeAsciiNoNul {
		phrase, err := parser.ParseAsciiNoNul(rawBytes, unitCount)
		log.PanicIf(err)

		return phrase, nil
	} else if tagType == TypeShort {
		items, err := parser.ParseShorts(rawBytes, unitCount, byteOrder)
		log.PanicIf(err)

		if len(items) > 0 {
			if justFirst == true {
				return fmt.Sprintf("%v%s", items[0], valueSuffix), nil
			} else {
				return fmt.Sprintf("%v", items), nil
			}
		} else {
			return "", nil
		}
	} else if tagType == TypeLong {
		items, err := parser.ParseLongs(rawBytes, unitCount, byteOrder)
		log.PanicIf(err)

		if len(items) > 0 {
			if justFirst == true {
				return fmt.Sprintf("%v%s", items[0], valueSuffix), nil
			} else {
				return fmt.Sprintf("%v", items), nil
			}
		} else {
			return "", nil
		}
	} else if tagType == TypeRational {
		items, err := parser.ParseRationals(rawBytes, unitCount, byteOrder)
		log.PanicIf(err)

		if len(items) > 0 {
			parts := make([]string, len(items))
			for i, r := range items {
				parts[i] = fmt.Sprintf("%d/%d", r.Numerator, r.Denominator)
			}

			if justFirst == true {
				return fmt.Sprintf("%v%s", parts[0], valueSuffix), nil
			} else {
				return fmt.Sprintf("%v", parts), nil
			}
		} else {
			return "", nil
		}
	} else if tagType == TypeSignedLong {
		items, err := parser.ParseSignedLongs(rawBytes, unitCount, byteOrder)
		log.PanicIf(err)

		if len(items) > 0 {
			if justFirst == true {
				return fmt.Sprintf("%v%s", items[0], valueSuffix), nil
			} else {
				return fmt.Sprintf("%v", items), nil
			}
		} else {
			return "", nil
		}
	} else if tagType == TypeSignedRational {
		items, err := parser.ParseSignedRationals(rawBytes, unitCount, byteOrder)
		log.PanicIf(err)

		parts := make([]string, len(items))
		for i, r := range items {
			parts[i] = fmt.Sprintf("%d/%d", r.Numerator, r.Denominator)
		}

		if len(items) > 0 {
			if justFirst == true {
				return fmt.Sprintf("%v%s", parts[0], valueSuffix), nil
			} else {
				return fmt.Sprintf("%v", parts), nil
			}
		} else {
			return "", nil
		}
	} else {
		// Affects only "unknown" values, in general.
		log.Panicf("value of type [%s] can not be formatted into string", tagType.String())

		// Never called.
		return "", nil
	}
}

func EncodeStringToBytes(tagType TagTypePrimitive, valueString string) (value interface{}, err error) {
	defer func() {
		if state := recover(); state != nil {
			err = log.Wrap(state.(error))
		}
	}()

	if tagType == TypeUndefined {
		// TODO(dustin): Circle back to this.
		log.Panicf("undefined-type values are not supported")
	}

	if tagType == TypeByte {
		return []byte(valueString), nil
	} else if tagType == TypeAscii || tagType == TypeAsciiNoNul {
		// Whether or not we're putting an NUL on the end is only relevant for
		// byte-level encoding. This function really just supports a user
		// interface.

		return valueString, nil
	} else if tagType == TypeShort {
		n, err := strconv.ParseUint(valueString, 10, 16)
		log.PanicIf(err)

		return uint16(n), nil
	} else if tagType == TypeLong {
		n, err := strconv.ParseUint(valueString, 10, 32)
		log.PanicIf(err)

		return uint32(n), nil
	} else if tagType == TypeRational {
		parts := strings.SplitN(valueString, "/", 2)

		numerator, err := strconv.ParseUint(parts[0], 10, 32)
		log.PanicIf(err)

		denominator, err := strconv.ParseUint(parts[1], 10, 32)
		log.PanicIf(err)

		return Rational{
			Numerator:   uint32(numerator),
			Denominator: uint32(denominator),
		}, nil
	} else if tagType == TypeSignedLong {
		n, err := strconv.ParseInt(valueString, 10, 32)
		log.PanicIf(err)

		return int32(n), nil
	} else if tagType == TypeSignedRational {
		parts := strings.SplitN(valueString, "/", 2)

		numerator, err := strconv.ParseInt(parts[0], 10, 32)
		log.PanicIf(err)

		denominator, err := strconv.ParseInt(parts[1], 10, 32)
		log.PanicIf(err)

		return SignedRational{
			Numerator:   int32(numerator),
			Denominator: int32(denominator),
		}, nil
	}

	log.Panicf("from-string encoding for type not supported; this shouldn't happen: [%s]", tagType.String())
	return nil, nil
}

func init() {
	for typeId, typeName := range TypeNames {
		TypeNamesR[typeName] = typeId
	}
}