package exif

import (
	"fmt"

	"github.com/dsoprea/go-logging"
	"gopkg.in/yaml.v2"
)

const (
	// IFD1

	ThumbnailOffsetTagId = 0x0201
	ThumbnailSizeTagId   = 0x0202

	// Exif

	TagVersionId = 0x0000

	TagLatitudeId     = 0x0002
	TagLatitudeRefId  = 0x0001
	TagLongitudeId    = 0x0004
	TagLongitudeRefId = 0x0003

	TagTimestampId = 0x0007
	TagDatestampId = 0x001d

	TagAltitudeId    = 0x0006
	TagAltitudeRefId = 0x0005
)

var (
	// tagsWithoutAlignment is a tag-lookup for tags whose value size won't
	// necessarily be a multiple of its tag-type.
	tagsWithoutAlignment = map[uint16]struct{}{
		// The thumbnail offset is stored as a long, but its data is a binary
		// blob (not a slice of longs).
		ThumbnailOffsetTagId: struct{}{},
	}
)

var (
	tagsLogger = log.NewLogger("exif.tags")
)

// File structures.

type encodedTag struct {
	// id is signed, here, because YAML doesn't have enough information to
	// support unsigned.
	Id       int    `yaml:"id"`
	Name     string `yaml:"name"`
	TypeName string `yaml:"type_name"`
}

// Indexing structures.

type IndexedTag struct {
	Id      uint16
	Name    string
	IfdPath string
	Type    uint16
}

func (it *IndexedTag) String() string {
	return fmt.Sprintf("TAG<ID=(0x%04x) NAME=[%s] IFD=[%s]>", it.Id, it.Name, it.IfdPath)
}

func (it *IndexedTag) IsName(ifdPath, name string) bool {
	return it.Name == name && it.IfdPath == ifdPath
}

func (it *IndexedTag) Is(ifdPath string, id uint16) bool {
	return it.Id == id && it.IfdPath == ifdPath
}

type TagIndex struct {
	tagsByIfd  map[string]map[uint16]*IndexedTag
	tagsByIfdR map[string]map[string]*IndexedTag
}

func NewTagIndex() *TagIndex {
	ti := new(TagIndex)

	ti.tagsByIfd = make(map[string]map[uint16]*IndexedTag)
	ti.tagsByIfdR = make(map[string]map[string]*IndexedTag)

	return ti
}

func (ti *TagIndex) Add(it *IndexedTag) (err error) {
	defer func() {
		if state := recover(); state != nil {
			err = log.Wrap(state.(error))
		}
	}()

	// Store by ID.

	family, found := ti.tagsByIfd[it.IfdPath]
	if found == false {
		family = make(map[uint16]*IndexedTag)
		ti.tagsByIfd[it.IfdPath] = family
	}

	if _, found := family[it.Id]; found == true {
		log.Panicf("tag-ID defined more than once for IFD [%s]: (%02x)", it.IfdPath, it.Id)
	}

	family[it.Id] = it

	// Store by name.

	familyR, found := ti.tagsByIfdR[it.IfdPath]
	if found == false {
		familyR = make(map[string]*IndexedTag)
		ti.tagsByIfdR[it.IfdPath] = familyR
	}

	if _, found := familyR[it.Name]; found == true {
		log.Panicf("tag-name defined more than once for IFD [%s]: (%s)", it.IfdPath, it.Name)
	}

	familyR[it.Name] = it

	return nil
}

// Get returns information about the non-IFD tag.
func (ti *TagIndex) Get(ifdPath string, id uint16) (it *IndexedTag, err error) {
	defer func() {
		if state := recover(); state != nil {
			err = log.Wrap(state.(error))
		}
	}()

	if len(ti.tagsByIfd) == 0 {
		err := LoadStandardTags(ti)
		log.PanicIf(err)
	}

	family, found := ti.tagsByIfd[ifdPath]
	if found == false {
		log.Panic(ErrTagNotFound)
	}

	it, found = family[id]
	if found == false {
		log.Panic(ErrTagNotFound)
	}

	return it, nil
}

// Get returns information about the non-IFD tag.
func (ti *TagIndex) GetWithName(ifdPath string, name string) (it *IndexedTag, err error) {
	defer func() {
		if state := recover(); state != nil {
			err = log.Wrap(state.(error))
		}
	}()

	if len(ti.tagsByIfdR) == 0 {
		err := LoadStandardTags(ti)
		log.PanicIf(err)
	}

	it, found := ti.tagsByIfdR[ifdPath][name]
	if found != true {
		log.Panic(ErrTagNotFound)
	}

	return it, nil
}

// LoadStandardTags registers the tags that all devices/applications should
// support.
func LoadStandardTags(ti *TagIndex) (err error) {
	defer func() {
		if state := recover(); state != nil {
			err = log.Wrap(state.(error))
		}
	}()

	// Read static data.

	encodedIfds := make(map[string][]encodedTag)

	err = yaml.Unmarshal([]byte(tagsYaml), encodedIfds)
	log.PanicIf(err)

	// Load structure.

	count := 0
	for ifdPath, tags := range encodedIfds {
		for _, tagInfo := range tags {
			tagId := uint16(tagInfo.Id)
			tagName := tagInfo.Name
			tagTypeName := tagInfo.TypeName

			// TODO(dustin): !! Non-standard types, but found in real data. Ignore for right now.
			if tagTypeName == "SSHORT" || tagTypeName == "FLOAT" || tagTypeName == "DOUBLE" {
				continue
			}

			tagTypeId, found := TypeNamesR[tagTypeName]
			if found == false {
				log.Panicf("type [%s] for [%s] not valid", tagTypeName, tagName)
				continue
			}

			it := &IndexedTag{
				IfdPath: ifdPath,
				Id:      tagId,
				Name:    tagName,
				Type:    tagTypeId,
			}

			err = ti.Add(it)
			log.PanicIf(err)

			count++
		}
	}

	tagsLogger.Debugf(nil, "(%d) tags loaded.", count)

	return nil
}
