package meta

import (
	"fmt"
	"path/filepath"
	"reflect"
	"runtime/debug"
	"strings"
	"time"

	"github.com/photoprism/photoprism/pkg/rnd"
	"github.com/photoprism/photoprism/pkg/txt"
	"github.com/tidwall/gjson"
	"gopkg.in/ugjka/go-tz.v2/tz"
)

// Parses JSON sidecar data as created by Exiftool.
func (data *Data) Exiftool(jsonData []byte, originalName string) (err error) {
	defer func() {
		if e := recover(); e != nil {
			err = fmt.Errorf("metadata: %s (exiftool panic)\nstack: %s", e, debug.Stack())
		}
	}()

	j := gjson.GetBytes(jsonData, "@flatten|@join")

	if !j.IsObject() {
		return fmt.Errorf("metadata: data is not an object in %s (exiftool)", txt.Quote(filepath.Base(originalName)))
	}

	jsonStrings := make(map[string]string)
	jsonValues := j.Map()

	for key, val := range jsonValues {
		jsonStrings[key] = val.String()
	}

	if fileName, ok := jsonStrings["FileName"]; ok && fileName != "" && originalName != "" && fileName != originalName {
		return fmt.Errorf("metadata: original name %s does not match %s (exiftool)", txt.Quote(originalName), txt.Quote(fileName))
	}

	v := reflect.ValueOf(data).Elem()

	// Iterate through all config fields
	for i := 0; i < v.NumField(); i++ {
		fieldValue := v.Field(i)

		tagData := v.Type().Field(i).Tag.Get("meta")

		// Automatically assign values to fields with "flag" tag
		if tagData != "" {
			tagValues := strings.Split(tagData, ",")

			var jsonValue gjson.Result
			var tagValue string

			for _, tagValue = range tagValues {
				if r, ok := jsonValues[tagValue]; !ok {
					continue
				} else {
					jsonValue = r
					break
				}
			}

			// Skip empty values.
			if !jsonValue.Exists() || !fieldValue.IsZero() {
				continue
			}

			switch t := fieldValue.Interface().(type) {
			case time.Time:
				s := strings.TrimSpace(jsonValue.String())
				s = strings.ReplaceAll(s, "/", ":")

				if tv, err := time.Parse("2006:01:02 15:04:05", strings.ReplaceAll(s, "-", ":")); err == nil {
					fieldValue.Set(reflect.ValueOf(tv.Round(time.Second).UTC()))
				} else if tv, err := time.Parse("2006:01:02 15:04:05-07:00", s); err == nil {
					fieldValue.Set(reflect.ValueOf(tv.Round(time.Second)))
				}
			case time.Duration:
				fieldValue.Set(reflect.ValueOf(StringToDuration(jsonValue.String())))
			case int, int64:
				fieldValue.SetInt(jsonValue.Int())
			case float32, float64:
				fieldValue.SetFloat(jsonValue.Float())
			case uint, uint64:
				fieldValue.SetUint(jsonValue.Uint())
			case string:
				fieldValue.SetString(strings.TrimSpace(jsonValue.String()))
			case bool:
				fieldValue.SetBool(jsonValue.Bool())
			default:
				log.Warnf("metadata: can't assign value of type %s to %s (exiftool)", t, tagValue)
			}
		}
	}

	// Set latitude and longitude if known and not already set.
	if data.Lat == 0 && data.Lng == 0 {
		if data.GPSPosition != "" {
			data.Lat, data.Lng = GpsToLatLng(data.GPSPosition)
		} else if data.GPSLatitude != "" && data.GPSLongitude != "" {
			data.Lat = GpsToDecimal(data.GPSLatitude)
			data.Lng = GpsToDecimal(data.GPSLongitude)
		}
	}

	// Set time zone and calculate UTC time.
	if data.Lat != 0 && data.Lng != 0 {
		zones, err := tz.GetZone(tz.Point{
			Lat: float64(data.Lat),
			Lon: float64(data.Lng),
		})

		if err == nil && len(zones) > 0 {
			data.TimeZone = zones[0]
		}

		if !data.TakenAtLocal.IsZero() {
			if loc, err := time.LoadLocation(data.TimeZone); err != nil {
				log.Warnf("metadata: unknown time zone %s (exiftool)", data.TimeZone)
			} else if tl, err := time.ParseInLocation("2006:01:02 15:04:05", data.TakenAtLocal.Format("2006:01:02 15:04:05"), loc); err == nil {
				if localUtc, err := time.ParseInLocation("2006:01:02 15:04:05", data.TakenAtLocal.Format("2006:01:02 15:04:05"), time.UTC); err == nil {
					data.TakenAtLocal = localUtc
				}

				data.TakenAt = tl.Round(time.Second).UTC()
			} else {
				log.Errorf("metadata: %s (exiftool)", err.Error()) // this should never happen
			}
		}
	} else if _, offset := data.TakenAtLocal.Zone(); offset != 0 && !data.TakenAtLocal.IsZero() {
		if localUtc, err := time.ParseInLocation("2006:01:02 15:04:05", data.TakenAtLocal.Format("2006:01:02 15:04:05"), time.UTC); err == nil {
			data.TakenAtLocal = localUtc
		}

		data.TakenAt = data.TakenAt.Round(time.Second).UTC()
	}

	if orientation, ok := jsonStrings["Orientation"]; ok && orientation != "" {
		switch orientation {
		case "1", "Horizontal (normal)":
			data.Orientation = 1
		case "2":
			data.Orientation = 2
		case "3", "Rotate 180 CW":
			data.Orientation = 3
		case "4":
			data.Orientation = 4
		case "5":
			data.Orientation = 5
		case "6", "Rotate 90 CW":
			data.Orientation = 6
		case "7":
			data.Orientation = 7
		case "8", "Rotate 270 CW":
			data.Orientation = 8
		}
	}

	if data.Orientation == 0 {
		// Set orientation based on rotation.
		switch data.Rotation {
		case 0:
			data.Orientation = 1
		case -180, 180:
			data.Orientation = 3
		case 90:
			data.Orientation = 6
		case -90, 270:
			data.Orientation = 8
		}
	}

	// Normalize compression information.
	data.Codec = strings.ToLower(data.Codec)
	if strings.Contains(data.Codec, CodecJpeg) {
		data.Codec = CodecJpeg
	}

	// Validate and normalize optional DocumentID.
	if data.DocumentID != "" {
		data.DocumentID = rnd.SanitizeUUID(data.DocumentID)
	}

	// Validate and normalize optional InstanceID.
	if data.InstanceID != "" {
		data.InstanceID = rnd.SanitizeUUID(data.InstanceID)
	}

	if data.Projection == "equirectangular" {
		data.AddKeyword(KeywordPanorama)
	}

	data.Title = SanitizeTitle(data.Title)
	data.Description = SanitizeDescription(data.Description)
	data.Keywords = SanitizeMeta(data.Keywords)
	data.Subject = SanitizeMeta(data.Subject)
	data.Artist = SanitizeMeta(data.Artist)

	return nil
}
