local RunService = game:GetService("RunService")
local StarterGui = game:GetService("StarterGui")
local Players = game:GetService("Players")
local Player = Players.LocalPlayer
local Shared = script:WaitForChild("Shared")
local Util = require(Shared:WaitForChild("Util"))

if RunService:IsClient() == false then
	error(
		"[Cmdr] Server scripts cannot require the client library. Please require the server library from the server to use Cmdr in your own code."
	)
end

--[=[
	@class CmdrClient
	@client
	The Cmdr client singleton and entry point.
]=]

--[=[
	@within CmdrClient
	@prop Registry Registry
	@readonly
	Refers to the current command Registry.
]=]

--[=[
	@within CmdrClient
	@prop Dispatcher Dispatcher
	@readonly
	Refers to the current command Dispatcher.
]=]

--[=[
	@within CmdrClient
	@prop Util Util
	@readonly
	Refers to a table containing many useful utility functions.
]=]

--[=[
	@within CmdrClient
	@prop Enabled boolean
	@readonly
	Whether or not Cmdr is enabled (will show via the defined activation keys). Use [`CmdrClient:SetEnabled`](#SetEnabled) to change.
]=]

--[=[
	@within CmdrClient
	@prop PlaceName string
	@readonly
	The current place name, displayed on the interface. Use [`CmdrClient:SetPlaceName`](#SetPlaceName) to change.
]=]

--[=[
	@within CmdrClient
	@prop ActivationKeys { [Enum.KeyCode] = true }
	@readonly
	The list of key codes that will show or hide Cmdr. Use [`CmdrClient:SetActivationKeys`](#SetActivationKeys) to change.
]=]

local Cmdr
do
	Cmdr = setmetatable({
		ReplicatedRoot = script,
		RemoteFunction = script:WaitForChild("CmdrFunction"),
		RemoteEvent = script:WaitForChild("CmdrEvent"),
		ActivationKeys = { [Enum.KeyCode.F2] = true },
		Enabled = true,
		MashToEnable = false,
		ActivationUnlocksMouse = false,
		HideOnLostFocus = true,
		PlaceName = "Cmdr",
		Util = Util,
		Events = {},
	}, {
		-- This sucks, and may be redone or removed
		-- Proxies dispatch methods on to main Cmdr object
		__index = function(self, k)
			local r = self.Dispatcher[k]
			if r and type(r) == "function" then
				return function(_, ...)
					return r(self.Dispatcher, ...)
				end
			end
		end,
	})

	Cmdr.Registry = require(Shared.Registry)(Cmdr)
	Cmdr.Dispatcher = require(Shared.Dispatcher)(Cmdr)
end

if StarterGui:WaitForChild("Cmdr") and wait() and Player:WaitForChild("PlayerGui"):FindFirstChild("Cmdr") == nil then
	StarterGui.Cmdr:Clone().Parent = Player.PlayerGui
end

local Interface = require(script.CmdrInterface)(Cmdr)

--[=[
	Sets the key codes that will used to show or hide Cmdr.

	@within CmdrClient
]=]
function Cmdr:SetActivationKeys(keys: { Enum.KeyCode })
	self.ActivationKeys = Util.MakeDictionary(keys)
end

--[=[
	Sets the place name label on the interface. This is useful for a quick way to tell what game you're playing in a universe game.

	@within CmdrClient
]=]
function Cmdr:SetPlaceName(name: string)
	self.PlaceName = name
	Interface.Window:UpdateLabel()
end

--[=[
	Sets whether or not Cmdr can be shown via the defined activation keys. Useful for when you want users to opt-in to show the console, for instance in a settings menu.

	@within CmdrClient
]=]
function Cmdr:SetEnabled(enabled: boolean)
	self.Enabled = enabled
end

--[=[
	Sets if activation will free the mouse.

	@within CmdrClient
]=]
function Cmdr:SetActivationUnlocksMouse(enabled: boolean)
	self.ActivationUnlocksMouse = enabled
end

--[=[
	Shows the Cmdr window. Does nothing if Cmdr isn't enabled.

	@within CmdrClient
]=]
function Cmdr:Show()
	if not self.Enabled then
		return
	end

	Interface.Window:Show()
end

--[=[
	Hides the Cmdr window.

	@within CmdrClient
]=]
function Cmdr:Hide()
	Interface.Window:Hide()
end

--[=[
	Toggles the Cmdr window. Does nothing if Cmdr isn't enabled.

	@within CmdrClient
]=]
function Cmdr:Toggle()
	if not self.Enabled then
		self:Hide()
		return
	end

	Interface.Window:SetVisible(not Interface.Window:IsVisible())
end

--[=[
	Enables the "Mash to open" feature.
	This feature, when enabled, requires the activation key to be pressed 5 times within a second to [enable](#SetEnabled) Cmdr.
	This may be helpful to guard against mispresses from opening the window, for example.

	@within CmdrClient
]=]
function Cmdr:SetMashToEnable(enabled: boolean)
	self.MashToEnable = enabled

	if enabled then
		self:SetEnabled(false)
	end
end

--[=[
	Sets the hide on 'lost focus' feature.
	This feature, which is enabled by default, will cause Cmdr to [hide](#Hide) when the user clicks off the window.

	@within CmdrClient
]=]
function Cmdr:SetHideOnLostFocus(enabled: boolean)
	self.HideOnLostFocus = enabled
end

--[=[
	Sets the [network event handler](/docs/networkeventhandlers) for a certain event type.

	@within CmdrClient
]=]
function Cmdr:HandleEvent(name: string, callback: (...any) -> ())
	self.Events[name] = callback
end

-- "Only register when we aren't in studio because don't want to overwrite what the server portion did"
-- This is legacy code which predates Accurate Play Solo (which is now the only way to test in studio), this code will never run.
if RunService:IsServer() == false then
	Cmdr.Registry:RegisterTypesIn(script:WaitForChild("Types"))
	Cmdr.Registry:RegisterCommandsIn(script:WaitForChild("Commands"))
end

-- Hook up event listener
Cmdr.RemoteEvent.OnClientEvent:Connect(function(name, ...)
	if Cmdr.Events[name] then
		Cmdr.Events[name](...)
	end
end)

require(script.DefaultEventHandlers)(Cmdr)

return Cmdr


return function()
	local Cmdr = Instance.new("ScreenGui")
	Cmdr.DisplayOrder = 1000
	Cmdr.Name = "Cmdr"
	Cmdr.ResetOnSpawn = false
	Cmdr.AutoLocalize = false

	local Frame = Instance.new("ScrollingFrame")
	Frame.BackgroundColor3 = Color3.fromRGB(17, 17, 17)
	Frame.BackgroundTransparency = 0.4
	Frame.BorderSizePixel = 0
	Frame.CanvasSize = UDim2.new(0, 0, 0, 0)
	Frame.Name = "Frame"
	Frame.Position = UDim2.new(0.025, 0, 0, 25)
	Frame.ScrollBarThickness = 6
	Frame.ScrollingDirection = Enum.ScrollingDirection.Y
	Frame.Selectable = false
	Frame.Size = UDim2.new(0.95, 0, 0, 0)
	Frame.Visible = false
	Frame.AutomaticCanvasSize = Enum.AutomaticSize.Y
	Frame.Parent = Cmdr

	local Autocomplete = Instance.new("ScrollingFrame")
	Autocomplete.BackgroundColor3 = Color3.fromRGB(59, 59, 59)
	Autocomplete.BackgroundTransparency = 0.5
	Autocomplete.BorderSizePixel = 0
	Autocomplete.CanvasSize = UDim2.new(0, 0, 0, 0)
	Autocomplete.Name = "Autocomplete"
	Autocomplete.Position = UDim2.new(0, 167, 0, 75)
	Autocomplete.ScrollBarThickness = 6
	Autocomplete.ScrollingDirection = Enum.ScrollingDirection.Y
	Autocomplete.Selectable = false
	Autocomplete.Size = UDim2.new(0, 200, 0, 200)
	Autocomplete.Visible = false
	Autocomplete.AutomaticCanvasSize = Enum.AutomaticSize.Y
	Autocomplete.Parent = Cmdr

	local UIListLayout = Instance.new("UIListLayout")
	UIListLayout.SortOrder = Enum.SortOrder.LayoutOrder
	UIListLayout.Parent = Frame

	local Line = Instance.new("TextBox")
	Line.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
	Line.BackgroundTransparency = 1
	Line.Font = Enum.Font.Code
	Line.Name = "Line"
	Line.Size = UDim2.new(1, 0, 0, 20)
	Line.AutomaticSize = Enum.AutomaticSize.Y
	Line.TextColor3 = Color3.fromRGB(255, 255, 255)
	Line.TextSize = 14
	Line.TextXAlignment = Enum.TextXAlignment.Left
	Line.TextEditable = false
	Line.ClearTextOnFocus = false
	Line.Parent = Frame

	local UIPadding = Instance.new("UIPadding")
	UIPadding.PaddingBottom = UDim.new(0, 10)
	UIPadding.PaddingLeft = UDim.new(0, 10)
	UIPadding.PaddingRight = UDim.new(0, 10)
	UIPadding.PaddingTop = UDim.new(0, 10)
	UIPadding.Parent = Frame

	local Entry = Instance.new("Frame")
	Entry.BackgroundTransparency = 1
	Entry.LayoutOrder = 999999999
	Entry.Name = "Entry"
	Entry.Size = UDim2.new(1, 0, 0, 20)
	Entry.Parent = Frame

	local UIListLayout2 = Instance.new("UIListLayout")
	UIListLayout2.SortOrder = Enum.SortOrder.LayoutOrder
	UIListLayout2.Parent = Autocomplete

	local Title = Instance.new("Frame")
	Title.BackgroundColor3 = Color3.fromRGB(59, 59, 59)
	Title.BackgroundTransparency = 0.2
	Title.BorderSizePixel = 0
	Title.LayoutOrder = -2
	Title.Name = "Title"
	Title.Size = UDim2.new(1, 0, 0, 40)
	Title.Parent = Autocomplete

	local Description = Instance.new("Frame")
	Description.BackgroundColor3 = Color3.fromRGB(59, 59, 59)
	Description.BackgroundTransparency = 0.2
	Description.BorderSizePixel = 0
	Description.LayoutOrder = -1
	Description.Name = "Description"
	Description.Size = UDim2.new(1, 0, 0, 20)
	Description.Parent = Autocomplete

	local TextButton = Instance.new("TextButton")
	TextButton.BackgroundColor3 = Color3.fromRGB(59, 59, 59)
	TextButton.BackgroundTransparency = 0.5
	TextButton.BorderSizePixel = 0
	TextButton.Font = Enum.Font.Code
	TextButton.Size = UDim2.new(1, 0, 0, 30)
	TextButton.Text = ""
	TextButton.TextColor3 = Color3.fromRGB(255, 255, 255)
	TextButton.TextSize = 14
	TextButton.TextXAlignment = Enum.TextXAlignment.Left
	TextButton.Parent = Autocomplete

	local UIListLayout3 = Instance.new("UIListLayout")
	UIListLayout3.FillDirection = Enum.FillDirection.Horizontal
	UIListLayout3.SortOrder = Enum.SortOrder.LayoutOrder
	UIListLayout3.Padding = UDim.new(0, 7)
	UIListLayout3.Parent = Entry

	local TextBox = Instance.new("TextBox")
	TextBox.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
	TextBox.BackgroundTransparency = 1
	TextBox.ClearTextOnFocus = false
	TextBox.Font = Enum.Font.Code
	TextBox.LayoutOrder = 999999999
	TextBox.Position = UDim2.new(0, 140, 0, 0)
	TextBox.Size = UDim2.new(1, 0, 0, 20)
	TextBox.Text = "x"
	TextBox.TextColor3 = Color3.fromRGB(255, 255, 255)
	TextBox.TextSize = 14
	TextBox.TextXAlignment = Enum.TextXAlignment.Left
	TextBox.Selectable = false
	TextBox.Parent = Entry

	local TextLabel = Instance.new("TextLabel")
	TextLabel.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
	TextLabel.BackgroundTransparency = 1
	TextLabel.Font = Enum.Font.Code
	TextLabel.Size = UDim2.new(0, 0, 0, 20)
	TextLabel.AutomaticSize = Enum.AutomaticSize.X
	TextLabel.Text = ""
	TextLabel.TextColor3 = Color3.fromRGB(255, 223, 93)
	TextLabel.TextSize = 14
	TextLabel.TextXAlignment = Enum.TextXAlignment.Left
	TextLabel.Parent = Entry

	local Field = Instance.new("TextLabel")
	Field.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
	Field.BackgroundTransparency = 1
	Field.Font = Enum.Font.SourceSansBold
	Field.Name = "Field"
	Field.Size = UDim2.new(0, 37, 1, 0)
	Field.Text = "from"
	Field.TextColor3 = Color3.fromRGB(255, 255, 255)
	Field.TextSize = 20
	Field.TextXAlignment = Enum.TextXAlignment.Left
	Field.Parent = Title

	local UIPadding2 = Instance.new("UIPadding")
	UIPadding2.PaddingLeft = UDim.new(0, 10)
	UIPadding2.Parent = Title

	local Label = Instance.new("TextLabel")
	Label.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
	Label.BackgroundTransparency = 1
	Label.Font = Enum.Font.SourceSansLight
	Label.Name = "Label"
	Label.Size = UDim2.new(1, 0, 1, 0)
	Label.Text = "The players to teleport. The players to teleport. The players to teleport. The players to teleport. "
	Label.TextColor3 = Color3.fromRGB(255, 255, 255)
	Label.TextSize = 16
	Label.TextWrapped = true
	Label.TextXAlignment = Enum.TextXAlignment.Left
	Label.TextYAlignment = Enum.TextYAlignment.Top
	Label.Parent = Description

	local UIPadding3 = Instance.new("UIPadding")
	UIPadding3.PaddingBottom = UDim.new(0, 10)
	UIPadding3.PaddingLeft = UDim.new(0, 10)
	UIPadding3.PaddingRight = UDim.new(0, 10)
	UIPadding3.Parent = Description

	local Typed = Instance.new("TextLabel")
	Typed.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
	Typed.BackgroundTransparency = 1
	Typed.Font = Enum.Font.Code
	Typed.Name = "Typed"
	Typed.Size = UDim2.new(1, 0, 1, 0)
	Typed.Text = "Lab"
	Typed.TextColor3 = Color3.fromRGB(131, 222, 255)
	Typed.TextSize = 14
	Typed.TextXAlignment = Enum.TextXAlignment.Left
	Typed.Parent = TextButton

	local UIPadding4 = Instance.new("UIPadding")
	UIPadding4.PaddingLeft = UDim.new(0, 10)
	UIPadding4.Parent = TextButton

	local Suggest = Instance.new("TextLabel")
	Suggest.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
	Suggest.BackgroundTransparency = 1
	Suggest.Font = Enum.Font.Code
	Suggest.Name = "Suggest"
	Suggest.Size = UDim2.new(1, 0, 1, 0)
	Suggest.Text = "   el"
	Suggest.TextColor3 = Color3.fromRGB(255, 255, 255)
	Suggest.TextSize = 14
	Suggest.TextXAlignment = Enum.TextXAlignment.Left
	Suggest.Parent = TextButton

	local Type = Instance.new("TextLabel")
	Type.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
	Type.BackgroundTransparency = 1
	Type.BorderColor3 = Color3.fromRGB(255, 153, 153)
	Type.Font = Enum.Font.SourceSans
	Type.Name = "Type"
	Type.Position = UDim2.new(1, 0, 0, 0)
	Type.Size = UDim2.new(0, 0, 1, 0)
	Type.Text = ": Players"
	Type.TextColor3 = Color3.fromRGB(255, 255, 255)
	Type.TextSize = 15
	Type.TextXAlignment = Enum.TextXAlignment.Left
	Type.Parent = Field

	Cmdr.Parent = game:GetService("StarterGui")
	return Cmdr
end


local ReplicatedStorage = game:GetService("ReplicatedStorage")
local StarterGui = game:GetService("StarterGui")
local CreateGui = require(script.Parent.CreateGui)

-- Handles initial preparation of the game server-side.
return function(cmdr)
	local ReplicatedRoot, RemoteFunction, RemoteEvent

	local function Create(class, name, parent)
		local object = Instance.new(class)
		object.Name = name
		object.Parent = parent or ReplicatedRoot

		return object
	end

	ReplicatedRoot = script.Parent.CmdrClient
	ReplicatedRoot.Parent = ReplicatedStorage

	RemoteFunction = Create("RemoteFunction", "CmdrFunction")
	RemoteEvent = Create("RemoteEvent", "CmdrEvent")

	Create("Folder", "Commands")
	Create("Folder", "Types")

	script.Parent.Shared.Parent = ReplicatedRoot

	cmdr.ReplicatedRoot = ReplicatedRoot
	cmdr.RemoteFunction = RemoteFunction
	cmdr.RemoteEvent = RemoteEvent

	cmdr:RegisterTypesIn(script.Parent.BuiltInTypes)

	script.Parent.BuiltInTypes:Destroy()
	script.Parent.BuiltInCommands.Name = "Server commands"

	if StarterGui:FindFirstChild("Cmdr") == nil then
		CreateGui()
	end
end


local RunService = game:GetService("RunService")

local Util = require(script.Parent.Util)

--[=[
	@class Registry

	The registry keeps track of all the commands and types that Cmdr knows about.
]=]

--[=[
	@type HookType "BeforeRun" | "AfterRun"
	@within Registry
]=]

--[=[
	@interface ArgumentDefinition
	@within Registry
	.Type string | TypeDefinition -- The argument type (case sensitive), or an [inline TypeDefinition object](/docs/commands#dynamic-arguments-and-inline-types).
	.Name string -- The argument name, this is displayed to the user as they type.
	.Description string? -- A description of what the argument is, this is also displayed to the user.
	.Optional boolean? -- If this is set to `true`, then the user can run the command without filling out the value. In which case, the argument will be sent to implementations as `nil`.
	.Default any? -- If present, the argument will be automatically made optional, so if the user doesn't supply a value, implementations will receive whatever the value of `Default` is.

	The `table` definition, usually contained in a [CommandDefinition](#CommandDefinition), which 'defines' the argument.
]=]

--[=[
	@interface CommandDefinition
	@within Registry
	.Name string -- The name of the command
	.Aliases {string}? -- Aliases which aren't part of auto-complete, but if matched will run this command just the same. For example, `m` might be an alias of `announce`.
	.Description string? -- A description of the command, displayed to the user in the `help` command and auto-complete menu.
	.Group string? -- This property is intended to be used in hooks, so that you can categorise commands and decide if you want a specific user to be able to run them or not. But the `help` command will use them as headings.
	.Args {ArgumentDefinition | (CommandContext) -> (ArgumentDefinition)} -- Arguments for the command; this is technically optional but if you have no args, set it to `{}` or you may experience some interface weirdness.
	.Data (CommandContext, ...) -> any -- If your command needs to gather some extra data from the client that's only available on the client, then you can define this function. It should accept the command context and tuple of transformed arguments, and return a single value which will be available in the command with [CommandContext:GetData](/api/CommandContext#GetData).
	.ClientRun (CommandContext, ...) -> string? -- If you want your command to run on the client, you can add this function to the command definition itself. It works exactly like the function that you would return from the Server module. If this function returns a string, the command will run entirely on the client and won't touch the server (which means server-only hooks won't run). If this function doesn't return anything, it will fall back to executing the Server module on the server.
	.Run (CommandContext, ...) -> string? -- An older version of ClientRun. There are very few scenarios where this is preferred to ClientRun (so, in other words, don't use it!). These days, `Run` is only used for some dark magic involving server-sided command objects.
]=]

--[=[
	@interface TypeDefinition
	@within Registry
	.Prefixes string? -- String containing [prefixed union types](/docs/commands#prefixed-union-types) for this type. This property should omit the inital type, so the string should begin with a prefix character, e.g. `Prefixes = "# integer ! boolean"`
	.DisplayName string? -- Overrides the user-facing name of this type in the autocomplete menu. Otherwise, the registered name of the type will be used.
	.Default ((Player) -> string)? -- Should return the "default" value for this type as a string. For example, the default value of the `player` type is the name of the player who ran the command.
	.Listable boolean? -- If true, this will tell Cmdr that comma-separated lists are allowed for this type. Cmdr will automatically split the list and parse each segment through your `Transform`, `Validate`, `Autocomplete` and `Parse` functions individually, so you don't have to change the logic of your type at all. The only limitation is that your `Parse` function **must return a table**, the tables from each individual segment's `Parse` functions will be merged into one table at the end of the parsing step. The uniqueness of values is ensured upon merging, so even if the user lists the same value several times, it will only appear once in the final table.
	.Transform (string, Player) -> T? -- Transform is an optional function that is passed two values: the raw text, and the player running the command. Then, whatever values this function returns will be passed to all other functions in the type (`Validate`, `Autocomplete` and `Parse`).
	.Validate (T) -> (boolean, string?) -- The `Validate` function is passed whatever is returned from the `Transform` function (or the raw value if there is no `Transform` function). If the value is valid for the type, it should return `true`. If the value is invalid, it should return two values: `false` and a string containing an error message. If this function is omitted, anything will be considered valid.
	.ValidateOnce (T) -> (boolean, string?) -- This function works exactly the same as the normal `Validate` function, except it only runs once (after the user presses Enter). This should only be used if the validation process is relatively expensive or needs to yield. For example, the `playerId` type uses this because it needs to call `GetUserIdFromNameAsync` in order to validate. For the vast majority of types, you should just use `Validate` instead.
	.Autocomplete (T) -> ({string}, {IsPartial: boolean?}?)? -- Should only be present for types that are possible to be auto-completed. It should return an array of strings that will be displayed in the auto-complete menu. It can also return a second value, containing a dictionary of options (currently, `IsPartial`: if true then pressing Tab to auto-complete won't continue onwards to the next argument.)
	.Parse (T) -> any -- Parse is the only required function in a type definition. It is the final step before the value is considered finalised. This function should return the actual parsed value that will be sent to implementations.

	The `table` definition, contained in an [ArgumentDefinition](#ArgumentDefinition) or [registered](#RegisterType), which 'defines' the argument.
]=]

--[=[
	@prop TypeMethods { [string]: true }
	@within Registry
	@readonly
	@private
]=]

--[=[
	@prop CommandMethods { [string]: true }
	@within Registry
	@readonly
	@private
]=]

--[=[
	@prop CommandArgProps { [string]: true }
	@within Registry
	@readonly
	@private
]=]

--[=[
	@prop Types table
	@within Registry
	@readonly
	@private
]=]

--[=[
	@prop TypeAliases table
	@within Registry
	@readonly
	@private
]=]

--[=[
	@prop Commands table
	@within Registry
	@readonly
	@private
]=]

--[=[
	@prop CommandsArray table
	@within Registry
	@readonly
	@private
]=]

--[=[
	@prop Cmdr Cmdr | CmdrClient
	@within Registry
	@readonly
	A reference to Cmdr. This may either be the server or client version of Cmdr depending on where the code is running.
]=]

--[=[
	@prop Hooks { [HookType]: table }
	@within Registry
	@readonly
	@private
]=]

--[=[
	@prop Stores table
	@within Registry
	@readonly
	@private
]=]

--[=[
	@prop AutoExecBuffer table
	@within Registry
	@readonly
	@private
]=]

local Registry = {
	TypeMethods = Util.MakeDictionary({
		"Transform",
		"Validate",
		"Autocomplete",
		"Parse",
		"DisplayName",
		"Listable",
		"ValidateOnce",
		"Prefixes",
		"Default",
		"ArgumentOperatorAliases",
	}),
	CommandMethods = Util.MakeDictionary({
		"Name",
		"Aliases",
		"AutoExec",
		"Description",
		"Args",
		"Run",
		"ClientRun",
		"Data",
		"Group",
		"Guards",
	}),
	CommandArgProps = Util.MakeDictionary({ "Name", "Type", "Description", "Optional", "Default" }),
	Types = {},
	TypeAliases = {},
	Commands = {},
	CommandsArray = {},
	Cmdr = nil,
	Hooks = {
		BeforeRun = {},
		AfterRun = {},
	},
	Stores = setmetatable({}, {
		__index = function(self, k)
			self[k] = {}
			return self[k]
		end,
	}),
	AutoExecBuffer = {},
}

--[=[
	Registers a type on the current realm (server/client), this function should be called from within the type definition ModuleScript.
	@param name -- The name of the type, this must be unique, alphanumeric, and start with a lower-case letter or digit.
	@param typeObject TypeDefinition
	@within Registry
]=]
function Registry:RegisterType(name: string, typeObject)
	if not name or typeof(name) ~= "string" then
		error("[Cmdr] Invalid type name provided: nil")
	end

	if not name:find("^[%d%l]%w*$") then
		error(
			`[Cmdr] Invalid type name provided: "{name}", type names must be alphanumeric and start with a lower-case letter or a digit.`
		)
	end

	for key in pairs(typeObject) do
		if self.TypeMethods[key] == nil then
			error(`[Cmdr] Unknown key/method in type "{name}": {key}`)
		end
	end

	if self.Types[name] ~= nil then
		error(`[Cmdr] Type {name} has already been registered.`)
	end

	typeObject.Name = name
	typeObject.DisplayName = typeObject.DisplayName or name

	self.Types[name] = typeObject

	if typeObject.Prefixes then
		self:RegisterTypePrefix(name, typeObject.Prefixes)
	end
end

--[=[
	Registers a Prefixed Union Type string on the current realm (server/client), this function should be called from within the type definition ModuleScript.

	If there are already type prefixes for the given type name, they will be concatenated. This allows you to add prefixes to default types, like `players`.

	@param name -- The name of the type.
	@param union -- The string should omit the initial type name, so this string should begin with a prefix character, e.g. `# integer ! boolean`
	@within Registry
]=]
function Registry:RegisterTypePrefix(name: string, union: string)
	if not self.TypeAliases[name] then
		self.TypeAliases[name] = name
	end

	self.TypeAliases[name] = ("%s %s"):format(self.TypeAliases[name], union)
end

--[=[
	Allows you to register a name which will be expanded into a longer type which can be used as a command argument type.
	For example, if you register the alias `stringOrNumber` it could be interpreted as `string # number` when used.

	@param name -- The name of the type, this must be unique, alphanumeric, and start with a lower-case letter or digit.
	@param alias -- The string should *include* the initial type name, e.g. `string # integer ! boolean`
	@within Registry
]=]
function Registry:RegisterTypeAlias(name: string, alias: string)
	assert(self.TypeAliases[name] == nil, `[Cmdr] Type alias {alias} already exists!`)
	self.TypeAliases[name] = alias
end

--[=[
	Registers all types from within a container on both the server and client.

	@server
	@within Registry
]=]
function Registry:RegisterTypesIn(container: Instance)
	for _, object in pairs(container:GetChildren()) do
		if object:IsA("ModuleScript") then
			object.Parent = self.Cmdr.ReplicatedRoot.Types

			require(object)(self)
		else
			self:RegisterTypesIn(object)
		end
	end
end

-- These are exactly the same thing. No one will notice. Except for you, dear reader.
--[=[
	@method RegisterHooksIn
	Registers all hooks from within a container on both the server and client.
	If you want to add a hook only on the server or client – e.g. for logging – then you should use the Register.RegisterHook method instead.

	@param container Instance
	@server
	@within Registry
]=]
Registry.RegisterHooksIn = Registry.RegisterTypesIn

--[=[
	Register a command purely based on its definition.
	Prefer using Registry:RegisterCommand for proper handling of client/server model.

	@param commandObject CommandDefinition
	@private
	@within Registry
]=]
function Registry:RegisterCommandObject(commandObject)
	for key in pairs(commandObject) do
		if self.CommandMethods[key] == nil then
			error(`[Cmdr] Unknown key/method in command "{commandObject.Name or "unknown command"}": {key}`)
		end
	end

	if commandObject.Args then
		for i, arg in pairs(commandObject.Args) do
			if type(arg) == "table" then
				for key in pairs(arg) do
					if self.CommandArgProps[key] == nil then
						error(
							`[Cmdr] Unknown property in command "{commandObject.Name or "unknown"}" argument #{i}: {key}`
						)
					end
				end
			end
		end
	end

	if commandObject.AutoExec and RunService:IsClient() then
		table.insert(self.AutoExecBuffer, commandObject.AutoExec)
		self:FlushAutoExecBufferDeferred()
	end

	-- Unregister the old command if it exists...
	local oldCommand = self.Commands[commandObject.Name:lower()]
	if oldCommand and oldCommand.Aliases then
		for _, alias in pairs(oldCommand.Aliases) do
			self.Commands[alias:lower()] = nil
		end
	elseif not oldCommand then
		table.insert(self.CommandsArray, commandObject)
	end

	self.Commands[commandObject.Name:lower()] = commandObject

	if commandObject.Aliases then
		for _, alias in pairs(commandObject.Aliases) do
			self.Commands[alias:lower()] = commandObject
		end
	end
end

--[=[
	Registers a command definition and its server equivalent. Handles replicating the definition to the client.

	@param filter (CommandDefinition -> boolean)? -- If present, will be passed a command definition which will then only be registered if the function returns `true`.

	@server
	@within Registry
]=]
function Registry:RegisterCommand(
	commandScript: ModuleScript,
	commandServerScript: ModuleScript?,
	filter: ((any) -> boolean)?
)
	local commandObject = require(commandScript)
	assert(
		typeof(commandObject) == "table",
		`[Cmdr] Invalid return value from command script "{commandScript.Name}" (CommandDefinition expected, got {typeof(commandObject)})`
	)

	if commandServerScript then
		assert(RunService:IsServer(), "[Cmdr] The commandServerScript parameter is not valid for client usage.")
		commandObject.Run = require(commandServerScript)
	end

	if filter and not filter(commandObject) then
		return
	end

	self:RegisterCommandObject(commandObject)

	commandScript.Parent = self.Cmdr.ReplicatedRoot.Commands
end

--[=[
	Registers all commands from within a container on both the server and client.

	Module scripts which include `Server` in their name will not be sent to the client.

	@param filter ((CommandDefinition) -> boolean)? -- If present, will be passed a command definition which will then only be registered if the function returns `true`.

	@server
	@within Registry
]=]
function Registry:RegisterCommandsIn(container: Instance, filter: ((any) -> boolean)?)
	local skippedServerScripts = {}
	local usedServerScripts = {}

	for _, commandScript in pairs(container:GetChildren()) do
		if commandScript:IsA("ModuleScript") then
			if not commandScript.Name:find("Server") then
				local serverCommandScript = container:FindFirstChild(commandScript.Name .. "Server")

				if serverCommandScript then
					usedServerScripts[serverCommandScript] = true
				end

				self:RegisterCommand(commandScript, serverCommandScript, filter)
			else
				skippedServerScripts[commandScript] = true
			end
		else
			self:RegisterCommandsIn(commandScript, filter)
		end
	end

	for skippedScript in pairs(skippedServerScripts) do
		if not usedServerScripts[skippedScript] then
			warn(
				`[Cmdr] Command script {skippedScript.Name} was skipped because it has 'Server' in its name, and has no equivalent shared script.`
			)
		end
	end
end

--[=[
	Registers the default commands on both the server and client.

	The optional `arrayOrFunc` parameter can be provided with:

	1. an array of strings — this will limit registration to only commands which have their `Group` property set to this
	2. a function which takes in a CommandDefinition and returns a `boolean` — only if `true` is returned will the command be registered

	@param arrayOrFunc {string} | (CommandDefinition) -> boolean | nil

	@server
	@within Registry
]=]
function Registry:RegisterDefaultCommands(arrayOrFunc: { string } | (any) -> boolean | nil)
	assert(RunService:IsServer(), "[Cmdr] RegisterDefaultCommands cannot be called from the client.")

	local dictionary = if type(arrayOrFunc) == "table" then Util.MakeDictionary(arrayOrFunc) else nil

	self:RegisterCommandsIn(self.Cmdr.DefaultCommandsFolder, dictionary ~= nil and function(command)
		return dictionary[command.Group] or false
	end or arrayOrFunc)
end

--[=[
	Returns the CommandDefinition from the given name, or nil if no command is found. Command aliases are also accepted.
	@return CommandDefinition?
	@within Registry
]=]
function Registry:GetCommand(name: string)
	name = name or ""
	return self.Commands[name:lower()]
end

--[=[
	Returns an array of all registers commands, not including aliases.
	@return {CommandDefinition}
	@within Registry
]=]
function Registry:GetCommands(): { any }
	return self.CommandsArray
end

--[=[
	Returns an array of containing the names of all registered commands, not including aliases.
	@within Registry
]=]
function Registry:GetCommandNames(): { string }
	local commands = {}

	for _, command in pairs(self.CommandsArray) do
		table.insert(commands, command.Name)
	end

	return commands
end

--[=[
	@method GetCommandsAsStrings
	@deprecated v1.8.0 -- This method was renamed to GetCommandNames in v1.8.0. The old name exists for backwards compatibility and should not be used for new work.
	@within Registry
]=]
Registry.GetCommandsAsStrings = Registry.GetCommandNames

--[=[
	Returns an array of containing the names of all registered types, not including aliases.
	@within Registry
]=]
function Registry:GetTypeNames(): { string }
	local typeNames = {}

	for typeName in pairs(self.Types) do
		table.insert(typeNames, typeName)
	end

	return typeNames
end

--[=[
	Returns the type definition from the given name, or nil if no argument is found.
	@return TypeDefinition?
	@within Registry
]=]
function Registry:GetType(name: string)
	return self.Types[name]
end

--[=[
	Returns a type name taking aliases into account. If there is no alias, the name parameter is simply returned as a pass through.
	@return TypeDefinition | string
	@within Registry
]=]
function Registry:GetTypeName(name: string): string | any
	return self.TypeAliases[name] or name
end

--[=[
	Registers a hook on the current realm (server/client). This should probably be ran on the server or in a hook module script, but can also work on the client.

	Hooks run in order of priority from lowest (running first) to highest.

	@param hookName HookType
	@param callback (CommandContext) -> string? -- returns nil for ok, string (errorText) for cancellation
	@param priority -- If unspecified, the priority will default to `0`.
	@within Registry
]=]
function Registry:RegisterHook(hookName: string, callback: (any) -> string?, priority: number)
	if not self.Hooks[hookName] then
		error(("[Cmdr] Invalid hook name: %q"):format(hookName), 2)
	end

	table.insert(self.Hooks[hookName], { callback = callback, priority = priority or 0 })
	table.sort(self.Hooks[hookName], function(a, b)
		return a.priority < b.priority
	end)
end

--[=[
	@method AddHook
	@deprecated v1.1.2 -- This method was renamed to RegisterHook in v1.1.2. The old name exists for backwards compatibility and should not be used for new work.
	@param hookName HookType
	@param callback (CommandContext) -> string? -- returns nil for ok, string (errorText) for cancellation
	@param priority number -- If unspecified, the priority will default to `0`.
	@within Registry
]=]
Registry.AddHook = Registry.RegisterHook

--[=[
	Returns a table saved with the given name. Always returns the same table on subsequent calls. Useful for commands that require persistent state, like bind or ban.
	This is the same as CommandContext.GetStore.
	@return table
	@within Registry
]=]
function Registry:GetStore(name: string)
	return self.Stores[name]
end

--[=[
	Calls Registry.FlushAutoExecBuffer at the end of the frame.
	@private
	@within Registry
]=]
function Registry:FlushAutoExecBufferDeferred()
	if self.AutoExecFlushConnection then
		return
	end

	self.AutoExecFlushConnection = RunService.Heartbeat:Connect(function()
		self.AutoExecFlushConnection:Disconnect()
		self.AutoExecFlushConnection = nil
		self:FlushAutoExecBuffer()
	end)
end

--[=[
	Runs all pending auto exec commands in Registry.AutoExecBuffer.
	@private
	@within Registry
]=]
function Registry:FlushAutoExecBuffer()
	for _, commandGroup in ipairs(self.AutoExecBuffer) do
		for _, command in ipairs(commandGroup) do
			self.Cmdr.Dispatcher:EvaluateAndRun(command)
		end
	end

	self.AutoExecBuffer = {}
end

return function(cmdr)
	Registry.Cmdr = cmdr

	return Registry
end


local RunService = game:GetService("RunService")
local Util = require(script.Shared:WaitForChild("Util"))

if RunService:IsServer() == false then
	error(
		"[Cmdr] Client scripts cannot require the server library. Please require the client library from the client to use Cmdr in your own code."
	)
end

--[=[
	@class Cmdr
	@server

	The Cmdr server singleton and entry point.
]=]

--[=[
	@within Cmdr
	@prop Registry Registry
	@readonly
	Refers to the current command Registry.
]=]

--[=[
	@within Cmdr
	@prop Dispatcher Dispatcher
	@readonly
	Refers to the current command Dispatcher.
]=]

--[=[
	@within Cmdr
	@prop Util Util
	@readonly
	Refers to a table containing many useful utility functions.
]=]

local Cmdr
do
	Cmdr = setmetatable({
		ReplicatedRoot = nil,
		RemoteFunction = nil,
		RemoteEvent = nil,
		Util = Util,
		DefaultCommandsFolder = script.BuiltInCommands,
	}, {
		__index = function(self, k)
			local r = self.Registry[k]
			if r and type(r) == "function" then
				return function(_, ...)
					return r(self.Registry, ...)
				end
			end
		end,
	})

	Cmdr.Registry = require(script.Shared.Registry)(Cmdr)
	Cmdr.Dispatcher = require(script.Shared.Dispatcher)(Cmdr)

	require(script.Initialize)(Cmdr)
end

-- Handle command invocations from the clients.
Cmdr.RemoteFunction.OnServerInvoke = function(player, text, options)
	if #text > 10000 then
		return "Input too long"
	end

	return Cmdr.Dispatcher:EvaluateAndRun(text, player, options)
end

return Cmdr


local vide = require "@self/src/lib"

export type source<T> = vide.source<T>
export type Source<T> = vide.Source<T>
export type context<T> = vide.context<T>
export type Context<T> = vide.Context<T>

return vide


local typeof = game and typeof or require "../test/mock".typeof :: never

local flags = require "./flags"
local implicit_effect = require "./implicit_effect"
local _, is_action = require "./action"()
local graph = require "./graph"
type Node<T> = graph.Node<T>

type Array<V> = { V }
type ArrayOrV<V> = {ArrayOrV<V>} | V
type Map<K, V> = { [K]: V }

type Cache = {
    -- event listeners to connect after properties are set
    events: Array<
        | string -- 1. event name
        | () -> () -- 2. listener
    >,

    -- actions to run after events are connected
    actions: Map<
        number, -- priority
        Array<(Instance) -> ()> -- action callbacks
    >,

    -- what to parent the instance to after running actions
    parent: unknown,

    -- cache to detect duplicate property setting at same nesting depth
    nested_debug: Map<
        number, -- depth
        Map<string, true> -- set of property names
    >,

    -- each nested layer occupies two indexes: 1. table ref 2. nested depth
    -- e.g. { t1 = { t3 = {} }, t2 = {} } -> { t1, 1, t2, 1, t3, 2 }
    nested_stack: { {} | number }
}

local free_cache: Cache?

local function borrow_cache(): Cache
    if free_cache then
        local cache = free_cache
        free_cache = nil
        return cache
    else
        return {
            events = {},
            actions = setmetatable({} :: any, { -- lazy init
                __index = function(self, i) self[i] = {}; return self[i] end
            }),
            parent = nil,
            nested_debug = setmetatable({} :: any, {
                __index = function(self, i: number) self[i] = {}; return self[i] end
            }),
            nested_stack = {}
        }
    end
end

local function return_cache(cache: Cache )
    free_cache = cache
end

local function process_properties(properties: Map<unknown, unknown>, instance: Instance, cache: Cache, depth: number)
    for property, value in properties do
        if type(property) == "string" then
            if flags.strict then -- check for duplicate property assignment at nesting depth
                if cache.nested_debug[depth][property] then
                    error(`duplicate property {property} at depth {depth}`, 0)
                end
                cache.nested_debug[depth][property] = true
            end

            if property == "Parent" then
                cache.parent = value
                continue
            end

            if type(value) == "function" then
                if typeof((instance :: any)[property]) == "RBXScriptSignal" then
                    table.insert(cache.events, property) -- add event name to buffer
                    table.insert(cache.events, value :: () -> ()) -- add event listener to buffer
                else
                    implicit_effect.property(instance, property, value :: () -> ()) -- create implicit effect for property
                end
            else
                (instance :: any)[property] = value -- set property
            end
        elseif type(property) == "number" then
            if type(value) == "function" then
                implicit_effect.children(instance, value :: () -> ArrayOrV<Instance>) -- bind children
            elseif type(value) == "table" then
                if is_action(value) then
                    table.insert(cache.actions[(value :: any).priority], (value :: any).callback :: () -> ()) -- add action to buffer
                elseif flags.defer_nested_properties then
                    table.insert(cache.nested_stack, value :: {})
                    table.insert(cache.nested_stack, depth + 1) -- push table to stack for later processing
                else
                    process_properties(value :: Map<unknown, unknown>, instance, cache, depth + 1)
                end
            elseif type(value) == "userdata" then
                (value :: Instance).Parent = instance -- parent child
            end
        end
    end
end

-- applies table of nested properties to an instance using full vide semantics
local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown }): T
    if not properties then
        error "attempt to call a constructor returned by create() with no properties"
    end

    local caches = borrow_cache()
    local events = caches.events
    local actions = caches.actions
    local nested_debug = caches.nested_debug
    local nested_stack = caches.nested_stack

    -- process all properties
    local depth = 1
    repeat
        process_properties(properties, instance, caches, depth)
        depth = table.remove(nested_stack) :: number
        properties = table.remove(nested_stack) :: {}
    until not properties

    for i = 1, #events, 2 do
        local event_name = events[i]
        local event_listener = events[i + 1]
        ;(instance :: any)[event_name]:Connect(event_listener)
    end

    for _, queued in actions do
        for _, callback in queued do
            callback(instance)
        end
    end

    local parent = caches.parent
    if parent then
        if type(parent) == "function" then
            implicit_effect.parent(instance, parent :: () -> Instance)
        else
            instance.Parent = parent :: Instance
        end
    end

    table.clear(events)
    for _, queued in actions do table.clear(queued) end
    caches.parent = nil
    if flags.strict then table.clear(nested_debug) end
    table.clear(nested_stack)

    return_cache(caches)

    return instance
end

return apply


local flags = require "./flags"
local graph = require "./graph"

local function batch(setter: () -> ())
    local already_batching = flags.batch
    local from

    if not already_batching then
        flags.batch = true
        from = graph.get_update_queue_length()
    end

    local ok, err: string? = xpcall(setter, debug.traceback)

    if not already_batching then
        flags.batch = false
        graph.flush_update_queue(from)
    end

    if not ok then error(`error occured while batching updates: {err}`, 0) end
end

return batch


local graph = require "./graph"
type Node<T> = graph.Node<T>
local create_node = graph.create_node
local push_scope = graph.push_scope
local pop_scope = graph.pop_scope
local destroy = graph.destroy
local get_scope = graph.get_scope

local function branch<T>(fn: () -> T): (() -> (), T)
    local current = get_scope()
    if not current then
        error(`cannot use branch() outside a stable or reactive scope`, 0)
    end

    local parent = current.owner
    if not parent then
        error(`current scope is not owned by a scope`, 0)
    end

    local node = create_node(parent, false, false)

    local destroy = function()
        destroy(node)
    end

    push_scope(node)

    local ok, result = xpcall(fn, debug.traceback)

    pop_scope()

    if not ok then
        destroy()
        error(`error while running branch():\n\n{result}`, 0)
    end

    return destroy, result
end

return branch


local action = require "./action"()
local cleanup = require "./cleanup"

local function changed<T>(property: string, callback: (T) -> ())
    return action(function(instance)
        local con = instance:GetPropertyChangedSignal(property):Connect(function()
            callback((instance :: any)[property])
        end)

        cleanup(function()
            con:Disconnect()
        end)

        callback((instance :: any)[property])
    end)
end

return changed


local typeof = game and typeof or require "../test/mock".typeof :: never

local graph = require "./graph"
local get_scope = graph.get_scope
local push_cleanup = graph.push_cleanup

local function helper(obj: any)
    return
        if typeof(obj) == "RBXScriptConnection" then function() obj:Disconnect() end
        elseif type(obj) == "thread" then function() task.cancel(obj) end
        elseif typeof(obj) == "Instance" then function() obj:Destroy() end
        elseif obj.destroy then function() obj:destroy() end
        elseif obj.disconnect then function() obj:disconnect() end
        elseif obj.Destroy then function() obj:Destroy() end
        elseif obj.Disconnect then function() obj:Disconnect() end
        else error "cannot cleanup given object"
end

local function cleanup(value: unknown)
    local scope = get_scope()

    if not scope then
        error "cannot cleanup outside a stable or reactive scope"
    end; assert(scope)

    if type(value) == "function" then
        push_cleanup(scope, value :: () -> ())
    else
        push_cleanup(scope, helper(value))
    end
end

type Destroyable = { destroy: (any) -> () } | { Destroy: (any) -> () }
type Disconnectable = { disconnect: (any) -> () } | { Disconnect: (any) -> () }

return cleanup ::
    ( (callback: () -> ()) -> () ) &
    ( (thread: thread) -> () ) &
    ( (instance: Destroyable) -> () ) &
    ( (connection: Disconnectable) -> () ) &
    ( (instance: Instance) -> () ) &
    ( (connection: RBXScriptConnection) -> () )


local graph = require "./graph"
type Node<T> = graph.Node<T>
local create_node = graph.create_node
local get_scope = graph.get_scope
local push_scope = graph.push_scope
local pop_scope = graph.pop_scope
local set_context = graph.set_context

export type Context<T> = (() -> T) & (<U>(T, () -> U) -> U)

local nil_symbol = newproxy()
local count = 0

local function context<T>(...: T): Context<T>
    count += 1
    local id = count

    local has_default = select("#", ...) > 0
    local default_value = ...

    return function<T>(...): any -- todo: fix type error
        local scope: Node<unknown>? | false = get_scope()

        if select("#", ...) == 0 then -- get
            while scope do
                local ctx = scope.context

                if not ctx then
                    scope = scope.owner
                    continue
                end

                local value = (ctx :: { unknown })[id]

                if value == nil then
                    scope = scope.owner
                    continue
                end

                return (if value ~= nil_symbol then value else nil) :: T
            end

            if has_default ~= nil then
                return default_value
            else
                error("attempt to get context when no context is set and no default context is set", 0)
            end
        else -- set
            if not scope then return error("attempt to set context outside of a vide scope", 0) end

            local value, component = ...

            local new_scope = create_node(scope, false, false)
            set_context(new_scope, id, if value == nil then nil_symbol else value)

            push_scope(new_scope)

            local function efn(err: string) return debug.traceback(err, 3) end
            local ok, result = xpcall(component, efn)

            pop_scope()

            if not ok then
                error(`error while running context:\n\n{result}`, 0)
            end

            return result
        end

        return nil :: any
    end
end

return context


local typeof = game and typeof or require "../test/mock".typeof :: never
local Instance = game and Instance or require "../test/mock".Instance :: never

local defaults = require "./defaults"
local apply = require "./apply"
local flags = require "./flags"

local ctor_cache = {} :: { [string]: (AnyProps) -> Instance }
local function lazy_init(_, class: string)
    local function ctor(properties: AnyProps): Instance
        local ok, instance: Instance = pcall(Instance.new, class :: any)
        if not ok then error(`invalid class name { class }`, 0) end

        if flags.defaults then
            local default: { [string]: unknown }? = defaults[class]
            if default then
                for i, v in default do
                    (instance :: any)[i] = v
                end
            end
        end

        return apply(instance, properties)
    end

    ctor_cache[class] = ctor
    return ctor
end
setmetatable(ctor_cache, { __index = lazy_init })

-- todo: remove support for different overloads
local function create(class_or_instance: string|Instance, properties: AnyProps?): ((AnyProps) -> Instance) | Instance
    if type(class_or_instance) ~= "string" and typeof(class_or_instance) ~= "Instance" then
        error("bad argument #1, expected string or instance, got " .. typeof(class_or_instance), 0)
    end

    local ctor = if type(class_or_instance) == "string"
        then ctor_cache[class_or_instance]
        else function(properties)
            local clone = class_or_instance:Clone()
            if not clone then error "attempt to clone a non-archivable instance" end
            return apply(clone, properties)
        end

    return if properties
        then ctor(properties)
        else ctor
end

type Instances = {
    Folder: Folder,
    BillboardGui: BillboardGui,
    CanvasGroup: CanvasGroup,
    Frame: Frame,
    ImageButton: ImageButton,
    ImageLabel: ImageLabel,
    ScreenGui: ScreenGui,
    ScrollingFrame: ScrollingFrame,
    SurfaceGui: SurfaceGui,
    TextBox: TextBox,
    TextButton: TextButton,
    TextLabel: TextLabel,
    UIAspectRatioConstraint: UIAspectRatioConstraint,
    UICorner: UICorner,
    UIGradient: UIGradient,
    UIGridLayout: UIGridLayout,
    UIListLayout: UIListLayout,
    Camera: Camera,
    WorldModel: WorldModel,
}

type function Properties(instance: type?)
    local properties = types.newtable()

    while instance do
        for i, v in instance:properties() do
            local connector = v.read and v.read.tag == "table" and v.read:readproperty(types.singleton("Connect"))
            if connector then
                local params = connector:parameters().head
                if not params then continue end
                local listener = params[2]
                if not listener then continue end
                properties:setproperty(i, types.optional(listener))
            elseif v.write then
                properties:setproperty(i, types.optional(types.unionof(
                    v.write,
                    types.newfunction({}, { head = { v.write } })
                )))
            end
        end

        instance = instance:readparent()
    end

    properties:setindexer(types.number, types.any)

    return properties
end

type AnyProps = { [any]: any }
type Create = (
    (Instance) -> (AnyProps) -> Instance
) & (
    (string, AnyProps) -> Instance
) & (
    <Name>(Name|keyof<Instances>) -> (Properties<index<Instances, Name>>) -> index<Instances, Name>
)

return (create :: any) :: Create


local Enum = game and Enum or require "../test/mock".Enum :: never
local Color3 = game and Color3 or require "../test/mock".Color3 :: never

return {
    Part = {
        Material = Enum.Material.SmoothPlastic,
        Size = vector.create(1, 1, 1),
        Anchored = true
    },

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

    CanvasGroup = nil,

    Frame = {
        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
    },

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

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

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

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

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

    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)
    },

    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)
    },

    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)
    },

    UIListLayout = {
        SortOrder = Enum.SortOrder.LayoutOrder
    },

    UIGridLayout = {
        SortOrder = Enum.SortOrder.LayoutOrder
    },

    UITableLayout = {
        SortOrder = Enum.SortOrder.LayoutOrder
    },

    UIPageLayout = {
        SortOrder = Enum.SortOrder.LayoutOrder
    },

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

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


local graph = require "./graph"
local create_node = graph.create_node
local push_scope_as_child_of = graph.push_scope_as_child_of
local assert_stable_scope = graph.assert_stable_scope
local evaluate_node = graph.evaluate_node

local function derive<T>(source: () -> T): () -> T
    local node = create_node(assert_stable_scope(), source, false :: any)

    evaluate_node(node)

    return function()
        push_scope_as_child_of(node)
        return node.cache
    end
end

return derive


local graph = require "./graph"
local create_node = graph.create_node
local assert_stable_scope = graph.assert_stable_scope
local evaluate_node = graph.evaluate_node

local function effect<T>(callback: (T) -> T, initial_value: T)
    local node = create_node(assert_stable_scope(), callback, initial_value)

    evaluate_node(node)
end

return effect :: (<T>(callback: (T) -> T, initial_value: T) -> ()) & ((callback: () -> ()) -> ())


local function inline_test(): string
    return debug.info(1, "n")
end

local is_O2 = inline_test() ~= "inline_test"

return {
    strict = not is_O2,
    defaults = true,
    defer_nested_properties = true,
    batch = false,
}


local flags = require "./flags"

export type SourceNode<T> = {
    cache: T,
    [number]: Node<T>
}

export type Node<T> =  {
    cache: T,
    effect:  ((T) -> T) | false,
    cleanups: { () -> () } | false,

    context: { [number]: unknown } | false,

    owned: { Node<T> } | false,
    owner: Node<T> | false,

    parents: { SourceNode<T> },
    [number]: Node<T> -- children
}

local scopes = { n = 0 } :: { [number]: Node<any>, n: number } -- scopes stack

local function efn(err: string)
    local trace = debug.traceback(err, 2)

    if string.find(err, "^effect error stacktrace") then -- if effect error is nested
        trace = string.gsub("    " .. trace, "\n", function() -- indent entire error
            return "\n    "
        end)
    end

    trace ..= "\nsource update stacktrace:"
return trace
end

local function ycall<T, U>(fn: (T) -> U, arg: T): (boolean, string|U)

    local thread = coroutine.create(xpcall)
    --local function efn(err: string) return debug.traceback(err, 3) end
    local resume_ok, run_ok, result = coroutine.resume(thread, fn, efn, arg)

    assert(resume_ok)

    if coroutine.status(thread) ~= "dead" then
        return false, debug.traceback(thread, "attempt to yield in reactive scope")
    end

    return run_ok, result
end

local function get_scope(): Node<unknown>?
    return scopes[scopes.n]
end

local function assert_stable_scope(): Node<unknown>
    local scope = get_scope()

    if not scope then
        local caller_name = debug.info(2, "n")
        return error(`cannot use {caller_name}() outside a stable or reactive scope`, 0)
    elseif scope.effect then
        error("cannot create a new reactive scope inside another reactive scope", 0)
    end

    return scope
end

local function push_child<T>(parent: SourceNode<any>, child: Node<any>)
    table.insert(parent, child)
    table.insert(child.parents, parent)
end

local function push_scope<T>(node: Node<T>)
    local n = scopes.n + 1
    scopes.n = n
    scopes[n] = node
end

local function pop_scope()
    local n = scopes.n
    scopes.n = n - 1
    scopes[n] = nil
end

local function push_cleanup<T>(node: Node<T>, cleanup: () -> ())
    if node.cleanups then
        table.insert(node.cleanups, cleanup)
    else
        node.cleanups = { cleanup }
    end
end

local function flush_cleanups<T>(node: Node<T>)
    if node.cleanups then
        for _, fn in node.cleanups do
            local ok, err: string? = xpcall(fn, debug.traceback)
            if not ok then error(`cleanup error: {err}`, 0) end
        end

        table.clear(node.cleanups)
    end
end

local function find_and_swap_pop<T>(t: { T }, v: T)
    local i = table.find(t, v) :: number
    local n = #t
    t[i] = t[n]
    t[n] = nil
end

local function unparent<T>(node: Node<T>)
    local parents = node.parents

    for i, parent in parents do
        find_and_swap_pop(parent, node)
        parents[i] = nil
    end
end

local function destroy<T>(node: Node<T>)
    if flags.strict and table.find(scopes, node) then
        error("attempt to destroy an active scope", 0)
    end

    flush_cleanups(node)
    unparent(node)

    if node.owner then
        find_and_swap_pop(node.owner.owned :: { Node<T> }, node)
        node.owner = false
    end

    if node.owned then
        local owned = node.owned
        while owned[1] do destroy(owned[1]) end
    end
end

local function destroy_owned<T>(node: Node<T>)
    if node.owned then
        local owned = node.owned
        while owned[1] do destroy(owned[1]) end
    end
end

local update_queue = { n = 0 } :: { n: number, [number]: Node<any> }

local function evaluate_node<T>(node: Node<T>)
    if flags.strict then
        if table.find(scopes, node) then
            error("a scope, that should rerun due to the update of a source, is already active", 0)
        end

        local initial_value = node.cache

        for i = 1, 2 do
            local cur_value = node.cache

            flush_cleanups(node)
            destroy_owned(node)

            push_scope(node)
            local ok, new_value = ycall(node.effect :: (T) -> T, cur_value)
            pop_scope()

            if not ok then
                table.clear(update_queue)
                update_queue.n = 0
                error(`effect error stacktrace\n{new_value :: string}`, 0)
            end

            node.cache = new_value :: T
        end

        return initial_value ~= node.cache
    else
        local cur_value = node.cache

        flush_cleanups(node)
        destroy_owned(node)

        push_scope(node)
        local ok, new_value = pcall(node.effect :: (T) -> T, node.cache)
        pop_scope()

        if not ok then
            table.clear(update_queue)
            update_queue.n = 0
            error(`effect error:\n{new_value}\n`, 0)
        end

        node.cache = new_value
        return cur_value ~= new_value
    end
end

local function queue_children_for_update<T>(node: SourceNode<T>)
    local i = update_queue.n
    while node[1] do
        i += 1
        update_queue[i] = node[1]
        unparent(node[1])
    end
    update_queue.n = i
end

local function get_update_queue_length()
    return update_queue.n
end

local function flush_update_queue(from: number)
    local i = from + 1
    while i <= update_queue.n do
        local node = update_queue[i]
        --assert(node.effect)

        if node.owner and evaluate_node(node) then
            queue_children_for_update(node)
        end

        update_queue[i] = false :: any
        i += 1
    end

    update_queue.n = from
end

local function update_descendants<T>(root: SourceNode<T>)
    local n0 = update_queue.n
    queue_children_for_update(root)

    if flags.batch then return end

    local i = n0 + 1
    while i <= update_queue.n do
        local node = update_queue[i]
        --assert(node.effect)

        -- check if node is still owned in case destroyed after queued
        if node.owner and evaluate_node(node) then
            queue_children_for_update(node)
        end

        update_queue[i] = false :: any -- false instead of nil to avoid sparse
        i += 1
    end

    update_queue.n = n0
end

local function push_scope_as_child_of<T>(node: SourceNode<T>)
    local scope = get_scope()
    if scope and scope.effect then -- do not track nodes with no effect
        push_child(node, scope)
    end
end

local function create_node<T>(owner: false | Node<any>, effect: false | (T) -> T, value: T): Node<T>
    local node: Node<T> = {
        cache = value,
        effect = effect,
        cleanups = false,

        context = false,

        owner = owner,
        owned = false,

        parents = {},
    }

    if owner then
        if owner.owned then
            table.insert(owner.owned, node)
        else
            owner.owned = { node }
        end
    end

    return node
end

local function create_source_node<T>(value: T): SourceNode<T>
    return { cache = value }
end

local function get_children<T>(node: Node<T>): { Node<unknown> }
    return { unpack(node) } :: { Node<any> }
end

local function set_context<T>(node: Node<T>, key: number, value: unknown)
    if node.context then
        node.context[key] = value
    else
        node.context = { [key] = value }
    end
end

return table.freeze {
    push_scope = push_scope,
    pop_scope = pop_scope,
    evaluate_node = evaluate_node,
    get_scope = get_scope,
    assert_stable_scope = assert_stable_scope,
    push_cleanup = push_cleanup,
    destroy = destroy,
    flush_cleanups = flush_cleanups,
    push_scope_as_child_of = push_scope_as_child_of,
    update_descendants = update_descendants,
    push_child = push_child,
    create_node = create_node,
    create_source_node = create_source_node,
    get_children = get_children,
    flush_update_queue = flush_update_queue,
    get_update_queue_length = get_update_queue_length,
    set_context = set_context,
    scopes = scopes,

    q = update_queue
}


local graph = require "./graph"
type Node<T> = graph.Node<T>
local create_node = graph.create_node
local assert_stable_scope = graph.assert_stable_scope
local get_scope = graph.get_scope
local evaluate_node = graph.evaluate_node
local push_cleanup = graph.push_cleanup

local function update_property_effect(p: {
    instance: Instance,
    property: string,
    source: () -> unknown
})
    (p.instance :: any)[p.property] = p.source()
    return p
end

local function update_parent_effect(p: {
    instance: Instance,
    source: () -> Instance
})
    p.instance.Parent = p.source()
    return p
end

-- todo: investigate if "count" method used in indexes() and values() can improve performance here
local function update_children_effect(p: {
    instance: Instance,
    cur_children_set: { [Instance]: true },
    new_children_set: { [Instance]: true },
    source: () -> Instance | { Instance }
})
    local cur_children_set: { [Instance]: true } = p.cur_children_set -- cache of all children parented before update
    local new_children_set: { [Instance]: true } = p.new_children_set -- cache of all children parented after update

    local new_children = p.source() -- all (and only) children that should be parented after this update

    local function process_child(child: Instance | { Instance })
        if type(child) == "userdata" then
            if new_children_set[child] then return end -- stops redundant reparenting

            new_children_set[child] = true -- record child set from this update

            if not cur_children_set[child] then
                child.Parent = p.instance -- if child wasn't already parented then parent it
            else
                cur_children_set[child] = nil -- remove child from cache if it was already in cache
            end
        elseif type(child) == "table" then
            for _, child in child do
                process_child(child)
            end
        elseif type(child) == "function" then
            local node = create_node(assert(get_scope()), update_children_effect, {
                instance = p.instance,
                cur_children_set = {},
                new_children_set = {},
                source = child
            })

            evaluate_node(node)

            push_cleanup(assert(get_scope()), function()
                for child in node.cache.cur_children_set do
                    child.Parent = nil
                end
            end)
        end
    end

    process_child(new_children)

    for child in cur_children_set do
        child.Parent = nil -- unparent all children that weren't in the new children set
    end

    table.clear(cur_children_set) -- clear cache, preserve capacity
    p.cur_children_set, p.new_children_set = new_children_set, cur_children_set

    return p
end

return {
    property = function(instance, property, source)
        local node = create_node(assert_stable_scope(), update_property_effect, {
            instance = instance,
            property = property,
            source = source
        })

        evaluate_node(node)

        return node
    end,

    parent = function(instance, parent)
        local node = create_node(assert_stable_scope(), update_parent_effect, {
            instance = instance,
            source = parent
        })

        evaluate_node(node)

        return node
    end,

    children = function(instance, children)
        local node = create_node(assert_stable_scope(), update_children_effect, {
            instance = instance,
            cur_children_set = {},
            new_children_set = {},
            source = children
        })

        evaluate_node(node)

        push_cleanup(assert_stable_scope(), function()
            for child in node.cache.cur_children_set do
                child.Parent = nil
            end
        end)

        return node
    end
}


local flags = require "./flags"
local branch = require "./branch"
local source = require "./source"
local effect = require "./effect"
local timeout = require "./timeout" ()

type Array<T> = { T }
type Map<K, V> = { [K]: V }
type Source<T> = () -> T

local function indexes<K, V, Obj>(
    input: Source<Map<K, V>>,
    component: (Source<V>, K, Source<boolean>) -> (Obj, ...number)
): Source<Array<Obj>>
    local update_count = 0
    local scopes = {} :: Map<K, {
        destroy: () -> (),
        object: Obj,
        value: V?,
        value_source: (V?) -> V,
        count: number,
        delay: number,
        present: (boolean?) -> boolean,
        timeout: { cancel: boolean }?,
    }>

    local output = source({} :: Array<Obj>)
    local function update_output()
        local objects = table.create(4)

        for _, scope in scopes do
            table.insert(objects, scope.object)
        end

        output(objects)
    end

    effect(function()
        local data = input()

        local count = update_count
        update_count += 1

        local children_need_update = false -- set to true if a scope is created or destroyed

        -- create or update scopes
        for i, v in data do
            local scope = scopes[i]

            if scope == nil then -- create new scope and create component
                local value_source = source(v)
                local present = source(false)

                local delay = nil :: number?
                local destroy, object = branch(function()
                    local object, t = component(value_source, i, present)
                    delay = t
                    return object
                end)

                present(true)

                children_need_update = true

                scopes[i] = {
                    destroy = destroy,
                    object = object,
                    value = v,
                    value_source = value_source,
                    count = count,
                    delay = delay or 0,
                    present = present,
                    timeout = nil,
                }
            else -- update scope
                scope.count = count

                if scope.value ~= v then
                    if scope.timeout then -- index is in input table again; cancel destruction
                        scope.timeout.cancel = true
                        scope.timeout = nil
                        scope.present(true)
                    end

                    scope.value = v
                    scope.value_source(v)
                end
            end
        end

        -- destroy scopes
        for i, scope in scopes do
            if scope.count < count then -- if count is not latest then index is no longer in the input table
                scope.present(false)

                if scope.delay == 0 then
                    scope.destroy()
                    scopes[i] = nil
                    children_need_update = true
                else
                    scope.value = nil -- set to nil for the `scope.value ~= v` check
                    if scope.timeout == nil then
                        scope.timeout = timeout(scope.delay, function() -- todo: possible redundant updates
                            scope.destroy()
                            scopes[i] = nil
                            update_output()
                        end)
                    end
                end
            end
        end

        if children_need_update then
            update_output()
        end
    end)

    return output
end

return indexes


assert(game, "when using vide outside of Roblox, require lib.luau instead")

local vide = require(script.lib)

export type source<T> = vide.source<T>
export type Source<T> = vide.Source<T>
export type context<T> = vide.context<T>
export type Context<T> = vide.Context<T>

return vide


local version = { major = 0, minor = 4, patch = 0 }

local root = require "./root"
local branch = require "./branch"
local mount = require "./mount"
local create = require "./create"
local apply = require "./apply"
local source = require "./source"
local effect = require "./effect"
local derive = require "./derive"
local cleanup = require "./cleanup"
local untrack = require "./untrack"
local read = require "./read"
local batch = require "./batch"
local context = require "./context"
local switch = require "./switch"
local show = require "./show"
local indexes = require "./indexes"
local values = require "./values"
local spring, update_springs = require "./spring"()
local action = require "./action"()
local changed = require "./changed"
local timeout, update_timeouts = require "./timeout"()
local flags = require "./flags"

export type Source<T> = source.Source<T>
export type source<T> = Source<T>
export type Context<T> = context.Context<T>
export type context<T> = Context<T>

local function step(dt: number)
    if game then debug.profilebegin("VIDE STEP") end

    if game then debug.profilebegin("VIDE SPRING") end
    update_springs(dt)
    if game then debug.profileend() end

    if game then debug.profilebegin("VIDE SCHEDULER") end
    update_timeouts(dt)
    if game then debug.profileend() end

    if game then debug.profileend() end
end

local stepped = game and game:GetService("RunService").Heartbeat:Connect(function(dt: number)
    task.defer(step, dt)
end)

local vide = {
    version = version,

    -- core
    root = root,
    --branch = branch,
    mount = mount,
    create = create,
    source = source,
    effect = effect,
    derive = derive,
    switch = switch,
    show = show,
    indexes = indexes,
    values = values,

    -- util
    cleanup = cleanup,
    untrack = untrack,
    read = read,
    batch = batch,
    context = context,

    -- animations
    spring = spring,

    -- actions
    action = action,
    changed = changed,

    -- flags
    strict = (nil :: any) :: boolean,
    defaults = (nil :: any) :: boolean,
    defer_nested_properties = (nil :: any) :: boolean,

    -- temporary
    apply = function(instance: Instance)
        return function(props: { [any]: any })
            apply(instance, props)
            return instance
        end
    end,

    -- runtime
    step = function(dt: number)
        if stepped then
            stepped:Disconnect()
            stepped = nil
        end
        step(dt)
    end
}

setmetatable(vide :: any, {
    __index = function(_, index: unknown): ()
        if flags[index] == nil then
            error(`{tostring(index)} is not a valid member of vide`, 0)
        else
            return flags[index]
        end
    end,

    __newindex = function(_, index: unknown, value: unknown)
        if flags[index] == nil then
            error(`{tostring(index)} is not a valid member of vide, 0`)
        else
            flags[index] = value
        end
    end
})

return vide


local root = require "./root"
local apply = require "./apply"

local function mount<T>(component: () -> T, target: Instance?): () -> ()
    return root(function()
        local result = component()
        if target then apply(target, { result }) end
    end)
end

return mount :: (<T>(component: () -> T, target: Instance) -> () -> ()) & ((component: () -> ()) -> () -> ())


local function read<T>(value: T | () -> T): T
    return if type(value) == "function" then value() else value
end

return read


local graph = require "./graph"
type Node<T> = graph.Node<T>
local create_node = graph.create_node
local push_scope = graph.push_scope
local pop_scope = graph.pop_scope
local destroy = graph.destroy

local refs = {}

local function root<T...>(fn: (destroy: () -> ()) -> T...): (() -> (), T...)
    local node = create_node(false, false, false)

    refs[node] = true -- prevent gc of root node

    local destroy = function()
        if not refs[node] then error "root already destroyed" end
        refs[node] = nil
        destroy(node)
    end

    push_scope(node)

    local result = { xpcall(fn, debug.traceback, destroy) }

    pop_scope()

    if not result[1] then
        destroy()
        error(`error while running root():\n\n{result[2]}`, 0)
    end

    return destroy, unpack(result :: any, 2)
end

return root :: <T...>(fn: (destroy: () -> ()) -> T...) -> (() -> (), T...)


local source = require "./source"
local derive = require "./derive"
local effect = require "./effect"
local untrack = require "./untrack"
local switch = require "./switch"

type Array<T> = { T }
type Source<T> = () -> T

local function show<T, Obj>(
    input: Source<T?>,
    component: (Source<T>, Source<boolean>) -> (Obj, ...number),
    fallback: ((Source<boolean>) -> (Obj, ...number))?
): Source<nil | Obj | Array<Obj>>
    local filtered_input = source()

    effect(function()
        local v = input()
        if v then
            filtered_input(v)
        end
    end)

    local input_is_truthy = derive(function()
        return not not input()
    end)

    return switch(input_is_truthy) {
        [true] = function(present)
            return component(filtered_input, present)
        end,
        [false] = fallback
    }
end

return show


local graph = require "./graph"
type Node<T> = graph.Node<T>
local create_source_node = graph.create_source_node
local push_scope_as_child_of = graph.push_scope_as_child_of
local update_descendants = graph.update_descendants

export type Source<T> = (() -> T) & ((value: T) -> T)

local function source<T>(initial_value: T): Source<T>
    local node = create_source_node(initial_value)

    local function update_source(...): T
        if select("#", ...) == 0 then -- no args were given
            push_scope_as_child_of(node)
            return node.cache
        end

        local v = ... :: T
        if node.cache == v and (type(v) ~= "table" or table.isfrozen(v)) then
            return v
        end

        node.cache = v
        update_descendants(node)
        return v
    end

    return update_source
end

return source :: (<T>(initial_value: T) -> Source<T>) & (<T>() -> Source<T>)


local branch = require "./branch"
local source = require "./source"
local effect = require "./effect"
local timeout = require "./timeout" ()

type Array<T> = { T }
type Map<K, V> = { [K]: V }
type Source<T> = () -> T
type Component<T> = (Source<boolean>) -> (T, ...number)

local function switch_map<K, Obj>(
    input: Source<K>,
    map: Map<K, Component<Obj>>
): Source<nil | Obj | Array<Obj>>
    local scopes = {} :: Map<K, {
        destroy: () -> (),
        object: Obj,
        delay: number,
        present: (boolean?) -> boolean,
        timeout: { cancel: boolean }?
    }>

    local output = source(nil :: nil | Obj | Array<Obj>)
    local function update_output()
        local objects = {}

        for _, scope in scopes do
            table.insert(objects, scope.object)
        end

        output(
            if objects[2] then objects
            elseif objects[1] then objects[1]
            else nil
        )
    end

    effect(function()
        local key: K? = input()

        -- destroy (or queue destroy) all scopes not associated with the input key
        for k, scope in scopes do
            if k == key then continue end

            scope.present(false)

            if scope.delay == 0 then
                scope.destroy()
                scopes[k] = nil
            else
                if scope.timeout == nil then
                    scope.timeout = timeout(scope.delay, function()
                        scope.destroy()
                        scopes[k] = nil
                        update_output()
                    end)
                end
            end
        end

        -- create new scope or abort destruction of existing scope if key exists
        if key ~= nil then
            local scope = scopes[key]

            if scope then
                scope.present(true)

                if scope.timeout then
                    scope.timeout.cancel = true
                    scope.timeout = nil
                end
            else
                local component = map[key]

                if component ~= nil then
                    if type(component) ~= "function" then
                        error("map must map a value to a function", 0)
                    end

                    local present = source(false)

                    local delay = nil :: number?
                    local destroy, object = branch(function()
                        local object, t = component(present)
                        delay = t
                        return object
                    end)

                    present(true)

                    scopes[key] = {
                        destroy = destroy,
                        object = object,
                        delay = delay or 0,
                        present = present,
                        timeout = nil
                    }
                end
            end
        end

        update_output()
    end)

    return output
end

local function switch<K, Obj>(input: Source<K>): (map: Map<K, Component<Obj>>) -> Source<nil | Obj | Array<Obj>>
    return function(map)
        return switch_map(input, map)
    end
end

return switch


local queue = {} :: {
    { t: number, fn: () -> (), cancel: boolean }
}

local function timeout(t: number, fn: () -> ())
    local handle = { t = t, fn = fn, cancel = false }
    table.insert(queue, handle)
    return handle
end

local function update_timeouts(dt: number)
    for i = #queue, 1, -1 do
        local handle = queue[i]
        handle.t -= dt

        if handle.cancel or handle.t <= 0 then
            queue[i] = queue[#queue]
            queue[#queue] = nil

            if not handle.cancel then
                handle.fn()
            end
        end
    end
end

return function() return timeout, update_timeouts end


local graph = require "./graph"
type Node<T> = graph.Node<T>
local get_scope = graph.get_scope

local function untrack<T>(source: () -> T): T
    local scope = get_scope()

    if scope then
        -- sources are only tracked if the node in scope has an effect
        local effect = scope.effect
        scope.effect = false

        local ok, result = xpcall(source, debug.traceback)

        scope.effect = effect :: () -> ()

        if not ok then error(result, 0) end

        return result :: T
    else
        return source()
    end
end

return untrack :: ( <T>(fn: () -> T) -> T ) & ( (fn: () -> ()) -> () )


local flags = require "./flags"
local branch = require "./branch"
local source = require "./source"
local effect = require "./effect"
local timeout = require "./timeout" ()

type Array<T> = { T }
type Map<K, V> = { [K]: V }
type Source<T> = () -> T

local function values<K, V, Obj>(
    input: Source<Map<K, V>>,
    component: (V, Source<K>, Source<boolean>) -> (Obj, ...number)
): Source<Array<Obj>>
    local update_count = 0
    local scopes = {} :: Map<V, {
        destroy: () -> (),
        object: Obj,
        index: K?,
        index_source: (K?) -> K,
        count: number,
        delay: number,
        present: (boolean?) -> boolean,
        timeout: { cancel: boolean }?,
    }>

    local output = source({} :: Array<Obj>)
    local function update_output()
        local objects = table.create(4)

        for _, scope in scopes do
            table.insert(objects, scope.object)
        end

        output(objects)
    end

    effect(function()
        local data = input()

        local count = update_count
        update_count += 1

        local children_need_update = false -- set to true if a scope is created or destroyed

        if flags.strict then -- check for duplicate values
            local map = {}
            for _, v in data do
                if map[v] then
                    error("table source passed to `values()` contains duplicate values", 0)
                end
                map[v] = true
            end
        end

        -- create or update scopes
        for i, v in data do
            local scope = scopes[v]

            if scope == nil then -- create new scope and create component
                local index_source = source(i)
                local present = source(false)

                local delay = nil :: number?
                local destroy, object = branch(function()
                    local object, t = component(v, index_source, present)
                    delay = t
                    return object
                end)

                present(true)

                children_need_update = true

                scopes[v] = {
                    destroy = destroy,
                    object = object,
                    index = i,
                    index_source = index_source,
                    count = count,
                    delay = delay or 0,
                    present = present,
                    timeout = nil,
                }
            else -- update scope
                scope.count = count

                if scope.index ~= i then
                    if scope.timeout then -- value is in input table again; cancel destruction
                        scope.timeout.cancel = true
                        scope.timeout = nil
                        scope.present(true)
                    end

                    scope.index = i
                    scope.index_source(i)
                end
            end
        end

        -- destroy scopes
        for v, scope in scopes do
            if scope.count < count then -- if count is not latest then value is no longer in the input table
                scope.present(false)

                if scope.delay == 0 then
                    scope.destroy()
                    scopes[v] = nil
                    children_need_update = true
                else
                    scope.index = nil -- set to nil for the `scope.index ~= i` check
                    if scope.timeout == nil then
                        scope.timeout = timeout(scope.delay, function() -- todo: possible redundant updates
                            scope.destroy()
                            scopes[v] = nil
                            update_output()
                        end)
                    end
                end
            end
        end

        if children_need_update then
            update_output()
        end
    end)

    return output
end

return values