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

WFXmp.lua

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

ADOBE CONFIDENTIAL
------------------
Copyright 2015 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.

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

local WFXmpImp = require 'WFXmpImp'

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

local sFieldXmpTypes = {
	boolean = 'boolean',
	integer = 'number',
	rational = 'table',
	string = 'string',
}

local sFieldLuaTypes = {
	boolean = 'boolean',
	[ 'nil' ] = true,
	number = 'integer',
	string = 'string',
	table = 'rational',
}

local sStructFieldXmpTypes = {
	boolean = 'boolean',
	integer = 'number',
	[ 'nil' ] = true,
	rational = false,
		-- Not yet supported
	string = 'string',
}

local sStructFieldLuaTypes = {
	boolean = 'boolean',
	[ 'nil' ] = true,
	number = 'integer',
	string = 'string',
	table = false,
		-- rational not yet supported
}

-- WFXmp does not throw any errors.  Thus we cannot use WFArguments. However,
-- our doxygen integration uses the function called check() for doing typing
-- on our docs, so it is included in the method declarations even though it
-- does nothing.

local function check()end

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

-- @mark WFXmp class

--[[!
	\class WFXmp
	\brief 	This namespace ...

	None of the WFXmp methods throw errors.  Instead, they indicate failure by
	returning false, an error code ( a short string ) and a detailed error
	message.  The error strings are numerous: string versions of then underlying
	XMP toolkit XMP_Error status code enumeraton.  The XMP toolkit enumeratons
	have been shortened by removing the 'kXMPErr_' prefix, for example
	kXMPErr_BadXPath is reported to lua as simply the string 'BadXPath'.  See
	XMPImp.cpp and the XMP toolkit documentation for a complete list of error
	codes.  This behavior is referred to below as a <B>XMP Success result</B>.

	Most of the time, if the intended behavior is to continue on in the face of
	failure, the second and third values in an XMP Success result can be ignored.
	However, when specifying a result type of 'boolean' be sure to disambiguate
	an error from a successful read of a false value.

	XMPFieldType: There are four valid strings that are used to specify how
	to retrieve data from Xmp:
	<ul>
		<li> 'string' retrieves the value as a string -- this is also the default.
		<li> 'integer' retrieves as a number but only if it has no fractional
			component.
		<li> 'boolean' retrieves it as a boolean, automatically decoding 'False',
			'false' and 0 as false and any other non-nil value as true.  Will
			also return nil if the value is not present.
		<li> 'rational' retrieves a specially encoded string (two integers separated
		by a forward slash) as an array containing two (integral) numbers.
	</ul>

]]


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

local WFXmp = {}

--[[!
	\brief The WFXmp namespace contains one function, WFXmp, used to construct
	a WFXmp instance.

	\return (WFXmp)
--]]
function WFXmp.WFXmp()
	-- DOXYGEN STUB
end

--------------------------------------------------------------------------------
WFXmp = {}

--[[!
	\brief Populate the xmp object with metadata from a string containing
		serialized RDF. This buffer must be a complete RDF parse stream.

	\return (bool, ?string, ?string) XMP Success result.
--]]
function WFXmp:parse( xmpString )

	check( 'WFXmp', '+string' ) -- Only for doxygen

	if type( xmpString ) ~= 'string' then
		return false, 'BadParam', 'non-string param passed to wfXmp:parse()'
	end

	return self:_parse( xmpString )
end

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

--[[!
	\brief Close the object, freeing memory for other purpose.

	It is illegal to call any method on a WFXmp instance after it has been closed.
	Doing so will always return an error result.

	\return (bool, ?string, ?string) XMP Success result.
--]]
function WFXmp:close( )
	check( 'WFXmp' ) -- Only for doxygen

	return self:_close( )
end

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

--[[!
	\brief Return a KSData containing the serialized Xmp.

	\return (KSData | bool, ?string, ?string) XMP Success result.
--]]
function WFXmp:serialize( )
	check( 'WFXmp' ) -- Only for doxygen

	return self:_serialize( )
end

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

--[[!
	\brief Fetch a value from a structure in XMP.

	\param namespace (string) : The XMP namespace of the structure and field.
	\param structureName (string) : The name of the structure
	\param fieldNS (string) : The XMP namespace of the field.
	\param fieldName (string) : The field
	\param optType (optional XmpFieldType) : Default 'string'.  What type of
		result is expected.  Note: type 'rational' is not yet supported in
		structures.

	\see XmpFieldType

	\return (string | number | boolean, ?string, ?string) XMP Success result.
--]]
function WFXmp:getStruct( namespace, structure, fieldNS, field, optType )
	check( 'WFXmp', '+string', '+string', '+string', '+string', '?XmpFieldType' )
			-- Only for doxygen

	if type( namespace ) ~= 'string' then
		return false, 'BadParam', 'non-string namespace passed to wfXmp:getStruct()'
	end

	if type( structure ) ~= 'string' then
		return false, 'BadParam', 'non-string structure passed to wfXmp:getStruct()'
	end
	
	if type( fieldNS ) ~= 'string' then
		return false, 'BadParam', 'non-string fieldNS passed to wfXmp:getStruct()'
	end

	if type( field ) ~= 'string' then
		return false, 'BadParam', 'non-string field passed to wfXmp:getStruct()'
	end

	if optType and not sStructFieldXmpTypes[ optType ] then
		return false, 'BadParam', 'Invalid optType passed to wfXmp:getStruct()'
	end

	return self:_getStruct( namespace, structure, fieldNS, field, optType )
end

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

--[[!
	\brief Set a value into a structure in XMP.

	\param namespace (string) : The XMP namespace of the structure.
	\param structure (string) : The name of the structure
	\param fieldNS (string) : The XMP namespace of the field.
	\param fieldName (string) : The field
	\param value (nil | boolean | string | number) : The new value for the
		field in the structure.  Pass nil to remove the field from the
		structure.  If a number is provided, it must be an integer.

	\see XmpFieldType

	\return (boolean, ?string, ?string) XMP Success result.
--]]
function WFXmp:setStruct( namespace, structure, fieldNS, field, value )
	check( 'WFXmp', '+string', '+string', '+string', '+string', 'nil|string|number|boolean' )
			 -- Only for doxygen

	if type( namespace ) ~= 'string' then
		return false, 'BadParam', 'non-string namespace passed to wfXmp:setStruct()'
	end

	if type( structure ) ~= 'string' then
		return false, 'BadParam', 'non-string structure passed to wfXmp:setStruct()'
	end

	if type( field ) ~= 'string' then
		return false, 'BadParam', 'non-string field passed to wfXmp:setStruct()'
	end

	local valType = type( value )
	if not sStructFieldLuaTypes[ valType ] then
		return false, 'BadValue', 'setStruct() value is an invalid type'
	end
	
	return self:_setStruct( namespace, structure, fieldNS, field, value )
end

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

--[[!
	\brief Fetch a localized value from the default language in XMP.

	\param namespace (string) : The XMP namespace of the structure and field.
	\param arrayName (string) : The name of the localized field
	\param optGenericLang (optional, string) : genericLang The name of
		the generic language as an RFC 3066 primary subtag. Can be nil or the
		empty string if no generic language is wanted
	\param optSpecificLang (optional, string) : The name of the specific
		language as an RFC 3066 tag.  Default value is "x-default".

	\return (string | boolean, ?string, ?string) the value or XMP Success result.
--]]
function WFXmp:getLocalizedText( namespace, arrayName, optGenericLang, optSpecificLang )
	check( 'WFXmp', '+string', '+string', '?+string', '?+string' ) -- Only for doxygen

	if type( namespace ) ~= 'string' then
		return false, 'BadParam', 'non-string namespace passed to wfXmp:getLocalizedText()'
	end

	if type( arrayName ) ~= 'string' then
		return false, 'BadParam', 'non-string arrayName passed to wfXmp:getLocalizedText()'
	end

	if optGenericLang ~= nil and type( optGenericLang ) ~= 'string' then
		return false,
			'BadParam',
			'non-string optGenericLang passed to wfXmp:getLocalizedText()'
	end

	if optSpecificLang ~= nil and type( optSpecificLang ) ~= 'string' then
		return false,
			'BadParam',
			'non-string optSpecificLang passed to wfXmp:getLocalizedText()'
	end

	return self:_getLocalizedText( namespace, arrayName, optGenericLang, optSpecificLang )
end

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

--[[!
	\brief Set a localized value into the default language in XMP.

	\param namespace (string) : The XMP namespace of the structure and field.
	\param arrayName (string) : The name of the localized field
	\param value (nil | boolean | string | number) : The new value for the
		field.  Pass nil to remove the default localized value for the field.
		(Note: Changing this field will only affect the default localization,
		any other localizations for this field will be unaffected by
		setLocalizedText().)

	\see XmpFieldType

	\return (boolean, ?string, ?string) XMP Success result.
--]]
function WFXmp:setLocalizedText( namespace, arrayName, optAltTextName, optGenericLang, value )
	check( 'WFXmp', '+string', '+string', '?+string', '?string', '?string' )
			-- Only for doxygen

	if type( namespace ) ~= 'string' then
		return false, 'BadParam', 'non-string namespace passed to wfXmp:setLocalizedText()'
	end

	if type( arrayName ) ~= 'string' then
		return false, 'BadParam', 'non-string arrayName passed to wfXmp:setLocalizedText()'
	end

	if optAltTextName ~= nil and type( optAltTextName ) ~= 'string' then
		return false,
			'BadParam',
			'non-string optAltTextName passed to wfXmp:setLocalizedText()'
	end

	if optGenericLang ~= nil and type( optGenericLang ) ~= 'string' then
		return false,
			'BadParam',
			'non-string optGenericLang passed to wfXmp:setLocalizedText()'
	end

	if value ~= nil and type( value ) ~= 'string' then
		return false,
			'BadValue',
			'non-string value passed to wfXmp:setLocalizedText()'
	end

	return self:_setLocalizedText( namespace, arrayName, optAltTextName, optGenericLang, value )
end

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

--[[!
	\brief Fetch a list of strings from an ordered array of strings in XMP.

	\param namespace (string) : The XMP namespace of the structure and field.
	\param fieldName (string) : The name of the localized field

	\return (table | boolean, ?string, ?string) the array or XMP Success result.
--]]
function WFXmp:getStringList( namespace, fieldName )
	check( 'WFXmp', '+string', '+string' ) -- Only for doxygen

	if type( namespace ) ~= 'string' then
		return false, 'BadParam', 'non-string namespace passed to wfXmp:getStringList()'
	end

	if type( fieldName ) ~= 'string' then
		return false, 'BadParam', 'non-string fieldName passed to wfXmp:getStringList()'
	end

	return self:_getStringList( namespace, fieldName )
end

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

--[[!
	\brief Set a list of strings into an ordered array of strings in XMP.

	\param namespace (string) : The XMP namespace of the structure and field.
	\param fieldName (string) : The name of the localized field
	\param value (nil | table) : The new value for the
		field.  Pass nil to remove ordered array from the XMP.
	\param optUnordered (optional boolean) : Tell XMP that the list is a "bag"
		type instead of an ordered array.

	\return (boolean, ?string, ?string) XMP Success result.
--]]
function WFXmp:setStringList( namespace, fieldName, value, optUnordered )
	check( 'WFXmp', '+string', '+string', '?table', '?boolean' ) -- Only for doxygen

	if type( namespace ) ~= 'string' then
		return false, 'BadParam', 'non-string namespace passed to wfXmp:setStringList()'
	end

	if type( fieldName ) ~= 'string' then
		return false, 'BadParam', 'non-string fieldName passed to wfXmp:setStringList()'
	end

	if value ~= nil and type( value ) ~= 'table' then
		return false, 'BadValue', 'non-array value passed to wfXmp:setStringList()'
	end

	return self:_setStringList( namespace, fieldName, value, optUnordered )
end

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

--[[!
	\brief Composes the path expression for a field in a struct

	\param namespace (string) : The XMP namespace of the struct
	\param structName (string) : The name of the struct
	\param fieldNamespace (string) : The XMP namespace of the struct field
	\param fieldName (string) : The name of the field

	\return (string) The composed path of the specified item
--]]
function WFXmp:composeStructFieldPath( namespace, structName, fieldNamespace, fieldName )
	check( 'WFXmp', '+string', '+string', '+string', '+string' ) -- Only for doxygen

	if type( namespace ) ~= 'string' then
		return false, 'BadParam', 'non-string namespace passed to wfXmp:composeStructFieldPath()'
	end

	if type( structName ) ~= 'string' then
		return false, 'BadParam', 'non-string structName passed to wfXmp:composeStructFieldPath()'
	end

	if type( fieldNamespace ) ~= 'string' then
		return false, 'BadParam', 'non-string fieldNamespace passed to wfXmp:composeStructFieldPath()'
	end
	
	if type( fieldName ) ~= 'string' then
		return false, 'BadParam', 'non-string fieldName passed to wfXmp:composeStructFieldPath()'
	end

	return self:_composeStructFieldPath( namespace, structName, fieldNamespace, fieldName )
end

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

--[[!
	\brief Appends a struct item to the array and returns the path

	\param namespace (string) : The XMP namespace of the array
	\param arrayName (string) : The name of the array
	\param unordered (boolean) : whether the array is a bag

	\return
--]]
function WFXmp:appendStructItemToArray( namespace, arrayName, unordered )
	check( 'WFXmp', '+string', '+string', 'boolean' ) -- Only for doxygen

	if type( namespace ) ~= 'string' then
		return false, 'BadParam', 'non-string namespace passed to wfXmp:appendStructItemToArray()'
	end

	if type( arrayName ) ~= 'string' then
		return false, 'BadParam', 'non-string arrayName passed to wfXmp:appendStructItemToArray()'
	end
	
	if type( unordered ) ~= 'boolean' then
		return false, 'BadParam', 'non-boolean unordered passed to wfXmp:appendStructItemToArray()'
	end

	self:_appendArrayItem( namespace, arrayName, nil, unordered, true )
	local index = self:_countArrayItems( namespace, arrayName )
	local pathToItem = self:_composeArrayItemPath( namespace, arrayName, index )
	
	return pathToItem
end

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

--[[!
	\brief Deletes an item from the array

	\param namespace (string) : The XMP namespace of the array
	\param arrayName (string) : The name of the array
	\param index (integer) : Index of the item to be removed

	\return
--]]
function WFXmp:deleteArrayItem( namespace, arrayName, index )
	check( 'WFXmp', '+string', '+string', 'boolean' ) -- Only for doxygen

	if type( namespace ) ~= 'string' then
		return false, 'BadParam', 'non-string namespace passed to wfXmp:deleteArrayItem()'
	end

	if type( arrayName ) ~= 'string' then
		return false, 'BadParam', 'non-string arrayName passed to wfXmp:deleteArrayItem()'
	end
	
	if type( index ) ~= 'number' then
		return false, 'BadParam', 'non-integer unordered passed to wfXmp:deleteArrayItem()'
	end

	return self:_deleteArrayItem( namespace, arrayName, index )
end

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

--[[!
	\brief Returns the number of items in the array

	\param namespace (string) : The XMP namespace of the array
	\param arrayName (string) : The name of the array

	\return (number) Number of elements in the array
--]]
function WFXmp:countArrayItems( namespace, arrayName )
	check( 'WFXmp', '+string', '+string' ) -- Only for doxygen

	if type( namespace ) ~= 'string' then
		return false, 'BadParam', 'non-string namespace passed to wfXmp:countArrayItems()'
	end

	if type( arrayName ) ~= 'string' then
		return false, 'BadParam', 'non-string arrayName passed to wfXmp:countArrayItems()'
	end

	return self:_countArrayItems( namespace, arrayName )
end

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

--[[!
	\brief Creates the path of the item at a given array index

	\param namespace (string) : The XMP namespace of the array
	\param arrayName (string) : The name of the array
	\param index (number) : The index of the item

	\return (string) path of the item
--]]
function WFXmp:composeArrayItemPath( namespace, arrayName, index )
	check( 'WFXmp', '+string', '+string' ) -- Only for doxygen

	if type( namespace ) ~= 'string' then
		return false, 'BadParam', 'non-string namespace passed to wfXmp:composeArrayItemPath()'
	end

	if type( arrayName ) ~= 'string' then
		return false, 'BadParam', 'non-string arrayName passed to wfXmp:composeArrayItemPath()'
	end
	
	if type( index ) ~= 'number' then
		return false, 'BadParam', 'non-integer index passed to wfXmp:composeArrayItemPath()'
	end

	return self:_composeArrayItemPath( namespace, arrayName, index )
end


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

--[[!
	\brief Checks whether the property is present.

	\param namespace (string) : The XMP namespace
	\param field (string) : The field
	
	\return (boolean) True if the property is has a value
--]]
function WFXmp:hasProperty( namespace, field )

	check( 'WFXmp', '+string', '+string' ) -- Only for doxygen

	if type( namespace ) ~= 'string' then
		return false, 'BadParam', 'non-string namespace passed to wfXmp:hasProperty()'
	end

	if type( field ) ~= 'string' then
		return false, 'BadParam', 'non-string field passed to wfXmp:hasProperty()'
	end

	return self:_hasProperty( namespace, field )

end

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

--[[!
	\brief Fetch a simple value from XMP.

	Note: use care when specifying result type 'boolean'.  To disambiguate
	the XMP containing a valid boolean entry from a failure, you must test
	the second return value -- if nil then the Xmp contains a valid false boolean
	entry.  If non-nil then the field could not be read as a boolean.

	\param namespace (string) : The XMP namespace
	\param field (string) : The field
	\param optType (optional XmpFieldType) : Default 'string'.  What type of
		result is expected.

	\see XmpFieldType

	\return (string | number | boolean, ?string, ?string) XMP Success result.
--]]
function WFXmp:get( namespace, field, optType )

	check( 'WFXmp', '+string', '+string', '?XmpFieldType' ) -- Only for doxygen

	if type( namespace ) ~= 'string' then
		return false, 'BadParam', 'non-string namespace passed to wfXmp:get()'
	end

	if type( field ) ~= 'string' then
		return false, 'BadParam', 'non-string field passed to wfXmp:get()'
	end

	if optType ~= nil and not sFieldXmpTypes[ optType ] then
		return false, 'BadParam', 'wfXmp:get() optType is an invalid XmpFieldType'
	end

	local isRational = optType == 'rational'
	if isRational then
		-- the native code doesn't understand rationals
		-- so we deal with them as strings
		local result, errCode, errMsg = self:_get( namespace, field, 'string' )
		if type( result ) == 'string' then
			local _, _, num, den = string.find( result, "(%d+)/(%d+)" )
			num = tonumber( num )
			den = tonumber( den )
			if num and den then
				return { num, den }
			end
			return false, 'BadValue', 'value could not be parsed as a rational'
		end
		return
			false,
			errCode or 'BadValue',
			errMsg or 'Xmp field contained a value that could not be converted to rational'
	end
	return self:_get( namespace, field, optType )

end

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

--[[!
	\brief Set a simple value into XMP.

	\param namespace (string) : The XMP namespace
	\param field (string) : The field
	\param optType (optional XmpFieldType) : Default 'string'.  What type of
		result is expected.

	\see XmpFieldType

	\return (KSData | bool, ?string, ?string) XMP Success result.
--]]
function WFXmp:set( namespace, field, value )

	check( 'WFXmp', '+string', '+string', 'boolean|nil|string|number|table' )
			 -- Only for doxygen

	if type( namespace ) ~= 'string' then
		return false, 'BadParam', 'non-string namespace passed to WFXmp:set()'
	end

	if type( field ) ~= 'string' then
		return false, 'BadParam', 'non-string field passed to WFXmp:set()'
	end

	local valType = type( value )
	if not sFieldLuaTypes[ valType ] then
		return false, 'BadValue', 'WFXmp:set() value is an invalid type'
	end

	if valType == 'table' then
		local isRational = type( value[ 1 ] ) == 'number'
			and type( value[ 2 ] ) == 'number'
			and #value == 2

		if isRational then
			-- the native code doesn't understand rationals
			-- so we deal with them as strings
			value = string.format( "%d/%d", unpack( value ) )
		else
			return false, 'BadValue', 'WFXmp:set() value is a table but is not a valid rational'
		end

	end

	return self:_set( namespace, field, value )

end

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

--[[!
	\brief Registers the given namespace with XMP utils

	\param namespace (string) : The XMP namespace
	\param suggestedPrefix (string) : Suggested prefix for the ns
	
	\return (boolean) True if the suggested prefix matches
		with the one one the ns is registered with.
		
	Note: This is a static function.
	
	If the process already uses ACR, register
	the namespace using ACR apis, instead of using this.
--]]

local function registerNamespace( namespace, suggestedPrefix )

	check( '+string', '+string' ) -- Only for doxygen

	if type( namespace ) ~= 'string' then
		return false, 'BadParam', 'non-string namespace passed to wfXmp:registerNamespace()'
	end

	if type( suggestedPrefix ) ~= 'string' then
		return false, 'BadParam', 'non-string suggestedPrefix passed to wfXmp:registerNamespace()'
	end

	return WFXmpImp.registerNamespace( namespace, suggestedPrefix )

end

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

local privateMethods = WFXmp
	-- Copy methods implemented in Lua into private data structure.

local function addPrivateMethods( metatable, methodTable )

	for methodName, luamethod in pairs( privateMethods ) do
		assert( methodTable[ methodName ] == nil )
		methodTable[ methodName ] = luamethod
	end

end

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

-- The actual implementation is native.  So redefine namespace.
WFXmp = {
	WFXmp = WFXmpImp.WFXmp,
	[ "< private constructor >" ] = addPrivateMethods,
	
	-- Add other static functions
	registerNamespace = registerNamespace,
}

return WFXmp

