--[[
	Represents the state relevant while executing a test plan.

	Used by TestRunner to produce a TestResults object.

	Uses the same tree building structure as TestPlanBuilder; TestSession keeps
	track of a stack of nodes that represent the current path through the tree.
]]

local TestEnum = require(script.Parent.TestEnum)
local TestResults = require(script.Parent.TestResults)
local Context = require(script.Parent.Context)
local ExpectationContext = require(script.Parent.ExpectationContext)

local TestSession = {}

TestSession.__index = TestSession

--[[
	Create a TestSession related to the given TestPlan.

	The resulting TestResults object will be linked to this TestPlan.
]]
function TestSession.new(plan)
	local self = {
		results = TestResults.new(plan),
		nodeStack = {},
		contextStack = {},
		expectationContextStack = {},
		hasFocusNodes = false
	}

	setmetatable(self, TestSession)

	return self
end

--[[
	Calculate success, failure, and skipped test counts in the tree at the
	current point in the execution.
]]
function TestSession:calculateTotals()
	local results = self.results

	results.successCount = 0
	results.failureCount = 0
	results.skippedCount = 0

	results:visitAllNodes(function(node)
		local status = node.status
		local nodeType = node.planNode.type

		if nodeType == TestEnum.NodeType.It then
			if status == TestEnum.TestStatus.Success then
				results.successCount = results.successCount + 1
			elseif status == TestEnum.TestStatus.Failure then
				results.failureCount = results.failureCount + 1
			elseif status == TestEnum.TestStatus.Skipped then
				results.skippedCount = results.skippedCount + 1
			end
		end
	end)
end

--[[
	Gathers all of the errors reported by tests and puts them at the top level
	of the TestResults object.
]]
function TestSession:gatherErrors()
	local results = self.results

	results.errors = {}

	results:visitAllNodes(function(node)
		if #node.errors > 0 then
			for _, message in ipairs(node.errors) do
				table.insert(results.errors, message)
			end
		end
	end)
end

--[[
	Calculates test totals, verifies the tree is valid, and returns results.
]]
function TestSession:finalize()
	if #self.nodeStack ~= 0 then
		error("Cannot finalize TestResults with nodes still on the stack!", 2)
	end

	self:calculateTotals()
	self:gatherErrors()

	return self.results
end

--[[
	Create a new test result node and push it onto the navigation stack.
]]
function TestSession:pushNode(planNode)
	local node = TestResults.createNode(planNode)
	local lastNode = self.nodeStack[#self.nodeStack] or self.results
	table.insert(lastNode.children, node)
	table.insert(self.nodeStack, node)

	local lastContext = self.contextStack[#self.contextStack]
	local context = Context.new(lastContext)
	table.insert(self.contextStack, context)

	local lastExpectationContext = self.expectationContextStack[#self.expectationContextStack]
	local expectationContext = ExpectationContext.new(lastExpectationContext)
	table.insert(self.expectationContextStack, expectationContext)
end

--[[
	Pops a node off of the navigation stack.
]]
function TestSession:popNode()
	assert(#self.nodeStack > 0, "Tried to pop from an empty node stack!")
	table.remove(self.nodeStack, #self.nodeStack)
	table.remove(self.contextStack, #self.contextStack)
	table.remove(self.expectationContextStack, #self.expectationContextStack)
end

--[[
	Gets the Context object for the current node.
]]
function TestSession:getContext()
	assert(#self.contextStack > 0, "Tried to get context from an empty stack!")
	return self.contextStack[#self.contextStack]
end

function TestSession:getExpectationContext()
	assert(#self.expectationContextStack > 0, "Tried to get expectationContext from an empty stack!")
	return self.expectationContextStack[#self.expectationContextStack]
end

--[[
	Tells whether the current test we're in should be skipped.
]]
function TestSession:shouldSkip()
	-- If our test tree had any exclusive tests, then normal tests are skipped!
	if self.hasFocusNodes then
		for i = #self.nodeStack, 1, -1 do
			local node = self.nodeStack[i]

			-- Skipped tests are still skipped
			if node.planNode.modifier == TestEnum.NodeModifier.Skip then
				return true
			end

			-- Focused tests are the only ones that aren't skipped
			if node.planNode.modifier == TestEnum.NodeModifier.Focus then
				return false
			end
		end

		return true
	else
		for i = #self.nodeStack, 1, -1 do
			local node = self.nodeStack[i]

			if node.planNode.modifier == TestEnum.NodeModifier.Skip then
				return true
			end
		end
	end

	return false
end

--[[
	Set the current node's status to Success.
]]
function TestSession:setSuccess()
	assert(#self.nodeStack > 0, "Attempting to set success status on empty stack")
	self.nodeStack[#self.nodeStack].status = TestEnum.TestStatus.Success
end

--[[
	Set the current node's status to Skipped.
]]
function TestSession:setSkipped()
	assert(#self.nodeStack > 0, "Attempting to set skipped status on empty stack")
	self.nodeStack[#self.nodeStack].status = TestEnum.TestStatus.Skipped
end

--[[
	Set the current node's status to Failure and adds a message to its list of
	errors.
]]
function TestSession:setError(message)
	assert(#self.nodeStack > 0, "Attempting to set error status on empty stack")
	local last = self.nodeStack[#self.nodeStack]
	last.status = TestEnum.TestStatus.Failure
	table.insert(last.errors, message)
end

--[[
	Add a dummy child node to the current node to hold the given error. This
	allows an otherwise empty describe node to report an error in a more natural
	way.
]]
function TestSession:addDummyError(phrase, message)
	self:pushNode({type = TestEnum.NodeType.It, phrase = phrase})
	self:setError(message)
	self:popNode()
	self.nodeStack[#self.nodeStack].status = TestEnum.TestStatus.Failure
end

--[[
	Set the current node's status based on that of its children. If all children
	are skipped, mark it as skipped. If any are fails, mark it as failed.
	Otherwise, mark it as success.
]]
function TestSession:setStatusFromChildren()
	assert(#self.nodeStack > 0, "Attempting to set status from children on empty stack")

	local last = self.nodeStack[#self.nodeStack]
	local status = TestEnum.TestStatus.Success
	local skipped = true

	-- If all children were skipped, then we were skipped
	-- If any child failed, then we failed!
	for _, child in ipairs(last.children) do
		if child.status ~= TestEnum.TestStatus.Skipped then
			skipped = false

			if child.status == TestEnum.TestStatus.Failure then
				status = TestEnum.TestStatus.Failure
			end
		end
	end

	if skipped then
		status = TestEnum.TestStatus.Skipped
	end

	last.status = status
end

return TestSession


--[[
	This test runner is invoked in all the environments that we want to test our
	project in.

	TestEZ's CLI targets Lemur, Roblox Studio, and Roblox-CLI.

	Assumes TestEZ is put into ReplicatedStorage and that test roots have a
	"TestEZTestRoot" CollectionService tag attached to them.
]]

-- luacheck: globals game script __LEMUR__

local TEST_CONTAINER_TAG = "TestEZTestRoot"

local hasCollectionService, CollectionService = pcall(game.GetService, game, "CollectionService")

-- ProcessService only exists when running under Roblox-CLI.
local isRobloxCli, ProcessService = pcall(game.GetService, game, "ProcessService")

local platform = {}

if __LEMUR__ then
	platform.exit = os.exit

	platform.error = function(message)
		print(message)
		platform.exit(1)
	end
elseif isRobloxCli then
	platform.exit = function(statusCode)
		ProcessService:ExitAsync(statusCode)
	end

	platform.error = function(message)
		print(message)
		platform.exit(1)
	end
else
	platform.exit = function() end

	platform.error = function(message)
		error(message, 0)
	end
end

local completed, suitePassed = xpcall(function()
	local TestEZ = require(script.TestEZ)

	local testContainers
	if hasCollectionService then
		testContainers = CollectionService:GetTagged(TEST_CONTAINER_TAG)
	else
		testContainers = _G.TESTEZ_TEST_CONTAINERS
	end

	if #testContainers == 0 then
		print(string.format(
			"No tests found. Did you give them the CollectionService tag %q?",
			TEST_CONTAINER_TAG
		))

		return true
	end

	local testResults = TestEZ.TestBootstrap:run(
		testContainers,
		TestEZ.Reporters.TextReporter
	)

	return testResults.failureCount == 0
end, debug.traceback)

if completed then
	if suitePassed then
		platform.exit(0)
	else
		platform.exit(1)
	end
else
	platform.error(suitePassed)
end


local TestEZ = require(script.Parent.Parent.TestEZ)

local noOptReporter = {
	report = function()
	end,
}

return {
	["run expect-extend e2e"] = function()
		TestEZ.run(script:FindFirstChild("expect-extend"), function(results)
			assert(#results.errors == 0,
				"Expected no errors, got " .. tostring(results.errors[1]) ..
				" plus " .. tostring(#results.errors - 1) .. " more.")
		end)
	end,

	["run afterEach e2e with error in it block"] = function()
		TestEZ.run(script:FindFirstChild("afterEachAfterFailure"):FindFirstChild("errorInItBlock"), function(results)
			assert(#results.errors == 1, "Expected one error, got " .. tostring(#results.errors))

			local afterEachError = string.find(results.errors[1], "afterEach threw an error as expected")
			assert(afterEachError ~= nil, "Expected afterEach to be reached after it throws an error")

			local cleaningUpError = string.find(results.errors[1], "While cleaning up the failed test another error was found")
			assert(cleaningUpError ~= nil, "Expected 'While cleaning up...' msg to be shown when afterEach fails after it fails")
		end)
	end,

	["run afterEach e2e with failure in it block"] = function()
		TestEZ.run(script:FindFirstChild("afterEachAfterFailure"):FindFirstChild("failureInItBlock"), function(results)
			assert(#results.errors == 1, "Expected one error, got " .. tostring(#results.errors))

			local afterEachError = string.find(results.errors[1], "afterEach threw an error as expected")
			assert(afterEachError ~= nil, "Expected afterEach to be reached after it throws an error")

			local cleaningUpError = string.find(results.errors[1], "While cleaning up the failed test another error was found")
			assert(cleaningUpError ~= nil, "Expected 'While cleaning up...' msg to be shown when afterEach fails after it fails")
		end)
	end,

	["run afterEach e2e with failure in afterEach block"] = function()
		TestEZ.run(script:FindFirstChild("afterEachAfterFailure"):FindFirstChild("errorInAfterEachBlock"), function(results)
			assert(#results.errors == 1, "Expected one error, got " .. tostring(#results.errors))

			local afterEachError = string.find(results.errors[1], "afterEach threw an error as expected")
			assert(afterEachError ~= nil, "Expected afterEach to be reached after it throws an error")

			local cleaningUpError = string.find(results.errors[1], "While cleaning up the failed test another error was found")
			assert(cleaningUpError == nil, "Expected 'While cleaning up...' msg to not be shown when only afterEach fails")
		end)
	end,

	["run lifecycle e2e w/ filter for describe block"] = function()
		TestEZ.TestBootstrap:run({
			script:FindFirstChild("lifecycle"):FindFirstChild("testNameFilterDescribeBlock"),
		},
		noOptReporter,
		{
			testNamePattern = "super specific describe block",
		})
	end,

	["run lifecycle e2e w/ filter for file name"] = function()
		TestEZ.TestBootstrap:run({
			script:FindFirstChild("lifecycle"):FindFirstChild("testNameFilterFileName"),
		},
		noOptReporter,
		{
			testNamePattern = "specificFileName",
		})
	end,
}


-- Shared test cases asserting lifecycle hooks are firing in the correct order

return function()
	describe("full example", function()
		beforeAll(function(context)
			context.globalState = {}

			table.insert(context.globalState, "beforeAll")
			expect(table.find(context.globalState, "beforeAll")).to.equal(1)
		end)

		beforeEach(function(context)
			table.insert(context.globalState, "beforeEach")
			expect(table.find(context.globalState, "beforeEach")).to.equal(2)
		end)

		it("it block", function(context)
			table.insert(context.globalState, "test")
			expect(table.find(context.globalState, "test")).to.equal(3)
		end)

		afterEach(function(context)
			table.insert(context.globalState, "afterEach")
			expect(table.find(context.globalState, "afterEach")).to.equal(4)
		end)

		afterAll(function(context)
			table.insert(context.globalState, "afterAll")
			expect(table.find(context.globalState, "afterAll")).to.equal(5)
		end)
	end)

	describe("no it block - should not call beforeEach or afterEach", function()
		beforeAll(function(context)
			context.globalState = {}

			table.insert(context.globalState, "beforeAll")
			expect(table.find(context.globalState, "beforeAll")).to.equal(1)
		end)

		beforeEach(function()
			fail("should not be called")
		end)

		afterEach(function()
			fail("should not be called")
		end)

		afterAll(function(context)
			table.insert(context.globalState, "afterAll")
			expect(table.find(context.globalState, "afterAll")).to.equal(2)
		end)
	end)
end


-- -----------------------------------------------------------------------------
--               Batched Yield-Safe Signal Implementation                     --
-- This is a Signal class which has effectively identical behavior to a       --
-- normal RBXScriptSignal, with the only difference being a couple extra      --
-- stack frames at the bottom of the stack trace when an error is thrown.     --
-- This implementation caches runner coroutines, so the ability to yield in   --
-- the signal handlers comes at minimal extra cost over a naive signal        --
-- implementation that either always or never spawns a thread.                --
--                                                                            --
-- License:                                                                   --
--   Licensed under the MIT license.                                          --
--                                                                            --
-- Authors:                                                                   --
--   stravant - July 31st, 2021 - Created the file.                           --
--   sleitnick - August 3rd, 2021 - Modified for Knit.                        --
-- -----------------------------------------------------------------------------

-- Signal types
export type Connection = {
	Disconnect: (self: Connection) -> (),
	Destroy: (self: Connection) -> (),
	Connected: boolean,
}

export type Signal<T...> = {
	Fire: (self: Signal<T...>, T...) -> (),
	FireDeferred: (self: Signal<T...>, T...) -> (),
	Connect: (self: Signal<T...>, fn: (T...) -> ()) -> Connection,
	Once: (self: Signal<T...>, fn: (T...) -> ()) -> Connection,
	DisconnectAll: (self: Signal<T...>) -> (),
	GetConnections: (self: Signal<T...>) -> { Connection },
	Destroy: (self: Signal<T...>) -> (),
	Wait: (self: Signal<T...>) -> T...,
}

-- The currently idle thread to run the next handler on
local freeRunnerThread = nil

-- Function which acquires the currently idle handler runner thread, runs the
-- function fn on it, and then releases the thread, returning it to being the
-- currently idle one.
-- If there was a currently idle runner thread already, that's okay, that old
-- one will just get thrown and eventually GCed.
local function acquireRunnerThreadAndCallEventHandler(fn, ...)
	local acquiredRunnerThread = freeRunnerThread
	freeRunnerThread = nil
	fn(...)
	-- The handler finished running, this runner thread is free again.
	freeRunnerThread = acquiredRunnerThread
end

-- Coroutine runner that we create coroutines of. The coroutine can be
-- repeatedly resumed with functions to run followed by the argument to run
-- them with.
local function runEventHandlerInFreeThread(...)
	acquireRunnerThreadAndCallEventHandler(...)
	while true do
		acquireRunnerThreadAndCallEventHandler(coroutine.yield())
	end
end

--[=[
	@within Signal
	@interface SignalConnection
	.Connected boolean
	.Disconnect (SignalConnection) -> ()

	Represents a connection to a signal.
	```lua
	local connection = signal:Connect(function() end)
	print(connection.Connected) --> true
	connection:Disconnect()
	print(connection.Connected) --> false
	```
]=]

-- Connection class
local Connection = {}
Connection.__index = Connection

function Connection:Disconnect()
	if not self.Connected then
		return
	end
	self.Connected = false

	-- Unhook the node, but DON'T clear it. That way any fire calls that are
	-- currently sitting on this node will be able to iterate forwards off of
	-- it, but any subsequent fire calls will not hit it, and it will be GCed
	-- when no more fire calls are sitting on it.
	if self._signal._handlerListHead == self then
		self._signal._handlerListHead = self._next
	else
		local prev = self._signal._handlerListHead
		while prev and prev._next ~= self do
			prev = prev._next
		end
		if prev then
			prev._next = self._next
		end
	end
end

Connection.Destroy = Connection.Disconnect

-- Make Connection strict
setmetatable(Connection, {
	__index = function(_tb, key)
		error(("Attempt to get Connection::%s (not a valid member)"):format(tostring(key)), 2)
	end,
	__newindex = function(_tb, key, _value)
		error(("Attempt to set Connection::%s (not a valid member)"):format(tostring(key)), 2)
	end,
})

--[=[
	@within Signal
	@type ConnectionFn (...any) -> ()

	A function connected to a signal.
]=]

--[=[
	@class Signal

	Signals allow events to be dispatched and handled.

	For example:
	```lua
	local signal = Signal.new()

	signal:Connect(function(msg)
		print("Got message:", msg)
	end)

	signal:Fire("Hello world!")
	```
]=]
local Signal = {}
Signal.__index = Signal

--[=[
	Constructs a new Signal

	@return Signal
]=]
function Signal.new<T...>(): Signal<T...>
	local self = setmetatable({
		_handlerListHead = false,
		_proxyHandler = nil,
		_yieldedThreads = nil,
	}, Signal)

	return self
end

--[=[
	Constructs a new Signal that wraps around an RBXScriptSignal.

	@param rbxScriptSignal RBXScriptSignal -- Existing RBXScriptSignal to wrap
	@return Signal

	For example:
	```lua
	local signal = Signal.Wrap(workspace.ChildAdded)
	signal:Connect(function(part) print(part.Name .. " added") end)
	Instance.new("Part").Parent = workspace
	```
]=]
function Signal.Wrap<T...>(rbxScriptSignal: RBXScriptSignal): Signal<T...>
	assert(
		typeof(rbxScriptSignal) == "RBXScriptSignal",
		"Argument #1 to Signal.Wrap must be a RBXScriptSignal; got " .. typeof(rbxScriptSignal)
	)

	local signal = Signal.new()
	signal._proxyHandler = rbxScriptSignal:Connect(function(...)
		signal:Fire(...)
	end)

	return signal
end

--[=[
	Checks if the given object is a Signal.

	@param obj any -- Object to check
	@return boolean -- `true` if the object is a Signal.
]=]
function Signal.Is(obj: any): boolean
	return type(obj) == "table" and getmetatable(obj) == Signal
end

--[=[
	@param fn ConnectionFn
	@return SignalConnection

	Connects a function to the signal, which will be called anytime the signal is fired.
	```lua
	signal:Connect(function(msg, num)
		print(msg, num)
	end)

	signal:Fire("Hello", 25)
	```
]=]
function Signal:Connect(fn)
	local connection = setmetatable({
		Connected = true,
		_signal = self,
		_fn = fn,
		_next = false,
	}, Connection)

	if self._handlerListHead then
		connection._next = self._handlerListHead
		self._handlerListHead = connection
	else
		self._handlerListHead = connection
	end

	return connection
end

--[=[
	@deprecated v1.3.0 -- Use `Signal:Once` instead.
	@param fn ConnectionFn
	@return SignalConnection
]=]
function Signal:ConnectOnce(fn)
	return self:Once(fn)
end

--[=[
	@param fn ConnectionFn
	@return SignalConnection

	Connects a function to the signal, which will be called the next time the signal fires. Once
	the connection is triggered, it will disconnect itself.
	```lua
	signal:Once(function(msg, num)
		print(msg, num)
	end)

	signal:Fire("Hello", 25)
	signal:Fire("This message will not go through", 10)
	```
]=]
function Signal:Once(fn)
	local connection
	local done = false

	connection = self:Connect(function(...)
		if done then
			return
		end

		done = true
		connection:Disconnect()
		fn(...)
	end)

	return connection
end

function Signal:GetConnections()
	local items = {}

	local item = self._handlerListHead
	while item do
		table.insert(items, item)
		item = item._next
	end

	return items
end

-- Disconnect all handlers. Since we use a linked list it suffices to clear the
-- reference to the head handler.
--[=[
	Disconnects all connections from the signal.
	```lua
	signal:DisconnectAll()
	```
]=]
function Signal:DisconnectAll()
	local item = self._handlerListHead
	while item do
		item.Connected = false
		item = item._next
	end
	self._handlerListHead = false

	local yieldedThreads = rawget(self, "_yieldedThreads")
	if yieldedThreads then
		for thread in yieldedThreads do
			if coroutine.status(thread) == "suspended" then
				warn(debug.traceback(thread, "signal disconnected; yielded thread cancelled", 2))
				task.cancel(thread)
			end
		end
		table.clear(self._yieldedThreads)
	end
end

-- Signal:Fire(...) implemented by running the handler functions on the
-- coRunnerThread, and any time the resulting thread yielded without returning
-- to us, that means that it yielded to the Roblox scheduler and has been taken
-- over by Roblox scheduling, meaning we have to make a new coroutine runner.
--[=[
	@param ... any

	Fire the signal, which will call all of the connected functions with the given arguments.
	```lua
	signal:Fire("Hello")

	-- Any number of arguments can be fired:
	signal:Fire("Hello", 32, {Test = "Test"}, true)
	```
]=]
function Signal:Fire(...)
	local item = self._handlerListHead
	while item do
		if item.Connected then
			if not freeRunnerThread then
				freeRunnerThread = coroutine.create(runEventHandlerInFreeThread)
			end
			task.spawn(freeRunnerThread, item._fn, ...)
		end
		item = item._next
	end
end

--[=[
	@param ... any

	Same as `Fire`, but uses `task.defer` internally & doesn't take advantage of thread reuse.
	```lua
	signal:FireDeferred("Hello")
	```
]=]
function Signal:FireDeferred(...)
	local item = self._handlerListHead
	while item do
		local conn = item
		task.defer(function(...)
			if conn.Connected then
				conn._fn(...)
			end
		end, ...)
		item = item._next
	end
end

--[=[
	@return ... any
	@yields

	Yields the current thread until the signal is fired, and returns the arguments fired from the signal.
	Yielding the current thread is not always desirable. If the desire is to only capture the next event
	fired, using `Once` might be a better solution.
	```lua
	task.spawn(function()
		local msg, num = signal:Wait()
		print(msg, num) --> "Hello", 32
	end)
	signal:Fire("Hello", 32)
	```
]=]
function Signal:Wait()
	local yieldedThreads = rawget(self, "_yieldedThreads")
	if not yieldedThreads then
		yieldedThreads = {}
		rawset(self, "_yieldedThreads", yieldedThreads)
	end

	local thread = coroutine.running()
	yieldedThreads[thread] = true

	self:Once(function(...)
		yieldedThreads[thread] = nil
		task.spawn(thread, ...)
	end)

	return coroutine.yield()
end

--[=[
	Cleans up the signal.

	Technically, this is only necessary if the signal is created using
	`Signal.Wrap`. Connections should be properly GC'd once the signal
	is no longer referenced anywhere. However, it is still good practice
	to include ways to strictly clean up resources. Calling `Destroy`
	on a signal will also disconnect all connections immediately.
	```lua
	signal:Destroy()
	```
]=]
function Signal:Destroy()
	self:DisconnectAll()

	local proxyHandler = rawget(self, "_proxyHandler")
	if proxyHandler then
		proxyHandler:Disconnect()
	end
end

-- Make signal strict
setmetatable(Signal, {
	__index = function(_tb, key)
		error(("Attempt to get Signal::%s (not a valid member)"):format(tostring(key)), 2)
	end,
	__newindex = function(_tb, key, _value)
		error(("Attempt to set Signal::%s (not a valid member)"):format(tostring(key)), 2)
	end,
})

return table.freeze({
	new = Signal.new,
	Wrap = Signal.Wrap,
	Is = Signal.Is,
})


local check = {}

function check.isDescendant(state1, state2)
	if not state2 then
		return function(item, index)
			return check.isDescendant(item, state1)
		end
	end

	local parent = state1.parent

	while parent do
		if parent == state2 then
			return true
		end
		parent = parent.parent
	end

	return false
end

function check.isAtomic(state)
	return state.type == "atomic"
end
function check.isCompound(state)
	return state.type == "compound"
end
function check.isHistory(state)
	return state.type == "history"
end
function check.isParallel(state)
	return state.type == "parallel"
end

return check


local Sift = require(script.Parent.Parent.sift)
local Check = require(script.Parent.Check)

local function getProperAncestors(state1, state2)
	local orderedAncestors = {}
	local parent = state1.parent

	while parent do
		table.insert(orderedAncestors, parent)
		parent = parent.parent
	end

	return orderedAncestors
end

local function findLCCA(stateList) -- completed
	for _, anc in Sift.Array.filter(getProperAncestors(Sift.Array.first(stateList)), Check.isCompound) do
		local tail = Sift.Array.removeIndex(stateList, 1)
		if Sift.Array.every(tail, Check.isDescendant(anc)) then
			return anc
		end
	end
end

local function getTransitionDomain(transition)
	local source = transition.source
	local target = transition.target
	if Check.isDescendant(target, source) then
		source = target
		while source.current do
			source = source.current
		end
	end
	return findLCCA({ target, source })
end

--Finds the LCCA of each transition and adds each active child of the LCCA to the exit list in ancestry order.
local function computeExitSet(enabledTransitions)
	local statesToExit = {}
	for i, t in enabledTransitions or {} do
		if t.target then -- for targetless transitions only
			local child = getTransitionDomain(t) --initial point of exit Check.is the LCCA of target and source
			while child.current do
				child = child.current
				table.insert(statesToExit, 1, child) --ensures exit set Check.is in ancestry order(from child to ancestor)
			end
		end
	end
	return statesToExit
end

local module = {}

function module.exitStates(enabledTransitions, machine, context, ...)
	local statesToExit = if not enabledTransitions then machine.configuration else computeExitSet(enabledTransitions)

	for i, s in statesToExit do
		if machine.debugMode then
			print("Exit: " .. s.id)
		end

		-- Get target state of the transition
		context.source = s
		for k, v in enabledTransitions or {} do
			if v.source == s then
				context.target = v.target
				break
			end
		end

		if s.OnExit then
			s.OnExit(context, ...)
		end

		context.source = nil
		context.target = nil

		if s.threads and #s.threads > 0 then
			while #s.threads > 0 do
				local thread = table.remove(s.threads, 1)

				if coroutine.status(thread) == "suspended" then
					task.cancel(thread)
				end
			end
		end

		if s.janitor then
			s.janitor:Cleanup()
		end

		table.remove(machine.configuration, table.find(machine.configuration, s))
		s.active = false
	end

	return statesToExit
end

local function hasIntersection(t1, t2)
	return Sift.Array.some(computeExitSet({ t1 }), function(value, index, array)
		return Sift.Array.includes(t2, value)
	end)
end

local function removeConflictingTransitions(enabledTransitions) -- completed
	local filteredTransitions = {}

	for _, t1 in enabledTransitions or {} do
		local t1Preempted = false
		local transitionsToRemove = {}

		for _, t2 in filteredTransitions do
			if hasIntersection(t1, t2) then
				if Check.isDescendant(t1.source, t2.source) then
					table.insert(transitionsToRemove, t2)
				else
					t1Preempted = true
					break
				end
			end
		end
		if not t1Preempted then
			for _, t3 in transitionsToRemove do
				filteredTransitions = Sift.Array.removeValue(filteredTransitions, t3)
			end
			table.insert(filteredTransitions, t1)
		end
	end

	return filteredTransitions
end

local function addDescendantStatesToEnter(state, statesToEnter, deepHistory) -- completed
	if state.history then
		addDescendantStatesToEnter(
			state.current or state.initial,
			statesToEnter,
			deepHistory or state.history == "deep"
		)
	else
		if Check.isCompound(state) then
			addDescendantStatesToEnter(
				if deepHistory then state.current or state.initial else state.initial,
				statesToEnter,
				deepHistory
			)
		elseif Check.isParallel(state) then
			for _, child in state.states do
				if not Sift.Array.some(statesToEnter, Check.isDescendant(child)) then
					addDescendantStatesToEnter(child, statesToEnter)
				end
			end
		end
	end
	table.insert(statesToEnter, 1, state)
end

local function addAncestorStatesToEnter(state, ancestor, statesToEnter) -- completed
	for i, anc in getProperAncestors(state, ancestor) do
		if not anc.parent or anc == ancestor then
			break -- test does this change functionality?
		end
		table.insert(statesToEnter, 1, anc)
		if Check.isParallel(anc) then
			for _, child in anc.states do
				if not Sift.Array.some(statesToEnter, Check.isDescendant(child)) then
					addDescendantStatesToEnter(child, statesToEnter)
				end
			end
		end
		--if anc Check.is a parallel state then fill descendants as well
	end
end

local function computeEntrySet(transitions)
	local statesToEnter = {}
	for i, t in transitions do
		if t.target then
			local ancestor = getTransitionDomain(t)
			addDescendantStatesToEnter(t.target, statesToEnter)
			addAncestorStatesToEnter(t.target, ancestor, statesToEnter)
		end
	end
	return statesToEnter
end

local function checkForAfter(state, machine)
	if not state.events or not state.events.after then
		return
	end

	local startTime = os.clock()

	for i, transition in state.events.after do
		local thread = task.delay(transition.delay, function()
			machine:Send("after", transition.delay, startTime)
		end)

		table.insert(state.threads, thread)
	end
end

function executeTransitionContent(enabledTransitions, machine, context, ...)
	for i, t in enabledTransitions or {} do
		for _, action in t.actions or {} do
			context.janitor = if t.target then t.target.janitor else nil
			context.source = t.source.id
			context.target = t.target.id
			action(context, ...)
			context.source = nil
			context.target = nil
		end
	end
end

local function passedGuards(transition, machine, context)
	for i, guard in transition.guards or {} do
		if not guard(context, table.unpack(machine._args)) then
			return false
		end
	end
	return true
end

function module.enterStates(enabledTransitions, machine, context, ...)
	local statesForDefaultEntry = {}

	local defaultHistoryContent = {}

	local statesToEnter = computeEntrySet(enabledTransitions)

	for i, s in statesToEnter do
		if machine.debugMode then
			print("Enter: " .. s.id)
		end

		if s.janitor then
			context.janitor = s.janitor
		end

		--Get source state of the transition
		context.target = s
		context.source = s
		for _, t in enabledTransitions or {} do
			if t.target == s then
				context.source = t.source
				break
			end
		end

		if s.OnEntry then
			s.OnEntry(context, ...)
		end

		context.target = nil
		context.source = nil

		if s.parent and not Check.isParallel(s.parent) then
			s.parent.current = s
		end

		s.active = true
		checkForAfter(s, machine)

		table.insert(machine.configuration, 1, s)
	end

	return statesToEnter
end

function module.selectTransitions(machine, context) --completed
	local enabledTransitions = {}

	local event = context.event
	local atomicStates = Sift.Array.filter(machine.configuration, Check.isAtomic)

	for _, state in atomicStates do
		for _, s in Sift.Array.insert(getProperAncestors(state), 1, state) do
			local breakLoop = false
			for _, transition in if s.events then s.events[event] or {} else {} do
				if table.find(enabledTransitions, transition) then
					breakLoop = true
					break
				end
				if passedGuards(transition, machine, context) then
					table.insert(enabledTransitions, transition)
					breakLoop = true
					break
				end
			end
			if breakLoop then
				break
			end
		end
	end

	enabledTransitions = removeConflictingTransitions(enabledTransitions)
	return enabledTransitions
end

function module.microstep(enabledTransitions, machine, context)
	local timeBegan = machine.logTime and workspace:GetServerTimeNow()

	local statesExited = module.exitStates(enabledTransitions, machine, context, table.unpack(machine._args))
	executeTransitionContent(enabledTransitions, machine, context, table.unpack(machine._args))
	local statesEntered = module.enterStates(enabledTransitions, machine, context, table.unpack(machine._args))

	local timeEnded = machine.logTime and workspace:GetServerTimeNow()

	local log = {
		statesEntered = statesEntered,
		statesExited = statesExited,
		transitions = enabledTransitions,
		context = context,
		timeBegan = timeBegan,
		timeEnded = timeEnded,
	}

	table.insert(machine.log, 1, log)

	if #machine.log > machine.maxLogs then
		repeat
			table.remove(machine.log, #machine.log)
		until #machine.log <= machine.maxLogs
	end

	if machine.debugMode then
		print("Microstep: " .. #statesExited .. " exited, " .. #statesEntered .. " entered")
	end

	return statesExited, statesEntered
end

return module


--[=[
	@class Arch
	This is the Arch class
]=]
--[=[
@type Context { janitor: any, event: string, Send: (string, any) -> (), target: string?, any: any, }
@within Arch ]=]
--[=[
@type Options { id: string, context: { any }?, actions: { string: (Context, any) -> () }?, guards: { string: (Context, any) -> boolean }?, maxLogs: number?, logTime: boolean?, debugMode: boolean?, }
@within Arch ]=]
--[=[
@type Action (Context, any) -> ()
@within Arch ]=]
--[=[
@type Transition string | { target: string?, actions: { string | Action }?, guards: { string | Action }? } | { { target: string?, actions: { string | Action }?, guards: { string | Action }? } }
@within Arch ]=]
--[=[
@type AtomicState { id: string, OnEntry: ( string | Action )?, OnExit: ( string | Action )?, events: { string: Transition }? }
@within Arch ]=]
--[=[
@type CompoundState AtomicState & { initial: string, states: States }
@within Arch ]=]
--[=[
@type ParallelState AtomicState & { parallel: true, states: States }
@within Arch ]=]
--[=[
@type HistoryState CompoundState & { history: string }
@within Arch ]=]
--[=[
@type Definition {id: string, initial: string?, parallel: boolean?, states: { string: ( CompoundState | AtomicState | HistoryState | ParallelState ) } } | ModuleScript
@within Arch
]=]
type Action = (Context, any) -> ()
type Guard = (Context, any) -> boolean

export type Context = { janitor: any, event: string, Send: (string, any) -> (), target: string?, any: any }

export type Transition =
	string
	| { target: string?, actions: { string | Action }?, guards: { string | Action }? }
	| { { target: string?, actions: { string | Action }?, guards: { string | Action }? } }

type AtomicState = {
	id: string,

	OnInit: ((Context, any) -> ())?,
	OnEntry: ((Context, any) -> ())?,
	OnExit: ((Context, any) -> ())?,
	OnDestroy: ((Context, any) -> ())?,

	events: { string: Transition }?,
}
type CompoundState = {
	id: string,
	initial: string,

	OnInit: ((Context, any) -> ())?,
	OnEntry: ((Context, any) -> ())?,
	OnExit: ((Context, any) -> ())?,
	OnDestroy: ((Context, any) -> ())?,

	events: { string: Transition }?,
	states: States,
}
type HistoryState = {
	id: string,
	initial: string,
	history: string,

	OnInit: ((Context, any) -> ())?,
	OnEntry: ((Context, any) -> ())?,
	OnExit: ((Context, any) -> ())?,
	OnDestroy: ((Context, any) -> ())?,

	events: { string: Transition }?,
	states: States,
}
type ParallelState = {
	id: string,
	parallel: true,

	OnInit: ((Context, any) -> ())?,
	OnEntry: ((Context, any) -> ())?,
	OnExit: ((Context, any) -> ())?,
	OnDestroy: ((Context, any) -> ())?,

	events: { string: Transition }?,
	states: States,
}

type States = { string: CompoundState | AtomicState | HistoryState | ParallelState }

export type Definition = {
	id: string,
	initial: string?,
	parallel: boolean?,

	states: States,
}

export type Options = {
	id: string?,
	context: { any }?, -- sets the initial context of the machine passed to each state
	actions: { string: Action }?, -- dictionary of actions that can be referenced by state and transition callbacks by setting them equal to a key in this dictionary
	guards: { string: Guard }?, -- dictionary of guards that can be referenced by transitions by setting them equal to a key in this dictionary
	maxLogs: number?, -- the max logs allowed in the log history at a time before the oldest log gets deleted
	logTime: boolean?, -- determines whether or not to include a time property to each log
	debugMode: boolean?, -- prints steps taken throughout the lifecycle for easier bug diagnosis
}

return 0


local function exitInterpreter() end

local function microstep(enabledTransitions) end

local function executeTransitionContent(enabledTransitions) end

local function isInFinalState(s) end

local function getProperAncestors(state1, state2)
	local orderedAncestors = {}
	local parent = state1.parent

	repeat
		table.insert(orderedAncestors, parent)
		parent = parent.parent
	until not parent or parent == state2

	return orderedAncestors
end

local function isDescendant(state1, state2)
	local parent = state1.parent

	while parent do
		if parent == state2 then
			return true
		end
		parent = parent.parent
	end

	return false
end

local function findLCCA(stateList)
	local properAncestorList = {}

	-- Populate the list of proper ancestors for each state
	for _, v in stateList do
		table.insert(properAncestorList, getProperAncestors(v))
	end

	-- Extract the head of properAncestorList
	local headOfProperAncestorList = table.remove(properAncestorList, 1)

	-- Iterate through the ancestors of the head
	for _, ancestor in headOfProperAncestorList do
		local isCommon = true

		-- Check if the ancestor is common across all lists
		for _, ancestorList in properAncestorList do
			if not table.find(ancestorList, ancestor) then
				isCommon = false
				break
			end
		end

		-- If the ancestor is common across all lists, return it
		if isCommon then
			return ancestor
		end
	end

	return -- If no common ancestor is found, return a default value (eventTransition.g., global ancestor)
end

local function getTransitionDomain(transition)
	return findLCCA({ transition.source, transition.target })
end

local function computeExitSet(enabledTransitions)
	local statesToExit = {}
	for i, t in enabledTransitions do
		if t.target then -- for targetless transitions only
			local child = getTransitionDomain(t) --initial point of exit is the LCCA of target and source
			while child do
				table.insert(statesToExit, 1, child) --ensures exit set is in ancestry order(from child to ancestor)
				child = child.current
			end
		end
	end
	return statesToExit
end

local function exitStates(enabledTransitions)
	local statesToExit = computeExitSet(enabledTransitions)

	for i, s in statesToExit do
		--assign history here in future
		s:OnExit()
	end
end

local function addDescendantStatesToEnter(state, statesToEnter, statesForDefaultEntry)
	table.insert(statesToEnter, state)
	if state.type == "compound" then
		table.insert(statesForDefaultEntry, state)
		addDescendantStatesToEnter(state.initial, statesToEnter, statesForDefaultEntry)
	end --elseif isParallelState
end

local function addAncestorStatesToEnter(state, ancestor, statesToEnter)
	for i, anc in getProperAncestors(state, ancestor) do
		table.insert(statesToEnter, anc)
		--if anc is a parallel state then fill descendants as well
	end
end

local function computeEntrySet(transitions, statesToEnter, statesForDefaultEntry)
	for i, t in transitions do
		local ancestor = getTransitionDomain(t)
		addDescendantStatesToEnter(t.target, statesToEnter, statesForDefaultEntry)
		addAncestorStatesToEnter(t.target, ancestor, statesToEnter)
	end
end

local function enterStates(enabledTransitions, context)
	local statesToEnter = {}
	local statesForDefaultEntry = {}

	local defaultHistoryContent = {}

	computeEntrySet(enabledTransitions, statesToEnter, statesForDefaultEntry)
	for i, s in statesToEnter do
		s:OnEntry()
	end
end

local function isCancelEvent() end

-- I'm not sure how this function works exactly, reference scxml before changing
local function removeConflictingTransitions(enabledTransitions)
	local filteredTransitions = {}

	for i, transition in enabledTransitions do
		local t1Preempted = false
		local transitionsToRemove = {}

		local filteredExitSet = computeExitSet(filteredTransitions)

		for _, state in computeExitSet({ transition }) do
			if table.find(filteredExitSet, state) then
				if isDescendant(transition.source, state) then
					table.insert(transitionsToRemove, state)
				else
					t1Preempted = true
					break
				end
			end
		end

		if not t1Preempted then
			for _, toRemove in transitionsToRemove do
				table.remove(filteredTransitions, table.find(filteredTransitions, toRemove))
			end
			table.insert(filteredTransitions, transition)
		end
	end

	return filteredTransitions
end

local function passedGuards(transition)
	for i, guard in transition.guards or {} do
		if not guard() then
			return false
		end
	end
	return true
end

local function selectEventlessTransitions(configuration)
	local enabledTransitions = {}

	for i, state in configuration do -- loops through active states
		if state.type ~= "atomic" then -- skips loop if state is not atomic, ensuring ancestry order
			continue
		end

		local transition
		local stateToQuery = state
		while stateToQuery do -- finds first eventless transition, from child to ancestor, that passes guards
			local broken = false

			for _, eventlessTransition in stateToQuery.on._noevent do
				if passedGuards(eventlessTransition) then
					transition = eventlessTransition
					broken = true
					break
				end
			end

			if broken then
				break
			end

			stateToQuery = stateToQuery.parent
		end
		if transition then
			table.insert(enabledTransitions, transition)
		end
	end

	enabledTransitions = removeConflictingTransitions(enabledTransitions)

	return enabledTransitions
end

local function selectTransitions(event, configuration)
	local enabledTransitions = {}

	for i, state in configuration do -- loops through active states
		if state.type ~= "atomic" then -- skips loop if state is not atomic, because atomic states are the base of ancestry order
			continue
		end

		local transition
		local stateToQuery = state
		while stateToQuery do -- finds first transition, from child to ancestor, that passes guards
			local broken = false

			for _, eventTransition in stateToQuery.on[event.name] do
				if passedGuards(eventTransition) then
					transition = eventTransition
					broken = true
					break
				end
			end

			if broken then
				break
			end

			stateToQuery = stateToQuery.parent
		end
		if transition then
			table.insert(enabledTransitions, transition)
		end
	end

	enabledTransitions = removeConflictingTransitions(enabledTransitions)

	return enabledTransitions
end

local function mainEventLoop(machine)
	while machine.running do
		local enabledTransitions = {}
		local macrostepDone = false
		--here we handle eventless transitions and transitions handled by internal events until macrostep is complete
		while machine.running and not macrostepDone do
			enabledTransitions = selectEventlessTransitions()
			if #enabledTransitions == 0 then
				if #machine.internalQueue == 0 then
					macrostepDone = true
				else
					local internalEvent = machine.internalQueue[1]
					table.remove(machine.internalQueue, 1)
					machine._event = internalEvent
					enabledTransitions = selectTransitions(internalEvent)
				end
			end
			if not #enabledTransitions == 0 then
				microstep(enabledTransitions)
			end
		end
		if not machine.running then --either we're in a final state, and we break out of the loop
			break
		end
		--or we've completed a macrostep, so we start a new macrostep by waiting for an external event

		if #machine.internalQueue > 0 then
			continue
		end

		local externalEvent = machine.externalQueue[1]
		table.remove(machine.externalQueue, 1)
		if isCancelEvent(externalEvent) then
			machine.running = false
			continue
		end
		machine._event = externalEvent

		enabledTransitions = selectTransitions(externalEvent)
		if not #enabledTransitions == 0 then
			microstep(enabledTransitions)
		end
		task.wait()
	end
	--if we get here it means we have reached a top-level final state or have been cancelled
	exitInterpreter()
end

local function enterInitialStates(state, actions, context)
	repeat
		state = state[state.initial]
		if state.OnEntry then
			if typeof(state.OnEntry) == "function" then
				state.OnEntry(context)
			elseif typeof(state.OnEntry) == "string" then
				actions[state.OnEntry](context)
			end
		end
		state = state.substates
	until not state
end

local function initializeDatamodel(datamodel, actions, context)
	for name, state in datamodel do
		if state.OnInit then
			if typeof(state.OnInit) == "function" then
				state.OnInit(context)
			elseif typeof(state.OnInit) == "string" then
				actions[state.OnInit](context)
			end
		end
		if state.substates then
			initializeDatamodel(state.substates, actions, context)
		end
	end
end

-- Add parent property to every state and ensure it is correctly types.
local function buildDatamodel(parent, document)
	local states = {}

	for name, state in document do -- in this loop I should check to ensure the necessary properties exist
		if not state.id then
			error("State does not have id!")
		end

		states[name] = state
		states[name].parent = parent

		if state.substates then
			states[name].type = state.type or "compound"
			states[name].substates = buildDatamodel(parent, state.substates)
		else
			states[name].type = "atomic"
		end
	end

	return states
end

local machineList = {}
local interpreter = {}
interpreter.__index = interpreter -- do fun metamagic to make sure users cant access any methods except what i want them to

local function createMachine(document)
	--ensure document is correctly typed
	local machine = {}

	machine.id = document.id
	machine.initial = document.initial
	machine.context = {} -- make immutable
	machine.running = false

	machine.configuration = {} --current configuration of active states
	machine.statesToInvoke = {}
	machine.internalQueue = {}
	machine.externalQueue = {}
	machine.historyValue = {}

	machine.actions = document.actions
	machine.guards = document.guards

	machine.datamodel = buildDatamodel(machine, document.states)

	return setmetatable(machine, interpreter)
end

local function mapOptionsToMachine(machine) end

function interpreter:Start(options)
	if self.running then
		error("This machine is already running!")
	end

	mapOptionsToMachine(self)

	initializeDatamodel(self.datamodel, self.actions, self.context)

	self.running = true

	enterInitialStates(self.datamodel, self.actions, self.context)
	--mainEventLoop(self)
end

return createMachine


local Sift = require(script.Parent.sift)
local Signal = require(script.Parent.signal)
local Janitor = require(script.Parent.janitor)
local Transition = require(script.Transition)
local Check = require(script.Check)

local function createContext(machine, context)
	context.Sift = Sift

	function context.Send(eventName, ...)
		machine:Send(eventName, ...)
	end

	return context
end

local function getActiveNestedStates(config, activeNestedStates)
	for _, state in config do
		if not state.active then
			continue
		end

		if Check.isParallel(state) then
			getActiveNestedStates(state.states, activeNestedStates)
		elseif Check.isCompound(state) or Check.isHistory(state) then
			local cont = false
			for _, nState in activeNestedStates do
				if Check.isDescendant(nState, state) then
					cont = true
					continue
				end
			end
			if cont then
				continue
			end
			getActiveNestedStates({ state.current }, activeNestedStates)
		elseif Check.isAtomic(state) then
			table.insert(activeNestedStates, state)
		end
	end
end

local function getExitedNestedStates(exited, stateList)
	for _, state in exited do
		if Check.isParallel(state) or Check.isCompound(state) or Check.isHistory(state) then
			getExitedNestedStates(state.states, stateList)
		elseif Check.isAtomic(state) then
			table.insert(stateList, state)
		end
	end
end

local function mainEventLoop(machine)
	while machine._running do
		local enabledTransitions = {}
		local macrostepDone = false
		local stepped = false
		local event

		local previousValue = table.clone(machine.value)

		while machine._running and not macrostepDone do
			if #machine._queue > 0 then
				event = table.remove(machine._queue, 1)
				machine.context.event = event.name
				machine._args = event.args

				enabledTransitions = Transition.selectTransitions(machine, machine.context)
			else
				macrostepDone = true
				continue
			end

			if #enabledTransitions > 0 then
				stepped = true
				Transition.microstep(enabledTransitions, machine, machine.context)
			end
		end

		if stepped then
			machine.value = {}
			machine.history = {}

			machine.value = Sift.Array.filter(machine.configuration, Check.isAtomic)
			machine.history = Sift.Array.difference(previousValue, machine.value)

			machine.onTransition:Fire({
				context = machine.context,
				value = machine.value,
				history = machine.history,
			}, table.unpack(machine._args or {}))
		end

		machine.EventAdded:Wait()
	end
end

local function transitionIsString(transition, events, event, state)
	events[event] = {
		{
			source = state.id,
			target = transition,
			guards = {},
			actions = {},
		},
	}
end

local function transitionIsTable(transition, events, event, state, datamodel, created)
	if not created then
		created = true
		events[event] = {}
	end
	if typeof(transition[1]) == "table" then
		events[event] = {}
		for _, t in transition do
			transitionIsTable(t, events, event, state, datamodel, true)
		end
	else
		if datamodel and transition.target then
			error("Top-Level transitions can not have a target!")
		end
		transition.guards = transition.guards or {}
		transition.actions = transition.actions or {}
		transition.source = state.id
		transition.event = event
		table.insert(events[event], transition)
	end
end

local function translateTransitions(events, state)
	for event, transition in events do
		if typeof(transition) == "string" then
			transitionIsString(transition, events, event, state)
		elseif typeof(transition) == "table" then
			transitionIsTable(transition, events, event, state)
		end

		if event == "after" then
			if not state.threads then
				state.threads = {}
			end
			for _, delayedT in transition do
				table.insert(delayedT.guards, "after")
			end
		end
	end
end

local function translateDefinition(parent, substates, stateList)
	local states = {}

	if substates.states then
		local datamodel = substates

		datamodel.id = datamodel.id

		datamodel.active = false

		if datamodel.events then
			for event, transitionList in datamodel.events do
				if typeof(transitionList) == "string" then
					error("Top-Level transitions can not be strings!")
				elseif typeof(transitionList) == "table" then
					transitionIsTable(transitionList, datamodel.events, event, datamodel)
				end
			end
		end

		datamodel.states = translateDefinition(datamodel, datamodel.states, stateList)
		datamodel.type = if datamodel.parallel then "parallel" else "compound"

		states = datamodel
		stateList[datamodel.id] = datamodel
	else
		for name, childState in substates do -- in this loop I should check to ensure the necessary properties exist
			childState.id = childState.id or name

			if stateList[childState.id] then
				error("The id " .. childState.id .. " already exists, every id must be unique!")
			end

			childState.active = false
			childState.parent = parent

			local events = childState.events

			if events then
				translateTransitions(events, childState)
			end

			if childState.states then
				childState.type = if childState.parallel then "parallel" else "compound"
				childState.states = translateDefinition(childState, childState.states, stateList)
			else
				childState.type = "atomic"
			end

			states[childState.id or name] = childState
			stateList[childState.id or name] = childState
		end
	end

	return states
end

local function addReferences(state, actions, guards, stateList)
	if Check.isCompound(state) or Check.isHistory(state) then
		if not state.initial then
			error("Compound states must have an initial state property!")
		end
		if not state.states[state.initial] then
			error("Initial state " .. state.initial .. " does not exist!")
		end
		state.initial = state.states[state.initial]
	end

	if state.events then
		for event, transitionList in state.events do
			for _, transition in transitionList do
				transition.source = state
				transition.target = if transition.target
					then state.parent.states[transition.target] or stateList[transition.target]
					else nil

				if transition.guards then
					for i, guard in transition.guards do
						if typeof(guard) == "string" then
							if not guards[guard] then
								error(guard .. " does not exist in the guards table!")
							end
							transition.guards[i] = guards[guard]
						end
					end
				end

				if transition.actions then
					for i, action in transition.actions do
						if typeof(action) == "string" then
							if not actions[action] then
								error(action .. " does not exist in the actions table!")
							end
							transition.actions[i] = actions[action]
						end
					end
				end
			end
		end
	end

	if state.OnInit and typeof(state.OnInit) == "string" then
		if not actions[state.OnInit] then
			error("OnInit does not exist in the actions table!")
		end
		state.OnInit = actions[state.OnInit]
	end
	if state.OnEntry and typeof(state.OnEntry) == "string" then
		if not actions[state.OnEntry] then
			error("OnEntry does not exist in the actions table!")
		end
		state.OnEntry = actions[state.OnEntry]
	end
	if state.OnExit and typeof(state.OnExit) == "string" then
		if not actions[state.OnExit] then
			error("OnExit does not exist in the actions table!")
		end
		state.OnExit = actions[state.OnExit]
	end
	if state.OnDestroy and typeof(state.OnDestroy) == "string" then
		if not actions[state.OnDestroy] then
			error("OnDestroy does not exist in the actions table!")
		end
		state.OnDestroy = actions[state.OnDestroy]
	end

	if state.janitor then
		state.janitor = Janitor.new()
	end

	for _, s in state.states or {} do
		addReferences(s, actions, guards, stateList)
	end
end

local function buildDatamodel(definition, stateList, machine)
	local datamodel = {}

	datamodel.id = definition.id
	datamodel.initial = definition.initial
	datamodel.active = false

	machine._guards["after"] = function(context, delayTime, startTime)
		local currentTime = os.clock()

		return currentTime - startTime >= delayTime
	end

	datamodel = translateDefinition(nil, definition, stateList)
	addReferences(datamodel, machine._actions, machine._guards, stateList)

	return datamodel
end

local function startStopDatamodel(datamodel, machine, stop)
	for name, state in datamodel.states do
		if not stop then
			if state.OnInit then
				state.OnInit(machine.context)
			end
			if machine.debugMode then
				print("Init: " .. state.id)
			end
		elseif stop then
			if state.OnDestroy then
				state.OnDestroy(machine.context)
			end
			if machine.debugMode then
				print("Destroy: " .. state.id)
			end
		end

		if state.states then
			startStopDatamodel(state, machine, stop)
		end
	end
end

--[=[
	@class Machine
	This is the machine.
]=]
local baseMachine = {}
baseMachine.__index = baseMachine
local livingMachines = {}

--[=[
	@within Arch
	For creating a new hierarchical state machine. Constructs a state machine instance based on a provided definition and options list.

	@param definition Definition -- the hierarchical structure that outlines the states, transitions, and behaviors of a state machine
	@param options Options -- for tailoring the behavior of the state machine, setting initial context, and adding references for the definition
	@return Machine
]=]
local function createMachine(definition, options)
	local machine = {}
	options = options or {}

	machine.context = createContext(machine, options.context or {})

	machine._running = false

	machine.configuration = {} -- set
	machine._queue = {} -- array
	machine._stateList = {} -- array

	machine.log = {} -- array

	machine._actions = options.actions or {} -- dictionary
	machine._guards = options.guards or {} -- dictionary

	machine.EventAdded = Signal.new()
	machine.onTransition = Signal.new()

	machine.value = {}
	machine.history = {}

	machine.maxLogs = options.maxLogs or 30
	machine.logTime = options.logTime or false

	machine.debugMode = options.debugMode or false

	machine.datamodel = buildDatamodel(Sift.Dictionary.copyDeep(definition), machine._stateList, machine)
	machine.id = options.id or definition.id

	return setmetatable(machine, baseMachine)
end

--[=[
	@within Arch
	Returns the machine with the matching id, if there is one.
]=]
local function getMachine(id: string)
	return livingMachines[id]
end

--[=[
	@within Machine
	Starts the machine, initializing all states and entering the initial configuration.
]=]
function baseMachine:Start()
	if self._running then
		warn("This machine is already running!")
		return
	end

	startStopDatamodel(self.datamodel, self)

	self._running = true
	self.datamodel.active = true

	Transition.enterStates({ { target = self.datamodel } }, self, self.context)
	self.value = Sift.Array.filter(self.configuration, Check.isAtomic)
	task.spawn(mainEventLoop, self)
	livingMachines[self.id] = self
end

--[=[
	@within Machine
	Stops the machine, exiting all active states then destroying all states in the datamodel.
]=]
function baseMachine:Stop()
	if not self._running then
		warn("This machine isn't even running!")
		return
	end

	Transition.exitStates(nil, self, self.context)
	startStopDatamodel(self.datamodel, self, true)

	self._running = false
	self.datamodel.active = false
	livingMachines[self.id] = nil
end

--[=[
	@within Machine
	Sends events to the machine by adding to the event queue.

	@param eventName string -- name of the event to be sent to the machine
	@param ... any -- all further parameters to be sent to all state and transition callbacks if the machine finds a transition for the event
]=]
function baseMachine:Send(eventName, ...)
	if not self._running then
		warn("This machine is not running yet, make sure to Start the machine before sending events!")
		return
	end

	table.insert(self._queue, { name = eventName, args = { ... } })
	self.EventAdded:Fire()
end

return {
	createMachine = createMachine,
	getMachine = getMachine,
	version = "0.3.8",
}


local TestEZ = require(script.Parent.Packages.testez)
local Arch = require(script.Parent.Packages.Arch)

TestEZ.TestBootstrap:run({ script.Parent["Arch.spec"] })


if not game:IsLoaded() then
    game.Loaded:Wait()
end

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Attrify = require(ReplicatedStorage.Attrify)

Attrify.start()


local Players = game:GetService("Players")

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Attrify = require(ReplicatedStorage.Attrify)

Attrify.start()

Players.PlayerAdded:Connect(function(player)
    Attrify.setPlayerCheckpoint(player, 2)
end)


local RunService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")

local Maid = require(script.Parent.Maid)
local Promise = require(script.Parent.Promise)
local Util = require(script.Parent.Util)

local DEFAULT_TWEEN_TIME = 0.2

--[=[
	@class Animation

	Animation library for animating instances.

	Install with wally by adding the following to your `wally.toml`:
	```toml
	Animation = "dig1t/animation@1.0.7"
	```
]=]
local Animation = {}

--[=[
	The default animation configuration.

	Example:
	```lua
	Animation.setDefaultConfig({
		tweenInfo = TweenInfo.new(1),
		start = {
			Transparency = 1
		},
		goal = {
			Transparency = 0
		}
	})

	Animation.weldModel(model)

	-- Animate the model
	Animation.animate({
		source = model.PrimaryPart,
	})
	```

	@prop defaultConfig AnimationConfig
	@within Animation
]=]
Animation.defaultConfig = {}

--[=[
	Animation configuration.

	Warning: setting loop, reverse, or delay will overwrite tweenInfo if it's defined.

	@interface AnimationConfig
	@within Animation
	.source Instance -- The instance to animate
	.start { [string]: any }? -- Starting properties (this will overwrite the current properties)
	.goal { [string]: any } -- Properties to animate to
	.tweenInfo TweenInfo? -- TweenInfo for the animation
	.frameCallback () -> ()? -- Callback that runs on each RunService step
	.stopped (Enum.PlaybackState) -> ()? -- Callback for when the animation is stopped
	.await boolean? -- Wait for the animation to complete
	.loop number? -- Times to loop the animation
	.reverse boolean? -- Reverse the animation after it's done
	.delay number? -- Delay before starting the animation
]=]
export type AnimationConfig = {
	source: Instance,
	start: { [string]: any }?,
	goal: { [string]: any },
	tweenInfo: TweenInfo?,
	frameCallback: () -> ()?,
	stopped: (Enum.PlaybackState) -> ()?,
	await: boolean?,
	loop: number?,
	reverse: boolean?,
	delay: number?,
}

--[=[
	Welds all descendant parts to the primary part.

	This requires the model to have a PrimaryPart set.

	@param model Model -- The model to prepare
]=]
function Animation.weldModel(model)
	if not model.PrimaryPart then
		return
	end

	model.PrimaryPart.Anchored = true

	for _, part in model:GetDescendants() do
		if part ~= model.PrimaryPart and part:IsA("BasePart") then
			Util.weld(part, model.PrimaryPart)
			part.Anchored = false
		end
	end
end

--[=[
	Sets the default animation configuration.

	@param config AnimationConfig -- The default animation configuration
]=]
function Animation.setDefaultConfig(config: AnimationConfig)
	assert(
		config and typeof(config) == "table",
		"Table must be passed to set the default animation config"
	)

	Animation.defaultConfig = config
end

--[=[
	Animates an instance and returns a promise.

	@param config AnimationConfig -- The animation configuration
	@return Promise.Promise
]=]
function Animation.promise(config: AnimationConfig): Promise.Promise
	if config.await then
		Animation.play(config)

		return Promise.resolve(Enum.PlaybackState.Completed)
	end

	return Promise.new(function(resolve)
		config.await = true

		Animation.play(config)

		resolve(Enum.PlaybackState.Completed)
	end)
end

--[=[
	Animates an instance.

	@param config AnimationConfig -- The animation configuration
	@return Tween -- The tween instance
]=]
function Animation.play(config: AnimationConfig): Tween
	local tween: Tween = Animation.new(config)

	if not config.await then
		tween:Play()
	end

	return tween
end

--[=[
	Animates multiple instances and returns a promise.

	@param instances { Instance } -- The instances to animate
	@param config AnimationConfig -- The animation configuration
	@return Promise.Promise
]=]
function Animation.promiseAll(
	instances: { Instance },
	config: AnimationConfig
): Promise.Promise
	local promises = {}

	for _, instance in instances do
		local newConfig = table.clone(config)
		newConfig.source = instance

		table.insert(promises, Animation.promise(newConfig))
	end

	return Promise.all(promises)
end

--[=[
	Constructs a tween

	@param config AnimationConfig -- The animation configuration
	@return Tween -- The tween instance
]=]
function Animation.new(config: AnimationConfig): (Tween, Promise.Promise?)
	assert(config and typeof(config) == "table", "Missing animation configuration")
	assert(typeof(config.source) == "Instance", "Missing instance to animate")
	assert(
		typeof(config.start) == "table" or config.start == nil,
		"Start properties must be a table"
	)
	assert(typeof(config.goal) == "table", "Goal properties must be a table")
	assert(
		typeof(config.tweenInfo) == "TweenInfo" or config.tweenInfo == nil,
		"tweenInfo must be a TweenInfo type"
	)
	assert(
		typeof(config.frameCallback) == "function" or config.frameCallback == nil,
		"frameCallback must be a function"
	)
	assert(
		typeof(config.stopped) == "function" or config.stopped == nil,
		"stopped must be a function"
	)
	assert(
		typeof(config.await) == "boolean" or config.await == nil,
		"await must be a boolean"
	)
	assert(typeof(config.loop) == "number" or config.loop == nil, "loop must be a number")
	assert(
		typeof(config.reverse) == "boolean" or config.reverse == nil,
		"reverse must be a boolean"
	)
	assert(
		typeof(config.delay) == "number" or config.delay == nil,
		"delay must be a number"
	)

	local tweenInfo = config.tweenInfo or TweenInfo.new(DEFAULT_TWEEN_TIME)

	if config.loop then
		tweenInfo.RepeatCount = config.loop
	end

	if config.reverse then
		tweenInfo.Reverses = true
	end

	if config.delay then
		tweenInfo.DelayTime = config.delay
	end

	-- Place default values
	for k, v in pairs(Animation.defaultConfig) do
		if config[k] == nil then
			config[k] = v
		end
	end

	if config.start then
		local source = (config.source :: any) :: { [any]: any }

		for property, value in pairs(config.start) do
			-- Safely set the new property
			pcall(function()
				source[property] = value
			end)
		end
	end

	local newTween = TweenService:Create(config.source, config.tweenInfo, config.goal)

	local maid = Maid.new()

	if config.frameCallback then
		maid:Task(RunService.Stepped:Connect(config.frameCallback))
	end

	if config.stopped then
		maid:Task(newTween.Completed:Connect(config.stopped))
	end

	newTween.Completed:Once(function()
		task.wait()
		maid:Destroy()
	end)

	if config.await then
		newTween:Play()

		if newTween.PlaybackState == Enum.PlaybackState.Completed then
			return newTween
		end

		newTween.Completed:Wait()
	end

	return newTween
end

return Animation


local BadgeService = game:GetService("BadgeService")
local Players = game:GetService("Players")

local Cache = require(script.Parent.Cache)

local badges = {}

--[=[
	@class Badge

	Badge management class for awarding badges to players.

	Use the `Badge.add` method to add badges to the cache.

	Example:
	```lua
	-- Setup
	local Badge = require(game.ReplicatedStorage.Packages.Badge)

	Badge.add("BadgeName", 999999999)

	-- Server
	local hasBadge = Badge.has(player, "BadgeName")

	print(`Player has badge: {hasBadge}!`)

	if not hasBadge then
		Badge.award(player, "BadgeName")
	end
	```

	Install with wally by adding the following to your `wally.toml`:
	```toml
	Badge = "dig1t/badge@1.0.4"
	```
]=]
local Badge = {}
local cache = Cache.new()

--[=[
	Adds a badge to the cache.

	```lua
	Badge.add("BadgeName", 999999999)
	```

	@param name string -- The name of the badge.
	@param badgeId number -- The id of the badge.
]=]
function Badge.add(name: string, badgeId: number)
	assert(name, "Badge.add - Missing badge name")
	assert(badgeId, "Badge.add - Missing badge id")

	badges[name] = badgeId
end

--[=[
	Checks if a player has a badge.

	```lua
	local hasBadge = Badge.has(player, "BadgeName")
	```

	@param player Player | number -- The player or userId to check.
	@param badgeName string -- The name of the badge.
	@return boolean -- Whether the player has the badge or not.
]=]
function Badge.has(player: Player | number, badgeName: string): boolean?
	assert(
		typeof(player) == "number" or player:IsA("Player"),
		"Badge.has - Invalid player"
	)
	assert(typeof(badgeName) == "string", "Badge.has - Invalid badge name")
	assert(badges[badgeName], `Badge.has - {badgeName} badge does not exist`)

	local _player: Player = typeof(player) == "number"
			and Players:GetPlayerByUserId(player)
		or player
	local userId: number = _player.UserId

	local cacheKey: string = `${userId}-${badgeName}`
	local cachedResult: boolean? = cache:Get(cacheKey)

	if cachedResult == nil then
		cachedResult = BadgeService:UserHasBadgeAsync(userId, badges[badgeName])
		cache:Set(cacheKey, cachedResult)
	end

	return cachedResult
end

--[=[
	Awards a badge to a player.

	```lua
	local awarded: boolean = Badge.award(player, "BadgeName")

	if not awarded then
		print("Failed to award badge!")
	end

	print("Badge awarded!")
	```

	@param player Player | number -- The player or userId to award the badge to.
	@param badgeName string -- The name of the badge.
	@return boolean -- Whether the badge was awarded or not. If the badge was already awarded, this will return true.
]=]
function Badge.award(player: Player | number, badgeName: string): boolean
	assert(
		typeof(player) == "number" or player:IsA("Player"),
		"Badge.award - Invalid player"
	)
	assert(typeof(badgeName) == "string", "Badge.award - Invalid badge name")

	local _player: Player = typeof(player) == "number"
			and Players:GetPlayerByUserId(player)
		or player
	local userId: number = _player.UserId

	if Badge.has(userId, badgeName) then
		return true
	end

	local success =
		pcall(BadgeService.AwardBadge, BadgeService, userId, badges[badgeName])

	if success then
		cache:Set(`${userId}-${badgeName}`, true)

		return true
	end

	return false
end

return Badge


local Maid = require(script.Parent.Maid)
local Util = require(script.Parent.Util)

--[=[
	@class Cache

	Cache class for storing data in a cache instance.

	Based off [node-cache](https://npmjs.com/package/node-cache).

	Install with wally by adding the following to your `wally.toml`:
	```toml
	Cache = "dig1t/cache@1.0.9"
	```
]=]
local Cache = {}
Cache.__index = Cache

export type Key = any

export type CacheObject = {
	key: Key,
	value: any,
	ttl: number?,
}

--[=[
	@interface CacheOptions
	@within Cache
	.defaultTTL? number -- The default time-to-live for a cache entry. Defaults to 6000.
	.checkInterval? number -- The interval in seconds to check for expired cache entries. Defaults to 600.
	.maxKeys? number -- The maximum amount of keys to store in the cache. 0 for unlimited. Defaults to 0.
]=]
export type CacheOptions = {
	defaultTTL: number?,
	checkInterval: number?,
	maxKeys: number?,
}

type CacheTypeOptions = {
	defaultTTL: number,
	checkInterval: number,
	maxKeys: number,
}

export type Cache = typeof(setmetatable(
	{} :: {
		_cache: { [string]: CacheEntry },
		options: CacheTypeOptions,
		[any]: any,
	},
	Cache
))

--[=[
	@interface CacheEntry
	@within Cache
	.key Key -- The key of the cache entry.
	.value any -- The value of the cache entry.
	.expires number -- The unix timestamp of when the cache entry expires.
]=]
type CacheEntry = {
	key: Key,
	value: any,
	expires: number,
}

local defaultOptions: CacheTypeOptions = {
	defaultTTL = 6000,
	checkInterval = 600,
	maxKeys = 0,
}

--[=[
	Creates a new Cache instance.

	@param options? { defaultTTL: number? }
]=]
function Cache.new(options: CacheOptions?): Cache
	assert(typeof(options) == "table" or options == nil, "options must be a table or nil")

	if options then
		assert(
			typeof(options.defaultTTL) == "number" or options.defaultTTL == nil,
			"options.defaultTTL must be a number or nil"
		)
	end

	local self = setmetatable({
		_cache = {},
		options = options and Util.extend(options, defaultOptions) or defaultOptions,

		_maid = Maid.new(),
	}, Cache) :: Cache

	self._maid:Add(Util.interval(self.options.checkInterval, function()
		local timestamp: number = Util.unix()

		for key: Key, cacheEntry: CacheEntry in pairs(self._cache) do
			if cacheEntry.expires ~= 0 and cacheEntry.expires <= timestamp then
				self:Delete(key)
			end
		end

		return
	end))

	return self
end

--[=[
	### Store a key:

	Sets a `key` `value` pair. It is possible to define a ttl (in seconds). Returns `true` on success.

	```lua
	local obj = { my = "Special", variable = 42 }

	local success = myCache:Set( "myKey", obj, 10000 )
	-- true
	```
	> Note: If the key expires based on it's `ttl` it will be deleted entirely from the internal data object.

	@method Set
	@within Cache
	@param key string -- The key to set.
	@param value any -- The value to set.
	@param ttl number? -- The time-to-live for the cache entry. Defaults to the defaultTTL option. 0 for unlimited.
	@return boolean -- Whether or not the value was set.
]=]
function Cache.Set(self: Cache, key: Key, value: any, ttl: number?): boolean
	local numberKeys: number = Util.tableLength(self:Keys())
	local maxKeys: number = self.options.maxKeys or 0

	if maxKeys > 0 and numberKeys >= maxKeys then
		return false
	end

	self._cache[key] = {
		key = key,
		value = value,
		expires = ttl
			or Util.unix() + (self.options.defaultTTL or defaultOptions.defaultTTL),
	} :: CacheEntry

	return true
end

--[=[
	### Store multiple keys:

	Sets multiple `key` `value` pairs. It is possible to define a `ttl` (in seconds). Returns `true` on success.

	```lua
	local obj = { my = "Special", variable = 42 }
	local obj2 = { my = "other special", variable = 1337 }

	local success = myCache:SetMultiple({
		{ key = "myKey", value = obj, ttl = 10000 },
		{ key = "myKey2", value = obj2 }
	})
	-- true
	```

	@method SetMultiple
	@within Cache
	@param cacheObjects { CacheObject } -- The key to set.
	@return boolean -- Whether or not the value was set.
]=]
function Cache.SetMultiple(self: Cache, cacheObjects: { CacheObject }): boolean
	for _, cacheObject: CacheObject in cacheObjects do
		local success: boolean? =
			self:Set(cacheObject.key, cacheObject.value, cacheObject.ttl)

		if success ~= true then
			return false
		end
	end

	return true
end

--[=[
	### Retrieve a key:

	Gets a saved `value` from the cache. Returns `nil` if not found or expired. If the `value` was found it returns the `value`.

	```lua
	local value = myCache:Get("myKey")
	```

	@method Get
	@within Cache
	@param key Key -- The key to retrieve.
	@return CacheEntry? -- The value of the key.
]=]
function Cache.Get(self: Cache, key: Key): CacheEntry?
	assert(typeof(key) == "string", "key must be a string")

	local cacheEntry = self._cache[key] :: CacheEntry?

	if cacheEntry and cacheEntry.expires > Util.unix() then
		return cacheEntry.value
	end

	return
end

--[=[
	### Take a key:

	Get the cached `value` and remove the `key` from the cache.
	Equivalent to calling `get(key)` + `delete(key)`.
	Useful for implementing `single use` mechanism, where once a value is read it will become obsolete.

	```lua
	myCache:Set("myKey", "myValue", 10000)
	myCache:Has("myKey") -- true
	local value = myCache:Take("myKey") -- "myValue"
	myCache:Has("myKey") -- false
	```

	@method Take
	@within Cache
	@param key Key -- The key to retrieve.
	@return CacheEntry? -- The value of the key.
]=]
function Cache.Take(self: Cache, key: Key): CacheEntry?
	assert(typeof(key) == "string", "key must be a string")

	local cacheEntry = self._cache[key] :: CacheEntry?

	if cacheEntry and cacheEntry.expires > Util.unix() then
		self._cache[key] = nil

		return cacheEntry.value
	end

	return
end

--[=[
	### Retrieve multiple keys:

	Gets multiple saved values from the cache. Returns an empty table `{}` if not found or expired.

	If the values were found it returns a table with the `key` `value` pair(s).

	```lua
	local values = myCache:GetMultiple({ "myKey", "myKey2" })
	```

	@method GetMultiple
	@within Cache
	@param keys { Key } -- The keys to retrieve.
	@return { [Key]: any } -- The values of the keys.
]=]
function Cache.GetMultiple(self: Cache, keys: { Key }): { [Key]: any }
	local values = {} :: { [string]: any }

	for _, key: Key in keys do
		values[key] = self:Get(key)
	end

	return values
end

--[=[
	Deletes a key. Returns the number of deleted entries. A delete will not fail if the key is not existing.

	```lua
	local deleted: number = myCache:Delete("myKey")
	```

	@method Delete
	@within Cache
	@param key Key -- The key to delete.
	@return number -- The number of deleted entries.
]=]
function Cache.Delete(self: Cache, key: Key): number
	assert(key ~= nil, "Key is required")

	if self._cache[key] == nil then
		return 0
	end

	self._cache[key] = nil

	return 1
end

--[=[
	Deletes multiple keys. Returns the number of deleted entries. This will not fail if a key is not existing.

	```lua
	local deleted: number = myCache:MultipleDelete({ "myKey", "myKey2" })
	```

	@method MultipleDelete
	@within Cache
	@param keys { Key } -- The keys to delete.
	@return number -- The number of deleted entries.
]=]
function Cache.MultipleDelete(self: Cache, keys: { Key }): number
	local deleted: number = 0

	for _, key: Key in keys do
		deleted += self:Delete(key)
	end

	return deleted
end

--[=[
	Updates the `ttl` of a key. Returns `true` on success.

	```lua
	local success = myCache:TTL("myKey", 10000)
	```

	@method TTL
	@within Cache
	@param key string -- The key to update.
	@param ttl number -- The time-to-live for the cache entry.
	@return boolean -- Whether or not the ttl was updated.
]=]
function Cache.TTL(self: Cache, key: Key, ttl: number): boolean
	assert(key ~= nil, "Key is required")
	assert(typeof(ttl) == "number", "ttl must be a number")

	local cacheEntry = self._cache[key] :: CacheEntry?

	if cacheEntry then
		cacheEntry.expires = Util.unix() + ttl

		return true
	end

	return false
end

--[=[
	Returns the `ttl` of a key. Returns `nil` if not found or expired.

	```lua
	local ttl = myCache:GetTTL("myKey")
	```

	@method GetTTL
	@within Cache
	@param key string -- The key to retrieve.
	@return number? -- The time-to-live for the cache entry.
]=]
function Cache.GetTTL(self: Cache, key: Key): number?
	assert(key ~= nil, "Key is required")

	local cacheEntry = self._cache[key] :: CacheEntry?

	if cacheEntry then
		return cacheEntry.expires - Util.unix()
	end

	return
end

--[=[
	Returns all keys in the cache.

	```lua
	local keys = myCache:Keys()
	```

	@method Keys
	@within Cache
	@return { Key } -- The keys in the cache.
]=]
function Cache.Keys(self: Cache): { Key }
	local keys = {} :: { Key }

	for key: Key, _ in pairs(self._cache) do
		keys[#keys + 1] = key
	end

	return keys
end

--[=[
	Returns a boolean indicating whether an element with the specified key exists or not.

	```lua
	myCache:Has("myKey") -- true
	```

	@method Has
	@within Cache
	@param key string -- The key to check.
	@return boolean -- Whether or not the key exists.
]=]
function Cache.Has(self: Cache, key: Key): boolean
	assert(key ~= nil, "Key is required")

	return self._cache[key] ~= nil
end

--- Clears the cache.
function Cache:Clear()
	self._cache = {}
end

--[=[
	Deletes all keys and values from the cache.

	Used for cleanup.

	```lua
	myCache:Destroy()
	```

	@method Destroy
	@within Cache
	@tag Cleanup
]=]
function Cache.Destroy(self: Cache)
	self._cache = {}

	self._maid:Destroy()
	self._maid = nil
end

return Cache


local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")

local Cache = require(script.Parent.Cache)
local Maid = require(script.Parent.Maid)

--[=[
	@class GamePass

	Game pass library that caches game pass ownership results and watches for pass purchases.

	This module fixes the caching issue with UserOwnsGamePassAsync
	by saving purchases made in the server. This allows the user to
	continue playing without having to re-join.

	```lua
	local hasPass = GamePass.has(player.UserId, 123456)
	```

	Install with wally by adding the following to your `wally.toml`:
	```toml
	GamePass = "dig1t/gamepass@1.0.8"
	```
]=]
local GamePass = {}

--[=[
	@type Callback (player: Player, gamePassId: number) -> ()
	@within GamePass
]=]
export type Callback = (player: Player, gamePassId: number) -> ()

local callbacks: { Callback } = {}
local watching: boolean = false

local _maid = Maid.new()
local cache = Cache.new()

--[=[
	Checks if a player has a game pass.

	```lua
	local hasPass = GamePass.has(player.UserId, 123456)
	```

	@param player Player -- The Player to check.
	@param gamePassId number -- The game pass id to check.
	@return boolean
]=]
function GamePass.has(player: Player, gamePassId: number): boolean
	assert(
		typeof(player) == "Instance" and player:IsA("Player"),
		"GamePass.has - Missing Player object"
	)
	assert(typeof(gamePassId) == "number", "GamePass.has - Missing GamePass id")

	local key = `${player.UserId}-${gamePassId}`

	local cached: boolean = cache:Get(key)

	if cache:Get(key) == nil then
		local success, res = pcall(
			MarketplaceService.UserOwnsGamePassAsync,
			MarketplaceService,
			player.UserId,
			gamePassId
		)

		if success then
			cache:Set(key, res)

			return res
		else
			-- error(res)
			return false
		end
	end

	return cached == true
end

--[=[
	Prompts the player to purchase a game pass.

	```lua
	GamePass.prompt(player, 123456)
	```

	@param player Player -- The player to prompt.
	@param gamePassId number -- The game pass id.
]=]
function GamePass.prompt(player: Player, gamePassId: number)
	assert(
		typeof(player) == "Instance" and player:IsA("Player"),
		"GamePass.prompt - Missing Player object"
	)
	assert(typeof(gamePassId) == "number", "GamePass.prompt - Missing game pass id")

	MarketplaceService:PromptGamePassPurchase(player, gamePassId)
end

--[=[
	Watches for game pass purchases.
	This allows the player to continue playing without having to re-join.

	This is automatically called when using `GamePass.onPurchase`.
]=]
function GamePass.watch()
	if watching then
		-- Prevent duplicating watchers
		return
	end

	_maid:Add(function()
		watching = false
		callbacks = {}
	end)

	watching = true

	_maid:Add(
		MarketplaceService.PromptGamePassPurchaseFinished:Connect(
			function(player: Player, gamePassId: number, purchased: boolean)
				if not purchased then
					return
				end

				cache:Set(`${player.UserId}-${gamePassId}`, true)

				for _, callback in pairs(callbacks) do
					callback(player, gamePassId)
				end
			end
		)
	)

	_maid:Add(Players.PlayerRemoving:Connect(function(player: Player)
		for key: Cache.Key, _owned: boolean in pairs(cache:Keys()) do
			if string.sub(key, 1, #tostring(player.UserId) + 1) == `{player.UserId}-` then
				-- Remove cache record in case the ownership changed
				-- while they were away and then re-joined the game.
				cache:Delete(key)
			end
		end
	end))
end

--[=[
	Triggers a callback when a player purchases a game pass.

	This calls `GamePass.watch` to start watching for game pass purchases.

	To stop watching for game pass purchases, use `GamePass.unwatch`.

	```lua
	GamePass.onPurchase(function(player: Player, gamePassId: number)
		print(player.Name .. " purchased game pass " .. gamePassId)
	end)
	```

	@param callback Callback -- The callback to trigger.
]=]
function GamePass.onPurchase(callback: Callback)
	if not watching then
		GamePass.watch()
	end

	callbacks[#callbacks + 1] = callback
end

--[=[
	Stops watching for game pass purchases.
]=]
function GamePass.unwatch()
	_maid:Clean()
end

return GamePass


local Debris = game:GetService("Debris") -- TODO: Replace with a better Debris module
local PhysicsService = game:GetService("PhysicsService")
local RunService = game:GetService("RunService")
local Workspace = game:GetService("Workspace")

local Util = require(script.Parent.Util)

local PLAYER_COLLISION_GROUP_NAME: string = "Player"
local RAGDOLL_COLLISION_GROUP_NAME: string = "Ragdoll"

--[=[
	@class Ragdoll

	Ragdoll package for converting a player's character into a ragdoll.

	Install with wally by adding the following to your `wally.toml`:
	```toml
	Ragdoll = "dig1t/ragdoll@1.0.4"
	```
]=]
local Ragdoll = {}

Ragdoll.isReady = false

--[=[
	Whether or not the ragdoll is collidable (default: false)

	@prop isCollidable boolean
	@within Ragdoll
]=]
Ragdoll.isCollidable = false

--[=[
	Seconds before the ragdoll is destroyed (default: 5)

	@prop destroyTime number
	@within Ragdoll
]=]
Ragdoll.destroyTime = 5 -- Roblox default for characters

--[=[
	Sets up the player's collision group and sets the attr_ragdoll_ready attribute to true.

	This should be run when the player's character is created or loaded.

	@param character Model -- The player's character
]=]
function Ragdoll.setup(character: Model) -- Sets up the player's collision group
	-- Insert collision groups if missing or if isCollidable changed
	if not Ragdoll.isReady and not Ragdoll.isCollidable and RunService:IsServer() then
		local playerGroupExists
		local ragdollGroupExists

		for _, group in PhysicsService:GetRegisteredCollisionGroups() do
			if group.name == PLAYER_COLLISION_GROUP_NAME then
				playerGroupExists = true
			elseif group.name == RAGDOLL_COLLISION_GROUP_NAME then
				ragdollGroupExists = true
			end
		end

		if not playerGroupExists then
			PhysicsService:RegisterCollisionGroup(PLAYER_COLLISION_GROUP_NAME)
		end

		if not ragdollGroupExists then
			PhysicsService:RegisterCollisionGroup(RAGDOLL_COLLISION_GROUP_NAME)
		end

		if not playerGroupExists or not ragdollGroupExists then
			-- Disable collisions with ragdoll characters
			PhysicsService:CollisionGroupSetCollidable(
				PLAYER_COLLISION_GROUP_NAME,
				RAGDOLL_COLLISION_GROUP_NAME,
				false
			)
		end

		Ragdoll.isReady = true
	end

	local _successful, humanoid, _tries = Util.attempt(function()
		return character:FindFirstChild("Humanoid")
	end, 80, 0.1) -- 80 tries * .1 yield times = 8 second max yield

	if not humanoid then
		return
	end

	humanoid.BreakJointsOnDeath = false

	if not Ragdoll.isCollidable then
		for _, obj: BasePart | Instance in character:GetDescendants() do
			if obj:IsA("BasePart") then
				obj.CollisionGroup = PLAYER_COLLISION_GROUP_NAME
			end
		end
	end

	character:SetAttribute("attr_ragdoll_ready", true)
end

-- Convert the player's character into a ragdoll
local function createRagdoll(character: Model)
	if not character or not character.Parent then
		return
	end

	local partAttachment: Attachment = Instance.new("Attachment")
	partAttachment.Parent = character:FindFirstChild("Head")
		or character.PrimaryPart
		or Util.getFirstPart(character)

	-- Push player backwards
	if partAttachment.Parent then
		local force: VectorForce = Instance.new("VectorForce")
		force.Force = Vector3.new(0, -200, 60)
		force.Attachment0 = partAttachment
		force.Parent = partAttachment
	end

	for _, obj: Motor6D | BasePart | Script | Instance in character:GetDescendants() do
		if obj:IsA("Motor6D") then
			local root: Attachment = Instance.new("Attachment")
			local attachment: Attachment = Instance.new("Attachment")
			root.CFrame = obj.C0
			root.Parent = obj.Part0
			attachment.CFrame = obj.C1
			attachment.Parent = obj.Part1

			local ballSocket: BallSocketConstraint = Instance.new("BallSocketConstraint")
			ballSocket.LimitsEnabled = true
			ballSocket.TwistLimitsEnabled = true -- Disable joints from freely rotating
			ballSocket.Attachment0 = root
			ballSocket.Attachment1 = attachment
			ballSocket.Parent = obj.Parent

			obj:Destroy() -- Remove the Motor6D instance
		end

		if obj:IsA("BasePart") then
			-- Switch collision group so players can't interact with the ragdoll
			obj.CanCollide = true

			if not Ragdoll.isCollidable then
				obj.CollisionGroup = RAGDOLL_COLLISION_GROUP_NAME
			end
		elseif obj:IsA("Script") then
			obj:Destroy()
		end
	end
end

--[=[
	Converts the player's character into a ragdoll and removes the player's character.

	This should be run when the player dies.

	@param player Player -- The player
	@param parent Instance? -- The parent of the ragdoll
	@param destroyTime number? -- The time in seconds before the ragdoll is destroyed
	@param keepRagdollInWorld boolean? -- Whether or not to keep the ragdoll in the world
]=]
function Ragdoll.playerDied(
	player: Player,
	parent: Instance?,
	destroyTime: number?,
	keepRagdollInWorld: boolean?
)
	if not player.Character or not player.Character:GetAttribute("attr_ragdoll_ready") then
		return
	end

	parent = parent or Workspace -- Default parent to Workspace

	local newCharacter: Model = Instance.new("Model")
	newCharacter.Name = player.Character.Name
	newCharacter.Parent = parent

	-- Ragdolls automatically disappear after 2 minutes
	-- whether or not keepRagdollInWorld is enabled
	Debris:AddItem(newCharacter, 120)

	parent = newCharacter -- Assign parent as newCharacter

	createRagdoll(player.Character) -- Convert the player's character into a ragdoll

	if parent then
		-- Place parts inside the ragdoll model (newCharacter)
		-- and filter out unwanted instances and scripts
		for _, obj in player.Character:GetChildren() do
			if not obj:IsA("Tool") then
				obj.Parent = parent
			elseif obj:FindFirstChild("Handle") then
				local handle = obj:FindFirstChild("Handle")

				if handle then
					handle.Parent = parent
					obj:Destroy()
				end
			end
		end
	end

	player.Character:Destroy()
	player.Character = nil

	task.wait(destroyTime or Ragdoll.destroyTime)

	if not parent or not parent.Parent then
		return
	elseif not keepRagdollInWorld then
		parent:Destroy()
	else
		for _, obj: BasePart | Instance in parent:GetDescendants() do
			if obj:IsA("BasePart") then
				obj.Anchored = true
			end
		end
	end
end

return Ragdoll


--[=[
	@class ReplicaClient
	@client
	@tag Replica

	A class for receiving replicated objects from the server.
]=]

local RunService = game:GetService("RunService")

local ReplicaTypes = require(script.Parent.ReplicaTypes)
local Signal = require(script.Parent.Parent.Signal)

local remote: RemoteEvent

local ReplicaClient = {}
ReplicaClient.__index = ReplicaClient

-- Replicated replcia objects
local replicas: { ReplicaClient } = {}
local signals = {} :: { [string]: Signal.Signal<ReplicaClient> }

export type ReplicaClient = typeof(setmetatable(
	{} :: {
		replicaId: string,
		class: string,

		data: ReplicaTypes.Data,

		keyChangedSignals: { [string]: { Signal.Signal<any> } },

		Changed: Signal.Signal<ReplicaTypes.Data>,
	},
	ReplicaClient
))

-- Used to create a new ReplicaClient object
local function newReplicaClient(
	existingReplica: ReplicaTypes.ReplicaFetchPayloadItem
): ReplicaClient
	assert(RunService:IsClient(), "ReplicaClient can only be used on the client")
	assert(existingReplica, "Expected existing replica")

	local self = setmetatable({
		replicaId = existingReplica.replicaId,
		class = existingReplica.class,
		data = existingReplica.data or {},

		keyChangedSignals = {},

		Changed = Signal.new(),
	}, ReplicaClient) :: ReplicaClient

	table.insert(replicas, self)

	if signals[self.class] then
		signals[self.class]:Fire(self)
	end

	return self
end

--[=[
	Returns a Signal that fires when a key in the replica is updated

	Usage:
	```lua
	local replica = ReplicaClient.getReplicaById("replicaId")

	replica:GetKeyChangedSignal("key"):Connect(function(value)
		print("Key changed:", value)
	end)
	```

	@method GetKeyChangedSignal
	@within ReplicaClient
	@param key string -- The key to get the value of
	@return Signal.Signal<ReplicaClient>
]=]
function ReplicaClient.GetKeyChangedSignal(
	self: ReplicaClient,
	key: string
): Signal.Signal<ReplicaClient>
	assert(RunService:IsClient(), "ReplicaClient can only be used on the client")
	assert(type(key) == "string", "Expected string for key")

	local signal = Signal.new()

	if not self.keyChangedSignals[key] then
		self.keyChangedSignals[key] = {}
	end

	table.insert(self.keyChangedSignals[key], signal)

	return signal
end

--[=[
	Gets an existing replica

	@param replicaId string -- The id of the replica to get
	@return ReplicaClient?
]=]
function ReplicaClient.getReplicaById(replicaId: string): ReplicaClient?
	for _, replica in replicas do
		if replica.replicaId == replicaId then
			return replica
		end
	end

	return nil
end

--[=[
	Starts listening for new replicated objects of the specified class

	Use `fireForExisting` to fire the signal for all existing replicas of the specified class.

	@param class string -- Configuration for the replicated object
	@param fireForExisting boolean? -- If true, fire the signal for all existing replicas
	@return Signal.Signal<ReplicaClient> -- The signal that is fired when a new replica is added
]=]
function ReplicaClient.getReplicaAddedSignal(
	class: string,
	fireForExisting: boolean? -- If true, fire the signal for all existing replicas
): Signal.Signal<ReplicaClient>
	assert(RunService:IsClient(), "ReplicaClient can only be used on the client")
	assert(type(class) == "string", "Expected string for class")
	assert(
		type(fireForExisting) == "boolean" or fireForExisting == nil,
		"Expected boolean or nil for fireForExisting"
	)

	-- Call the server to fetch all replicas of class
	remote:FireServer({ type = "FETCH_CLASS_REPLICAS", payload = class })

	-- Create a new listener signal if it doesn't exist
	if not signals[class] then
		signals[class] = Signal.new()
	end

	local newSignal: Signal.Signal<ReplicaClient> = Signal.new()

	signals[class].onDestroy = function()
		signals[class] = nil
	end

	signals[class]:Connect(function(replica: ReplicaClient)
		newSignal:Fire(replica)
	end)

	if fireForExisting then
		-- Fire the signal for any existing replicas
		for _, replica in replicas do
			if replica.class == class then
				newSignal:Fire(replica)
			end
		end
	end

	return newSignal
end

if RunService:IsClient() then
	remote = script.Parent:WaitForChild("ReplicaEvents", 8)

	remote.OnClientEvent:Connect(function(action: ReplicaTypes.Action)
		print("recevied from server", action.type)

		if action.type == "FETCH_CLASS_REPLICAS" then
			assert(not replicas[action.payload.replicaId], "Replica already exists")

			-- Create new replica instances for each existing replica
			for _, existingReplica: ReplicaTypes.ReplicaFetchPayloadItem in action.payload do
				newReplicaClient(existingReplica)
			end
		elseif action.type == "REPLICA_NEW" then
			-- Does the replica already exist?
			for _, replica: ReplicaClient in replicas do
				if replica.replicaId == action.payload.replicaId then
					return
				end
			end

			-- Create a new replica instance
			newReplicaClient(action.payload :: ReplicaTypes.ReplicaFetchPayloadItem)
		elseif action.type == "REPLICA_UPDATE" then
			local replica = ReplicaClient.getReplicaById(action.payload.replicaId)

			if not replica then
				return
			end

			replica.data[action.payload.key] = action.payload.value

			replica.Changed:Fire(replica.data)

			if replica.keyChangedSignals[action.payload.key] then
				for _, signal in replica.keyChangedSignals[action.payload.key] do
					signal:Fire(action.payload.value)
				end
			end

			-- FIRE ANY ATTACHED SIGNALS THAT LISTEN FOR THE REPLICA_UPDATE KEY
		elseif action.type == "REPLICA_DESTROY" then
			local replica = ReplicaClient.getReplicaById(action.payload.replicaId)

			if not replica then
				return
			end

			for index, child: ReplicaClient in replicas do
				if child == replica then
					-- if child.Destroying then
					-- 	child.Destroying:Fire()
					-- end

					table.remove(replicas, index)
					break
				end
			end
		end
	end)
end

return ReplicaClient


--[=[
	@interface Action
	@within Replica
	.type string -- The type of the action
	.payload any -- The data of the action
]=]
export type Action = {
	type: string,
	payload: any,
}

--[=[
	@interface ReplicaTypes
	@within Replica
	.replicaId string -- The unique identifier for the replica
	.class string -- The class of the replica
	.key string -- The key of the data that was changed
	.value any -- The new value of the data (Set to nil or leave empty to remove the key)
]=]
export type EventData = Action & {
	payload: {
		replicaId: string,
		class: string,
		key: string,
		value: any,
	},
}

export type ClientAction = Action & {
	payload: string,
}

export type InitialReplicaAction = Action & {
	payload: {
		replicaId: string,
		class: string,
		data: Data,
	},
}

export type ReplicaUpdateAction = Action & {
	payload: {
		replicaId: string,
		class: string,
		key: string,
		value: any,
	},
}

export type ReplicaFetchPayloadItem = {
	replicaId: string,
	class: string,
	data: Data,
}

export type ReplicaFetchAction = Action & {
	type: "FETCH_CLASS_REPLICAS",
	payload: { ReplicaFetchPayloadItem },
}

--[=[
	@type Data { [string]: any }
	@within Replica
]=]
export type Data = { [string]: any }

return nil


--[=[
	@class Replica

	A class for creating replicated objects.

	[Replica.Server](/api/ReplicaServer) is used on the server to create and manage replicated objects.
	[Replica.Client](/api/ReplicaClient) is used on the client to receive replicated objects.

	Install with wally by adding the following to your `wally.toml`:
	```toml
	Replica = "dig1t/replica@1.0.3"
	```
]=]

--!strict

local ReplicaClient = require(script.ReplicaClient)
local ReplicaServer = require(script.ReplicaServer)
local ReplicaTypes = require(script.ReplicaTypes)

export type ReplicaServer = ReplicaServer.ReplicaServer
export type ReplicaClient = ReplicaClient.ReplicaClient

export type EventData = ReplicaTypes.EventData
export type Data = ReplicaTypes.Data

local Replica = {}

--[=[
	@prop Server ReplicaServer
	@within Replica
]=]
Replica.Server = ReplicaServer

--[=[
	@prop Client ReplicaClient
	@within Replica
]=]
Replica.Client = ReplicaClient

return Replica