type ExistingOptions = {
	rawStrings: boolean?,
}

local isPrimitiveType = { string = true, number = true, boolean = true }

local typeSortOrder: { [string]: number } = {
	["boolean"] = 1,
	["number"] = 2,
	["string"] = 3,
	["function"] = 4,
	["vector"] = 5,
	["buffer"] = 6,
	["thread"] = 7,
	["table"] = 8,
	["userdata"] = 9,
	["nil"] = 10,
}

local function isPrimitiveArray(array: { [unknown]: unknown }): boolean
	local max, len = 0, #array

	for key, value in array do
		if type(key) ~= "number" then
			return false
		elseif key <= 0 then
			return false
		-- userdatas arent primitives
		elseif not isPrimitiveType[type(value)] then
			return false
		end

		max = math.max(key, max)
	end

	return len == max
end

local function getFormattedAdress(t: {}): string
	return `table<({t})>`
end

local function formatValue(value: unknown, options: ExistingOptions?): string
	if type(value) == "table" then
		return getFormattedAdress(value) -- simple representation for table values
	elseif type(value) ~= "string" then
		return tostring(value)
	end

	if options and options.rawStrings then
		return `"{value}"`
	end
	return string.format("%q", value)
end

local function formatKey(key: unknown, seq: boolean): string
	if seq then
		return ""
	end

	if type(key) == "table" then
		return `[{getFormattedAdress(key)}] = ` -- TODO: handling for table keys
	end
	if type(key) ~= "string" then
		return `[{tostring(key)}] =`
	end

	-- key is a simple identifier
	if string.match(key, "^[%a_][%w_]-$") == key then
		return `{key} = `
	end

	return `[{string.format("%q", key)}] = `
end

local function isEmpty(t: { [unknown]: unknown }): boolean
	for _ in t do
		return false
	end
	return true
end

-- FIXME(luau): mark `dataTable` indexer as read-only
local function traverseTable(
	dataTable: { [unknown]: unknown },
	seen: { [unknown]: boolean },
	indent: number,
	options: ExistingOptions?
): string
	local output = ""
	local indentStr = string.rep("  ", indent)

	local keys = {}

	-- Collect all keys, not just primitives
	for key in dataTable do
		table.insert(keys, key)
	end

	table.sort(keys :: any, function(a: string, b: string): boolean
		local typeofTableA, typeofTableB = typeof(dataTable[a]), typeof(dataTable[b])

		if typeofTableA ~= typeofTableB then
			return typeSortOrder[typeofTableA] < typeSortOrder[typeofTableB]
		end

		if type(a) == "number" and type(b) == "number" then
			return a < b
		end

		return tostring(a) < tostring(b)
	end)

	local inSequence = false
	local previousKey = 0

	for _, key in keys do
		if type(key) == "number" and key > 0 and key - 1 == previousKey then
			previousKey = key
			inSequence = true
		else
			inSequence = false
		end

		local value = dataTable[key]

		if type(value) ~= "table" then
			output = `{output}{indentStr}{formatKey(key, inSequence)}{formatValue(value, options)},\n`
			continue
		end

		-- prevents self-referential tables from looping infinitely
		if seen[value] then
			output = `{output}{indentStr}{formatKey(key, inSequence)}[Circular Reference <({value})>],\n`
			continue
		else
			seen[value] = true
		end

		if isEmpty(value :: { [unknown]: unknown }) then
			output = string.format("%s%s%s{},\n", output, indentStr, formatKey(key, inSequence))
			continue
		end

		if isPrimitiveArray(value :: { [unknown]: unknown }) then -- collapse primitive arrays
			local outputConcatTbl = table.create(#value) :: { string }

			for valueIndex, valueInArray in value :: { unknown } do
				outputConcatTbl[valueIndex] = formatValue(valueInArray)
			end

			output = string.format(
				"%s%s%s{%*},\n",
				output,
				indentStr,
				formatKey(key, inSequence),
				table.concat(outputConcatTbl, ", ")
			)
			continue
		end

		output = string.format(
			"%s%s%s{\n%s%s},\n",
			output,
			indentStr,
			formatKey(key, inSequence),
			traverseTable(value :: any, seen, indent + 1, options),
			indentStr
		)

		seen[value] = nil
	end

	return output
end

return function(data: unknown, options: ExistingOptions?): string
	options = options or {} :: ExistingOptions

	-- if it's not a primitive, we'll pretty print it as a value
	if type(data) ~= "table" then
		return formatValue(data, options)
	end

	return `\{\n{traverseTable(data :: { [unknown]: unknown }, { [data] = true }, 1, options)}\}`
end


--#selene: allow(bad_string_escape, shadowing)
--[[
	richterm
	terminal ansi formatter
]]

export type Formatter = (s: string, nocolor: boolean?) -> string

-- @noinline (if it ever exists)
local function format(s: string, open: string, close: string, replace: string): string
	local index = string.find(s, close, #open + 1, true)
	local middle = s

	if index then
		local closeLen = #close
		local middleTbl = {}
		local cursor = 1

		while index do
			table.insert(middleTbl, string.sub(s, cursor, index - 1))
			cursor = index + closeLen
			index = string.find(s, close, cursor, true)
		end

		middle = table.concat(middleTbl, replace)
	end

	return open .. middle .. close
end

local function formatter(open: string, close: string, replace: string?): Formatter
	replace = replace or open

	return function(s, nocolor)
		return if nocolor then s else format(s, open, close, replace :: string)
	end
end

local richterm = {
	reset = formatter("\x1b[0m", "\x1b[0m"),
	bold = formatter("\x1b[1m", "\x1b[22m", "\x1b[22m\x1b[1m"),
	dim = formatter("\x1b[2m", "\x1b[22m", "\x1b[22m\x1b[2m"),
	italic = formatter("\x1b[3m", "\x1b[23m"),
	underline = formatter("\x1b[4m", "\x1b[24m"),
	inverse = formatter("\x1b[7m", "\x1b[27m"),
	hidden = formatter("\x1b[8m", "\x1b[28m"),
	strikethrough = formatter("\x1b[9m", "\x1b[29m"),

	black = formatter("\x1b[30m", "\x1b[39m"),
	red = formatter("\x1b[31m", "\x1b[39m"),
	green = formatter("\x1b[32m", "\x1b[39m"),
	yellow = formatter("\x1b[33m", "\x1b[39m"),
	blue = formatter("\x1b[34m", "\x1b[39m"),
	magenta = formatter("\x1b[35m", "\x1b[39m"),
	cyan = formatter("\x1b[36m", "\x1b[39m"),
	white = formatter("\x1b[37m", "\x1b[39m"),
	gray = formatter("\x1b[90m", "\x1b[39m"),

	bgBlack = formatter("\x1b[40m", "\x1b[49m"),
	bgRed = formatter("\x1b[41m", "\x1b[49m"),
	bgGreen = formatter("\x1b[42m", "\x1b[49m"),
	bgYellow = formatter("\x1b[43m", "\x1b[49m"),
	bgBlue = formatter("\x1b[44m", "\x1b[49m"),
	bgMagenta = formatter("\x1b[45m", "\x1b[49m"),
	bgCyan = formatter("\x1b[46m", "\x1b[49m"),
	bgWhite = formatter("\x1b[47m", "\x1b[49m"),

	brightBlack = formatter("\x1b[90m", "\x1b[39m"),
	brightRed = formatter("\x1b[91m", "\x1b[39m"),
	brightGreen = formatter("\x1b[92m", "\x1b[39m"),
	brightYellow = formatter("\x1b[93m", "\x1b[39m"),
	brightBlue = formatter("\x1b[94m", "\x1b[39m"),
	brightMagenta = formatter("\x1b[95m", "\x1b[39m"),
	brightCyan = formatter("\x1b[96m", "\x1b[39m"),
	brightWhite = formatter("\x1b[97m", "\x1b[39m"),

	bgBrightBlack = formatter("\x1b[100m", "\x1b[49m"),
	bgBrightRed = formatter("\x1b[101m", "\x1b[49m"),
	bgBrightGreen = formatter("\x1b[102m", "\x1b[49m"),
	bgBrightYellow = formatter("\x1b[103m", "\x1b[49m"),
	bgBrightBlue = formatter("\x1b[104m", "\x1b[49m"),
	bgBrightMagenta = formatter("\x1b[105m", "\x1b[49m"),
	bgBrightCyan = formatter("\x1b[106m", "\x1b[49m"),
	bgBrightWhite = formatter("\x1b[107m", "\x1b[49m"),
}

function richterm.combine(...: Formatter): Formatter
	local formatters = { ... }

	return function(s, nocolor)
		if not nocolor then
			for _, formatter in formatters do
				s = formatter(s)
			end
		end

		return s
	end
end

return table.freeze(richterm)


local Summon = require("@lib/Summon")

do
	local build = Summon.new(
		"lute",
		"run",
		"build",
		"model",
		"--mark-attribution",
		"--mark-language-mode strict",
		"--mark-optimization-level 2"
	)

	build:assert()
end


local config = require("@lib/config")()
local fs = require("@std/fs")
local path = require("@std/path")
local process = require("@lute/process")

local function removeSafe<Args...>(remover: (string, Args...) -> (), filepath: string, ...: Args...)
	if fs.exists(filepath) then
		remover(filepath, ...)
	end
end

do
	local cwd = process.cwd()
	removeSafe(fs.removedirectory, path.join(cwd, config.build.output), { recursive = true })
	removeSafe(fs.remove, path.join(cwd, config.sourcemap.filename))
end


local fs = require("@std/fs")
local path = require("@std/path")
local process = require("@lute/process")

local PESDE_ARTIFACTS: { [string]: (string) -> () } = {
	luau_packages = fs.removedirectory,
	lune_packages = fs.removedirectory,
	roblox_packages = fs.removedirectory,

	["pesde.lock"] = fs.remove,
}

do
	local cwd = process.cwd()
	for artifact, remover in PESDE_ARTIFACTS do
		local artifactPath = path.join(cwd, artifact)
		if fs.exists(artifactPath) then
			remover(artifactPath, { recursive = true })
		end
	end
end


local Summon = require("@lib/Summon")
local process = require("@lute/process")

do
	process.exit(Summon.codeFromMany(Summon.new("lute", "clean-build"), Summon.new("lute", "clean-pkgs")))
end


local Summon = require("@lib/Summon")
local fs = require("@std/fs")
local path = require("@std/path")
local process = require("@lute/process")
local system = require("@lute/system")

do
	local now = os.time()
	Summon.new("lute", "run", "build-release"):assert()

	if system.os == "Darwin" then
		local model = path.join(process.cwd(), "Rocket.rbxm")
		local timestamp = os.date("(%B %d, %Y at %I:%M:%S %p)", now)
		local filename = `Rocket Pre-Alpha {timestamp}.rbxm`
		local destination = path.join(process.homedir(), "Downloads", filename)

		fs.copy(model, destination)
		print("Exported to " .. path.format(destination))
		process.exit(0)
	else
		print("Unsupported operating system: " .. system.os)
		process.exit(1)
	end
end


local cli = require("@batteries/cli")
local process = require("@lute/process")
local richterm = require("@batteries/richterm")
local stringext = require("@std/stringext")

local Summon = {}
Summon.__index = Summon

export type Summon = setmetatable<{
	program: string,
	arguments: { string },
	cwd: string,
	env: { [string]: string },
	stdio: process.StdioKind,
}, typeof(Summon)>

function Summon.withArg(self: Summon, arg: string?)
	if arg then
		table.insert(self.arguments, arg)
	end
	return self
end

function Summon.withCliFlag(self: Summon, args: cli.Parser, flag: string)
	if args:has(flag) then
		table.insert(self.arguments, `--{flag}`)
	end
	return self
end

function Summon.withCliOption(self: Summon, args: cli.Parser, option: string)
	local value = args:get(option)
	if value then
		table.insert(self.arguments, `--{option}={value}`)
	end
	return self
end

function Summon.withCwd(self: Summon, cwd: string)
	self.cwd = cwd
	return self
end

function Summon.withEnv(self: Summon, key: string, value: string?)
	self.env[key] = value
	return self
end

function Summon.withStdio(self: Summon, stdio: process.StdioKind)
	self.stdio = stdio
	return self
end

function Summon.getCommand(self: Summon)
	return `{self.program} {table.concat(self.arguments, " ")}`
end

function Summon.printErr(self: Summon, result: process.ProcessResult)
	print(stringext.trim(result.stdout .. result.stderr))
	print(`Command {self.program} failed with code {result.exitcode}`)
end

function Summon.run(self: Summon)
	local command = self:getCommand()
	print(richterm.black(`> {command}`))
	return process.system(command, {
		cwd = self.cwd,
		env = self.env,
		stdio = self.stdio,
	})
end

function Summon.assert(self: Summon)
	local result = self:run()
	if result.ok then
		return result
	end
	self:printErr(result)
	return process.exit(1)
end

function Summon.new(program: string, ...: string): Summon
	return setmetatable({
		program = program,
		arguments = { ... },
		cwd = process.cwd(),
		env = {},
		stdio = "inherit",
	}, Summon)
end

function Summon.codeFromMany(...: Summon)
	local code = 0

	for i = 1, select("#", ...) do
		local summon: Summon = select(i, ...)
		local result = summon:run()
		if not result.ok then
			summon:printErr(result)
			code = 1
		end
	end

	return code
end

return table.freeze(Summon)


local Boba = require("@include/boba")
local fs = require("@std/fs")
local path = require("@std/path")
local process = require("@lute/process")
local toml = require("@batteries/toml")

local Config = Boba.Struct {
	version = Boba.String.inner,
	copyright = Boba.String.inner,

	flags = Boba.Map(Boba.String, Boba.Unknown).inner,

	sourcemap = Boba.Struct {
		filename = Boba.String.inner,
		project = Boba.String.inner,
	}.inner,

	build = Boba.Struct {
		src = Boba.String.inner,
		extensions = Boba.String.inner,

		output = Boba.String.inner,
		model = Boba.String.inner,
		project = Boba.String.inner,
	}.inner,
}:Nickname("Config")

export type Config = typeof(Config.inner)

local function config()
	local CONFIG_PATH = path.join(process.cwd(), "rocket.config.toml")
	if fs.exists(CONFIG_PATH) then
		return Config:assert(toml.deserialize(fs.readfiletostring(CONFIG_PATH)))
	end
	print("Missing `rocket.config.toml` configuration!")
	return process.exit(1)
end

return config


local Summon = require("@lib/Summon")
local path = require("@std/path")
local process = require("@lute/process")

Summon.new("bun", "run", "dev"):withCwd(path.format(path.join(process.cwd(), "docs"))):assert()


local config = require("@lib/config")()
local fs = require("@lute/fs")
local path = require("@std/path")
local process = require("@lute/process")
local richterm = require("@batteries/richterm")
local task = require("@lute/task")

local commandArgs = { "lute", "build", "plugin" }
table.move({ ... }, 2, select("#", ...), #commandArgs + 1, commandArgs)

local cwd = process.cwd()

local watchers: { [string]: fs.WatchHandle } = {}
local knownEntries, numFiles, numDirs = {} :: { [string]: true }, 0, 0
local needsRebuild, isRebuilding = false, false
local lastRebuiltAt = os.time()
local watchRocket

local function clearOutput()
	print((string.rep("\n", 120)))
end

local function printStatus(color: richterm.Formatter, status: string)
	local out = ` ⋅ Last rebuilt at {os.date("%H:%M:%S", lastRebuiltAt)}`
	out ..= ` ⋅ Watching {numFiles} files and {numDirs} directories`
	out = richterm.bold(color("→ " .. status)) .. richterm.gray(out)
	print(out)
	print()
end

local function setRebuildNeeded()
	needsRebuild = true
end

local function rebuild()
	if isRebuilding then
		return
	end

	isRebuilding = true
	needsRebuild = false
	lastRebuiltAt = os.time()

	clearOutput()
	printStatus(richterm.cyan, "Rebuilding...")

	local result = process.run(commandArgs)
	clearOutput()
	print((string.gsub(result.stdout .. result.stderr, "\\n$", "")))
	if result.ok then
		printStatus(richterm.green, "Build success!")
	else
		printStatus(richterm.red, "Build failed!")
	end
	watchRocket()

	isRebuilding = false
end

local function watchRecursive(dir: string)
	for _, entry in fs.listdir(dir) do
		local fullPath = path.format(path.join(dir, entry.name))

		-- TODO: removed files
		if not knownEntries[fullPath] then
			knownEntries[fullPath] = true
			setRebuildNeeded()
		end

		if entry.type ~= "dir" then
			numFiles += 1
			continue
		end

		if watchers[fullPath] == nil then
			watchers[fullPath] = fs.watch(fullPath, setRebuildNeeded)
			numDirs += 1
		end

		watchRecursive(fullPath)
	end
end

function watchRocket()
	watchRecursive(path.format(path.join(cwd, config.build.src)))
	watchRecursive(path.format(path.join(cwd, config.build.extensions)))
end

do
	watchRocket()
	while task.wait(0.1) do
		if needsRebuild then
			rebuild()
		end
	end
end


local Extension = require("@src/core/Extension")
local UserIds = require("@src/constants/UserIds")
local assets = require("@include/assets")
local createCoreExtension = require("@src/core/constructors/createCoreExtension")

local ext = createCoreExtension({
	id = "rocket-color-picker",
	title = "Color Picker",
	description = "TBA",
	icon = assets.extensions.rocket,
	contributors = {
		{ type = "user", id = UserIds.znotfireman },
	},
})

ext:newCommand({
	id = "pick-from-part",
	title = "Pick Color from Part",
	description = "TODO",
}, function(r: Extension.CommandRuntime)
	local output = r:createCommandOutput()

	function output.renderViewport() end

	function output.renderWindow() end

	return output
end)

ext:newCommand({
	id = "wheel",
	title = "Color Wheel",
	description = "TODO",
}, function(r: Extension.CommandRuntime)
	local output = r:createCommandOutput()
	--
	function output.renderViewport() end

	function output.renderWindow() end

	return output
end)

return ext


local types = require("./types")

local function wrapClassInserter(class: string, assetProperty: string): types.Inserter
	return function(product)
		local instance = Instance.new(class);
		(instance :: any)[assetProperty] = `rbxassetid://{product.AssetId}`
		return instance
	end
end

local insertImageOptions: { types.InsertOptions } = {
	{ label = "As Decal", insert = wrapClassInserter("Decal", "Texture") },
	{ label = "As Texture", insert = wrapClassInserter("Texture", "Texture") },
	{ label = "As ImageLabel", insert = wrapClassInserter("ImageLabel", "Image") },
	{ label = "As ImageButton", insert = wrapClassInserter("ImageButton", "Image") },
}

local insertAssetOptions = {
	{
		label = "Insert",
		insert = function(product: types.ProductInfo)
			-- InsertSevice:LoadAsset requires you to own the asset </3
			local objects = game:GetObjects(`rbxassetid://{product.AssetId}`)
			if #objects == 1 then
				return objects[1]
			end

			local container: Instance = Instance.new("Model")
			container.Name = product.Name

			for _, child in objects do
				child.Parent = container
			end

			return container
		end,
	},
}

local assetInserters: { [number]: { types.InsertOptions }? } = {
	[Enum.AssetType.Image.Value] = insertImageOptions,
	[Enum.AssetType.Decal.Value] = insertImageOptions,

	[Enum.AssetType.Lua.Value] = insertAssetOptions,
	[Enum.AssetType.Plugin.Value] = insertAssetOptions,
	[Enum.AssetType.MeshPart.Value] = insertAssetOptions,

	[Enum.AssetType.Audio.Value] = {
		{ label = "Insert", insert = wrapClassInserter("Sound", "SoundId") },
	},

	[Enum.AssetType.Mesh.Value] = {
		{
			label = "Insert",
			insert = function(product: types.ProductInfo)
				local instance = Instance.new("Part")
				instance.Anchored = true

				local mesh = Instance.new("SpecialMesh")
				mesh.MeshId = `rbxassetid://{product.AssetId}`
				mesh.Parent = instance

				return instance
			end,
		},
	},
}

return assetInserters


local UserInputService = game:GetService("UserInputService")
local Workspace = game:GetService("Workspace")

local Extension = require("@src/core/Extension")
local Iris = require("@src/Iris")
local UserIds = require("@src/constants/UserIds")
local assetInserters = require("@self/assetInserters")
local assets = require("@include/assets")
local createCoreExtension = require("@src/core/constructors/createCoreExtension")
local products = require("@self/products")
local types = require("@self/types")
local widgets = require("@src/widgets")

local ext = createCoreExtension {
	id = "rocket-insert",
	title = "Insert",
	description = "TBA",
	icon = assets.extensions.rocket,
	contributors = {
		{ type = "user", id = UserIds.znotfireman },
	},
}

ext:newCommand({
	id = "insert-asset",
	title = "Insert Asset",
	description = "Fetches and inserts the given asset ID into the game.",
}, function(r: Extension.CommandRuntime)
	local output = r:createCommandOutput()

	function output.renderWindow(window: Iris.Window)
		local maybeAssetId, assetInput = widgets.InstantInputText({ hint = "ie. 1234567890", label = "Asset ID" })

		if r.isFirstRun then
			window.state.position:set(UserInputService:GetMouseLocation())
			assetInput.state.text:set("")
			maybeAssetId = ""
		end

		local assetId = tonumber(maybeAssetId)
		if not assetId then
			return
		end

		local product = products.fetch(assetId)
		if not product then
			widgets.Subtext({
				if products.invalidIds[assetId] then `Invalid asset ID "{assetId}"!` else "Fetching asset...",
			})
			return
		end

		local assetTypeId = product.AssetTypeId
		-- selene: allow(incorrect_standard_library_use)
		local maybeAssetType = Enum.AssetType:FromValue(assetTypeId)
		local assetTypeName = if maybeAssetType then maybeAssetType.Name else "Unknown"

		local inserter = assetInserters[assetTypeId]
		if not inserter then
			Iris.Text({ `Rocket doesn't support inserting assets of type {assetTypeName}!` })
			widgets.Subtext({ `asset type ID: {assetTypeId}` })
			return
		end

		widgets.Subtext({ `Got asset of type {assetTypeName}! (asset type ID: {assetTypeId})` })

		Iris.SameLine()
		for _, options in inserter do
			if not Iris.Button({ options.label }).clicked() then
				continue
			end

			task.spawn(function(r: Extension.CommandRuntime, product: types.ProductInfo)
				window.state.isOpened:set(false)
				local finishRecording = r:recordChangeHistory()

				local success, inserted: Instance = pcall(options.insert, product)
				local recordingOperation = Enum.FinishRecordingOperation.Commit
				if success then
					r.log:trace(`Successfully inserted "{product.Name}" (ID: {product.AssetId}) as`, inserted)
					inserted.Parent = r:useSelection()[1] or Workspace
					r:setSelection({ inserted })
				else
					r.log:warn(`Failed to insert "{product.Name}" (ID: {product.AssetId}):`, inserted)
					recordingOperation = Enum.FinishRecordingOperation.Cancel
				end

				finishRecording(recordingOperation)
				window.state.isOpened:set(true)
				r:cleanup()
			end, r, product)
		end
		Iris.End()
	end

	return output
end)

ext:newCommand({
	id = "insert-avatar",
	title = "Insert Avatar",
	description = "Search, fetches, and inserts the given user's avatar into the game.",
}, function()
	-- TODO
end)

ext:newCommand({
	id = "insert-pkg-pesde",
	title = "Insert Package from pesde",
	description = "Search, fetches, and inserts from the pesde package manager.",
}, function()
	-- TODO
end)

ext:newCommand({
	id = "insert-pkg-wally",
	title = "Insert Package from Wally",
	description = "Search, fetches, and inserts from the Wally package manager.",
}, function()
	-- TODO
end)

ext:newCommand({
	id = "insert-git",
	title = "Insert from Git",
	description = "Inserts a Git repository into the game.",
}, function()
	-- TODO
end)

return ext


local MarketplaceService = game:GetService("MarketplaceService")

local pluginTrove = require("@src/pluginTrove")
local types = require("./types")

local cache: { [number]: types.ProductInfo } = {}
local threads = {}
local invalidIds = {}

local function fetch(assetId: number): types.ProductInfo?
	local existing = cache[assetId]
	if existing then
		return existing
	end

	if not (threads[assetId] or invalidIds[assetId]) then
		threads[assetId] = task.spawn(function(assetId: number)
			local productExists, productInfo =
				pcall(MarketplaceService.GetProductInfoAsync, MarketplaceService, assetId, nil)

			if productExists then
				cache[assetId] = productInfo
			else
				invalidIds[assetId] = true
			end

			threads[assetId] = nil
		end, assetId)
	end

	return nil
end

pluginTrove:Add(function()
	for _, thread in threads do
		pcall(task.cancel, thread)
	end

	table.clear(cache)
	table.clear(threads)
	table.clear(invalidIds)
end)

return table.freeze({
	cache = cache,
	threads = threads,
	invalidIds = invalidIds,
	fetch = fetch,
})


export type Inserter = (product: ProductInfo) -> Instance
export type InsertOptions = { label: string, insert: Inserter }

export type ProductInfo = {
	Name: string,
	Description: string,
	PriceInRobux: number,
	ProductId: number,
	ProductType: string,
	Created: string,
	Updated: string,
	ContentRatingTypeId: number,
	MinimumMembershipLevel: number,
	IsPublicDomain: boolean,
	TargetId: number,

	AssetId: number,
	AssetTypeId: number,
}

return nil


local Gizmos = require("@src/core/Gizmos")

local LENGTH = 5

local function drawAxis(mousePosition: Vector3, axisComponent: number, axisVector: Vector3)
	Gizmos.drawLine(mousePosition, mousePosition + axisVector * math.sign(axisComponent) * LENGTH)
end

local function drawDirection(mousePosition: Vector3, direction: Vector3)
	Gizmos.pushProperties({ Transparency = 0.5 })
	drawAxis(mousePosition, direction.X, Vector3.xAxis)
	drawAxis(mousePosition, direction.Y, Vector3.yAxis)
	drawAxis(mousePosition, direction.Z, Vector3.zAxis)
	Gizmos.popProperties()

	Gizmos.drawArrow(mousePosition, mousePosition + direction * LENGTH)
end

return drawDirection


local Gizmos = require("@src/core/Gizmos")

local function drawLine(from: CFrame, to: CFrame)
	Gizmos.drawSphere(from, Gizmos.POINT_RADIUS)
	Gizmos.drawLine(from.Position, to.Position)
	Gizmos.drawSphere(to, Gizmos.POINT_RADIUS)
end

return drawLine


local Lighting = game:GetService("Lighting")
local UserInputService = game:GetService("UserInputService")

local Extension = require("@src/core/Extension")
local Iris = require("@src/Iris")
local UserIds = require("@src/constants/UserIds")
local assets = require("@include/assets")
local createCoreExtension = require("@src/core/constructors/createCoreExtension")
local getRepositionState = require("@self/utils/getRepositionState")
local repositions = require("@self/repositions")
local sources = require("@self/sources")
local types = require("@self/types")

local ext = createCoreExtension {
	id = "rocket-light-sources",
	title = "Light Sources",
	description = "Artist-friendly visual control of the sun, the moon, and light instances.",
	icon = assets.extensions.lighting.sun,
	contributors = {
		{ type = "user", id = UserIds.znotfireman },
	},
}

for _, source in sources do
	for _, reposition in repositions do
		ext:newCommand({
			id = string.lower(`repositions-{source.name}-by-{reposition.name}`),
			title = `Reposition {source.name} by {reposition.name}`,
			description = `Repositions the {source.name} by {reposition.describedBy}.`,
		}, function(r: Extension.CommandRuntime)
			r:recordChangeHistory()
			local initial: types.RepositionState = nil :: any
			local hasInitiallyClicked = false

			local output = r:createCommandOutput()
			function output.renderViewport()
				if UserInputService:IsKeyDown(Enum.KeyCode.Escape) then
					r.trove:Clean()
					return
				end

				local isMouseDown = UserInputService:IsMouseButtonPressed(Enum.UserInputType.MouseButton1)
				if hasInitiallyClicked then
					if not isMouseDown then
						initial = nil :: any
						hasInitiallyClicked = false
						r:cleanup()
						return
					end

					local now = getRepositionState(r)
					initial = initial or now
					local direction = reposition.updateDirection(initial, getRepositionState(r), true)
					if direction then
						source.updateDirection(direction)
					else
						Lighting.ClockTime = initial.clockTime
						Lighting.GeographicLatitude = initial.geographicLatitude
					end
				elseif isMouseDown then
					r:setSelection({})
					hasInitiallyClicked = true
				end
			end

			return output
		end)
	end
end

ext:newCommand({
	id = "reposition",
	title = "Reposition Light Sources",
	description = "Summons a toolbar to visually drag and adjust the sun, the moon, and light instances.",
}, function(r: Extension.CommandRuntime)
	local result = r:createCommandOutput()

	function result.renderViewport()
		if Iris.Window({ "Reposition Light Sources" }).closed() then
			r:cleanup()
			Iris.End()
			return
		end

		Iris.End()
	end

	return result
end)

return ext


local types = require("../types")

local cursor = {} :: types.Reposition
cursor.name = "Cursor"
cursor.describedBy = "dragging your cursor"

function cursor.updateDirection(_, now)
	return now.mouseTransform.LookVector
end

return cursor


local Iris = require("@src/Iris")
local drawDirection = require("../gizmos/drawDirection")
local reflect = require("../utils/reflect")
local types = require("../types")

local face = {} :: types.Reposition
face.name = "Face"
face.describedBy = "shinning the surface of a part"

function face.updateDirection(_, now, isGizmos)
	local rayDirection = CFrame.lookAt(now.cameraTransform.Position, now.mouseTransform.Position)
	local raycast = workspace:Raycast(now.cameraTransform.Position, rayDirection.LookVector * 10_000)

	if not raycast then
		Iris.Tooltip({ "Cursor must be on a face!" })
		return nil
	end

	local reflection = reflect(rayDirection.LookVector, raycast.Normal)
	local direction = CFrame.lookAt(raycast.Position, raycast.Position + reflection).LookVector

	if isGizmos then
		drawDirection(now.mouseTransform.Position, direction)
	end

	return direction
end

return face


local types = require("./types")

local repositions: { types.Reposition } = {
	require("@self/cursor"),
	require("@self/face"),
	require("@self/line"),
	require("@self/reflection"),
	require("@self/shadow"),
}

return repositions


local drawLine = require("../gizmos/drawLine")
local types = require("../types")

local line = {} :: types.Reposition
line.name = "Line"
line.describedBy = "a line that shadows will follow"

function line.updateDirection(initial, now, isGizmos)
	local transform = CFrame.lookAt(now.mouseTransform.Position, initial.mouseTransform.Position)
	if isGizmos then
		drawLine(initial.mouseTransform, now.mouseTransform)
	end
	return transform.LookVector
end

return line


local drawDirection = require("../gizmos/drawDirection")
local reflect = require("../utils/reflect")
local types = require("../types")

local reposition = {} :: types.Reposition
reposition.name = "Reflection"
reposition.describedBy = "casting a reflection at your mouse"

function reposition.updateDirection(_, now, isGizmos)
	local reflection = reflect(now.mouseTransform.LookVector, Vector3.new(0, 1, 0))
	local direction = CFrame.lookAt(now.mouseTransform.Position, now.mouseTransform.Position + reflection).LookVector

	if isGizmos then
		drawDirection(now.mouseTransform.Position, direction)
	end

	return direction
end

return reposition


local drawLine = require("../gizmos/drawLine")
local types = require("../types")

local shadow = {} :: types.Reposition
shadow.name = "Shadow"
shadow.describedBy = "moving shadows with your cursor"

function shadow.updateDirection(initial, now, isGizmos)
	local negative = (now.mouseTransform.Position - initial.mouseTransform.Position)
	local otherDistance = (now.mouseTransform.Position - initial.mouseTransform.Position).Magnitude

	if isGizmos then
		drawLine(initial.mouseTransform, now.mouseTransform)
	end

	return CFrame.lookAt(
		initial.mouseTransform.Position - initial.sunDirection * otherDistance,
		initial.mouseTransform.Position - negative
	).LookVector
end

return shadow


local types = require("./types")

local sources: { types.Source } = {
	require("@self/sun"),
	require("@self/moon"),
}

return sources


local setLightingDirection = require("../utils/setLightingDirection")
local types = require("../types")

local moon = {} :: types.Source
moon.name = "Moon"

function moon.updateDirection(direction)
	setLightingDirection(direction, -1)
end

return moon


local setLightingDirection = require("../utils/setLightingDirection")
local types = require("../types")

local sun = {} :: types.Source
sun.name = "Sun"

function sun.updateDirection(direction)
	setLightingDirection(direction, 1)
end

return sun


export type Reposition = {
	name: string,
	describedBy: string,

	updateDirection: (initial: RepositionState, now: RepositionState, showGizmos: boolean) -> Vector3?,
}

export type RepositionState = {
	clockTime: number,
	geographicLatitude: number,
	sunDirection: Vector3,
	mouseTransform: CFrame,
	cameraTransform: CFrame,
}

export type Source = {
	name: string,
	updateDirection: (direction: Vector3) -> (),
}

return nil


local Lighting = game:GetService("Lighting")
local Workspace = game:GetService("Workspace")

local Extension = require("@src/core/Extension")
local types = require("../types")

local function useRepositionState(r: Extension.CommandRuntime): types.RepositionState
	local mouse = r:getMouse()
	return {
		clockTime = Lighting.ClockTime,
		geographicLatitude = Lighting.GeographicLatitude,
		sunDirection = Lighting:GetSunDirection(),
		cameraTransform = Workspace.CurrentCamera.CFrame,
		mouseTransform = mouse.Hit,
		mouseRay = mouse.UnitRay,
	}
end

return useRepositionState


local function reflect(vector: Vector3, normal: Vector3): Vector3
	return vector - (2 * vector:Dot(normal) * normal)
end

return reflect


local Lighting = game:GetService("Lighting")

local TAU = math.pi * 2

local function setLightingDirection(direction: Vector3, longitudeFactor: number)
	local latitude = math.atan2(direction.Z, math.sqrt(direction.X ^ 2 + direction.Y ^ 2))
	local longtitude = math.atan2(direction.Y * -longitudeFactor, direction.X * -longitudeFactor)

	local clockTime = (longtitude / TAU) * 24 - 6
	local geographicLatitude = (latitude / TAU) * 360 + 23.5

	Lighting.ClockTime = clockTime % 24
	Lighting.GeographicLatitude = geographicLatitude
end

return setLightingDirection


local Lighting = game:GetService("Lighting")

local Extension = require("@src/core/Extension")
local UserIds = require("@src/constants/UserIds")
local assets = require("@include/assets")
local createCoreExtension = require("@src/core/constructors/createCoreExtension")

local ext = createCoreExtension {
	id = "rocket-lighting",
	title = "Lighting",
	description = "Artist-friendly lighting control with visual control of the sun and moon.",
	icon = assets.extensions.lighting.sun,
	contributors = {
		{ type = "user", id = UserIds.znotfireman },
	},
}

ext:newCommand({
	id = "toggle-shadows",
	title = "Toggle Shadows",
	description = "Toggles Lighting's GlobalShadows property",
}, function(r: Extension.CommandRuntime)
	r:recordChangeHistory()
	Lighting.GlobalShadows = not Lighting.GlobalShadows
end)

return ext


local Command = require("@src/core/commands/Command")
local CommandStore = require("@src/core/commands/CommandStore")
local Extension = require("@src/core/Extension")
local Iris = require("@src/Iris")
local UserIds = require("@src/constants/UserIds")
local assets = require("@include/assets")
local createCoreExtension = require("@src/core/constructors/createCoreExtension")
local widgets = require("@src/widgets")

local ext = createCoreExtension {
	id = "rocket",
	title = "Rocket",
	description = "Basic Rocket utilities and commands.",
	icon = assets.extensions.rocket,
	contributors = {
		{ type = "user", id = UserIds.znotfireman },
	},
}

ext:newCommand({
	id = "kill-all-commands",
	title = "Kill All Commands",
	description = "Kills all currently running commands.",
}, function(r: Extension.CommandRuntime)
	for _, otherRuntime in CommandStore.runtimes() do
		if otherRuntime ~= r then
			otherRuntime:cleanup()
		end
	end
end)

ext:newCommand({
	id = "kill-commands",
	title = "Kill Commands",
	description = "Select and kill running commands.",
}, function(r: Extension.CommandRuntime)
	local output = r:createCommandOutput()

	local firstRun = true
	output.windowConfig = { WindowPadding = Vector2.zero, ItemSpacing = Vector2.zero }
	function output.renderWindow()
		widgets.List({
			captureFocus = firstRun,

			entries = CommandStore.runningCommands(),
			getEntryTitle = function(entry: Command.Command)
				return entry.interface.title
			end,
			renderEntry = function(entry: Command.Command)
				Iris.Image({ entry:getIcon(), UDim2.fromOffset(18, 18) })
				Iris.Text({ entry.interface.title })
				widgets.Subtext({ entry._extension.interface.title })
			end,
			onEntrySelected = function(entry: Command.Command)
				CommandStore.runtimes()[entry]:cleanup()
			end,
		} :: widgets.ListProps<Command.Command>)

		firstRun = false
	end

	return output
end)

ext:newCommand({
	id = "test-list-widget",
	title = "Test List Widget",
	description = "N/A",
}, function(r: Extension.CommandRuntime)
	local output = r:createCommandOutput()

	local firstRun = true
	output.windowConfig = { WindowPadding = Vector2.zero, ItemSpacing = Vector2.zero }
	function output.renderWindow()
		widgets.List({
			captureFocus = firstRun,

			entries = { "Vietnam", "USA", "China", "Venezuela", "Russia" },
			renderEntry = function(entry)
				Iris.Text({ tostring(entry) })
			end,
			getEntryTitle = function(entry)
				return tostring(entry)
			end,
			onEntrySelected = function(entry)
				print(entry)
			end,
		} :: any)

		firstRun = false
	end

	return output
end)

return ext


local ScriptEditorService = game:GetService("ScriptEditorService")

local Extension = require("@src/core/Extension")
local Iris = require("@src/Iris")
local UserIds = require("@src/constants/UserIds")
local assets = require("@include/assets")
local createCoreExtension = require("@src/core/constructors/createCoreExtension")
local pluginTrove = require("@src/pluginTrove")
local widgets = require("@src/widgets")

local ext = createCoreExtension {
	id = "rocket-scripts",
	title = "Scripts",
	description = "TBA",
	icon = assets.extensions.rocket,
	contributors = {
		{ type = "user", id = UserIds.znotfireman },
	},
}

local scripts: { LuaSourceContainer } = {}
local scriptIndex: { [LuaSourceContainer]: number } = {}
local function tryAddScript(script: Instance)
	if script:IsA("LuaSourceContainer") then
		table.insert(scripts, script)
		scriptIndex[script] = #scripts
	end
end
pluginTrove:Add(task.spawn(function()
	for _, d in game:GetDescendants() do
		tryAddScript(d)
	end
	pluginTrove:Add(game.DescendantAdded:Connect(tryAddScript))
end))

-- TODO: can we implement virtualized lists this is lagging rlly bad, restrain
-- to like first 50 or sm shit
ext:newCommand({
	id = "search-all-scripts",
	title = "Search All Scripts",
	description = "TBA",
}, function(r: Extension.CommandRuntime)
	local output = r:createCommandOutput()

	local search = ""
	local firstRun = true
	output.windowConfig = { WindowPadding = Vector2.zero, ItemSpacing = Vector2.zero }
	function output.renderWindow()
		widgets.List({
			captureFocus = firstRun,

			search = search,
			onSearchChange = function(newSearch)
				search = newSearch
			end,

			entries = scripts,
			getEntryTitle = function(entry: LuaSourceContainer)
				return entry:GetFullName()
			end,
			renderEntry = function(entry: LuaSourceContainer)
				Iris.Text({ entry.Name })
				widgets.Subtext({ entry:GetFullName() })
			end,
			onEntrySelected = function(entry: LuaSourceContainer)
				task.spawn(ScriptEditorService.OpenScriptDocumentAsync, ScriptEditorService, entry, nil)
			end,
		} :: widgets.ListProps<LuaSourceContainer>)

		firstRun = false
	end

	return output
end)

ext:newCommand({
	id = "search-open-scripts",
	title = "Search Open Scripts",
	description = "TBA",
}, function(r: Extension.CommandRuntime)
	local output = r:createCommandOutput()

	local firstRun = true
	output.windowConfig = { WindowPadding = Vector2.zero, ItemSpacing = Vector2.zero }

	function output.renderWindow()
		local scriptDocuments = ScriptEditorService:GetScriptDocuments() :: { ScriptDocument }
		local realDocuments = table.create(#scriptDocuments) :: { ScriptDocument }
		for _, document in scriptDocuments do
			if not document:IsCommandBar() then
				table.insert(realDocuments, document)
			end
		end

		widgets.List({
			captureFocus = firstRun,

			entries = realDocuments,
			getEntryTitle = function(entry: ScriptDocument)
				return entry:GetScript():GetFullName()
			end,
			renderEntry = function(entry: ScriptDocument)
				local entryScript = assert(entry:GetScript())
				Iris.Text({ entryScript.Name })
				widgets.Subtext({ entryScript:GetFullName() })
			end,
			onEntrySelected = function(entry: ScriptDocument)
				task.spawn(ScriptEditorService.OpenScriptDocumentAsync, ScriptEditorService, entry:GetScript(), nil)
			end,
		} :: widgets.ListProps<ScriptDocument>)

		firstRun = false
	end

	return output
end)

return ext


local Selection = game:GetService("Selection")
local Workspace = game:GetService("Workspace")

local Extension = require("@src/core/Extension")
local Iris = require("@src/Iris")
local RegExp = require("@pkgs/RegExp")
local UserIds = require("@src/constants/UserIds")
local assets = require("@include/assets")
local createCoreExtension = require("@src/core/constructors/createCoreExtension")
local widgets = require("@src/widgets")

local ext = createCoreExtension {
	id = "rocket-selection",
	title = "Selection",
	description = "Expanding, reducing, and transforming instance selections.",
	icon = assets.extensions.selection,
	contributors = {
		{ type = "user", id = UserIds.znotfireman },
	},
}

ext:newCommand({
	id = "select-children",
	title = "Select Children",
	description = "Selects children of the current selection.",
}, function(r: Extension.CommandRuntime)
	r:recordChangeHistory()
	local oldSelection = Selection:Get()
	local newSelection = table.clone(oldSelection)
	for _, selected in oldSelection do
		local children = selected:GetChildren()
		table.move(children, 1, #children, #newSelection + 1, newSelection)
	end
	Selection:Set(newSelection)
end)

ext:newCommand({
	id = "select-descendants",
	title = "Select Descendants",
	description = "Selects descendants of the current selection.",
}, function(r: Extension.CommandRuntime)
	r:recordChangeHistory()
	local oldSelection = Selection:Get()
	local newSelection = table.clone(oldSelection)
	for _, selected in oldSelection do
		local descendants = selected:GetDescendants()
		table.move(descendants, 1, #descendants, #newSelection + 1, newSelection)
	end
	r:setSelection(newSelection)
end)

ext:newCommand({
	id = "select-just-children",
	title = "Select Just Children",
	description = "Selects only the children of the current selection.",
}, function(r: Extension.CommandRuntime)
	r:recordChangeHistory()
	local newSelection = {}
	for _, selected in r:useSelection() do
		local children = selected:GetChildren()
		table.move(children, 1, #children, #newSelection + 1, newSelection)
	end
	r:setSelection(newSelection)
end)

ext:newCommand({
	id = "select-just-descendants",
	title = "Select Just Descendants",
	description = "Selects only the descendants of the current selection.",
}, function(r: Extension.CommandRuntime)
	r:recordChangeHistory()
	local newSelection = {}
	for _, selected in Selection:Get() do
		local descendants = selected:GetDescendants()
		table.move(descendants, 1, #descendants, #newSelection + 1, newSelection)
	end
	r:setSelection(newSelection)
end)

ext:newCommand({
	id = "select-just-first",
	title = "Select Just First",
	description = "Reduce to just your first selection.",
}, function(r: Extension.CommandRuntime)
	r:recordChangeHistory()
	r:setSelection({ r:useSelection()[1] })
end)

ext:newCommand({
	id = "select-just-last",
	title = "Select Just Last",
	description = "Reduce to just your last selection.",
}, function(r: Extension.CommandRuntime)
	r:recordChangeHistory()
	local oldSelection = Selection:Get()
	Selection:Set({ oldSelection[#oldSelection] })
end)

ext:newCommand({
	id = "deselect-all",
	title = "Deselect All",
	description = "Clears your selection.",
}, function(r: Extension.CommandRuntime)
	r:recordChangeHistory()
	r:setSelection({})
end)

ext:newCommand({
	id = "destroy-selection",
	title = "Destroy Selection",
	description = "Destroys the selection.",
}, function(r: Extension.CommandRuntime)
	r:recordChangeHistory()
	for _, instance in r:useSelection() do
		pcall(game.Destroy, instance)
	end
end)

ext:newCommand({
	id = "destroy-just-children",
	title = "Destroy Just Children",
	description = "Destroys just the children of the current selection.",
}, function(r: Extension.CommandRuntime)
	r:recordChangeHistory()
	for _, parent in r:useSelection() do
		for _, children in parent:GetChildren() do
			pcall(game.Destroy, children)
		end
	end
end)

ext:newCommand({
	id = "narrow-selection",
	title = "Narrow Selection",
	description = "Search to narrow the current selection down to one instance.",
}, function(r: Extension.CommandRuntime)
	local output = r:createCommandOutput()
	local firstRun = true

	output.windowConfig = { ItemSpacing = Vector2.new(8, 0), WindowPadding = Vector2.zero }
	function output.renderWindow()
		widgets.List({
			captureFocus = firstRun,
			entries = r:useSelection(),
			getEntryTitle = function(entry: Instance)
				return entry:GetFullName()
			end,
			renderEntry = function(entry: Instance)
				Iris.Text({ entry.Name })
				widgets.Subtext({ entry:GetFullName() })
			end,
			onEntrySelected = function(entry: Instance)
				r:recordChangeHistory()
				r:setSelection({ entry })
				r:cleanup()
			end,
		} :: widgets.ListProps<Instance>)
		firstRun = false
	end

	return output
end)

-- TODO: unfinished
ext:newCommand({
	id = "rename-selection",
	title = "Rename Selection",
	description = "Renames the current selection to the given pattern.",
}, function(r: Extension.CommandRuntime)
	r:recordChangeHistory()

	local firstRun = true
	local output = r:createCommandOutput()

	function output.renderWindow(window: Iris.Window)
		if firstRun then
			firstRun = false

			local viewportSize = Workspace.CurrentCamera.ViewportSize
			local windowSize = viewportSize / 2
			window.state.position:set(windowSize / 2)
			window.state.size:set(windowSize)
		end

		local selection = r:useSelection()

		Iris.SameLine()

		---

		widgets.Frame { size = UDim2.fromScale(0, 1), automaticSize = Enum.AutomaticSize.None }
		widgets.Flex(Enum.UIFlexMode.Fill)

		Iris.SeparatorText { `Rename {#selection} instances` }

		local rawPattern = widgets.InstantInputText {
			Hint = "Match instances (optional)",
			Width = UDim.new(1, 0),
		}

		local _patternSuccess, _pattern = pcall(RegExp.new, rawPattern, nil)
		widgets.Spacer()

		local rawRename, renameInput = widgets.InstantInputText {
			Hint = "Rename to...",
			Width = UDim.new(1, 0),
		}

		local _renameSuccess, _rename = pcall(RegExp.new, rawRename, nil)
		widgets.Spacer()

		Iris.SameLine()
		widgets.Subtext { "Insert:" }
		if Iris.Button { "Current name" }.clicked() then
			rawRename ..= "$&"
		end
		if Iris.Button { "Number Ascending" }.clicked() then
			rawRename ..= "$nn"
		end
		if Iris.Button { "Number Descending" }.clicked() then
			rawRename ..= "$NN"
		end
		renameInput.state.text:set(rawRename)
		Iris.End()

		widgets.FlexSpacer(Enum.UIFlexMode.Fill)

		Iris.Button { "Rename" }
		widgets.Subtext { "This command supports regular expressions!" }

		Iris.End()

		widgets.Frame { size = UDim2.fromScale(1 / 3, 1), automaticSize = Enum.AutomaticSize.None }
		Iris.SeparatorText { "Preview" }
		Iris.End()

		Iris.End()

		---
	end

	return output
end)

return ext


local Extension = require("@src/core/Extension")
local UserIds = require("@src/constants/UserIds")
local assets = require("@include/assets")
local createCoreExtension = require("@src/core/constructors/createCoreExtension")

local ext = createCoreExtension({
	id = "rocket-tidy",
	title = "Tidy",
	description = "Keep your game organized.",
	icon = assets.extensions.rocket,
	contributors = {
		{ type = "user", id = UserIds.znotfireman },
	},
})

local CONTAINERS = { "Folder", "Model" }

for _, containerFrom in CONTAINERS do
	for _, containerTo in CONTAINERS do
		if containerFrom == containerTo then
			continue
		end

		-- TODO: maybe move this command to a dedicated reclass plugin?
		ext:newCommand({
			id = string.lower(`{containerFrom}-to-{containerTo}`),
			title = `{containerFrom} to {containerTo}`,
			description = `Converts all {containerFrom}s in the selection to {containerTo}s.`,
		}, function(r: Extension.CommandRuntime)
			r:recordChangeHistory()

			local newSelection = {}
			for index, selected in r:useSelection() do
				if selected:IsA(containerFrom) then
					local newContainer = Instance.new(containerTo)
					newContainer.Name = selected.Name
					newContainer.Archivable = selected.Archivable

					for _, tag in selected:GetTags() do
						selected:AddTag(tag)
					end

					for attribute, value in selected:GetAttributes() do
						selected:SetAttribute(attribute, value)
					end

					for _, child in selected:GetChildren() do
						child.Parent = newContainer
					end

					newContainer.Parent = selected.Parent
					selected:Destroy()
					newSelection[index] = newContainer
				else
					newSelection[index] = selected
				end
			end

			r:setSelection(newSelection)
		end)
	end

	-- ext:newCommand({
	-- 	id = string.lower(`group-{containerFrom}`),
	-- 	title = `Group Selection into {containerFrom}`,
	-- 	description = `Groups the current selection into a new {containerFrom}.`,
	-- }, function(context: Extension.CommandRuntime)
	-- 	context:recordChangeHistory()

	-- 	local container = Instance.new(containerFrom)
	-- 	local newSelection = {}
	-- 	for index, selected in context:useSelection() do
	-- 	end

	-- 	context:setSelection({container})
	-- end)
end

ext:newCommand({
	id = "ungroup",
	title = `Ungroup Selection`,
	description = `Expands the children of the current selection, thus ungrouping it.`,
}, function(r: Extension.CommandRuntime)
	r:recordChangeHistory()

	local newSelection = {}
	for _, selected in r:useSelection() do
		local parent = selected.Parent
		for _, children in selected:GetChildren() do
			children.Parent = parent
			table.insert(newSelection, children)
		end
		selected:Destroy()
	end

	r:setSelection(newSelection)
end)

return ext


local Extension = require("@src/core/Extension")
local Tags = require("@src/constants/Tags")
local assets = require("@include/assets")
local createCoreExtension = require("@src/core/constructors/createCoreExtension")

local ext = createCoreExtension {
	id = "rocket-trusses",
	title = "Trusses",
	description = "Quickly fill TrussParts with solid materials for obbies.",
	icon = assets.extensions.rocket,
	contributors = { "Team Fireworks" },
}

local function fill(truss: TrussPart)
	local filler: BasePart
	local fillerName = "TrussFill"

	local existing = truss:FindFirstChild(fillerName)
	if existing then
		if existing:IsA("BasePart") and existing:HasTag(Tags.TRUSS_FILLER) then
			filler = existing
		else
			existing:Destroy()
		end
	end

	filler = filler or Instance.new("Part")

	filler.Anchored = true
	filler.CanCollide = false
	filler.CastShadow = truss.CastShadow
	filler.CFrame = truss.CFrame
	filler.Color = truss.Color
	filler.Material = truss.Material
	filler.Name = fillerName
	filler.Reflectance = truss.Reflectance
	filler.Size = truss.Size - Vector3.new(0.5, 0.5, 0.5)
	filler.Transparency = truss.Transparency

	filler.Parent = truss
	filler:AddTag(Tags.TRUSS_FILLER)

	return filler
end

local function fillRecursive(instances: { Instance }): { Instance }
	local result = {}
	for _, child in instances do
		if child:IsA("TrussPart") then
			table.insert(result, fill(child))
		end

		local childFillers = fillRecursive(child:GetChildren())
		table.move(childFillers, 1, #childFillers, #result, result)
	end
	return result
end

ext:newCommand({
	id = `fill-trusses`,
	title = `Fill Trusses`,
	description = `Fills the insides of selected TrussParts, then selects the fills.`,
}, function(r: Extension.CommandRuntime)
	r:recordChangeHistory()
	r:setSelection(fillRecursive(r:useSelection()))
end)

ext:newCommand({
	id = "delete-truss-fill",
	title = "Delete Truss Fill",
	description = "Deletes fills in selected TrussParts that was created by this extension.",
}, function(r: Extension.CommandRuntime)
	r:recordChangeHistory()

	for _, selected in r:useSelection() do
		if selected:HasTag(Tags.TRUSS_FILLER) then
			pcall(game.Destroy, selected)
			continue
		end

		for _, descendant in selected:GetDescendants() do
			if descendant:HasTag(Tags.TRUSS_FILLER) then
				pcall(game.Destroy, descendant)
			end
		end
	end
end)

return ext


--!nonstrict
--!optimize 2

-- Modified for new solver usage in Rocket
-- TODO: Boba for new solver?
--[[

    ▄ █ ▀█▀ █ ▄  Welcome To Hell @ wthrblx.com
     █▀█ █ █▀█   (c) Team Fireworks 2024-2026.

    This software is provided 'as-is', without any express or implied
    warranty. In no event will the contributors be held liable for any damages
    arising from the use of this software.

    Permission is granted to anyone to use this software for any purpose,
    including commercial applications, and to alter it and redistribute it
    freely, subject to the following restrictions:

    1. The origin of this software must not be misrepresented; you must not
       claim that you wrote the original software. If you use this software
       in a product, an acknowledgment in the product documentation would be
       appreciated but is not required.
    2. Altered source versions must be plainly marked as such, and must not be
       misrepresented as being the original software.
    3. This notice may not be removed or altered from any source distribution.

]]

local INDENT = "  "

local function quote(x: any)
	local xType = typeof(x)
	if xType == "string" then
		x = string.format("%q", x)
	end
	return `{x} ({xType})`
end

local function quoteGot(x: any)
	return `got {quote(x)}`
end

local Result = {}
Result.__index = Result

export type ResultOk = typeof(setmetatable({} :: { ok: true }, Result))
export type ResultFail = typeof(setmetatable(
	{} :: { ok: false, expectedType: string, message: string, children: { Result } },
	Result
))

export type Result = ResultOk | ResultFail

Result.ok = table.freeze(setmetatable({ ok = true :: true }, Result)) :: Result

function Result.fail(expectedType: string, message: string, children: { Result }?): Result
	return setmetatable({
		ok = false :: false,
		expectedType = expectedType,
		message = message,
		children = children or {},
	}, Result)
end

function Result.clone(self: Result)
	return if self.ok then self else Result.fail(self.expectedType, self.message, self.children)
end

function Result.because(self: Result, sub: Result)
	return if self.ok
		then sub:clone()
		elseif sub.ok then self:clone()
		else Result.fail(self.expectedType, self.message, table.move(self.children, 1, #self.children, 2, { sub }))
end

function Result.format(self: Result, maybeIndent: string?): string
	if self.ok then
		return ""
	end

	local indent = maybeIndent or ""
	local lines = {}

	table.insert(lines, indent .. `expected {self.expectedType}`)
	table.insert(lines, indent .. "> " .. self.message)
	indent ..= INDENT

	for _, sub in self.children do
		if not sub.ok then
			table.insert(lines, sub:format(indent))
		end
	end

	return table.concat(lines, "\n")
end

function Result.__tostring(self: Result)
	return self:format()
end

---

local Boba = {}
Boba.__index = Boba
Boba.Result = Result
Boba.quote = quote
Boba.quoteGot = quoteGot

export type BobaInner<T> = {
	expectedType: string,
	match: (self: Boba<T>, x: unknown) -> Result,
	inner: T,
}

export type Boba<T> = typeof(setmetatable({} :: BobaInner<T>, Boba))

function Boba.new<T>(expectedType: string, match: (self: Boba<T>, x: any) -> Result): Boba<T>
	local self = setmetatable({
		expectedType = expectedType,
		match = match,
	}, Boba)
	self.inner = self :: any
	return self
end

-- FIXME: Luau types are bad </3
-- function boba.__call<T>(self: Type<T>, x: any)
function Boba.__call(self: any, x: any): Result
	return self:match(x)
end

function Boba.__tostring(self: Boba<any>)
	return `Boba<{self.expectedType}>`
end

function Boba.isMatch<T>(self: Boba<T>, x: any): boolean
	return self:match(x).ok
end

function Boba.cast<T>(self: Boba<T>, x: any): T?
	return if self:match(x).ok then x else nil
end

function Boba.assert<T>(self: Boba<T>, x: any): T
	local result = self:match(x)
	return if result.ok then x else error(result:format())
end

function Boba.fail(self: Boba<any>, message: string)
	return Result.fail(self.expectedType, message)
end

function Boba.Nickname<T>(self: Boba<T>, name: string): Boba<T>
	return Boba.new(name, self.match)
end

function Boba.Retype<T>(self: Boba<any>): Boba<T>
	return self
end

function Boba.Untype<T>(self: Boba<T>): Boba<any>
	return self
end

function Boba.And<L, R>(left: Boba<L>, right: Boba<R>): Boba<L & R>
	return Boba.new(`({left.expectedType}) & ({right.expectedType})`, function(self: Boba<any>, x: any)
		local leftResult = left:match(x)
		if leftResult.ok then
			local rightResult = right:match(x)
			return if rightResult.ok
				then Result.ok
				else self:fail("it didn't match the right type"):because(rightResult)
		end
		return self:fail("it didn't match the left type"):because(leftResult)
	end)
end

function Boba.Or<L, R>(left: Boba<L>, right: Boba<R>): Boba<L | R>
	return Boba.new(`{left.expectedType} | {right.expectedType}`, function(self: Boba<any>, x: any)
		local leftResult = left:match(x)
		local rightResult = right:match(x)
		if leftResult.ok or rightResult.ok then
			return Result.ok
		end
		return self:fail(`did not match any of: {left.expectedType}, {right.expectedType}`)
			:because(leftResult)
			:because(rightResult)
	end)
end

function Boba.Optional<T>(inner: Boba<T>): Boba<T?>
	return Boba.new(`({inner.expectedType})?`, function(_: Boba<any>, x: any)
		if x == nil then
			return Result.ok
		end
		return inner:match(x)
	end)
end

function Boba.Predicate<T>(inner: Boba<T>, predicate: (value: T) -> (boolean, string?)): Boba<T>
	return Boba.new(`Predicate<{inner.expectedType}>`, function(self: Boba<any>, x: any)
		local innerResult = inner:match(x)

		if innerResult.ok then
			local predicateOk, predicateError = predicate(x)
			if predicateOk then
				return Result.ok
			end

			return self:fail(predicateError or "value did not satisfy the predicate")
		end

		return innerResult
	end)
end

function Boba.Literal<T>(literal: T, nickname: string?): Boba<T>
	return Boba.new(nickname or tostring(literal), function(self: Boba<any>, x: any)
		return if rawequal(x, literal) then Result.ok else self:fail(`only {literal} is accepted, but {quoteGot(x)}`)
	end)
end

function Boba.Typeof<T>(expected: string)
	return Boba.new(expected, function(self: Boba<any>, x: any)
		return if typeof(x) == expected then Result.ok else self:fail(quoteGot(x))
	end)
end

function Boba.Array<V>(values: Boba<V>): Boba<{ V }>
	return Boba.new(`\{ {values.expectedType} \}`, function(self: Boba<any>, x: any)
		if typeof(x) ~= "table" then
			return self:fail(`expected a table, but {quoteGot(x)}`)
		end

		local expected = #x
		local encountered = 0

		for key, value in pairs(x) do
			encountered += 1

			if encountered > expected or typeof(key) ~= "number" then
				return self:fail(`because there's non-numeric indexes, such as key {quote(key)}`)
			end

			local valueResult = values:match(value)
			if not valueResult.ok then
				return self:fail(`value at index #{key} doesn't match`):because(valueResult)
			end
		end

		return Result.ok
	end)
end

function Boba.Map<K, V>(keys: Boba<K>, values: Boba<V>): Boba<{ [K]: V }>
	return Boba.new(`\{ [{keys.expectedType}]: {values.expectedType} \}`, function(self: Boba<any>, x: any)
		if typeof(x) ~= "table" then
			return self:fail(`expected a table, but {quoteGot(x)}`)
		end

		for key, value in pairs(x) do
			local keyResult = keys:match(key)
			if not keyResult.ok then
				return self:fail(`quoteGot unexpected key {quote(key)}`):because(keyResult)
			end

			local valueResult = values:match(value)
			if not valueResult.ok then
				return self:fail(`at key {quote(key)}`):because(valueResult)
			end
		end

		return Result.ok
	end)
end

function Boba.Set<K>(keys: Boba<K>): Boba<{ [K]: true }>
	return Boba.new(`\{ [{keys.expectedType}]: true \}`, function(self: Boba<any>, x: any)
		if typeof(x) ~= "table" then
			return self:fail(`expected a table, but {quoteGot(x)}`)
		end

		for key, value in pairs(x) do
			local keyResult = keys:match(key)
			if not keyResult.ok then
				return self:fail(`quoteGot unexpected key {quote(key)}`):because(keyResult)
			end

			if value ~= true then
				return self:fail(`expected literal \`true\`, but at key {quote(key)} quoteGot {quote(value)}`)
			end
		end

		return Result.ok
	end)
end

function Boba.Struct<T>(struct: T): Boba<T>
	local expectedPairs = { "[any]: any" }
	for key, value in struct :: { [string]: Boba<any> } do
		-- TODO: key could be better formatted here
		table.insert(expectedPairs, `{key}: {value.expectedType}`)
	end

	return Boba.new(`\{ {table.concat(expectedPairs, ", ")} \}`, function(self: Boba<any>, x: any)
		if typeof(x) ~= "table" then
			return self:fail(`expected a table, but {quoteGot(x)}`)
		end

		for key, valueType in pairs(struct) do
			local value = x[key]
			local valueResult = valueType:match(value)
			if not valueResult.ok then
				return self:fail(`value at key {quote(key)}`):because(valueResult)
			end
		end

		return Result.ok
	end)
end

function Boba.ExhaustiveStruct<T>(struct: T): Boba<T>
	local expectedPairs = {}
	for key, value in (struct :: any) :: { [string]: Boba<any> } do
		-- TODO: key could be better formatted here
		table.insert(expectedPairs, `{key}: {value.expectedType}`)
	end

	return Boba.new(`\{ {table.concat(expectedPairs, ", ")} \}`, function(self: Boba<any>, x: any)
		if typeof(x) ~= "table" then
			return self:fail(`expected a table, but {quoteGot(x)}`)
		end

		for key, valueType: any in pairs(struct :: any) do
			local value = x[key]
			local valueResult = valueType:match(value)
			if not valueResult.ok then
				return self:fail(`value at key {quote(key)}`):because(valueResult)
			end
		end

		for key in pairs(x) do
			if (struct :: any)[key] == nil then
				return self:fail(`{quote(key)} isn't included in exhaustive struct`)
			end
		end

		return Result.ok
	end)
end

Boba.Any = Boba.new("any", function()
	return Result.ok
end) :: Boba<any>

Boba.Unknown = Boba.new("unknown", function()
	return Result.ok
end) :: Boba<unknown>

Boba.Never = Boba.new("never", function(self)
	return self:fail("there are no valid values")
end) :: Boba<never>

Boba.Boolean = Boba.Typeof("boolean") :: Boba<boolean>
Boba.Buffer = Boba.Typeof("buffer") :: Boba<buffer>
Boba.Function = Boba.Typeof("function") :: Boba<(...any) -> ...any>
Boba.Number = Boba.Typeof("number") :: Boba<number>
Boba.String = Boba.Typeof("string") :: Boba<string>
Boba.Table = Boba.Typeof("table") :: Boba<{ [any]: any }>
Boba.Thread = Boba.Typeof("thread") :: Boba<thread>
Boba.Vector = Boba.Typeof("vector") :: Boba<vector>

Boba.True = Boba.Literal(true):Untype() :: Boba<true>
Boba.False = Boba.Literal(false):Untype() :: Boba<false>
Boba.Nil = Boba.Literal(nil) :: Boba<nil>

table.freeze(Boba)
return Boba


-- The lua implementation of the fzy string matching algorithm

local SCORE_GAP_LEADING = -0.005
local SCORE_GAP_TRAILING = -0.005
local SCORE_GAP_INNER = -0.01
local SCORE_MATCH_CONSECUTIVE = 1.0
local SCORE_MATCH_SLASH = 0.9
local SCORE_MATCH_WORD = 0.8
local SCORE_MATCH_CAPITAL = 0.7
local SCORE_MATCH_DOT = 0.6
local SCORE_MAX = math.huge
local SCORE_MIN = -math.huge
local MATCH_MAX_LENGTH = 1024

local fzy = {}

-- Check if `needle` is a subsequence of the `haystack`.
--
-- Usually called before `score` or `positions`.
--
-- Args:
--   needle (string)
--   haystack (string)
--   case_sensitive (bool, optional): defaults to false
--
-- Returns:
--   bool
function fzy.has_match(needle, haystack, case_sensitive)
	if not case_sensitive then
		needle = string.lower(needle)
		haystack = string.lower(haystack)
	end

	local j = 1
	for i = 1, string.len(needle) do
		j = string.find(haystack, needle:sub(i, i), j, true)
		if not j then
			return false
		else
			j = j + 1
		end
	end

	return true
end

local function is_lower(c)
	return c:match("%l")
end

local function is_upper(c)
	return c:match("%u")
end

local function precompute_bonus(haystack)
	local match_bonus = {}

	local last_char = "/"
	for i = 1, string.len(haystack) do
		local this_char = haystack:sub(i, i)
		if last_char == "/" or last_char == "\\" then
			match_bonus[i] = SCORE_MATCH_SLASH
		elseif last_char == "-" or last_char == "_" or last_char == " " then
			match_bonus[i] = SCORE_MATCH_WORD
		elseif last_char == "." then
			match_bonus[i] = SCORE_MATCH_DOT
		elseif is_lower(last_char) and is_upper(this_char) then
			match_bonus[i] = SCORE_MATCH_CAPITAL
		else
			match_bonus[i] = 0
		end

		last_char = this_char
	end

	return match_bonus
end

local function compute(needle, haystack, D, M, case_sensitive)
	-- Note that the match bonuses must be computed before the arguments are
	-- converted to lowercase, since there are bonuses for camelCase.
	local match_bonus = precompute_bonus(haystack)
	local n = string.len(needle)
	local m = string.len(haystack)

	if not case_sensitive then
		needle = string.lower(needle)
		haystack = string.lower(haystack)
	end

	-- Because lua only grants access to chars through substring extraction,
	-- get all the characters from the haystack once now, to reuse below.
	local haystack_chars = {}
	for i = 1, m do
		haystack_chars[i] = haystack:sub(i, i)
	end

	for i = 1, n do
		D[i] = {}
		M[i] = {}

		local prev_score = SCORE_MIN
		local gap_score = i == n and SCORE_GAP_TRAILING or SCORE_GAP_INNER
		local needle_char = needle:sub(i, i)

		for j = 1, m do
			if needle_char == haystack_chars[j] then
				local score = SCORE_MIN
				if i == 1 then
					score = ((j - 1) * SCORE_GAP_LEADING) + match_bonus[j]
				elseif j > 1 then
					local a = M[i - 1][j - 1] + match_bonus[j]
					local b = D[i - 1][j - 1] + SCORE_MATCH_CONSECUTIVE
					score = math.max(a, b)
				end
				D[i][j] = score
				prev_score = math.max(score, prev_score + gap_score)
				M[i][j] = prev_score
			else
				D[i][j] = SCORE_MIN
				prev_score = prev_score + gap_score
				M[i][j] = prev_score
			end
		end
	end
end

-- Compute a matching score.
--
-- Args:
--   needle (string): must be a subequence of `haystack`, or the result is
--     undefined.
--   haystack (string)
--   case_sensitive (bool, optional): defaults to false
--
-- Returns:
--   number: higher scores indicate better matches. See also `get_score_min`
--     and `get_score_max`.
function fzy.score(needle, haystack, case_sensitive)
	local n = string.len(needle)
	local m = string.len(haystack)

	if n == 0 or m == 0 or m > MATCH_MAX_LENGTH or n > m then
		return SCORE_MIN
	elseif n == m then
		return SCORE_MAX
	else
		local D = {}
		local M = {}
		compute(needle, haystack, D, M, case_sensitive)
		return M[n][m]
	end
end

-- Compute the locations where fzy matches a string.
--
-- Determine where each character of the `needle` is matched to the `haystack`
-- in the optimal match.
--
-- Args:
--   needle (string): must be a subequence of `haystack`, or the result is
--     undefined.
--   haystack (string)
--   case_sensitive (bool, optional): defaults to false
--
-- Returns:
--   {int,...}: indices, where `indices[n]` is the location of the `n`th
--     character of `needle` in `haystack`.
--   number: the same matching score returned by `score`
function fzy.positions(needle, haystack, case_sensitive)
	local n = string.len(needle)
	local m = string.len(haystack)

	if n == 0 or m == 0 or m > MATCH_MAX_LENGTH or n > m then
		return {}, SCORE_MIN
	elseif n == m then
		local consecutive = {}
		for i = 1, n do
			consecutive[i] = i
		end
		return consecutive, SCORE_MAX
	end

	local D = {}
	local M = {}
	compute(needle, haystack, D, M, case_sensitive)

	local positions = {}
	local match_required = false
	local j = m
	for i = n, 1, -1 do
		while j >= 1 do
			if D[i][j] ~= SCORE_MIN and (match_required or D[i][j] == M[i][j]) then
				match_required = (i ~= 1) and (j ~= 1) and (M[i][j] == D[i - 1][j - 1] + SCORE_MATCH_CONSECUTIVE)
				positions[i] = j
				j = j - 1
				break
			else
				j = j - 1
			end
		end
	end

	return positions, M[n][m]
end

-- Apply `has_match` and `positions` to an array of haystacks.
--
-- Args:
--   needle (string)
--   haystack ({string, ...})
--   case_sensitive (bool, optional): defaults to false
--
-- Returns:
--   {{idx, positions, score}, ...}: an array with one entry per matching line
--     in `haystacks`, each entry giving the index of the line in `haystacks`
--     as well as the equivalent to the return value of `positions` for that
--     line.
function fzy.filter(needle: string, haystacks: { string }, case_sensitive: boolean?): { { number } }
	local result = {}

	for i, line in ipairs(haystacks) do
		if fzy.has_match(needle, line, case_sensitive) then
			local p, s = fzy.positions(needle, line, case_sensitive)
			table.insert(result, { i, p, s })
		end
	end

	return result
end

-- The lowest value returned by `score`.
--
-- In two special cases:
--  - an empty `needle`, or
--  - a `needle` or `haystack` larger than than `get_max_length`,
-- the `score` function will return this exact value, which can be used as a
-- sentinel. This is the lowest possible score.
function fzy.get_score_min()
	return SCORE_MIN
end

-- The score returned for exact matches. This is the highest possible score.
function fzy.get_score_max()
	return SCORE_MAX
end

-- The maximum size for which `fzy` will evaluate scores.
function fzy.get_max_length()
	return MATCH_MAX_LENGTH
end

-- The minimum score returned for normal matches.
--
-- For matches that don't return `get_score_min`, their score will be greater
-- than than this value.
function fzy.get_score_floor()
	return MATCH_MAX_LENGTH * SCORE_GAP_INNER
end

-- The maximum score for non-exact matches.
--
-- For matches that don't return `get_score_max`, their score will be less than
-- this value.
function fzy.get_score_ceiling()
	return MATCH_MAX_LENGTH * SCORE_MATCH_CONSECUTIVE
end

-- The name of the currently-running implmenetation, "lua" or "native".
function fzy.get_implementation_name()
	return "lua"
end

return fzy


--!nocheck
--!nolint
--#selene: allow(unused_variable, shadowing)
--- repr - Version 1.2
-- Ozzypig - ozzypig.com - http://twitter.com/Ozzypig
-- Check out this thread for more info:
-- https://devforum.roblox.com/t/repr-function-for-printing-tables/276575
--[[

local repr = require(3148021300)

local myTable = {
	hello = "world",
	score = 5,
	isCool = true
}
print(repr(myTable)) --> {hello = "world", isCool = true, score = 5}

]]

local defaultSettings = {
	pretty = false,
	robloxFullName = false,
	robloxProperFullName = true,
	robloxClassName = true,
	tabs = false,
	semicolons = false,
	spaces = 3,
	sortKeys = true,
}

-- lua keywords
local keywords = {
	["and"] = true,
	["break"] = true,
	["do"] = true,
	["else"] = true,
	["elseif"] = true,
	["end"] = true,
	["false"] = true,
	["for"] = true,
	["function"] = true,
	["if"] = true,
	["in"] = true,
	["local"] = true,
	["nil"] = true,
	["not"] = true,
	["or"] = true,
	["repeat"] = true,
	["return"] = true,
	["then"] = true,
	["true"] = true,
	["until"] = true,
	["while"] = true,
}

local function isLuaIdentifier(str)
	if type(str) ~= "string" then
		return false
	end
	-- must be nonempty
	if str:len() == 0 then
		return false
	end
	-- can only contain a-z, A-Z, 0-9 and underscore
	if str:find("[^%d%a_]") then
		return false
	end
	-- cannot begin with digit
	if tonumber(str:sub(1, 1)) then
		return false
	end
	-- cannot be keyword
	if keywords[str] then
		return false
	end
	return true
end

-- works like Instance:GetFullName(), but invalid Lua identifiers are fixed (e.g. workspace["The Dude"].Humanoid)
local function properFullName(object, usePeriod)
	if object == nil or object == game then
		return ""
	end

	local s = object.Name
	local usePeriod = true
	if not isLuaIdentifier(s) then
		s = ("[%q]"):format(s)
		usePeriod = false
	end

	if not object.Parent or object.Parent == game then
		return s
	else
		return properFullName(object.Parent) .. (usePeriod and "." or "") .. s
	end
end

local depth = 0
local shown
local INDENT
local reprSettings

local function repr(value: any, reprSettings: any?)
	reprSettings = reprSettings or defaultSettings
	INDENT = (" "):rep(reprSettings.spaces or defaultSettings.spaces)
	if reprSettings.tabs then
		INDENT = "\t"
	end

	local v = value --args[1]
	local tabs = INDENT:rep(depth)

	if depth == 0 then
		shown = {}
	end
	if type(v) == "string" then
		return ("%q"):format(v)
	elseif type(v) == "number" then
		if v == math.huge then
			return "math.huge"
		end
		if v == -math.huge then
			return "-math.huge"
		end
		return tonumber(v)
	elseif type(v) == "boolean" then
		return tostring(v)
	elseif type(v) == "nil" then
		return "nil"
	elseif type(v) == "table" and type(v.__tostring) == "function" then
		return tostring(v.__tostring(v))
	elseif type(v) == "table" and getmetatable(v) and type(getmetatable(v).__tostring) == "function" then
		return tostring(getmetatable(v).__tostring(v))
	elseif type(v) == "table" then
		if shown[v] then
			return "{CYCLIC}"
		end
		shown[v] = true
		local str = "{" .. (reprSettings.pretty and ("\n" .. INDENT .. tabs) or "")
		local isArray = true
		for k, v in pairs(v) do
			if type(k) ~= "number" then
				isArray = false
				break
			end
		end
		if isArray then
			for i = 1, #v do
				if i ~= 1 then
					str = str
						.. (reprSettings.semicolons and ";" or ",")
						.. (reprSettings.pretty and ("\n" .. INDENT .. tabs) or " ")
				end
				depth = depth + 1
				str = str .. repr(v[i], reprSettings)
				depth = depth - 1
			end
		else
			local keyOrder = {}
			local keyValueStrings = {}
			for k, v in pairs(v) do
				depth = depth + 1
				local kStr = isLuaIdentifier(k) and k or ("[" .. repr(k, reprSettings) .. "]")
				local vStr = repr(v, reprSettings)
				--[[str = str .. ("%s = %s"):format(
					isLuaIdentifier(k) and k or ("[" .. repr(k, reprSettings) .. "]"),
					repr(v, reprSettings)
				)]]
				table.insert(keyOrder, kStr)
				keyValueStrings[kStr] = vStr
				depth = depth - 1
			end
			if reprSettings.sortKeys then
				table.sort(keyOrder)
			end
			local first = true
			for _, kStr in pairs(keyOrder) do
				if not first then
					str = str
						.. (reprSettings.semicolons and ";" or ",")
						.. (reprSettings.pretty and ("\n" .. INDENT .. tabs) or " ")
				end
				str = str .. ("%s = %s"):format(kStr, keyValueStrings[kStr])
				first = false
			end
		end
		shown[v] = false
		if reprSettings.pretty then
			str = str .. "\n" .. tabs
		end
		str = str .. "}"
		return str
	elseif typeof then
		-- Check Roblox types
		if typeof(v) == "Instance" then
			return (
				reprSettings.robloxFullName
					and (reprSettings.robloxProperFullName and properFullName(v) or v:GetFullName())
				or v.Name
			) .. (reprSettings.robloxClassName and ((" (%s)"):format(v.ClassName)) or "")
		elseif typeof(v) == "Axes" then
			local s = {}
			if v.X then
				table.insert(s, repr(Enum.Axis.X, reprSettings))
			end
			if v.Y then
				table.insert(s, repr(Enum.Axis.Y, reprSettings))
			end
			if v.Z then
				table.insert(s, repr(Enum.Axis.Z, reprSettings))
			end
			return ("Axes.new(%s)"):format(table.concat(s, ", "))
		elseif typeof(v) == "BrickColor" then
			return ("BrickColor.new(%q)"):format(v.Name)
		elseif typeof(v) == "CFrame" then
			return ("CFrame.new(%s)"):format(table.concat({ v:GetComponents() }, ", "))
		elseif typeof(v) == "Color3" then
			return ("Color3.new(%d, %d, %d)"):format(v.r, v.g, v.b)
		elseif typeof(v) == "ColorSequence" then
			if #v.Keypoints > 2 then
				return ("ColorSequence.new(%s)"):format(repr(v.Keypoints, reprSettings))
			else
				if v.Keypoints[1].Value == v.Keypoints[2].Value then
					return ("ColorSequence.new(%s)"):format(repr(v.Keypoints[1].Value, reprSettings))
				else
					return ("ColorSequence.new(%s, %s)"):format(
						repr(v.Keypoints[1].Value, reprSettings),
						repr(v.Keypoints[2].Value, reprSettings)
					)
				end
			end
		elseif typeof(v) == "ColorSequenceKeypoint" then
			return ("ColorSequenceKeypoint.new(%d, %s)"):format(v.Time, repr(v.Value, reprSettings))
		elseif typeof(v) == "DockWidgetPluginGuiInfo" then
			return ("DockWidgetPluginGuiInfo.new(%s, %s, %s, %s, %s, %s, %s)"):format(
				repr(v.InitialDockState, reprSettings),
				repr(v.InitialEnabled, reprSettings),
				repr(v.InitialEnabledShouldOverrideRestore, reprSettings),
				repr(v.FloatingXSize, reprSettings),
				repr(v.FloatingYSize, reprSettings),
				repr(v.MinWidth, reprSettings),
				repr(v.MinHeight, reprSettings)
			)
		elseif typeof(v) == "Enums" then
			return "Enums"
		elseif typeof(v) == "Enum" then
			return ("Enum.%s"):format(tostring(v))
		elseif typeof(v) == "EnumItem" then
			return ("Enum.%s.%s"):format(tostring(v.EnumType), v.Name)
		elseif typeof(v) == "Faces" then
			local s = {}
			for _, enumItem in pairs(Enum.NormalId:GetEnumItems()) do
				if v[enumItem.Name] then
					table.insert(s, repr(enumItem, reprSettings))
				end
			end
			return ("Faces.new(%s)"):format(table.concat(s, ", "))
		elseif typeof(v) == "NumberRange" then
			if v.Min == v.Max then
				return ("NumberRange.new(%d)"):format(v.Min)
			else
				return ("NumberRange.new(%d, %d)"):format(v.Min, v.Max)
			end
		elseif typeof(v) == "NumberSequence" then
			if #v.Keypoints > 2 then
				return ("NumberSequence.new(%s)"):format(repr(v.Keypoints, reprSettings))
			else
				if v.Keypoints[1].Value == v.Keypoints[2].Value then
					return ("NumberSequence.new(%d)"):format(v.Keypoints[1].Value)
				else
					return ("NumberSequence.new(%d, %d)"):format(v.Keypoints[1].Value, v.Keypoints[2].Value)
				end
			end
		elseif typeof(v) == "NumberSequenceKeypoint" then
			if v.Envelope ~= 0 then
				return ("NumberSequenceKeypoint.new(%d, %d, %d)"):format(v.Time, v.Value, v.Envelope)
			else
				return ("NumberSequenceKeypoint.new(%d, %d)"):format(v.Time, v.Value)
			end
		elseif typeof(v) == "PathWaypoint" then
			return ("PathWaypoint.new(%s, %s)"):format(repr(v.Position, reprSettings), repr(v.Action, reprSettings))
		elseif typeof(v) == "PhysicalProperties" then
			return ("PhysicalProperties.new(%d, %d, %d, %d, %d)"):format(
				v.Density,
				v.Friction,
				v.Elasticity,
				v.FrictionWeight,
				v.ElasticityWeight
			)
		elseif typeof(v) == "Random" then
			return "<Random>"
		elseif typeof(v) == "Ray" then
			return ("Ray.new(%s, %s)"):format(repr(v.Origin, reprSettings), repr(v.Direction, reprSettings))
		elseif typeof(v) == "RBXScriptConnection" then
			return "<RBXScriptConnection>"
		elseif typeof(v) == "RBXScriptSignal" then
			return "<RBXScriptSignal>"
		elseif typeof(v) == "Rect" then
			return ("Rect.new(%d, %d, %d, %d)"):format(v.Min.X, v.Min.Y, v.Max.X, v.Max.Y)
		elseif typeof(v) == "Region3" then
			local min = v.CFrame.p + v.Size * -0.5
			local max = v.CFrame.p + v.Size * 0.5
			return ("Region3.new(%s, %s)"):format(repr(min, reprSettings), repr(max, reprSettings))
		elseif typeof(v) == "Region3int16" then
			return ("Region3int16.new(%s, %s)"):format(repr(v.Min, reprSettings), repr(v.Max, reprSettings))
		elseif typeof(v) == "TweenInfo" then
			return ("TweenInfo.new(%d, %s, %s, %d, %s, %d)"):format(
				v.Time,
				repr(v.EasingStyle, reprSettings),
				repr(v.EasingDirection, reprSettings),
				v.RepeatCount,
				repr(v.Reverses, reprSettings),
				v.DelayTime
			)
		elseif typeof(v) == "UDim" then
			return ("UDim.new(%d, %d)"):format(v.Scale, v.Offset)
		elseif typeof(v) == "UDim2" then
			return ("UDim2.new(%d, %d, %d, %d)"):format(v.X.Scale, v.X.Offset, v.Y.Scale, v.Y.Offset)
		elseif typeof(v) == "Vector2" then
			return ("Vector2.new(%d, %d)"):format(v.X, v.Y)
		elseif typeof(v) == "Vector2int16" then
			return ("Vector2int16.new(%d, %d)"):format(v.X, v.Y)
		elseif typeof(v) == "Vector3" then
			return ("Vector3.new(%d, %d, %d)"):format(v.X, v.Y, v.Z)
		elseif typeof(v) == "Vector3int16" then
			return ("Vector3int16.new(%d, %d, %d)"):format(v.X, v.Y, v.Z)
		elseif typeof(v) == "DateTime" then
			return ("DateTime.fromIsoDate(%q)"):format(v:ToIsoDate())
		else
			return "<Roblox:" .. typeof(v) .. ">"
		end
	else
		return "<" .. type(v) .. ">"
	end
end

return repr