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

WFCatExportMetadata.lua

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

ADOBE CONFIDENTIAL
------------------
Copyright 2016 Adobe Systems Incorporated
All Rights Reserved.

NOTICE: All information contained herein is, and remains 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 are protected by all applicable
intellectual property laws, including trade secret and copyright laws.
Dissemination of this information or reproduction of this material is
strictly forbidden unless prior written permission is obtained from
Adobe Systems Incorporated.

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

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

	-- WF.oz_client.imaging.metadata
local WFXmp = require 'WFXmp'

	-- WF.oz_client.managedCatalog.metadata
local WFCatMetadataSchema = require 'WFCatMetadataSchema'
local WFCatGPSFormatUtils = require 'WFCatGPSFormatUtils'
local WFXMLNS = require 'WFXMLNS'

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

local derefOpt = KSTableUtils.derefOpt

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

local WFCatExportMetadata = {}

--------------------------------------------------------------------------------
-- @mark Converters
--------------------------------------------------------------------------------

local function textConverter( fieldInfo, value )
	assert( type( value ) == 'string', 'Value should be string')
	return value
end

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

local function rationalConverter( fieldInfo, value )
	if type( value ) == 'string' then
		local special = fieldInfo._special
		if type( special ) == 'table' then
			local specialValue = special[ value ]
			if specialValue then
				return specialValue
			end
		end
	end
	
	local num, den = unpack( value )
	num = num and tonumber( num )
	den = den and tonumber( den )
	if ( not num ) or ( not den ) then
		return
	end
	
	local xmpValue = string.format( "%s/%s", num, den)
	
	return xmpValue
end

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

local function dateConverter( fieldInfo, value )
	assert( type( value ) == 'string', 'Value should be string')
	return value
end

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

local function booleanConverter( fieldInfo, value )
	assert( type( value ) == 'boolean', 'Value should be string')
	return value and "True" or "False"
end

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

local function enumConverter( fieldInfo, value )
	assert( type( value ) == 'string', 'Value should be string')
	
	local values = fieldInfo._values or {}
	local enumvalue = values[ value ]
	if enumvalue then
		return enumvalue
	end
end

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

local function nilConverter( fieldInfo, value )
	-- Unknown types get dropped.
end

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

local function passThruConverter( fieldInfo, value )
	return value
end

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

local function integerConverter( fieldInfo, value )
	assert( type( value ) == 'number', 'Value should be number')
	return value
end

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

local function firstArrayElementIntegerConverter( fieldInfo, value )
	--[[
	FIXME:ashj: The value of ISOSpeedRatings, which is of type
	firstArrayElementInteger, after being parsed by the import
	counterpart, is a simple integer. But once it is uploaded
	to oz, the value becomes a list with just this integer
	element. Not sure, what is the actual format intended and
	how this conversion is happening. Supporting both the types
	for time being.
	]]
	if type( value ) == 'table' then
		return value[ 1 ]
	end
	
	if type( value ) == 'number' then
		return value
	end
	
	assert( false, 'Value should be number or table' ) 
	
end

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

local sConverters = {
	text = textConverter,
	rational = rationalConverter,
	date = dateConverter,
	enum = enumConverter,
	boolean = booleanConverter,
	passThru = passThruConverter,
	integer = integerConverter,
	firstArrayElementInteger = firstArrayElementIntegerConverter,
}

--------------------------------------------------------------------------------
-- @mark Setters
--------------------------------------------------------------------------------

local function valueSetter( metadata, defaultNamespace, defaultFieldName, fieldInfo, value )
	local namespace = fieldInfo._namespace or defaultNamespace
	local fieldName = fieldInfo._fieldName or defaultFieldName
	return metadata:set( namespace, fieldName, value )
end

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

local function fieldSetter( metadata, defaultNamespace, defaultFieldName, parentFieldInfo, value)

	local namespace = parentFieldInfo._namespace or defaultNamespace
	local fieldName = parentFieldInfo._fieldName or defaultFieldName
	local subFieldName = parentFieldInfo._xmpSubfield
	local dataType = parentFieldInfo._type

	local converter = sConverters[ dataType ] or nilConverter

	return 
		metadata:setStruct(
			namespace,
			fieldName,
			namespace, -- Using the same ns as the parent
			subFieldName,
			converter(
				parentFieldInfo,
				value
			)
		)

end

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

local function firstArrayElementSetter(
			metadata,
			defaultNamespace,
			defaultFieldName,
			fieldInfo,
			value)

	local namespace = fieldInfo._namespace or defaultNamespace
	local fieldName = fieldInfo._fieldName or defaultFieldName
	
	local list = { value }

	metadata:setStringList( namespace, fieldName, list )

end

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

local function stringListSetter( metadata, defaultNamespace, defaultFieldName, fieldInfo, value )

	local namespace = fieldInfo._namespace or defaultNamespace
	local fieldName = fieldInfo._fieldName or defaultFieldName
	
	metadata:setStringList( namespace, fieldName, value )

end

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

local function stringBagSetter( metadata, defaultNamespace, defaultFieldName, fieldInfo, value )

	local namespace = fieldInfo._namespace or defaultNamespace
	local fieldName = fieldInfo._fieldName or defaultFieldName
	
	if type( value ) == 'table' and next( value ) then		
		local list = {}
		for item, _ in pairs( value ) do
			table.insert( list, item )
		end
		metadata:setStringList( namespace, fieldName, list )
	end
	
end

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

local function altLangDefaultSetter(
			metadata,
			defaultNamespace,
			defaultFieldName,
			fieldInfo,
			value )

	local namespace = fieldInfo._namespace or defaultNamespace
	local fieldName = fieldInfo._fieldName or defaultFieldName
	metadata:setLocalizedText( namespace, fieldName, nil, nil, value )

end

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

local sSetters = {
	field = fieldSetter,
	value = valueSetter,
	firstArrayElement = firstArrayElementSetter,
	stringList = stringListSetter,
	stringBag = stringBagSetter,
	altLangDefault = altLangDefaultSetter,
}

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

local function setFieldsInSection( metadata, xmpSection, schemaDocForSection )

	local defaultNamespace = schemaDocForSection._namespace
	for key, value in pairs( xmpSection ) do
		local fieldInfo = schemaDocForSection[ key ]

		if fieldInfo then
			local dataType
			local dataStructure = 'value'
			if type( fieldInfo ) == 'string' then
				dataType = fieldInfo
				fieldInfo = { _type = dataType }
			else
				dataType = fieldInfo._type
			end
			local xmpField = fieldInfo._xmpField or key
			local xmpSubfield = fieldInfo._xmpSubfield
			if xmpSubfield then
				dataStructure = 'field'
				dataType = 'passThru'
			end
			if dataType == 'stringList' then
				dataStructure = 'stringList'
				dataType = 'passThru'
			end
			if dataType == 'stringBag' then
				dataStructure = 'stringBag'
				dataType = 'passThru'
			end
			if dataType == 'firstArrayElementAsInteger' then
				dataStructure = 'firstArrayElement'
				dataType = 'firstArrayElementInteger'
			end
			if dataType == 'altLangDefault' then
				dataStructure = 'altLangDefault'
				dataType = 'passThru'
			end

			local metadataFieldName = xmpField
			local namespace = fieldInfo._namespace or defaultNamespace
			local converter = sConverters[ dataType ] or nilConverter
			local setter = sSetters[ dataStructure ]
			setter( metadata, namespace, metadataFieldName, fieldInfo, converter( fieldInfo, value) )
		end
	end

end

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

local function setMetadataAccordingToSchema( xmp, optInitialMetadata, schema )
	
	local xmpMetadata = optInitialMetadata or WFXmp.WFXmp( )
	if not xmp then
		return xmpMetadata
	end
	
	for nsPrefix, section in pairs( xmp ) do
		
		local schemaDocForSection = schema[ nsPrefix ]
		if schemaDocForSection then
			setFieldsInSection( xmpMetadata, section, schemaDocForSection )
		end
	end
	
	return xmpMetadata

end

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

--[[!
	\brief Interprets the metadata present in the json table
	and sets the correspoding fields in the output xmp.
	The mapping will be done based on the xmp schema mapping
	provided by oz.

	\param xmp (table) the json table which represent the 
		xmp subsection of the assets payload section.
	\param optInitialMetadata (WFXmp) The initial xmp
		metadata instance to which the xmp data has to be
		applied. 

	\return WFXmp after applying the xmp metadata
]]--

function WFCatExportMetadata.setXmpInfo( xmp, optInitialMetadata )

	return setMetadataAccordingToSchema( xmp, optInitialMetadata, WFCatMetadataSchema.getOzMetadataSchema() )
	
end


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

local function convertRating( docValue, xmpMetadata, userId, deleteIfNotFound )

	local ratingInfo = derefOpt( docValue, "ratings", userId ) or {}

	local rating = tonumber( ratingInfo.rating )

	if rating and rating > 0 and rating < 6 then
		xmpMetadata:set( WFXMLNS.xap, "Rating", rating )

	elseif deleteIfNotFound then
		xmpMetadata:set( WFXMLNS.xap, "Rating", nil )
	end

	return xmpMetadata
end

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

local function numberToFraction( number, precision )

	precision = 10 ^ math.floor( precision )
	local ratio = WFRatio.reduce( { math.floor( number * precision ), precision } )
	
	return string.format( "%d/%d", ratio[1], ratio[2] )

end

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

local function convertLocation( docValue, xmpMetadata, deleteIfNotFound )
	
	local location = derefOpt( docValue, "location" ) or {}
	local hasGpsInfo = false
	
	local latitude = location.latitude
	local longitude = location.longitude

	if latitude and longitude then
	
		local xmpLat = WFCatGPSFormatUtils.xmpLatLonCoordFromFloat( latitude, "lat" )
		local xmpLng = WFCatGPSFormatUtils.xmpLatLonCoordFromFloat( longitude, "lon" )
		
		xmpLat = string.format( "%s,%s%s", xmpLat[ 3 ], xmpLat[ 4 ], xmpLat[ 5 ] )
		xmpLng = string.format( "%s,%s%s", xmpLng[ 3 ], xmpLng[ 4 ], xmpLng[ 5 ] )

		xmpMetadata:set( WFXMLNS.exif, "GPSLatitude", xmpLat )
		xmpMetadata:set( WFXMLNS.exif, "GPSLongitude", xmpLng )
		hasGpsInfo = true
		
	elseif deleteIfNotFound then
		xmpMetadata:set( WFXMLNS.exif, "GPSLatitude", nil )
		xmpMetadata:set( WFXMLNS.exif, "GPSLongitude", nil )
	end
	
	local altitude = location.altitude
	
	if altitude then
	
		local xmpAltitude = numberToFraction( math.abs( altitude ), 4 )
		xmpMetadata:set( WFXMLNS.exif, "GPSAltitude", xmpAltitude )
		xmpMetadata:set( WFXMLNS.exif, "GPSAltitudeRef", altitude < 0 and 1 or nil )
		hasGpsInfo = true
		
	elseif deleteIfNotFound then
		xmpMetadata:set( WFXMLNS.exif, "GPSAltitude", nil )
		xmpMetadata:set( WFXMLNS.exif, "GPSAltitudeRef", nil )
	end

	local direction = location.direction
	if direction then
		local ratio = WFRatio.createFromNumber( direction )
		if ratio then
			xmpMetadata:set( WFXMLNS.exif, "GPSImgDirection", ratio )
			hasGpsInfo = true
		end
	elseif deleteIfNotFound then
		xmpMetadata:set( WFXMLNS.exif, "GPSImgDirection", nil )
	end
	
	local directionRef = WFCatGPSFormatUtils.convertOzRefToGPSDirectionRef( location.reference )
	if directionRef then
		xmpMetadata:set( WFXMLNS.exif, "GPSImgDirectionRef", directionRef )
		hasGpsInfo = true
	elseif deleteIfNotFound then
		xmpMetadata:set( WFXMLNS.exif, "GPSImgDirectionRef", nil )
	end

	if hasGpsInfo then
		xmpMetadata:set( WFXMLNS.exif, "GPSVersionID", "2.2.0.0" )
	elseif deleteIfNotFound then
		xmpMetadata:set( WFXMLNS.exif, "GPSVersionID", nil )
	end
	
	return xmpMetadata
end

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

local _flagXmpGoodMap = {
	pick = true,
	reject = false
}

local function convertFlag( docValue, xmpMetadata, userId, deleteIfNotFound )

	local review = derefOpt( docValue, "reviews", userId ) or {}

	local value = _flagXmpGoodMap[ review.flag ]

	if value ~= nil then

		xmpMetadata:set( WFXMLNS.dm, "good", value )
	elseif deleteIfNotFound then
		xmpMetadata:set( WFXMLNS.dm, "good", nil )
	end

	return xmpMetadata
end

--------------------------------------------------------------------------------
--[[
	This method sets the tiff:orientation with the
	orientation value in develop:userOrientation.

	Xmp:tiff:Orientation will be set at the time
	of import, but will not be modified if the user
	changes the asset orientation. Hence we have to
	set the tiff:orientation from the develop section
	on export.
]]--
local function convertOrientation( docValue, xmpMetadata )

	local orientation = derefOpt( docValue, "develop", "userOrientation" )
	orientation = tonumber( orientation ) or 1

	xmpMetadata:set( WFXMLNS.tiff, "Orientation", orientation )

	return xmpMetadata
end

--------------------------------------------------------------------------------
--[[
	This method sets the exif:DateTimeOriginal with the
	value in captureDate.

	V1 clients are using captureDate field for any
	practical purpose. CaptureDate is a mandatory
	field in asset document. Though we have added
	xmp:exif:DateTimeOriginal, only v2 clients might
	use it for now and even they are supposed to change
	the captureDate field if they modify DateTimeOriginal.
	Hence it makes sense to always use the captureDate.
]]--
local function convertDateTimeOriginal( docValue, xmpMetadata )

	local captureDate = derefOpt( docValue, "captureDate" )

	if not captureDate then
		return xmpMetadata
	end

	xmpMetadata:set( WFXMLNS.exif, "DateTimeOriginal", captureDate )

	return xmpMetadata
end

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

local function convertEditHistory( docValue, xmpMetadata )

	local userUpdated = derefOpt( docValue, "develop", "userUpdated" )

	if not userUpdated then
		return xmpMetadata
	end

	local pathToStruct = xmpMetadata:appendStructItemToArray( WFXMLNS.xmpMM, "History", false )
	xmpMetadata:setStruct( WFXMLNS.xmpMM, pathToStruct, WFXMLNS.stEvt, "action", "edited" )
	xmpMetadata:setStruct( WFXMLNS.xmpMM, pathToStruct, WFXMLNS.stEvt, "when", userUpdated )
	
	return xmpMetadata
end

--------------------------------------------------------------------------------
--[[
	This method removes the face regions if the param
	`removeFaceInfo` is true. After removing the faces,
	if there are no regions left, then we remove the
	region info as well.
]]--
local function maybeRemoveFaceInfo( docValue, xmpMetadata, removeFaceInfo )

	if not removeFaceInfo then
		return xmpMetadata
	end
	
	if not xmpMetadata:hasProperty( WFXMLNS.mwgRegions, 'Regions' ) then
		-- No region data
		return xmpMetadata
	end
	
	local regionListPath = xmpMetadata:composeStructFieldPath( WFXMLNS.mwgRegions, 'Regions', WFXMLNS.mwgRegions, 'RegionList' )
	
	local numOfRegions = xmpMetadata:countArrayItems( WFXMLNS.mwgRegions, regionListPath )
	
	local i = 1
	local unvisitedRegions = numOfRegions
	
	while unvisitedRegions > 0 do
		local regionPath = xmpMetadata:composeArrayItemPath( WFXMLNS.mwgRegions, regionListPath, i )
		local regionType = xmpMetadata:getStruct( WFXMLNS.mwgRegions, regionPath, WFXMLNS.mwgRegions, "Type" )
		if regionType and string.lower( regionType ) == "face" then
			-- Its a face region.
			-- Lets remove this region.
			xmpMetadata:deleteArrayItem( WFXMLNS.mwgRegions, regionListPath, i )
		else
			-- Increment the index only if we didnt delete,
			-- since delete will shift the remaining elements.
			i = i + 1
		end
		
		-- Visited one region. Reduce the count.
		unvisitedRegions = unvisitedRegions - 1
	end
	
	-- After removing all faces, if the regionList is empty, 
	-- then remove the region data as well.
	if xmpMetadata:countArrayItems( WFXMLNS.mwgRegions, regionListPath ) == 0 then
		xmpMetadata:set( WFXMLNS.mwgRegions, 'Regions', nil )
	end
	
	return xmpMetadata
end


--------------------------------------------------------------------------------
--[[
	This method removes the person info if the param
	`removePersonInImageInfo` is true. 
]]--
local function maybeRemovePersonInImageInfo( docValue, xmpMetadata, removePersonInImageInfo )

	if not removePersonInImageInfo then
		return xmpMetadata
	end
	
	if not xmpMetadata:hasProperty( WFXMLNS.Iptc4xmpExt, 'PersonInImage' ) then
		-- No personInImage data
		return xmpMetadata
	end
	
	xmpMetadata:set( WFXMLNS.Iptc4xmpExt, 'PersonInImage', nil )
	
	return xmpMetadata
end

--------------------------------------------------------------------------------
--[[!
	\brief Generally, metadata has to be translated
	using the schema document provided by oz. But
	that doesn't cover some of the metadata. This 
	function takes care of translating those params
	which lie outside of the asset xmp subsection.

	\param userId (UID) id of the current active user.
	\param docValue (table) the asset document value.
	\param optInitialMetadata (WFXmp) The initial xmp
		metadata instance to which the xmp data has to be
		applied. 
	\param deleteIfNotFound (boolean) true if certain
		data should be removed from the final metadata
		if it is not present in the xmp provided.
	\param removeFaceInfo (boolean) true if face info
		should be removed from the final metadata.

	\return WFXmp after applying the xmp metadata
]]--
function WFCatExportMetadata.setExtraXmpInfo( 
	userId, docValue, optInitialMetadata, deleteIfNotFound, removeFaceInfo )

	local xmpMetadata = optInitialMetadata or WFXmp.WFXmp( )

	xmpMetadata = convertRating( docValue, xmpMetadata, userId, deleteIfNotFound )
	xmpMetadata = convertLocation( docValue, xmpMetadata, deleteIfNotFound )
	xmpMetadata = convertFlag( docValue, xmpMetadata, userId, deleteIfNotFound )
	xmpMetadata = convertOrientation( docValue, xmpMetadata )
	xmpMetadata = convertDateTimeOriginal( docValue, xmpMetadata )
	xmpMetadata = convertEditHistory( docValue, xmpMetadata )
	xmpMetadata = maybeRemoveFaceInfo( docValue, xmpMetadata, removeFaceInfo )
	xmpMetadata = maybeRemovePersonInImageInfo( docValue, xmpMetadata, removeFaceInfo )

	return xmpMetadata

end

--------------------------------------------------------------------------------
--[[!
	\brief Generally, metadata has to be translated
	using the schema document provided by oz. But
	that doesn't cover some of the metadata. This 
	function takes care of translating those params
	which lie outside of the asset xmp subsection.
	
	\param docValue (table) the asset document value.
	\param userId (UID) id of the current active user.
	\param xmp (string) The xmp settings which should be 
		used in place of the one from document.
	\param deleteIfNotFound (boolean) true if certain
		data should be removed from the final metadata
		if it is not present in the xmp provided.
	\param removeFaceInfo (boolean) true if face info
		should be removed from the final metadata.

	\return Observable that returns the WFXmp after applying the xmp metadata
]]--
function WFCatExportMetadata.createCompleteXmp( docValue, userId, 
	xmp, deleteIfNotFound, removeFaceInfo )
	
	local xmpDoc = derefOpt( docValue, "xmp" )
	
	local metadata = WFXmp.WFXmp()
	metadata:parse( xmp )
			
	metadata = WFCatExportMetadata.setXmpInfo( xmpDoc, metadata )
	metadata = WFCatExportMetadata.setExtraXmpInfo( 
		userId, docValue, metadata, deleteIfNotFound, removeFaceInfo )
	
	return metadata
	
end
--==============================================================================

return WFCatExportMetadata
