--[[----------------------------------------------------------------------------

WFCatGPSFormatUtils.lua

--------------------------------------------------------------------------------

ADOBE CONFIDENTIAL
------------------
Copyright 2015 Adobe Systems IncorporatedAll Rights Reserved.

NOTICE: All information contained herein is, and remain the property of
Adobe Systems Incorporated and its suppliers, if any. The intellectual and
technical concepts contained herein are proprietary to Adobe Systems
Incorporated and its suppliers and may be covered by U.S. and Foreign
Patents, patents in process, and are protected by trade secret or copyright
law.  Dissemination of this information or reproduction of this material is
strictly forbidden unless prior written permission is obtained from
Adobe Systems Incorporated.

----------------------------------------------------------------------------]]--

	-- WF.core.string
local KSStringUtils = require 'KSStringUtils'
local KSTableUtils = require 'KSTableUtils'

	-- WF.core.utils
local WFArguments = require 'WFArguments'

local WFCatFormatterUtils = require 'WFCatFormatterUtils'

local lowercase = KSStringUtils.lower
local check = WFArguments.check

--==============================================================================

local kCardinalDirection = {
	"$$$/AgLocation/CardinalDirection/NorthEast=North-East",
	"$$$/AgLocation/CardinalDirection/East=East",
	"$$$/AgLocation/CardinalDirection/SouthEast=South-East",
	"$$$/AgLocation/CardinalDirection/South=South",
	"$$$/AgLocation/CardinalDirection/SouthWest=South-West",
	"$$$/AgLocation/CardinalDirection/West=West",
	"$$$/AgLocation/CardinalDirection/NorthWest=North-West",
	"$$$/AgLocation/CardinalDirection/North=North",
}

--------------------------------------------------------------------------------

local WFCatGPSFormatUtils = {}

--------------------------------------------------------------------------------

local kMetersPerFoot = 0.3048
WFCatGPSFormatUtils.MetersPerFoot = kMetersPerFoot

--------------------------------------------------------------------------------

local kDirectionRefToOzRef = { C = 'M', T = 'T' }
local kOzRefTODirectionRef = KSTableUtils.invert( kDirectionRefToOzRef )
--------------------------------------------------------------------------------

function WFCatGPSFormatUtils.floatLatLonCoord( input )

	if not input then

		return

	end

	local pat = "[0-9.]+"
	local value = 0
	local divisor = 1
	for x in string.gfind( input, pat ) do
		value = value + tonumber( x ) / divisor
		divisor = divisor * 60

	end

	local ref = string.gfind( input, "[NSWE]" )()

	if ref == "S" or ref == "W" then

		value = value * -1

	end

	return value
end

------------------------------------------------------------------------------

function WFCatGPSFormatUtils.normalizeGPSCoord( input, maxValue )

	-- convert the input value to be within [-maxValue, maxValue]

	local period = 2 * maxValue;

	if input < -period or input > period then

		input = math.fmod( input, period )

	end

	if input < -maxValue then

		input = input + period

	elseif input > maxValue then
	
		input = input - period

	end

	return input
end

------------------------------------------------------------------------------

local function latLonCoordFromFloatInternal( input, latOrLon, forUIDisplay )

	local compass

	if latOrLon == "lat" then

		input = WFCatGPSFormatUtils.normalizeGPSCoord( input, 90 )

		if input < 0 then
			input = input * -1
			compass = "S"
		else
			compass = "N"
		end

	else
	
		input = WFCatGPSFormatUtils.normalizeGPSCoord( input, 180 )

		if input < 0 then
			input = input * -1
			compass = "W"
		else
			compass = "E"
		end

	end

	local degrees
	degrees, input = math.modf( input )
	input = input * 60
	local minutesWithFractionalAmount = input
	local minutes
	minutes, input = math.modf( input )
	input = input * 60
	local seconds
	seconds = string.format( "%.2f", input )
	local result

	if forUIDisplay == true then

		result = { 'ZSTR', "$$$/AgLibrary/Exif/LatitudeLongitude=^1^D^2\'^3\" ^4",
			degrees, minutes, seconds, compass }
			
	else
	
		minutesWithFractionalAmount = string.format("%.10f", minutesWithFractionalAmount):gsub('%.?0+$', '')
		result = { 'ZSTR', "$$$/AgLibrary/Exif/LatitudeLongitudeXMP=^1,^2^3",
			degrees, minutesWithFractionalAmount, compass }
			-- XMP spec don't allow fractional numbers in the second field. This representation gives more
			-- precision in specifying the GPS coordinates

	end

	return result
	
end

--------------------------------------------------------------------------------

-- The Latitude/Longitude coordinate string returned by this function is good
-- for UI display.

function WFCatGPSFormatUtils.latLonCoordFromFloat( input, latOrLon )
	return latLonCoordFromFloatInternal( input, latOrLon, true )
end

--------------------------------------------------------------------------------

-- The Latitude/Longitude coordinate string returned by this function is good
-- for persistent storage in XMP.

function WFCatGPSFormatUtils.xmpLatLonCoordFromFloat( input, latOrLon )
	return latLonCoordFromFloatInternal( input, latOrLon, false )
end

--------------------------------------------------------------------------------

-- The Latitude/Longitude coordinate string returned by this function is good
-- for persistent storage in XMP.

function WFCatGPSFormatUtils.normalizeXmpLatLonCoord( input, latOrLon )
	local latLonFloat = WFCatGPSFormatUtils.floatLatLonCoord( input )
	return latLonCoordFromFloatInternal( latLonFloat, latOrLon, false )
end

--------------------------------------------------------------------------------

-- The Latitude/Longitude coordinate string returned by this function is good
-- for UI display.

function WFCatGPSFormatUtils.normalizeLatLonCoord( input, latOrLon )
	local latLonFloat = WFCatGPSFormatUtils.floatLatLonCoord( input )
	return latLonCoordFromFloatInternal( latLonFloat, latOrLon, true )
end

--------------------------------------------------------------------------------

function WFCatGPSFormatUtils.getElevationOverlay( elevation )

	if elevation then
		return {
			location = {
				altitude = math.floor( elevation * 1000000 ) / 1000000
			}
		}
	else
		return {
			location = {
				altitude = { _delete = true }
			}
		}
	end

end

--------------------------------------------------------------------------------

function WFCatGPSFormatUtils.parseElevation( value )

	local altitude = value

	if type( value ) == 'string' then

		value = KSStringUtils.trimWhitespace( value )

		if string.len( value ) > 0 then

			value = lowercase( value )
			local metric = false -- kUseMetric -- TODO [arahn 2015-01-30]: We shouldn't just "know" to use metric.
			local unitFeet, unitMeters
			
			-- TODO [arahn 2015-01-30]: Doing string matching using a ZSTR as an input is bad juju.
			for _, pattern in ipairs { "$$$/AgLibrary/Exif/Feet/Abbrev=ft", "$$$/AgLibrary/Exif/Feet=Feet" } do
				unitFeet = string.find( value, lowercase( pattern ) )

				if unitFeet then
					break
				end
			end

			-- TODO [arahn 2015-01-30]: Doing string matching using a ZSTR as an input is bad juju.
			for _, pattern in ipairs { "$$$/AgLibrary/Exif/Meters/Abbrev=m", "$$$/AgLibrary/Exif/Meters=Meters" } do
				unitMeters = string.find( value, lowercase( pattern ) )

				if unitMeters then
					break
				end
			end

			if type( unitFeet ) == "number" and unitFeet > 1 then
				value = string.sub( value, 1, unitFeet - 1 )
				metric = false

			elseif type( unitMeters ) == "number" and unitMeters > 1 then
				value = string.sub( value, 1, unitMeters - 1 )
				metric = true
			end
			altitude = tonumber( value )

			if altitude then

				if not metric then
					altitude = altitude * kMetersPerFoot
				end
			end

		end

	end

	return altitude
end

--------------------------------------------------------------------------------
--[[!
	\brief Parse an input string into GPS coordinates (latitude and longitude)

	\param value (string) input string to be parsed
	
	\return (number, number) latitude and longitude

	NOTE: Make a best-effort to extract GPS coordinates from the user-input
	string. Supports both the DMS and decimal formats. Coordinates can be exchanged
	if you specify NS/EW. Case is ignored.
]]--

function WFCatGPSFormatUtils.parseLatLong( value )

	check( 'string' )

	value = KSStringUtils.trimWhitespace( value )
	value = KSStringUtils.upper( value )

	if string.len( value ) > 0 then
		local ns = string.find( value, "[NS]" )
		local ew = string.find( value, "[EW]" )
		local sep = string.find( value, "[,;]" )

		if type( ns ) == "number" and type( ew ) == "number" and ns > 1 and ew > 1 then
			local lat, lng

			if ns < ew then

				-- latitude specified first

				lat = string.sub( value, 1, ns )
				lng = string.sub( value, ns + 1 )

			else

				-- longitude specified first

				lng = string.sub( value, 1, ew)
				lat = string.sub( value, ew + 1 )

			end

			if string.find( lat, "[0-9.]+" ) and string.find( lng, "[0-9.]+" ) then
				local floatLat = WFCatGPSFormatUtils.floatLatLonCoord( KSStringUtils.trimWhitespace( lat ), "lat" )
				local floatLng = WFCatGPSFormatUtils.floatLatLonCoord( KSStringUtils.trimWhitespace( lng ), "lon" )

				return floatLat, floatLng

		-- This unusual formatting of end followed by elseif is a way to hide a coverage
		-- idiosyncrasy (lua coverage never reports the end line hit).

		end elseif ns == nil and ew == nil and type( sep ) == "number" and sep > 1 then

			-- eg. "-24.755449, 15.292478"

			local lat, lng

			-- assume that latitude is specified first, longitude is specified the second

			lat = string.sub( value, 1, sep - 1)
			lng = string.sub( value, sep + 1 )
			local latFloat = tonumber( lat )
			local lngFloat = tonumber( lng )

			if latFloat and lngFloat then
				return latFloat, lngFloat
			end

		end

	end

	return nil, nil
end

--------------------------------------------------------------------------------

function WFCatGPSFormatUtils.getLocationOverlay( lat, lng )

	if lat and lng then

		return {
			location = {
				longitude = lng,
				latitude = lat,
				
				-- Reset fields that Oz may have set by reverse-geocoding
				city = { _delete = true },
				state = { _delete = true },
				country = { _delete = true },
				isoCountryCode = { _delete = true },
				sublocation = { _delete = true },
			},
		}

	else

		return {
			location = {
				longitude = { _delete = true },
				latitude = { _delete = true },
			}
		}

	end

end

--------------------------------------------------------------------------------

function WFCatGPSFormatUtils.getDirectionOverlay( direction )

	if direction then

		return {
			location = {
				direction = direction,
			}
		}
		
	else

		return {
			location = {
				direction = { _delete = true },
			}
		}
		
	end

end

--------------------------------------------------------------------------------

function WFCatGPSFormatUtils.formatDirection( direction, format )

	if type( direction ) == "number" then

		if format == "cardinal" then
			local index = math.floor( direction / 45 + 0.5 )

			if index == 0 then index = 8 end
			local name = kCardinalDirection[ index ]
			return 'ZSTR', "$$$/AgLibrary/Exif/Direction/DisplayName=^1", { 'ZSTR', name }
		else
			return 'ZSTR', "$$$/AgLibrary/Exif/Direction/DisplayDegrees=^1^D", math.floor( direction + 0.5 )
		end

	end

end

--------------------------------------------------------------------------------

function WFCatGPSFormatUtils.convertGPSDirectionRefToOzRef( directionRef )

	return directionRef and kDirectionRefToOzRef[ string.upper( directionRef ) ]
	
end

--------------------------------------------------------------------------------

function WFCatGPSFormatUtils.convertOzRefToGPSDirectionRef( ozRef )
	
	return ozRef and kOzRefTODirectionRef[ string.upper( ozRef ) ]

end

--------------------------------------------------------------------------------

function WFCatGPSFormatUtils.convertGPSImgDirectionToNumber( direction )
	
	local numerator, denominator = WFCatFormatterUtils.extractNumeratorDenominator( direction )
	
	local ratio = {}
	ratio[ 1 ] = numerator
	ratio[ 2 ] = denominator
	
	return WFCatFormatterUtils.convertToNumericValue( ratio )

end

--------------------------------------------------------------------------------

local function formatGPSCoord( input, posRef, negRef )

	local pat = '[0-9.]+'
	local value = 0
	local divisor = 1
	
	-- Convert string to a number, while simultaneously
	-- converting negative numbers to positive ones.

	for x in string.gfind( input, pat ) do
		value = value + tonumber( x ) / divisor
		divisor = divisor * 60
	end

	value = tonumber( value )

	local degrees = math.floor( value )
	local fractionalMinutes = ( value - degrees ) * 60
	local minutes = math.floor( fractionalMinutes )
	local seconds = math.floor( 60 * ( fractionalMinutes - minutes ) + 0.5 )
		-- round to the nearest integer
		
	-- Prepare formatted values that we'll suppply to LOC that the
	-- default ZString will ignore, but will be available should the user want
	-- to change the displayed format themselves.
	
	local formattedValue = string.format( '%.7f', value ) -- will be ^7
	local formattedFractionalMinutes = string.format( '%.4f', fractionalMinutes ) -- will be ^8
	
	-- Prepare a string containing the plus/minus sign appropriate for the coordinate,
	-- and another that is a minus or empty.
	local plusMinus = '+'  -- will be ^5
	local blankMinus = ''  -- will be ^6
	local ref = posRef
	
	if tonumber( input ) < 0 then

		plusMinus = '-'
		blankMinus = '-'
		ref = negRef

	end

	return { 'ZSTR', "$$$/AgLibrary/Exif/LatitudeLongitude=^1^D^2\'^3\" ^4",
		degrees, minutes, seconds, ref, plusMinus, blankMinus, formattedValue, formattedFractionalMinutes }

end

--------------------------------------------------------------------------------

function WFCatGPSFormatUtils.formatGPSCoordinates( latitude, longitude )

	if tonumber( latitude ) and tonumber( longitude ) then
	
		local latPosRef = { 'ZSTR', "$$$/Metadata/Exif/GPS/NorthLatitudeAbbr=N" }
		local latNegRef = { 'ZSTR', "$$$/Metadata/Exif/GPS/SouthLatitudeAbbr=S" }
		
		local lngPosRef = { 'ZSTR', "$$$/Metadata/Exif/GPS/EastlongitudeAbbr=E" }
		local lngNegRef = { 'ZSTR', "$$$/Metadata/Exif/GPS/WestLongitudeAbbr=W" }

		local latitudeFmt = formatGPSCoord( latitude, latPosRef, latNegRef )
		local longitudeFmt = formatGPSCoord( longitude, lngPosRef, lngNegRef )
		
		return 'ZSTR', "$$$/AgLibrary/Exif/GPS/Pair=^1 ^2", latitudeFmt, longitudeFmt
	end
	
end

--------------------------------------------------------------------------------

function WFCatGPSFormatUtils.GPSCoordStringToNumber( input, maxValue )

	if not input then
		return
	end

	local pat = '[0-9.]+'
	local value = 0
	local divisor = 1

	for x in string.gfind( input, pat ) do
		value = value + tonumber( x ) / divisor
		divisor = divisor * 60
	end

	local ref = string.gfind( input, '[NSWE]' )()
	if ref == 'S' or ref == 'W' then
		value = - value
	end
	
	local output = tonumber( string.format( "%.6f", value ) )
	
	return WFCatGPSFormatUtils.normalizeGPSCoord( output, maxValue )

end

--==============================================================================

return WFCatGPSFormatUtils
