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

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

local KSIdGenerator = require 'KSIdGenerator'
local KSTableUtils = require 'KSTableUtils'
local WFArguments = require 'WFArguments'
local WFTableMerge = require 'WFTableMerge'
local WFLogger = require 'WFLogger'

local WFCatAlbums = require 'WFCatAlbums'
local WFCatDelete = require 'WFCatDelete'
local WFCatIndexUtils = require 'WFCatIndexUtils'
local WFCatIdUtils = require 'WFCatIdUtils'
local WFCatUtils = require 'WFCatUtils'

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

local logger = WFLogger( 'WFCatStacks' )

local check = assert( WFArguments.check )

local derefOpt = KSTableUtils.derefOpt

local WFCatStacks = {}

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

function WFCatStacks.allStacks( wfCatalog )

	return wfCatalog:indexStore():query {
		sql = [[SELECT DISTINCT stacks.docId, stacks.coverId, stacks.coverSubtype, assets.docId, assets.subType FROM stacks
						JOIN stack_assets USING ( stackId )
						JOIN assets ON stack_assets.assetId = assets.assetId 
						WHERE stacks.docId NOT NULL
						ORDER BY stacks.docId, assets.captureDate, assets.filenameLC COLLATE i18n_collate ]],
		calm = 0.25,
	}
	:map( function( rows )
		local stackInfo = {}
		local stackInfoById = {}
		
		for _, row in ipairs( rows ) do
			local stackId = row[ 1 ]
			local coverId = row[ 2 ]
			local coverSubtype = row[ 3 ]
			local stackAssetId = row[ 4 ]
			local assetSubType = row[ 5 ]
			
			local currentStack = stackInfoById[ stackId ] 
			
			local isNewStack = not currentStack
			
			currentStack = currentStack or {}
			currentStack.id = stackId
			currentStack.leader = coverId
			currentStack.leaderSubtype = coverSubtype
			
			local contents = currentStack.contents or {}
			table.insert( contents, stackAssetId )
			currentStack.contents = contents
			
			local subTypes = currentStack.subTypes or {}
			subTypes[ tostring( stackAssetId ) ] = assetSubType
			currentStack.subTypes = subTypes
			
			stackInfoById[ stackId ] = currentStack
			if isNewStack then
				table.insert( stackInfo, currentStack )
			end
		end
		
		return stackInfo
	end )
	
end		
		
--------------------------------------------------------------------------------

local function getOzAsset( wfCatalog, localDocId )
	
	if not localDocId then
		return Observable.just( nil )
	end
	
	return wfCatalog:docStore():docValue( localDocId )
			:take( 1 )
			
end

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

local function getOzStackId( wfCatalog, docId )
	
	if type( docId ) == 'number' then
		return wfCatalog:indexStore():queryOnce {
			sql = [[SELECT assetId FROM assets WHERE docId = ? AND subtype = 'stack']],
			quiesceUntilUpToDate = true,
			singleColumnQuery = true,
			docId
		}
		:pluck( 1 )
	else
		return Observable.just( docId )
	end
end

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

local function ozAlbumId( wfCatalog, localDocId )
	
	if not localDocId then
		return Observable.just( nil )
	end
	
	return wfCatalog:indexStore():queryOnce {
				sql = [[
						SELECT albumId FROM albums WHERE docId = ?
						UNION 
						SELECT albumId FROM project_albums WHERE docId = ?
					]],
				quiesceUntilUpToDate = true,
				singleColumnQuery = true,
				localDocId,
				localDocId
			}
			:map( function( rows )
				local maybeAlbumId = derefOpt( rows, 1 )
				if not maybeAlbumId then	
					logger:errorf( 'No albumId found for local id: ' .. tostring( localDocId) )
					return nil
				end
				return maybeAlbumId
			end )
end

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

local function createStackDocument( stackId, coverAsset )

	local baseDocValue = WFCatUtils.createBaseOverlayWithUserCreatedFields( stackId )

	local coverAssetId = coverAsset._id
	
	local stackDocValue = {
		type = 'asset',
		subtype = 'stack', -- change to stack once Oz supports
		_localOnly = {
			cover = coverAsset,
		},
		cover = {
			id = coverAssetId,
		}
	}
	
	WFTableMerge.applyOverlay( stackDocValue, baseDocValue )
	
	return stackDocValue
	
end

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

local function createStackAssetDocument( stackId, assetId )
	
	local baseDocValue = WFCatUtils.createBaseOverlayWithUserCreatedFields( KSIdGenerator.uuidInLowercaseHex() )
	
	local stackAssetDocValue = {
		type = 'stack_asset',
		stack = {
			id = stackId,
		},
		asset = {
			id = assetId,
		}
	}
	
	WFTableMerge.applyOverlay( stackAssetDocValue, baseDocValue )
	
	return stackAssetDocValue
	
end

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

local function createAlbumAssetForStack( albumId, stackId )

	local baseDocValue = WFCatUtils.createBaseOverlayWithUserCreatedFields( KSIdGenerator.uuidInLowercaseHex() )

	local albumAssetDocValue = {
		type = 'album_asset',
		album = {
			id = albumId,
		},
		asset = {
			id = stackId,
		}
	}
	
	WFTableMerge.applyOverlay( albumAssetDocValue, baseDocValue )
	
	return albumAssetDocValue
	
end

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

local function createStackAssetDocuments( ozStackId, ozAssetIds )

	local stackAssets = {}
			
	for _, ozAssetId in ipairs( ozAssetIds ) do
		table.insert( stackAssets, createStackAssetDocument( ozStackId, ozAssetId ) )
	end
			
	return stackAssets

end

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

local function getAssetsInStackAsMap( wfCatalog, stackId )
	
	local column = type( stackId ) == 'number' and 'docId' or 'stackId'
	
	return wfCatalog:indexStore():queryOnce {
		sql = [[ SELECT DISTINCT( stack_assets.assetId ) FROM stacks JOIN stack_assets USING( stackId ) 
					WHERE stacks.]] .. column .. [[= ?]],
		stackId,
		quiesceUntilUpToDate = true,
	}
	:map( function( rows )
		local assetIdMap = {}
		for _, row in ipairs( rows ) do
			assetIdMap[ row[ 1 ] ] = true
		end
		return assetIdMap
	end )
	
end

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

local function getAlbumsStackIsIn( wfCatalog, stackId )
	
	local column = type( stackId ) == 'number' and 'docId' or 'assetId'
	
	return wfCatalog:indexStore():query {
		sql = [[ SELECT DISTINCT( album_asset_v2.albumId ) 
				FROM assets JOIN album_asset_v2 USING( assetId ) WHERE assets.]]..column..[[ = ?]],
		stackId,
		quiesceUntilUpToDate = true,
	}
	:map( function( rows )
		local results = {}
		for _, row in ipairs( rows ) do
			local albumId = row[ 1 ]
			table.insert( results, albumId )
		end
		return results
	end )
		
end

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

local function createNewStackAssetDocuments( wfCatalog, ozStackId, ozImageIds )
	
	local currentAssetIdsInStack = getAssetsInStackAsMap( wfCatalog, ozStackId )
	
	return currentAssetIdsInStack:map( function( assetsInStack )
			-- need to filter out image ids that may already be part of stack
			for i = #ozImageIds, 1, -1 do
				local ozImageId = ozImageIds[ i ]
				if assetsInStack[ ozImageId ] then
					table.remove( ozImageIds, i )
				end
			end
			return createStackAssetDocuments( ozStackId, ozImageIds )
		end )
end

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

local function createNewAlbumAssetDocuments( wfCatalog, ozStackId, ozAssetIds )

	return getAlbumsStackIsIn( wfCatalog, ozStackId )
		:flatMap( function( albumIds )
			
			if #albumIds == 0 then
				return Observable.just( {} )
			end
			
			-- get albums these images already exist in
			return wfCatalog:indexStore():queryOnce {
					sql = [[ SELECT oai.value, album_asset_v2.albumId FROM 
								CARRAY( ?, ?, 'char*' ) AS oai JOIN album_asset_v2 ON 
								album_asset_v2.assetId = oai.value ]],
					ozAssetIds,
					#ozAssetIds,
				}
				:map( function( rows )
				
					local existingAlbumImages = {}
					for _, row in ipairs( rows ) do
						local assetId = row[ 1 ]
						local albumId = row[ 2 ]
						
						local albumImages = existingAlbumImages[ albumId ] or {}
						
						albumImages[ assetId ] = true
						
						existingAlbumImages[ albumId ] = albumImages
						
					end
					
					return albumIds, existingAlbumImages
				end )
				
				:map( function( albumIds, existingAlbumImages )
					
					local newAlbumAssetDocuments = {}
					
					for _, albumId in ipairs( albumIds ) do
						
						local maybeAlbumImages = existingAlbumImages[ albumId ] or {}
						
						for _, ozAssetId in ipairs( ozAssetIds ) do
							
							if not maybeAlbumImages[ ozAssetId ] then
								
								local albumAsset = WFCatAlbums.createAlbumAssetDocument( albumId, ozAssetId )
								table.insert( newAlbumAssetDocuments, albumAsset )
							
							end
						
						end
					end
				
					return newAlbumAssetDocuments
				end )
		end )
		
end

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

local function createStackWithId_imp( wfCatalog, stackId, coverImageId, imageIds, maybeAlbumId )
	
	local stackDocument = getOzAsset( wfCatalog, coverImageId )
		:map( function( maybeCoverAsset )
			
			if not maybeCoverAsset then
				error( 'Asset for top of stack not found' )
			end
			
			local stack = createStackDocument( stackId, maybeCoverAsset )
			
			return stack
		end )
		
	local stackAssetDocuments = WFCatIdUtils.getOzAssetIdsForLocalIds( wfCatalog, imageIds, true )
		:map( function( ozAssetIds )
			return createStackAssetDocuments( stackId, ozAssetIds )
		end )
		
	local maybeAlbumAssetDoc = ozAlbumId( wfCatalog, maybeAlbumId )
		:map( function( maybeOzAlbumId )
		
			local maybeAlbumAsset
			if maybeOzAlbumId then
				maybeAlbumAsset = createAlbumAssetForStack( maybeOzAlbumId, stackId )	
			end
				
			return maybeAlbumAsset
			
		end )
		
	return Observable.combineLatest( function( stackDocument, stackAssets, maybeAlbumAsset )
				
				local allStackDocs = {}
				
				if stackDocument then
					table.insert( allStackDocs, stackDocument )
				end
				
				for _, stackAsset in ipairs( stackAssets or {} ) do
					table.insert( allStackDocs, stackAsset )
				end
				
				if maybeAlbumAsset then
					table.insert( allStackDocs, maybeAlbumAsset )
				end
					
				return allStackDocs
			end,
			stackDocument, stackAssetDocuments, maybeAlbumAssetDoc )
			
			:flatMap( function( allStackDocs )
				
				local docStore = wfCatalog:docStore()
				
				return docStore:bulkSetDocValue( allStackDocs )
				
			end )
			:map( function( bulkResults )
				local localStackId
				for _, result in ipairs( bulkResults ) do
					if result.id == stackId then
						localStackId = result.localId
						break
					end
				end
				
				return stackId, localStackId
			end )

end

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

function WFCatStacks.createStackAssetDocument( stackId, assetId )

	return createStackAssetDocument( stackId, assetId )

end

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

function WFCatStacks.createStack( wfCatalog, imageIds, maybeAlbumId )
	
	check( 'WFCatalog', 'table', '?number' )
	
	if #imageIds < 2 then
		-- this may actually need to be done after we look up Oz ids for the 
		-- local image id
		return Observable.throw( "Stack must have 2 or more assets in it" )
	end
	
	-- just choose the first id for the cover, at least for now
	local coverImageId = imageIds[ 1 ] 
	
	local stackId = KSIdGenerator.uuidInLowercaseHex()
	
	return createStackWithId_imp( wfCatalog, stackId, coverImageId, imageIds, maybeAlbumId )

end

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

function WFCatStacks.createStackWithId( wfCatalog, newStackId, coverOzId, assetIds )

	return createStackWithId_imp( wfCatalog, newStackId, coverOzId, assetIds )

end

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

function WFCatStacks.deleteStack( wfCatalog, stackId )
	
	check( 'WFCatalog', 'string|number' )
	
	local stackIdColumn = type( stackId ) == 'string' and 'stacks.stackId' or 'stacks.docId'
	local stackAssetDocDeletion = wfCatalog:indexStore():queryOnce {
			sql = [[SELECT stack_assets.docId FROM stacks JOIN stack_assets USING ( stackId ) WHERE ]] .. stackIdColumn .. [[ = ?]],
			stackId,
			quiesceUntilUpToDate = true,
		}
		:map( function( stackAssetRows )
		
			-- We do not not want to send stack_asset deletions to Oz when the stack is removed just like 
			-- we don't with album_assets when the the album is removed.
			return WFCatDelete.deleteDocsFromQueryResult( wfCatalog:docStore(), stackAssetRows, true )
		
		end )
		
	return Observable.combineLatest( WFCatDelete.deleteStackAssets( wfCatalog, { stackId } ), stackAssetDocDeletion )
		:takeLast( 0 )
end

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

function WFCatStacks.cleanUpStacks( wfCatalog )
	
	check( 'WFCatalog' )
	
	local stacks = wfCatalog:indexStore():queryOnce {
		sql = [[SELECT docId FROM stacks]],
		quiesceUntilUpToDate = true,
		singleColumnQuery = true
	}
	
	
	local stackAssets = wfCatalog:indexStore():queryOnce {
		sql = [[SELECT docId FROM stack_assets]],
		quiesceUntilUpToDate = true,
		singleColumnQuery = true
	}
	
	return Observable.combineLatest( stacks, stackAssets )
		:map( function( stacks, stackAssets )
			local overlays = {}
			
			for _, stackId in ipairs( stacks ) do
				table.insert( overlays, { _id = stackId, _deleted = true } ) 
			end
			
			for _, stackAssetId in ipairs( stackAssets ) do
				table.insert( overlays, { _id = stackAssetId, _deleted = true } ) 
			end
			
			return overlays
		end )
		:flatMap( function( overlays )
			
			return wfCatalog:docStore():bulkApplyOverlay( overlays )
		
		end )
end

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

function WFCatStacks.addAssetsToStack( wfCatalog, stackId, imageIds )

	check( 'WFCatalog', 'number|string', 'table' )
	
	if #imageIds <= 0 then
		error( "Must provide at least one image id to add" )
	end
	
	local getOzStackId = getOzStackId( wfCatalog, stackId )
	
	local getOzImageIds = WFCatIdUtils.getOzAssetIdsForLocalIds( wfCatalog, imageIds, true )
	
	return Observable.combineLatest( getOzStackId, getOzImageIds )
	
		:flatMap( function( ozStackId, ozImageIds )
			
			return Observable.combineLatest( createNewStackAssetDocuments( wfCatalog, ozStackId, ozImageIds ),
											createNewAlbumAssetDocuments( wfCatalog, ozStackId, ozImageIds ) )
		end )		
		:take( 1 )
		:flatMap( function( newStackAssetDocs, newAlbumAssetDocs )
			
			local docStore = wfCatalog:docStore()
			
			local newDocs = newAlbumAssetDocs
			for _, doc in ipairs( newStackAssetDocs ) do
				newDocs[ #newDocs + 1 ] = doc
			end
			
			local undoOverlays = {} -- put undos in reverse order
			for _, document in ipairs( newStackAssetDocs ) do
				undoOverlays[ #undoOverlays + 1 ] = {
						_id = document._id,
						_deleted = true
				}
			end
			for _, document in ipairs( newAlbumAssetDocs ) do
				undoOverlays[ #undoOverlays + 1 ] = {
						_id = document._id,
						_deleted = true
				}
			end
			
			local undoToken = require 'WFCatUndo'.saveUndoState( Observable.just( )
				:flatMap( function( )
					return docStore:bulkApplyOverlay( undoOverlays ) 
				end ) )
			
			local result
			if #newDocs > 0 then
				result = docStore:bulkSetDocValue( newDocs )
			else
				result = Observable.just()
			end
			
			return result:map( function()
				return { undoToken = undoToken }
			end )
		end )

end

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

function WFCatStacks.removeAssetsFromStack( wfCatalog, stackId, imageIds )

	if #imageIds <= 0 then
		return Observable.just()
	end

	local getOzStackId = getOzStackId( wfCatalog, stackId )
	local getOzImageIds = WFCatIdUtils.getOzAssetIdsForLocalIds( wfCatalog, imageIds, true )
	
	return Observable.combineLatest( getOzStackId, getOzImageIds )
		:take( 1 )
		:flatMap( function( ozStackId, ozImageIds )

			return wfCatalog:indexStore():queryOnce {
				sql = [[SELECT stack_assets.docId, ( stack_assets.assetId == stacks.ozCoverId ) 
						FROM stack_assets JOIN stacks USING ( stackId ) 
						INNER JOIN CARRAY( ?, ?, 'char*' ) AS oii ON oii.value == stack_assets.assetId 
						WHERE stack_assets.stackId = ?]],
				ozImageIds,
				#ozImageIds,
				ozStackId,
			}
			:map( function( rows )
				return rows, ozStackId, ozImageIds
			end )
			
		end )
		:flatMap( function( stackAssetRows, ozStackId, ozImageIds )
			
			local isCoverBeingRemoved = false
			for _, stackAssetRow in ipairs( stackAssetRows ) do
				if stackAssetRow[ 2 ] == 1 then
					isCoverBeingRemoved = true
					break
				end
			end
			
			if not isCoverBeingRemoved then

				return WFCatDelete.deleteDocsFromQueryResult( wfCatalog:docStore(), stackAssetRows, false )
			
			else
				-- If the cover is being removed we need to find a different cover that can be used. If all 
				-- images are being removed from the stack we should consider this a deletion of the stack(?)
				
				-- First, get the assets that would be left in the stack.
				return wfCatalog:indexStore():queryOnce {
					sql = [[SELECT stack_assets.assetId FROM stack_assets 
						WHERE stack_assets.stackId = ? AND stack_assets.assetId not IN CARRAY( ?, ?, 'char*' )]]
						,
					ozStackId,
					ozImageIds,
					#ozImageIds,
				}
				:flatMap( function( rows )
				
					if #rows == 0 then
					
						-- delete the stack
						return WFCatStacks.deleteStack( wfCatalog, ozStackId )
					
					else
						
						-- set the stack cover to the first stack_asset left
						return Observable.combineLatest(
							WFCatStacks.setCover( wfCatalog, ozStackId, derefOpt( rows, 1, 1 ) ),
						 	WFCatDelete.deleteDocsFromQueryResult( wfCatalog:docStore(), stackAssetRows, false ) )

					end
				end )
			end
		end )
		:takeLast( 0 )
	
end

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

function WFCatStacks.setCover( wfCatalog, stackId, imageId )

	local getOzStackId = getOzStackId( wfCatalog, stackId )
	local getOzAsset = getOzAsset( wfCatalog, imageId )
	local currentAssetIdsInStack = getAssetsInStackAsMap( wfCatalog, stackId )
	
	return Observable.combineLatest( getOzStackId, getOzAsset, currentAssetIdsInStack )
		:take( 1 )
		:flatMap( function( ozStackId, ozAsset, currentStackAssetIdMap )
			
			local docStore = wfCatalog:docStore()
						
			local newCoverId = ozAsset._id
			
			local newStackAssetDocuments = not currentStackAssetIdMap[ newCoverId ] 
											and createStackAssetDocuments( ozStackId, { newCoverId } )
											or {}
			
			local addStackAsset = #newStackAssetDocuments > 0
									and docStore:bulkSetDocValue( newStackAssetDocuments )
									or Observable.just( nil )
			
			-- make sure we replace the cover info in the localOnly section, instead of merge
			ozAsset._replace = true 
			
			local stackOverlay = WFCatUtils.createBaseOverlayWithUserUpdatedField( ozStackId )
			
			stackOverlay._localOnly = {
				cover = ozAsset,
			}
			
			stackOverlay.cover = {
				id = newCoverId,
			}
			
			return addStackAsset
				:flatMap( function( )
					return docStore:applyOverlay( stackOverlay )
				end )
			
		end )
		:takeLast( 0 )
		
end

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

function WFCatStacks.getOverlaysForNewCovers( wfCatalog, stackIds, assetIdsToAvoid )
	
	if #stackIds < 1 then
		return Observable.just( {} )
	end
	
	local mapOfAssetIdsToAvoid  = {}
	for _, assetId in ipairs( assetIdsToAvoid ) do
		mapOfAssetIdsToAvoid[ assetId ] = true
	end

	return wfCatalog:indexStore():queryOnce {
			sql = [[SELECT stackId, assetId FROM stack_assets JOIN CARRAY( ?, ?, 'char*' ) AS si ON si.value = stackId]],
			stackIds,
			#stackIds,
			quiesceUntilUpToDate = true,
		}
		:map( function( rows )
		
			local stacksAlreadyProcessed = {}
			local newStackCoverOverlays = {}
			
			for _, row in pairs( rows ) do
				local stackId = row[ 1 ]
				local assetId = row[ 2 ]
				if not stacksAlreadyProcessed[ stackId ] and
					not mapOfAssetIdsToAvoid[ assetId ] then
					-- we don't already have an overlay for this stack
					-- and the asset id isn't one we shouldn't use (probably deleted)
					-- then create an overlay to change the cover to this asset id
					-- and mark the stack as processed
					local newStackCoverOverlay = {
							_id = stackId,
							cover = {
								id = assetId,
							}
						}
						
					newStackCoverOverlay = WFCatUtils.addUserUpdatedFields( newStackCoverOverlay )
						
					table.insert( newStackCoverOverlays, 
						 newStackCoverOverlay )
					
					stacksAlreadyProcessed[ stackId ] = true
				end
			end
			
			return newStackCoverOverlays
			
		end )
end

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

function WFCatStacks.generateIndexSchema( indexStoreSchema )

	check( 'WFRxIndexStoreSchema' )
	
	indexStoreSchema:addTable {

		sql = [[CREATE TABLE stacks( stackId, ozCoverId, coverId, coverSubtype )]],

		useLocalId = true,
		
		hasOneRowPerDocument = true,
		
		relevantTypeSubtypeTuples = {
			{ 'asset', 'stack' }
		},

		map = function( docValue, emit )

			if WFCatIndexUtils.isValidAssetDocument( docValue ) and docValue.subtype == 'stack' then
				
				local stackId = derefOpt( docValue, '_id' )
				
				local stackCoverOzId = derefOpt( docValue, 'cover', 'id' )
				
				-- embedded cover info
				local localCoverDocId = derefOpt( docValue, '_localOnly', 'cover', '_localId' )
				local localCoverOzId = derefOpt( docValue, '_localOnly', 'cover', '_id' )
				local localCoverSubtype = derefOpt( docValue, '_localOnly', 'cover', 'subtype' )
				
				if stackCoverOzId == localCoverOzId then
					-- stack id matches the locally embedded asset info so it's the correct asset
					emit( stackId, stackCoverOzId, localCoverDocId, localCoverSubtype )
				else
					-- stack id doesn't match the locally embedded asset so we need to reprocess it
					emit( stackId, stackCoverOzId, nil, nil )
				end
				
			end

		end,

	}
	
	indexStoreSchema:addIndex [[CREATE INDEX stacks_stackId ON stacks( stackId )]]
			
	indexStoreSchema:addTable {
		
		sql = [[CREATE TABLE stack_assets( stackId, assetId )]],
		
		useLocalId = true,
		
		hasOneRowPerDocument = true,
		
		relevantTypeSubtypeTuples = {
			{ 'stack_asset', 'NULL' }
		},

		map = function( docValue, emit )
			
			if docValue.type == 'stack_asset' and not docValue._deleted then
				
				local stackId = derefOpt( docValue, 'stack', 'id' )
				local assetId = derefOpt( docValue, 'asset', 'id' )
			
				emit( stackId, assetId )
		
			end
		
		end,
	
	}
	
	indexStoreSchema:addIndex [[CREATE INDEX stackassets_assetIdstackId ON stack_assets( assetId, stackId )]]
	
end

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

return WFCatStacks
