package tiff

import (
	"bytes"
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"math/big"
	"strings"
	"unicode"
	"unicode/utf8"
)

// Format specifies the Go type equivalent used to represent the basic
// tiff data types.
type Format int

const (
	IntVal Format = iota
	FloatVal
	RatVal
	StringVal
	UndefVal
	OtherVal
)

var ErrShortReadTagValue = errors.New("tiff: short read of tag value")

var formatNames = map[Format]string{
	IntVal:    "int",
	FloatVal:  "float",
	RatVal:    "rational",
	StringVal: "string",
	UndefVal:  "undefined",
	OtherVal:  "other",
}

// DataType represents the basic tiff tag data types.
type DataType uint16

const (
	DTByte      DataType = 1
	DTAscii              = 2
	DTShort              = 3
	DTLong               = 4
	DTRational           = 5
	DTSByte              = 6
	DTUndefined          = 7
	DTSShort             = 8
	DTSLong              = 9
	DTSRational          = 10
	DTFloat              = 11
	DTDouble             = 12
)

var typeNames = map[DataType]string{
	DTByte:      "byte",
	DTAscii:     "ascii",
	DTShort:     "short",
	DTLong:      "long",
	DTRational:  "rational",
	DTSByte:     "signed byte",
	DTUndefined: "undefined",
	DTSShort:    "signed short",
	DTSLong:     "signed long",
	DTSRational: "signed rational",
	DTFloat:     "float",
	DTDouble:    "double",
}

// typeSize specifies the size in bytes of each type.
var typeSize = map[DataType]uint32{
	DTByte:      1,
	DTAscii:     1,
	DTShort:     2,
	DTLong:      4,
	DTRational:  8,
	DTSByte:     1,
	DTUndefined: 1,
	DTSShort:    2,
	DTSLong:     4,
	DTSRational: 8,
	DTFloat:     4,
	DTDouble:    8,
}

// Tag reflects the parsed content of a tiff IFD tag.
type Tag struct {
	// Id is the 2-byte tiff tag identifier.
	Id uint16
	// Type is an integer (1 through 12) indicating the tag value's data type.
	Type DataType
	// Count is the number of type Type stored in the tag's value (i.e. the
	// tag's value is an array of type Type and length Count).
	Count uint32
	// Val holds the bytes that represent the tag's value.
	Val []byte
	// ValOffset holds byte offset of the tag value w.r.t. the beginning of the
	// reader it was decoded from. Zero if the tag value fit inside the offset
	// field.
	ValOffset uint32

	order     binary.ByteOrder
	intVals   []int64
	floatVals []float64
	ratVals   [][]int64
	strVal    string
	format    Format
}

// DecodeTag parses a tiff-encoded IFD tag from r and returns a Tag object. The
// first read from r should be the first byte of the tag. ReadAt offsets should
// generally be relative to the beginning of the tiff structure (not relative
// to the beginning of the tag).
func DecodeTag(r ReadAtReader, order binary.ByteOrder) (*Tag, error) {
	t := new(Tag)
	t.order = order

	err := binary.Read(r, order, &t.Id)
	if err != nil {
		return nil, errors.New("tiff: tag id read failed: " + err.Error())
	}

	err = binary.Read(r, order, &t.Type)
	if err != nil {
		return nil, errors.New("tiff: tag type read failed: " + err.Error())
	}

	err = binary.Read(r, order, &t.Count)
	if err != nil {
		return nil, errors.New("tiff: tag component count read failed: " + err.Error())
	}

	// There seems to be a relatively common corrupt tag which has a Count of
	// MaxUint32. This is probably not a valid value, so return early.
	if t.Count == 1<<32-1 {
		return t, errors.New("invalid Count offset in tag")
	}

	valLen := typeSize[t.Type] * t.Count
	if valLen == 0 {
		return t, errors.New("zero length tag value")
	}

	if valLen > 4 {
		binary.Read(r, order, &t.ValOffset)

		// Use a bytes.Buffer so we don't allocate a huge slice if the tag
		// is corrupt.
		var buff bytes.Buffer
		sr := io.NewSectionReader(r, int64(t.ValOffset), int64(valLen))
		n, err := io.Copy(&buff, sr)
		if err != nil {
			return t, errors.New("tiff: tag value read failed: " + err.Error())
		} else if n != int64(valLen) {
			return t, ErrShortReadTagValue
		}
		t.Val = buff.Bytes()

	} else {
		val := make([]byte, valLen)
		if _, err = io.ReadFull(r, val); err != nil {
			return t, errors.New("tiff: tag offset read failed: " + err.Error())
		}
		// ignore padding.
		if _, err = io.ReadFull(r, make([]byte, 4-valLen)); err != nil {
			return t, errors.New("tiff: tag offset read failed: " + err.Error())
		}

		t.Val = val
	}

	return t, t.convertVals()
}

func (t *Tag) convertVals() error {
	r := bytes.NewReader(t.Val)

	switch t.Type {
	case DTAscii:
		if len(t.Val) > 0 {
			t.strVal = string(t.Val[:len(t.Val)-1]) // ignore the last byte (NULL).
		}
	case DTByte:
		var v uint8
		t.intVals = make([]int64, int(t.Count))
		for i := range t.intVals {
			err := binary.Read(r, t.order, &v)
			if err != nil {
				return err
			}
			t.intVals[i] = int64(v)
		}
	case DTShort:
		var v uint16
		t.intVals = make([]int64, int(t.Count))
		for i := range t.intVals {
			err := binary.Read(r, t.order, &v)
			if err != nil {
				return err
			}
			t.intVals[i] = int64(v)
		}
	case DTLong:
		var v uint32
		t.intVals = make([]int64, int(t.Count))
		for i := range t.intVals {
			err := binary.Read(r, t.order, &v)
			if err != nil {
				return err
			}
			t.intVals[i] = int64(v)
		}
	case DTSByte:
		var v int8
		t.intVals = make([]int64, int(t.Count))
		for i := range t.intVals {
			err := binary.Read(r, t.order, &v)
			if err != nil {
				return err
			}
			t.intVals[i] = int64(v)
		}
	case DTSShort:
		var v int16
		t.intVals = make([]int64, int(t.Count))
		for i := range t.intVals {
			err := binary.Read(r, t.order, &v)
			if err != nil {
				return err
			}
			t.intVals[i] = int64(v)
		}
	case DTSLong:
		var v int32
		t.intVals = make([]int64, int(t.Count))
		for i := range t.intVals {
			err := binary.Read(r, t.order, &v)
			if err != nil {
				return err
			}
			t.intVals[i] = int64(v)
		}
	case DTRational:
		t.ratVals = make([][]int64, int(t.Count))
		for i := range t.ratVals {
			var n, d uint32
			err := binary.Read(r, t.order, &n)
			if err != nil {
				return err
			}
			err = binary.Read(r, t.order, &d)
			if err != nil {
				return err
			}
			t.ratVals[i] = []int64{int64(n), int64(d)}
		}
	case DTSRational:
		t.ratVals = make([][]int64, int(t.Count))
		for i := range t.ratVals {
			var n, d int32
			err := binary.Read(r, t.order, &n)
			if err != nil {
				return err
			}
			err = binary.Read(r, t.order, &d)
			if err != nil {
				return err
			}
			t.ratVals[i] = []int64{int64(n), int64(d)}
		}
	case DTFloat: // float32
		t.floatVals = make([]float64, int(t.Count))
		for i := range t.floatVals {
			var v float32
			err := binary.Read(r, t.order, &v)
			if err != nil {
				return err
			}
			t.floatVals[i] = float64(v)
		}
	case DTDouble:
		t.floatVals = make([]float64, int(t.Count))
		for i := range t.floatVals {
			var u float64
			err := binary.Read(r, t.order, &u)
			if err != nil {
				return err
			}
			t.floatVals[i] = u
		}
	}

	switch t.Type {
	case DTByte, DTShort, DTLong, DTSByte, DTSShort, DTSLong:
		t.format = IntVal
	case DTRational, DTSRational:
		t.format = RatVal
	case DTFloat, DTDouble:
		t.format = FloatVal
	case DTAscii:
		t.format = StringVal
	case DTUndefined:
		t.format = UndefVal
	default:
		t.format = OtherVal
	}

	return nil
}

// Format returns a value indicating which method can be called to retrieve the
// tag's value properly typed (e.g. integer, rational, etc.).
func (t *Tag) Format() Format { return t.format }

func (t *Tag) typeErr(to Format) error {
	return &wrongFmtErr{typeNames[t.Type], formatNames[to]}
}

// Rat returns the tag's i'th value as a rational number. It returns a nil and
// an error if this tag's Format is not RatVal.  It panics for zero deminators
// or if i is out of range.
func (t *Tag) Rat(i int) (*big.Rat, error) {
	n, d, err := t.Rat2(i)
	if err != nil {
		return nil, err
	}
	return big.NewRat(n, d), nil
}

// Rat2 returns the tag's i'th value as a rational number represented by a
// numerator-denominator pair. It returns an error if the tag's Format is not
// RatVal. It panics if i is out of range.
func (t *Tag) Rat2(i int) (num, den int64, err error) {
	if t.format != RatVal {
		return 0, 0, t.typeErr(RatVal)
	}
	return t.ratVals[i][0], t.ratVals[i][1], nil
}

// Int64 returns the tag's i'th value as an integer. It returns an error if the
// tag's Format is not IntVal. It panics if i is out of range.
func (t *Tag) Int64(i int) (int64, error) {
	if t.format != IntVal {
		return 0, t.typeErr(IntVal)
	}
	return t.intVals[i], nil
}

// Int returns the tag's i'th value as an integer. It returns an error if the
// tag's Format is not IntVal. It panics if i is out of range.
func (t *Tag) Int(i int) (int, error) {
	if t.format != IntVal {
		return 0, t.typeErr(IntVal)
	}
	return int(t.intVals[i]), nil
}

// Float returns the tag's i'th value as a float. It returns an error if the
// tag's Format is not IntVal.  It panics if i is out of range.
func (t *Tag) Float(i int) (float64, error) {
	if t.format != FloatVal {
		return 0, t.typeErr(FloatVal)
	}
	return t.floatVals[i], nil
}

// StringVal returns the tag's value as a string. It returns an error if the
// tag's Format is not StringVal. It panics if i is out of range.
func (t *Tag) StringVal() (string, error) {
	if t.format != StringVal {
		return "", t.typeErr(StringVal)
	}
	return t.strVal, nil
}

// String returns a nicely formatted version of the tag.
func (t *Tag) String() string {
	data, err := t.MarshalJSON()
	if err != nil {
		return "ERROR: " + err.Error()
	}

	if t.Count == 1 {
		return strings.Trim(fmt.Sprintf("%s", data), "[]")
	}
	return fmt.Sprintf("%s", data)
}

func (t *Tag) MarshalJSON() ([]byte, error) {
	switch t.format {
	case StringVal, UndefVal:
		return nullString(t.Val), nil
	case OtherVal:
		return []byte(fmt.Sprintf("unknown tag type '%v'", t.Type)), nil
	}

	rv := []string{}
	for i := 0; i < int(t.Count); i++ {
		switch t.format {
		case RatVal:
			n, d, _ := t.Rat2(i)
			rv = append(rv, fmt.Sprintf(`"%v/%v"`, n, d))
		case FloatVal:
			v, _ := t.Float(i)
			rv = append(rv, fmt.Sprintf("%v", v))
		case IntVal:
			v, _ := t.Int(i)
			rv = append(rv, fmt.Sprintf("%v", v))
		}
	}
	return []byte(fmt.Sprintf(`[%s]`, strings.Join(rv, ","))), nil
}

func nullString(in []byte) []byte {
	rv := bytes.Buffer{}
	rv.WriteByte('"')
	for _, b := range in {
		if unicode.IsPrint(rune(b)) {
			rv.WriteByte(b)
		}
	}
	rv.WriteByte('"')
	rvb := rv.Bytes()
	if utf8.Valid(rvb) {
		return rvb
	}
	return []byte(`""`)
}

type wrongFmtErr struct {
	From, To string
}

func (e *wrongFmtErr) Error() string {
	return fmt.Sprintf("cannot convert tag type '%v' into '%v'", e.From, e.To)
}
