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

NmbProfileCatalogUtils.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.

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

local Rx = require 'Rx'
local Observable = Rx.Observable

local KSTableUtils = require 'KSTableUtils'
local WFCrypto = require 'WFCrypto'
local WFCatFileSpec = require 'WFCatFileSpec'
--------------------------------------------------------------------------------

local KSTableUtils_derefOpt = assert( KSTableUtils.derefOpt, 
										"KSTableUtils.derefOpt not defined" )

local md5 = assert( KSTableUtils_derefOpt( WFCrypto, "MD5", "digest" ),
						"WFCrypto.MD5.digest does not exist" )

local kProfileType = {
	Unknown = -1,
	Internal = 0,
	Standard = 1,
	Custom = 2,
}

local kProfileCamera = "camera"
local kProfileLens = "lens"
--============================================================================--

local NmbProfileCatalogUtils = {}
local profileCache = {}

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

local function startsWith( str, startStr )

   return string.sub( str, 1, string.len( startStr ) ) == startStr
   
end

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

local function endsWith( str, endStr )

   return string.sub( str, -string.len( endStr ) ) == endStr
   
end

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

local function cacheFilenameAndHrefFn( normalizedDigest )
	
	return function( filename, href )
		
		profileCache[ normalizedDigest ] = { filename = filename, href = href } 
	
	end
	
end

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

local function getCameraProfileFilenameAndHrefForDigest( profileCatalog, resolvedDigest, wfCatalog, 
		optCameraModel, optIsFallbackProfile, optCameraProfileDigest )
	
	local normalizedResolvedDigest = resolvedDigest:lower()
	local normalizedCameraProfileDigest = optCameraProfileDigest and optCameraProfileDigest:lower()
	
	-- Check in custom profile cache first
	
	local customProfileCheckStream
	if optCameraModel and optCameraProfileDigest then
		customProfileCheckStream = wfCatalog:getCatalogProfileCache():getHrefAndFileNameForProfileDigestModel( 
			normalizedCameraProfileDigest, optCameraModel )
	else
		customProfileCheckStream = wfCatalog:getCatalogProfileCache():getHrefAndFileNameForProfileUUID( normalizedResolvedDigest )
	end
	
	return customProfileCheckStream
		:flatMapLatest( function( filename, href )
			
			if filename and href then
				return Observable.just( filename, href )
			end
			
			if optIsFallbackProfile and optCameraModel and normalizedCameraProfileDigest then
				
				-- Applied profile doesn't exist locally. Use model and cameraProfileDigest
				-- to find the link.
				return profileCatalog:getProfileCache():getFilenameAndHrefForModelAndDigest( 
					optCameraModel, normalizedCameraProfileDigest )
			end
			
			-- Applied profile exists locally. Use the resolved digest to find the link.
			return profileCatalog:getProfileCache():getFilenameAndHrefForResolvedDigest( normalizedResolvedDigest )
			
		end )

end

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

local function getLensProfileFilenameAndHrefForDigest( profileCatalog, digest, wfCatalog )
	
	
	local normalizedDigest = digest:lower()
	
	local maybeCacheResult = profileCache[ normalizedDigest ]
	if maybeCacheResult then
		-- Cache has entry.
		if maybeCacheResult.filename and maybeCacheResult.href then
			return Observable.just( maybeCacheResult.filename, maybeCacheResult.href )
		end
	end

	return profileCatalog:indexStore():queryOnce
		{
			sql = [[SELECT filename, href FROM profiles WHERE digest = ?]], 
			quiesceUntilUpToDate = true,
			normalizedDigest
		}
		:map( function( results )
			
			if #results > 0 then
				local filename = KSTableUtils_derefOpt( results, 1, 1 )
				local href = KSTableUtils_derefOpt( results, 1, 2 )
				return filename, href
			end
			
		end )
		:tap( cacheFilenameAndHrefFn( normalizedDigest ) )
end

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

local function getFilenameAndHrefForDigest( profileSubtype, profileCatalog, resolvedDigest, wfCatalog, 
		optCameraModel, optIsFallbackProfile, optCameraProfileDigest )
	
	if not resolvedDigest then
		return Observable.just( nil )
	end

	if profileSubtype == kProfileCamera then
		return getCameraProfileFilenameAndHrefForDigest( profileCatalog, resolvedDigest, wfCatalog, 
			optCameraModel, optIsFallbackProfile, optCameraProfileDigest )
	end
	
	-- Return filename and href for lens profile
	return getLensProfileFilenameAndHrefForDigest( profileCatalog, resolvedDigest, wfCatalog )
end

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

local function addCameraProfileToProfileHash( profileCatalog, wfCatalog, maybeProfileHash, maybeResolvedDigest, 
	optCameraModel, optIsFallbackProfile, optCameraProfileDigest )
	
	return getFilenameAndHrefForDigest( kProfileCamera, profileCatalog, maybeResolvedDigest, wfCatalog,
		optCameraModel, optIsFallbackProfile, optCameraProfileDigest )
			:map( function( filename, href )
				
				if filename and href then
					maybeProfileHash = maybeProfileHash or {}
					maybeProfileHash.camera = {
						filename = filename,
						href = href,
					}
				end
					
				return maybeProfileHash
			end )
end

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

local function addLensProfileToProfileHash( profileCatalog, wfCatalog, maybeProfileHash, maybeLensProfileDigest )

	return getFilenameAndHrefForDigest( kProfileLens, profileCatalog, maybeLensProfileDigest, wfCatalog )
			:map( function( filename, href )
				
				if filename and href then
				
					maybeProfileHash = maybeProfileHash or {}
					maybeProfileHash.lens = {
						filename = filename,
						href = href,
					}
					
				end
					
				return maybeProfileHash
				
			end )

end

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


function NmbProfileCatalogUtils.addCameraAndLensProfilesToDoc( profileCatalog, wfCatalog, assetDocValue, 
	maybeCameraProfileResolvedDigest, maybeLensProfileDigest, optCameraModel, optIsFallbackCameraProfile, optCameraProfileDigest )
	
	local currentProfileHash = KSTableUtils_derefOpt( assetDocValue, 'develop', 'profiles' )
	
	return Observable.just( currentProfileHash )
		:flatMap( function( maybeHash )
			return addCameraProfileToProfileHash( profileCatalog, wfCatalog, maybeHash, maybeCameraProfileResolvedDigest,
				optCameraModel, optIsFallbackCameraProfile, optCameraProfileDigest )
		end )
		:flatMap( function( maybeHash )
			return addLensProfileToProfileHash( profileCatalog, wfCatalog, maybeHash, maybeLensProfileDigest )
		end )
		:map( function( maybeHash )
			
			if maybeHash and next( maybeHash ) then -- don't add an empty profiles hash
				assetDocValue.develop = assetDocValue.develop or {}
				assetDocValue.develop.profiles = maybeHash
			end
			
			return assetDocValue
		end )
end

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

local function lensOrCameraProfileOverlay( wfCatalog, profileCatalog, imageId, 
		maybeProfileFingerprint, typeKey, optCameraModel, optIsFallbackCameraProfile, optCameraProfileDigest )

	if not wfCatalog then
		return
	end
	
	return getFilenameAndHrefForDigest( typeKey, profileCatalog, maybeProfileFingerprint, wfCatalog, 
				optCameraModel, optIsFallbackCameraProfile, optCameraProfileDigest )
		:flatMap( function( filename, href )
			
			if filename and href then
				
				return Observable.just( 
						{
							develop = {
								profiles = { 
									[ typeKey ] = {
										filename = filename,
										href = href,
									}
								}
							}
						} )
				
			else 
				
				-- check to see if we need to remove profile overlay
				return wfCatalog:docStore():docValue( imageId )
					
					:map( function( docValue )
						
						local section = KSTableUtils.derefOpt( docValue, 'develop', 'profiles', typeKey )
						

						if not section then
							-- if there's no section no need to remove it, provide an empty overlay
							return {}
						end
						
						-- if there is a section, we have to remove it. but calculating the overlay 
						-- here is a bit annoying, removing the section may require us to also 
						-- remove the profiles section
						local profilesSection = KSTableUtils_derefOpt( docValue, 'develop', 'profiles' )
						
						
						-- try remove the section, then see if there's anything left in profiles, if not
						-- we want to remove the whole profiles section
						local profilesCopy = KSTableUtils.deepCopy( profilesSection )
						profilesCopy[ typeKey ] = nil
						
						local newProfiles
						if not next( profilesCopy ) then
							newProfiles = { _delete = true }
						else
							newProfiles = { [ typeKey ] = { _delete = true } }
						end
						
						return { 
								develop = { 
									profiles = newProfiles 
								}
							}
						
					end )
			end
			
		end )
end
--------------------------------------------------------------------------------
	-- isFallbackProfile - Whether the cameraProfileUniqueID corresponds to a fallback profile
	-- cameraProfileDigest - The profile digest of the actual camera profile( not the fallback one )
	-- cameraProfileName - The name of the actual camera profile( not the fallback one )
--[[!
	\brief Returns the custom profile localOnly overlay for the asset
	
	\param catalogId (UUID) Id of the current catalog
	\param docValueOverride (table) Table corresponding to the overlay which is to be applied.
	\param maybeIsFallbackProfile (boolean) Whether the cameraProfileUniqueID corresponds to a fallback profile
	\param maybeCameraProfileDigest (string) The profile digest of the actual camera profile( not the fallback one )
	\param maybeCameraProfileName (string) The name of the actual camera profile( not the fallback one )

	\return (table) localOnly overlay
]]--
function NmbProfileCatalogUtils.getCameraProfileInfoOverlay( catalogId, profileCatalog,
	docValueOverride, maybeIsFallbackProfile, maybeCameraProfileDigest,
	maybeCameraProfileName, cameraModelName )

	return NmbProfileCatalogUtils.getProfileType( 
		profileCatalog, maybeCameraProfileName, cameraModelName )
		
		:map( function( profileType )
		
			if profileType == kProfileType.Unknown then
				-- For now, consider profile as internal
				-- if the profile type is Unknown, since
				-- we dont have any other info and we dont 
				-- want this record to get processed again.
				profileType = kProfileType.Internal 
			end
		
			-- Do not add info for embedded profiles
			local cameraProfileDigest = profileType ~= kProfileType.Internal 
				and maybeCameraProfileDigest and maybeCameraProfileDigest:lower()
			
			local isStandard = profileType == kProfileType.Standard
			
			local isMissing = cameraProfileDigest and maybeIsFallbackProfile or nil
			local xmpCameraRawProfileDigest = 
				NmbProfileCatalogUtils.getXmpCameraRawProfileDigest( nil, docValueOverride )
				
			local profileLinkCheckDoneOverlay
			if isMissing and profileType == kProfileType.Custom then
				profileLinkCheckDoneOverlay = { _delete = true }
			end
			
			local localOnly = {
				_localOnly = {
					cameraProfileInfo = {
						profileDigest = cameraProfileDigest or "NA",
						isStandard = isStandard,
						isMissing = isMissing or { _delete = true },
						xmpCameraRawProfileDigest = xmpCameraRawProfileDigest,
						profileLinkCheckDone = profileLinkCheckDoneOverlay,
					}
				},
			}
			
			return localOnly
		end )

end

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

function NmbProfileCatalogUtils.lensProfileOverlay( wfCatalog, profileCatalog, imageId, maybeProfileDigest )

	return lensOrCameraProfileOverlay( wfCatalog, profileCatalog, imageId, maybeProfileDigest, kProfileLens )

end

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

function NmbProfileCatalogUtils.cameraProfileOverlay( wfCatalog, profileCatalog, imageId, maybeResolvedDigest,
	optCameraModel, optIsFallbackCameraProfile, optCameraProfileDigest )

	return lensOrCameraProfileOverlay( wfCatalog, profileCatalog, imageId, maybeResolvedDigest, kProfileCamera, 
		optCameraModel, optIsFallbackCameraProfile, optCameraProfileDigest )
end
--------------------------------------------------------------------------------

function NmbProfileCatalogUtils.getXmpCameraRawProfileDigest( docValue, docValueOverride )

	docValue = docValueOverride or docValue
	local xmpCameraRaw = KSTableUtils_derefOpt( 
				docValue, "develop", "xmpCameraRaw" )
	local xmpCameraRawProfileDigest = type( xmpCameraRaw ) == 'table' 
					and xmpCameraRaw.sha256 
					or xmpCameraRaw
	local xmpDevelopInfo = WFCatFileSpec.getXmpDevelopInfoFromDocValue( docValue ) or {}
	local cameraProfileLink = WFCatFileSpec.getCameraProfileLinkFromDocValue( docValue )
					
	xmpCameraRawProfileDigest = string.format( "%s_%s_%s", xmpCameraRawProfileDigest, 
		xmpDevelopInfo.path, cameraProfileLink )

	return md5( xmpCameraRawProfileDigest )
end


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

function NmbProfileCatalogUtils.isInternalProfile( cameraProfileName )

	-- For certain images, the profile info can be
	-- embedded in the image itself, or it might be
	-- available as part of ACR code. Hence in those
	-- cases, we shouldn't report them as custom profile.
	
	if not cameraProfileName then
		-- No cameraProfile name provided.
		return false
	end

	-- Adapted from cr_params, missingProfile validation.
	
	cameraProfileName = string.lower( cameraProfileName )
	
	local isInternal = cameraProfileName == "embedded"
		or cameraProfileName == "matrix"
		or cameraProfileName =="beta"
		or startsWith( cameraProfileName, "acr" )
		or endsWith( cameraProfileName, " beta" )
	
	return isInternal
	
end

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

function NmbProfileCatalogUtils.getProfileType( profileCatalog, cameraProfileName, cameraModelName )

	if not cameraProfileName then
		-- We cannot determine the profile type without knowing
		-- the profile name. 
		return Observable.just( kProfileType.Unknown )
	end
	
	
	-- Has embedded or internal profile
	if NmbProfileCatalogUtils.isInternalProfile( cameraProfileName ) then
		
		return Observable.just( kProfileType.Internal )
	end
	
	if not cameraModelName then
		-- For profiles which are not embedded ones, we cannot
		-- figure out the profile type without knowing the
		-- camera model.
		return kProfileType.Unknown
	end
	
	-- TODO [ashj 2018-07-26]: 
	-- Camera model exif info might not be there in some images.
	-- In that case, we might need to figure out the type in some
	-- other way, maybe from digest and name.
	
	-- has a profile link which points to user's catalog
	return profileCatalog:getProfileCache():isStandardProfile( cameraProfileName, cameraModelName )
		:map( function( isStandardProfile )
			
			return isStandardProfile and kProfileType.Standard or kProfileType.Custom
		end )
end

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

return NmbProfileCatalogUtils
