-- Get access to the Waste of Space provided 'module' that lets us access player usernames if
-- we provide it a `UserId`.
local players = require("players")

-- Create a whitelist of `UserId`s, it uses a 'dictionary' format, where the keys are the `UserId`s
-- and the values are `true` so we can access whether someone is whitelisted by doing `WHITELIST[userId]`
-- rather than `table.find(WHITELIST, userId)`, which is both cleaner and "faster" (faster is in quotes
-- because the performance difference is minimal if you only have a few people in your whitelist)
local WHITELIST = {
	[1178125707] = true,
}

-- Don't know your `UserId`? You can find it at runtime by using the `players:GetUserId("YourUsername")`
-- function. Note that this will add a bit of extra overhead (if you're not in the server, waste of
-- space has to make a web request), so it is not reccomended.
-- local WHITELIST = {
--     [players:GetUserId("ArvidSilverlock")] = true,
-- }

-- Try and get the life sensor, throw an informative error if we don't find it using `assert`.
local lifeSensor = assert(Network:GetPart("LifeSensor"), "no life sensor connected")

-- Create a 'function' that we'll run whenever we find a player outside of the whitelist.
-- You may notice the `: number` and `: CFrame`, these are 'types', they tell luau what types
-- of values variables are storing (the `CFrame?` just means `CFrame` or `nil`), which helps
-- with autofill and linting (linting is the squiggly lines that show up when you do bad things)
local function foundThreat(threatUserId: number, position: CFrame?)
	-- Get the player's username from their `UserId` using the `players` module that is
	-- provided by Waste of Space via `require("players")`.
	local threatUsername = players:GetUsername(threatUserId)

	-- If we have been given the position (and therefore the player is within the life sensor range)
	-- then we can utilise the player's position in our operations.
	if position then
		-- What we do here is get the position of the target player *relative* to the life sensor,
		-- then get the 'magnitude' of the resulting value, which is the fancy way of saying 'length'.
		local distance = (position.Position - lifeSensor.Position).Magnitude

		-- Output the player's username and their distance.
		print(`There is a threat! Their name is {threatUsername} and they are {math.floor(distance)} studs away!`)
	else
		-- Output just the player's username, and add a note about how they're too far away to know their distance.
		print(`There is a threat! Their name is {threatUsername}! They are too far away to tell their distance.`)
	end
end

-- Connect to the `Loop` event of the `Microcontroller` that is running the code,
-- this will make the code within run precisely every game tick.
Microcontroller.Loop:Connect(function()
	-- Get an 'array' of the player `UserId`s, this function ignores the range limit.
	local presenceReading = lifeSensor:ListPlayers()

	-- This one returns a 'dictionary' where the keys are the player `UserId's`, and
	-- the values are the positions of the specific players, but it only includes
	-- players within 2,000 studs of the `LifeSensor`, which is why we need
	-- the result of `ListPlayers`.
	local positionalReading = lifeSensor:GetPlayers()

	-- Iterate over each of the players returned by `ListPlayers`, the 'index' variable is
	-- called `_` as is customary when defining a variable that is not ever used.
	for _, userId in presenceReading do
		-- If the user is whitelisted, skip to the next player using a `continue` statement.
		if WHITELIST[userId] then
			continue
		end

		-- Here, we try and get the player's position from the `GetPlayers` dictionary, if the
		-- player is outside of the `LifeSensor`'s range, this will be `nil` and *won't* throw
		-- any sort of error.
		local playerPosition = positionalReading[userId]

		-- Merely call the `foundThread` function, giving it the user who we're talking about,
		-- along with their position (their position *may* or *may not* be specified.)]

		-- Note: If you were to do some computation within `foundThread` that takes a lot of
		--       time (or maybe has a `task.wait` or two), you would want to do something
		--       like `task.spawn(foundThread, userId, playerPosition)` instead, as this
		--       will allow this function to keep looking for other players whilst the
		--       `foundThreat` function is running (rather than waiting for it to finish).
		--       Look into the roblox task scheduler if you want to know more.
		foundThreat(userId, playerPosition)
	end
end)


local polysilicons = GetParts("Polysilicon")

while true do
	for _, polysilicon in polysilicons do
		polysilicon.PolysiliconMode = if math.random() < 0.01
			then "FlipFlop"
			elseif math.random() > 0.5 then "Activate"
			else "Deactivate"
		polysilicon.Frequency = math.random(1, 10)
		polysilicon:Trigger()
	end

	task.wait()
end


local DISPENSE_COOLDOWN = 1

for _, port in Network:GetPorts() do
	local subnet = Network:GetSubnet(port)
	local reactor = subnet:GetPart("Reactor")
	local dispenser = subnet:GetPart("Dispenser")

	if not reactor or not dispenser then
		warn("invalid port", reactor, dispenser)
		continue
	end

	local dispenseCooldown = 0
	dispenser.Filter = "Uranium"

	reactor.Loop:Connect(function(tickInterval)
		dispenseCooldown = math.max(dispenseCooldown - tickInterval, 0)
		if dispenseCooldown > 0 then
			return
		end

		for _, fuel in reactor:GetFuel() do
			if fuel <= 0 then
				dispenser:Dispense()
				dispenseCooldown = DISPENSE_COOLDOWN
				break
			end
		end
	end)
end


-- Try and get the screen part, throw an error if we don't find it using `assert`.
local screen = assert(Network:GetPart("Screen"), "no screen connected")

-- Clear the left over screen elements from the last time GUIs were loaded onto it.
-- Note: If you utilise the `Instance.new` API instead, you do not need to do this,
--       as objects created through said API are *automatically* deleted when the
--       microcontroller stops running.
screen:ClearElements()

-- Call the `CreateElement` method with the `ClassName` of the object you want to create,
-- and a dictionary of the properties to apply (you cannot specify the `Parent` property).
local textLabel = screen:CreateElement("TextLabel", {
	Text = "Hello, world!",
	Size = UDim2.fromScale(1, 1),
	BorderSizePixel = 0,
	BackgroundColor3 = Color3.fromRGB(255, 255, 255),
})

-- Keep the microcontroller on by permanently 'yielding' the code (making it wait).
coroutine.yield()


-- Try and get the screen part, throw an error if we don't find it using `assert`.
local screen = assert(Network:GetPart("Screen"), "no screen connected")

-- Store the current screen size.
local previousScreenSize = screen:GetDimensions()

-- Connect to the `Loop` event of the `Microcontroller` that is running the code,
-- this will make the code within run precisely every game tick.
Microcontroller.Loop:Connect(function()
	-- Get the new current screen dimensions.
	local screenSize = screen:GetDimensions()

	-- Compare these new dimensions with the previously recorded dimensions, if
	-- they are equal, cancel this running code with 'return'.
	if screenSize == previousScreenSize then
		return
	end

	-- Do something with the information about the screen dimensions changing,
	-- in this case, it triggers a print statement.
	print(`The screen size changed! It used to be {previousScreenSize}, but is now {screenSize}!`)

	-- Update the stored current dimensions as to make them accurately reflect reality.
	previousScreenSize = screenSize
end)


local sift = require("sift") -- Grab this fancy library for manipulating tables provided by wos.

-- Get all of the types of objects with a `SwitchValue` (save for `TemperatureGate`) configurable
-- and merge them into one big table.
local objects = sift.Array.merge(
	Network:GetParts("Switch"),
	Network:GetParts("TriggerSwitch"),
	Network:GetParts("Valve"),
	Network:GetParts("Hatch"),
	Network:GetParts("HeatValve")
)

-- Have a 50/50 chance to be on/off when the code starts running.
local initialState = math.random() > 0.5

-- Iterate over all of the switch objects and disable them. `_` is used as a placeholder variable.
for _, switch in objects do
	-- You could alternatively do `switch:Configure({ SwitchValue = initialState })`
	switch.SwitchValue = initialState
end

-- Connect to the `Loop` event of the `Microcontroller` that is running the code,
-- this will make the code within run precisely every game tick.
Microcontroller.Loop:Connect(function()
	-- Iterate over all the switches again!
	for _, switch in objects do
		-- Here we *read* the `SwitchValue`, then invert it, then write it back, as
		-- to toggle the switch.
		switch.SwitchValue = not switch.SwitchValue
	end
end)


local coordinate = require("coordinate")

local teleporter = assert(Network:GetPart("Teleporter"), "expected teleporter to be connected")
local keyboard = assert(Network:GetPart("Keyboard"), "expected keyboard to be connected")

local FALSE_TELEPORTER_COORDINATES = coordinate.new(0, 0, 0, 0, false)

keyboard.TextInputted:Connect(function(text)
	teleporter.Coordinates = coordinate.fromString(text)
	teleporter:Trigger()
	teleporter.Coordinates = FALSE_TELEPORTER_COORDINATES
end)


local UPDATES_PER_TICK = 16 -- How many times to rotate per tick, higher is smoother.
local ANGLE_INCREMENT = 4 -- Every 1/UPDATES_PER_TICK seconds increment the servo angle by this.
local SHIFT_HELD_MULTIPLIER = 0.25 -- When shift is held, multiply the angle increment by this.

-- Who can rotate the servos.
local WHITELIST = {
	[1178125707] = true,
}

-- Get references to all the hardware, everything is *required* except for the
-- seat, which is optional.
local yawServo = assert(Network:GetPartFromPort(1, "Servo"), "no yaw servo connected")
local pitchServo = assert(Network:GetPartFromPort(2, "Servo"), "no pitch servo connected")
local keyboard = assert(Network:GetPart("Keyboard"), "no keyboard connected")
local seat = Network:GetPart("Seat")

-- Make sure the servos are wired properly!
assert(yawServo ~= pitchServo, "there is only one servo connected")

local servoAngles = {}
local heldKeys = {}

local function incrementAngle(servo, direction)
	-- Make the `ANGLE_INCREMENT` negative if the direction is specified as `-1`
	-- with a bit of multiplication.
	local baseIncrement = ANGLE_INCREMENT * direction

	-- If we need to initialise the target angle of the servo:
	if not servoAngles[servo] then
		-- Rotate it back to 0 degrees and initialise the current state.
		servo:SetAngle(0)
		servoAngles[servo] = 0
	end

	-- Here we return a new function, that when called, applies the angle increment
	-- operation to the servo, this is a trick called a "closure".
	return function()
		-- Calculate the new angle, check if we're holding shift, if we are multiply
		-- the increment by the `SHIFT_HELD_MULTIPLIER`
		local multiplier = if heldKeys[Enum.KeyCode.LeftShift] then SHIFT_HELD_MULTIPLIER else 1
		local currentAngle = baseIncrement * multiplier

		-- Update both the real and internal servo angle.
		servo:SetAngle(currentAngle)
		servoAngles[servo] = currentAngle
	end
end

-- Register all of the angle incrementing functions that should keep running
-- whilst the user keeps holding the bind.
local HOLD_BINDS = {
	[Enum.KeyCode.W] = incrementAngle(pitchServo, 1),
	[Enum.KeyCode.A] = incrementAngle(yawServo, 1),
	[Enum.KeyCode.S] = incrementAngle(pitchServo, -1),
	[Enum.KeyCode.D] = incrementAngle(yawServo, -1),
}

-- Register all of the general binds that only run on key down.
local BINDS = {
	[Enum.KeyCode.F] = function()
		TriggerPort(3) -- Assume port 3 has something like guns!
	end,
}

-- If we have a seat, we can detect when the user gets up and clear all the
-- held keys as if they stopped inputting.
if seat then
	seat.OccupantChanged:Connect(function(occupant)
		if occupant then
			return
		end -- If someone just sat down, cancel
		table.clear(heldKeys) -- Whereas if someone got up, clear the held keys
	end)
else
	-- Provide a warning about issues that might occur when no seat is connected.
	warn([[There is no seat connected to the microcontroller!
              If a player jumps whilst holding an input, the input will get stuck!]])
end

keyboard.UserInput:Connect(function(input, userId)
	-- Check if the user is whitelisted, if they're not, cancel.
	if not WHITELIST[userId] then
		return
	end

	-- If the bind pressed has a function to run on key down, run it.
	if BINDS[input.KeyCode] then
		local callback = BINDS[input.KeyCode]
		callback()
	end

	-- If the user *started* pressing the key, register it as held, otherwise
	-- deregister it.
	if input.UserInputState == Enum.UserInputState.Begin then
		heldKeys[input.KeyCode] = true
	elseif input.UserInputState == Enum.UserInputState.End then
		heldKeys[input.KeyCode] = nil
	end
end)

-- For each tick we want to call all the functions that should run for held keys.
-- You could alternatively use a simple `while true do` loop, but this keeps it
-- tick aligned, and tick alignment may provide benefits in some cases.
Microcontroller.Loop:Connect(function(tickDuration)
	for index = 1, UPDATES_PER_TICK do
		for key, callback in HOLD_BINDS do
			-- If we're not holding this bind, look at the next bind.
			if not heldKeys[key] then
				continue
			end
			callback()
		end

		-- Wait a little for for the next time we should update.
		task.wait(tickDuration / UPDATES_PER_TICK)
	end
end)


local documentation = require("../modules/PilotDocumentation")
local Utility = require("../modules/Utility")

local Hydrators = {}

function Hydrators.object(className: string, class: any, vanilla: boolean?)
	local reformattedClass = Utility.deepClone(class)
	local hydrator = reformattedClass.hydrator
	reformattedClass.hydrator = nil

	local resultingType = {}

	if typeof(hydrator) == "table" then
		if hydrator.events then
			for event, eventData in hydrator.events do
				eventData.generics = {
					{ ["string-type"] = event },
					{
						["function"] = {
							parameters = eventData.parameters,
						},
					},
				}
				eventData.type = "Event"
				eventData.parameters = nil
				resultingType[event] = eventData
			end
		end

		if hydrator.methods then
			for method, methodData in hydrator.methods do
				if methodData.overloads then
					methodData.intersection = {}
					for _, overload in methodData.overloads do
						table.insert(methodData.intersection, { method = overload })
					end
				else
					methodData.method = {
						parameters = methodData.parameters,
						returns = methodData.returns,
					}
				end

				methodData.overloads, methodData.parameters, methodData.returns = nil

				resultingType[method] = methodData
			end
		end

		if hydrator.properties then
			for property, propertyData in hydrator.properties do
				if typeof(propertyData.documentation) == "string" then
					propertyData.documentation = {
						description = propertyData.documentation,
						readonly = true,
					}
				elseif propertyData.documentation then
					propertyData.documentation.readonly = true
				else
					propertyData.documentation = { readonly = true }
				end

				resultingType[property] = propertyData
			end
		end

		if hydrator.configurables then
			local configurationTypeName = className .. "Configuration"

			for configurable, configurableData in hydrator.configurables do
				resultingType[configurable] = configurableData
			end

			resultingType.Configure = {
				documentation = "A method to configure the object, will yield until specified string configurables are filtered.",
				method = {
					parameters = {
						{
							name = "configuration",
							documentation = "The configuration to apply to the object",
							type = configurationTypeName,
						},
					},
				},
			}
		end

		resultingType.ClassName = {
			documentation = "The name of the class of the object.",
			["string-type"] = hydrator["class-name"] or className,
		}
	else
		resultingType.ClassName =
			{ documentation = "The name of the class of the object.", ["string-type"] = className }
	end

	if vanilla then
		reformattedClass.intersection = { { type = resultingType } }
		if hydrator.extends then
			if typeof(hydrator.extends) == "table" then
				for _, source in hydrator.extends do
					table.insert(reformattedClass.intersection, 1, { type = source })
				end
			else
				table.insert(reformattedClass.intersection, 1, { type = hydrator.extends })
			end
		elseif not hydrator.abstract then
			table.insert(reformattedClass.intersection, 1, { type = "PilotObject" })
		end
	else
		reformattedClass.class = resultingType
		local inheritanceSource = if hydrator.extends
			then (
				if typeof(hydrator.extends) == "table"
					then hydrator.extends[1] -- sorry
					else hydrator.extends
			)
			elseif not hydrator.abstract then "PilotObject"
			else nil

		reformattedClass.extends = inheritanceSource
	end

	return reformattedClass
end

function Hydrators.getPart(value)
	local copy = Utility.deepClone(value)

	local hydrator = copy.hydrator
	copy.hydrator = nil

	copy["function"] = nil
	copy.method = nil
	copy.intersection = {}

	local initial = if hydrator.port
		then {
			{
				name = "port",
				["nullable-type"] = "PortLike",
			},
		}
		else {}

	local functionType = if hydrator.method then "method" else "function"

	for className, class in documentation.classes do
		if not class.hydrator or typeof(class.hydrator) ~= "table" then
			continue
		end

		local classHydrator = class.hydrator
		if
			not classHydrator.events
			and not classHydrator.methods
			and not classHydrator.properties
			and not classHydrator.configurables
		then
			continue
		end

		local parameters = table.clone(initial)
		table.insert(parameters, { name = "class", ["string-type"] = className })
		table.insert(copy.intersection, {
			[functionType] = {
				parameters = parameters,
				returns = if hydrator.multiple
					then { { ["array-type"] = className } }
					else { { ["nullable-type"] = className } },
			},
		})
	end

	do
		local parameters = table.clone(initial)
		table.insert(parameters, { name = "class", ["nullable-type"] = "string" })
		table.insert(copy.intersection, {
			[functionType] = {
				parameters = parameters,
				returns = if hydrator.multiple
					then { { ["array-type"] = "PilotObject" } }
					else { { type = "PilotObject" } },
			},
		})
	end

	return copy
end

function Hydrators.apply(value)
	if not value.hydrator then
		return value
	end

	local name = value.hydrator.name
	if name == "get-part" then
		return Hydrators.getPart(value)
	end

	return value
end

return Hydrators


local luauTypes = {}

export type TypeStyle = "type" | "function" | "union" | "intersection" | "class"

export type Context = {
	style: TypeStyle,
	isMethod: boolean,
	isNullable: boolean,
	isString: boolean,
	isOpenString: boolean,
	isArray: boolean,
	isSparseArray: boolean,
}

export type TypeGroup = {
	key: { string },
	value: any,
	context: Context,
	generics: { any }?,
	genericDefinitions: { any }?,
}

export type State = {
	isRoot: false,
	base: TypeGroup,
	metatable: TypeGroup,
	self: TypeGroup,
	parent: State | RootState,
	root: RootState,
	source: any,
}

export type TypeContext = "class" | "declaration" | "type"

export type RootState = {
	isRoot: true,
	name: string,
	context: TypeContext?,
	define: boolean?,
}

local NULLABLE_MODIFIER = "nullable"
local STRING_MODIFIER = "string"
local ARRAY_MODIFIER = "array"

local OPEN_STRING_PREFIX = "open"
local SPARSE_ARRAY_PREFIX = "sparse"

local METATABLE_PREFIX = "metatable"
local SELF_PREFIX = "self"

local TYPE_STYLES = { "type", "function", "method", "union", "intersection", "class", "pack" }

local Snippet = require("./Snippet")

local function getTypeStyle(values): string?
	for _, possibleTypeStyle in TYPE_STYLES do
		if table.find(values, possibleTypeStyle) then
			return possibleTypeStyle
		end
	end

	return nil
end

local function getContextTable(value, key, typeStyle, generics, genericDefinitions)
	local stringIndex = table.find(key, STRING_MODIFIER)
	local arrayIndex = table.find(key, ARRAY_MODIFIER)

	return {
		key = key,
		value = value,
		context = {
			style = if typeStyle == "method" then "function" else typeStyle,
			isMethod = typeStyle == "method",
			isNullable = table.find(key, NULLABLE_MODIFIER) ~= nil,
			isString = stringIndex ~= nil,
			isOpenString = stringIndex ~= nil and key[stringIndex - 1] == OPEN_STRING_PREFIX,
			isArray = arrayIndex ~= nil,
			isSparseArray = arrayIndex ~= nil and key[arrayIndex - 1] == SPARSE_ARRAY_PREFIX,
		},
		generics = generics,
		genericDefinitions = genericDefinitions,
	}
end

local function moveMerge(t, key, value)
	local output = t[key]
	if output then
		for subKey, subValue in value do
			assert(output[subKey] == nil, "duplicate keys")
			output[subKey] = subValue
		end
	else
		t[key] = value
	end
end

function luauTypes.getTypeState(source, parent: (State | RootState)?, hydratorCallback): State?
	local output: State = {} :: any

	if hydratorCallback then
		source = hydratorCallback(source)
	end

	for key, value in source do
		if key == METATABLE_PREFIX or key == SELF_PREFIX then
			assert(value.generics, "no generics table found")

			local typeKey = next(value, "generics") or next(value)

			if typeKey then
				local splitKey = typeKey:split("-")
				local typeStyle = getTypeStyle(splitKey)

				local bundled = getContextTable(value[typeKey], splitKey, typeStyle, value.generics)
				moveMerge(output, key, bundled)
			else
				moveMerge(output, key, { generics = value })
			end
		else
			local splitKey = key:split("-")
			local typeStyle = getTypeStyle(splitKey)

			if not typeStyle then
				continue
			end

			if splitKey[1] == METATABLE_PREFIX or splitKey[1] == SELF_PREFIX then
				local bundled = getContextTable(value, splitKey, typeStyle)
				moveMerge(output, splitKey[1], bundled)
			else
				local bundled =
					getContextTable(value, splitKey, typeStyle, source.generics, source["generic-definitions"])
				moveMerge(output, "base", bundled)
			end
		end
	end

	if next(output) then
		if parent then
			output.parent = parent
			output.root = if parent.isRoot then parent else parent.root
		end
		output.source = source
		return output
	else
		return nil
	end
end

function luauTypes.writeFromState(output: Snippet.Snippet, state: State, hydratorCallback)
	local context = state.base.context
	local source = state.source

	if state.parent.isRoot then
		if state.parent.define == false then
			local generics = state.base.genericDefinitions
			if generics then
				output:Push("<")
				for index, genericValue in generics do
					output:Push(genericValue.name)
					if index < #generics then
						output:Push(", ")
					end
				end
				output:Push(">")
			end
		elseif state.root.context == "type" then
			output:Push(if source.export ~= false then "export " else nil, "type ", state.root.name)
			local generics = state.base.genericDefinitions
			if generics then
				output:Push("<")
				for index, genericValue in generics do
					output:Push(genericValue.name)
					if next(genericValue, "name") or next(genericValue) then
						output:Push(" = ")
						luauTypes.write(output, genericValue, state, hydratorCallback)
					end
					if index < #generics then
						output:Push(", ")
					end
				end
				output:Push(">")
			end
			output:Push(" = ")
		elseif state.root.context == "class" then
			output:Push("declare class ", state.root.name)
			if source.extends then
				output:Push(" extends ", source.extends)
			end
			output:NewLine():Indent()
		elseif state.root.context == "declaration" then
			if context.style == "function" then
				output:Push("declare function ", state.root.name)
			else
				output:Push("declare ", state.root.name, ": ")
			end
		end
	end

	if state.metatable then
		local metatableContext = state.metatable.context

		output:PushLine("typeof(setmetatable("):Indent():Push("{} :: ")
		luauTypes[`write{context.style:gsub("^%l", string.upper)}`](output, state, state.base, hydratorCallback)
		output:Push(",\n{} :: ")
		luauTypes[`write{metatableContext.style:gsub("^%l", string.upper)}`](
			output,
			state,
			state.metatable,
			hydratorCallback
		)
		output:Dedent():NewLine():Push("))")
	else
		luauTypes[`write{context.style:gsub("^%l", string.upper)}`](output, state, state.base, hydratorCallback)
	end

	if state.parent.isRoot then
		if state.root.context == "class" then
			output:Dedent():PushLine("end")
		else
			output:NewLine()
		end
	end
end

function luauTypes.write(output: Snippet.Snippet, source: any, parent: State, hydratorCallback)
	local state = assert(luauTypes.getTypeState(source, parent, hydratorCallback), "no context found")
	luauTypes.writeFromState(output, state, hydratorCallback)
end

local function writeKey(output: Snippet.Snippet, key: { string } | string)
	if typeof(key) == "table" then
		local indexer = (key :: any)["!indexer"]
		for k, _ in key do
			if k ~= "!indexer" then
				error("malformed indexer")
			end
		end

		output:Push("[", indexer, "]: ")
	elseif typeof(key) == "string" then
		if key:match("^[_%a][_%w]*$") then
			output:Push(key, ": ")
		else
			output:Push("[", string.format("%q", key), "]: ")
		end
	else
		error("invalid luau key")
	end
end

function luauTypes.writeType(output: Snippet.Snippet, state: State, group: TypeGroup, hydratorCallback)
	local value, context = group.value, group.context

	if context.isArray then
		output:Push("{ ")
	end

	if context.isString then
		if typeof(value) == "string" then
			output:Push(string.format("%q", value))
		else
			error("expected a string literal")
		end
	else
		if typeof(value) == "string" or typeof(value) == "boolean" then
			output:Push(tostring(value))

			if group.generics then
				output:Push("<")
				for index, subValue in group.generics do
					luauTypes.write(output, subValue, state, hydratorCallback)
					if index < #group.generics then
						output:Push(", ")
					end
				end
				output:Push(">")
			end
		elseif typeof(value) == "table" then
			output:PushLine("{"):Indent()
			for key, subValue in value do
				writeKey(output, key)
				luauTypes.write(output, subValue, state, hydratorCallback)
				output:PushLine(",")
			end
			output:Dedent():Push("}")
		else
			error("expected a string literal or dictionary")
		end
	end

	if context.isArray then
		if context.isSparseArray then
			output:Push("?")
		end
		output:Push(" }")
	end

	if context.isNullable then
		output:Push("?")
	end
end

function luauTypes.writePack(output: Snippet.Snippet, state: State, _group: TypeGroup, hydratorCallback)
	local parameters = state.base.value

	output:Push("(")
	for index, parameter in parameters do
		luauTypes.write(output, parameter, state, hydratorCallback)
		if index < #parameters then
			output:Push(", ")
		end
	end
	output:Push(")")
end

local function getParameters(state: State, group: TypeGroup)
	if not state.base.context.isMethod then
		return group.value.parameters
	end

	local node: RootState | State = state
	local previous
	while node ~= nil and not node.isRoot and not node.self do
		node, previous = node.parent, node
	end

	local parameters = if group.value.parameters then table.clone(group.value.parameters) else {}

	if node and node.isRoot then
		local generics
		if previous.base.genericDefinitions then
			generics = {}
			for _, generic in previous.base.genericDefinitions do
				table.insert(generics, { type = generic.name })
			end
		end

		table.insert(parameters, 1, {
			name = "self",
			type = node.name,
			generics = generics,
		})
	elseif node and not node.isRoot then
		local selfType = node.self

		local keyTable = table.clone(selfType.key)
		if keyTable[1] == SELF_PREFIX then
			table.remove(keyTable, 1)
		end
		local key = table.concat(keyTable, "-")

		table.insert(parameters, 1, {
			name = "self",
			[key] = selfType.value,
			generics = selfType.generics,
		})
	else
		table.insert(parameters, 1, { name = "self", type = "unknown" })
	end

	return parameters
end

function luauTypes.writeFunction(output: Snippet.Snippet, state: State, group: TypeGroup, hydratorCallback)
	local value, context = group.value, group.context
	assert(not context.isString, "you cannot have a string literal function")

	local isDirectlyUnderRoot = if state.root.context == "declaration"
		then state.parent and state.parent.isRoot
		else state.parent and (state :: any).parent.parent and (state :: any).parent.parent.isRoot
	local isClassMethod = isDirectlyUnderRoot and (state.root.context == "declaration" or state.root.context == "class")

	local needsBrackets = not state.parent.isRoot
		and (state.parent.base.context.style == "union" or state.parent.base.context.style == "intersection")

	local parameters = getParameters(state, group)
	local returns = value.returns

	if needsBrackets then
		output:Push("(")
	end

	output:Push("(")

	if parameters and #parameters > 0 then
		for index, parameter in parameters do
			local shouldWriteType = if isClassMethod then parameter.name ~= "self" else true

			if isClassMethod or parameter.name ~= "..." then
				output:Push(parameter.name)
			else
				output:Push("...")
			end

			if shouldWriteType then
				if parameter.name and (isClassMethod or parameter.name ~= "...") then
					output:Push(": ")
				end
				luauTypes.write(output, parameter, state, hydratorCallback)
			end

			if index < #parameters then
				output:Push(", ")
			end
		end
	end

	if isClassMethod then
		output:Push("): ")
	else
		output:Push(") -> ")
	end

	if returns and #returns > 0 then
		output:Push("(")

		for index, returnValue in returns do
			if returnValue.name == "..." then
				output:Push("...")
			end
			luauTypes.write(output, returnValue, state, hydratorCallback)
			if index < #returns then
				output:Push(", ")
			end
		end

		output:Push(")")
	else
		output:Push("()")
	end

	if needsBrackets then
		output:Push(")")
	end

	if context.isNullable then
		output:Push("?")
	end
end

function luauTypes.writeUnion(output: Snippet.Snippet, state: State, group: TypeGroup, hydratorCallback)
	local value, context = group.value, group.context
	local parent = state.parent

	if context.isArray then
		output:Push("{ ")
		if context.isSparseArray then
			output:Push("(")
		end
	elseif parent and parent.style == "intersection" then
		output:Push("(")
		if context.isNullable then
			output:Push("(")
		end
	elseif context.isNullable then
		output:Push("(")
	end

	if context.isString then
		for index, subValue in value do
			output:Push(string.format("%q", subValue))
			if context.isOpenString or index < #value then
				output:Push(" | ")
			end
		end

		if context.isOpenString then
			output:Push("string")
		end
	else
		for index, subValue in value do
			luauTypes.write(output, subValue, state, hydratorCallback)
			if index < #value then
				output:Push(" | ")
			end
		end
	end

	if context.isArray then
		if context.isSparseArray then
			output:Push(")?")
		end
		output:Push(" }")
	elseif parent and parent.style == "intersection" then
		output:Push(")")
	elseif context.isNullable then
		output:Push(")")
	end

	if context.isNullable then
		output:Push("?")
	end
end

function luauTypes.writeIntersection(output: Snippet.Snippet, state: State, group: TypeGroup, hydratorCallback)
	local value, context = group.value, group.context

	assert(not context.isString, "you cannot have a string literal intersection")

	local parent = state.parent

	if context.isArray then
		output:Push("{ ")
		if context.isSparseArray then
			output:Push("(")
		end
	elseif context.isNullable or (parent and parent.style == "union") then
		output:Push("(")
	end

	for index, type in value do
		luauTypes.write(output, type, state, hydratorCallback)
		if index < #value then
			output:Push(" & ")
		end
	end

	if context.isArray then
		if context.isSparseArray then
			output:Push(")?")
		end
		output:Push(" }")
	elseif parent and parent.style == "union" then
		output:Push(")")
	end

	if context.isNullable then
		output:Push("?")
	end
end

function luauTypes.writeClass(output: Snippet.Snippet, state: State, group: TypeGroup, hydratorCallback)
	local value = group.value

	assert(state.parent.isRoot, "classes must be defined as the root")

	for key, member in value do
		local memberState = assert(luauTypes.getTypeState(member, state, hydratorCallback), "no context found")

		if memberState.base.context.style == "function" and memberState.base.context.isMethod then
			assert(
				typeof(key) == "string" and key:match("^[_%a][_%w]*$"),
				"class methods must have valid luau identifiers as keys"
			)
			output:Push("function ", key)
		else
			writeKey(output, key)
		end

		luauTypes.writeFromState(output, memberState)
		output:NewLine()
	end
end

function luauTypes.setNullable(type, nullable: boolean, hydratorCallback)
	local typeState = luauTypes.getTypeState(type, nil, hydratorCallback)
	if typeState.base.context.isNullable == nullable then
		return table.clone(type)
	end

	local currentKey = typeState.base.key
	local newKey = table.clone(currentKey)

	if nullable then
		table.insert(newKey, 1, NULLABLE_MODIFIER)
	else
		local index = table.find(newKey, NULLABLE_MODIFIER)
		if index then
			table.remove(newKey, index)
		end
	end

	local typeCopy = table.clone(type)

	local currentKeyString = table.concat(currentKey, "-")
	local newKeyString = table.concat(newKey, "-")

	typeCopy[newKeyString] = typeCopy[currentKeyString]
	typeCopy[currentKeyString] = nil

	return typeCopy
end

return luauTypes


local Snippet = require("./Snippet")

local MarkdownSnippet = Snippet.new() :: any
MarkdownSnippet.__index = MarkdownSnippet
MarkdownSnippet.__tostring = Snippet.__tostring

function MarkdownSnippet.new(title: string)
	local self = setmetatable({}, MarkdownSnippet)
	self.title = title
	self._lines = {}
	self._indent = 0
	self._bulletPointDepth = 0
	self._toc = {}
	return self
end

function MarkdownSnippet.Insert(self: MarkdownSnippet, index: number?, value: any?)
	local str = tostring(value)
	if str == nil or str == "" then
		return self
	end

	local indent = string.rep("\t", self._indent)
	local text = {}

	local prefix = ""
	if self._bulletPointDepth > 0 then
		prefix = string.rep("  ", self._bulletPointDepth) .. "- "
	end

	local previousLine = self._lines[#self._lines]
	if previousLine == nil or previousLine:match("\n%s*$") then
		table.insert(text, indent)
		table.insert(text, prefix)
	end

	table.insert(text, (str:gsub("\n", "\n" .. indent):gsub("\n%s+\n", "\n\n")))
	table.insert(self._lines, index or #self._lines + 1, table.concat(text))

	return self
end

function MarkdownSnippet.BlockStart(self: MarkdownSnippet, block: string)
	self:Push("<", block, ">")
	return self
end

function MarkdownSnippet.BlockEnd(self: MarkdownSnippet, block: string)
	self:Push("</", block, ">")
	return self
end

function MarkdownSnippet.InlineWrap(self: MarkdownSnippet, block, ...)
	self:BlockStart(block):Push(...):BlockEnd(block)
	return self
end

function MarkdownSnippet.MultilineWrap(self: MarkdownSnippet, block, ...)
	self:BlockStart(block):PushLine(...):BlockEnd(block)
	return self
end

function MarkdownSnippet.Delimiter(self: MarkdownSnippet)
	self:Push("\n\n---\n\n")
	return self
end

function MarkdownSnippet.BulletPointUp(self: MarkdownSnippet)
	self._bulletPointDepth += 1
	return self
end

function MarkdownSnippet.BulletPointDown(self: MarkdownSnippet)
	self._bulletPointDepth -= 1
	return self
end

function MarkdownSnippet.MemberHeader(self: MarkdownSnippet, id: string, title: string?, group: string?, depth: number?)
	if group and group ~= "group" then
		self:Delimiter()
	else
		self:Push("\n\n")
	end

	self:Push('<Member id="', id, '" group="', group, '"')

	if depth then
		self:Push(" depth={", depth, "}")
	end

	self:Push(">", title or id, "</Member>\n\n")

	return self
end

local function createInlineWrapper(block)
	return function(self, ...)
		self:InlineWrap(block, ...)
		return self
	end
end

local function createMultilineWrapper(block: string, blockEnd: string?)
	if blockEnd then
		return function(self: MarkdownSnippet, ...)
			self:PushLine(block):Push(...):PushLine(blockEnd)
			return self
		end, function(self: MarkdownSnippet)
			self:PushLine(block)
			return self
		end, function(self: MarkdownSnippet)
			self:PushLine(blockEnd)
			return self
		end
	else
		return function(self: MarkdownSnippet, ...)
			self:MultilineWrap(block, ...)
			return self
		end, function(self: MarkdownSnippet)
			self:BlockStart(block)
			return self
		end, function(self: MarkdownSnippet)
			self:BlockEnd(block)
			return self
		end
	end
end

MarkdownSnippet.Bold = createInlineWrapper("b")
MarkdownSnippet.Italic = createInlineWrapper("i")
MarkdownSnippet.Code, MarkdownSnippet.CodeStart, MarkdownSnippet.CodeEnd = createMultilineWrapper("```lua", "```")
MarkdownSnippet.InlineCode = createInlineWrapper("code")
MarkdownSnippet.Strikethrough = createInlineWrapper("del")

function MarkdownSnippet.AdmonitionStart(self: MarkdownSnippet, blockName: string, title: string?)
	self:Push("\n:::", blockName)
	if title then
		self:Push("[", title, "]")
	end
	self:Push("\n\n")
	return self
end

function MarkdownSnippet.AdmonitionEnd(self: MarkdownSnippet)
	self:PushLine("\n\n:::\n\n")
	return self
end

local function createAdmonitionWrapper(blockName: string)
	return function(self, ...)
		self:AdmonitionStart(blockName):Push(...):AdmonitionEnd()
		return self
	end
end

MarkdownSnippet.NoteAdmonition = createAdmonitionWrapper("note")
MarkdownSnippet.TipAdmonition = createAdmonitionWrapper("tip")
MarkdownSnippet.InfoAdmonition = createAdmonitionWrapper("info")
MarkdownSnippet.WarningAdmonition = createAdmonitionWrapper("warning")
MarkdownSnippet.DangerAdmonition = createAdmonitionWrapper("danger")

export type MarkdownSnippet = any

return MarkdownSnippet


local fs = require("@lune/fs")
local serde = require("@lune/serde")

local luauTypes = require("../modules/LuauType")

local documentation = serde.decode("yaml", fs.readFile("documentation/documentation.yaml")) :: any

documentation.configurations = {}

for className, class in documentation.classes do
	local hydrator = class.hydrator
	if typeof(hydrator) == "table" then
		if hydrator.configurables then
			local configurationTypeName = className .. "Configuration"
			documentation.configurations[configurationTypeName] = {
				documentation = `The configuration of the <code>{className}</code> object.`,
				type = { [{ ["!indexer"] = "string" }] = { type = "any" } }, -- yeah, i know
			}

			for configurable, configurableData in hydrator.configurables do
				documentation.configurations[configurationTypeName].type[configurable] =
					luauTypes.setNullable(configurableData, true)
			end
		end
	end
end

return documentation


local fs = require("@lune/fs")

local Utility = require("../modules/Utility")

local Darklua = {}

function Darklua.save(options, _generatedData)
	local darkluaConfig = {
		bundle = {
			require_mode = {
				name = "luau",
				use_luau_configuration = true,
			},
			excludes = {},
		},
		rules = {
			"remove_types",
			"remove_comments",
		},
	}

	for _, module in fs.readDir("./documentation/modules") do
		table.insert(darkluaConfig.bundle.excludes, Utility.stripFileExtension(module))
	end

	Utility.writeConfigurationFile(`{options.o}/.darklua.json`, "json", darkluaConfig)
end

return Darklua


local fs = require("@lune/fs")

local luauTypes = require("../modules/LuauType")
local Snippet = require("../modules/Snippet")
local hydrators = require("../modules/Hydrators")

local documentation = require("../modules/PilotDocumentation")

local Definition = {}

local function writeTypeGroup(snippet, key: string, value: any, context: luauTypes.TypeContext)
	local typeState = luauTypes.getTypeState(value, {
		isRoot = true,
		name = key,
		context = context,
		define = true,
	}, hydrators.apply)

	if typeState then
		luauTypes.writeFromState(snippet, typeState, hydrators.apply)
	else
		for childName, child in hydrators.apply(value) do
			writeTypeGroup(snippet, childName, child, context)
		end
	end
end

function Definition.generate(_options)
	local snippet = Snippet.new()

	writeTypeGroup(snippet, "globals", documentation.globals, "declaration")
	writeTypeGroup(snippet, "types", documentation.types, "type")

	local independentClasses = {}
	local inheritanceDependencies = {}
	local classSnippets = {}

	for className, class in documentation.classes do
		local reformattedClass = hydrators.object(className, class)

		local classSnippet = Snippet.new()
		writeTypeGroup(classSnippet, className, reformattedClass, "class")
		classSnippets[className] = classSnippet

		local inheritanceSource = reformattedClass.extends
		if inheritanceSource then
			if inheritanceDependencies[inheritanceSource] then
				table.insert(inheritanceDependencies[inheritanceSource], className)
			else
				inheritanceDependencies[inheritanceSource] = { className }
			end
		else
			table.insert(independentClasses, className)
		end
	end

	local classStack = table.clone(independentClasses)

	while #classStack > 0 do
		local currentClass = table.remove(classStack)
		local classSnippet = classSnippets[currentClass]

		snippet:Push(tostring(classSnippet))

		local dependantClasses = inheritanceDependencies[currentClass]
		if dependantClasses then
			table.move(dependantClasses, 1, #dependantClasses, #classStack + 1, classStack)
		end
	end

	writeTypeGroup(snippet, "configurations", documentation.configurations, "type")

	return tostring(snippet)
end

function Definition.save(options, generatedData)
	fs.writeFile(`{options.o}/{options["types-dir"]}/global.d.luau`, generatedData)
end

return Definition


local fs = require("@lune/fs")
local serde = require("@lune/serde")

local luauTypes = require("../modules/LuauType")
local hydrators = require("../modules/Hydrators")
local documentation = require("../modules/PilotDocumentation")

local NO_PARAMETER_RESERVED = "__RESERVE__NO_PARAMETERS___RESERVE_"
local NO_PARAMETER_REMOVE = `"params":%s*{string.format("%q", NO_PARAMETER_RESERVED)}`

local function extractDescription(value): string?
	if not value.documentation then
		return nil
	elseif typeof(value.documentation) == "string" then
		return value.documentation
	elseif typeof(value.documentation) == "table" then
		return value.documentation.description
	else
		error(`Invalid documentation type: {typeof(value.documentation)}`)
	end
end

local documentFunctionParameters, documentType

function documentFunctionParameters(group: "parameters" | "returns", output, key, state)
	local list: { { documentation: string, name: string } | string } = {}

	if not state.base.value[group] then
		return list
	end

	local prefix
	local indexOffset = 1

	if group == "parameters" then
		prefix = `{key}/params`

		if state.base.context.isMethod then
			table.insert(list, {
				name = "self",
				documentation = key,
			})
			indexOffset = 0
		end
	else
		prefix = `{key}/returns`
	end

	for parameterIndex, parameter in state.base.value[group] do
		local substate = assert(luauTypes.getTypeState(parameter, state), key)

		local parameterKey = `{prefix}/{parameterIndex - indexOffset}`

		documentType(output, parameterKey, substate)

		if group == "parameters" then
			table.insert(list, {
				name = parameter.name or `parameter{parameterIndex}`,
				documentation = parameterKey,
			})
		else
			table.insert(list, parameterKey)
		end
	end

	return list
end

function documentType(output, key: string, state: luauTypes.State)
	local context = state.base.context
	local value = state.base.value

	if (context.style == "type" or context.style == "class") and typeof(value) == "table" then
		local keys = {}

		for memberName, memberValue in value do
			if typeof(memberName) ~= "string" then
				continue
			end

			local substate = assert(luauTypes.getTypeState(memberValue, state), key)

			local subkey = `{key}.{memberName}`
			documentType(output, subkey, substate)

			keys[memberName] = subkey
		end

		output[key] = {
			documentation = extractDescription(state.source),
			keys = if next(keys) then keys else nil,
		}
	elseif context.style == "function" then
		local parameters = documentFunctionParameters("parameters", output, key, state)
		local returns = documentFunctionParameters("returns", output, key, state)

		output[key] = {
			documentation = extractDescription(state.source),
			params = if next(parameters) then parameters else NO_PARAMETER_RESERVED,
			returns = if next(returns) then returns else nil,
		}
	else
		output[key] = {
			documentation = extractDescription(state.source),
		}
	end

	return { key }
end

local function writeTypeGroup(output, prefix: string, key: string, value: any, context: luauTypes.TypeContext)
	if value.hydrator and value.hydrator.name == "get-part" then
		value = hydrators.getPart(value)
	end

	local typeState = luauTypes.getTypeState(value, {
		isRoot = true,
		name = key,
		context = context,
		define = true,
	})

	if typeState then
		documentType(output, `{prefix}/{key}`, typeState)
	else
		for childName, child in value do
			writeTypeGroup(output, prefix, childName, child, context)
		end
	end
end

local Documentation = {}

function Documentation.generate()
	local output = {}

	writeTypeGroup(output, "@roblox/global", "globals", documentation.globals, "declaration")
	writeTypeGroup(output, "@roblox/globaltype", "types", documentation.types, "type")

	for className, class in documentation.classes do
		local reformattedClass = hydrators.object(className, class)
		writeTypeGroup(output, "@roblox/globaltype", className, reformattedClass, "class")
	end

	writeTypeGroup(output, "@roblox/globaltype", "configurations", documentation.configurations, "type")

	return output
end

function Documentation.save(options, generatedData)
	local jsonData = serde.encode("json", generatedData, true):gsub(NO_PARAMETER_REMOVE, '"params": []')
	fs.writeFile(`{options.o}/{options["types-dir"]}/documentation.d.json`, jsonData)
end

return Documentation


local fs = require("@lune/fs")

local Utility = require("../modules/Utility")

local MODULE_SOURCE_DIRECTORY = "./documentation/modules"

local Modules = {}

function Modules.save(options)
	local outputDirectory, typesDirectory = options.o, options["types-dir"]
	local modulesDirectory = `{outputDirectory}/{typesDirectory}/modules`

	if not fs.isDir(modulesDirectory) then
		fs.writeDir(modulesDirectory)
	end

	local fileRequireAliases = {}
	for _, module in fs.readDir(MODULE_SOURCE_DIRECTORY) do
		local modulePath = `{outputDirectory}/{typesDirectory}/modules/{module}`
		local filePath

		if fs.isFile(modulePath) then
			fs.removeFile(modulePath)
			filePath = `./{typesDirectory}/modules/{module}`
		elseif fs.isDir(modulePath) then
			fs.removeDir(modulePath)
			filePath = `./{typesDirectory}/modules/{module}/init.luau`
		end

		fileRequireAliases[Utility.stripFileExtension(module)] = filePath
		fs.copy(`{MODULE_SOURCE_DIRECTORY}/{module}`, modulePath)
	end

	Utility.writeConfigurationFile(`{options.o}/.luaurc`, "json", {
		aliases = {
			wos = `./{typesDirectory}/modules`,
		},
	})
end

return Modules


local serde = require("@lune/serde")
local fs = require("@lune/fs")

local documentation = require("../modules/PilotDocumentation")
local Utility = require("../modules/Utility")
local luauTypes = require("../modules/LuauType")
local hydrators = require("../modules/Hydrators")
local Snippet = require("../modules/Snippet")

local VALID_SELENE_TYPES = {
	"any",
	"bool",
	"function",
	"nil",
	"number",
	"string",
	"table",
	"...",
}

local NO_PARAMETER_RESERVED = "__RESERVE__NO_PARAMETERS___RESERVE_"
local NO_PARAMETER_REMOVE = `args:\n%s*%-%s*{string.format("%q", NO_PARAMETER_RESERVED):sub(2, -2)}`

local Selene = {}

local function documentSeleneType(typeState): any
	local isOverloadedFunction = true
	if typeState.base.context.style == "intersection" then
		for _, intersection in typeState.base.value do
			local result = luauTypes.getTypeState(intersection, typeState)
			if not (result and result.base.context.style == "function") then
				isOverloadedFunction = false
				break
			end
		end
	else
		isOverloadedFunction = false
	end

	if isOverloadedFunction then
		return { args = { { type = "..." } } }
	elseif typeState.base.context.style == "function" then
		local functionDefinition: any = { args = {} }

		local parameters = typeState.base.value.parameters

		if parameters ~= nil and #parameters > 0 then
			for _, argument in parameters do
				if argument.Name == "..." then
					table.insert(functionDefinition.args, { type = "..." })
				else
					local parameterTypeState =
						assert(luauTypes.getTypeState(argument, typeState), "parameter has no type state")
					local typeContext = parameterTypeState.base.context

					table.insert(functionDefinition.args, {
						type = documentSeleneType(parameterTypeState),
						required = if typeContext.isNullable then false else nil,
					})
				end
			end
		else
			table.insert(functionDefinition.args, NO_PARAMETER_RESERVED)
		end

		return functionDefinition
	elseif typeState.base.context.style == "type" then
		if typeof(typeState.base.value) == "string" then
			local luauTypeSnippet = Snippet.new()
			luauTypes.write(luauTypeSnippet, luauTypes.setNullable(typeState.source, false), typeState)

			local trimmed = tostring(luauTypeSnippet):match("^%s*(.-)%s*$") :: string

			return {
				type = if table.find(VALID_SELENE_TYPES, trimmed) then trimmed else { display = trimmed },
			}
		else
			local dictionary = { property = "read-only" }

			for subkey, subvalue in typeState.base.value do
				local subTypeState = assert(luauTypes.getTypeState(subvalue, typeState), "parameter has no type state")
				dictionary[subkey] = documentSeleneType(subTypeState)
			end

			return dictionary
		end
	else
		return { property = "read-only", ["*"] = { any = true } }
	end
end

local function writeTypeGroup(output, key: string, value: any, context: luauTypes.TypeContext)
	if value.hydrator and value.hydrator.name == "get-part" then
		value = hydrators.getPart(value)
	end

	local typeState = luauTypes.getTypeState(value, {
		isRoot = true,
		name = key,
		context = context,
		define = true,
	})

	if typeState then
		output.globals[key] = documentSeleneType(typeState)
	else
		for childName, child in value do
			writeTypeGroup(output, childName, child, context)
		end
	end
end

function Selene.generate(_options)
	local output = {
		base = "roblox",
		name = "pilot",
		globals = {},
	}

	writeTypeGroup(output, "globals", documentation.globals, "type")

	return output
end

function Selene.save(options, generatedData)
	local outputDirectory, typesDirectory = options.o, options["types-dir"]

	local yamlData = serde.encode("yaml", generatedData, true):gsub(NO_PARAMETER_REMOVE, "args: []")
	fs.writeFile(`{options.o}/{options["types-dir"]}/pilot.yml`, yamlData)

	Utility.writeConfigurationFile(`{outputDirectory}/selene.toml`, "toml", {
		std = `luau+{typesDirectory}/pilot`,
	})
end

return Selene


local Utility = require("../modules/Utility")

local EXTENSIONS = {
	luaulsp = "JohnnyMorganz.luau-lsp",
	selene = "Kampfkarren.selene-vscode",
}

-- group 1 is a prefix of characters to apply a case transformation to, you insert a space between group 2 & 3
local SPLIT_PASCAL_REGEX = "(.*?)([a-z]|[0-9](?=[a-z]|[A-Z])|[A-Z](?=[A-Z][a-z0-9]))([A-Z]|[0-9]*)?"

-- group 1 is the first word, group 2 is the rest of the pascal variable
local FIRST_PASCAL_FIRST_WORD_REGEX = "^([A-Z]+?)(?=[A-Z][a-z]|[a-z]|$)(.*)$"

local GET_PART_FORMAT = 'local %s = assert(Network:GetPart("%s"), "%s")'
local GET_PORT_FORMAT = 'local %s = assert(Network:GetPartFromPort(%s, "%s"), "%s")'

local REGEX_FORMAT = "${%%d/%s/%s/g}"
local PASCAL_TO_PRINTABLE = REGEX_FORMAT:format(SPLIT_PASCAL_REGEX, "${1:/downcase}${2:/downcase}${3:+ }${3:/downcase}")

local function getPascalCaseTransform(caseStyle)
	local pattern, replace

	if caseStyle == "snake" then
		pattern = SPLIT_PASCAL_REGEX
		replace = "${1:/downcase}${2:/downcase}${3:+_}${3:/downcase}"
	elseif caseStyle == "manual" then
		return "${1:screen}"
	elseif caseStyle == "pascal" then
		return "${1:Screen}"
	else -- default to camel
		pattern = FIRST_PASCAL_FIRST_WORD_REGEX
		replace = "${1:/downcase}${2}"
	end

	return REGEX_FORMAT:format(pattern, replace):format(1)
end

local function formatMessage(message, partPattern, portPattern)
	message = message:gsub("<part>", PASCAL_TO_PRINTABLE:format(partPattern))
	if portPattern then
		message = message:gsub("<port>", portPattern)
	end
	return message
end

local function getPartSnippet(casing, message, transformation)
	local partName, messageFormat

	if casing == "manual" then
		partName, messageFormat = "${2:Screen}", formatMessage(message, 2)
	else
		partName, messageFormat = "${1:Screen}", formatMessage(message, 1)
	end

	return GET_PART_FORMAT:format(transformation, partName, messageFormat)
end

local function getPartFromPortSnippet(casing, message, transformation)
	local portIndex, partType, messageFormat

	if casing == "manual" then
		portIndex, partType = "${3:1}", "${2:Screen}"
		messageFormat = formatMessage(message, 2, "${3}")
	else
		portIndex, partType = "${2:1}", "${1:Screen}"
		messageFormat = formatMessage(message, 1, "${2}")
	end

	return GET_PORT_FORMAT:format(transformation, portIndex, partType, messageFormat)
end

local VSCode = {}

function VSCode.save(options)
	local typesDirectory = options["types-dir"]
	local path = Utility.pathJoiner(options.o)

	local recommendedExtensions = {}

	table.insert(recommendedExtensions, EXTENSIONS.luaulsp)
	if options.selene then
		table.insert(recommendedExtensions, EXTENSIONS.selene)
	end

	Utility.makeDirectory(path(".vscode"))

	Utility.writeConfigurationFile(path(".vscode/settings.json"), "json", {
		["luau-lsp.sourcemap.enabled"] = false,
		["luau-lsp.completion.imports.suggestServices"] = false,
		["luau-lsp.platform.type"] = "roblox",
		["luau-lsp.types.definitionFiles"] = { `./{typesDirectory}/global.d.luau` },
		["luau-lsp.types.documentationFiles"] = { `./{typesDirectory}/documentation.d.json` },
	})

	Utility.writeConfigurationFile(path(".vscode/extensions.json"), "json", {
		recommendations = recommendedExtensions,
	})

	local transformation = getPascalCaseTransform(options["snippet-casing"])

	local snippets = {
		GetPart = {
			prefix = "getpart",
			description = "Snippet to create a variable to get a part, asserting that it is connected.",
			scope = "luau,lua",
			body = {
				getPartSnippet(options["snippet-casing"], options["snippet-part-message"], transformation),
			},
		},
		GetPartFromPort = {
			prefix = "getport",
			description = "Snippet to create a variable to get a part from a port, asserting that it is connected.",
			scope = "luau,lua",
			body = {
				getPartFromPortSnippet(options["snippet-casing"], options["snippet-port-message"], transformation),
			},
		},
	}
	Utility.writeConfigurationFile(path(".vscode/pilot.code-snippets"), "json", snippets, true)
end

return VSCode


-- Hardcoded indefinite articles for all uses. This is to avoid non-phonetic spellings messing with the articles.
local indefiniteArticles = {
	json = "a",
	range = "a",
	network = "a",
	port = "a",
	random = "a",
	callback = "a",
	event = "an",
	file = "a",
	arcursor = "an",
	blade = "a",
	boolean = "a",
	canvas = "a",
	cframe = "a",
	color = "a",
	component = "a",
	coordinates = "a",
	coupler = "a",
	cursor = "a",
	enum = "an",
	folder = "a",
	frame = "a",
	handle = "a",
	instance = "an",
	instrument = "an",
	microcontroller = "a",
	modem = "a",
	number = "a",
	object = "an",
	one = "a", -- not used anywhere, but included just in case
	part = "a",
	permission = "a",
	pilot = "a",
	player = "a",
	polysilicon = "a",
	region = "a",
	relay = "a",
	remote = "a",
	resource = "a",
	scanner = "a",
	sound = "a",
	string = "a",
	user = "a",
	vector = "a",
	vehicle = "a",
	world = "a",
}

local function determineIndefiniteArticle(str)
	local firstWord = (str:match("^%u*%l*") or str):lower()
	local indefiniteArticle = indefiniteArticles[firstWord]

	-- Guess the indefinite article based on the first letter, not reliable ('an umbrella' vs 'a uniform').
	if not indefiniteArticle then
		warn(`No guarantee the used indefinite article of '{firstWord}' is correct.`)
		return if table.find({ "a", "e", "i", "o" }, str:sub(1, 1):lower()) then "an" else "a"
	end

	return indefiniteArticle
end

return determineIndefiniteArticle


local function escapeMarkdown(text)
	local markdownCharacters = [[\`*_{}[%]()#+-.!|<>]]
	return (text:gsub("([" .. markdownCharacters .. "])", "\\%1"))
end

return escapeMarkdown


local sizes = {}

function sizes.formatSize(x, y, z)
	return (string.format("%.2fx%.2fx%.2f", x, y, z):gsub("%.00", ""):gsub("(%.%d)0*", "%1"))
end

-- This finds all possible sizes that multiply to the given malleability,
-- with each dimension being at most 2048 and an integer.
function sizes.findPossibleSizes(malleability)
	local possibleSizes = {}

	for i = 1, math.min(math.sqrt(malleability), 2048) do
		if malleability % i ~= 0 then
			continue
		end

		local remaining = malleability / i
		for j = i, math.min(math.sqrt(remaining), 2048) do
			if remaining % j ~= 0 then
				continue
			end

			local k = remaining / j

			local order = { i, j, k }
			table.sort(order)

			local formatted = sizes.formatSize(unpack(order))
			if k <= 2048 and not table.find(possibleSizes, formatted) then
				table.insert(possibleSizes, formatted)
			end
		end
	end

	return possibleSizes
end

return sizes


local luauTypes = require("../../modules/LuauType")
local Snippet = require("../../modules/Snippet")

local escapeMarkdown = require("./EscapeMarkdown")

local LuauTypeStrings = {}

function LuauTypeStrings.getType(value)
	local output = Snippet.new()
	luauTypes.write(output, value, { isRoot = true, context = "type", define = false } :: any)
	return tostring(output):match("^%s*(.-)%s*$")
end

function LuauTypeStrings.getMarkdownType(value)
	return escapeMarkdown(
		(assert(LuauTypeStrings.getType(value), "no type to generate markdown of"):gsub("\n\t*", " "):gsub(", }", " }"))
	)
end

return LuauTypeStrings


-- stylua: ignore start
local UNITS = { "first", "second", "third", "fourth", "fifth", "sixth", "seventh", "eighth", "ninth" }
local TEENS = { "eleventh", "twelfth", "thirteenth", "fourteenth", "fifteenth", "sixteenth", "seventeenth", "eighteenth", "nineteenth" }
local TENS = { "tenth", "twentieth", "thirtieth", "fortieth", "fiftieth", "sixtieth", "seventieth", "eightieth", "ninetieth" }
local TENS_PREFIX = { "", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety" }
-- stylua: ignore end

local function numberToOrdinalWord(number)
	assert(number < 1 or number > 99, "number must be between 1 and 99")

	if number <= 9 then
		return UNITS[number]
	elseif number == 10 then
		return TENS[1]
	elseif number >= 11 and number <= 19 then
		return TEENS[number - 10]
	elseif number % 10 == 0 then
		return TENS[number / 10]
	else
		return TENS_PREFIX[number // 10] .. "-" .. UNITS[number % 10]
	end
end

return numberToOrdinalWord


local function sortedIterate<T>(dictionary: { [string]: T }, deprioritizeDeprecated: boolean?): () -> (string, T)
	local primary, secondary = {}, {}
	for key, value in dictionary do
		if
			typeof(value) == "table"
			and deprioritizeDeprecated
			and value.documentation
			and value.documentation.deprecated
		then
			table.insert(secondary, key)
		else
			table.insert(primary, key)
		end
	end

	table.sort(primary)
	if deprioritizeDeprecated then
		table.sort(secondary)
	end

	local index = 0
	return function()
		index += 1
		local key = primary[index] or secondary[index - #primary]
		return key, dictionary[key]
	end
end

return sortedIterate


-- Not actually real URL encoding, but this seems to work for docusaurus links, at least for the limited subset of symbols in the link names.
local function urlEncode(url: string)
	return url:gsub(" ", "%%20")
end

return urlEncode


local fs = require("@lune/fs")
local serde = require("@lune/serde")

local MarkdownSnippet = require("../../../modules/MarkdownSnippet")

local rawPartData = serde.decode("json", fs.readFile("documentation/partdata.json")) :: any

local CodeSamples = require("./CodeSamples")
local sortedIterate = require("../SortedIterate")
local sizes = require("../FindPossibleSizes")

local function appendDocumentation(
	output: MarkdownSnippet.MarkdownSnippet,
	object: any,
	metadata: any?,
	dontWriteDescription: boolean?,
	objectName: string?,
	imageLink: string?
)
	if imageLink then
		output
			:PushLine('
')
			:Indent()
			:PushLine('')
	end

	if typeof(object.documentation) == "string" then
		if not dontWriteDescription then
			output:PushLine(object.documentation)
		end
		return
	elseif typeof(object.documentation) ~= "table" then
		return
	end

	local documentation = object.documentation

	if typeof(documentation) == "table" then
		if documentation.description and not dontWriteDescription then
			output:Push(documentation.description)
		end

		local incrementSuffix = if documentation.increment
			then ", and an increment of " .. documentation.increment
			else ""
		if documentation.minimum then
			output:PushLine("It has a minimum value of ", documentation.minimum, incrementSuffix, ".")
		elseif documentation.maximum then
			output:PushLine("It has a maximum value of ", documentation.maximum, incrementSuffix, ".")
		else
			if documentation.range then
				output:PushLine("It ranges between ", documentation.range[1], " and ", documentation.range[2], ".")
			end
			if documentation.increment then
				output:PushLine("It increments by ", documentation.increment, ".")
			end
		end
	elseif documentation and not dontWriteDescription then
		output:Push(documentation)
	end

	if documentation.length then
		output:PushLine("It will always have a length of ", documentation.length, ".\n")
	elseif documentation["max-length"] then
		output:PushLine("It has a maximum length of ", documentation["max-length"], ".\n")
	end

	local rawData = rawPartData.Parts[objectName]
	if rawData then
		output:Push("\n")

		output:Push(
			`It is a{if rawData.Craftable and rawData.Spawnable
				then " craftable and spawnable"
				elseif rawData.Craftable then " craftable"
				elseif rawData.Spawnable then " spawnable"
				else ""} {if rawData.Flammable then "flammable" else "non-flammable"} {if rawData.ClassState
				then rawData.ClassState:lower()
				else "object"}.\n\n`
		)

		if rawData.ResourceType and rawData.ResourceAmount then
			if rawData.ResourceType == "Power" then
				output:Push(`It consumes {rawData.ResourceAmount} power per tick.\n\n`)
			elseif rawData.ResourceType ~= "Trigger" then
				output:Push(`It generates {rawData.ResourceAmount} <code>{rawData.ResourceType}</code> per tick.\n\n`)
			end
		end

		if rawData.Malleability == 0 then
			output:Push(`It cannot be resized.\n\n`)
		elseif rawData.Malleability then
			local possibleSizes = sizes.findPossibleSizes(rawData.Malleability)
			if #possibleSizes > 0 then
				output:Push(
					`Here is a list of possible sizes that reach the maximum malleability ({rawData.Malleability}) that have integer components: `
				)
				output:Push(table.concat(possibleSizes, ", "))
				output:Push("\n\n")
			else
				output:Push(
					"It has a malleability of ",
					rawData.Malleability,
					". You cannot reach this size with only integer components.\n\n"
				)
			end
		end

		local defaultSize = sizes.formatSize(unpack(rawData.BaseSize))

		if rawData.BaseDurability == "inf" then
			output:Push("It has infinite durability and a default size of ", defaultSize, ".\n\n")
		elseif rawData.BaseDurability then
			local baseVolume = rawData.BaseSize[1] * rawData.BaseSize[2] * rawData.BaseSize[3]
			output:Push("At its default size (", defaultSize, ") it has a durability of ", rawData.BaseDurability)

			if rawData.Malleability and rawData.Malleability > 0 then
				output:Push(
					", at its maximum size it has a durability of ",
					math.floor((rawData.Malleability / baseVolume) ^ (1 / 3) * rawData.BaseDurability + 1)
				)
			end

			output:Push(".\n\n")
		elseif rawData.BaseSize then
			output:PushLine("It has a default size of ", defaultSize, ".\n\n")
		end

		if rawData.Color then
			local hex = string.format("%02x%02x%02x", unpack(rawData.Color))
			output:PushLine(
				"By default, its colour is #",
				hex,
				".\n"
			)
		end

		if rawData.Recipe then
			local recipe = { "It requires " }
			for key, value in sortedIterate(rawData.Recipe) do
				table.insert(recipe, `{value} <code>{key}</code>`)
				if #recipe > 1 then
					table.insert(recipe, ", ")
				end
			end

			if #recipe > 3 then
				recipe[#recipe - 2] = if #recipe == 5 then " and " else ", and "
				recipe[#recipe] = " "
			end
			table.insert(recipe, "to be crafted.")

			output:Push(table.concat(recipe) .. "\n\n")
		end
	end

	if imageLink then
		output:Dedent():PushLine("\n
")
	end

	if metadata then
		metadata.tags = if documentation.categories then table.clone(documentation.categories) else {}
	end

	CodeSamples.addCodeSamples(output, object)
end

return appendDocumentation


local fs = require("@lune/fs")

local CodeSamples = {}

function CodeSamples.addIndividualCodeSample(output, codeSample)
	if not codeSample then
		return
	end

	output:Push("\n```lua\n")

	local source = if codeSample:match("^.-%.luau$")
		then fs.readFile(`documentation/programs/{codeSample}`)
		else codeSample
	local trimmedSource = source:match("^%s*(.-)%s*$")
	output:Push(trimmedSource)

	output:Push("\n```")
end

function CodeSamples.addCodeSamples(output, data)
	if typeof(data.documentation) ~= "table" then
		return
	end

	if data.documentation["code-sample"] then
		CodeSamples.addIndividualCodeSample(output, data.documentation["code-sample"])
	end

	local codeSamples = data.documentation["code-samples"]
	if codeSamples then
		for _, codeSample in codeSamples do
			CodeSamples.addIndividualCodeSample(output, codeSample)
		end
	end
end

return CodeSamples


local fs = require("@lune/fs")
local serde = require("@lune/serde")

local MarkdownSnippet = require("../../../modules/MarkdownSnippet")

local LuauTypeStrings = require("../LuauTypeStrings")
local escapeMarkdown = require("../EscapeMarkdown")
local determineIndefiniteArticle = require("../DetermineIndefiniteArticle")
local sortedIterate = require("../SortedIterate")
local urlEncode = require("../UrlEncode")

local CodeSamples = require("./CodeSamples")
local writeParameters = require("./Parameters")
local appendDocumentation = require("./AppendDocumentation")

local function writeGroupHeader(output, tableOfContents, header: string)
	output:MemberHeader(header)
	table.insert(tableOfContents, `\{value: "{header}", id: "{header}", "level": 2\}`)
end

local function writeMemberHeader(
	output,
	tableOfContents,
	header: string,
	group: "method" | "event" | "configurable" | "property",
	id: string,
	isDeprecated: boolean?
)
	output:MemberHeader(id, header, group)

	local tableOfContentsHeader = id .. if group == "method" then "()" else ""
	if isDeprecated then
		tableOfContentsHeader = "" .. tableOfContentsHeader .. ""
	end

	table.insert(tableOfContents, `\{value: "{tableOfContentsHeader}", id: "{id}", "level": 3\}`)
end

local function documentMethod(output, tableOfContents, objectName, methodData, methodName, isDeprecated)
	local parameterNames = {}
	for index, parameter in methodData.parameters or {} do
		table.insert(parameterNames, parameter.name or `<i>{string.char(96 + index)}</i>`)
	end

	local returnNames = {}
	for index, value in methodData.returns or {} do
		table.insert(returnNames, value.name or `<i>{string.char(96 + index)}</i>`)
	end

	writeMemberHeader(
		output,
		tableOfContents,
		(if isDeprecated then "" else "")
			.. methodName
			.. "("
			.. table.concat(parameterNames, ", ")
			.. ")"
			.. (if #returnNames > 1
				then " → (" .. table.concat(returnNames, ", ") .. ")"
				elseif #returnNames > 0 then " → " .. returnNames[1]
				else "")
			.. (if isDeprecated then "" else ""),
		"method",
		methodName,
		isDeprecated
	)

	if isDeprecated then
		output:AdmonitionStart("warning")
		output:Push("This method is deprecated.")
		if typeof(isDeprecated) == "string" then
			output:Push(" " .. isDeprecated)
		end
		output:AdmonitionEnd()
	end

	if typeof(methodData.documentation) == "table" then
		if methodData.documentation.description then
			output:Push(methodData.documentation.description)
		end
	elseif methodData.documentation then
		output:Push(methodData.documentation)
	end

	if methodData.parameters then
		writeParameters(output, objectName, methodName, methodData.parameters, "parameter", "parameters")
	end

	if methodData.returns then
		writeParameters(output, objectName, methodName, methodData.returns, "return", "returns")
	end

	CodeSamples.addCodeSamples(output, methodData)
end

local function documentOverloadedMethod(output, tableOfContents, objectName, methodData, methodName, isDeprecated)
	writeMemberHeader(
		output,
		tableOfContents,
		(if isDeprecated then "" else "") .. methodName .. (if isDeprecated then "" else ""),
		"method",
		methodName,
		isDeprecated
	)

	if isDeprecated then
		output:Push("\n:::warning\n\n")
		output:Push("This method is deprecated.")
		if typeof(isDeprecated) == "string" then
			output:Push(" " .. isDeprecated)
		end
		output:Push("\n\n:::\n\n")
	end

	if typeof(methodData.documentation) == "table" then
		if methodData.documentation.description then
			output:Push(methodData.documentation.description)
		end
	elseif methodData.documentation then
		output:Push(methodData.documentation)
	end

	for index, overload in methodData.overloads do
		local parameterNames = {}
		for parameterIndex, parameter in overload.parameters or {} do
			table.insert(parameterNames, parameter.name or `<i>{string.char(96 + parameterIndex)}</i>`)
		end

		local returnNames = {}
		for returnIndex, value in overload.returns or {} do
			table.insert(returnNames, value.name or `<i>{string.char(96 + returnIndex)}</i>`)
		end

		output:Push(
			"\n\n#### Overload ",
			string.char(64 + index),
			" – (",
			table.concat(parameterNames, ", "),
			")",
			if #returnNames > 1
				then " → (" .. table.concat(returnNames, ", ") .. ")"
				elseif #returnNames > 0 then " → " .. returnNames[1]
				else "",
			"\n"
		)

		if typeof(overload.documentation) == "table" then
			if overload.documentation.description then
				output:Push(overload.documentation.description)
			end
		elseif overload.documentation then
			output:Push(overload.documentation)
		end

		if overload.parameters then
			writeParameters(output, objectName, methodName, overload.parameters, "parameter", "parameters")
		end

		if overload.returns then
			writeParameters(output, objectName, methodName, overload.returns, "return", "returns")
		end

		CodeSamples.addCodeSamples(output, methodData)
	end
end

local function documentMethods(output, tableOfContents, methods, objectName)
	writeGroupHeader(output, tableOfContents, "Methods")
	for methodName, methodData in sortedIterate(methods, true) do
		local isDeprecated = if typeof(methodData.documentation) == "table"
			then methodData.documentation.deprecated
			else false

		local document = if methodData.overloads then documentOverloadedMethod else documentMethod
		document(output, tableOfContents, objectName, methodData, methodName, isDeprecated)
	end
end

local function documentConfigurables(output, tableOfContents, configurables, readonly)
	writeGroupHeader(output, tableOfContents, if readonly then "Properties" else "Configurables")

	for configurableName, configurableData in sortedIterate(configurables, true) do
		writeMemberHeader(output, tableOfContents, configurableName, "configurable", configurableName)

		local configurableDocs = configurableData.documentation
		local typeString = LuauTypeStrings.getType(configurableData) or "unknown"
		local configurableType =
			`It is {determineIndefiniteArticle(typeString)} <code>{escapeMarkdown(typeString)}</code>.`

		if typeof(configurableDocs) == "table" then
			if configurableDocs.description then
				output:Push(configurableDocs.description)
				output:Push(" " .. configurableType)
			else
				output:Push(configurableType)
			end

			local incrementSuffix = if configurableDocs.increment
				then ", and an increment of " .. configurableDocs.increment
				else ""
			if configurableDocs.minimum then
				output:Push("\nIt has a minimum value of ", configurableDocs.minimum, incrementSuffix, ".")
			elseif configurableDocs.maximum then
				output:Push("\nIt has a maximum value of ", configurableDocs.maximum, incrementSuffix, ".")
			elseif configurableDocs.range then
				output:Push("\nIt ranges between ", configurableDocs.range[1], " and ", configurableDocs.range[2], ".")
				if configurableDocs.increment then
					output:Push(" It increments by ", configurableDocs.increment, ".")
				end
			elseif configurableDocs.increment then
				output:Push("\nIt increments by ", configurableDocs.increment, ".")
			end
		elseif configurableDocs then
			output:Push(configurableDocs, " ", configurableType)
		else
			output:Push(configurableType)
		end

		CodeSamples.addCodeSamples(output, configurableData)
	end
end

local function documentEvents(output, tableOfContents, events, objectName)
	writeGroupHeader(output, tableOfContents, "Events")
	for eventName, eventData in sortedIterate(events, true) do
		local parameterNames = {}
		for index, parameter in eventData.parameters or {} do
			table.insert(parameterNames, parameter.name or `<i>{string.char(96 + index)}</i>`)
		end

		local header = eventName .. if #parameterNames > 0 then `({table.concat(parameterNames, ", ")})` else ""
		writeMemberHeader(output, tableOfContents, header, "event", eventName)

		if typeof(eventData.documentation) == "table" then
			if eventData.documentation.description then
				output:Push("\n" .. eventData.documentation.description)
			end
		elseif eventData.documentation then
			output:Push("\n" .. eventData.documentation)
		end

		if eventData.parameters then
			writeParameters(output, objectName, eventName, eventData.parameters, "parameter", "parameters")
		end
		CodeSamples.addCodeSamples(output, eventData)
	end
end

local function handleHydrator(output, objectName, tableOfContents, metadata, hydrator)
	local isProgrammable = false
	local isConfigurable = false

	if hydrator.methods and next(hydrator.methods) then
		isProgrammable = true
		documentMethods(output, tableOfContents, hydrator.methods)
	end

	if hydrator.configurables and next(hydrator.configurables) then
		isConfigurable = true
		documentConfigurables(output, tableOfContents, hydrator.configurables, false)
	end

	if hydrator.properties and next(hydrator.properties) then
		documentConfigurables(output, tableOfContents, hydrator.properties, true)
	end

	if hydrator.events and next(hydrator.events) then
		local events = hydrator.events

		local badEvents = { "Configured", "Damaged", "Loop", "OnClick", "Triggered" }
		for event, _ in events do
			if not table.find(badEvents, event) then
				isProgrammable = true
			end
		end

		documentEvents(output, tableOfContents, events, objectName)
	end

	table.insert(metadata.tags, if isProgrammable then "Programmable" else "Non-Programmable")
	table.insert(metadata.tags, if isConfigurable then "Configurable" else "Non-Configurable")
end

local function documentObject(objectName: string, object: any, codeBlockLinks)
	if not object.hydrator then
		return
	end

	codeBlockLinks[objectName] = `/objects/{urlEncode(objectName)}`

	objectName = object.hydrator["class-name"] or objectName

	local output = MarkdownSnippet.new(objectName)
	local metadata: any = {}

	local tableOfContents = {}

	if objectName == "PilotObject" then
		metadata.sidebar_position = 1
	end

	local imageExists = fs.isFile(`./wiki/static/part-images/{objectName}.png`)
	local imageLink = if imageExists then `/Pilot.lua-Luau-LSP/part-images/{objectName}.png` else nil

	appendDocumentation(output, object, metadata, false, objectName, imageLink)

	if not metadata.tags then
		metadata.tags = {}
	end

	if typeof(object.hydrator) == "table" then
		handleHydrator(output, objectName, tableOfContents, metadata, object.hydrator)
	else
		table.insert(metadata.tags, "Non-Programmable")
		table.insert(metadata.tags, "Non-Configurable")
	end

	if #tableOfContents > 0 then
		output:Insert(1, `\nexport const toc = [{table.concat(tableOfContents, ",")}]\n\n`)
		output:Insert(1, '\nimport Member from "@theme/Member";\n\n')
	end

	if next(metadata) then
		output:Insert(1, `---\n{serde.encode("yaml", metadata)}---\n`)
	end

	return tostring(output)
end

return documentObject


local luauTypes = require("../../../modules/LuauType")
local MarkdownSnippet = require("../../../modules/MarkdownSnippet")

local numberToOrdinalWord = require("../NumberToOrdinalWord")
local determineIndefiniteArticle = require("../DetermineIndefiniteArticle")
local sortedIterate = require("../SortedIterate")

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

local function writeUnionType(output, context, base)
	if context.isString then
		if context.isOpenString then
			output:Push(" can be any string, some example possibilities include: ")
		else
			output:Push(" is a string that can be ")
		end
		for index, value in base.value do
			if index == #base.value then
				if #base.value > 2 then
					output:Push(", ")
				end
				output:Push(" or ")
			elseif index > 1 then
				output:Push(", ")
			end
			output:InlineCode(string.format("%q", value):sub(2, -2))
		end
	else
		output:Push(" can be any of the following types:\n"):BulletPointUp()
		for _, value in base.value do
			output:InlineCode(LuauTypeStrings.getMarkdownType(value)):NewLine()
		end
		output:BulletPointDown():Push("\n")
	end
end

local function writeTableType(output, context, base)
	local indexer, value = next(base.value)
	assert(indexer, "never")

	output
		:Push(" is a dictionary with keys that are ")
		:InlineCode(indexer[1])
		:Push("s and values that are ")
		:InlineCode(LuauTypeStrings.getMarkdownType(value))
		:Push("s.")
end

local function writeStandardType(output, context, base)
	if base.value == "any" then
		output:Push(" can be anything.")
	elseif context.isArray then
		output:Push(" is an array of ", determineIndefiniteArticle(base.value), " "):InlineCode(base.value):Push("s.")
	elseif context.isString then
		output:Push(" is the string "):InlineCode(base.value):Push(".")
	else
		output:Push(" is ", determineIndefiniteArticle(base.value), " "):InlineCode(base.value):Push(".")
	end
end

local function writeMapType(output, context, base)
	output:PushLine(" is a dictionary:")
	output:BulletPointUp()
	for key, value in sortedIterate(base.value) do
		local t = LuauTypeStrings.getMarkdownType(value)
		output:InlineCode(key):Push(" is ", determineIndefiniteArticle(t), " "):InlineCode(t):PushLine(".")
	end
	output:BulletPointDown()
end

local function writeParameters(
	output: MarkdownSnippet.MarkdownSnippet,
	objectName,
	memberName,
	parameters,
	groupName,
	groupNamePlural
)
	for parameterIndex, parameter in parameters do
		if #parameters == 1 then
			output:Push("\n\nThe ")
			if parameter.name == "..." then
				output:Push(`variadic {groupName}`)
			elseif parameter.name then
				output:Push(`<b>{parameter.name}</b> {groupName}`)
			else
				output:Push(`only {groupName}`)
			end
		else
			if parameterIndex == 1 then
				output:Push("\n\nThe ", groupNamePlural, " for "):InlineCode(memberName):PushLine(" are as follows:")
			end

			output:Push("- The ")
			if parameter.name == "..." then
				output:Push(`variadic {groupName}`)
			elseif parameter.name then
				output:Push(`<b>{parameter.name}</b> {groupName}`)
			else
				output:Push(`{numberToOrdinalWord(parameterIndex)} {groupName}`)
			end
		end

		local description = if typeof(parameter.documentation) == "table"
			then parameter.documentation.description
			else parameter.documentation
		if description then
			output:Push(" is ", description:gsub("^%u", string.lower):gsub("\n+", " "), " It")
		end

		local base = assert(
			luauTypes.getTypeState(parameter, {
				isRoot = true,
				name = parameter.name or "_" .. tostring(parameterIndex),
				context = "type",
				define = false,
			}),
			"no type state"
		).base
		local context = base.context

		if context.style == "union" then
			writeUnionType(output, context, base)
		elseif context.style == "type" and typeof(base.value) == "table" and typeof((next(base.value))) == "table" then
			writeTableType(output, context, base)
		elseif context.style == "type" and typeof(base.value) == "string" then
			writeStandardType(output, context, base)
		elseif context.style == "type" then
			writeMapType(output, context, base)
		elseif context.style == "function" then
			output:Push(" is a function of type "):InlineCode(LuauTypeStrings.getMarkdownType(parameter)):Push(".")
		else
			error(`{objectName}.{groupNamePlural}.{parameter.name or parameterIndex}`)
		end

		if context.isNullable then
			if output._lines[#output._lines]:sub(-1, -1) ~= "\n" then
				output:Push(" ")
			end
			output:Push("It can also be nil.")
		end

		output:Push("\n")

		-- output:Push(luauType)
	end
end

return writeParameters


local luauTypes = require("../../../modules/LuauType")
local MarkdownSnippet = require("../../../modules/MarkdownSnippet")

local appendDocumentation = require("./AppendDocumentation")
local writeParameters = require("./Parameters")
local sortedIterate = require("../SortedIterate")
local urlEncode = require("../UrlEncode")

local writeIndividualTypeDescription

local function writeWithoutState(output, name, source: any, parent: luauTypes.State, depth, tableOfContents)
	local state = assert(luauTypes.getTypeState(source, parent), "no context found")
	writeIndividualTypeDescription(output, name, state, depth, tableOfContents)
end

local function writeDescription(output, documentation)
	if typeof(documentation) == "table" then
		if documentation.deprecated then
			output:AdmonitionStart("warning")
			output:Push("This method is deprecated.")
			if typeof(documentation.deprecated) == "string" then
				output:Push(" ", documentation.deprecated)
			end
			output:AdmonitionEnd()
		end

		if documentation.warning then
			output:AdmonitionStart("danger")
			if typeof(documentation.warning) == "string" then
				output:Push(" ", documentation.warning)
			else
				output:Push("This method may be dangerous.")
			end
			output:AdmonitionEnd()
		end

		if documentation.description then
			output:PushLine(documentation.description)
		end
	elseif typeof(documentation) == "string" then
		output:PushLine(documentation)
	end
end

local function writeStandardType(output, context, state, depth, tableOfContents)
	if typeof(state.base.value) == "table" then
		if context.isArray then
			output:PushLine("It is an array of dictionaries of the form:")
		end

		for memberName, memberValue in sortedIterate(state.base.value) do
			writeWithoutState(output, memberName, memberValue, state, depth + 1, tableOfContents)
		end
	elseif context.isString then
		output:Push("It is always ")
		if context.isArray then
			output:Push("an array of ")
		end
		output:Push("the string "):InlineCode(state.base.value):PushLine(".")
	else
		output:Push("It is ")
		if context.isArray then
			output:Push("an array whose values are ")
		end
		output:Push("of the type "):InlineCode(state.base.value):PushLine(".")
	end
end

local function writeFunctionType(output, source, name, state)
	local parameters = state.base.value.parameters
	local returns = state.base.value.returns

	if parameters and #parameters > 0 then
		writeParameters(output, source.name, name, parameters, "parameter", "parameters")
	else
		output:PushLine("\n\nIt has no parameters.")
	end

	if returns and #returns > 0 then
		writeParameters(output, source.name, name, returns, "return", "returns")
	else
		output:PushLine("\n\nIt has no return values.")
	end
end

local function writeStringUnionType(output, context, state)
	if context.isArray then
		output:Push("It is an array whose values ")
	else
		output:Push("It ")
	end
	if context.isOpenString then
		output:Push("can be any string, some "):Italic("example"):Push(" possibilities include: ")
	else
		output:Push("can be any of the following strings: ")
	end
	for index, memberValue in state.base.value do
		output:InlineCode(string.format("%q", memberValue))
		if index < #state.base.value then
			output:Push(", ")
		end
	end
	output:PushLine(".")
end

local function writeUnionType(output, context, state, depth, tableOfContents)
	if context.isString then
		writeStringUnionType(output, context, state)
		return
	end
	output:Push("It is ")
	if context.isArray then
		output:Push("an array whose values are all ")
	end
	output:PushLine("one (not all) of the following types:")
	for index, memberValue in state.base.value do
		writeWithoutState(output, `Subtype {string.char(64 + index)}`, memberValue, state, depth + 1, tableOfContents)
	end
end

local function writeIntersectionType(output, context, state, depth, tableOfContents)
	output:Push("It is ")
	if context.isArray then
		output:Push("an array whose values are all ")
	end
	output:PushLine("the combination of the following types:")
	for index, memberValue in state.base.value do
		writeWithoutState(output, `Subtype {string.char(64 + index)}`, memberValue, state, depth + 1, tableOfContents)
	end
end

function writeIndividualTypeDescription(
	output: MarkdownSnippet.MarkdownSnippet,
	key,
	state: luauTypes.State,
	depth,
	tableOfContents
)
	local context = state.base.context
	local source = state.source

	if context.style == "type" and state.base.value == "never" then
		return
	end

	local headerText = key
	if typeof(key) == "table" then
		headerText = "[" .. (key :: any)["!indexer"] .. "]"
		for k, _ in key do
			if k ~= "!indexer" then
				error("malformed indexer")
			end
		end
	end

	if depth > 1 then
		if context.style == "function" then
			local parameterNames, returnNames = {}, {}

			if context.isMethod then
				table.insert(parameterNames, "self")
			end

			for index, parameter in state.base.value.parameters or {} do
				table.insert(parameterNames, parameter.name or `<i>{string.char(96 + index)}</i>`)
			end
			for index, value in state.base.value.returns or {} do
				table.insert(returnNames, value.name or `<i>{string.char(96 + index)}</i>`)
			end

			headerText ..= "(" .. table.concat(parameterNames, ", ") .. ")" .. (if #returnNames > 1
				then " → (" .. table.concat(returnNames, ", ") .. ")"
				elseif #returnNames > 0 then " → " .. returnNames[1]
				else "")
		end

		output
			:MemberHeader(
				headerText,
				headerText,
				if context.style == "function" then (if context.isMethod then "method" else "function") else "property",
				depth + 1
			)
			:NewLine()

		table.insert(
			tableOfContents,
			`\{value: "{headerText .. if context.style == "function" then "()" else ""}", id: "{headerText}", "level": {depth}\}`
		)
	end

	writeDescription(output, source.documentation)
	appendDocumentation(output, source, nil, true)

	if context.style == "type" then
		writeStandardType(output, context, state, depth, tableOfContents)
	elseif context.style == "function" then
		writeFunctionType(output, source, headerText, state)
	elseif context.style == "union" then
		writeUnionType(output, context, state, depth, tableOfContents)
	elseif context.style == "intersection" then
		writeIntersectionType(output, context, state, depth, tableOfContents)
	else
		error(`Unknown context style: {context.style}`)
	end

	if context.isNullable then
		output:PushLine("It may also be nil.")
	end

	output:NewLine(2)
end

local function generateTypePage(key: string, value: any, parent, path: string, generatedPages: any, codeBlockLinks: any)
	local typeState = luauTypes.getTypeState(value, {
		isRoot = true,
		name = key,
		context = "type",
		define = true,
	})

	local encodedKey = urlEncode(key)
	if typeState then
		codeBlockLinks[key] = if path:match("[^/]+$") == encodedKey then path else `{path}/{urlEncode(key)}`

		local tableOfContents = {}

		local snippet = MarkdownSnippet.new()

		snippet:PushLine('\nimport Member from "@theme/Member";\n\n')

		writeIndividualTypeDescription(snippet, key, typeState, 1, tableOfContents)

		snippet:MemberHeader("Luau Type", "Luau Type", "group")
		table.insert(tableOfContents, '{value: "Luau Type", id: "Luau Type", "level": 2}')

		snippet
			:Push("This is the luau type for ")
			:InlineCode(key)
			:PushLine(
				". It may help clear up mistranslations from the raw data to the wiki page (as it is an automatic process)."
			)
			:CodeStart()

		luauTypes.writeFromState(snippet, typeState)

		snippet:CodeEnd():PushLine("\n\n")

		if #tableOfContents > 0 then
			snippet:Insert(1, `\nexport const toc = [{table.concat(tableOfContents, ",")}]\n\n`)
		end

		generatedPages[key] = tostring(snippet)
	else
		generatedPages[key] = {}
		for childName, child in value do
			generateTypePage(childName, child, parent, `{path}/{encodedKey}`, generatedPages[key], codeBlockLinks)
		end
	end
end

return generateTypePage


local fs = require("@lune/fs")

local MarkdownSnippet = require("./../modules/MarkdownSnippet")

local partDocumentation = require("./../modules/PilotDocumentation")

local WIKI_DIRECTORY = "./wiki/docs/"

local generateTypePage = require("@self/Write/Types")
local generateObjectPage = require("@self/Write/Objects")

local Wiki = {}

function Wiki.generate(_options)
	local codeBlockLinks = {}
	local generatedPages = {
		objects = {},
		types = {},
	}

	generateTypePage("types", partDocumentation.types, nil, "", generatedPages, codeBlockLinks)

	for objectName, object in partDocumentation.classes do
		generatedPages.objects[objectName] = generateObjectPage(objectName, object, codeBlockLinks)
	end

	return generatedPages, codeBlockLinks
end

local function recursiveWrite(pages, path, codeBlockLinks)
	if not fs.isDir(path) then
		fs.writeDir(path)
	end

	for name, value in pages do
		if typeof(value) == "table" then
			recursiveWrite(value, `{path}/{name}`, codeBlockLinks)
			continue
		end

		local content = value:gsub("<code>(.-)</code>", function(match)
			local codeBlock = `<code>{match}</code>`
			return if codeBlockLinks[match] then `[{codeBlock}]({codeBlockLinks[match]})` else codeBlock
		end)

		local writePath = `{path}/{name}.mdx`
		if not fs.isFile(writePath) or fs.readFile(writePath) ~= content then
			fs.writeFile(writePath, content)
		end
	end
end

function Wiki.save(options, generatedPages, codeBlockLinks)
	recursiveWrite(generatedPages.objects, `{WIKI_DIRECTORY}/objects`, codeBlockLinks)
	recursiveWrite(generatedPages.types, `{WIKI_DIRECTORY}/types`, codeBlockLinks)

	local rawDocumentation = fs.readFile("documentation/documentation.yaml")
	local rawDocumentationSnippet = MarkdownSnippet.new(rawDocumentation)

	rawDocumentationSnippet
		:PushLine("Use Ctrl+F, or ⌘+F on Mac to search for information you need.")
		:NewLine()
		:PushLine("```yaml")
		:PushLine(rawDocumentation)
		:PushLine("```")

	fs.writeFile("./wiki/docs/raw-documentation.md", tostring(rawDocumentationSnippet))

	local contributingContent = fs.readFile("CONTRIBUTING.md")
	local contributingSections = {}

	for _, section in contributingContent:sub(3):split("\n# ") do
		local title, content = section:match("^(.-)\n(.*)")
		if title and content then
			local sanitizedTitle = title:match("^%s*(.-)%s*$"):gsub("%s+", "-"):gsub("[^%w%-]", ""):lower()
			contributingSections[sanitizedTitle] = "# " .. section
		end
	end

	local contributingDir = "./wiki/docs/contributing"
	if not fs.isDir(contributingDir) then
		fs.writeDir(contributingDir)
	end

	for title, content in contributingSections do
		local filePath = `{contributingDir}/{title}.md`
		fs.writeFile(filePath, content)
	end
end

return Wiki


--!native
--!optimize 2

local aptint = require(game.ReplicatedStorage.AptInt)

type AptInt = aptint.AptInt

local clock: () -> number = os.clock
local random: (number?, number?) -> number = math.random
local insert = table.insert

local MAX: number = 2^24-1
local LOG2_10: number = 3.32192809489

local digits: {number} = {1, 50, 100, 500, 1_000, 5_000, 10_000, 50_000, 100_000}
--local digits: {number} = {100_000}

local function BuildRandomNum(digitCount: number): AptInt
	if digitCount <= 1 then
		return aptint.new({random(1, MAX)})
	end

	local len: number = digitCount * LOG2_10/24
	local tbl: {number} = {}

	for i: number = 1, len do
		if i == len then -- avoid leading zeros
			insert(tbl, random(1, MAX))
			continue
		end

		insert(tbl, random(0, MAX))
	end

	return aptint.new(tbl)
end

local function BenchFunc(name: string, fn: (AptInt, AptInt) -> ()): ()
	local start: number

	--> division takes more the smaller the divisor is.
	local isDiv: boolean = (name == "divBench")

	--> sqrt requires non-negative number.
	local isSqrt: boolean = (name == "sqrtBench")

	for _: number, digitCount: number in digits do
		local int1: AptInt = BuildRandomNum(digitCount)
		local int2: AptInt = BuildRandomNum(isDiv and digitCount/2+1 or digitCount)

		if isSqrt then
			int1.signum = 1
		end

		start = clock()
		fn(int1, int2)
		print(`Took {clock()-start} to run {name} with {digitCount} digits`)

		task.wait(0.25)
	end
end

BenchFunc("addBench", function(int1: AptInt, int2: AptInt)
	int1:AddRaw(int2)
end)

BenchFunc("subBench", function(int1: AptInt, int2: AptInt)
	int1:SubtractRaw(int2)
end)

BenchFunc("mulBench", function(int1: AptInt, int2: AptInt)
	int1:MultiplyRaw(int2)
end)

BenchFunc("divBench", function(int1: AptInt, int2: AptInt)
	int1:DivideRaw(int2)
end)

BenchFunc("sqrtBench", function(int1: AptInt, int2: AptInt)
	int1:sqrt()
end)


--!native
--!strict
--!optimize 2

--> THIS IS VERY UGLY CODE!!! I ONLY USE THIS FOR WHEN BENCHMARKING ALGORITHM THRESHOLDS

local aptint = require(game.ReplicatedStorage.AptInt.AptInt)

type AptInt = aptint.AptInt

local clock: () -> number = os.clock
local random: (number?, number?) -> number = math.random
local insert = table.insert

local MAX: number = 2^24-1
local LOG2_10: number = 3.32192809489

local function BuildRandomNum(digitCount: number): AptInt
	if digitCount <= 1 then
		return aptint.new({random(1, MAX)})
	end

	local len: number = digitCount * LOG2_10/24
	local tbl: {number} = {}

	for i: number = 1, len do
		if i == len then -- avoid leading zeros
			insert(tbl, random(1, MAX))
			continue
		end

		insert(tbl, random(0, MAX))
	end

	return aptint.new(tbl)
end

local function bench(fn: () -> (), runs: number)
	local start = os.clock()
	for i = 1, runs do
		fn()
	end

	return (os.clock() - start)/runs
end

local inarow=0
local best = 0
for i = 1, 1000, 7 do
	local int1 = BuildRandomNum(i)
	local int2 = BuildRandomNum(i)

	local Atime = bench(function()

	end, 30)
	local Btime = bench(function()

	end, 30)

	if Atime < Btime then
		inarow+=1
		continue
	end

	if inarow >= best then
		print("NEW BEST! "..inarow)

		best=inarow
		inarow=0

		continue
	end

	inarow = 0
	task.wait()
end


return {
	BASE_POW = 24,
	BASE = 2^24,
	BASE_MASK = 16777215, -- BASE - 1 (=0xffffff)

	RADIX_LEN_CONST = 7.224719895935548, -- BASE_POW * log10(2)
	RADIX_CHUNK_SIZE = 10^8,
	RADIX_CHUNK_POW = 8,
	MAX_TONUMBER_LEN = 43, -- min{k | BASE^k > 2^1024-1}

	--> algorithm thresholds (size in limbs)
	MIN_MINADD_OFFSET = 20,
	KARATSUBA_THRESHOLD = 45,

	SQRT_KARATSUBA_THRESHOLD = 32,
	SQRT_NEWTON_THRESHOLD = 19,

	TOOM3_THRESHOLD = 90,
	BURNIKEL_THRESHOLD = 280,
	MIN_BURNIKEL_OFFSET = 40,
}


--!native
--!strict
--!optimize 2

local Add = {}

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

local abs: (number) -> number = math.abs

local tclone: ({number}) -> {number} = table.clone
local tinsert: ({number}, number) -> () = table.insert

local BASE: number = Constants.BASE
local MIN_MINADD_OFFSET: number = Constants.MIN_MINADD_OFFSET

local function MinAdd(term: {number}, result: {number}): {number}
	--> since result has self's limbs, we can just iterate over y
	--> carry is 99.99% of times very small, so the while loop doesnt run much
	--> overall faster

	--> from right to left
	local sum: number = 0
	for i: number, yLimb: number in term do
		sum += result[i] + yLimb

		if sum >= BASE then
			result[i] = sum - BASE
			sum = 1
			continue
		end

		result[i] = sum
		sum = 0
	end

	if sum == 0 then return result end

	--> carry loop
	local resultIndex: number = #term + 1
	while true do
		sum = (result[resultIndex] or 0) + 1

		if sum >= BASE then
			result[resultIndex] = sum - BASE
			resultIndex += 1
			continue
		end

		result[resultIndex] = sum
		break
	end

	return result
end

function Add.AddByInt(self: {number}, num: number, inPlace: boolean?): {number}
	local result: {number} = (inPlace and self or tclone(self))
	local sum: number = result[1] + num

	--> carry loop
	local resultIndex: number = 1
	while true do
		sum = (result[resultIndex] or 0) + 1

		if sum >= BASE then
			result[resultIndex] = sum - BASE
			resultIndex += 1
			continue
		end

		result[resultIndex] = sum
		break
	end

	return result
end

function Add.AddLimbs(self: {number}, term: {number}, inPlace: boolean?): {number}
	local selfLen: number, termLen: number = #self, #term

	--> choose the longer number to avoid extra "or 0" with selfDigit
	local result: {number}
	if selfLen < termLen then
		result = tclone(term)
		self, term = term, self
	else
		result = (inPlace and self or tclone(self))
	end

	if abs(termLen - selfLen) > MIN_MINADD_OFFSET then
		return MinAdd(term, result)
	end

	--> from right to left
	local sum: number = 0
	for i: number, selfLimb: number in self do
		sum += selfLimb + (term[i] or 0)

		if sum >= BASE then
			result[i] = sum - BASE
			sum = 1
			continue
		end

		result[i] = sum
		sum = 0
	end

	if sum > 0 then tinsert(result, sum) end
	return result
end

return Add


--!native
--!strict
--!optimize 2

local Cmp = {}

local Utils = require("./Utils")
local Constants = require("../Constants")

function Cmp.LowerThan(x: {number}, y: {number}): boolean
	local xLen: number, yLen: number = #x, #y

	if xLen < yLen then
		return true
	elseif xLen > yLen then
		return false
	end

	for i: number = xLen, 1, -1 do
		local xLimb: number, yLimb: number = x[i], y[i]
		if xLimb == yLimb then continue end

		return xLimb < yLimb
	end

	return false
end

function Cmp.LowerOrEqualTo(x: {number}, y: {number}): boolean
	local xLen: number, yLen: number = #x, #y
	if xLen < yLen then
		return true
	elseif xLen > yLen then
		return false
	end

	for i: number = xLen, 1, -1 do
		local xLimb: number, yLimb: number = x[i], y[i]
		if xLimb == yLimb then continue end

		return xLimb < yLimb
	end

	return true
end

function Cmp.Equals(x: {number}, y: {number}): boolean
	if #x ~= #y then
		return false
	end

	for i: number, xLimb: number in x do
		if xLimb ~= y[i] then return false end
	end

	return true
end

function Cmp.Compare(x: {number}, y: {number}): number
	local xLen: number, yLen: number = #x, #y
	if xLen < yLen then
		return -1
	elseif xLen > yLen then
		return 1
	end

	for i: number = xLen, 1, -1 do
		local xLimb: number, yLimb: number = x[i], y[i]
		if xLimb == yLimb then continue end
		return xLimb > yLimb and 1 or -1
	end

	return 0
end

return Cmp