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

WFRxLowFidelityTimer.lua

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

ADOBE CONFIDENTIAL
------------------
Copyright 2016 Adobe Systems Incorporated
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
local WFRxScheduler = require 'WFRxScheduler'

	-- WF.core.logger
local WFLogger = require 'WFLogger'

	-- WF.core.utils
local WFArguments = require 'WFArguments'
local KSData = require 'KSData'
local WFRxRunUntilDone = require 'WFRxRunUntilDone'
local KSDate = require 'KSDate'
local WFCloseSupport = require 'WFCloseSupport'

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

local check = assert( WFArguments.check )
KSData.registerArgumentChecker()

local runUntilDone = WFRxRunUntilDone

local logger = WFLogger 'WFRxLowFidelityTimer'

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

local timers = {}
local docValueTimers = {}
	
local lowFidelityTickLength = 3 -- seconds
local lowFidelityTickCount = 0
local lowFidelityTimerCount = 0

local lowFidelityLastTimerId = 0

local coreTimer

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

local function getNextTimerId()

	lowFidelityLastTimerId = lowFidelityLastTimerId + 1
	return lowFidelityLastTimerId

end

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

local function onErrorOrCompleted()

	logger:info( "coreTimer disposed." )

	coreTimer:dispose()
	coreTimer = nil

end

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

local function shiftTimerCount( shift )

	lowFidelityTimerCount = lowFidelityTimerCount - shift

	logger:infof( "lowFidelityTimerCount = %d", lowFidelityTimerCount )
		
	if lowFidelityTimerCount == 0 then
		onErrorOrCompleted()
	end

end

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

local function processTimers()

	local maybeTimers = timers[ lowFidelityTickCount ]
	
	if maybeTimers and next( maybeTimers ) then

		timers[ lowFidelityTickCount ] = nil
	
		local numberOfTimersFiring = 0

		logger:info( "low fidelity timers are firing" )

		for _, timer in pairs( maybeTimers ) do

			numberOfTimersFiring = numberOfTimersFiring + 1

			timer:onNext( true )
			timer:onCompleted()
			
		end

		logger:infof( "%d low fidelity timers completed", numberOfTimersFiring )
	
		shiftTimerCount( numberOfTimersFiring )
	
	end

end

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

local function processDocValueTimers()

	local maybeDocValueTimers = docValueTimers[ lowFidelityTickCount ]
	
	if maybeDocValueTimers and next( maybeDocValueTimers ) then
	
		local firingTimers = {}
		local docStore
		local assetIds = {}
		local numberOfTimersFiring = 0
		
		for _, timerTable in pairs( maybeDocValueTimers ) do

			numberOfTimersFiring = numberOfTimersFiring + 1
			
			-- get the timer's doc store if it hasn't been closed
			local maybeDocStore = not WFCloseSupport.hasBeenOrWillBeClosed( timerTable.docStore ) and timerTable.docStore
			
			-- if we don't have a doc store yet and this timer's doc store is valid, use it
			if not docStore and maybeDocStore then
				docStore = maybeDocStore
			end

			if docStore == maybeDocStore then
				assetIds[ #assetIds + 1 ] = timerTable.assetId
				firingTimers[ #assetIds ] = timerTable.timer
			else
				logger:warnf( "low fidelity timer found for different/invalid doc store and asset id %s, removing", timerTable.assetId )
--				logger:warnf( "Traceback =  %s", timerTable.traceback )
			end

		end

		docValueTimers[ lowFidelityTickCount ] = nil

		if not docStore then
			if numberOfTimersFiring > 0 then
				logger:warnf( "Unable to process doc value timers! No valid doc store!" )
			end
			return
		end

		logger:infof( "%d low fidelity doc value timers are firing", numberOfTimersFiring )

		docStore:bulkDocValue( assetIds )
		
			:tap( function( docValues )
			
				for i, docValue in ipairs( docValues ) do

					-- bulkDocValue guarantees assets in order of request so use
					-- a parallel array for getting the corresponding firingTimer.
					local timer = firingTimers[ i ]
			
					if timer then
						timer:onNext( docValue )
						timer:onCompleted()
					end
					
				end

				logger:infof( "%d low fidelity doc value timers completed", numberOfTimersFiring )

				shiftTimerCount( numberOfTimersFiring )

			end )
			
			:call( runUntilDone )
	
	end

end

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

local function lowFidelityTick()

	lowFidelityTickCount = lowFidelityTickCount + 1
	
	logger:infof( "Low Fidelity TICK %d", lowFidelityTickCount )
	
	processTimers()
	
	processDocValueTimers()

end

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

local function maybeInitializeTimerCore()

	if not coreTimer then
	
		coreTimer = Observable.timer( lowFidelityTickLength, lowFidelityTickLength, WFRxScheduler.sharedScheduler() )
			:subscribe( lowFidelityTick, onErrorOrCompleted, onErrorOrCompleted )
	
	end

end

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

local function calculateFireTime( delay )

	if delay < 10 then
		logger:warn( "YOU SHOULD NOT USE WFRxLowFidelityTimer WITH DELAYS LESS THAN 10 SECONDS." )
	end
	
	maybeInitializeTimerCore()

	logger:infof( "Creating new low fidelity timer to fire in about %d seconds.", delay )

	lowFidelityTimerCount = lowFidelityTimerCount + 1

	local currentTimeInSeconds = KSDate.timeNow()
	
	local fireTime = currentTimeInSeconds + delay

	local secondsUntilTimerFires = fireTime - currentTimeInSeconds
	
	local lowFidelityTicksUntilTimerFires = math.ceil( secondsUntilTimerFires / lowFidelityTickLength )

	local fireOnTick = lowFidelityTickCount + lowFidelityTicksUntilTimerFires

	logger:infof( "Will fire at tick %d", fireOnTick )

	return fireOnTick

end

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

--[[!

	\class WFRxLowFidelityTimer
	
	\brief This class can be used to create low fidelity timers. The timers
	are guaranteed to fire within lowFidelityTickLength seconds of the delay.
		
]]--

-- @mark WFRxLowFidelityTimer class

local WFRxLowFidelityTimer = {}

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

function WFRxLowFidelityTimer.timer( delay )

	check( 'number' )

	return Observable.create( function( observer )

		local fireOnTick = calculateFireTime( delay )

		local maybeTimers = timers[ fireOnTick ]

		if not maybeTimers then
			maybeTimers = {}
		end

		local timerId = getNextTimerId()
		
		local disposable = Rx.Disposable.create( function()

			local maybeUnfiredTimers = timers[ fireOnTick ]

			if maybeUnfiredTimers then

				logger:infof( "Disposed low fidelity timer Id before it fired %d", timerId )

				maybeUnfiredTimers[ timerId ] = nil
				if next( maybeUnfiredTimers ) == nil then
					timers[ fireOnTick ] = nil
				end

				shiftTimerCount( 1 )
			end

		end )

		maybeTimers[ timerId ] = observer
		
		timers[ fireOnTick ] = maybeTimers
		
		return disposable

	end )

end

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

function WFRxLowFidelityTimer.docValueTimer( delay, docStore, assetId )

	check( 'number', 'WFRxDocStore', 'number|string' )

--	local traceback = debug.traceback()
	return Observable.create( function( observer )

		local timerId = getNextTimerId()

		logger:infof( "Creating new docValueTimer for %s timer to fire in about %d seconds.", assetId, delay )

		local fireOnTick = calculateFireTime( delay )

		local maybeTimers = docValueTimers[ fireOnTick ]

		if not maybeTimers then
			maybeTimers = {}
		end

		local disposable = Rx.Disposable.create( function()

			local maybeUnfiredTimers = docValueTimers[ fireOnTick ]

			if maybeUnfiredTimers then

				logger:infof( "Disposed low fidelity timer Id before it fired %d", timerId )

				maybeUnfiredTimers[ timerId ] = nil
				if next( maybeUnfiredTimers ) == nil then
					docValueTimers[ fireOnTick ] = nil
				end

				shiftTimerCount( 1 )

			end

		end )

		maybeTimers[ timerId ] = {
			timer = observer,
			docStore = docStore,
			assetId = assetId,
--			traceback = traceback,
		}
		
		docValueTimers[ fireOnTick ] = maybeTimers
		
		return disposable

	end )

end

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

function WFRxLowFidelityTimer.getTickCount()

	return lowFidelityTickCount

end

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

function WFRxLowFidelityTimer.getTimerCount()

	return lowFidelityTimerCount

end

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

return WFRxLowFidelityTimer
