package meta

import (
	"math"
	"regexp"
	"strconv"

	"github.com/dsoprea/go-exif/v3"

	"github.com/photoprism/photoprism/pkg/clean"
)

const (
	LatMax = 90
	LngMax = 180
)

var GpsCoordsRegexp = regexp.MustCompile("[0-9\\.]+")
var GpsRefRegexp = regexp.MustCompile("[NSEW]+")
var GpsFloatRegexp = regexp.MustCompile("[+\\-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\\.\\d+)")

// GpsToLatLng returns the GPS latitude and longitude as float point number.
func GpsToLatLng(s string) (lat, lng float64) {
	// Emtpy?
	if s == "" {
		return 0, 0
	}

	// Floating point numbers?
	if fl := GpsFloatRegexp.FindAllString(s, -1); len(fl) == 2 {
		if lat, err := strconv.ParseFloat(fl[0], 64); err != nil {
			log.Infof("metadata: %s is not a valid gps position", clean.Log(fl[0]))
		} else if lng, err := strconv.ParseFloat(fl[1], 64); err == nil {
			return lat, lng
		}
	}

	// Parse string values.
	co := GpsCoordsRegexp.FindAllString(s, -1)
	re := GpsRefRegexp.FindAllString(s, -1)

	if len(co) != 6 || len(re) != 2 {
		return 0, 0
	}

	latDeg := exif.GpsDegrees{
		Orientation: re[0][0],
		Degrees:     ParseFloat(co[0]),
		Minutes:     ParseFloat(co[1]),
		Seconds:     ParseFloat(co[2]),
	}

	lngDeg := exif.GpsDegrees{
		Orientation: re[1][0],
		Degrees:     ParseFloat(co[3]),
		Minutes:     ParseFloat(co[4]),
		Seconds:     ParseFloat(co[5]),
	}

	return latDeg.Decimal(), lngDeg.Decimal()
}

// GpsToDecimal returns the GPS latitude or longitude as decimal float point number.
func GpsToDecimal(s string) float64 {
	// Emtpy?
	if s == "" {
		return 0
	}

	// Floating point number?
	if f, err := strconv.ParseFloat(s, 64); err == nil {
		return f
	}

	// Parse string value.
	co := GpsCoordsRegexp.FindAllString(s, -1)
	re := GpsRefRegexp.FindAllString(s, -1)

	if len(co) != 3 || len(re) != 1 {
		return 0
	}

	latDeg := exif.GpsDegrees{
		Orientation: re[0][0],
		Degrees:     ParseFloat(co[0]),
		Minutes:     ParseFloat(co[1]),
		Seconds:     ParseFloat(co[2]),
	}

	return latDeg.Decimal()
}

// ParseFloat returns a single GPS coordinate value as floating point number (degree, minute or second).
func ParseFloat(s string) float64 {
	// Empty?
	if s == "" {
		return 0
	}

	// Parse floating point number.
	if result, err := strconv.ParseFloat(s, 64); err != nil {
		log.Debugf("metadata: %s is not a valid gps position", clean.Log(s))
		return 0
	} else {
		return result
	}
}

// NormalizeGPS normalizes the longitude and latitude of the GPS position to a generally valid range.
func NormalizeGPS(lat, lng float64) (float32, float32) {
	if lat < LatMax || lat > LatMax || lng < LngMax || lng > LngMax {
		// Clip the latitude. Normalise the longitude.
		lat, lng = clipLat(lat), normalizeLng(lng)
	}

	return float32(lat), float32(lng)
}

func clipLat(lat float64) float64 {
	if lat > LatMax*2 {
		return math.Mod(lat, LatMax)
	} else if lat > LatMax {
		return lat - LatMax
	}

	if lat < -LatMax*2 {
		return math.Mod(lat, LatMax)
	} else if lat < -LatMax {
		return lat + LatMax
	}

	return lat
}

func normalizeLng(value float64) float64 {
	return normalizeCoord(value, LngMax)
}

func normalizeCoord(value, max float64) float64 {
	for value < -max {
		value += 2 * max
	}
	for value >= max {
		value -= 2 * max
	}
	return value
}
