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

WFCatFaces.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.logging
local WFLogger = require 'WFLogger'
local logger = WFLogger "WFCatFaces"

	-- WF.core.utils
local KSIdGenerator = require 'KSIdGenerator'
local WFArguments = require 'WFArguments'
local KSDate = require 'KSDate'
local WFTableMerge = require 'WFTableMerge'
local KSTableUtils = require 'KSTableUtils'

	-- WF.oz_client.managed_catalog
local WFCatIndexUtils = require 'WFCatIndexUtils'
local WFCatUtils = require 'WFCatUtils'
local WFRxBatchedBulkApplyOverlay = require "WFRxBatchedBulkApplyOverlay"

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

local check = WFArguments.check
local derefOpt = KSTableUtils.derefOpt

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

local WFCatFaces = {}

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

local function clearTable( t )
	if t then
		for k,_ in pairs( t ) do
			t[ k ] = nil
		end

		t = nil
	end
end

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

--[[!
	\brief 

	\param 
	\param 

	\return 
--]]

function WFCatFaces.createAddFaceOverlay( faceInfo, faceArea, optPeople )
	
	check( 'table', 'table', '?table' )
	
	local area = {
		x = assert( faceArea.x ),
		y = assert( faceArea.y ),
		w = assert( faceArea.w ),
		h = assert( faceArea.h ),
		unit = "normalized"
	}
	
	
	local regionId = faceInfo.uuid or KSIdGenerator.uuidInLowercaseHex( )
	local name  = faceInfo.name 
	local desc = faceInfo.desc
	local timestamp = faceInfo.timestamp or KSDate.formatDateTimeAsUTC( KSDate.timeNow() ) .. "Z"
	local confidenceLevel = faceInfo.confidenceLevel
	
	local people 
	if optPeople then
		
		people = {}
		for i, person in ipairs( optPeople ) do
			people[ person.id ] = {
				Confirmed = person.confirmed,
				ConfidenceLevel = person.confidenceLevel,
			}
		end
	end
	
	local extensions = {}
	extensions[ "adobe-fi" ] = {
		Timestamp = timestamp,
		ConfidenceLevel = confidenceLevel,
		People = people,
	}
	
	return {
		xmp = {
			["mwg-rs"] = {
				Regions = {
					RegionList = {
						[ regionId ] = {
							Area = area,
							Name = name,
							Description = desc,
							Type = "Face",
							Extensions = extensions,
						}
					}
				}
			}
		}
	}
	
end

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

--[[!
	\brief 

	\param 
	\param 

	\return 
--]]

function WFCatFaces.createAddFaceOverlaysForAsset( assetId, appliedToDimensions, faceDetailsArray )
	
	check( '+string|number', 'table', 'table' )
	
	local overlay = WFCatUtils.createBaseOverlayWithUserCreatedFields( assetId )

	for i, faceDetails in ipairs( faceDetailsArray ) do
		WFTableMerge.applyOverlay( overlay, 
			WFCatFaces.createAddFaceOverlay( faceDetails.faceInfo or {}, faceDetails.faceArea, faceDetails.people ) )
	end
	
	local dimensionOverlay = {
		xmp = {
			["mwg-rs"] = {
				Regions = {
					AppliedToDimensions = {
						w = assert( appliedToDimensions.w ),
						h = assert( appliedToDimensions.h ),
						unit = "pixel",
					}
				}
			}
		}
	}
	
	
	return WFTableMerge.applyOverlay( overlay, dimensionOverlay )
	
end



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

--[[!
	\brief 

	\param 
	\param 

	\return 
--]]

function WFCatFaces.addFacesToAsset( catalog, assetId, appliedToDimensions, faceDetailsArray )
	
	check( 'WFCatalog', '+string|number', 'table', 'table' )
	
	local ok, overlay = pcall( WFCatFaces.createAddFaceOverlaysForAsset, assetId, appliedToDimensions, faceDetailsArray )
	if not ok then
		return Observable.throw( "face_overlay_error" )
	end
	
	return catalog:docStore():applyOverlay( overlay )
	
end

--------------------------------------------------------------------------------
--[[!
	\brief get image count for personIds

	\param wfCatalog (WFCatalog) catalog
	\param imageIds (table) personIDs

	\return Observable. Data: Table containing personId and it's count. key is personid and value is image count
--]]
function WFCatFaces.getPhotoCountforPersonIds( wfCatalog, personIds )
	check( 'WFCatalog', 'table' )
		
	return wfCatalog:indexStore():queryOnce {
		sql = [[ SELECT pi.value, COUNT( DISTINCT faces.docId )
					FROM person 
					INNER JOIN CARRAY( ?, ?, 'char*' ) AS pi ON pi.value == person.personId OR pi.value == person.parentId
					INNER JOIN faces ON person.personId == faces.personId 
					GROUP BY pi.value ]],
					
		personIds,
		#personIds,
	}
	:map( function( rows )
		local t = {}
		
		for _, row in ipairs( rows ) do
			t[ row[ 1 ] ] = row[ 2 ] 
		end
		
		return t
	end )
end

--------------------------------------------------------------------------------
--[[!
	\brief Used in keywords/tags panel for displaying names. This stream is open to show updates and 
			should be closed when the panel is not visible or closed. 

	\param wfCatalog (WFCatalog) catalog
	\param imageIds (table) image-ids.

	\return Observable. Data: Table containing all the people(table) preset in the images-ids.
--]]
function WFCatFaces.getPeopleForImageIds( wfCatalog, imageIds )

	check( 'WFCatalog', 'table' )
		
	return wfCatalog:indexStore():query {
		sql = [[ SELECT ifnull(parent.name, child.name), ifnull(parent.hidden, child.hidden), 
						child.parentId, child.personId, parent.personId,
						faces.docId, faces.assetId, faces.faceId, faces.personId,
						faces.x_axis, faces.y_axis, faces.height, faces.width
					FROM faces 
					INNER JOIN CARRAY(?,?,'int32') AS ii ON ii.value = faces.docId
					LEFT JOIN person as child ON child.personId == faces.personId
					LEFT JOIN person as parent ON parent.personId == child.parentId ]],
		imageIds,
		#imageIds,
		calm = 2,
	}
	:map( function( rows )
	
		local peopleList = {}
		local peopleCount = 1
				
		local noPeopleList = {}
		local noPeopleCount = 1
		
		local hiddenPeopleList = {}
		local hiddenPeopleCount = 1
		
		local total = #rows
		
		for i = 1, total do
			local row = rows[ i ]
			
			local name = row[ 1 ]
			local hidden = row[ 2 ]
			if hidden == 0 then
				hidden = false
			elseif hidden == 1 then
				hidden = true
			end
			
			--local childsParentId = row[ 3 ]
			local personId = row[ 4 ]
			local parentId = row[ 5 ]
			
			local localAssetId = row[ 6 ]
			local ozAssetId = row[ 7 ]
			local faceId = row[ 8 ]
			local facesPersonId = row[ 9 ]
			
			local x_axis = row[ 10 ]
			local y_axis = row[ 11 ]
			local height = row[ 12 ]
			local width = row[ 13 ]
			
			if not facesPersonId or hidden == nil then
				local data = {
					localAssetId = localAssetId,
					ozAssetId = ozAssetId,
					faceId = faceId,
					x_axis = x_axis,
					y_axis = y_axis,
					height = height,
					width = width,
				}
					
				noPeopleList[ noPeopleCount ] = data
				noPeopleCount = noPeopleCount + 1
			else
				local data = {
					name = name,
					localAssetId = localAssetId,
					ozAssetId = ozAssetId,
					faceId = faceId,
					id = parentId or personId,
					childId = personId,
					x_axis = x_axis,
					y_axis = y_axis,
					height = height,
					width = width,
				}
					
				if not hidden then
					peopleList[ peopleCount ] = data
					peopleCount = peopleCount + 1
				else
					hiddenPeopleList[ hiddenPeopleCount ] = data
					hiddenPeopleCount = hiddenPeopleCount + 1
				end
			end
		end
		
		clearTable( rows )
		
		local result = {
			noPeopleList = noPeopleList,
			peopleList = peopleList,
			hiddenPeopleList = hiddenPeopleList,
		}
		
		return result
	end )
end

--------------------------------------------------------------------------------
--go thru the merged Ids set to remove cyclic relationships
-- ie, see if any child is already a parent, then remove the child
				
local function removeChildIfParent( peopleTable, mergedIdsSet )
	for k,_ in pairs( mergedIdsSet ) do
		if peopleTable[ k ] then
			mergedIdsSet[ k ] = nil
		end
	end
end

--------------------------------------------------------------------------------
--[[!
	\brief Used in filter bar for displaying distinct names. This stream is open to show updates and 
			should be closed when the filter bar is not visible or closed. 

	\param wfCatalog (WFCatalog) catalog
	\param imageIds (table) image-ids.

	\return Observable. Data: Table containing all the people(table) preset in the images-ids.
--]]
function WFCatFaces.getDistinctPeopleForImageIds( wfCatalog, imageIds )	
	
	check( 'WFCatalog', 'table' )
	
	return wfCatalog:indexStore():query {
		sql = [[ SELECT DISTINCT ifnull(child.parentId, child.personId),
						ifnull(parent.name, child.name) as n
					FROM faces 
					INNER JOIN CARRAY(?,?,'int32') AS ii ON ii.value = faces.docId
					INNER JOIN person as child ON child.personId == faces.personId
					LEFT JOIN person as parent ON parent.personId == child.parentId 
					WHERE n NOTNULL and ifnull(parent.hidden, child.hidden) == 0 ]],
		imageIds,
		#imageIds,
		calm = 2,
		quiesceUntilUpToDate = true,
	}
end

--------------------------------------------------------------------------------
--[[!
	\brief Used in filter bar for displaying distinct names. This stream is open to show updates and 
			should be closed when the filter bar is not visible or closed. 

	\param wfCatalog (WFCatalog) catalog

	\return Observable. Data: Table containing all the people(table) preset in All Photos. 
			The person table also contains the merge list (person buckets those are merged to that person)
--]]
function WFCatFaces.getDistinctPeopleForAllPhotos( wfCatalog )	
	
	check( 'WFCatalog' )
	
	return wfCatalog:indexStore():query {
		sql = [[ SELECT DISTINCT ifnull(child.parentId, child.personId),
						ifnull(parent.name, child.name) as n
					FROM faces
					INNER JOIN person as child ON child.personId == faces.personId
					LEFT JOIN person as parent ON parent.personId == child.parentId 
					WHERE n NOTNULL and ifnull(parent.hidden, child.hidden) == 0 ]],
		calm = 2,
		quiesceUntilUpToDate = true,
	}
end

--------------------------------------------------------------------------------
--[[!
	\brief Used in People View to display people buckets. This stream is open to show updates and 
			should be closed when the people view is not visible or closed. 

	\param wfCatalog (WFCatalog) catalog

	\return Observable. Data: Table containing all the people(table) whose parentId is NULL 
			and number of photos(count) greater than 0. 
			The person table contains the count, if hidden or not, name and 
			the merge set (person buckets those are merged to that person, 
			i.e their parentId is equal to that person)
--]]

function WFCatFaces.getPeopleInfoForAllAssets( wfCatalog, minimumImagesCount, dontIncludeHidden, namedOnly )
	
	check( 'WFCatalog', '?number', '?boolean', '?boolean' )
	
	return wfCatalog:indexStore():query {
		sql = [[ SELECT ifnull(parent.name, child.name), ifnull(parent.hidden, child.hidden), 
						ifnull(parent.cover, child.cover), ifnull(parent.href, child.href),
						child.parentId, child.personId, parent.personId,
						faces.docId, faces.assetId, faces.x_axis, faces.y_axis, faces.height, faces.width
					FROM faces 
					INNER JOIN person as child ON child.personId == faces.personId
					LEFT JOIN person as parent ON parent.personId == child.parentId ]],
		calm = 2,
	}
	:map( function( rows )	
		
		local peopleTable = {}
		local assetIdToDocIdMap = {}
		
		local total = #rows
		
		for i = 1, total do
			local row = rows[ i ]
			
			local name = row[ 1 ]
			local hidden = row[ 2 ] ~= 0
			local cover = row[ 3 ]
			local href = row[ 4 ]
			
			local childsParentId = row[ 5 ]
			local personId = row[ 6 ]
			local parentId = row[ 7 ]
			
			local assetLocalId = row[ 8 ]
			local assetUUID = row[ 9 ]
			
			local faceInfo = {
				x_axis = row[ 10 ],
				y_axis = row[ 11 ],
				height = row[ 12 ],
				width = row[ 13 ],
			}
			
			if childsParentId and parentId and parentId ~= personId then
				if not peopleTable[ childsParentId ] then
					local mergedIdsSet = {}
					mergedIdsSet[ personId ] = name or true
					
					local c = 0
					local assetSet = {}
					if assetLocalId then
						assetSet[ assetLocalId ] = faceInfo
						c = 1
					end	
					
					local t = {
						id = childsParentId,
						name = name,
						hidden = hidden,
						cover = cover,
						href = href,
						mergedIdsSet = mergedIdsSet,
						assetSet = assetSet,
						count = c,
					}
					
					peopleTable[ childsParentId ] = t
				else
					local t = peopleTable[ childsParentId ]
					t.mergedIdsSet[ personId ] = name or true
					
					if assetLocalId and not t.assetSet[ assetLocalId ] then
						t.assetSet[ assetLocalId ] = faceInfo
						t.count = t.count + 1
					end
				end
			else
				if not peopleTable[ personId ] then
					
					local c = 0
					local assetSet = {}
					if assetLocalId then
						assetSet[ assetLocalId ] = faceInfo
						c = 1
					end	
					
					local t = {
						id = personId,
						name = name,
						hidden = hidden,
						cover = cover,
						href = href,
						mergedIdsSet = {},
						assetSet = assetSet,
						count = c,
					}
					
					peopleTable[ personId ] = t
				else
					local t = peopleTable[ personId ]
					if assetLocalId and not t.assetSet[ assetLocalId ] then
						t.assetSet[ assetLocalId ] = faceInfo
						t.count = t.count + 1
					end
				end
			end
			
			if assetUUID then
				assetIdToDocIdMap[ assetUUID ] = assetLocalId
			end
		end
		
		clearTable( rows )

		local count = minimumImagesCount or 5
		local peopleTableWithMergedDocIds = {}
		for key,v in pairs( peopleTable ) do
			if v.id then
				if not namedOnly or ( namedOnly and v.name ) then
					if not dontIncludeHidden or not v.hidden then
						if v.count and v.count > 0 then
							--dont show people with less than image count unless named
							if v.name or v.count >= count then 
								
								local pickAnotherCover = false
								local localCoverId = v.cover and assetIdToDocIdMap[ v.cover ] or nil
								local coverFaceInfo = nil
								if localCoverId then
									coverFaceInfo = v.assetSet[localCoverId]
									if not coverFaceInfo then
										--logger:warnf( "Cover send by Oz is wrong. No FaceInfo for PersonId : %s Cover: %s", key, v.cover )
										pickAnotherCover = true
									end
								else
									--logger:warnf( "No Cover or Cover set is not in Faces table. PersonId : %s Cover: %s", key, v.cover )
									pickAnotherCover = true
								end
								
								if pickAnotherCover then
									for kk,vv in pairs( v.assetSet ) do
										localCoverId = kk
										coverFaceInfo = vv
										break
									end
								end
								
								removeChildIfParent( peopleTable, v.mergedIdsSet )
								
								local data = {
									id = v.id,
									name = v.name,
									hidden = v.hidden,
									cover = localCoverId,
									coverFaceInfo = coverFaceInfo,
									href = v.href,
									count = v.count,
									mergedIdsSet = v.mergedIdsSet,
								}
								
								peopleTableWithMergedDocIds[ v.id ] = data
							end
						end
					end
				end
			else
				logger:warnf( "WFFaces No Parent for personId : %s", key )
			end
			
			v.assetSet = nil
		end
		
		clearTable( peopleTable )
		clearTable( assetIdToDocIdMap )
		
		return peopleTableWithMergedDocIds
	end )
end

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

local function bulkRemovePeopleFromAssets( wfCatalog, ozFaceIdsAssetIdsPersonIds )
	
	local overlays = {}
	local undoOverlays = {}
	
	for _,value in ipairs( ozFaceIdsAssetIdsPersonIds ) do
		
		local assetId = value.assetId
		local faceId = value.faceId
		local personId = value.personId
		
		if assetId and faceId and personId then
		
			local overlay = {
				_id = assetId,
				subtype = "image",
				type = "asset",
				xmp = {
					["mwg-rs"] = {
						Regions = {
							RegionList = {
								[ faceId ] = {
									Type = "Face",
									Extensions = {
										["adobe-fi"] = {
											People = {
												[ personId ] = {
													Rejected = true,
													Confirmed = false,
												},
											}
										}
									}
								}
							}
						}
					}
				}
			}
			
			overlays[ #overlays + 1 ] = overlay
			
			local undoOverlay = {
				_id = assetId,
				subtype = "image",
				type = "asset",
				xmp = {
					["mwg-rs"] = {
						Regions = {
							RegionList = {
								[ faceId ] = {
									Type = "Face",
									Extensions = {
										["adobe-fi"] = {
											People = {
												[ personId ] = {
													Confirmed = true,
													Rejected = false
												}
											}
										}
									}
								}
							}
						}
					}
				}
			}

			undoOverlays[ #undoOverlays + 1 ] = undoOverlay
		end
	end
	
	local undoToken = require 'WFCatUndo'.saveUndoState( Observable.just( )
				:flatMap( function( )
					return WFRxBatchedBulkApplyOverlay( "undo remove faces", wfCatalog:docStore(), undoOverlays ) 
				end ) )
	
	local result
	if #overlays > 0 then
		result = WFRxBatchedBulkApplyOverlay( "remove faces", wfCatalog:docStore(), overlays )
	else
		result = Observable.just( {} )
	end
	
	return result:map( function()
		return { undoToken = undoToken }
	end )
end

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

--[[!
	\brief Remove people from assets. This function finds the associated faceId and
	removes the combination by calling "bulkRemovePeopleFromAssets"

	\param wfCatalog (WFCatalog) catalog
	\param personIds (table) list of personIds
	\param assetIds (table) list of assetIds

	\return Observable which fires only once.
--]]
function WFCatFaces.removePeopleFromAssets( wfCatalog, personIds, assetIds )
	
	check( 'WFCatalog', 'table', 'table' )
	
	local personIdsMap = {}
	for i,v in ipairs( personIds ) do
		personIdsMap[v] = true
	end
	
	return wfCatalog:indexStore():queryOnce {
		sql = [[SELECT faces.faceId, faces.assetId, faces.personId 
				FROM faces
				INNER JOIN CARRAY( ?, ?, 'int32') AS ai ON faces.docId == ai.value ]],
		assetIds,
		#assetIds,
		quiesceUntilUpToDate = true,
	}
	:map( function( rows )
		local results = {}
		for i, row in ipairs( rows ) do
			local fid = row[ 1 ]
			local aid = row[ 2 ]
			local personId = row[ 3 ]
			
			if personIdsMap[ personId ] then
				local data = {
					faceId = fid,
					assetId = aid,
					personId = personId,
				}
				
				results[#results + 1] = data
			end
		end
		
		return results
	end)
	:flatMap( function( ozFaceIdsAssetIdsPersonIds )
		return bulkRemovePeopleFromAssets( wfCatalog, ozFaceIdsAssetIdsPersonIds )
	end)
end

--------------------------------------------------------------------------------
--[[!
	\brief Remove person from asset. ozFaceIdsAssetIdsPersonIds is a list/array of table 
		containing faceId, assetId and personId. This combination is removed. 
		Look at "bulkRemovePeopleFromAssets"

	\param wfCatalog (WFCatalog) catalog
	\param ozFaceIdsAssetIdsPersonIds (table) list of table containing faceId, 
											assetId and personId

	\return Observable which fires only once.
--]]
function WFCatFaces.removePersonFromTagsPanel( wfCatalog, ozFaceIdsAssetIdsPersonIds )
	
	check( 'WFCatalog', 'table' )
	
	return bulkRemovePeopleFromAssets( wfCatalog, ozFaceIdsAssetIdsPersonIds )
end

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

local function bulkMoveAssetsToAnotherPerson( wfCatalog, ozPersonIdTo, ozFaceIdsAssetIdsPersonIds )
	local overlays = {}
	local undoOverlays = {}
	for _,value in ipairs( ozFaceIdsAssetIdsPersonIds ) do
		local overlay = {
			_id = value.assetId,
			subtype = "image",
			type = "asset",
			xmp = {
				["mwg-rs"] = {
					Regions = {
						RegionList = {
							[ value.faceId ] = {
								Type = "Face",
								Extensions = {
									["adobe-fi"] = {
										People = {
											[ozPersonIdTo] = {
												Confirmed = true,
												Rejected = false
											},
											[value.personId] = {
												Rejected = true,
												Confirmed = false
											},
										}
									}
								}
							}
						}
					}
				}
			}
		}
		
		local undoOverlay = {
			_id = value.assetId,
			subtype = "image",
			type = "asset",
			xmp = {
				["mwg-rs"] = {
					Regions = {
						RegionList = {
							[ value.faceId ] = {
								Type = "Face",
								Extensions = {
									["adobe-fi"] = {
										People = {
											[value.personId] = {
												Confirmed = true,
												Rejected = false
											},
											[ozPersonIdTo] = {
												Rejected = true,
												Confirmed = false
											},
										}
									}
								}
							}
						}
					}
				}
			}
		}

		overlays[ #overlays + 1 ] = overlay
		undoOverlays[ #undoOverlays + 1 ] = undoOverlay
	end
	
	local undoToken = require 'WFCatUndo'.saveUndoState( Observable.just( )
				:flatMap( function( )
					return WFRxBatchedBulkApplyOverlay( "undo move assets to person", wfCatalog:docStore(), undoOverlays ) 
				end ) )
	
	local result
	if #overlays > 0 then
		result = WFRxBatchedBulkApplyOverlay( "move assets to person", wfCatalog:docStore(), overlays )
	else
		result = Observable.just( {} )
	end
	
	return result:map( function()
		return { undoToken = undoToken }
	end )
end

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

--[[!
	\brief Move assets to another person.

	\param wfCatalog (WFCatalog) catalog
	\param ozFaceIdsAssetIdsPersonIds (table) people info list.

	\return Observable which fires only once.
--]]
function WFCatFaces.moveAssetsToAnotherPersonTable( wfCatalog, ozPersonIdTo, ozFaceIdsAssetIdsPersonIds )
	
	check( 'WFCatalog', 'string', 'table' )
	
	return bulkMoveAssetsToAnotherPerson( wfCatalog, ozPersonIdTo, ozFaceIdsAssetIdsPersonIds )
end

--------------------------------------------------------------------------------
--[[!
	\brief Move assets to another person.

	\param wfCatalog (WFCatalog) catalog
	\param personIdTo (uuid) destination person id
	\param personIds (table) source person ids.
	\param assetIds (table) local asset-ids

	\return Observable which fires only once.
--]]
function WFCatFaces.moveAssetsToAnotherPerson( wfCatalog, personIdTo, personIds, assetIds )
	
	check( 'WFCatalog', 'uuid', 'table', 'table' )
	
	local personIdsMap = {}
	for i,v in ipairs( personIds ) do
		personIdsMap[v] = true
	end
	
	return wfCatalog:indexStore():queryOnce {
		sql = [[SELECT faces.faceId, faces.assetId, faces.personId 
				FROM faces
				INNER JOIN CARRAY( ?, ?,'int32') AS ai ON faces.docId == ai.value]],
		assetIds,
		#assetIds,
		quiesceUntilUpToDate = true,
	}
	:map( function( rows )
		local results = {}
		for i, row in ipairs( rows ) do
			local fid = row[ 1 ]
			local aid = row[ 2 ]
			local personId = row[ 3 ]
			
			if personIdsMap[ personId ] then
				local data = {}
				data.faceId = fid
				data.assetId = aid
				data.personId = personId
				
				results[#results + 1] = data
			end
		end
		
		return results
	end)
	:flatMap( function( ozFaceIdsAssetIdsPersonIds )
		return bulkMoveAssetsToAnotherPerson( wfCatalog, personIdTo, ozFaceIdsAssetIdsPersonIds )
	end)
end

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

--[[!
	\brief add face to asset.

	\param wfCatalog (WFCatalog) catalog

	\return Observable which fires only once.
--]]
function WFCatFaces.addFaceToAsset( wfCatalog, assetId, faceId, personId )
	
	check( 'WFCatalog', 'uuid', 'uuid', 'uuid' )
	
	local overlay = {
		_id = assetId,
		subtype = "image",
		type = "asset",
		xmp = {
			["mwg-rs"] = {
				Regions = {
					RegionList = {
						[ faceId ] = {
							Type = "Face",
							Extensions = {
								["adobe-fi"] = {
									People = {
										[personId] = {
											Rejected = false,
											Confirmed = true
										},
									}
								}
							}
						}
					}
				}
			}
		}
	}
	
	return wfCatalog:docStore():applyOverlay( overlay )
end
		
--------------------------------------------------------------------------------

--[[!
	\brief Update Face Region

	\param wfCatalog (WFCatalog) catalog

	\return Observable which fires only once.
--]]
function WFCatFaces.updateFaceRegion( wfCatalog, localAssetId, faceId, faceArea )
	
	check( 'WFCatalog', 'number', 'uuid', 'table' )
	
	local area = {
		x = assert( faceArea.x ),
		y = assert( faceArea.y ),
		w = assert( faceArea.w ),
		h = assert( faceArea.h ),
	}
	
	for k,v in pairs( area ) do
		if v < 0 or v > 1 then
			assert( false, "Face Area param should be between 0 and 1" )
		end
	end
	
	area.unit = "normalized"
	
	local overlay = {
		_id = localAssetId,
		subtype = "image",
		type = "asset",
		xmp = {
			["mwg-rs"] = {
				Regions = {
					RegionList = {
						[ faceId ] = {
							Type = "Face",
							Area = area,
						}
					}
				}
			}
		}
	}
	
	return wfCatalog:docStore():applyOverlay( overlay )
end

--------------------------------------------------------------------------------
--[[!
	\brief Removes "faceid" section from "RegionList" inside "xmp". 
			Refer to XMP Data Model http://apidocs.adbecrsl.com/v2/data-model/asset/xmp.html
			If there are no Regions, then remove "mwg-rs" as well

	\param wfCatalog (WFCatalog) catalog
	\param assetFaceMap (table) asset to face map.
	\param assetDocValues (table) asset doc values.

	\return Observable
--]]
local function bulkRemoveFacesFromAssets( wfCatalog, assetIdsFaceIds, assetDocValues, isOzId )
	
	check( 'WFCatalog', 'table', 'table', '?boolean' )
	
	local overlays = {}
			
	for _, docValue in ipairs( assetDocValues ) do
		local faceValue
		
		local assetId = docValue._localId
		if isOzId then
			assetId = docValue._id
		end
		
		local ozFaceIds = assetIdsFaceIds[ assetId ] 
		
		local regionList = derefOpt( docValue, 'xmp','mwg-rs', 'Regions', 'RegionList' )
		local count = 0
		local match = 0
		for key,value in pairs( regionList or {} ) do
			if ozFaceIds[ key ] == true then
				match = match + 1
			end
			count = count + 1
		end
		
		if count == 0 or match == 0 then
		
			return Observable.just( {} )
		
		--{Hari} adding this logic because Oz does not accept empty RegionList 
		elseif match == count then
			faceValue = {
				_id = assetId,
				subtype = "image",
				type = "asset",
				xmp = {
					["mwg-rs"] = {
						_delete = true,
					}
				}
			}
		else
			local deleteRegionList = {}
			for key,value in pairs( ozFaceIds ) do
				deleteRegionList[ key ] = {
					_delete = true,
				}
			end
			
			faceValue = {
				_id = assetId,
				subtype = "image",
				type = "asset",
				xmp = {
					["mwg-rs"] = {
						Regions = {
							RegionList = deleteRegionList
						},
					},
				},
			}
		end
		
		overlays[ #overlays + 1 ] = faceValue
	end
	
	if #overlays > 0 then
		return WFRxBatchedBulkApplyOverlay( "Remove Faces From Assets", wfCatalog:docStore(), overlays )
	else
		return Observable.just( {} )
	end
end

--------------------------------------------------------------------------------
--[[!
	\brief Remove faces from assets.

	\param wfCatalog (WFCatalog) catalog
	\param assetFaceMap (table) asset to face map.

	\return Observable which fires only once.
--]]
function WFCatFaces.removeFaceRegionsFromAssets( wfCatalog, assetFaceMap )
	
	check( 'WFCatalog', 'table' )
	
	local localAssetIds = {}
	local localAssetIdsOzFaceIds = {}
	for k,v in pairs( assetFaceMap ) do
		localAssetIds[ #localAssetIds + 1 ] = k
		
		local aid = k
		local fid = v
		
		if not localAssetIdsOzFaceIds[ aid ] then
			local faces = {}
			faces[ fid ] = true
			localAssetIdsOzFaceIds[ aid ] = faces
		else
			local faces = localAssetIdsOzFaceIds[ aid ]
			faces[ fid ] = true
			localAssetIdsOzFaceIds[ aid ] = faces
		end
	end
	
	return wfCatalog:docStore():bulkDocValue( localAssetIds )
		:map( function( docValues )
			return localAssetIdsOzFaceIds, docValues
		end )
        :flatMap( function( localAssetIdsOzFaceIds, assetDocValues )
			return bulkRemoveFacesFromAssets( wfCatalog, localAssetIdsOzFaceIds, assetDocValues )
		end )
		:take( 1 )
end

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

function WFCatFaces.generateIndexSchema( indexStoreSchema )

	check( 'WFRxIndexStoreSchema' )
	
	indexStoreSchema:addTable {

		sql = [[CREATE TABLE faces( faceId, assetId, personId, x_axis, y_axis, width, height )]],

		useLocalId = true,
		
		hasOneRowPerDocument = false,
		
		relevantTypeSubtypeTuples = {
			{ 'asset', 'image' },
			{ 'asset', 'video' },
		},
		
		map = function( docValue, emit )

			if WFCatIndexUtils.isValidAssetDocument( docValue ) 
				and ( docValue.subtype == 'image' or docValue.subtype == 'video' ) then
					-- Stacks do have same properties as the main asset.
					-- Hence consider only normal assets here.
					-- Also don't bother with trash assets (deleted_image, deleted_video)
				
				local regionList = derefOpt( docValue, 'xmp','mwg-rs', 'Regions', 'RegionList' )
				
				for k, region in pairs( regionList or {} ) do
					
					if region.Type == "Face" then
					
						local area = derefOpt( region, 'Area' )
						
						local people = derefOpt( region, 'Extensions', 'adobe-fi', 'People' )
						
						if people then
							for personId, person in pairs( people ) do
								if not person.Rejected then
									emit( k, docValue._id, personId, area.x, area.y, area.w, area.h )
								end
							end
						else
							emit( k, docValue._id, nil, area.x, area.y, area.w, area.h )
						end
					end
				end
				
			end

		end,
	}
	
	indexStoreSchema:addIndex [[CREATE INDEX faces_personId ON faces( personId )]]
end

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

function WFCatFaces.addSearchToSearchSchema( searchSchema )

	searchSchema:addSearchableData {
 		term = 'people',
		dataType = 'fullTextSearch',
		sql = [[SELECT DISTINCT faces.docId FROM faces WHERE faces.personId == ? ORDER BY faces.docId]],
		query_params = 'personId',
 	}

	searchSchema:addSearchableData {
		term = 'peopleUpToDate',
		dataType = 'peopleSearch',
		sql = [[SELECT DISTINCT faces.docId FROM faces INNER JOIN person ON person.personId == faces.personId
					WHERE person.personId == ? OR person.parentId == ? ORDER BY faces.docId]],
		query_params = 'personId'
	}

end

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

return WFCatFaces
