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

WFCatIdUtils.lua

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

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

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

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

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

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

local WFCatIdUtils = {}

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

local logger = WFLogger( 'WFCatIdUtils' )

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

local check = assert( WFArguments.check )
local derefOpt = assert( KSTableUtils.derefOpt )

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

local albumUuidByLocalId = {}
	-- key: album local Id
	-- value: album uuid

local assetUuidByLocalId = {}
	-- key: asset local Id
	-- value: asset uuid
	
local assetSubtypeByLocalId = {}
	-- key: asset local Id
	-- value: asset subtype
	
local assetSubtypeByAssetUuid = {}
	-- key: asset uuid
	-- value: asset subtype
	
local personUuidByLocalId = {}
	-- key: person local Id
	-- value: person uuid

local trashAssetUuidByLocalId = {}
	-- key: trash asset local Id
	-- value: asset uuid

local debug_queryCount = 0 -- DEBUG
	
local incrementQueryCount = function() -- DEBUG
	debug_queryCount = debug_queryCount + 1
end
	
--------------------------------------------------------------------------------
	
local function checkCache( cacheToCheck, localIds )
	
	local cachedResults = {}
		
	for i, localId in ipairs( localIds ) do
	
		local cachedValue = cacheToCheck[ localId ]
			
		if cachedValue then
			table.insert( cachedResults, cachedValue )
		else
			cachedResults = false
			break
		end
	
	end

	return cachedResults

end
	
--------------------------------------------------------------------------------

function WFCatIdUtils._DEBUG_getLocalIdAndSubtypeForOzAssetId( wfCatalog, ozAssetId )

	return wfCatalog:docStore():docValue( ozAssetId )
	:map( function( docVal )
		return { localId = docVal._localId, subtype = docVal.subtype }
	end )
	
end

--------------------------------------------------------------------------------
--[[!
	\brief Fetches the oz-asset-ids corresponding to the local-ids
		passed. If the ones passed as local-ids were already uuids, 
		then we will return the same.

	\param wfCatalog (WFCatalog) catalog
	\param maybeLocalIds (table) list of localids.
	\param maybeIgnoreStackType (boolean) true if the asset id translation should ignore any stack documents

	\return Observable which fires only once with the list of
		oz-asset-ids.
--]]

function WFCatIdUtils.getOzAssetIdsForLocalIds( wfCatalog, maybeLocalIds, maybeIgnoreStackType )
	
	maybeLocalIds = maybeLocalIds or {}
	local idsAreNumbers = type( maybeLocalIds[ 1 ] ) == 'number' -- assume they're all the same type
	
	local sql = [[SELECT mbli.value, assetId, subtype FROM CARRAY( ?, ?, 'char*' ) AS mbli INNER JOIN assets ON  mbli.value == assets.docId]]
	
	if idsAreNumbers then
		sql = [[SELECT mbli.value, assetId, subtype FROM CARRAY( ?, ?, 'int32' ) AS mbli INNER JOIN assets ON  mbli.value == assets.docId]]
	end
	
	-- Check the cache to see if we need to run the query or not
	
	local cachedResults = {}
	
	if maybeIgnoreStackType then
	
		for i, localId in ipairs( maybeLocalIds ) do
		
			local cachedAssetUuid = assetUuidByLocalId[ localId ]
			local cachedSubtype = assetSubtypeByLocalId[ localId ]
			
			if cachedAssetUuid and cachedSubtype then
				
				if cachedSubtype ~= 'stack' then
					table.insert( cachedResults, cachedAssetUuid )
				end
				
			else
				cachedResults = false
				break
			end
		
		end
	
	else
	
		cachedResults = checkCache( assetUuidByLocalId, maybeLocalIds )
		
	end
	
	if cachedResults then
		return Observable.just( cachedResults )
	end
	
	-- First run the query without quiesceUntilUpToDate. If we get all the Uuids, we're done. 
	-- If not rerun the query WITH quiesceUntilUpToDate.
	
	local assetIdsQueryFn = function( quiesceUntilUpToDate )
		
		quiesceUntilUpToDate = quiesceUntilUpToDate or false
		
		return wfCatalog:indexStore():queryOnce {
				sql = sql,
				quiesceUntilUpToDate = quiesceUntilUpToDate,
				maybeLocalIds,
				#maybeLocalIds,
			}
	
	end
	
	if not idsAreNumbers then
		-- ids are strings so we'll assume they're full asset ids and not local doc ids and
		-- just return them
		return Observable.just( maybeLocalIds or {} )
	end
	
	if maybeIgnoreStackType then
		-- we need to filter stacks out so we need to go to the index store tables as
		-- type/subtype information is not available in the doc store tables
		incrementQueryCount()
		
		return assetIdsQueryFn( true ) 
				-- if we need to ignore stacks, then we need to make this always quiesceUntilUpToDate
			:map( function( rows )
				local result = {}
				for i, row in ipairs( rows ) do
				
					local localId = row[ 1 ]
					local assetId = row[ 2 ]
					local subtype = row[ 3 ]
				
					if subtype ~= 'stack' then
						table.insert( result, assetId )
					end
					
					assetUuidByLocalId[ localId ] = assetId
					assetSubtypeByLocalId[ localId ] = subtype
					assetSubtypeByAssetUuid[ assetId ] = subtype
					
				end
				return result
			end )
	else
	
		-- if we don't need to ignore stacks, then we can go straight to the doc store id translation
		return wfCatalog:docStore():bulkLocalIdToDocIdAsMap( maybeLocalIds )
			:map( function( idMap )
				local result = {}
				for i, localId in ipairs( maybeLocalIds ) do
					local assetId = idMap[ localId ]
					result[ i ] = assetId
				end
				return result
			end )
	end
			
end

--------------------------------------------------------------------------------
--[[!	
	\brief Fetches the oz-asset-ids and asset type corresponding to the local-ids
		passed. If the ones passed as local-ids were already uuids, 
		then we will return the same.

	\param wfCatalog (WFCatalog) catalog
	\param maybeLocalIds (table) list of localids. 

	\return Observable which fires only once with the list of
		oz-asset-ids and asset type pairs
--]]

function WFCatIdUtils.getOzAssetIdsAndTypeForIds( wfCatalog, maybeLocalIds )
	
	maybeLocalIds = maybeLocalIds or {}
	local idsAreNumbers = type( maybeLocalIds[ 1 ] ) == 'number' -- assume they're all the same type
	
	local sql
	
	local cachedResults = {}
	
	if idsAreNumbers then 
	
		for i, localId in ipairs( maybeLocalIds ) do
	
			local cachedAssetUuid = assetUuidByLocalId[ localId ]
			local cachedSubtype = assetSubtypeByLocalId[ localId ]
			
			if cachedAssetUuid and cachedSubtype then
				
				local row = {
					cachedAssetUuid,
					cachedSubtype
				}
				
				table.insert( cachedResults, row )
				
			else
				cachedResults = false
				break
			end
		
		end
	
		sql = [[SELECT assets.docId, assetId, subtype FROM assets INNER JOIN CARRAY( ?, ?, 'int32' ) AS mbli ON mbli.value == assets.docId]]
		
	else
	
		for i, assetUuid in ipairs( maybeLocalIds ) do
	
			local cachedSubtype = assetSubtypeByAssetUuid[ assetUuid ]
			
			if assetUuid and cachedSubtype then
				
				local row = {
					assetUuid,
					cachedSubtype
				}
				
				table.insert( cachedResults, row )
				
			else
				cachedResults = false
				break
			end
		
		end
	
		sql = [[SELECT assets.docId, assetId, subtype FROM assets INNER JOIN CARRAY( ?, ?, 'char*' ) AS mbli ON mbli.value == assets.assetId]]
		
	end

	if cachedResults then
		return Observable.just( cachedResults )
	end
	
	incrementQueryCount()
	
	-- First run the query without quiesceUntilUpToDate. If we get all the Uuids, we're done. 
	-- If not rerun the query WITH quiesceUntilUpToDate.
	
	local getAssetIdsAndTypesQuery = function( quiesceUntilUpToDate )
		
		quiesceUntilUpToDate = quiesceUntilUpToDate or false
		
		return wfCatalog:indexStore():queryOnce {
				sql = sql,
				quiesceUntilUpToDate = quiesceUntilUpToDate,
				maybeLocalIds,
				#maybeLocalIds,
			}
	
	end
	
	return getAssetIdsAndTypesQuery( false )
	
		:flatMap( function( rows )
		
			if #rows ~= #maybeLocalIds then
				logger:warn( 'WFCatIdUtils.getOzAssetIdsAndTypeForIds retrying query with quiesceUntilUpToDate' )
					-- Maybe record this in highbeam? Or diagnostic log?
				return getAssetIdsAndTypesQuery( true )
			end
		
			return Observable.just( rows )
		
		end )

		:map( function( rows )

			local result = {}

			for i, row in ipairs( rows ) do
			
				local localId = row[ 1 ]
				local assetId = row[ 2 ]
				local subtype = row[ 3 ]
				
				table.insert( result, {
					assetId,
					subtype
				} )
				
				assetUuidByLocalId[ localId ] = assetId
				assetSubtypeByLocalId[ localId ] = subtype
				assetSubtypeByAssetUuid[ assetId ] = subtype
				
			end

			return result

		end )
		
end

--------------------------------------------------------------------------------
--[[!
	\brief Fetches the oz-asset-ids corresponding to the local trash ids
		passed. If the ones passed as local trash ids were already uuids,
		then we will return the same.

	\param wfCatalog (WFCatalog) catalog
	\param maybeLocalTrashIds (table) list of local trash ids.

	\return Observable which fires only once with the list of
		oz-asset-ids.
--]]

function WFCatIdUtils.getOzTrashAssetIdsForLocalIds( wfCatalog, maybeLocalTrashIds )
	
	maybeLocalTrashIds = maybeLocalTrashIds or {}
	local idsAreNumbers = type( maybeLocalTrashIds[ 1 ] ) == 'number' -- assume they're all the same type

	-- Check the cache to see if we need to run the query or not
	local cachedResults = checkCache( trashAssetUuidByLocalId, maybeLocalTrashIds )
	if cachedResults then
		return Observable.just( cachedResults )
	end

	if idsAreNumbers then

		incrementQueryCount()

		return wfCatalog:docStore():bulkLocalIdToDocIdAsMap( maybeLocalTrashIds )
			:map( function( idMap )
				local result = {}
				for i, localId in ipairs( maybeLocalTrashIds ) do
					local assetId = idMap[ localId ]
					trashAssetUuidByLocalId[ localId ] = assetId
					result[ i ] = assetId
				end
				return result
			end )

	else

		return Observable.just( maybeLocalTrashIds or {} )

	end

end

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

--[[!
	\brief Fetches the oz album ids corresponding to the ids passed in. If the ids passed in 
		are already uuids, then we return the same.
		
	\param wfCatalog( WFCatalog) catalog
	\param maybeLocalIds (table) list of ids
	
	\return Observable which fires only once with the list of oz album ids
--]]
function WFCatIdUtils.getOzAlbumIdsForLocalId( wfCatalog, maybeLocalIds )

	maybeLocalIds = maybeLocalIds or {}
	local idsAreNumbers = type( maybeLocalIds[ 1 ] ) == 'number' -- assume they're all the same type
	
	if idsAreNumbers then
	
		-- Check the cache to see if we need to run the query or not
		
		local cachedResults = checkCache( albumUuidByLocalId, maybeLocalIds )

		if cachedResults then
			return Observable.just( cachedResults )
		end
	
		incrementQueryCount()
	
		-- Else, run the query.
	
		-- First run the query without quiesceUntilUpToDate. If we get all the Uuids, we're done. 
		-- If not rerun the query WITH quiesceUntilUpToDate.
		return wfCatalog:docStore():bulkLocalIdToDocIdAsMap( maybeLocalIds )
			:map( function( idMap )
				local result = {}
				for i, localId in ipairs( maybeLocalIds ) do
					local assetId = idMap[ localId ]
					albumUuidByLocalId[ localId ] = assetId
					result[ i ] = assetId
				end
				return result
			end )
	
	else
	
		return Observable.just( maybeLocalIds or {} )
	
	end

end

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

--[[!
	\brief Finds the uuid for an album's local-id

	\param wfCatalog (WFCatalog) The catalog.
	\param albumId (string) The local-id of the album

	\return <code>Observable< ></code> An Observable that emits the uuid of the album.
]]--

function WFCatIdUtils.albumUuidForMaybeLocalId( wfCatalog, maybeLocalAlbumId )
	
	check( "WFCatalog", '+string|number' )

	local cachedAlbumUuid = albumUuidByLocalId[ maybeLocalAlbumId ]
		
	if cachedAlbumUuid then
		return Observable.just( cachedAlbumUuid )
	end

	if type( maybeLocalAlbumId ) == 'number' then
	
		incrementQueryCount()
		
		-- First run the query without quiesceUntilUpToDate. If we get all the Uuids, we're done. 
		-- If not rerun the query WITH quiesceUntilUpToDate.
	
		local getAlbumIdQuery = function( quiesceUntilUpToDate )
			
			quiesceUntilUpToDate = quiesceUntilUpToDate or false
			
			return wfCatalog:indexStore():queryOnce {
					sql = [[SELECT albumId FROM albums WHERE docId = ?
								UNION 
							SELECT albumId FROM project_albums WHERE docId = ?]],
					quiesceUntilUpToDate = quiesceUntilUpToDate,
					singleColumnQuery = true,
					maybeLocalAlbumId,
					maybeLocalAlbumId,
				}
		
		end
		
		local albumUuidObservable = getAlbumIdQuery( false )
		
			:flatMap( function( albumIds )
			
				local maybeAlbumUuid = derefOpt( albumIds, 1 ) 
			
				if not maybeAlbumUuid then
					logger:warn( 'WFCatIdUtils.albumUuidForMaybeLocalId retrying query with quiesceUntilUpToDate' )
						-- Maybe record this in highbeam? Or diagnostic log?
					return getAlbumIdQuery( true )
				end
			
				return Observable.just( albumIds )
			
			end )
			
			:map( function( albumIds )
			
				logger:debugf( 'WFCatIdUtils.albumUuidForMaybeLocalId for localId %s complete w/ %d rows', maybeLocalAlbumId, #albumIds )
			
				local maybeAlbumUuid = derefOpt( albumIds, 1 )
				
				if not maybeAlbumUuid then
					return error( "No such album: " .. tostring( maybeLocalAlbumId ) )
				else
					albumUuidByLocalId[ maybeLocalAlbumId ] = maybeAlbumUuid
				end
				
				return maybeAlbumUuid
				
			end )
		
		return albumUuidObservable
	
	end

	-- maybeLocalAlbumId is already string
	return Observable.just( maybeLocalAlbumId )
	
end


--------------------------------------------------------------------------------
--[[!
	\brief Fetches the oz person id corresponding to the local-id
		passed. If the one passed as local-id were already uuid, 
		then we will return the same.

	\param wfCatalog (WFCatalog) catalog
	\param maybeLocalPersonId (string/number) local-id of a person.

	\return Observable which fires only once with the oz-person-id.
--]]
function WFCatIdUtils.getOzPersonIdForMaybeLocalId( wfCatalog, maybeLocalPersonId )
	check( "WFCatalog", '+string|number' )
	
	local cachedPersonUuid = personUuidByLocalId[ maybeLocalPersonId ]
		
	if cachedPersonUuid then
		return Observable.just( cachedPersonUuid )
	end
	
	if type( maybeLocalPersonId ) == 'number' then
		incrementQueryCount()
		
		local getPersonIdQuery = function( quiesceUntilUpToDate )
		
			quiesceUntilUpToDate = quiesceUntilUpToDate or false
			
			return wfCatalog:indexStore():queryOnce {
					sql = "SELECT personId FROM person WHERE docId == ?",
					quiesceUntilUpToDate = quiesceUntilUpToDate,
					singleColumnQuery = true,
					maybeLocalPersonId
				}
		
		end
		
		local personUuidObservable = getPersonIdQuery( false )
		
			:flatMap( function( personIds )
			
				local maybePersonUuid = derefOpt( personIds, 1 ) 
			
				if not maybePersonUuid then
					logger:warn( 'WFCatIdUtils.getOzPersonIdForMaybeLocalId retrying query with quiesceUntilUpToDate' )
						-- Maybe record this in highbeam? Or diagnostic log?
					return getPersonIdQuery( true )
				end
			
				return Observable.just( personIds )
			
			end )
			
			:map( function( personIds )
			
				logger:debugf( 'WFCatIdUtils.getOzPersonIdForMaybeLocalId for localId %s complete w/ %d rows', maybeLocalPersonId, #personIds )
			
				local maybePersonUuid = derefOpt( personIds, 1 )
				
				if not maybePersonUuid then
					return error( "No such person: " .. tostring( maybeLocalPersonId ) )
				else
					personUuidByLocalId[ maybeLocalPersonId ] = maybePersonUuid
				end
				
				return maybePersonUuid
				
			end )
		
		return personUuidObservable
	else
		return Observable.just( maybeLocalPersonId )
	end
end

--------------------------------------------------------------------------------
--[[!
	\brief Fetches the oz people id corresponding to the local-ids
		passed. If the one passed as local-id were already uuid, 
		then we will return the same.

	\param wfCatalog (WFCatalog) catalog
	\param maybeLocalPersonIds (table) local-id of a people.

	\return Observable which fires only once with the oz-person-id.
--]]
function WFCatIdUtils.getOzPeopleIdForMaybeLocalIds( wfCatalog, maybeLocalPeopleId )
	check( "WFCatalog", 'table' )
	
	maybeLocalPeopleId = maybeLocalPeopleId or {}
	
	local cachedResults = {}
	
	for i, localId in ipairs( maybeLocalPeopleId ) do
		local cachedPersonUuid = personUuidByLocalId[ localId ]
	
		cachedResults[#cachedResults + 1] = cachedPersonUuid
	end
	
	if #cachedResults == #maybeLocalPeopleId then
		return Observable.just( cachedResults )
	end
	
	local maybeLocalPersonId = maybeLocalPeopleId[1] -- hope they're all the same type
	
	if type( maybeLocalPersonId ) == 'number' then 
		incrementQueryCount()
		
		local getPersonIdQuery = function( quiesceUntilUpToDate )
		
			quiesceUntilUpToDate = quiesceUntilUpToDate or false
			
			return wfCatalog:indexStore():queryOnce {
					sql = [[SELECT person.personId, person.docId 
							FROM person 
							INNER JOIN CARRAY( ?, ?, 'int32' ) AS mblpi ON person.docId = mblpi.value ]],
					quiesceUntilUpToDate = quiesceUntilUpToDate,
					maybeLocalPeopleId,
					#maybeLocalPeopleId,
				}

		end
		
		local personUuidObservable = getPersonIdQuery( false )
		
			:flatMap( function( rows )
			
				if #rows ~= #maybeLocalPeopleId then
					logger:warn( 'WFCatIdUtils.getOzPeopleIdForMaybeLocalIds retrying query with quiesceUntilUpToDate' )
						-- Maybe record this in highbeam? Or diagnostic log?
					return getPersonIdQuery( true )
				end
			
				return Observable.just( rows )
			
			end )
			
			:map( function( rows )
				local result = {}
				for _, row in ipairs( rows ) do
					
					local maybePersonUuid = row[1]
					local maybeLocalPersonId = row[2]
					personUuidByLocalId[ maybeLocalPersonId ] = maybePersonUuid
					
					result[#result + 1] = maybePersonUuid

				end
				return result
				
			end )
		
		return personUuidObservable
	else
		return Observable.just( maybeLocalPeopleId )
	end
end


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

--[[!
	\brief DEBUG - Only used in unit tests. Clear the WFCatIdUtils caches used to
		speed up Id translations.

	\return void
]]--

function WFCatIdUtils.debug_clearCaches()

	albumUuidByLocalId = {}
	assetUuidByLocalId = {}
	trashAssetUuidByLocalId = {}
	assetSubtypeByLocalId = {}
	assetSubtypeByAssetUuid = {}
	personUuidByLocalId = {}

end

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

--[[!
	\brief DEBUG - Only used in unit tests.
	
	\return (numter) The number of times this class has run index store queries.
]]--

function WFCatIdUtils.debug_getQueryCount()

	return debug_queryCount

end

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

return WFCatIdUtils
