--!strict
--!nolint LocalUnused
--!nolint LocalShadow
local task = nil -- Disable usage of Roblox's task scheduler

--[[
	Given a `tweenInfo` and `currentTime`, returns a ratio which can be used to
	tween between two values over time.
]]

local TweenService = game:GetService("TweenService")

local function getTweenRatio(
	tweenInfo: TweenInfo,
	currentTime: number
): number
	local delay = tweenInfo.DelayTime
	local duration = tweenInfo.Time
	local reverses = tweenInfo.Reverses
	local numCycles = 1 + tweenInfo.RepeatCount
	local easeStyle = tweenInfo.EasingStyle
	local easeDirection = tweenInfo.EasingDirection
	local cycleDuration = delay + duration
	if reverses then
		cycleDuration += duration
	end
	-- If currentTime is infinity, then presumably the tween should be over.
	-- This avoids NaN when the duration of an infinitely repeating tween is given.
	if currentTime == math.huge then
		return 1
	end
	if currentTime >= cycleDuration * numCycles and tweenInfo.RepeatCount > -1 then
		return 1
	end
	local cycleTime = currentTime % cycleDuration
	if cycleTime <= delay then
		return 0
	end
	local tweenProgress = (cycleTime - delay) / duration
	if tweenProgress > 1 then
		tweenProgress = 2 - tweenProgress
	end
	local ratio = TweenService:GetValue(tweenProgress, easeStyle, easeDirection)
	return ratio
end

return getTweenRatio


--!strict
--!nolint LocalUnused
--!nolint LocalShadow
local task = nil -- Disable usage of Roblox's task scheduler

--[[
	Packs an array of numbers into a given animatable data type.
	If the type is not animatable, nil will be returned.
]]

local Package = script.Parent.Parent
local Types = require(Package.Types)
local Oklab = require(Package.Colour.Oklab)

local function packType(
	numbers: {number},
	typeString: string
): Types.Animatable?
	if typeString == "number" then
		return numbers[1]

	elseif typeString == "CFrame" then
		return
			CFrame.new(numbers[1], numbers[2], numbers[3]) *
			CFrame.fromAxisAngle(
				Vector3.new(numbers[4], numbers[5], numbers[6]).Unit,
				numbers[7]
			)

	elseif typeString == "Color3" then
		return Oklab.toSRGB(
			Vector3.new(numbers[1], numbers[2], numbers[3]),
			false
		)

	elseif typeString == "ColorSequenceKeypoint" then
		return ColorSequenceKeypoint.new(
			numbers[4],
			Oklab.toSRGB(
				Vector3.new(numbers[1], numbers[2], numbers[3]),
				false
			)
		)

	elseif typeString == "DateTime" then
		return DateTime.fromUnixTimestampMillis(numbers[1])

	elseif typeString == "NumberRange" then
		return NumberRange.new(numbers[1], numbers[2])

	elseif typeString == "NumberSequenceKeypoint" then
		return NumberSequenceKeypoint.new(numbers[2], numbers[1], numbers[3])

	elseif typeString == "PhysicalProperties" then
		return PhysicalProperties.new(numbers[1], numbers[2], numbers[3], numbers[4], numbers[5])

	elseif typeString == "Ray" then
		return Ray.new(
			Vector3.new(numbers[1], numbers[2], numbers[3]),
			Vector3.new(numbers[4], numbers[5], numbers[6])
		)

	elseif typeString == "Rect" then
		return Rect.new(numbers[1], numbers[2], numbers[3], numbers[4])

	elseif typeString == "Region3" then
		-- FUTURE: support rotated Region3s if/when they become constructable
		local position = Vector3.new(numbers[1], numbers[2], numbers[3])
		local halfSize = Vector3.new(numbers[4] / 2, numbers[5] / 2, numbers[6] / 2)
		return Region3.new(position - halfSize, position + halfSize)

	elseif typeString == "Region3int16" then
		return Region3int16.new(
			Vector3int16.new(math.round(numbers[1]), math.round(numbers[2]), math.round(numbers[3])),
			Vector3int16.new(math.round(numbers[4]), math.round(numbers[5]), math.round(numbers[6]))
		)

	elseif typeString == "UDim" then
		return UDim.new(numbers[1], math.round(numbers[2]))

	elseif typeString == "UDim2" then
		return UDim2.new(numbers[1], math.round(numbers[2]), numbers[3], math.round(numbers[4]))

	elseif typeString == "Vector2" then
		return Vector2.new(numbers[1], numbers[2])

	elseif typeString == "Vector2int16" then
		return Vector2int16.new(math.round(numbers[1]), math.round(numbers[2]))

	elseif typeString == "Vector3" then
		return Vector3.new(numbers[1], numbers[2], numbers[3])

	elseif typeString == "Vector3int16" then
		return Vector3int16.new(math.round(numbers[1]), math.round(numbers[2]), math.round(numbers[3]))
	else
		return nil
	end
end

return packType


--!strict
--!nolint LocalUnused
--!nolint LocalShadow
local task = nil -- Disable usage of Roblox's task scheduler

--[[
	Returns a 2x2 matrix of coefficients for a given time, damping and angular
	frequency (aka 'speed').

	Specifically, this returns four coefficients - posPos, posVel, velPos, and
	velVel - which can be multiplied with position and velocity like so:

	local newPosition = oldPosition * posPos + oldVelocity * posVel
	local newVelocity = oldPosition * velPos + oldVelocity * velVel

	For speed = 1 and damping = 0, the result is a simple harmonic oscillator
	with a period of tau.

	Special thanks to AxisAngle for helping to improve numerical precision.
]]

local function springCoefficients(
	time: number,
	damping: number,
	speed: number
): (number, number, number, number)
	-- if time or speed is 0, then the spring won't move
	if time == 0 or speed == 0 then
		return 1, 0, 0, 1
	end
	local posPos, posVel, velPos, velVel

	if damping > 1 then
		-- overdamped spring

		local alpha = math.sqrt(damping^2 - 1)
		local negHalf_over_alpha_speed = -0.5 / (alpha * speed)
		local z1 = speed * (alpha + damping) * -1
		local z2 = speed * (alpha - damping)
		local exp1 = math.exp(time * z1)
		local exp2 = math.exp(time * z2)

		posPos = (exp2 * z1 - exp1 * z2) * negHalf_over_alpha_speed
		posVel = (exp1 - exp2) * negHalf_over_alpha_speed / speed
		velPos = (exp2 - exp1) * negHalf_over_alpha_speed * speed
		velVel = (exp1 * z1 - exp2 * z2) * negHalf_over_alpha_speed

	elseif damping == 1 then
		-- critically damped spring

		local time_speed = time * speed
		local time_speed_neg1 = time_speed * -1
		local exp = math.exp(time_speed_neg1)

		posPos = exp * (time_speed + 1)
		posVel = exp * time
		velPos = exp * (time_speed_neg1 * speed)
		velVel = exp * (time_speed_neg1 + 1)
	else
		-- underdamped spring

		local alpha = speed * math.sqrt(1 - damping^2)
		local overAlpha = 1 / alpha
		local exp = math.exp(-1 * time * speed * damping)
		local sin = math.sin(alpha * time)
		local cos = math.cos(alpha * time)
		local exp_sin = exp * sin
		local exp_cos = exp * cos
		local exp_sin_speed_damping_overAlpha = exp_sin * speed * damping * overAlpha

		posPos = exp_sin_speed_damping_overAlpha + exp_cos
		posVel = exp_sin * overAlpha
		velPos = -1 * ( exp_sin * alpha + speed * damping * exp_sin_speed_damping_overAlpha )
		velVel = exp_cos - exp_sin_speed_damping_overAlpha
	end

	return posPos, posVel, velPos, velVel
end

return springCoefficients


--!strict
--!nolint LocalUnused
--!nolint LocalShadow
local task = nil -- Disable usage of Roblox's task scheduler

--[[
	Abstraction layer between Fusion internals and external environments,
	allowing for flexible integration with schedulers and test mocks.
]]

local Package = script.Parent
local formatError = require(Package.Logging.formatError)
local Types = require(Package.Types)

local ERROR_INFO_URL = "https://elttob.uk/Fusion/0.3/api-reference/general/errors/#"

local External = {}

-- Multiplier for running-time safety checks across the Fusion codebase. Used to
-- stricten tests on infinite loop detection during unit testing.
External.safetyTimerMultiplier = 1

local updateStepCallbacks = {}
local currentProvider: Types.ExternalProvider? = nil
local lastUpdateStep = 0

--[[
	Swaps to a new provider for external operations.
	Returns the old provider, so it can be used again later.
]]
function External.setExternalProvider(
	newProvider: Types.ExternalProvider?
): Types.ExternalProvider?
	local oldProvider = currentProvider
	if oldProvider ~= nil then
		oldProvider.stopScheduler()
	end
	currentProvider = newProvider
	if newProvider ~= nil then
		newProvider.startScheduler()
	end
	return oldProvider
end

--[[
   Sends an immediate task to the external provider. Throws if none is set.
]]
function External.doTaskImmediate(
	resume: () -> ()
)
	if currentProvider == nil then
		External.logError("noTaskScheduler")
	else
		currentProvider.doTaskImmediate(resume)
	end
end

--[[
	Sends a deferred task to the external provider. Throws if none is set.
]]
function External.doTaskDeferred(
	resume: () -> ()
)
	if currentProvider == nil then
		External.logError("noTaskScheduler")
	else
		currentProvider.doTaskDeferred(resume)
	end
end

--[[
	Errors in the current thread and halts execution.
]]
function External.logError(
	messageID: string,
	errObj: Types.Error?,
	...: unknown
): never
	error(formatError(currentProvider, messageID, errObj, ...), 0)
end

--[[
	Errors in a different thread to preserve the flow of execution.
]]
function External.logErrorNonFatal(
	messageID: string,
	errObj: Types.Error?,
	...: unknown
): ()
	local errorString = formatError(currentProvider, messageID, errObj, ...)
	if currentProvider ~= nil then
		currentProvider.logErrorNonFatal(errorString)
	else
		print(errorString)
	end
end

--[[
	Shows a warning message in the output.
]]
function External.logWarn(
	messageID: string,
	...: unknown
): ()
	local errorString = formatError(currentProvider, messageID, debug.traceback(nil, 2), ...)
	if currentProvider ~= nil then
		currentProvider.logWarn(errorString)
	else
		print(errorString)
	end
end

--[[
	Registers a callback to the update step of the external provider.
	Returns a function that can be used to disconnect later.

	Callbacks are given the current number of seconds since an arbitrary epoch.

	TODO: This epoch may change between providers. We could investigate ways
	of allowing providers to co-operate to keep the epoch the same, so that
	monotonicity can be better preserved.
]]
function External.bindToUpdateStep(
	callback: (
		now: number
	) -> ()
): () -> ()
	local uniqueIdentifier = {}
	updateStepCallbacks[uniqueIdentifier] = callback
	return function()
		updateStepCallbacks[uniqueIdentifier] = nil
	end
end

--[[
	Steps time-dependent systems with the current number of seconds since an
	arbitrary epoch. This should be called as early as possible in the external
	provider's update cycle.
]]
function External.performUpdateStep(
	now: number
)
	lastUpdateStep = now
	for _, callback in updateStepCallbacks do
		callback(now)
	end
end

--[[
	Returns the timestamp of the last update step.
]]
function External.lastUpdateStep()
	return lastUpdateStep
end

return External


--!strict
--!nolint LocalUnused
--!nolint LocalShadow
local task = nil -- Disable usage of Roblox's task scheduler

--[[
	A graph object that runs user code when it's updated by the reactive graph.

	http://elttob.uk/Fusion/0.3/api-reference/state/types/observer/
]]

local Package = script.Parent.Parent
local Types = require(Package.Types)
local External = require(Package.External)
-- Memory
local checkLifetime = require(Package.Memory.checkLifetime)
-- Graph
local castToGraph = require(Package.Graph.castToGraph)
local depend = require(Package.Graph.depend)
local evaluate = require(Package.Graph.evaluate)
-- Utility
local nicknames = require(Package.Utility.nicknames)

type Self = Types.Observer & {
	_watchingGraph: Types.GraphObject?,
	_changeListeners: {[unknown]: () -> ()}
}

local class = {}
class.type = "Observer"
class.timeliness = "eager"
class.dependentSet = table.freeze {}

local METATABLE = table.freeze {__index = class}

local function Observer(
	scope: Types.Scope<unknown>,
	watching: unknown
): Types.Observer
	local createdAt = os.clock()

	local self: Self = setmetatable(
		{
			scope = scope,
			createdAt = createdAt,
			dependencySet = {},
			lastChange = nil,
			validity = "invalid",
			_watchingGraph = castToGraph(watching),
			_changeListeners = {}
		},
		METATABLE
	) :: any
	local destroy = function()
		self.scope = nil
		for dependency in pairs(self.dependencySet) do
			dependency.dependentSet[self] = nil
		end
	end
	self.oldestTask = destroy
	nicknames[self.oldestTask] = "Observer"
	table.insert(scope, destroy)

	if self._watchingGraph ~= nil then
		checkLifetime.bOutlivesA(
			scope, self.oldestTask,
			self._watchingGraph.scope, self._watchingGraph.oldestTask,
			checkLifetime.formatters.observer
		)
	end

	-- Eagerly evaluated objects need to evaluate themselves so that they're
	-- valid at all times.
	evaluate(self, true)

	return self
end

function class.onBind(
	self: Self,
	callback: () -> ()
): () -> ()
	External.doTaskImmediate(callback)
	return self:onChange(callback)
end

function class.onChange(
	self: Self,
	callback: () -> ()
): () -> ()
	local uniqueIdentifier = table.freeze {}
	self._changeListeners[uniqueIdentifier] = callback
	return function()
		self._changeListeners[uniqueIdentifier] = nil
	end
end

function class._evaluate(
	self: Self
): ()
	if self._watchingGraph ~= nil then
		depend(self, self._watchingGraph)
	end
	for _, callback in self._changeListeners do
		External.doTaskImmediate(callback)
	end
	return true
end

table.freeze(class)
return Observer :: Types.ObserverConstructor


--!strict
--!nolint LocalUnused
--!nolint LocalShadow
local task = nil -- Disable usage of Roblox's task scheduler

--[[
	A special key for property tables, which allows users to save instance attributes
	into state objects
]]

local Package = script.Parent.Parent
local Types = require(Package.Types)
local External = require(Package.External)
-- Memory
local checkLifetime = require(Package.Memory.checkLifetime)
-- State
local castToState = require(Package.State.castToState)

local keyCache: {[string]: Types.SpecialKey} = {}

local function AttributeOut(
	attributeName: string
): Types.SpecialKey
	local key = keyCache[attributeName]
	if key == nil then
		key = {
			type = "SpecialKey",
			kind = "AttributeOut",
			stage = "observer",
			apply = function(
				self: Types.SpecialKey,
				scope: Types.Scope<unknown>,
				value: unknown,
				applyTo: Instance
			)
				local event = applyTo:GetAttributeChangedSignal(attributeName)

				if not castToState(value) then
					External.logError("invalidAttributeOutType")
				end
				local value = value :: Types.StateObject<unknown>
				if value.kind ~= "Value" then
					External.logError("invalidAttributeOutType")
				end
				local value = value :: Types.Value<unknown>
				checkLifetime.bOutlivesA(
					scope, applyTo,
					value.scope, value.oldestTask,
					checkLifetime.formatters.attributeOutputsTo, attributeName
				)

				value:set((applyTo :: any):GetAttribute(attributeName))
				table.insert(scope, event:Connect(function()
					value:set((applyTo :: any):GetAttribute(attributeName))
				end))
			end
		}
		keyCache[attributeName] = key
	end
	return key
end

return AttributeOut


--!strict
--!nolint LocalUnused
--!nolint LocalShadow
local task = nil -- Disable usage of Roblox's task scheduler

--[[
	Helper function for type checking purposes. Casts the input to a `Child`
	type, while constraining the input to be an array of `Child` - this prevents
	Luau from erroneously inferring a different array type for the input.
]]

local Package = script.Parent.Parent
local Types = require(Package.Types)

local function Child(
	x: {Types.Child}
): Types.Child
	return x
end

return Child


--!strict
--!nolint LocalUnused
--!nolint LocalShadow
local task = nil -- Disable usage of Roblox's task scheduler

--[[
	Processes and returns an existing instance, with options for setting
	properties, event handlers and other attributes on the instance.
]]

local Package = script.Parent.Parent
local Types = require(Package.Types)
local applyInstanceProps = require(Package.Instances.applyInstanceProps)

local function Hydrate(
	scope: Types.Scope<unknown>,
	target: Instance
)
	return function(
		props: Types.PropertyTable
	): Instance

		table.insert(scope, target)
		applyInstanceProps(scope, props, target)
		return target
	end
end

return Hydrate


--!strict
--!nolint LocalUnused
--!nolint LocalShadow
local task = nil -- Disable usage of Roblox's task scheduler

--[[
	Constructs and returns a new instance, with options for setting properties,
	event handlers and other attributes on the instance right away.
]]

local Package = script.Parent.Parent
local Types = require(Package.Types)
local External = require(Package.External)
local defaultProps = require(Package.Instances.defaultProps)
local applyInstanceProps = require(Package.Instances.applyInstanceProps)

type Component = (Types.PropertyTable) -> Instance

local function New(
	scope: Types.Scope<unknown>,
	className: string
)
	-- This might look appealing to try and cache. But please don't. The scope
	-- upvalue is shared between the two curried function calls, so this will
	-- open incredible cross-codebase wormholes like you've never seen before.
	return function(
		props: Types.PropertyTable
	): Instance
		local ok, instance = pcall(Instance.new, className)
		if not ok then
			External.logError("cannotCreateClass", nil, className)
		end

		local classDefaults = defaultProps[className]
		if classDefaults ~= nil then
			for defaultProp, defaultValue in pairs(classDefaults) do
				(instance :: any)[defaultProp] = defaultValue
			end
		end

		table.insert(scope, instance)
		applyInstanceProps(scope, props, instance)

		return instance
	end
end

return New


--!strict
--!nolint LocalUnused
--!nolint LocalShadow
local task = nil -- Disable usage of Roblox's task scheduler

--[[
	A special key for property tables, which allows users to extract values from
	an instance into an automatically-updated Value object.
]]

local Package = script.Parent.Parent
local Types = require(Package.Types)
local External = require(Package.External)
-- Memory
local checkLifetime = require(Package.Memory.checkLifetime)
-- State
local castToState = require(Package.State.castToState)

local keyCache: {[string]: Types.SpecialKey} = {}

local function Out(
	propertyName: string
): Types.SpecialKey
	local key = keyCache[propertyName]
	if key == nil then
		key = {
			type = "SpecialKey",
			kind = "Out",
			stage = "observer",
			apply = function(
				self: Types.SpecialKey,
				scope: Types.Scope<unknown>,
				value: unknown,
				applyTo: Instance
			)
				local ok, event = pcall(applyTo.GetPropertyChangedSignal, applyTo, propertyName)
				if not ok then
					External.logError("invalidOutProperty", nil, applyTo.ClassName, propertyName)
				end

				if not castToState(value) then
					External.logError("invalidOutType")
				end
				local value = value :: Types.StateObject<unknown>
				if value.kind ~= "Value" then
					External.logError("invalidOutType")
				end
				local value = value :: Types.Value<unknown>
				checkLifetime.bOutlivesA(
					scope, applyTo,
					value.scope, value.oldestTask,
					checkLifetime.formatters.propertyOutputsTo, propertyName
				)

				value:set((applyTo :: any)[propertyName])
				table.insert(
					scope,
					event:Connect(function()
						value:set((applyTo :: any)[propertyName])
					end)
				)
			end
		}
		keyCache[propertyName] = key
	end
	return key
end

return Out


--!strict
--!nolint LocalUnused
--!nolint LocalShadow
local task = nil -- Disable usage of Roblox's task scheduler

--[[
	A special key for property tables, which allows users to apply custom
	CollectionService tags to instances
]]

local Package = script.Parent.Parent
local Types = require(Package.Types)
-- Memory
local checkLifetime = require(Package.Memory.checkLifetime)
-- Graph
local Observer = require(Package.Graph.Observer)
-- State
local castToState = require(Package.State.castToState)
local peek = require(Package.State.peek)

local keyCache: { [string]: Types.SpecialKey } = {}

-- TODO: should this accept tagName: UsedAs<string>?
local function Tag(tagName: string): Types.SpecialKey
	local key = keyCache[tagName]
	if key == nil then
		key = {
			type = "SpecialKey",
			kind = "Tag",
			stage = "self",
			apply = function(self: Types.SpecialKey, scope: Types.Scope<unknown>, value: unknown, applyTo: Instance)
				if castToState(value) then
					local value = value :: Types.StateObject<unknown>
					checkLifetime.bOutlivesA(
						scope,
						applyTo,
						value.scope,
						value.oldestTask,
						checkLifetime.formatters.boundTag,
						tagName
					)
					Observer(scope, value :: any):onBind(function()
						if peek(value) == true then
							applyTo:AddTag(tagName)
						elseif applyTo:HasTag(tagName) then
							applyTo:RemoveTag(tagName)
						end
					end)
				else
					if value == true then
						applyTo:AddTag(tagName)
					end
				end
			end,
		}
		keyCache[tagName] = key
	end
	return key
end

return Tag


--!strict
--!nolint LocalUnused
--!nolint LocalShadow
local task = nil -- Disable usage of Roblox's task scheduler

--[[
	Applies a table of properties to an instance, including binding to any
	given state objects and applying any special keys.

	No strong reference is kept by default - special keys should take care not
	to accidentally hold strong references to instances forever.

	If a key is used twice, an error will be thrown. This is done to avoid
	double assignments or double bindings. However, some special keys may want
	to enable such assignments - in which case unique keys should be used for
	each occurrence.
]]

local Package = script.Parent.Parent
local Types = require(Package.Types)
local External = require(Package.External)
-- Logging
local parseError = require(Package.Logging.parseError)
-- Memory
local checkLifetime = require(Package.Memory.checkLifetime)
-- Graph
local Observer = require(Package.Graph.Observer)
-- State
local castToState = require(Package.State.castToState)
local peek = require(Package.State.peek)
-- Utility
local xtypeof = require(Package.Utility.xtypeof)

local function setProperty_unsafe(
	instance: Instance,
	property: string,
	value: unknown
)
	(instance :: any)[property] = value
end

local function testPropertyAssignable(
	instance: Instance,
	property: string
)
	(instance :: any)[property] = (instance :: any)[property]
end

local function setProperty(
	instance: Instance,
	property: string,
	value: unknown
)
	local success, err = xpcall(setProperty_unsafe :: any, parseError, instance, property, value)

	if not success then
		if not pcall(testPropertyAssignable, instance, property) then
			External.logErrorNonFatal("cannotAssignProperty", nil, instance.ClassName, property)
		else
			-- property is assignable, but this specific assignment failed
			-- this typically implies the wrong type was received
			local givenType = typeof(value)
			local expectedType = typeof((instance :: any)[property])

			if givenType == expectedType then
				External.logErrorNonFatal("propertySetError", err)
			else
				External.logErrorNonFatal("invalidPropertyType", nil, instance.ClassName, property, expectedType, givenType)
			end
		end
	end
end

local function bindProperty(
	scope: Types.Scope<unknown>,
	instance: Instance,
	property: string,
	value: Types.UsedAs<unknown>
)
	if castToState(value) then
		local value = value :: Types.StateObject<unknown>
		checkLifetime.bOutlivesA(
			scope, instance,
			value.scope, value.oldestTask,
			checkLifetime.formatters.boundProperty, property
		)
		-- value is a state object - bind to changes
		Observer(scope, value :: any):onBind(function()
			setProperty(instance, property, peek(value))
		end)
	else
		-- value is a constant - assign once only
		setProperty(instance, property, value)
	end
end

local function applyInstanceProps(
	scope: Types.Scope<unknown>,
	props: Types.PropertyTable,
	applyTo: Instance
)
	local specialKeys = {
		self = {} :: {[Types.SpecialKey]: unknown},
		descendants = {} :: {[Types.SpecialKey]: unknown},
		ancestor = {} :: {[Types.SpecialKey]: unknown},
		observer = {} :: {[Types.SpecialKey]: unknown}
	}

	for key, value in pairs(props) do
		local keyType = xtypeof(key)

		if keyType == "string" then
			if key ~= "Parent" then
				bindProperty(scope, applyTo, key :: string, value)
			end
		elseif keyType == "SpecialKey" then
			local stage = (key :: Types.SpecialKey).stage
			local keys = specialKeys[stage]
			if keys == nil then
				External.logError("unrecognisedPropertyStage", nil, stage)
			else
				keys[key] = value
			end
		else
			-- we don't recognise what this key is supposed to be
			External.logError("unrecognisedPropertyKey", nil, keyType)
		end
	end

	for key, value in pairs(specialKeys.self) do
		key:apply(scope, value, applyTo)
	end
	for key, value in pairs(specialKeys.descendants) do
		key:apply(scope, value, applyTo)
	end

	if props.Parent ~= nil then
		bindProperty(scope, applyTo, "Parent", props.Parent)
	end

	for key, value in pairs(specialKeys.ancestor) do
		key:apply(scope, value, applyTo)
	end
	for key, value in pairs(specialKeys.observer) do
		key:apply(scope, value, applyTo)
	end
end

return applyInstanceProps


--!strict
--!nolint LocalUnused
--!nolint LocalShadow
local task = nil -- Disable usage of Roblox's task scheduler

--[[
	Stores 'sensible default' properties to be applied to instances created by
	the New function.
]]

return {
	ScreenGui = {
		ResetOnSpawn = false,
		ZIndexBehavior = Enum.ZIndexBehavior.Sibling
	},

	BillboardGui = {
		ResetOnSpawn = false,
		ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
		Active = true
	},

	SurfaceGui = {
		ResetOnSpawn = false,
		ZIndexBehavior = Enum.ZIndexBehavior.Sibling,

		SizingMode = Enum.SurfaceGuiSizingMode.PixelsPerStud,
		PixelsPerStud = 50
	},

	Frame = {
		BackgroundColor3 = Color3.new(1, 1, 1),
		BorderColor3 = Color3.new(0, 0, 0),
		BorderSizePixel = 0
	},

	ScrollingFrame = {
		BackgroundColor3 = Color3.new(1, 1, 1),
		BorderColor3 = Color3.new(0, 0, 0),
		BorderSizePixel = 0,

		ScrollBarImageColor3 = Color3.new(0, 0, 0)
	},

	TextLabel = {
		BackgroundColor3 = Color3.new(1, 1, 1),
		BorderColor3 = Color3.new(0, 0, 0),
		BorderSizePixel = 0,

		Font = Enum.Font.SourceSans,
		Text = "",
		TextColor3 = Color3.new(0, 0, 0),
		TextSize = 14
	},

	TextButton = {
		BackgroundColor3 = Color3.new(1, 1, 1),
		BorderColor3 = Color3.new(0, 0, 0),
		BorderSizePixel = 0,

		AutoButtonColor = false,

		Font = Enum.Font.SourceSans,
		Text = "",
		TextColor3 = Color3.new(0, 0, 0),
		TextSize = 14
	},

	TextBox = {
		BackgroundColor3 = Color3.new(1, 1, 1),
		BorderColor3 = Color3.new(0, 0, 0),
		BorderSizePixel = 0,

		ClearTextOnFocus = false,

		Font = Enum.Font.SourceSans,
		Text = "",
		TextColor3 = Color3.new(0, 0, 0),
		TextSize = 14
	},

	ImageLabel = {
		BackgroundColor3 = Color3.new(1, 1, 1),
		BorderColor3 = Color3.new(0, 0, 0),
		BorderSizePixel = 0
	},

	ImageButton = {
		BackgroundColor3 = Color3.new(1, 1, 1),
		BorderColor3 = Color3.new(0, 0, 0),
		BorderSizePixel = 0,

		AutoButtonColor = false
	},

	ViewportFrame = {
		BackgroundColor3 = Color3.new(1, 1, 1),
		BorderColor3 = Color3.new(0, 0, 0),
		BorderSizePixel = 0
	},

	VideoFrame = {
		BackgroundColor3 = Color3.new(1, 1, 1),
		BorderColor3 = Color3.new(0, 0, 0),
		BorderSizePixel = 0
	},

	CanvasGroup = {
		BackgroundColor3 = Color3.new(1, 1, 1),
		BorderColor3 = Color3.new(0, 0, 0),
		BorderSizePixel = 0
	},

	SpawnLocation = {
		Duration = 0
	},

	BoxHandleAdornment = {
		ZIndex = 0
	},
	ConeHandleAdornment = {
		ZIndex = 0
	},
	CylinderHandleAdornment = {
		ZIndex = 0
	},
	ImageHandleAdornment = {
		ZIndex = 0
	},
	LineHandleAdornment = {
		ZIndex = 0
	},
	SphereHandleAdornment = {
		ZIndex = 0
	},
	WireframeHandleAdornment = {
		ZIndex = 0
	},

	Part = {
		Anchored = true,
		Size = Vector3.one,
		FrontSurface = Enum.SurfaceType.Smooth,
		BackSurface = Enum.SurfaceType.Smooth,
		LeftSurface = Enum.SurfaceType.Smooth,
		RightSurface = Enum.SurfaceType.Smooth,
		TopSurface = Enum.SurfaceType.Smooth,
		BottomSurface = Enum.SurfaceType.Smooth,
	},

	TrussPart = {
		Anchored = true,
		Size = Vector3.one * 2,
		FrontSurface = Enum.SurfaceType.Smooth,
		BackSurface = Enum.SurfaceType.Smooth,
		LeftSurface = Enum.SurfaceType.Smooth,
		RightSurface = Enum.SurfaceType.Smooth,
		TopSurface = Enum.SurfaceType.Smooth,
		BottomSurface = Enum.SurfaceType.Smooth,
	},

	MeshPart = {
		Anchored = true,
		Size = Vector3.one,
		FrontSurface = Enum.SurfaceType.Smooth,
		BackSurface = Enum.SurfaceType.Smooth,
		LeftSurface = Enum.SurfaceType.Smooth,
		RightSurface = Enum.SurfaceType.Smooth,
		TopSurface = Enum.SurfaceType.Smooth,
		BottomSurface = Enum.SurfaceType.Smooth,
	},

	CornerWedgePart = {
		Anchored = true,
		Size = Vector3.one,
		FrontSurface = Enum.SurfaceType.Smooth,
		BackSurface = Enum.SurfaceType.Smooth,
		LeftSurface = Enum.SurfaceType.Smooth,
		RightSurface = Enum.SurfaceType.Smooth,
		TopSurface = Enum.SurfaceType.Smooth,
		BottomSurface = Enum.SurfaceType.Smooth,
	},

	VehicleSeat = {
		Anchored = true,
		Size = Vector3.one,
		FrontSurface = Enum.SurfaceType.Smooth,
		BackSurface = Enum.SurfaceType.Smooth,
		LeftSurface = Enum.SurfaceType.Smooth,
		RightSurface = Enum.SurfaceType.Smooth,
		TopSurface = Enum.SurfaceType.Smooth,
		BottomSurface = Enum.SurfaceType.Smooth,
	},
}


--!strict
--!nolint LocalUnused
--!nolint LocalShadow
local task = nil -- Disable usage of Roblox's task scheduler

--[[
	Procedures for checking lifetimes and printing helpful warnings about them.
]]

local Package = script.Parent.Parent
local Types = require(Package.Types)
local External = require(Package.External)
local nameOf = require(Package.Utility.nameOf)

local checkLifetime = {}

checkLifetime.formatters = {}

function checkLifetime.formatters.useFunction(
	self: unknown,
	used: unknown
): (string, string)
	local selfName = nameOf(self, "object")
	local usedName = nameOf(used, "object")
	return `The use()-d {usedName}`, `the {selfName}`
end

function checkLifetime.formatters.boundProperty(
	instance: Instance,
	bound: unknown,
	property: string
): (string, string)
	local selfName = instance.Name
	local boundName = nameOf(bound, "value")
	return `The {boundName} (bound to the {property} property)`, `the {selfName} instance`
end

function checkLifetime.formatters.boundAttribute(
	instance: Instance,
	bound: unknown,
	attribute: string
): (string, string)
	local selfName = instance.Name
	local boundName = nameOf(bound, "value")
	return `The {boundName} (bound to the {attribute} attribute)`, `the {selfName} instance`
end

function checkLifetime.formatters.boundTag(
	instance: Instance,
	bound: unknown,
	tag: string
): (string, string)
	local selfName = instance.Name
	local boundName = nameOf(bound, "value")
	return `The {boundName} (bound to the {tag} CollectionService tag)`, `the {selfName} instance`
end

function checkLifetime.formatters.propertyOutputsTo(
	instance: Instance,
	bound: unknown,
	property: string
): (string, string)
	local selfName = instance.Name
	local boundName = nameOf(bound, "object")
	return `The {boundName} (which the {property} property outputs to)`, `the {selfName} instance`
end

function checkLifetime.formatters.attributeOutputsTo(
	instance: Instance,
	bound: unknown,
	attribute: string
): (string, string)
	local selfName = instance.Name
	local boundName = nameOf(bound, "object")
	return `The {boundName} (which the {attribute} attribute outputs to)`, `the {selfName} instance`
end

function checkLifetime.formatters.refOutputsTo(
	instance: Instance,
	bound: unknown
): (string, string)
	local selfName = instance.Name
	local boundName = nameOf(bound, "object")
	return `The {boundName} (which the Ref key outputs to)`, `the {selfName} instance`
end

function checkLifetime.formatters.animationGoal(
	self: unknown,
	goal: unknown
): (string, string)
	local selfName = nameOf(self, "object")
	local goalName = nameOf(goal, "object")
	return `The goal {goalName}`, `the {selfName} that is following it`
end

function checkLifetime.formatters.parameter(
	self: unknown,
	used: unknown,
	parameterName: string | false
): (string, string)
	local selfName = nameOf(self, "object")
	local usedName = nameOf(used, "object")
	if parameterName == false then
		return `The {usedName} parameter`, `the {selfName} that it was used for`
	else
		return `The {usedName} representing the {parameterName} parameter`, `the {selfName} that it was used for`
	end
end

function checkLifetime.formatters.observer(
	self: unknown,
	watched: unknown
): (string, string)
	local selfName = nameOf(self, "object")
	local watchedName = nameOf(watched, "object")
	return `The watched {watchedName}`, `the {selfName} that's observing it for changes`
end

function checkLifetime.bOutlivesA<A, B, Args...>(
	scopeA: Types.Scope<unknown>,
	a: A,
	scopeB: Types.Scope<unknown>?,
	b: B,
	formatter: (a: A, b: B, Args...) -> (string, string),
	...: Args...
)
	if scopeB == nil then
		External.logError("useAfterDestroy", nil, formatter(a, b, ...))
	end
end

return checkLifetime


--!strict
--!nolint LocalUnused
--!nolint LocalShadow
local task = nil -- Disable usage of Roblox's task scheduler

--[[
	Creates an empty scope with the same metatables as the original scope. Used
	for preserving access to constructors when creating inner scopes.

	This is the internal version of the function, which does not implement
	external debugging hooks.
]]
local Package = script.Parent.Parent
local Types = require(Package.Types)
local merge = require(Package.Utility.merge)

-- This return type is technically a lie, but it's required for useful type
-- checking behaviour.
local function deriveScopeImpl<T>(
	existing: Types.Scope<T>,
	methods: {[unknown]: unknown}?,
	...: {[unknown]: unknown}
): any
	local metatable = getmetatable(existing)
	if methods ~= nil then
		metatable = table.clone(metatable)
		metatable.__index = merge(
			true, {},
			metatable.__index,
			merge(
				false, {},
				methods,
				...
			)
		)
	end
	return setmetatable({}, metatable)
end

return (deriveScopeImpl :: any) :: Types.DeriveScopeConstructor


--!strict
--!nolint LocalUnused
--!nolint LocalShadow
local task = nil -- Disable usage of Roblox's task scheduler

--[[
	Cleans up the tasks passed in as the arguments.
	A task can be any of the following:

	- an Instance - will be destroyed
	- an RBXScriptConnection - will be disconnected
	- a function - will be run
	- a table with a `Destroy` or `destroy` function - will be called
	- an array - `cleanup` will be called on each item
]]
local Package = script.Parent.Parent
local Types = require(Package.Types)
local External = require(Package.External)
local ExternalDebug = require(Package.ExternalDebug)

local alreadyDestroying: {[Types.Task]: true} = {}

local function doCleanup(
	task: Types.Task
): ()
	if alreadyDestroying[task] then
		return External.logError("destroyedTwice")
	end
	alreadyDestroying[task] = true

	-- case 1: Instance
	if typeof(task) == "Instance" then
		task:Destroy()

	-- case 2: RBXScriptConnection
	elseif typeof(task) == "RBXScriptConnection" then
		task:Disconnect()

	-- case 3: callback
	elseif typeof(task) == "function" then
		task()

	elseif typeof(task) == "table" then
		local task = (
			task :: any
		) :: {
			Destroy: (...unknown) -> ...unknown?,
			destroy: (...unknown) -> ...unknown?,
			Disconnect: (...unknown) -> ...unknown?,
			disconnect: (...unknown) -> ...unknown?,
		}

		-- case 4: destroy() function
		if typeof(task.destroy) == "function" then
			local task = (task :: any) :: {destroy: (...unknown) -> (...unknown)}
			task:destroy()

		-- case 5: Destroy() function
		elseif typeof(task.Destroy) == "function" then
			local task = (task :: any) :: {Destroy: (...unknown) -> (...unknown)}
			task:Destroy()

		-- case 6: table of tasks with an array part
		elseif task[1] ~= nil then
			local task = task :: {Types.Task}

			-- It is important to iterate backwards through the table, since
			-- objects are added in order of construction.
			for index = #task, 1, -1 do
				doCleanup(task[index])
				task[index] = nil
			end

			ExternalDebug.untrackScope(task)

		-- case 7: Disconnect() function
		elseif typeof(task.Disconnect) == "function" then
			local task = (task :: any) :: { Disconnect: (...unknown) -> ...unknown }
			task:Disconnect()

		-- case 8: disconnect() function
		elseif typeof(task.disconnect) == "function" then
			local task = (task :: any) :: { disconnect: (...unknown) -> ...unknown }
			task:disconnect()
		end
	end

	alreadyDestroying[task] = nil
end

return doCleanup


--!strict
--!nolint LocalUnused
--!nolint LocalShadow
local task = nil -- Disable usage of Roblox's task scheduler

--[[
	Inserts clean up tasks passed in to the scope.
]]
local Package = script.Parent.Parent
local Types = require(Package.Types)

local function insert<Tasks...>(
	scope: Types.Scope<unknown>,
	...: Tasks...
): Tasks...
	for index = 1, select("#", ...) do
		table.insert(scope, select(index, ...))
	end
	return ...
end

return insert


--!strict
--!nolint LocalUnused
--!nolint LocalShadow
local task = nil -- Disable usage of Roblox's task scheduler

--[[
	Creates cleanup tables with access to constructors as methods.
]]

local Package = script.Parent.Parent
local Types = require(Package.Types)
local ExternalDebug = require(Package.ExternalDebug)
local merge = require(Package.Utility.merge)

local function scoped(
	...: {[unknown]: unknown}
): any
	local metatable = {__index = merge(false, {}, ...)}
	local scope = setmetatable({}, metatable) :: any
	ExternalDebug.trackScope(scope)
	return scope
end

return (scoped :: any) :: Types.ScopedConstructor


--!strict
--!nolint LocalUnused
--!nolint LocalShadow
local task = nil -- Disable usage of Roblox's task scheduler

--[[
	A specialised state object for tracking single values computed from a
	user-defined computation.

	https://elttob.uk/Fusion/0.3/api-reference/state/types/computed/
]]

local Package = script.Parent.Parent
local Types = require(Package.Types)
local External = require(Package.External)
-- Logging
local parseError = require(Package.Logging.parseError)
-- Utility
local isSimilar = require(Package.Utility.isSimilar)
-- Graph
local depend = require(Package.Graph.depend)
-- State
local castToState = require(Package.State.castToState)
local peek = require(Package.State.peek)
-- Memory
local doCleanup = require(Package.Memory.doCleanup)
local deriveScope = require(Package.Memory.deriveScope)
local checkLifetime = require(Package.Memory.checkLifetime)
-- Utility
local nicknames = require(Package.Utility.nicknames)

type Self<T, S> = Types.Computed<T> & {
	_innerScope: Types.Scope<S>?,
	_processor: (Types.Use, Types.Scope<S>) -> T
}

local class = {}
class.type = "State"
class.kind = "Computed"
class.timeliness = "lazy"

local METATABLE = table.freeze {__index = class}

local function Computed<T, S>(
	scope: S & Types.Scope<unknown>,
	processor: (Types.Use, S) -> T
): Types.Computed<T>
	local createdAt = os.clock()
	local self: Self<T, S> = setmetatable(
		{
			createdAt = createdAt,
			dependencySet = {},
			dependentSet = {},
			lastChange = nil,
			scope = scope,
			validity = "invalid",
			_EXTREMELY_DANGEROUS_usedAsValue = nil,
			_innerScope = nil,
			_processor = processor
		},
		METATABLE
	) :: any
	local destroy = function()
		self.scope = nil
		for dependency in pairs(self.dependencySet) do
			dependency.dependentSet[self] = nil
		end
		if self._innerScope ~= nil then
			doCleanup(self._innerScope)
			self._innerScope = nil
		end
	end
	self.oldestTask = destroy
	nicknames[self.oldestTask] = "Computed"
	table.insert(scope, destroy)
	return self
end

function class._evaluate<T, S>(
	self: Self<T, S>
): boolean
	if self.scope == nil then
		return false
	end
	local outerScope = self.scope :: S & Types.Scope<unknown>
	local innerScope = deriveScope(outerScope)
	local function use<T>(target: Types.UsedAs<T>): T
		local targetState = castToState(target)
		if targetState ~= nil then
			checkLifetime.bOutlivesA(
				outerScope, self.oldestTask,
				targetState.scope, targetState.oldestTask,
				checkLifetime.formatters.useFunction
			)
			depend(self, targetState)
		end
		return peek(target)
	end
	local ok, newValue = xpcall(self._processor, parseError, use, innerScope)
	if ok then
		local similar = isSimilar(self._EXTREMELY_DANGEROUS_usedAsValue, newValue)
		if self._innerScope ~= nil then
			doCleanup(self._innerScope)
			self._innerScope = nil
		end
		self._innerScope = innerScope

		self._EXTREMELY_DANGEROUS_usedAsValue = newValue
		return not similar
	else
		local errorObj = (newValue :: any) :: Types.Error
		doCleanup(innerScope)

		-- this needs to be non-fatal, because otherwise it'd disrupt the
		-- update process
		External.logErrorNonFatal("callbackError", errorObj)
		return false
	end
end

table.freeze(class)
return Computed :: Types.ComputedConstructor


--!strict
--!nolint LocalUnused
--!nolint LocalShadow
local task = nil -- Disable usage of Roblox's task scheduler

--[[
	Breaks down an input table into reactive sub-objects for each pair.
]]

local Package = script.Parent.Parent.Parent
local Types = require(Package.Types)
local External = require(Package.External)
-- Graph
local depend = require(Package.Graph.depend)
-- State
local peek = require(Package.State.peek)
local castToState = require(Package.State.castToState)
local ForTypes = require(Package.State.For.ForTypes)
-- Memory
local doCleanup = require(Package.Memory.doCleanup)
local deriveScope = require(Package.Memory.deriveScope)
-- Utility
local nameOf = require(Package.Utility.nameOf)
local nicknames = require(Package.Utility.nicknames)

type Self<S, KI, KO, VI, VO> = ForTypes.Disassembly<S, KI, KO, VI, KO> & {
	scope: (S & Types.Scope<unknown>)?,
	_inputTable: Types.UsedAs<{[KI]: VI}>,
	_constructor: (
		Types.Scope<S>,
		initialKey: KI,
		initialValue: VI
	) -> ForTypes.SubObject<S, KI, KO, VI, VO>,
	_subObjects: {[ForTypes.SubObject<S, KI, KO, VI, VO>]: true}
}

local class = {}
class.type = "Graph"
class.kind = "For.Disassembly"
class.timeliness = "lazy"

local METATABLE = table.freeze {__index = class}

local function Disassembly<S, KI, KO, VI, VO>(
	scope: S & Types.Scope<unknown>,
	inputTable: Types.UsedAs<{[KI]: VI}>,
	constructor: (
		Types.Scope<S>,
		initialKey: KI,
		initialValue: VI
	) -> ForTypes.SubObject<S, KI, KO, VI, VO>
): ForTypes.Disassembly<S, KI, KO, VI, KO>
	local createdAt = os.clock()
	local self = setmetatable(
		{
			createdAt = createdAt,
			dependencySet = {},
			dependentSet = {},
			scope = scope,
			validity = "invalid",
			_inputTable = inputTable,
			_constructor = constructor,
			_subObjects = {}
		},
		METATABLE
	) :: any

	local destroy = function()
		self.scope = nil
		for dependency in pairs(self.dependencySet) do
			dependency.dependentSet[self] = nil
		end
		for subObject in self._subObjects do
			if subObject.maybeScope ~= nil then
				doCleanup(subObject.maybeScope)
				subObject.maybeScope = nil
			end
		end
	end
	self.oldestTask = destroy
	nicknames[self.oldestTask] = "For (internal disassembler)"
	table.insert(scope, destroy)

	return self
end

function class.populate<S, KI, KO, VI, VO>(
	self: Self<S, KI, KO, VI, VO>,
	use: Types.Use,
	output: {[KO]: VO}
): ()
	local minArrayIndex = math.huge
	local maxArrayIndex = -math.huge
	local hasHoles = false
	for subObject in self._subObjects do
		local outputKey, outputValue = subObject:useOutputPair(use)
		if outputKey == nil or outputValue == nil then
			hasHoles = true
			continue
		elseif output[outputKey] ~= nil then
			External.logErrorNonFatal("forKeyCollision", nil, tostring(outputKey))
			continue
		end
		output[outputKey] = outputValue
		if typeof(outputKey) == "number" then
			minArrayIndex = math.min(minArrayIndex, outputKey)
			maxArrayIndex = math.max(maxArrayIndex, outputKey)
		end
	end
	-- Be careful of NaN here
	if hasHoles and maxArrayIndex > minArrayIndex then
		local output: {[number]: VO} = output :: any
		local moveToIndex = minArrayIndex
		for moveFromIndex = minArrayIndex, maxArrayIndex do
			local outputValue = output[moveFromIndex]
			if outputValue == nil then
				continue
			end
			-- The ordering is important in case the indices are the same
			output[moveFromIndex] = nil
			output[moveToIndex] = outputValue
			moveToIndex += 1
		end
	end
end

function class._evaluate<S, KI, KO, VI, VO>(
	self: Self<S, KI, KO, VI, VO>
): boolean
	local outerScope = self.scope :: S & Types.Scope<unknown>
	local inputState = castToState(self._inputTable)
	if inputState ~= nil then
		if inputState.scope == nil then
			External.logError(
				"useAfterDestroy",
				nil,
				`The input {nameOf(inputState, "table")}`,
				`the For object that is watching it`
			)
		end
		depend(self, inputState)
	end

	local pendingPairs = {} :: {[KI]: VI}
	for key, value in peek(self._inputTable) do
		pendingPairs[key] = value
	end

	local newSubObjects = {} :: typeof(self._subObjects)

	for subObject in self._subObjects do
		local reused = false
		local oldInputKey = subObject.inputKey
		local oldInputValue = subObject.inputValue
		local newInputKey: KI
		-- Reuse when the keys are identical.
		if not subObject.roamKeys and pendingPairs[oldInputKey] ~= nil then
			reused = true
			newInputKey = oldInputKey
		else -- Try and reuse some other pair instead.
			for pendingKey, pendingValue in pendingPairs do
				reused = true
				newInputKey = pendingKey
				if subObject.roamValues then
					break
				end
				if pendingValue == oldInputValue then
					-- If the values are the same, then no need to update those,
					-- so prefer this choice to any other.
					break
				end
			end
		end
		if reused then
			local newInputValue = pendingPairs[newInputKey]
			newSubObjects[subObject] = true
			if newInputKey ~= oldInputKey then
				subObject.inputKey = newInputKey
				subObject:invalidateInputKey()
			end
			if newInputValue ~= oldInputValue then
				subObject.inputValue = newInputValue
				subObject:invalidateInputValue()
			end
			pendingPairs[newInputKey] = nil
		else -- Too many sub objects for the number of pairs.
			if subObject.maybeScope ~= nil then
				doCleanup(subObject.maybeScope)
				subObject.maybeScope = nil
			end
		end
	end

	-- Generate new objects if needed to cover the remaining pending pairs.
	for pendingKey, pendingValue in pendingPairs do
		local subObject = self._constructor(deriveScope(outerScope), pendingKey, pendingValue)
		newSubObjects[subObject] = true
	end

	self._subObjects = newSubObjects

	return true
end

table.freeze(class)
return Disassembly


--!strict
--!nolint LocalUnused
--!nolint LocalShadow
local task = nil -- Disable usage of Roblox's task scheduler

--[[
	Stores types that are commonly used between For objects.
]]

local Package = script.Parent.Parent.Parent
local Types = require(Package.Types)

export type SubObject<S, KI, KO, VI, VO> = {
	-- Not all sub objects need to store a scope, for example if the scope
	-- remains empty, it'll be given back to the scope pool.
	maybeScope: Types.Scope<S>?,
	inputKey: KI,
	inputValue: VI,
	roamKeys: boolean,
	roamValues: boolean,
	invalidateInputKey: (SubObject<S, KI, KO, VI, VO>) -> (),
	invalidateInputValue: (SubObject<S, KI, KO, VI, VO>) -> (),
	useOutputPair: (SubObject<S, KI, KO, VI, VO>, Types.Use) -> (KO?, VO?)
}

export type Disassembly<S, KI, KO, VI, VO> = Types.GraphObject & {
	populate: (Disassembly<S, KI, KO, VI, VO>, Types.Use, output: {[KO]: VO}) -> ()
}

return nil


--!strict
--!nolint LocalUnused
--!nolint LocalShadow
local task = nil -- Disable usage of Roblox's task scheduler

--[[
	The generic implementation for all `For` objects.
]]

local Package = script.Parent.Parent
local Types = require(Package.Types)
-- Graph
local depend = require(Package.Graph.depend)
-- State
local peek = require(Package.State.peek)
local castToState = require(Package.State.castToState)
local ForTypes = require(Package.State.For.ForTypes)
-- Utility
local nicknames = require(Package.Utility.nicknames)

local Disassembly = require(Package.State.For.Disassembly)

type Self<S, KI, KO, VI, VO> = Types.For<KO, VO> & {
	_disassembly: ForTypes.Disassembly<S, KI, KO, VI, VO>
}

local class = {}
class.type = "State"
class.kind = "For"
class.timeliness = "lazy"

local METATABLE = table.freeze {__index = class}

local function For<S, KI, KO, VI, VO>(
	scope: Types.Scope<S>,
	inputTable: Types.UsedAs<{[KI]: VI}>,
	constructor: (
		Types.Scope<S>,
		initialKey: KI,
		initialValue: VI
	) -> ForTypes.SubObject<S, KI, KO, VI, VO>
): Types.For<KO, VO>
	local createdAt = os.clock()
	local self: Self<S, KI, KO, VI, VO> = setmetatable(
		{
			createdAt = createdAt,
			dependencySet = {},
			dependentSet = {},
			scope = scope,
			validity = "invalid",
			_EXTREMELY_DANGEROUS_usedAsValue = {},
			_disassembly = Disassembly(
				scope,
				inputTable,
				constructor
			)
		},
		METATABLE
	) :: any

	local destroy = function()
		self.scope = nil
		for dependency in pairs(self.dependencySet) do
			dependency.dependentSet[self] = nil
		end
	end
	self.oldestTask = destroy
	nicknames[self.oldestTask] = "For"
	table.insert(scope, destroy)

	return self
end

function class._evaluate<S, KI, KO, VI, VO>(
	self: Self<S, KI, KO, VI, VO>
): boolean
	if self.scope == nil then
		return false
	end
	local outerScope = self.scope :: S & Types.Scope<unknown>

	depend(self, self._disassembly)
	table.clear(self._EXTREMELY_DANGEROUS_usedAsValue)
	self._disassembly:populate(
		function<T>(
			maybeState: Types.UsedAs<T>
		): T
			local state = castToState(maybeState)
			if state ~= nil then
				depend(self, state)
			end
			return peek(maybeState)
		end,
		self._EXTREMELY_DANGEROUS_usedAsValue
	)

	return true
end

table.freeze(class)
return For


--!strict
--!nolint LocalUnused
--!nolint LocalShadow
local task = nil -- Disable usage of Roblox's task scheduler

--[[
	Constructs and returns a new For state object which processes keys and
	preserves values.

	https://elttob.uk/Fusion/0.3/api-reference/state/members/forkeys/

	TODO: the sub objects constructed here can be more efficiently implemented
	as a dedicated state object.
]]

local Package = script.Parent.Parent
local Types = require(Package.Types)
local External = require(Package.External)
-- Memory
local doCleanup = require(Package.Memory.doCleanup)
-- State
local For = require(Package.State.For)
local Value = require(Package.State.Value)
local Computed = require(Package.State.Computed)
local ForTypes = require(Package.State.For.ForTypes)
-- Logging
local parseError = require(Package.Logging.parseError)

local SUB_OBJECT_META = {
	__index = {
		roamKeys = false,
		roamValues = true,
		invalidateInputKey = function(self): ()
			self._inputKeyState:set(self.inputKey)
		end,
		invalidateInputValue = function(self): ()
			-- do nothing
		end,
		useOutputPair = function(self, use)
			return use(self._outputKeyState), self.inputValue
		end
	}
}

local function SubObject<KI, KO, V, S>(
	scope: Types.Scope<S>,
	initialKey: KI,
	initialValue: V,
	processor: (Types.Use, Types.Scope<S>, KI) -> KO
): ForTypes.SubObject<S, KI, KO, V, V>
	local self = {}
	self.maybeScope = scope
	self.inputKey = initialKey
	self.inputValue = initialValue
	self._inputKeyState = Value(scope, initialKey)
	self._processor = processor
	self._outputKeyState = Computed(scope, function(use, scope): KO?
		local inputKey = use(self._inputKeyState)
		local ok, outputKey = xpcall(self._processor, parseError, use, scope, inputKey)
		if ok then
			return outputKey
		else
			local error: Types.Error = outputKey :: any
			error.context = `while processing key {tostring(inputKey)}`
			External.logErrorNonFatal("callbackError", error)
			doCleanup(scope)
			table.clear(scope)
			return nil
		end
	end)
	return setmetatable(self, SUB_OBJECT_META) :: any
end

local function ForKeys<KI, KO, V, S>(
	scope: Types.Scope<S>,
	inputTable: Types.UsedAs<{[KI]: V}>,
	processor: (Types.Use, Types.Scope<S>, KI) -> KO
): Types.For<KO, V>
	return For(
		scope,
		inputTable,
		function(scope, initialKey, initialValue)
			return SubObject(scope, initialKey, initialValue, processor)
		end
	)
end

return ForKeys :: Types.ForKeysConstructor


--!strict
--!nolint LocalUnused
--!nolint LocalShadow
local task = nil -- Disable usage of Roblox's task scheduler

--[[
	Constructs and returns a new For state object which processes keys and
	values in pairs.

	https://elttob.uk/Fusion/0.3/api-reference/state/members/forpairs/

	TODO: the sub objects constructed here can be more efficiently implemented
	as a dedicated state object.
]]

local Package = script.Parent.Parent
local Types = require(Package.Types)
local External = require(Package.External)
-- State
local For = require(Package.State.For)
local Value = require(Package.State.Value)
local Computed = require(Package.State.Computed)
local ForTypes = require(Package.State.For.ForTypes)
-- Logging
local parseError = require(Package.Logging.parseError)
-- Memory
local doCleanup = require(Package.Memory.doCleanup)

local SUB_OBJECT_META = {
	__index = {
		roamKeys = false,
		roamValues = false,
		invalidateInputKey = function(self): ()
			self._inputKeyState:set(self.inputKey)
		end,
		invalidateInputValue = function(self): ()
			self._inputValueState:set(self.inputValue)
		end,
		useOutputPair = function(self, use)
			local pair = use(self._outputPairState)
			return pair.key, pair.value
		end
	}
}

local function SubObject<KI, KO, VI, VO, S>(
	scope: Types.Scope<S>,
	initialKey: KI,
	initialValue: VI,
	processor: (Types.Use, Types.Scope<S>, KI, VI) -> (KO, VO)
): ForTypes.SubObject<S, KI, KO, VI, VO>
	local self = {}
	self.maybeScope = scope
	self.inputKey = initialKey
	self.inputValue = initialValue
	self._inputKeyState = Value(scope, initialKey)
	self._inputValueState = Value(scope, initialValue)
	self._processor = processor
	self._outputPairState = Computed(scope, function(use, scope): {key: KO?, value: VO?}
		local inputKey = use(self._inputKeyState)
		local inputValue = use(self._inputValueState)
		local ok, outputKey, outputValue = xpcall(self._processor, parseError, use, scope, inputKey, inputValue)
		if ok then
			return {key = outputKey, value = outputValue}
		else
			local error: Types.Error = outputKey :: any
			error.context = `while processing key {tostring(inputKey)} and value {tostring(inputValue)}`
			External.logErrorNonFatal("callbackError", error)
			doCleanup(scope)
			table.clear(scope)
			return {key = nil, value = nil}
		end
	end)
	return setmetatable(self, SUB_OBJECT_META) :: any
end

local function ForPairs<KI, KO, VI, VO, S>(
	scope: Types.Scope<S>,
	inputTable: Types.UsedAs<{[KI]: VI}>,
	processor: (Types.Use, Types.Scope<S>, KI, VI) -> (KO, VO)
): Types.For<KO, VO>
	return For(
		scope,
		inputTable,
		function(scope, initialKey, initialValue)
			return SubObject(scope, initialKey, initialValue, processor)
		end
	)
end

return ForPairs :: Types.ForPairsConstructor


--!strict
--!nolint LocalUnused
--!nolint LocalShadow
local task = nil -- Disable usage of Roblox's task scheduler

--[[
	Constructs and returns a new For state object which processes values and
	preserves keys.

	https://elttob.uk/Fusion/0.3/api-reference/state/members/forvalues/

	TODO: the sub objects constructed here can be more efficiently implemented
	as a dedicated state object.
]]

local Package = script.Parent.Parent
local Types = require(Package.Types)
local External = require(Package.External)
-- State
local For = require(Package.State.For)
local Value = require(Package.State.Value)
local Computed = require(Package.State.Computed)
local ForTypes = require(Package.State.For.ForTypes)
-- Logging
local parseError = require(Package.Logging.parseError)
-- Memory
local doCleanup = require(Package.Memory.doCleanup)

local SUB_OBJECT_META = {
	__index = {
		roamKeys = true,
		roamValues = false,
		invalidateInputKey = function(self): ()
			-- do nothing
		end,
		invalidateInputValue = function(self): ()
			self._inputValueState:set(self.inputValue)
		end,
		useOutputPair = function(self, use)
			return self.inputKey, use(self._outputValueState)
		end
	}
}

local function SubObject<K, VI, VO, S>(
	scope: Types.Scope<S>,
	initialKey: K,
	initialValue: VI,
	processor: (Types.Use, Types.Scope<S>, VI) -> VO
): ForTypes.SubObject<S, K, K, VI, VO>
	local self = {}
	self.maybeScope = scope
	self.inputKey = initialKey
	self.inputValue = initialValue
	self._inputValueState = Value(scope, initialValue)
	self._processor = processor
	self._outputValueState = Computed(scope, function(use, scope): VO?
		local inputValue = use(self._inputValueState)
		local ok, outputValue = xpcall(self._processor, parseError, use, scope, inputValue)
		if ok then
			return outputValue
		else
			local error: Types.Error = outputValue :: any
			error.context = `while processing value {tostring(inputValue)}`
			External.logErrorNonFatal("callbackError", error)
			doCleanup(scope)
			table.clear(scope)
			return nil
		end
	end)
	return setmetatable(self, SUB_OBJECT_META) :: any
end

local function ForValues<K, VI, VO, S>(
	scope: Types.Scope<S>,
	inputTable: Types.UsedAs<{[K]: VI}>,
	processor: (Types.Use, Types.Scope<S>, VI) -> VO
): Types.For<K, VO>
	return For(
		scope,
		inputTable,
		function(scope, initialKey, initialValue)
			return SubObject(scope, initialKey, initialValue, processor)
		end
	)
end

return ForValues :: Types.ForValuesConstructor


--!strict
--!nolint LocalUnused
--!nolint LocalShadow
local task = nil -- Disable usage of Roblox's task scheduler

--[[
	A state object which allows regular Luau code to control its value.

	https://elttob.uk/Fusion/0.3/api-reference/state/types/value/
]]

local Package = script.Parent.Parent
local Types = require(Package.Types)
-- Graph
local change = require(Package.Graph.change)
-- Utility
local isSimilar = require(Package.Utility.isSimilar)
local nicknames = require(Package.Utility.nicknames)

type Self<T, S> = Types.Value<T, S>

local class = {}
class.type = "State"
class.kind = "Value"
class.timeliness = "lazy"
class.dependencySet = table.freeze {}

local METATABLE = table.freeze {__index = class}

local function Value<T>(
	scope: Types.Scope<unknown>,
	initialValue: T
): Types.Value<T, any>
	local createdAt = os.clock()
	local self: Self<T, any> = setmetatable(
		{
			createdAt = createdAt,
			dependentSet = {},
			lastChange = os.clock(),
			scope = scope,
			validity = "valid",
			_EXTREMELY_DANGEROUS_usedAsValue = initialValue
		},
		METATABLE
	) :: any
	local destroy = function()
		self.scope = nil
	end
	self.oldestTask = destroy
	nicknames[self.oldestTask] = "Value"
	table.insert(scope, destroy)
	return self
end

function class.set<T, S>(
	self: Self<T, S>,
	newValue: S
): S
	local oldValue = self._EXTREMELY_DANGEROUS_usedAsValue
	if not isSimilar(oldValue, newValue) then
		self._EXTREMELY_DANGEROUS_usedAsValue = newValue :: any
		change(self)
	end
	return newValue
end

function class._evaluate<T, S>(
	_self: Self<T, S>
): boolean
	-- The similarity test is done in advance when the value is set, so this
	-- should be fine.
	return true
end

table.freeze(class)
return Value :: Types.ValueConstructor


--!strict
--!nolint LocalUnused
--!nolint LocalShadow
local task = nil -- Disable usage of Roblox's task scheduler

--[[
	Stores common public-facing type information for Fusion APIs.
]]

export type Error = {
	type: "Error",
	raw: string,
	message: string,
	trace: string,
	context: string?
}

-- Types that can be expressed as vectors of numbers, and so can be animated.
export type Animatable =
	number |
	CFrame |
	Color3 |
	ColorSequenceKeypoint |
	DateTime |
	NumberRange |
	NumberSequenceKeypoint |
	PhysicalProperties |
	Ray |
	Rect |
	Region3 |
	Region3int16 |
	UDim |
	UDim2 |
	Vector2 |
	Vector2int16 |
	Vector3 |
	Vector3int16

-- A task which can be accepted for cleanup.
export type Task =
	Instance |
	RBXScriptConnection |
	() -> () |
	{destroy: (unknown) -> ()} |
	{Destroy: (unknown) -> ()} |
	{disconnect: (unknown) -> ()} |
	{Disconnect: (unknown) -> ()} |
	{Task}

-- A scope of tasks to clean up.
export type Scope<Constructors = any> = {Task} & Constructors

-- An object which uses a scope to dictate how long it lives.
export type ScopedObject = {
	scope: Scope<unknown>?,
	oldestTask: unknown
}

-- Script-readable version information.
export type Version = {
	major: number,
	minor: number,
	isRelease: boolean
}

-- An object which stores a value scoped in time.
export type Contextual<T> = {
	type: "Contextual",
	now: (Contextual<T>) -> T,
	is: (Contextual<T>, T) -> ContextualIsMethods
}

type ContextualIsMethods = {
	during: <R, A...>(ContextualIsMethods, (A...) -> R, A...) -> R
}

-- A graph object which can have dependencies and dependencies.
export type GraphObject = ScopedObject & {
	createdAt: number,
	dependencySet: {[GraphObject]: unknown},
	dependentSet: {[GraphObject]: unknown},
	lastChange: number?,
	timeliness: "lazy" | "eager",
	validity: "valid" | "invalid" | "busy",
	_evaluate: (GraphObject) -> boolean
}

-- An object which stores a piece of reactive state.
export type StateObject<T> = GraphObject & {
	type: "State",
	kind: string,
	_EXTREMELY_DANGEROUS_usedAsValue: T
}

-- Passing values of this type to `Use` returns `T`.
export type UsedAs<T> = StateObject<T> | T

-- Function signature for use callbacks.
export type Use = <T>(target: UsedAs<T>) -> T

-- A state object whose value can be set at any time by the user.
export type Value<T, S = T> = StateObject<T> & {
	kind: "State",
	timeliness: "lazy",
 	set: (Value<T, S>, newValue: S, force: boolean?) -> S,
	____phantom_setType: (never) -> S -- phantom data so this contains S
}
export type ValueConstructor = <T>(
	scope: Scope<unknown>,
	initialValue: T
) -> Value<T, any>

-- A state object whose value is derived from other objects using a callback.
export type Computed<T> = StateObject<T> & {
	kind: "Computed",
	timeliness: "lazy"
}
export type ComputedConstructor = <T, S>(
	scope: S & Scope<unknown>,
	callback: (Use, S) -> T
) -> Computed<T>

-- A state object which maps over keys and/or values in another table.
export type For<KO, VO> = StateObject<{[KO]: VO}>  & {
	kind: "For"
}
export type ForPairsConstructor =  <KI, KO, VI, VO, S>(
	scope: S & Scope<unknown>,
	inputTable: UsedAs<{[KI]: VI}>,
	processor: (Use, S, key: KI, value: VI) -> (KO, VO)
) -> For<KO, VO>
export type ForKeysConstructor =  <KI, KO, V, S>(
	scope: S & Scope<unknown>,
	inputTable: UsedAs<{[KI]: V}>,
	processor: (Use, S, key: KI) -> KO
) -> For<KO, V>
export type ForValuesConstructor =  <K, VI, VO, S>(
	scope: S & Scope<unknown>,
	inputTable: UsedAs<{[K]: VI}>,
	processor: (Use, S, value: VI) -> VO
) -> For<K, VO>

-- An object which can listen for updates on another state object.
export type Observer = GraphObject & {
	type: "Observer",
	timeliness: "eager",
	onChange: (Observer, callback: () -> ()) -> (() -> ()),
	onBind: (Observer, callback: () -> ()) -> (() -> ())
}
export type ObserverConstructor = (
	scope: Scope<unknown>,
	watching: unknown
) -> Observer

-- A state object which follows another state object using tweens.
export type Tween<T> = StateObject<T> & {
	kind: "Tween"
}
export type TweenConstructor = <T>(
	scope: Scope<unknown>,
	goalState: UsedAs<T>,
	tweenInfo: UsedAs<TweenInfo>?
) -> Tween<T>

-- A state object which follows another state object using spring simulation.
export type Spring<T> = StateObject<T>  & {
	kind: "Spring",
	setPosition: (Spring<T>, newPosition: T) -> (),
	setVelocity: (Spring<T>, newVelocity: T) -> (),
	addVelocity: (Spring<T>, deltaVelocity: T) -> ()
}
export type SpringConstructor = <T>(
	scope: Scope<unknown>,
	goalState: UsedAs<T>,
	speed: UsedAs<number>?,
	damping: UsedAs<number>?
) -> Spring<T>

-- Denotes children instances in an instance or component's property table.
export type SpecialKey = {
	type: "SpecialKey",
	kind: string,
	stage: "self" | "descendants" | "ancestor" | "observer",
	apply: (
		self: SpecialKey,
		scope: Scope<unknown>,
		value: unknown,
		applyTo: Instance
	) -> ()
}

-- A collection of instances that may be parented to another instance.
export type Child = Instance | StateObject<Child> | {[unknown]: Child}

-- A table that defines an instance's properties, handlers and children.
export type PropertyTable = {[string | SpecialKey]: unknown}

export type NewConstructor = (
	scope: Scope<unknown>,
	className: string
) -> (propertyTable: PropertyTable) -> Instance

export type HydrateConstructor = (
	scope: Scope<unknown>,
	target: Instance
) -> (propertyTable: PropertyTable) -> Instance

-- Is there a sane way to write out this type?
-- ... I sure hope so.

export type DeriveScopeConstructor = (<S>(Scope<S>) -> Scope<S>)
	& (<S, A>(Scope<S>, A & {}) -> Scope<S & A>)
	& (<S, A, B>(Scope<S>, A & {}, B & {}) -> Scope<S & A & B>)
	& (<S, A, B, C>(Scope<S>, A & {}, B & {}, C & {}) -> Scope<S & A & B & C>)
	& (<S, A, B, C, D>(Scope<S>, A & {}, B & {}, C & {}, D & {}) -> Scope<S & A & B & C & D>)
	& (<S, A, B, C, D, E>(Scope<S>, A & {}, B & {}, C & {}, D & {}, E & {}) -> Scope<S & A & B & C & D & E>)
	& (<S, A, B, C, D, E, F>(Scope<S>, A & {}, B & {}, C & {}, D & {}, E & {}, F & {}) -> Scope<S & A & B & C & D & E & F>)
	& (<S, A, B, C, D, E, F, G>(Scope<S>, A & {}, B & {}, C & {}, D & {}, E & {}, F & {}, G & {}) -> Scope<S & A & B & C & D & E & F & G>)
	& (<S, A, B, C, D, E, F, G, H>(Scope<S>, A & {}, B & {}, C & {}, D & {}, E & {}, F & {}, G & {}, H & {}) -> Scope<S & A & B & C & D & E & F & G & H>)
	& (<S, A, B, C, D, E, F, G, H, I>(Scope<S>, A & {}, B & {}, C & {}, D & {}, E & {}, F & {}, G & {}, H & {}, I & {}) -> Scope<S & A & B & C & D & E & F & G & H & I>)
	& (<S, A, B, C, D, E, F, G, H, I, J>(Scope<S>, A & {}, B & {}, C & {}, D & {}, E & {}, F & {}, G & {}, H & {}, I & {}, J & {}) -> Scope<S & A & B & C & D & E & F & G & H & I & J>)
	& (<S, A, B, C, D, E, F, G, H, I, J, K>(Scope<S>, A & {}, B & {}, C & {}, D & {}, E & {}, F & {}, G & {}, H & {}, I & {}, J & {}, K & {}) -> Scope<S & A & B & C & D & E & F & G & H & I & J & K>)
	& (<S, A, B, C, D, E, F, G, H, I, J, K, L>(Scope<S>, A & {}, B & {}, C & {}, D & {}, E & {}, F & {}, G & {}, H & {}, I & {}, J & {}, K & {}, L & {}) -> Scope<S & A & B & C & D & E & F & G & H & I & J & K & L>)

export type ScopedConstructor = (() -> Scope<{}>)
	& (<A>(A & {}) -> Scope<A>)
	& (<A, B>(A & {}, B & {}) -> Scope<A & B>)
	& (<A, B, C>(A & {}, B & {}, C & {}) -> Scope<A & B & C>)
	& (<A, B, C, D>(A & {}, B & {}, C & {}, D & {}) -> Scope<A & B & C & D>)
	& (<A, B, C, D, E>(A & {}, B & {}, C & {}, D & {}, E & {}) -> Scope<A & B & C & D & E>)
	& (<A, B, C, D, E, F>(A & {}, B & {}, C & {}, D & {}, E & {}, F & {}) -> Scope<A & B & C & D & E & F>)
	& (<A, B, C, D, E, F, G>(A & {}, B & {}, C & {}, D & {}, E & {}, F & {}, G & {}) -> Scope<A & B & C & D & E & F & G>)
	& (<A, B, C, D, E, F, G, H>(A & {}, B & {}, C & {}, D & {}, E & {}, F & {}, G & {}, H & {}) -> Scope<A & B & C & D & E & F & G & H>)
	& (<A, B, C, D, E, F, G, H, I>(A & {}, B & {}, C & {}, D & {}, E & {}, F & {}, G & {}, H & {}, I & {}) -> Scope<A & B & C & D & E & F & G & H & I>)
	& (<A, B, C, D, E, F, G, H, I, J>(A & {}, B & {}, C & {}, D & {}, E & {}, F & {}, G & {}, H & {}, I & {}, J & {}) -> Scope<A & B & C & D & E & F & G & H & I & J>)
	& (<A, B, C, D, E, F, G, H, I, J, K>(A & {}, B & {}, C & {}, D & {}, E & {}, F & {}, G & {}, H & {}, I & {}, J & {}, K & {}) -> Scope<A & B & C & D & E & F & G & H & I & J & K>)
	& (<A, B, C, D, E, F, G, H, I, J, K, L>(A & {}, B & {}, C & {}, D & {}, E & {}, F & {}, G & {}, H & {}, I & {}, J & {}, K & {}, L & {}) -> Scope<A & B & C & D & E & F & G & H & I & J & K & L>)

export type ContextualConstructor = <T>(defaultValue: T) -> Contextual<T>

export type Safe = <Success, Fail>(
	callbacks: {
		try: () -> Success,
		fallback: (err: unknown) -> Fail
	}
) -> Success | Fail

export type Fusion = {
	version: Version,
	Contextual: ContextualConstructor,
	Safe: Safe,

	doCleanup: (Task) -> (),
	scoped: ScopedConstructor,
	deriveScope: DeriveScopeConstructor,
	innerScope: DeriveScopeConstructor,
	insert: <Tasks...>(scope: Scope<unknown>, Tasks...) -> Tasks...,

	peek: Use,
	Value: ValueConstructor,
	Computed: ComputedConstructor,
	ForPairs: ForPairsConstructor,
	ForKeys: ForKeysConstructor,
	ForValues: ForValuesConstructor,
	Observer: ObserverConstructor,

	Tween: TweenConstructor,
	Spring: SpringConstructor,

	New: NewConstructor,
	Hydrate: HydrateConstructor,

	Child: ({Child}) -> Child,
	Children: SpecialKey,
	Out: (propertyName: string) -> SpecialKey,
	OnEvent: (eventName: string) -> SpecialKey,
	OnChange: (propertyName: string) -> SpecialKey,
	Attribute: (attributeName: string) -> SpecialKey,
	AttributeChange: (attributeName: string) -> SpecialKey,
	AttributeOut: (attributeName: string) -> SpecialKey,
	Tag: (tagName: string) -> SpecialKey,
}

export type ExternalProvider = {
	policies: {
		allowWebLinks: boolean
	},

	logErrorNonFatal: (
		errorString: string
	) -> (),
	logWarn: (
		errorString: string
	) -> (),

	doTaskImmediate: (
		resume: () -> ()
	) -> (),
	doTaskDeferred: (
		resume: () -> ()
	) -> (),
	startScheduler: () -> (),
	stopScheduler: () -> ()
}

export type ExternalDebugger = {
	startDebugging: () -> (),
	stopDebugging: () -> (),

	trackScope: (
		scope: Scope<unknown>
	) -> (),
	untrackScope: (
		scope: Scope<unknown>
	) -> ()
}

return nil


--!strict
--!nolint LocalUnused
--!nolint LocalShadow
local task = nil -- Disable usage of Roblox's task scheduler

--[[
	Implements the 'similarity test' used to determine whether two values have
	a meaningful difference.

	https://elttob.uk/Fusion/0.3/tutorials/best-practices/optimisation/#similarity
]]

local function isSimilar(
	a: unknown,
	b: unknown
): boolean
	local typeA = typeof(a)
	local isTable = typeA == "table"
	local isUserdata = typeA == "userdata"
	return
		if not (isTable or isUserdata) then
			a == b or a ~= a and b ~= b
		elseif typeA == typeof(b) and (isUserdata or table.isfrozen(a :: any) or getmetatable(a :: any) ~= nil) then
			a == b
		else
			false
end

return isSimilar


--!strict
--!nolint LocalUnused
--!nolint LocalShadow
local task = nil -- Disable usage of Roblox's task scheduler

--[[
	Attempts to merge a variadic number of tables together.
]]

local Package = script.Parent.Parent
local External = require(Package.External)

local function merge(
	overwrite: boolean,
	into: {[unknown]: unknown},
	...: {[unknown]: unknown}
): {[unknown]: unknown}
	local fromTables = {...}
	if #fromTables < 1 then
		return into
	else
		for _, fromTable in fromTables do
			for key, value in fromTable do
				if overwrite or into[key] == nil then
					into[key] = value
				elseif not overwrite then
					External.logError("mergeConflict", nil, tostring(key))
				end
			end
		end
		return into
	end
end

return merge


--!strict
--!nolint LocalUnused
--!nolint LocalShadow
local task = nil -- Disable usage of Roblox's task scheduler
--[[
	The entry point for the Fusion library.
]]

local Types = require(script.Types)
local External = require(script.External)

type Fusion = Types.Fusion

export type Animatable = Types.Animatable
export type UsedAs<T> = Types.UsedAs<T>
export type Child = Types.Child
export type Computed<T> = Types.Computed<T>
export type Contextual<T> = Types.Contextual<T>
export type GraphObject = Types.GraphObject
export type For<KO, VO> = Types.For<KO, VO>
export type Observer = Types.Observer
export type PropertyTable = Types.PropertyTable
export type Scope<Constructors = Fusion> = Types.Scope<Constructors>
export type ScopedObject = Types.ScopedObject
export type SpecialKey = Types.SpecialKey
export type Spring<T> = Types.Spring<T>
export type StateObject<T> = Types.StateObject<T>
export type Task = Types.Task
export type Tween<T> = Types.Tween<T>
export type Use = Types.Use
export type Value<T, S = T> = Types.Value<T, S>
export type Version = Types.Version

-- Down the line, this will be conditional based on whether Fusion is being
-- compiled for Roblox.
do
	local RobloxExternal = require(script.RobloxExternal)
	External.setExternalProvider(RobloxExternal)
end

local Fusion: Fusion = table.freeze({
	-- General
	version = { major = 0, minor = 4, isRelease = false },
	Contextual = require(script.Utility.Contextual),
	Safe = require(script.Utility.Safe),

	-- Memory
	deriveScope = require(script.Memory.deriveScope),
	doCleanup = require(script.Memory.doCleanup),
	innerScope = require(script.Memory.innerScope),
	insert = require(script.Memory.insert),
	scoped = require(script.Memory.scoped),

	-- Graph
	Observer = require(script.Graph.Observer),

	-- State
	Computed = require(script.State.Computed),
	ForKeys = require(script.State.ForKeys) :: Types.ForKeysConstructor,
	ForPairs = require(script.State.ForPairs) :: Types.ForPairsConstructor,
	ForValues = require(script.State.ForValues) :: Types.ForValuesConstructor,
	peek = require(script.State.peek),
	Value = require(script.State.Value),

	-- Roblox API
	Attribute = require(script.Instances.Attribute),
	AttributeChange = require(script.Instances.AttributeChange),
	AttributeOut = require(script.Instances.AttributeOut),
	Child = require(script.Instances.Child),
	Children = require(script.Instances.Children),
	Hydrate = require(script.Instances.Hydrate),
	New = require(script.Instances.New),
	OnChange = require(script.Instances.OnChange),
	OnEvent = require(script.Instances.OnEvent),
	Out = require(script.Instances.Out),
	Tag = require(script.Instances.Tag),

	-- Animation
	Tween = require(script.Animation.Tween),
	Spring = require(script.Animation.Spring),
})

return Fusion


--[==[
	File: Client-AdonisChatAddition.lua
	Author: Coasterteam
	Description: This is a client dep. that makes it so removing custom chat works properly

	Last Updated: August 11th, 2024
	Version: 1.0
]==]

return function(Vargs)
	local client, service = Vargs.Client, Vargs.Service

	client.UI.Remove = function(name, ignore)
		if name == "Chat" and client.Handlers.RemoveCustomChat then
			client.Handlers.RemoveCustomChat()
		else
			local gui = client.UI.Get(name, ignore)
			if gui then
				for i,v in gui do
					v.Destroy()
				end
			end
		end
	end
end


--[[
	Author: WalkerOfBacon
	Name: Auto Channel Tabs
	Description: This plugin will automatically add an "Admins" channel tab and remove unused team channel tabs.
	Place in a ModuleScript under Adonis_Loader > Config > Plugins and named "Server-ChannelTab"
--]]

return function(Vargs)
	local server, service = Vargs.Server, Vargs.Service

	local Remote = server.Remote
	local Admin = server.Admin

	local TextChatService: TextChatService = service.TextChatService

	if TextChatService.ChatVersion ~= Enum.ChatVersion.TextChatService then
		warn("Game uses Legacy TextChatService! Specific chat features wont work!")
		return
	end

	if not TextChatService.ChannelTabsConfiguration.Enabled then
		warn("Game has channel tabs disabled! Specific chat features wont work!")
		return
	end

	local TextChannels = TextChatService and TextChatService:FindFirstChild("TextChannels")

	local adminChannel: TextChannel = service.New("TextChannel", {
		Name = "Admins";
		Parent = TextChannels
	})

	-- Deletes the team channel tab once nobody is in the channel anymore
	TextChannels.ChildAdded:Connect(function(channel)
		if string.find(channel.Name, "RBXTeam")	~= 1 then return end

		channel.ChildRemoved:Connect(function()
			if #channel:GetChildren() == 0 then
				channel:Destroy()
			end
		end)
	end)

	service.HookEvent("PlayerAdded", function(plr: Player)
		if Admin.CheckAdmin(plr) then
			adminChannel:AddUserAsync(plr.UserId)
		end

		-- Handles whenever the player has their team changed
		local teamName
		plr:GetPropertyChangedSignal("Team"):Connect(function()
			if (not teamName and plr.Team) or
				((teamName and plr.Team)
				and
				teamName ~= plr.Team.Name)
			then
				teamName = plr.Team.Name
			end

			if not plr.Team then
				teamName = nil
			end
		end)
	end)

	local oldFunc = Admin.AddAdmin
	Admin.AddAdmin = function(p, level, temp)
		oldFunc(p, level, temp)
		adminChannel:AddUserAsync(p.UserId)
	end

	local oldFunc2 = Admin.RemoveAdmin
	Admin.RemoveAdmin = function(p, temp, override)
		oldFunc2(p, temp, override)
		local channel = TextChannels:WaitForChild("Admins")
		local plr = channel:FindFirstChild(p.Name)
		if channel and plr then
			plr:Destroy()
		end
	end
end


--[[
	File: Server-Banning API.lua
	Author: Coasterteam
	Description: A temporary addition of the Banning API to allow Adonis to communicate with it.
		This plugin should be used temporarily until Adonis fully implements the rewritten moderation systems (TBD)
	Last Updated: June 26th, 2024
	Version: 1.0
--]]

return function(Vargs)
	local server, service = Vargs.Server, Vargs.Service

	-- Check if Adonis has this already, if so don't load! The main Adonis officially supports this now.

	if server.Admin.BanAsync then
		warn("Adonis Banning-API plugin is no longer needed as Adonis officially supports it in the main module. Please remove the additional plugin.")
		return
	end

	server.Variables.BanAPIConfig = {
		BanningAPIEnabled = true; -- Allow BanningAPI to be used
		UseBackupDatastore = true; -- Save to Datastore anyway

		LoadBansFromDatastore = true;
		-- Make it so datastore doesn't actually load bans so it doesn't interfere with BanningAPI
		-- Be warned; it does not allow you to see who is banned as it's not something you can iterate through easily
		-- A more official system IS coming, this is just temporary. I recommend keeping this enabled
		-- 							unless you're having trouble unbanning people.

		-- READ FULL COMMENT NEXT TO SETTING!
		FastBanMigration = false; -- This strips the custom reason of bans and uses a static ban message; allowing bans to chunk together (faster to migrate if you have a lot of bans)
		FastBanMigrationReason = "Adonis Ban Migration from DataStore";

		BanAltAccounts = true; -- Allow AltAccounts to be banned
		-- True => Do not exclude alt accounts
		-- False => Exclude alt accounts

		BansSupported = {
			Permanent = true; -- Listen in on :gameban and :ungameban & allow migration
			Temporary = true; -- Listen in on :timeban and :untimeban & allow migration
		};
	}

	if not server.Variables.BanAPIConfig.LoadBansFromDatastore then
		server.Core.DS_BLACKLIST.Banned = true
		server.Core.DS_BLACKLIST["Core.Variables.TimeBans"] = true
	end

	server.Admin.BanAsync = function(userIds, reason, moderator, options)

		if not server.Variables.BanAPIConfig.BanningAPIEnabled then
			return false
		end

		-- Default stuff
		if not options then
			options = {
				ExcludeAltAccounts = not server.Variables.BanAPIConfig.BanAltAccounts; -- BanAltAccounts = true == false exclude them
			}
		end

		local success, errMessage
		service.Queue("BanningAPI", function()
			success, errMessage = pcall(function()
				service.Players:BanAsync({
					UserIds = userIds;
					DisplayReason = options.OverrideDisplayReason or reason;
					Duration = options.Duration or -1;
					PrivateReason = `Moderated by {tostring(moderator)} via Adonis for: {reason}`;
					ApplyToUniverse = if options.ApplyToUniverse ~= nil then options.ApplyToUniverse else true;
					ExcludeAltAccounts = if options.ExcludeAltAccounts ~= nil then options.ExcludeAltAccounts else not server.Variables.BanAPIConfig.BanAltAccounts;
				})
				task.wait()
			end)
			if not success and errMessage then
				-- Error filtering
				warn("Failed to issue BanAsync:", errMessage)
				if options.DontRetryBan then return end
				if string.match(string.lower(errMessage), "was deemed inappropriate.") then
					options.OverrideDisplayReason = 'Display reason was moderated, see game admins.'
				else
					task.wait(1)
				end
				-- rerun it now
				-- I hate this roblox please make this not so bad (also on me for making it bad in the first place)
				success, errMessage = pcall(function()
					service.Players:BanAsync({
						UserIds = userIds;
						DisplayReason = options.OverrideDisplayReason or reason;
						Duration = options.Duration or -1;
						PrivateReason = `Moderated by {tostring(moderator)} via Adonis for: {reason}`;
						ApplyToUniverse = if options.ApplyToUniverse ~= nil then options.ApplyToUniverse else true;
						ExcludeAltAccounts = if options.ExcludeAltAccounts ~= nil then options.ExcludeAltAccounts else not server.Variables.BanAPIConfig.BanAltAccounts;
					})
					task.wait()
				end)
			end
		end, nil, true) -- shouldn't take more than 30s to do but timeout just in case
		return success
	end

	server.Admin.UnbanAsync = function(userIds, options)
		-- We're not going to check for BanningAPIEnabled as this is fine to keep open
		if not options then options = {} end
		local success, errMessage
		service.Queue("UnBanningAPI", function()
			success, errMessage = pcall(function()
				service.Players:UnbanAsync({
					UserIds = userIds;
					ApplyToUniverse = if options.ApplyToUniverse ~= nil then options.ApplyToUniverse else true;
				})
				task.wait()
			end)
			if not success and errMessage then
				-- Error filtering
				if not options.DontRetryBan then
					task.wait(1) -- timeout maybe?
					success, errMessage = pcall(function()
						service.Players:UnbanAsync({
							UserIds = userIds;
							ApplyToUniverse = if options.ApplyToUniverse ~= nil then options.ApplyToUniverse else true;
						})
						task.wait()
					end)
				end
			end
		end, nil, true) -- shouldn't take more than 30s to do but timeout just in case
		return success
	end

	local OldAddBan = server.Admin.AddBan
	server.Admin.AddBan = function(p, reason, doSave, moderator, banType, ...)
		if server.Variables.BanAPIConfig.BanningAPIEnabled and doSave and server.Variables.BanAPIConfig.BansSupported.Permanent then
			-- This is a permanent ban
			server.Admin.BanAsync({p.UserId}, reason, moderator, ...)
		end
		if not doSave or server.Variables.BanAPIConfig.UseBackupDatastore then -- Use old system
			return OldAddBan(p, reason, doSave, moderator, banType, ...)
		end
	end

	local OldAddTimeBan = server.Admin.AddTimeBan
	server.Admin.AddTimeBan = function(p : Player | {[string]: any}, duration: number, reason: string, moderator: Player?, ...)
		if server.Variables.BanAPIConfig.BanningAPIEnabled and server.Variables.BanAPIConfig.BansSupported.Temporary then
			-- This is a permanent ban
			local options = ...
			if not options then options = {} end
			options.Duration = tonumber(duration)
			if not options.Duration then error("Duration is not a number!") end
			server.Admin.BanAsync({p.UserId}, reason, moderator, options)
		end
		if server.Variables.BanAPIConfig.UseBackupDatastore then -- Use old system
			return OldAddTimeBan(p, duration, reason, moderator)
		end
	end

	-- Remove ban
	local OldRemoveBan = server.Admin.RemoveBan
	server.Admin.RemoveBan = function(name, doSave, ...)
		if doSave then
			-- This is a permanent ban
			local UserId = server.Functions.GetUserIdFromNameAsync(name)
			if UserId then
				server.Admin.UnbanAsync({UserId}, ...)
			end
		end
		if not doSave or server.Variables.BanAPIConfig.UseBackupDatastore then -- Use old system
			return OldRemoveBan(name, doSave)
		end
		return doSave
	end

	local OldRemoveTimeBan = server.Admin.RemoveTimeBan
	server.Admin.RemoveTimeBan = function(name, ...)
		local UserId = server.Functions.GetUserIdFromNameAsync(name)
		if UserId then
			server.Admin.UnbanAsync({UserId}, ...)
		end
		if server.Variables.BanAPIConfig.UseBackupDatastore then -- Use old system
			return OldRemoveTimeBan(name)
		end
	end

	-- Returns the UserId from a record (tries to account for a string record too)
	local function GetUserIdFromRecord(check)
		if type(check) == "table" then
			if check.UserId then
				return tonumber(check.UserId)
			end
		elseif type(check) == "string" then
			local cName, cId = string.match(check, "(.*):(.*)")
			if not cName and cId then cName = check end

			if cId and tonumber(cId) then
				return cId
			end
		elseif type(check) == "number" then
			return check -- userid ban? old ig
		end
		return false
	end

	-- chunks bans together
	local function chunks(lst, size)
		local i = 1
		local count = 0
		return function()
			if i > #lst then return end
			local chunk = table.move(lst, i, i + size -1, 1, {})
			i = i + size
			count = count + 1
			return count, chunk
		end
	end

	server.Commands.MigrateSavedBans = {
		Prefix = server.Settings.Prefix;
		Commands = {"migratebans"};
		Args = {};
		Description = "Migrates all permanent and temporary bans to Banning API; please ensure you have configured it to your liking in its settings!";
		AdminLevel = "Creators";
		Function = function(plr: Player, args: {string}, data: {any})

			server.Functions.Hint("Fetching banned data from DataStore...", {plr})
			local PermBanData = if server.Variables.BanAPIConfig.BansSupported.Permanent then server.Core.GetData("SAVEDTABLE_Banned") else {}
			local TempBanData = if server.Variables.BanAPIConfig.BansSupported.Temporary then server.Core.GetData("SAVEDTABLE_TimeBans") else {}
			server.Functions.Hint("Fetched all ban data from DataStore", {plr})

			local PermBans = {}
			local TempBans = {}

			local TotalPerm, TotalTemp = 0, 0
			for _,BanTable in {PermBanData, TempBanData} do
				for _,ban in BanTable do
					local success, err = pcall(function()
						local Action = ban.Action
						if Action == "Add" and ban.Value then
							local Record = ban.Value
							local Reason = server.Variables.BanAPIConfig.FastBanMigrationReason
							local UserId = GetUserIdFromRecord(Record)
							if not UserId or UserId == 1 then return end -- No clue what this record is! Also don't ban the Roblox Id (weird lol)
							local IsPermanent = true
							if type(Record) == "table" then
								if Record.EndTime then
									IsPermanent = Record.EndTime - os.time() -- Get the duration remaining
									if IsPermanent < 0 then
										-- It's supposed to be over now
										return
									end
								end
								if Record.Reason then
									Reason = server.Functions.Trim(Record.Reason)
								end
							end
							if server.Variables.BanAPIConfig.FastBanMigration then
								Reason = server.Variables.BanAPIConfig.FastBanMigrationReason
							end
							Reason = string.sub(Reason, 1, 400) -- Limit reason
							if IsPermanent == true then
								-- For mass-banning simplicity
								if not PermBans[Reason] then
									PermBans[Reason] = {UserId}
								else
									table.insert(PermBans[Reason], UserId)
								end
								TotalPerm += 1
							else
								local Data = {math.ceil(IsPermanent); Reason;}
								local EncodeSuccess, Data = pcall(function() return service.HttpService:JSONEncode(Data) end)
								if not EncodeSuccess then
									warn(Data)
								end
								if Data and not TempBans[Data] then
									TempBans[Data] = {UserId}
								elseif Data then
									table.insert(TempBans[Data], UserId)
								end
								TotalTemp += 1
							end
						elseif Action == "Remove" and ban.Value then
							local Record = ban.Value
							local UserId = GetUserIdFromRecord(Record)
							-- wip (adding later so I can at least get it out)
						end
					end)
					if not success then
						warn(err)
					end
				end
			end

			local answer = server.Remote.MakeGuiGet(plr, "YesNoPrompt", {
				Question = `Do you want to start migrating data? We found a total of {TotalPerm} permanent ban(s) and {TotalTemp} temporary ban(s).`
			})
			if answer == "Yes" then
				 -- about 1200 a minute-ish when doing 50, so let's assume 25 a chunk and go 600 is what my average was (unconfirmed)
				local totalChunks = 0
				for _,BanTable in {PermBans, TempBans} do
					for BanData,UserIds in BanTable do
						local count = service.CountTable(UserIds)
						totalChunks += math.ceil(count / 50)
					end
				end
				local estimatedTimeInSeconds = (totalChunks) * 0.8 -- About 1 second per chunk-call but I'll cut it under a little bit
				local confirm2 = server.Remote.MakeGuiGet(plr, "YesNoPrompt", {
					Question = `It will take an estimated* {estimatedTimeInSeconds/60} minutes to complete this migration. Please do not leave the game during this.`
				})
				if confirm2 == "Yes" then
					local Total = 0
					for BanReason,UserIds in PermBans do
						for index,UserIdChunk in chunks(UserIds, 50) do
							if #UserIdChunk == 0 then continue end
							Total += #UserIdChunk
							server.Admin.BanAsync(UserIdChunk, BanReason, `Adonis System Migration`)
							server.Functions.Hint(`Permanently BanAsync'd {Total}/{TotalPerm} users`, {plr})
						end
					end
					Total = 0
					for Data,UserIds in TempBans do
						local DecodedData = service.HttpService:JSONDecode(Data)
						local Duration, BanReason = DecodedData[1], DecodedData[2] -- Keep its order to ensure JSON doesnt mess it up
						for index,UserIdChunk in chunks(UserIds, 50) do
							if #UserIdChunk == 0 then continue end
							Total += #UserIdChunk
							server.Admin.BanAsync(UserIdChunk, BanReason, `Adonis System Migration`, {
								Duration = Duration;
							})
							server.Functions.Hint(`Temporarily BanAsync'd {Total}/{TotalTemp} users`, {plr})
						end
					end
					server.Functions.Notification("Notice", "Ban API Migration is now complete", {plr}, nil)
				end
			end
		end,
	}

end


--!strict
----------
--[[ Info:
		A plugin to automatically assign commands' cooldown depending on its rank.
]]
---------------------------------------------------------------------------------------------|

local CooldownsByLevel = {
	--| Format: [AdminLevel] = {Player = <Seconds>, Server = <Seconds>, CrossServer = <Seconds>}

	[0] = {
		Player = 5,
		Server = 0,
		CrossServer = 10,
	};

	[100] = {
		Player = 5,
		Server = 0,
		CrossServer = 25,
	};

	[200] = {
		Player = 5,
		Server = 0,
		CrossServer = 25,
	};

	[300] = {
		Player = 0,
		Server = 0,
		CrossServer = 5,
	};
}

return function (Vargs)
	local Server = Vargs.Server

	for CMDName: string, CMD in pairs(Server.Commands) do
		local CMDAdminLevel = CMD.AdminLevel

		if CooldownsByRank[CMDAdminLevel] then
			if CooldownsByRank[CMDAdminLevel].Player then
				CMD.PlayerCooldown = CooldownsByRank[CMDAdminLevel].Player
			end

			if CooldownsByRank[CMDAdminLevel].Server then
				CMD.ServerCooldown = CooldownsByRank[CMDAdminLevel].Server
			end

			if CooldownsByRank[CMDAdminLevel].CrossServer then
				CMD.CrossCooldown = CooldownsByRank[CMDAdminLevel].CrossServer
			end
		end
	end
end


--!strict
--[[
	Author: Sceleratis (Davey_Bones)
	Description: This plugin will remove certain commands

	Place in a ModuleScript under Adonis_Loader > Config > Plugins and named "Server-Command_Remover"
--]]

local COMMANDS_TO_REMOVE: {string} = { --// List of command indices or names to remove
	"kill",
	"DonorHat",
	"fire"
}

return function(Vargs)
	local server = Vargs.Server
	local service = Vargs.Service

	local Admin = server.Admin
	local Commands = server.Commands

	local count = 0

	for ind, cmd in Commands do
		for _, v in COMMANDS_TO_REMOVE do
			if string.lower(v) == string.lower(ind) or table.find(cmd.Commands, string.lower(v)) then
				Commands[ind] = nil
				count += 1
				break
			end
		end
	end

	warn(string.format("Removed %d command%s from the Commands table", count, count == 1 and "" or "s"))

	Admin.CacheCommands()
end


return function(Vargs)
    local server, service = Vargs.Server, Vargs.Service

    local Settings = server.Settings
    local Commands = server.Commands
    local Functions = server.Functions
    local Admin = server.Admin

    Commands.VIP = {
            Prefix = Settings.Prefix;
            Commands = {"vip"};
            Args = {"player/user"};
            Description = "Makes the target player(s) a VIP; saves";
            AdminLevel = "Admins";
            Function = function(plr: Player, args: {string}, data: {any})
                local senderLevel = data.PlayerData.Level

                for _, v in service.GetPlayers(plr, assert(args[1], "Missing target player (argument #1)"), {
                    UseFakePlayer = true;
                    })
                do
                if senderLevel > Admin.GetLevel(v) then
                    Admin.AddAdmin(v, "VIP")
                    Functions.Hint(`{service.FormatPlayer(v, true)} is now a VIP`, {plr})
                else
                    Functions.Hint(`{service.FormatPlayer(v, true)} is already the same admin level as you or higher`, {plr})
                end
            end
        end}
    Admin.CacheCommands()
end


--!nonstrict
--[[
	Author: joritochip
	Name: Disable oldlogs
	Description: This plugin allows you to fully disable Adonis old logs
	Place in a ModuleScript under Adonis_Loader > Config > Plugins and named "Server-DisableOldlogs"
--]]

return function(Vargs)
	local server, service = Vargs.Server, Vargs.Service

	server.Commands.OldLogs = nil

	-- The code below this will COMPLETELY remove the OldCommandLogs functionality and it will stop saving old logs entirely.
	-- Delete from here to the line if you just want the command disabled.
	server.Logs.ListUpdaters.OldCommandLogs = function()
		return {}
	end
	server.Logs.SaveCommandLogs = function() end
	--------------------------------------------------------------------
	server.Admin.CacheCommands()
end


--!nonstrict
--[[
	Original Author: crywink
	Rewrite Author: Expertcoderz
	Name: Server-DiscordWebhookLogs
	Description: Enables logging of commands to a Discord webhook

	Place in a ModuleScript under Adonis_Loader > Config > Plugins, named "Server-DiscordWebhookLogs"

	NOTICE: Discord has blocked Roblox from sending out requests; you are required to use a proxy for your webhook to work.
			Try this DevForum post: https://devforum.roblox.com/t/discord-webhook-proxy/1500688
			-GalacticInspired

	WARNING: DISCORD IS NOT INTENDED AS A LOGGING SERVICE; USE THIS AT YOUR OWN RISK!
--]]

local WEBHOOK_URL: string = "YOUR_WEBHOOK_LINK_HERE" --// Your webhook URL

local EMBED_INFO = {
	Color = Color3.fromRGB(85, 170, 255),
	Title = "Command Ran",

	IncludePlaceName = true,
	IncludeTimestamp = true
}

local IGNORE_PLAYER_COMMANDS: boolean = true --// Whether player-level commands such as !info won't be logged to the webhook

local IGNORED_COMMANDS: {string} = { --// Commands that won't be logged to the webhook (array of command names/indices)

}

return function(Vargs)
	local server, service = Vargs.Server, Vargs.Service

	local Admin = server.Admin

	local HttpService: HttpService = service.HttpService

	local embedAuthorInfo = if EMBED_INFO.IncludePlaceName then {
		name = service.MarketplaceService:GetProductInfo(game.PlaceId).Name,
		url = "https://www.roblox.com/games/"..game.PlaceId
	} else nil

	local webhookBatch = {}

	service.Events.CommandRan:Connect(function(plr: Player, data)
		local cmd = data.Command

		if
			(IGNORE_PLAYER_COMMANDS and cmd.AdminLevel == 0) --// player command
			or (table.find(IGNORED_COMMANDS, data.Index)) --// "Fly"
			or (table.find(IGNORED_COMMANDS, data.Matched)) --// ":fly"
			or (table.find(IGNORED_COMMANDS, data.Matched:sub(#cmd.Prefix + 1))) --// "fly"
		then
			return
		end

		table.insert(webhookBatch, {
			author = embedAuthorInfo,
			title = EMBED_INFO.Title,
			color = if type(EMBED_INFO.Color) == "number" then EMBED_INFO.Color else tonumber(EMBED_INFO.Color:ToHex(), 16),
			fields = {
				{
					name = "User:",
					value = string.format(
						"[%s](https://www.roblox.com/users/%d/profile) [%s]",
						service.FormatPlayer(plr),
						plr.UserId,
						Admin.LevelToListName(data.PlayerData.Level)
					)
				},
				{
					name = "Command:",
					value = "``"..data.Message:gsub("[%*_`~]", "\\%1").."``"
				},
				if data.Error then {
					name = "Error:",
					value = "*"..data.Error:gsub("[%*_`~]", "\\%1").."*"
				} else nil
			},
			timestamp = if EMBED_INFO.IncludeTimestamp then DateTime.now():ToIsoDate() else nil
		})
	end)

	task.defer(function()
		while true do
			task.wait(math.random(1, 5))
			if #webhookBatch > 0 then
				local success, err = pcall(HttpService.PostAsync, HttpService, WEBHOOK_URL, HttpService:JSONEncode({
					embeds = webhookBatch
				}))
				if success then
					table.clear(webhookBatch)
				else
					warn("Webhook command log failed to post;", err)
				end
			end
		end
	end)
end


--!nonstrict
--[[
	Author: Sceleratis (Davey_Bones)
	Description: This plugin gives admin to every non-admin player who joins

	Place in a ModuleScript under Adonis_Loader > Config > Plugins, named "Server-Free_Admin"
--]]

local FREE_ADMIN_LEVEL: number = 100 --// Level of admin to give everyone; rank levels are defined in loader Settings
--// NOTE: Creator (level 900+) is EXTREMELY risky and not advised as they can do everything including change settings.

local ADMIN_NOTIFICATIONS: boolean = true --// Whether players are notified upon being ranked

return function(Vargs)
	local server = Vargs.Server
	local service = Vargs.Service

	local Settings = server.Settings
	local Remote = server.Remote
	local Admin = server.Admin
	local Core = server.Core

	service.Events.PlayerAdded:Connect(function(p)
		task.wait(1)

		if not Admin.CheckAdmin(p) then
			Admin.SetLevel(p, FREE_ADMIN_LEVEL)

			if ADMIN_NOTIFICATIONS then
				Remote.MakeGui(p, "Notification", {
					Title = "Notification";
					Icon = server.MatIcons.Verified;
					Message = string.format("You are a(n) %s. Click to view commands.", Admin.LevelToListName(FREE_ADMIN_LEVEL));
					Time = 10;
					OnClick = Core.Bytecode("client.Remote.Send('ProcessCommand','"..Settings.Prefix.."cmds')");
				})
			end
		end
	end)
end


--!nonstrict
--[[
	Name: Gear Blacklist
	Author: Sceleratis (Davey_Bones)
	Description: This plugins allows for the blacklisting of certain IDs from the :gear command

	Place in a ModuleScript under Adonis_Loader > Config > Plugins and name it "Server-Gear_Blacklist"
--]]

local BLACKLISTED_GEAR_IDS: {number} = { --// List of gear IDs to blacklist
	144950355,
	4842190633,
	4842207161
}

return function(Vargs)
	local server = Vargs.Server
	local service = Vargs.Service

	local Settings = server.Settings
	local Variables = server.Variables
	local Commands = server.Commands
	local Remote = server.Remote
	local Admin = server.Admin

	local oldFunc = Commands.Gear and Commands.Gear.Function
	if not oldFunc then
		warn("The command definition and/or function for "..Settings.Prefix.."gear is missing")
		return
	end

	Commands.Gear.Function = function(plr: Player, args: {string}, ...)
		local gearId = assert(tonumber(args[1]), "Gear ID (argument #1) invalid or missing")
		if table.find(BLACKLISTED_GEAR_IDS, gearId) then
			local success, gearInfo = pcall(service.MarketplaceService.GetProductInfo, service.MarketplaceService, gearId)
			error(string.format("The gear %s is blacklisted", if success then '"'..gearInfo.Name..'"' else "of ID "..gearId))
		end
		return oldFunc(plr, args, ...)
	end

	Admin.CacheCommands()
end


--!nonstrict
--[[
	Author: Expertcoderz
	Description: Prevents certain leaderstat values from being changed by admins using :change, :add or :subtract

	Place this in a ModuleScript under Adonis_Loader > Config > Plugins and name it "Server-Leaderstat_Protector"
]]

type Permission = string|number|{string|number}

--// List of leaderstat names to protect:
local PROTECTED_LEADERSTAT_NAMES: {string} = {
	"Precious Money", "Time"
}

--// People with this admin level or rank can bypass leaderstat protection:
local OVERRIDE_PERMISSION: Permission? = 900

return function(Vargs)
	local server = Vargs.Server
	local service = Vargs.Service

	local Admin = server.Admin
	local Commands = server.Commands

	for _, ind in {"Change", "AddToStat", "SubtractFromStat"} do
		local cmd = Commands[ind]
		if cmd and cmd.Function then
			local oldFunc = cmd.Function
			cmd.Function = function(plr: Player, args: {string}, data)
				if not (OVERRIDE_PERMISSION and Admin.CheckComLevel(data.PlayerData.Level, OVERRIDE_PERMISSION)) then
					local statName = args[2]
					if statName then
						for _, protectedName in PROTECTED_LEADERSTAT_NAMES do
							assert(
								not string.match(protectedName:lower(), "^"..statName:lower()),
								"You do not have permission to change the leaderstat \""..protectedName..'"'
							)
						end
					end
				end
				return oldFunc(plr, args, data)
			end
		end
	end

	Admin.CacheCommands()
end