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

WFRxDocStoreUtils.lua

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

ADOBE CONFIDENTIAL
------------------
Copyright 2021 Adobe Inc.
All Rights Reserved.

NOTICE: All information contained herein is, and remain 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 may be covered by U.S. and Foreign
Patents, patents in process, and are protected by trade secret or copyright
law.  Dissemination of this information or reproduction of this material is
strictly forbidden unless prior written permission is obtained from
Adobe Systems Incorporated.

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

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

	--WF.core.utils
local KSTableUtils = require 'KSTableUtils'
local WFArguments = require 'WFArguments'
local WFFileUtils = require 'WFFileUtils'
local WFFunctionUtils = require 'WFFunctionUtils'
local WFLogger = require 'WFLogger'
local WFMsgPack = require 'WFMsgPack'
local WFPlatformInfo = require 'WFPlatformInfo'
local WFTableArchiving = require 'WFTableArchiving'

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

local WFTableArchiving_isArchive = assert( WFTableArchiving.isArchive )

local WFMsgPack_unpackDocValue = assert( WFMsgPack.unpackDocValue )

local KSTableUtils_derefOpt = assert( KSTableUtils.derefOpt )

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

local valueFnEnv = {} -- disallow all function calls

local docStoreImpLog = WFLogger 'WFRxDocStoreImp'

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

local WFRxDocStoreUtils = {}

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

local function isMsgPacked( ... )
	return not WFTableArchiving_isArchive( ... )
end

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

local kAllDocTypesAndSubtypes = { [ "*" ] = { "*" } }

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

local function isAllDocTypesAndSubtypes( typeSubtypeMap )

	return KSTableUtils.deepEquals( typeSubtypeMap, kAllDocTypesAndSubtypes )

end

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

WFRxDocStoreUtils.isMsgPacked = isMsgPacked

local function contentFromFlattenedValueImp( value )

	-- Lightweight sandboxing. The one potentially malicious piece of code
	-- somebody could inject (and they'd have to do it directly into the
	-- SQLite database) would be to put an infinite loop here.
	-- Some more potentially valuable approaches we could take are
	-- described in http://lua-users.org/wiki/SandBoxes.

	local valueFn = loadstring( 'return ' .. value )
	assert( type( valueFn ) == 'function' )
	setfenv( valueFn, valueFnEnv ) -- disallow all function calls

	return valueFn()

end

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

local function contentFromFlattenedValue( value )

	local success, result
	
	if isMsgPacked( value ) then
	
		success, result = pcall( WFMsgPack_unpackDocValue, value )
		
		if not success then
			docStoreImpLog:warnf( "WFMsgPack.unpack failed: %s", result )
		end
		
	end
	
	if not success then
	
		success, result = pcall( contentFromFlattenedValueImp, value )
		
		if not success then
			docStoreImpLog:warnf( "contentFromFlattenedValueImp failed to deserialize the value: %s", result )
		end
		
	end
	
	return success and result
	
end

WFRxDocStoreUtils.contentFromFlattenedValue = contentFromFlattenedValue

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

local function onlyFieldsInSpec( spec, t )
	if not spec then
		return t
	end

	if not t then
		return nil
	end

	local res = { }
	
	for k, subspec in pairs( spec ) do
		local v = t[ k ]
		if type( subspec ) == "table" then
			if type( v ) == "table" then
				res[ k ] = onlyFieldsInSpec( subspec, v ) -- apply recursively
			end
			
			-- if the value is not a table, it is not a match, so exclude it
		elseif subspec == true then
			res[ k ] = v -- include the whole value, regardless of type
		else
			docStoreImpLog:warnf( "invalid field spec (type %s) for key: %s", 
				type( subspec ), tostring( k ) )
		end
	end
	
	return res
end

WFRxDocStoreUtils.onlyFieldsInSpec = onlyFieldsInSpec

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

function WFRxDocStoreUtils.unpackDocValue( docArchive, optFieldSpec )

	WFArguments.check( 'table', '?table' )

	local baseDocValue, packedContent, packedAnnotation = unpack( docArchive )

	baseDocValue = onlyFieldsInSpec( optFieldSpec, baseDocValue )

	local docValue = {}

	if packedContent then

		docValue = onlyFieldsInSpec( optFieldSpec, contentFromFlattenedValue( packedContent ) )

	end
	
	if packedAnnotation then

		local annotation = onlyFieldsInSpec( optFieldSpec, contentFromFlattenedValue( packedAnnotation ) )

		for k, v in pairs( annotation ) do
			docValue[ k ] = v
		end

	end
	
	if baseDocValue then
	
		for k, v in pairs( baseDocValue ) do
			docValue[ k ] = v
		end
	
	end
	
	return docValue
	
end

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

function WFRxDocStoreUtils.processChangedDocsRawDocRows( rawDocRows,
		changeCounter, limit, shouldIncludeAllTypesAndSubtypes,
		nestedTypeSubtypeMap )

	WFArguments.check( 'table', 'number', 'number', 'boolean', 'table' )

	local result = {}
	local seenDocsById = {}

	local docCount = 0

	local numberOfRows = #rawDocRows

	local maxIndex = math.min( numberOfRows, limit )

	for i = 1, maxIndex do

		local currentRevRow = rawDocRows[ i ]

		local docId = currentRevRow[ 1 ]

		local docValue = seenDocsById[ docId ]

		if docValue then

			local labelName = currentRevRow[ 13 ]

			if type( labelName ) == "string" and labelName ~= "" then
			
				local labels = docValue._labels or {}
				labels[ labelName ] = true
				docValue._labels = labels
				
			end

		else

			local docType = currentRevRow[ 11 ]
			local subType = currentRevRow[ 12 ] or 'NULL'

			if shouldIncludeAllTypesAndSubtypes or
					KSTableUtils_derefOpt( nestedTypeSubtypeMap, docType, subType ) or
					KSTableUtils_derefOpt( nestedTypeSubtypeMap, docType, '*' ) then

				docValue = contentFromFlattenedValue( currentRevRow[ 4 ] )
				local annotation = contentFromFlattenedValue( currentRevRow[ 8 ] )

				for k, v in pairs( annotation ) do
					docValue[ k ] = v
				end

				docValue._id = docId
				docValue._localId = currentRevRow[ 7 ]
				docValue._revId = currentRevRow[ 2 ]
				docValue._changeCounter = currentRevRow[ 3 ]
				docValue._spaceId = currentRevRow[ 10 ]

				local labelName = currentRevRow[ 13 ]

				if labelName then
					local labels = docValue._labels or {}
					labels[ labelName ] = true
					docValue._labels = labels
				end

				if currentRevRow[ 5 ] == 1 then
					docValue._deleted = true
				end

				if currentRevRow[ 6 ] == 1 then
					docValue._conflict = true
				end

				docCount = docCount + 1

				result[ docCount ] = docValue
				seenDocsById[ docId ] = docValue

			end

		end

	end

	if rawDocRows[ limit + 1 ] then
		result.continueFrom = rawDocRows[ limit + 1 ][ 3 ]
	else
		result.continueFrom = changeCounter + 1
	end

	return result

end

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

function WFRxDocStoreUtils.processFilterByTypeSubtypeMap( filterByTypeSubtypeMap )

	WFArguments.check( '?table' )

	local typeSubtypeMap

	if filterByTypeSubtypeMap
			and not isAllDocTypesAndSubtypes( filterByTypeSubtypeMap ) then
		typeSubtypeMap = filterByTypeSubtypeMap
	end

	local nestedTypeSubtypeMap = {}

	for docType, docSubtypes in pairs( typeSubtypeMap or {} ) do

		nestedTypeSubtypeMap[ docType ] = {}

		for _, docSubtype in ipairs( docSubtypes ) do
			nestedTypeSubtypeMap[ docType ][ docSubtype ] = true
		end

	end

	local shouldIncludeAllTypesAndSubtypes = not not ( not typeSubtypeMap
			or KSTableUtils_derefOpt( nestedTypeSubtypeMap, '*', '*' ) )

	return nestedTypeSubtypeMap, shouldIncludeAllTypesAndSubtypes

end

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

--[[!
	\brief Dump documents that are currently syncing or have sync errors to a new SQLite database file.

	\param wfCatalog (WFCatalog)
	\param dbPath (string) The path where the new SQLite database file should be created.
		If a file already exists at this path, a unique name will be generated.

	\return <code>Observable</code> Resolves to the path of the created SQLite database file
		containing the syncing documents. The observable will only fire once.
--]]

function WFRxDocStoreUtils.dumpSyncingDocsToPath( wfCatalog, dbPath )

	local docIdsAndStatusStream

	if WFPlatformInfo.isClassic() then
		docIdsAndStatusStream = require 'LrClassicSyncStatus'.getDocIdsAndStatusWithSyncPendingOrError( wfCatalog )
	elseif WFPlatformInfo.isDesktop then
		docIdsAndStatusStream = require 'WFCatSyncStatus'.getDocIdsAndStatusWithSyncPendingOrError( wfCatalog )
	elseif WFPlatformInfo.isMobile() then
		docIdsAndStatusStream = require 'LrmCatSyncStatus'.getDocIdsAndStatusWithSyncPendingOrError( wfCatalog )
	else
		docIdsAndStatusStream = Observable.just( {} )
	end

	dbPath = WFFileUtils.ensureFileNameIsUnique( dbPath )

	return docIdsAndStatusStream

		:map( function( docIdStatusObjects )

			-- extract a list of full doc ids from output, removing dupes

			local docIdsToCopySet =
					WFFunctionUtils.reduce(
						{},
						function( idsToCopy, docIdStatusObject )

							if not docIdStatusObject.assetId then
								return idsToCopy
							end

							idsToCopy[ docIdStatusObject.assetId ] = true

							return idsToCopy

						end,
						docIdStatusObjects )

			return KSTableUtils.extractKeys( docIdsToCopySet )

		end )

		:flatMap( function( docIdsToCopy )

			-- convert full doc ids to local ids

			return wfCatalog
					:docStore()
					:bulkDocIdToLocalIdAsMap( docIdsToCopy, true )
					:map( KSTableUtils.extractValues )

		end )

		:flatMap( function( localIdsToCopy )

			-- copy all of these ids to the new path

			return wfCatalog
					:docStore()
					:dumpDocsToDbAtPath( localIdsToCopy, dbPath )
					:map( function()
						return dbPath
					end )

		end )

		:take( 1 )

end

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

return WFRxDocStoreUtils
