--!nonstrict
local UIBloxRoot = script.Parent.Parent
local Cryo = require(UIBloxRoot.Parent.Cryo)

local StateTable = {}

StateTable.__index = StateTable

local function validateStateTableItem(item, qualifier)
	local type = typeof(item)
	local isValid = type == "string" or type == "userdata"
	assert(isValid, string.format("%s '%s' must be a string or userdata but is a %s", qualifier, tostring(item), type))
end

--[[
	This class method creates a new StateTable instance that you can use to control complicated
	logic that is based upon your state machine design. Ex:

		self.stateTable = StateTable.new(name, initialState, initialContext, {
			InitialState = {
				EventName1 = { nextState = "StateOne", action = self.actionDoSomething },
				EventName2 = { nextState = "FinalState" } -- actions are optional
			},
			StateOne = {
				EventName1 = { nextState = "FinalState", action = self.actionDoSomethingAtLast },
				EventName3 = { action = self.actionDoSomethingElse } -- will maintain current state
			},
			FinalState = {} -- transitions are optional
		})

	Arguments:
		name 	-		 A debug name for this StateTable. (String)
		initialState 	- Name of the beginning state for this StateTable. (String)
		initialContext 	- A reference to an existing table where you hold all sidecar contextual data
						  that needs to be manipulated by this StateTable's actions. (Table)
		transitionTable	- Description of the state machine structure. (Table)

	The outermost keys in "transitionTable" represent individual states in your design, each of which
	contains a description of the events that can be called while in that state. Calling an event
	triggers a transition to a new state while also (optionally) running an action functor.

	(All states and events must be simple strings or userdata.)
	(If using userdata for states and events, implement a tostring metamethod for ease of debugging.)

	Named events in your state table will be converted into functions that you can call directly.
	Calling these event functions will transition the StateTable to the appropriate nextState and
	call the registered action handler, if any. You may pass arguments to your actions through the
	event function by passing them as a table. Ex:

		self.stateTable.events.EventName1(args)

	The combination of named states and events in StateTable make up the control flow portion of your
	state machine. To run business logic, you need to implement Actions.

	Each action functor accepts four arguments: the current state, the next state, event arguments,
	and the contextual data table that you passed in when you called the event function. Your action
	functor should return a table containing the keys that need to be updated from currentContext.
	The returned table will be merged into currentContext and passed back to your onStateChange
	callback. Ex:

		function actionDoSomething(currentState, nextState, args, currentContext)
			local contextDiff = doSomething(args, currentContext)
			return contextDiff
		end

	Do NOT update your own copy of the StateTable's internal state variable or your context in actions.
	If this is an action-less transition, you'll fail to update it!

	To update your context and own tracking of the current state at the same time, listen to changes
	via StateTable:onStateChange. See the documentation of that method for more details.
]]
function StateTable.new(name, initialState, initialContext, transitionTable)
	assert(typeof(name) == "string", "name must be a string")
	assert(#name > 0, "name must not be an empty string")

	validateStateTableItem(initialState, "initialState")
	assert(initialContext == nil or typeof(initialContext) == "table", "initialContext must be a table or nil")

	assert(typeof(transitionTable) == "table", "transitionTable must be a table")
	assert(typeof(transitionTable[initialState]) == "table", "initialState must be present in transitionTable")

	local self = {}
	setmetatable(self, StateTable)

	self.name = name
	self.currentState = initialState
	self.currentContext = initialContext or {}
	self.transitionTable = {}
	self.events = {}

	for state, eventTable in pairs(transitionTable) do
		validateStateTableItem(state, "state")
		assert(typeof(eventTable) == "table", string.format("state '%s' must map to a table", tostring(state)))

		local parsedEventTable = {}
		for event, eventData in pairs(eventTable) do
			validateStateTableItem(event, "event")
			assert(typeof(eventData) == "table", string.format("event '%s' must map to a table", tostring(event)))

			local nextState = eventData.nextState
			local action = eventData.action

			if nextState ~= nil then
				validateStateTableItem(nextState, "nextState")

				-- Check that the transition lands on a known state
				assert(
					transitionTable[nextState] ~= nil,
					string.format("nextState '%s' does not exist in transitionTable", tostring(nextState))
				)
			end

			assert(action == nil or typeof(action) == "function", "action must be a function")

			parsedEventTable[event] = eventData

			-- Create a function to make it easy to call this event
			if self.events[event] == nil then
				self.events[event] = function(args)
					return self:handleEvent(event, args)
				end
			end
		end

		self.transitionTable[state] = parsedEventTable
	end

	-- catch calls to invalid events earlier
	setmetatable(self.events, {
		__index = function(_, event)
			error(string.format("'%s' is not a valid event in StateTable '%s'", tostring(event), self.name), 2)
		end,
	})

	return self
end

--[[
	It is recommended that you use the auto-generated event functions instead
	of calling this method; see StateTable.new.

	Process an event through this StateTable instance. Pass in the name
	of the event, and optional arguments. The arguments will be passed to the
	registered action handler for the state/event transition, if any.

	This function does not return anything. Listen to changes using
	StateTable:onStateChange if you need to store the current state or use the
	results of an action.
]]
function StateTable:handleEvent(event, args)
	validateStateTableItem(event, "event")
	assert(args == nil or typeof(args) == "table", "args must be nil or valid table")

	local currentState = self.currentState
	local eventMap = self.transitionTable[currentState]

	assert(eventMap ~= nil, "no transition events for current state")

	if eventMap[event] ~= nil then
		local eventData = eventMap[event]
		local nextState = eventData.nextState or currentState
		local action = eventData.action

		local updatedContext = self.currentContext
		if action ~= nil then
			local contextDiff = action(currentState, nextState, args, self.currentContext) or {}
			updatedContext = Cryo.Dictionary.join(self.currentContext, contextDiff)
			self.currentContext = updatedContext
		end

		self.currentState = nextState

		if self.stateChangeHandler ~= nil then
			self.stateChangeHandler(currentState, nextState, updatedContext)
		end
	end
end

--[[
	Register a function to process changes in state. Your function should have
	the following signature and return nothing:

		function handleStateChange(oldState, newState, updatedContext)
			self.currentState = newState
			self.currentContext = updatedContext
		end

	The updatedContext parameter contains the table that was returned by the action
	handler associated with the event transition.
]]
function StateTable:onStateChange(stateChangeHandler)
	assert(
		stateChangeHandler == nil or typeof(stateChangeHandler) == "function",
		"stateChangeHandler must be nil or a function"
	)
	self.stateChangeHandler = stateChangeHandler
end

return StateTable
