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

WFCatFormatter_exif.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'

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

	-- WF.oz_client.managed_catalog.metadata_formatters
local WFCatFormatterUtils = require 'WFCatFormatterUtils'
local WFCatGPSFormatUtils = require 'WFCatGPSFormatUtils'

local derefOpt = KSTableUtils.derefOpt
local upper = KSStringUtils.upper
local lower = KSStringUtils.lower

--==============================================================================
-- @mark	Utilities
--==============================================================================

local nunpack = function(...)

	if (...) == nil then
		return
	end
	return unpack( ... )

end

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

local function optToString( val, zDefault )

	if val == nil and zDefault ~= nil then
		return 'ZSTR', zDefault
	end

	local result = nil

	if val ~= nil and val ~= "" then
		result = tostring( val )
	end
	
	if result then
		return 'literal', result
	end

end

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

local function optToBoolean( value )

	if value == true then
		return 'ZSTR', "$$$/AgLibrary/DNG/Yes=Yes"
	elseif value == false then
		return 'ZSTR', "$$$/AgLibrary/DNG/No=No"
	end

end

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

local function optNumberToString( val, precision )

	if type( val ) == 'number' then

		return 'number', val, precision
	else
		return optToString( val )

	end

end

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

local function numericValue( val )
	return WFCatFormatterUtils.convertToNumericValue( val ) or 0
end

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

local function normalizeCoord( 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

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

local function combinePrefsAndOptions( formatter, options )

	if options == nil then

		options = {}

	end

	if type( options.preferASCII ) ~= 'boolean' then

		local prefs = formatter.prefs or {}
		options.preferASCII = not prefs.AgCommonUI_useTypographicFractions

	end

	return options
end

--==============================================================================
-- @mark	Camera value display
--==============================================================================
--------------------------------------------------------------------------------
-- Given the possibly-nil make and model, return a human-readable camera name.
--------------------------------------------------------------------------------
-- by Jeffrey Friedl           Kyoto, Japan                   http://regex.info/
--------------------------------------------------------------------------------

local function cameraMakeModeltoName( Make, Model )

   local MAKE  = "" -- capitalized version of Make  (if exists, otherwise an empty string)
   local MODEL = "" -- capitalized version of Model (if exists, otherwise an empty string)

   if Make then
      -- strip leading/trailing whitespace and canonicalize internal whitespace to a single space
      Make = Make:gsub( '^%s+', '' ):gsub( '%s+$', '' ):gsub( '%s%s+', ' ' )
      MAKE = upper( Make )
   end

   if Model then
      -- strip leading/trailing whitespace and canonicalize internal whitespace to a single space
      -- Also remove wrapping <...> or "..."
      Model = Model:gsub( '^["<]?%s*', '' ):gsub( '%s*[>"]?$', '' ):gsub( '%s%s+', ' ' )
      MODEL = upper( Model )
   end

   -- At this point, MAKE/MODEL are guaranteed to be strings (perhaps empty), all uppercase.
   -- Make/Model may still be nil
   if Make then
      if     MAKE == 'FLY'                    	then Make = 'Fly' -- three-character name that's not an acronym
      elseif MAKE:match( '^[A-Z][A-Z][A-Z]?$' ) then Make = MAKE  -- other 3-char (and 2-char) names I've seen are all acronyms (e.g. HP, HTC)
      elseif MAKE:match( '^ASAHI OPT' )         then Make = 'Asahi'    -- "Asahi Opt. Co., Japan", "Asahi Optical Co.,Ltd.", etc.
      elseif MAKE:match( '^ASUS' )              then Make = 'ASUS'     -- usually "ASUS" but sometimes "asus"
      elseif MAKE:match( '^BENQ' )              then Make = 'BenQ'     -- "BenQ Corporation", "BenQ"
      elseif MAKE:match( '^CASIO' )             then Make = 'Casio'    -- "CASIO COMPUTER CO.,LTD."
      elseif MAKE:match( '^DOCOMO' )            then Make = 'DoCoMo'   -- I love this name... it's derived from "anywhere"
      elseif MAKE:match( '^EASTMAN KODAK' )     then Make = 'Kodak'    -- usually "EASTMAN KODAK COMPANY"
      elseif MAKE:match( '^FLIR ' )             then Make = 'FLIR'     -- "FLIR Systems AB", "FLIR Systems"
      elseif MAKE:match( '^FUJI ' )             then Make = 'Fujifilm' -- "FUJI PHOTO FILM CO., LTD."
      elseif MAKE:match( '^FUJIFILM' )          then Make = 'Fujifilm' -- "FUJIFILM"
      elseif MAKE:match( '^FUJITSU' )           then Make = 'Fujitsu'  -- "FUJITSU", "Fujitsu-Siemens", "Fujitsu"
      elseif MAKE:match( '^HEWLETT%-PACKARD' )  then Make = 'HP'       -- "Hewlett Packard", "Hewlett-Packard Co.", "Hewlett-Packard Company"
      elseif MAKE:match( '^KONICA MINOLTA' )    then Make = 'Konica Minolta' --"KONICA MINOLTA DIGITAL CAMERA", "Konica Minolta Camera, Inc.", etc.
      elseif MAKE:match( '^KONICA' )            then Make = 'Konica' -- "KONICA", "Konica Co., Ltd.", etc.
      elseif MAKE:match( '^LEICA' )             then Make = 'Leica'  -- "LEICA", "LEICA CAMERA AG", "Leica Microsystems", etc.
      elseif MAKE:match( '^LG ' )               then Make = 'LG Electronics' -- "LG Elec.", "LG ELECTRONICS", etc.
      elseif MAKE:match( '^MEDION' )            then Make = 'Medion'  -- "MEDION OPTICAL CO,LTD", "MEDION", "MEDION AG", etc.
      elseif MAKE:match( '^MINOLTA' )           then Make = 'Minolta' -- "Minolta Co., Ltd", "MINOLTA", etc.
      elseif MAKE:match( '^OLYMPUS' )           then Make = 'Olympus' -- "OLYMPUS IMAGING CORP.", "OLYMPUS CORPORATION", "OLYMPUS DIGITAL CAMERA", etc.
      elseif MAKE:match( '^PENTAX' )            then Make = 'Pentax'  -- "PENTAX Corporation", "PENTAX RICOH IMAGING", "PENTAX LTD.", etc.
      elseif MAKE:match( '^RICOH' )             then Make = 'Ricoh'   -- "RICOH IMAGING COMPANY, LTD."
      elseif MAKE:match( '^SAMSUNG' )           then Make = 'Samsung' -- "SAMSUNG ELECTRONICS", "SAMSUNG", "SAMSUNG TECHWIN CO., LTD", etc.
      elseif MAKE:match( '^SANYO' )             then Make = 'Sanyo'   -- "SANYO Electric Co., Ltd", "SANYO DIGITAL CAMERA", "Sanyo", etc.
      elseif MAKE:match( '^SONY ERICSSON' )     then Make = 'Sony Ericsson' -- "SONY ERICSSON", "Sony Ericsson"
      elseif MAKE:match( '^SONY' )              then Make = 'Sony' -- "SONY CORPORATION", "SONY", "Sony Computer Entertainment Inc.", etc.

      -- RIM sometimes uses "BlackBerry" and "Research In Motion" for the Make
      elseif MAKE:match( '^BLACKBERRY' )
         and MODEL:match( '^BLACKBERRY' )         then Make = 'RIM'
      elseif MAKE:match( '^RESEARCH IN MOTION' )  then Make = 'RIM'
      else
         --
         -- General fallback.... cut any of the following from the end of the Make:
         --
         --    Corporation
         --    Corp.
         --    Corp
         --    Inc
         --    Inc.
         --    Co
         --    Co.
         --
         Make = Make:gsub( ' [Cc][Oo][Rr][Pp][Oo][Rr][Aa][Tt][Ii][Oo][Nn]$', '' )
                    :gsub( ' [Ii][Nn][Cc].?$', '' )
                    :gsub( ' [Cc][Oo][Rr][Pp].?$', '' )
                    :gsub( ' [Cc][Oo].?$', '' )

         -- ... and convert all-uppercase words (if just letters) in the Make to mixed case,
         -- e.g. "SONY SUPER CAMERA XY73" to "Sony Super Camera XY73"
         Make = Make:gsub('(%S)(%S+)', function( first_letter, rest_of_word )
					  if first_letter:match( '[A-Z]' ) and rest_of_word:match( '^[A-Z]+$' ) then
						 return upper( first_letter ) .. lower( rest_of_word )
					  else
						 return first_letter .. rest_of_word
					  end
			   end)
      end
   end

   if Model then

      -- If the model name includes the make, remove make from the model, e.g.
      -- If make is "Canon" and model is "Canon EOS 5D Mark II" make the model "EOS 5D Mark II"
      if Make and #Model > #Make and lower( Model:sub( 1, #Make ) ) == lower( Make ) then
         Model = Model:sub( #Make + 1 ):gsub( '^%s+', '' )
      end

      -- Remove parenthetical phrases from the end of the model.
      Model = Model:gsub( '%s*%(.*%)$', '' ) -- Old Kodak cameras have names like "KODAK DC290 Zome Digital Camera (V01.00)"


      -- A few more things to tidy up in the model.
      Model = Model:replace( 'DIGITAL REBEL',   'Digital Rebel' )

      Model = Model:gsub( ' [Cc][Aa][Mm][Ee][Rr][Aa]$', '' ) -- this'll turn '...Digital Camera' into '...Digital', to be further cleaned next
      Model = Model:gsub( ' [Dd][Ii][Gg][Ii][Tt][Aa][Ll]$', '' )
      Model = Model:gsub( ' [Zz][Oo][Oo][Mm]$',           '' ) -- if 'zoom' is at the end, just get rid of it. If embedded, smartcase it
      Model = Model:gsub( ' [Zz][Oo][Oo][Mm]',           ' Zoom' )

      Model = Model:gsub( '^COOLPIX',        'Coolpix' )
      Model = Model:gsub( '^PENTAX',         'Pentax' )
      Model = Model:replace( 'EASYSHARE',       'EasyShare' )
   end

   if Make and Model then

      local camera = Make .. " " .. Model

      -- Fixups for the combined result....
      camera = camera:replace( "Canon DIGITAL", "Canon" ) -- e.g. "Canon DIGITAL IXUS 960 IS"

      return camera

   else

      return Make or Model

   end

end

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

local function getLensFromDocValue( docValue )

	return derefOpt( docValue, 'importSource', 'lens', 'name' )
			or derefOpt( docValue, 'xmp', 'aux', 'Lens' ) 

end

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

local function getCameraFromDocValue( docValue )

	local canonicalName = derefOpt( docValue, 'importSource', 'camera', 'name' )
	
	if not canonicalName then
		local rawmodel = derefOpt( docValue, 'xmp', 'tiff', 'Model' )
		local rawmake  = derefOpt( docValue, 'xmp', 'tiff', 'Make' )
		canonicalName = cameraMakeModeltoName( rawmake, rawmodel )
	end
	
	return canonicalName

end

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

local function localizedFStopString( stopVal, preferASCII )

	local result = ""

	if stopVal < 1.0 then
		result = { "number", stopVal, 2 }
	elseif stopVal < 10.0 then
		result = { "number", stopVal, 1 }
	else
		result = { "number", stopVal, 0 }
	end

	local template

	if preferASCII then
		template = "$$$/AgLibrary/Exif/FStopTemplate/ASCII=f / ^0"
	else
		template = "$$$/AgLibrary/Exif/FStopTemplate=^U+0192 / ^0"

	end

	return 'ZSTR', template, result
end

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

local function convertAPEXToAperture( apertureValue, preferASCII )

	local numResult = numericValue( apertureValue )
	numResult = math.pow( 2.0, numResult / 2.0 )

	-- round it to two decimal places in case it is a number
	-- like 10.9999999 which should be displayed as 11

	numResult = math.floor( numResult * 100 + 0.5 ) / 100
	return localizedFStopString( numResult, preferASCII )
end

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

local function formatNumeratorDenominator( num, den, preferASCII )

	if den == 0 then

		return ""

	end

	den = math.floor( den + 0.5 )
	num = math.floor( num + 0.5 )

	return { 'formattedFraction', num, den, preferASCII }

end

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

local function localizedFractionalSeconds( val, preferASCII )

	local numerator, denominator = WFCatFormatterUtils.extractNumeratorDenominator( val )
	
	if numerator then

		local fractionString = 0
		
		if numerator > denominator then
			fractionString = KSStringUtils.numberToString( numerator/denominator, 1 )
		else
			fractionString = formatNumeratorDenominator( numerator, denominator, preferASCII )
		end
		
		return 'ZSTR', "$$$/AgMetadataFormatter/NumSecondsTemplate=^1 sec", fractionString

	end

end

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

local pointThreeBot = 0.275
local pointThreeTop = 0.31667

local function getDecimalBasedDisplayValue( dec )

	if 0.95 < dec and dec < 1.0 then
		return 1
	elseif 0.85 <= dec and dec < 0.95 then
		return 0.9
	elseif 0.75 <= dec and dec < 0.85 then
		return 0.8
	elseif 0.65 <= dec and dec < 0.75 then
		return 0.7
	elseif 0.55 <= dec and dec < 0.65 then
		return 0.6
	elseif 0.45 <= dec and dec < 0.55 then
		return 0.5
	elseif 0.35 <= dec and dec < 0.45 then
		return 0.4
	elseif pointThreeBot <= dec and dec < pointThreeTop then
		return 0.3
	end

end

local function apexToNumDenom( n )
	return 1, math.pow( 2, n )
end

local function convertAPEXToShutterSpeed( shutterSpeedVal, preferASCII )

	local numResult = 0

	if type( shutterSpeedVal ) == "number" then

		numResult = shutterSpeedVal

	else

		local denominator = shutterSpeedVal[ 2 ]

		if denominator ~= 0 then
			numResult = numericValue( shutterSpeedVal )
		else
			return ""
		end

	end

	local result = ""

	if numResult < 1 and numResult > 0 then

		numResult = math.pow( 2 , -numResult )
		local decimalValue = getDecimalBasedDisplayValue( numResult )
		result = { "number", decimalValue, 1 }
		return 'ZSTR', "$$$/AgMetadataFormatter/NumSecondsTemplate=^1 sec", result
		
	elseif numResult < 0 then
	
		numResult = math.pow( 2 , -numResult )
		result = { "number", numResult, 1 }
		return 'ZSTR', "$$$/AgMetadataFormatter/NumSecondsTemplate=^1 sec", result
		
	else

		local n, d = apexToNumDenom( numResult )
		local decimalValue = getDecimalBasedDisplayValue( n / d )

		if decimalValue then
			result = { "number", decimalValue, 1 }
			return 'ZSTR', "$$$/AgMetadataFormatter/NumSecondsTemplate=^1 sec", result
		else
			numResult = math.pow( 2, numResult )
			return localizedFractionalSeconds( { 1, numResult }, preferASCII )
		end

	end

end

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

local function formatDistance( val, optPrecision )

	-- Exif2-2 spec p 34 : SubjectDistance

	-- The distance to the subject, given in meters. Note that if the numerator of the recorded value is FFFFFFFF.H,

	-- Infinity shall be indicated; and if the numerator is 0, Distance unknown shall be indicated.

	if val == 4294967295 then

		return 'ZSTR', "$$$/AgMetadataFormatters/SubjectDistance/Infinity=^U+221E"

	elseif val == 0 then

		return 'ZSTR', "$$$/AgMetadataFormatters/SubjectDistance/Unknown=Unknown"

	end

	return 'distance', val, optPrecision, false

end

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

local function formatExposure( assetDoc, options )

	local shutterSpeed = derefOpt( assetDoc, 'xmp', 'exif', 'ShutterSpeedValue' )
	local FStopVal = derefOpt( assetDoc, 'xmp', 'exif', 'FNumber' )
	local preferASCII = options.preferASCII

	if FStopVal and shutterSpeed then

		local numVal = numericValue( FStopVal )
		return 'ZSTR',
			"$$$/AgLibrary/Exif/ExposureShutterSpeedAtAperture=^1 at ^2",
			{ convertAPEXToShutterSpeed( shutterSpeed, preferASCII ) },
			{ localizedFStopString( numVal, preferASCII ) }

	end
	
	if shutterSpeed then
		return convertAPEXToShutterSpeed( shutterSpeed, preferASCII )
	end
	
	if FStopVal then
		return localizedFStopString( numericValue( FStopVal ), preferASCII )
	end

end

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

local function formatFocalLengthValue( val, optPrecision )

	if val then

		local focalLength = numericValue( val )
		local precision = 4 - math.max( 0, math.floor( math.log10( focalLength ) ) )

		if optPrecision then

			precision = optPrecision

		end

		if focalLength == 0 then

			return 'ZSTR', "$$$/AgLibrary/Exif/FocalLengthTemplate=^1 mm", "0"

		else

			local focalLengthFormat = { 'number', focalLength, precision, true }

			return 'ZSTR', "$$$/AgLibrary/Exif/FocalLengthTemplate=^1 mm",
					focalLengthFormat

		end

	end

end

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

local function formatFocalLength( assetDoc, options )

	return formatFocalLengthValue( derefOpt( assetDoc, 'xmp', 'exif', 'FocalLength' ),
								   options.focalLengthPrecision )
end

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

local function formatFocalPlaneResolutionValue( val, unit )

	if val then

		local resolution = numericValue( val )

		-- round up to the nearest integer

		resolution = math.floor( resolution + 0.5 )
		return string.format( "%d %s", resolution, unit or "" )

	end

end

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

local function formatExposureAndISO( assetDoc, options )

	local exposure = { formatExposure( assetDoc, options ) }
	local iso = derefOpt( assetDoc, 'xmp', 'exif', 'ISOSpeedRatings' )
	if type( iso ) == 'table' then
		iso = iso[ 1 ]
	end

	if exposure[ 1 ] and iso then

		return 'ZSTR', "$$$/AgMetadataFormatters/exif/commonPhotoSettings/exposureISO=^1, ISO ^2",
				exposure, { 'number', iso, 0 }

	end
	
	if iso then
		return 'ZSTR', "$$$/AgMetadataFormatters/exif/ISOSpeedRating=ISO ^1", { 'number', iso, 0 }
	end

	return nunpack( exposure )
end

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

local function formatLensSetting( assetDoc, options )

	local lensOnly = getLensFromDocValue( assetDoc )
			
	local focalLen = { formatFocalLength( assetDoc, options ) }

	if lensOnly and focalLen[ 1 ] then

		return 'ZSTR', "$$$/AgMetadataFormatters/exif/commonPhotoSettings/focalLenAndLens=^1 (^2)", focalLen, lensOnly

	end

	if lensOnly then

		return 'literal', lensOnly

	end

	if focalLen then

		return nunpack( focalLen )

	end

end

--==============================================================================
-- @mark	Formatters
--==============================================================================

local formatters, add, unused = WFCatFormatterUtils.newSuite {
	default_category = 'com.adobe.formatterCategories.exif',
}

--==============================================================================
-- @mark	Camera / Lens Settings
--==============================================================================

add {
	id = 'com.adobe.make',
	title = "$$$/AgLibrary/Exif/Make=Make",
	full_title = "$$$/AgLibrary/Exif/MakeFull=Camera Make",

	format_asset = function( self, assetDoc, options )

		return optToString( derefOpt( assetDoc, 'xmp', 'tiff', 'Make' ) )

	end,
}

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

add {
	id = 'com.adobe.model',
	title = "$$$/AgLibrary/Exif/Model=Model",
	full_title = "$$$/AgLibrary/Exif/ModelFull=Camera Model",

	format_asset = function( self, assetDoc, options )

		return optToString( derefOpt( assetDoc, 'xmp', 'tiff', 'Model' ) )

	end,
}

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

add {
	id = 'com.adobe.combinedCameraName',
	title = "$$$/AgLibrary/Exif/Camera=Camera",

	format_asset = function( self, assetDoc, options )

		local formattedName = getCameraFromDocValue( assetDoc )
		local ser = derefOpt( assetDoc, 'xmp', 'aux', 'SerialNumber' )
		
		if formattedName and ser then

			return 'ZSTR', "$$$/AgLibrary/Exif/CameraTempl/ModelSer=^1 #^2", formattedName, ser

		end
		
		if ser then

			return 'literal', ser

		end

		if formattedName then

			return 'literal', formattedName

		end

	end,
}

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

add {
	id = 'com.adobe.combinedCameraNameNoSerial',
	title = "$$$/AgLibrary/Exif/CameraNoSerial=Camera",

	format_asset = function( self, assetDoc )

		local formattedName = getCameraFromDocValue( assetDoc )

		if formattedName then

			return 'literal', formattedName

		end

	end,
}

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

add {
	id = 'com.adobe.combinedCameraLens',
	title = "$$$/AgLibrary/Exif/CameraPlusLens=Camera + Lens",

	format_asset = function( self, assetDoc, options )

		local formattedName = getCameraFromDocValue( assetDoc )
		
		local lens = getLensFromDocValue( assetDoc )
		
		if formattedName and lens then

			return 'ZSTR', "$$$/AgLibrary/Exif/CameraTempl/MakeOrModelLensTempl=^1 + ^2", formattedName, lens

		end

		if lens then

			return 'literal', lens

		end

		if formattedName then

			return 'literal', formattedName

		end

	end,
}

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

add {
	id = 'com.adobe.combinedCameraLensSetting',
	title = "$$$/AgLibrary/Exif/CameraPlusLensSetting=Camera + Lens Setting",

	format_asset = function( self, assetDoc, options )

		local formattedName = getCameraFromDocValue( assetDoc )

		options = combinePrefsAndOptions( self, options )
		local lens = { formatLensSetting( assetDoc, options ) }

		if formattedName and lens[ 1 ] then

			return 'ZSTR', "$$$/AgLibrary/Exif/CameraTempl/MakeOrModelLensTempl=^1 + ^2", formattedName, lens

		end

		if lens[ 1 ] then

			return nunpack( lens )

		end

		if formattedName then

			return 'literal', formattedName

		end

	end,
}

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

add {
	id = 'com.adobe.serialNumber',
	title = "$$$/AgLibrary/Exif/SerialNumber=Serial Number",
	full_title = "$$$/AgLibrary/Exif/SerialNumberFull=Camera Serial Number",

	format_asset = function( self, assetDoc, options )

		return optToString( derefOpt( assetDoc, 'xmp', 'aux', 'SerialNumber' ) )

	end,
}

--==============================================================================
-- @mark	Exposure
--==============================================================================

add {
	id = 'com.adobe.exposure',
	title = "$$$/AgLibrary/Exif/Exposure=Exposure",

	format_asset = function( self, assetDoc, options )

		options = combinePrefsAndOptions( self, options )
		return formatExposure( assetDoc, options )

	end,
}

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

add {
	id = 'com.adobe.exposureTime',
	title = "$$$/AgLibrary/Exif/ExposureTime=Exposure Time",

	format_asset = function( self, assetDoc, options )

		local exposureVal = derefOpt( assetDoc, 'xmp', 'exif', 'ExposureTime' )
		options = combinePrefsAndOptions( self, options )

		if exposureVal then

			return localizedFractionalSeconds( exposureVal, options.preferASCII )

		end

	end,
}

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

add {
	id = 'com.adobe.shutterSpeedValue',
	title = "$$$/AgLibrary/Exif/ShutterSpeedValue=Shutter Speed",

	format_asset = function( self, assetDoc, options )

		local shutterSpeed = derefOpt( assetDoc, 'xmp', 'exif', 'ShutterSpeedValue' )
		options = combinePrefsAndOptions( self, options )

		if shutterSpeed then

			return convertAPEXToShutterSpeed( shutterSpeed, options.preferASCII )

		end

	end,
}

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

add {
	id = 'com.adobe.FNumber',
	title = "$$$/AgLibrary/Exif/FNumber=F-Stop",

	format_asset = function( self, assetDoc, options )

		local FStopVal = derefOpt( assetDoc, 'xmp', 'exif', 'FNumber' )
		options = combinePrefsAndOptions( self, options )

		if FStopVal then

			local preferASCII = options.preferASCII
			local numVal = numericValue( FStopVal )
			return localizedFStopString( numVal, preferASCII )

		end

	end,
}

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

add {
	id = 'com.adobe.apertureValue',
	title = "$$$/AgLibrary/Exif/ApertureValue=Aperture",

	format_asset = function( self, assetDoc, options )

		local value = derefOpt( assetDoc, 'xmp', 'exif', 'ApertureValue' )
		options = combinePrefsAndOptions( self, options )

		if value then

			return convertAPEXToAperture( value, options.preferASCII )

		end

	end,
}

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

add {
	id = 'com.adobe.maxApertureValue',
	title = "$$$/AgLibrary/Exif/MaxApertureValue=Max Aperture",

	format_asset = function( self, assetDoc, options )

		local val = derefOpt( assetDoc, 'xmp', 'exif', 'MaxApertureValue' )
		options = combinePrefsAndOptions( self, options )

		if val then

			return convertAPEXToAperture( val, options.preferASCII )

		end

	end,
}

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

add {
	id = 'com.adobe.exposureBiasValue',
	title = "$$$/AgLibrary/Exif/ExposureBiasValue=Exposure Bias",

	format_asset = function( self, assetDoc, options )

		options = combinePrefsAndOptions( self, options )
		local numerator, denominator = WFCatFormatterUtils.extractNumeratorDenominator( derefOpt( assetDoc, 'xmp', 'exif', 'ExposureBiasValue' ) )
		local numberPart

		if numerator then

			if numerator == 0 then
				numberPart = 0
			else
				numberPart = formatNumeratorDenominator( numerator, denominator, options.preferASCII )
			end
			return 'ZSTR', "$$$/AgMetadataFormatters/exif/ExposureBiasValue=^1 EV", numberPart

		end

	end,
}

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

add {
	id = 'com.adobe.focalLength',
	title = "$$$/AgLibrary/Exif/FocalLength=Focal Length",

	format_asset = function( self, assetDoc, options )

		options = combinePrefsAndOptions( self, options )
		return formatFocalLength( assetDoc, options )

	end,
}

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

add {
	id = 'com.adobe.focalLength35mm',
	title = "$$$/AgLibrary/Exif/FocalLength35mm=Focal Length 35mm",

	format_asset = function( self, assetDoc, options )

		return formatFocalLengthValue( derefOpt( assetDoc, 'xmp', 'exif', 'FocalLengthIn35mmFilm' ),
								   options.focalLengthPrecision )

	end,
}

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

add {
	id = 'com.adobe.focalPlaneXResolution',
	title = "$$$/AgLibrary/Exif/FocalPlaneXResolution=Focal Plane X Resolution",

	format_asset = function( self, assetDoc, options )

		local resolution = derefOpt( assetDoc, 'xmp', 'exif', 'FocalPlaneXResolution' )
		local unit = derefOpt( assetDoc, 'xmp', 'exif', 'FocalPlaneResolutionUnit' )
		return 'literal', formatFocalPlaneResolutionValue( resolution, unit )

	end,
}

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

add {
	id = 'com.adobe.focalPlaneYResolution',
	title = "$$$/AgLibrary/Exif/FocalPlaneYResolution=Focal Plane Y Resolution",

	format_asset = function( self, assetDoc, options )

		local resolution = derefOpt( assetDoc, 'xmp', 'exif', 'FocalPlaneYResolution' )
		local unit = derefOpt( assetDoc, 'xmp', 'exif', 'FocalPlaneResolutionUnit' )
		return 'literal', formatFocalPlaneResolutionValue( resolution, unit )

	end,
}

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

add {
	id = 'com.adobe.lens',
	title = "$$$/AgLibrary/Exif/Lens=Lens",

	format_asset = function( self, assetDoc )

		return optNumberToString( getLensFromDocValue( assetDoc ) )

	end,
}

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

add {
	id = 'com.adobe.brightnessValue',
	title = "$$$/AgLibrary/Exif/BrightnessValue=Brightness Value",

	format_asset = function( self, assetDoc, options )

		local numerator, denominator = WFCatFormatterUtils.extractNumeratorDenominator( derefOpt( assetDoc, 'xmp', 'exif', 'BrightnessValue' ) )
		if numerator and denominator ~= 0 then

			-- Fix PR1321380: "Lightroom should display brightness to two decimal places"

			local numberPart = numerator / denominator
			numberPart = math.floor( numberPart * 100 ) / 100;

			return 'ZSTR', "$$$/AgMetadataFormatters/exif/BrightnessValue=^1", { 'number', numberPart, 2 }

		end

	end,
}

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

add {
	id = 'com.adobe.subjectDistance',
	title = "$$$/AgLibrary/Exif/SubjectDistance=Subject Distance",

	format_asset = function( self, assetDoc, options )

		options = combinePrefsAndOptions( self, options )
		local subjectDistance = derefOpt( assetDoc, 'xmp', 'exif', 'SubjectDistance' )

		if type( subjectDistance ) == 'number' then

			return formatDistance( subjectDistance )

		end

	end,
}

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

add {
	id = 'com.adobe.approximateFocusDistance',
	title = "$$$/AgLibrary/Exif/FocusDistance=Focus Distance",

	format_asset = function( self, assetDoc, options )

		options = combinePrefsAndOptions( self, options )
		local focusDistance = derefOpt( assetDoc, 'xmp', 'aux', 'ApproximateFocusDistance' )

		if type( focusDistance ) == 'number' then

			return formatDistance( focusDistance )

		end

	end,
}

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

add {
	id = 'com.adobe.commonPhotoSettings',
	title = "$$$/AgLibrary/Exif/commonPhotoSettings=Common Photo Settings",

	format_asset = function( self, assetDoc, options )

		options = combinePrefsAndOptions( self, options )
		local exposureStr = { formatExposureAndISO( assetDoc, options ) }
		local lens = { formatLensSetting( assetDoc, options ) }

		if exposureStr[ 1 ] and lens[ 1 ] then

			return 'ZSTR', "$$$/AgMetadataFormatters/exif/commonPhotoSettings/full=^1, ^2",
					exposureStr, lens
		elseif exposureStr[ 1 ] then

			return nunpack( exposureStr )

		elseif lens[ 1 ] then

			return nunpack( lens )

		end

	end,
}

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

add {
	id = 'com.adobe.exposureAndISO',
	title = "$$$/AgLibrary/Exif/exposureAndISO=Exposure and ISO",

	format_asset = function( self, assetDoc, options )

		options = combinePrefsAndOptions( self, options )
		return formatExposureAndISO( assetDoc, options )

	end,
}

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

add {
	id = 'com.adobe.lensSetting',
	title = "$$$/AgLibrary/Exif/lensSetting=Lens Setting",

	format_asset = function( self, assetDoc, options )

		options = combinePrefsAndOptions( self, options )
		return formatLensSetting( assetDoc, options )

	end,
}

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

add {
	id = 'com.adobe.ISOSpeedRating',
	title = "$$$/AgLibrary/Exif/ISOSpeedRating=ISO Speed Rating",

	format_asset = function( self, assetDoc, options )

		local value = derefOpt( assetDoc, 'xmp', 'exif', 'ISOSpeedRatings' )
		if type( value ) == 'table' then
			value = value[ 1 ]
		end

		if value then

			return 'ZSTR', "$$$/AgMetadataFormatters/exif/ISOSpeedRating=ISO ^1", { 'number', value, 0 }

		end

	end,
}

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

add {
	id = 'com.adobe.flash',
	title = "$$$/AgLibrary/Exif/Flash=Flash",

	format_asset = function( self, assetDoc, options )

		local flashFired = derefOpt( assetDoc, 'xmp', 'exif', 'Flash', 'Fired' )
		if flashFired == true then
			return 'ZSTR', "$$$/AgLibrary/Exif/FlashDidFire=Did fire"
		elseif flashFired == false then
			return 'ZSTR', "$$$/AgLibrary/Exif/FlashDidNotFire=Did not fire"
	
		end

	end,
}

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

add {
	id = 'com.adobe.exposureProgram',
	title = "$$$/AgLibrary/Exif/ExposureProgram=Exposure Program",

	format_asset = function( self, assetDoc, options )

		return optToString( derefOpt( assetDoc, 'xmp', 'exif', 'ExposureProgram' ) )

	end,
}

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

add {
	id = 'com.adobe.meteringMode',
	title = "$$$/AgLibrary/Exif/MeteringMode=Metering Mode",

	format_asset = function( self, assetDoc, options )

		return optToString( derefOpt( assetDoc, 'xmp', 'exif', 'MeteringMode' ) )

	end,
}

--==============================================================================
-- @mark	UserComment
--==============================================================================

add {
	id = 'com.adobe.userComment',
	title = "$$$/AgLibrary/Exif/UserComment=User Comment",
	read_only = false,

	format_asset = function( self, assetDoc, options )

		return optToString( derefOpt( assetDoc, 'xmp', 'exif', 'UserComment' ) )

	end,

	asset_overlay = function( self, value )

		return {
			xmp = {
				exif = {
					UserComment = value,
				}
			}
		}

	end,
}

--==============================================================================
-- @mark	Artist / Software
--==============================================================================

add {
	id = 'com.adobe.artist',
	title = "$$$/AgLibrary/Exif/Artist=Artist",

	format_asset = function( self, assetDoc, options )

		return optToString( derefOpt( assetDoc, 'xmp', 'tiff', 'Artist' ) )

	end,
}

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

add {
	id = 'com.adobe.software',
	title = "$$$/AgLibrary/Exif/Software=Software",

	format_asset = function( self, assetDoc, options )

		return optToString( derefOpt( assetDoc, 'xmp', 'xmp', 'CreatorTool' ) )

	end,
}

--==============================================================================
-- @mark	GPS
--==============================================================================

unused 'com.adobe.gps.yahooUrl'
unused 'com.adobe.gps.googleUrl'

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

add {
	id = 'com.adobe.GPS',
	title = "$$$/AgLibrary/Exif/GPS=GPS",
	data_type = 'latitudeLongitude',

	format_asset = function( self, assetDoc, options )

		local latitude = derefOpt( assetDoc, 'location', 'latitude' )
		local longitude = derefOpt( assetDoc, 'location', 'longitude' )

		if tonumber( latitude ) and tonumber( longitude ) then

			local latitudeFmt = normalizeCoord(
				latitude,
				{ 'ZSTR', "$$$/Metadata/Exif/GPS/NorthLatitudeAbbr=N" },
				{ 'ZSTR', "$$$/Metadata/Exif/GPS/SouthLatitudeAbbr=S" }
			)
			local longitudeFmt = normalizeCoord(
				longitude,
				{ 'ZSTR', "$$$/Metadata/Exif/GPS/EastlongitudeAbbr=E" },
				{ 'ZSTR', "$$$/Metadata/Exif/GPS/WestLongitudeAbbr=W" }
			)
			return 'ZSTR', "$$$/AgLibrary/Exif/GPS/Pair=^1 ^2", latitudeFmt, longitudeFmt

		end

	end,

	asset_overlay = function( self, value )

		if type( value ) == 'string' then

			local floatLat, floatLng = WFCatGPSFormatUtils.parseLatLong( value )
			
			return WFCatGPSFormatUtils.getLocationOverlay( floatLat, floatLng )

		end

	end,
}

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

add {
	id = 'com.adobe.GPSLatLng',
	title = "$$$/AgLibrary/Exif/GPS=GPS",
	data_type = 'latitudeLongitude',

	format_asset = function( self, assetDoc, options )

		local latitude = derefOpt( assetDoc, 'location', 'latitude' )
		local longitude = derefOpt( assetDoc, 'location', 'longitude' )

		return { latitude = latitude, longitude = longitude, }

	end,

	asset_overlay = function( self, value )

		if type( value ) == 'table' then

			local lat = value.latitude
			local lng = value.longitude

			if type( lat ) == "number" and type( lng ) == "number" then
				return WFCatGPSFormatUtils.getLocationOverlay( lat, lng )
			end

			return WFCatGPSFormatUtils.getLocationOverlay( nil, nil )

		end

	end,
}

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

add {
	id = 'com.adobe.GPSAltitude',
	title = "$$$/AgLibrary/Exif/Altitude=Altitude",
	data_type = 'distance',

	format_asset = function( self, assetDoc, options )

		local altitude = derefOpt( assetDoc, 'location', 'altitude' )

		if altitude then

			return 'distance', altitude, 1, true

		end

	end,

	asset_overlay = function( self, value )

		local altitude = WFCatGPSFormatUtils.parseElevation( value )
		return WFCatGPSFormatUtils.getElevationOverlay( altitude )

	end,
}

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

add {
	id = 'com.adobe.GPSImgDirection',
	title = "$$$/AgLibrary/Exif/Direction=Direction",

	format_asset = function( self, assetDoc, options )

		return WFCatGPSFormatUtils.formatDirection( derefOpt( assetDoc, 'location', 'imgDirection' ), "cardinal" )

	end,
}

--==============================================================================
-- @mark	DNG Info
--==============================================================================

add {
	id = 'com.adobe.dng.version',
	title = "$$$/AgLibrary/DNG/DNGVersion=DNG Version",

	format_asset = function( self, assetDoc, options )

		return optToString( derefOpt( assetDoc, 'xmp', 'dng', 'DNGVersion' ), "$$$/AgMetadataFormatters/SubjectDistance/Unknown=Unknown" )

	end,
}

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

add {
	id = 'com.adobe.dng.backwardVersion',
	title = "$$$/AgLibrary/DNG/BackwardVersion=DNG Compatibility",

	format_asset = function( self, assetDoc, options )

		return optToString( derefOpt( assetDoc, 'xmp', 'dng', 'DNGBackwardVersion' ), "$$$/AgMetadataFormatters/SubjectDistance/Unknown=Unknown" )

	end,
}

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

add {
	id = 'com.adobe.dng.compatibility',
	title = "$$$/AgLibrary/DNG/CameraRawVersion=Compatibility",

	format_asset = function( self, assetDoc, options )

		local lightroom = derefOpt( assetDoc, 'xmp', 'dng', 'LightroomCompatibility' )
		
		if lightroom then
			return 'ZSTR',
				"$$$/AgLibrary/DNG/CompatibilityTemplate=Lightroom ^1",
				tostring( lightroom )
		end

		return 'ZSTR', "$$$/AgMetadataFormatters/SubjectDistance/Unknown=Unknown"

	end,
}

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

add {
	id = 'com.adobe.dng.hasMosaicData',
	title = "$$$/AgLibrary/DNG/MosaicData=Mosaic Data",

	format_asset = function( self, assetDoc, options )

		return optToBoolean( derefOpt( assetDoc, 'xmp', 'dng', 'HasMosaicData' ) )

	end,
}

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

add {
	id = 'com.adobe.dng.lossyCompression',
	title = "$$$/AgLibrary/DNG/LossyCompression=Lossy Compression",

	format_asset = function( self, assetDoc, options )

		return optToBoolean( derefOpt( assetDoc, 'xmp', 'dng', 'LossyCompression' ) )

	end,
}

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

add {
	id = 'com.adobe.dng.floatingPointType',
	title = "$$$/AgLibrary/DNG/PixelDataType=Pixel Data Type",

	format_asset = function( self, assetDoc, options )

		local pixelDataType = derefOpt( assetDoc, 'xmp', 'dng', 'FloatingPoint' )
		
		if pixelDataType == true then
			return 'ZSTR', "$$$/AgLibrary/DNG/FloatingPoint=Floating Point"
		elseif pixelDataType == false then
			return 'ZSTR', "$$$/AgLibrary/DNG/Integral=Integer"
		end

	end,
}

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

add {
	id = 'com.adobe.dng.hasTransparency',
	title = "$$$/AgLibrary/DNG/TransparencyData=Transparency Data",

	format_asset = function( self, assetDoc, options )

		return optToBoolean( derefOpt( assetDoc, 'xmp', 'dng', 'HasTransparency' ) )

	end,
}

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

local function optToEmbedded( value )

	if value == true then
		return 'ZSTR', "$$$/AgLibrary/DNG/Embedded=Embedded"
	elseif value == false then
		return 'ZSTR', "$$$/AgLibrary/DNG/NoneEmbedded=None Embedded"
	end

end

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

add {
	id = 'com.adobe.dng.hasFastLoadData',
	title = "$$$/AgLibrary/DNG/FastLoadDataEmbedded=Fast Load Data",

	format_asset = function( self, assetDoc, options )

		return optToEmbedded( derefOpt( assetDoc, 'xmp', 'dng', 'HasFastLoadData' ) )

	end,
}

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

add {
	id = 'com.adobe.dng.hasEmbeddedOriginalRawFile',
	title = "$$$/AgLibrary/DNG/OriginalRawFileEmbedded=Original Raw File",

	format_asset = function( self, assetDoc, options )

		return optToEmbedded( derefOpt( assetDoc, 'xmp', 'dng', 'HasEmbeddedOriginalRawFile' ) )

	end,
}

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

add {
	id = 'com.adobe.dng.originalRawFileName',
	title = "$$$/AgLibrary/DNG/OriginalRawFileName=Original Raw File Name",

	format_asset = function( self, assetDoc, options )

		return optToString( derefOpt( assetDoc, 'xmp', 'dng', 'OriginalRawFileName' ) )

	end,
}

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

add {
	id = 'com.adobe.dng.bitsPerSample',
	title = "$$$/AgLibrary/DNG/BitsPerSample=Bits Per Sample",

	format_asset = function( self, assetDoc, options )

		return optNumberToString( derefOpt( assetDoc, 'xmp', 'dng', 'BitsPerSample' ) )

	end,
}

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

local function formatDimensions( width, height )

	if width and height then

		return 'ZSTR', "$$$/AgLibrary/Exif/DimensionsTemplate=^1 x ^2",
							tostring( width ), tostring( height )

	end

end

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

add {
	id = 'com.adobe.dng.previewDimensions',
	title = "$$$/AgLibrary/DNG/PreviewDimensions=Preview Dimensions",

	format_asset = function( self, assetDoc, options )

		return formatDimensions( derefOpt( assetDoc, 'xmp', 'dng', 'PreviewWidth' ), derefOpt( assetDoc, 'xmp', 'dng', 'PreviewLength' ) )

	end,
}

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

add {
	id = 'com.adobe.dng.imageDimensions',
	title = "$$$/AgLibrary/DNG/ImageDimensions=Dimensions",

	format_asset = function( self, assetDoc, options )

		return formatDimensions( derefOpt( assetDoc, 'xmp', 'dng', 'ImageWidth' ), derefOpt( assetDoc, 'xmp', 'dng', 'ImageLength' ) )

	end,
}

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

add {
	id = 'com.adobe.dng.originalImageDimensions',
	title = "$$$/AgLibrary/DNG/OriginalDimensions=Original Dimensions",

	format_asset = function( self, assetDoc, options )

		return formatDimensions( derefOpt( assetDoc, 'xmp', 'dng', 'OriginalImageWidth' ), derefOpt( assetDoc, 'xmp', 'dng', 'OriginalImageLength' ) )

	end,
}

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

add {
	id = 'com.adobe.EnhanceDetailsAlreadyApplied',

	format_asset = function( self, assetDoc )

		local isEnhanced = derefOpt( assetDoc, 'xmp', 'aux', 'EnhanceDetailsAlreadyApplied' )
		
		return "literal", isEnhanced and "yes" or "no"

	end,
}

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

return formatters
