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

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

	-- core
local WFFileUtils = require 'WFFileUtils'
local KSPathUtils = require 'KSPathUtils'
local WFArguments = require 'WFArguments'
local WFArguments_uuid = require 'WFArguments_uuid'
local WFRequestDispatcher = require 'WFRequestDispatcher'
local WFCloseSupport = require 'WFCloseSupport'
local WFLogger = require 'WFLogger'
local KSTableUtils = require 'KSTableUtils'
local WFTableArchiving = require 'WFTableArchiving'

	-- SQLite
local WFSQLiteDatabase = require 'WFSQLiteDatabase'
local WFSQLiteDatabaseSchema = require 'WFSQLiteDatabaseSchema'

	-- WF.oz_client.managed_catalog.import
local WFCatImportManifestConstants = require 'WFCatImportManifestConstants'

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

local logger = WFLogger 'WFCatImportManifestBase'
local check = assert( WFArguments.check )
local WFArguments_uuid_isUuid = WFArguments_uuid.isUuid

local kClosed = { __closed = "CLOSED" }

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

local WFCatImportManifestBase = {
	_db = false,
		-- Handle to a native instance of the sqlite database	
	
	_path = false,
	
	_name = false,
	
	_queryClass = false,
	
	WFCloseSupport.addCloseSupportMethods,
}

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

function WFCatImportManifestBase:__init( path, name, queryClass )
	
	check( 'table', 'string', 'string', 'table' )
	
	self._path = path
	self._name = name
	self._queryClass = queryClass
	
	if WFFileUtils.exists( self._path ) == 'directory' then
	
		local dbPath = KSPathUtils.child( self._path, self._name )

		local schema = WFSQLiteDatabaseSchema( "1.0" )
		
		local createQueries = self._queryClass.getCreateQueries()
		for _, createQuery in pairs( createQueries ) do
		
			schema:addTable( createQuery )
		
		end
			
		self._db = WFSQLiteDatabase( dbPath, schema )
	end
	
end

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

local function TextValidator( data )
	local success = type( data ) == 'string'
		or type( data ) == 'number' -- WFSQLiteDatabase will convert to string
	return success and WFCatImportManifestConstants.validation.passed or WFCatImportManifestConstants.validation.failed
end

local function RealValidator( data )
	local success = type( data ) == 'number'
	return success and WFCatImportManifestConstants.validation.passed or WFCatImportManifestConstants.validation.failed
end

local function IntegerValidator( data )
	local success = type( data ) == 'number' and data % 1 == 0
	return success and WFCatImportManifestConstants.validation.passed or WFCatImportManifestConstants.validation.failed
end

local function UuidValidator( data )
	local success = WFArguments_uuid_isUuid( data )
	return success and WFCatImportManifestConstants.validation.passed or WFCatImportManifestConstants.validation.failed
end

local function UuidOrEmptyValidator( data )
	if type( data ) == 'string' then
		if WFArguments_uuid_isUuid( data ) then
			return WFCatImportManifestConstants.validation.passed
		else
			return WFCatImportManifestConstants.validation.modified, ''
		end
	end
	return WFCatImportManifestConstants.validation.failed
end

local DataValidators = {
	text = TextValidator,
	real = RealValidator,
	integer = IntegerValidator,
	uuid = UuidValidator,
	uuid_or_empty = UuidOrEmptyValidator,
}

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

function WFCatImportManifestBase:_validateInputFields( inputFields, inputData )
	
	local modifiedData, modifiedInfo
	
	for i, inputField in ipairs( inputFields ) do
		
		local data = inputData[ inputField.name ]

		local validator = DataValidators[ inputField.data_type ]
		
		if data ~= nil then
			local success, correctedValue = validator( data )
			
			if success == WFCatImportManifestConstants.validation.failed then
			
				logger:debugf( "Data failed validation: %s = %s", inputField.name, tostring( data ) )
				return false, { err = WFCatImportManifestConstants.validation.failed, fields = { inputField.name } }
			
			elseif success == WFCatImportManifestConstants.validation.modified then
				
				data = correctedValue
				
				modifiedData = modifiedData or KSTableUtils.shallowcopy( inputData )
				modifiedData[ inputField.name ] = data
				
				modifiedInfo = modifiedInfo or { err = WFCatImportManifestConstants.validation.modified, fields = {} }
				table.insert( modifiedInfo.fields, inputField.name )
				
			end
		end
		
		if type( data ) == 'string' and #data == 0 then
			data = nil
		end
		
		if inputField.mandatory and data == nil then
			logger:debugf( "Mandatory field missing: %s", inputField.name )
			return false, { err = WFCatImportManifestConstants.validation.failed, fields = { inputField.name } }
		end
		
	end
	
	return true, modifiedInfo, modifiedData
	
end

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

function WFCatImportManifestBase:_serializeInputFields( inputFields, inputData )
	
	local serializedData = {}
	for i, inputField in ipairs( inputFields ) do
		serializedData[ i ] = inputData[ inputField.name ]
	end
	
	return serializedData, #inputFields
	
end

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

function WFCatImportManifestBase:_addOrUpdate( queryInfo, dataArray )

	local dataSize = #dataArray
	local batchSize = queryInfo.batchSize or dataSize
	local inputFieldsInOrder = queryInfo.inputFieldsInOrder
	local validationResult = {}
	
	local start = 1
	while start <= dataSize do

		self._db:withTransactionDo( function()
		
			local last = math.min( dataSize, start + batchSize - 1 )
			for i = start, last do
				local data = dataArray[ i ]
				
				local success, validationInfo, modifiedData = self:_validateInputFields( inputFieldsInOrder, data )
				
				if validationInfo then
					-- Both errors and modifications go in here
					table.insert( validationResult, { data = data, info = validationInfo } )
				end
				
				if success then
					data = modifiedData or data
					
					local serializedData, dataCount = self:_serializeInputFields( inputFieldsInOrder, data )
					local ok, err = pcall( function()
						self._db:exec( queryInfo.sql, unpack( serializedData, 1, dataCount) )
					end )
					if not ok then
						local inputData = WFTableArchiving.tableToString( serializedData )
						local msg = string.format( "Msg:%s Data:%s", tostring( err ), inputData )
						error( msg )
					end
				end
			end
			
		end )
		
		start = start + batchSize
	
	end
	
	return validationResult
	
end

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

function WFCatImportManifestBase:_deleteTable( queryInfo )

	self._db:withTransactionDo( function()
			-- we need the dataCount to ensure not to miss the nil entries in the list.
			self._db:exec( queryInfo.sql )
	end )
	
end

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

function WFCatImportManifestBase:writeData( operationName, data )
	local queryInfo = self._queryClass.getWriteQuery( operationName )
	return self:_addOrUpdate( queryInfo, data )
end

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

function WFCatImportManifestBase:writeDataAsync( operationName, data )
	local queryInfo = self._queryClass.getWriteQuery( operationName )
	
	return Observable.create( function( observer )
		local failedEntries = self:_addOrUpdate( queryInfo, data )
		
		observer:onNext( failedEntries )
		observer:onCompleted()
		
	end )
end

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

function WFCatImportManifestBase:deleteData( operationName )
	local queryInfo = self._queryClass.getDeleteQuery( operationName )
	if queryInfo then
		-- From ranik :  @ashj, @jj, added this as temp to support the queries like
		-- deleteAllAlbumAssets, deleteAllStacks etc
		-- deleteOperations in WFCatImportManifestQueries
		return self:_deleteTable( queryInfo )
	end
end

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

function WFCatImportManifestBase:attachDatabase( attachQueryKey, dbPath )
	
	local queryInfo = self._queryClass.getAttachDetachQuery( attachQueryKey )
	self._db:exec( queryInfo.sql, dbPath )

end

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

function WFCatImportManifestBase:detachDatabase( detachQueryKey )
	
	local queryInfo = self._queryClass.getAttachDetachQuery( detachQueryKey )
	self._db:exec( queryInfo.sql )

end

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

function WFCatImportManifestBase:_formatRows( queryInfo, rows )
	
	local ret = {}
	
	local outputFieldsInOrder = queryInfo.outputFieldsInOrder
	assert( outputFieldsInOrder )
	
	for _, row in ipairs( rows or {} ) do
		local formattedRow = {}
		
		for i, fieldName in ipairs( outputFieldsInOrder ) do
			local value = row[ i ]
			
			if type( value ) == 'string' and #value == 0 then
				value = nil
			end
			
			formattedRow[ fieldName ] = value
		end
		
		table.insert( ret, formattedRow )
	end
	
	return ret
	
end

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

function WFCatImportManifestBase:_read( queryInfo, params, fn )
	
	local conditionFields = queryInfo.conditionFieldsInOrder

	local sql = queryInfo.sql
	
	local extendedParams, numParams = {}, 0
	
	if conditionFields then
		
		local success, validationInfo, modifiedParams = self:_validateInputFields( conditionFields, params )
		
		if not success or validationInfo or modifiedParams then
			-- No leeway here
			local _, info = next( validationInfo )
			error( "Read query data failed validation: " .. table.concat( info, ', ' ) )
		end
		
		extendedParams, numParams = self:_serializeInputFields( conditionFields, params )

	end
	
	local offset, offsetPosition = 0, 0
	local shouldBatch, batchSize = queryInfo.batchSize ~= nil, queryInfo.batchSize
	
	if shouldBatch then
		sql = sql .. " LIMIT ? OFFSET ?"
		
		extendedParams[ numParams + 1 ] = batchSize
		numParams = numParams + 2
		offsetPosition = numParams
	end

	repeat
		
		if shouldBatch then
			extendedParams[ offsetPosition ] = offset
		end
		
		local results = self._db:gatherArrayOfArray( sql, unpack( extendedParams, 1, numParams ) )
		
		local formattedResults = self:_formatRows( queryInfo, results )
		fn( formattedResults )
		
		-- Prepare for the next batch
		local numRows = #results
		offset = offset + numRows

	until shouldBatch == false or numRows < batchSize

end

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

function WFCatImportManifestBase:readData( operationName, params, fn )
	
	local queryInfo = self._queryClass.getReadQuery( operationName )
	self:_read( queryInfo, params, fn )
	
end

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

function WFCatImportManifestBase:readDataAsync( operationName, params )
	
	local queryInfo = self._queryClass.getReadQuery( operationName )
	return Observable.create( function( observer )
		
			self:_read( queryInfo, params, function( results )
			
				observer:onNext( results )
				
			end )
			
			observer:onCompleted()
	end )

end

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

function WFCatImportManifestBase:clear()
	local dbPath = KSPathUtils.child( self._path, self._name )
	WFFileUtils.deleteDirectory( dbPath )
end

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

function WFCatImportManifestBase:close()
	
	if self._db and self._db ~= kClosed then
		self._db:close()
		self._db = kClosed
	end
	
end

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

WFCatImportManifestBase.class = {}

function WFCatImportManifestBase.class.initMessageProcessor( Class, encodedPackedThreadArgs )

	local packedThreadArgs = require "KSJsonDecoder".decodeWebSafeJson(
								encodedPackedThreadArgs )

	local manifestBase = Class( unpack( packedThreadArgs ) )
	
	WFCloseSupport.mustCloseAtThreadTermination( manifestBase )

	local messageForwarder = setmetatable({}, {
		
		__index = function( _, methodName )
			
			return function( self, ... )
				assert( manifestBase, 'Manifest base not yet inited' )
				assert( manifestBase ~= kClosed, 'Manifest base closed' )
				local method = assert( manifestBase[ methodName ], 'No such method on manifest base' )
				return method( manifestBase, ... )
			end
			
		end,
	} )

	WFRequestDispatcher.onMessage( 'close', function()

		assert( manifestBase, 'Manifest base not yet inited' )
		assert( manifestBase ~= kClosed, 'Manifest base closed' )
		local oldManifestBase = manifestBase
		manifestBase = kClosed
		return oldManifestBase:close()

	end )

	WFRequestDispatcher.onMessageDo {
		message = 'readDataAsync',
		target = messageForwarder,
	}
	
	WFRequestDispatcher.onMessageDo {
		message = 'writeDataAsync',
		target = messageForwarder,
	}
	
end

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

return Rx.Class 'WFCatImportManifestBase'( WFCatImportManifestBase )
