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
    }
}
