-- ------------------------------------------------------------------------------ --
--                                TradeSkillMaster                                --
--                          https://tradeskillmaster.com                          --
--    All Rights Reserved - Detailed license information included with addon.     --
-- ------------------------------------------------------------------------------ --

--- FSMMachine Class.
-- This class allows implementing event-driving finite state machines.
-- @classmod FSMMachine

local _, TSM = ...
local Machine = TSM.Init("Util.FSMClasses.Machine")
local State = TSM.Include("Util.FSMClasses.State")
local TempTable = TSM.Include("Util.TempTable")
local Log = TSM.Include("Util.Log")
local LibTSMClass = TSM.Include("LibTSMClass")
local FSMMachine = LibTSMClass.DefineClass("FSMMachine")
local private = {
	eventTransitionHandlerCache = {},
}



-- ============================================================================
-- Module Functions
-- ============================================================================

function Machine.Create(name)
	return FSMMachine(name)
end



-- ============================================================================
-- Class Meta Methods
-- ============================================================================

function FSMMachine.__init(self, name)
	self._name = name
	self._currentState = nil
	self._context = nil
	self._loggingDisabledCount = 0
	self._stateObjs = {}
	self._defaultEvents = {}
	self._handlingEvent = nil
end



-- ============================================================================
-- Public Class Methods
-- ============================================================================

--- Add an FSM state.
-- @tparam FSM self The FSM object
-- @tparam FSMState stateObj The FSM state object to add
-- @treturn FSM The FSM object
function FSMMachine.AddState(self, stateObj)
	assert(State.IsInstance(stateObj))
	local name = stateObj:_GetName()
	assert(not self._stateObjs[name], "state already exists")
	self._stateObjs[stateObj:_GetName()] = stateObj
	return self
end

--- Add a default event handler.
-- @tparam FSM self The FSM object
-- @tparam string event The event name
-- @tparam function handler The default event handler
-- @treturn FSM The FSM object
function FSMMachine.AddDefaultEvent(self, event, handler)
	assert(not self._defaultEvents[event], "event already exists")
	self._defaultEvents[event] = handler
	return self
end

--- Add a simple default event-based transition.
-- @tparam FSMMachine self The FSMMachine object
-- @tparam string event The event name
-- @tparam string toState The state to transition to
-- @treturn FSMMachine The FSMMachine object
function FSMMachine.AddDefaultEventTransition(self, event, toState)
	if not private.eventTransitionHandlerCache[toState] then
		private.eventTransitionHandlerCache[toState] = function(context, ...)
			return toState, ...
		end
	end
	return self:AddDefaultEvent(event, private.eventTransitionHandlerCache[toState])
end

--- Initialize the FSM.
-- @tparam FSM self The FSM object
-- @tparam string initialState The name of the initial state
-- @param[opt={}] context The FSM context table which gets passed to all state and event handlers
-- @treturn FSM The FSM object
function FSMMachine.Init(self, initialState, context)
	assert(self._stateObjs[initialState], "invalid initial state")
	self._currentState = initialState
	self._context = context or {}
	-- validate all the transitions
	for name, obj in pairs(self._stateObjs) do
		for _, toState in obj:_ToStateIterator() do
			assert(self._stateObjs[toState], format("toState doesn't exist (%s -> %s)", name, toState))
		end
	end
	return self
end

--- Process an event.
-- @tparam FSM self The FSM object
-- @tparam string event The name of the event
-- @tparam[opt] vararg ... Additional arguments to pass to the handler function
-- @treturn FSM The FSM object
function FSMMachine.ProcessEvent(self, event, ...)
	assert(self._currentState, "FSM not initialized")
	if self._handlingEvent then
		Log.RaiseStackLevel()
		Log.Warn("[%s] %s (ignored - handling event - %s)", self._name, event, self._handlingEvent)
		Log.LowerStackLevel()
		return self
	elseif self._inTransition then
		Log.RaiseStackLevel()
		Log.Warn("[%s] %s (ignored - in transition)", self._name, event)
		Log.LowerStackLevel()
		return self
	end

	if self._loggingDisabledCount == 0 then
		Log.RaiseStackLevel()
		Log.Info("[%s] %s", self._name, event)
		Log.LowerStackLevel()
	end
	self._handlingEvent = event
	local currentStateObj = self._stateObjs[self._currentState]
	if currentStateObj:_HasEventHandler(event) then
		self:_Transition(TempTable.Acquire(currentStateObj:_ProcessEvent(event, self._context, ...)))
	elseif self._defaultEvents[event] then
		self:_Transition(TempTable.Acquire(self._defaultEvents[event](self._context, ...)))
	end
	self._handlingEvent = nil
	return self
end

--- Enable or disable event and state transition logs (can be called recursively).
-- @tparam FSM self The FSM object
-- @tparam boolean enabled Whether or not logging should be enabled
-- @treturn FSM The FSM object
function FSMMachine.SetLoggingEnabled(self, enabled)
	self._loggingDisabledCount = self._loggingDisabledCount + (enabled and -1 or 1)
	assert(self._loggingDisabledCount >= 0)
	return self
end



-- ============================================================================
-- Private Class Methods
-- ============================================================================

function FSMMachine._Transition(self, eventResult)
	local result = eventResult
	while result[1] do
		-- perform the transition
		local currentStateObj = self._stateObjs[self._currentState]
		local toState = tremove(result, 1)
		local toStateObj = self._stateObjs[toState]
		if self._loggingDisabledCount == 0 then
			Log.RaiseStackLevel()
			Log.RaiseStackLevel()
			Log.Info("[%s] %s -> %s", self._name, self._currentState, toState)
			Log.LowerStackLevel()
			Log.LowerStackLevel()
		end
		assert(toStateObj and currentStateObj:_IsTransitionValid(toState), "invalid transition")
		self._inTransition = true
		currentStateObj:_Exit(self._context)
		self._currentState = toState
		result = TempTable.Acquire(toStateObj:_Enter(self._context, TempTable.UnpackAndRelease(result)))
		self._inTransition = false
	end
	TempTable.Release(result)
end