--!optimize 2
--!strict

local Chalk = require("@packages/Chalk")
local ChalkUtilities = require("@utilities/ChalkUtilities")
local Debug = require("@packages/Debug")
local GreenTea = require("@packages/GreenTea")
local GreenTeaAssert = require("@functions/GreenTeaAssert")
local GreenTeaUtilities = require("@packages/GreenTeaUtilities")
local LuauPolyfill = require("@packages/LuauPolyfill")
local TypeError = require("@errors/TypeError")
local process = require("@lune/process")

local console = LuauPolyfill.console

local ExecuteUtilities = {}

export type ExecResult = process.ExecResult

export type AssertableResult = ExecResult & {
	Assert: (self: AssertableResult) -> AssertableResult,
}
export type SafeAssertableResult = ExecResult & {
	Assert: (self: SafeAssertableResult, onFail: nil | (code: number) -> ()) -> SafeAssertableResult,
	AssertWithResult: (self: SafeAssertableResult, onFail: (result: ExecResult) -> ()) -> SafeAssertableResult,
	AssertWithMessage: (
		self: SafeAssertableResult,
		onFail: nil | (onMessage: string, code: number) -> ()
	) -> SafeAssertableResult,
}

local function CreateToString(className: string): (self: AssertableResult | SafeAssertableResult) -> string
	local function toString(self: AssertableResult | SafeAssertableResult): string
		local array = {`{Chalk.Magenta(`{className}<`)}{Chalk.Gray("{")}`}
		local length = 1

		for key, value in Debug.AlphabeticalOrder(self) do
			length += 1
			array[length] = `\t{key} {Chalk.Gray("=")} {ChalkUtilities.FormatValue(value)}{Chalk.Gray(",")}`
		end

		array[length + 1] = Chalk.Gray("}") .. Chalk.Magenta(">")
		return table.concat(array, "\n")
	end
	return toString
end

local AssertableResult = {} :: AssertableResult
do
	(AssertableResult :: any).__index = AssertableResult;
	(AssertableResult :: any).__tostring = CreateToString("AssertableResult")
	function AssertableResult:Assert(): AssertableResult
		if not self.ok then
			console.warn(`Command failed:\n{self.stderr}\n{self.stdout}`)
			process.exit(self.code)
		end

		return self
	end
end

local SafeAssertableResult = {} :: SafeAssertableResult
do
	(SafeAssertableResult :: any).__index = SafeAssertableResult;
	(SafeAssertableResult :: any).__tostring = CreateToString("SafeAssertableResult")
	function SafeAssertableResult:Assert(onFail: nil | (code: number) -> ()): SafeAssertableResult
		if not self.ok then
			console.warn(`Command failed:\n{self.stderr}\n{self.stdout}`)
			if onFail then
				onFail(self.code)
			end
		end

		return self
	end
	function SafeAssertableResult:AssertWithResult(onFail: (result: ExecResult) -> ()): SafeAssertableResult
		if not self.ok then
			onFail({
				code = self.code;
				ok = self.ok;
				stderr = self.stderr;
				stdout = self.stdout;
			})
		end

		return self
	end
	function SafeAssertableResult:AssertWithMessage(
		onFail: nil | (onMessage: string, code: number) -> ()
	): SafeAssertableResult
		if not self.ok and onFail then
			onFail(`{self.stderr}\n{self.stdout}`, self.code)
		end

		return self
	end
end

type ExecOptions = process.ExecOptions
type ExecStdioKind = process.ExecStdioKind

local IsExecStdioKindRaw = GreenTeaUtilities.SmartLiteral("default", "inherit", "forward", "none") :: ExecStdioKind
local IsExecStdioOptionsRaw = {
	stderr = GreenTea.optional(IsExecStdioKindRaw) :: ExecStdioKind?;
	stdin = GreenTea.optional(GreenTea.union(GreenTea.string(), GreenTea.buffer()));
	stdout = GreenTea.optional(IsExecStdioKindRaw) :: ExecStdioKind?;
}
local IsExecOptionsRaw = {
	cwd = GreenTea.optional(GreenTea.string());
	env = GreenTea.optional({[GreenTea.string()] = GreenTea.string()});
	shell = GreenTea.optional(GreenTea.union(GreenTea.boolean(), GreenTea.string()));
	stdio = GreenTea.optional(GreenTea.union(IsExecStdioKindRaw, IsExecStdioOptionsRaw));
	[GreenTea.any()] = GreenTea.never();
}
type ExecOptionsGreenTea = typeof(IsExecOptionsRaw)
local function AsExecOptionsGreenTea(execOptions: ExecOptions): ExecOptionsGreenTea
	return execOptions
end
local function AsExecOptions(execOptions: ExecOptionsGreenTea): ExecOptions
	-- sure why ot
	local replacementCauseLuauSucks = {} :: ExecOptions
	for key, value in execOptions do
		replacementCauseLuauSucks[key] = value
	end
	return replacementCauseLuauSucks
end

local IsStringArray = GreenTea.build(GreenTea.array(GreenTea.string()))
local IsMaybeExecOptions = GreenTea.build(GreenTea.optional(IsExecOptionsRaw))

local function Execute(command: {string}, options: ExecOptions?): AssertableResult
	GreenTeaAssert(IsStringArray, command, Execute, TypeError)
	GreenTeaAssert(IsMaybeExecOptions, options, Execute, TypeError)

	local binaryName = command[1]

	local arguments = table.clone(command)
	table.remove(arguments, 1)

	local execOptions = AsExecOptionsGreenTea(if options then table.clone(options) else {})
	if execOptions.shell == false then
		execOptions.shell = nil
	elseif execOptions.shell == nil then
		execOptions.shell = true
	end

	local self = table.clone(process.exec(binaryName, arguments, AsExecOptions(execOptions)))
	return (setmetatable(self, AssertableResult) :: unknown) :: AssertableResult
end
ExecuteUtilities.Execute = Execute

local function SafeExecute(command: {string}, options: ExecOptions?): SafeAssertableResult
	GreenTeaAssert(IsStringArray, command, SafeExecute, TypeError)
	GreenTeaAssert(IsMaybeExecOptions, options, SafeExecute, TypeError)

	local binaryName = command[1]

	local arguments = table.clone(command)
	table.remove(arguments, 1)

	local execOptions = AsExecOptionsGreenTea(if options then table.clone(options) else {})
	if execOptions.shell == false then
		execOptions.shell = nil
	elseif execOptions.shell == nil then
		execOptions.shell = true
	end

	local self = table.clone(process.exec(binaryName, arguments, AsExecOptions(execOptions)))
	return (setmetatable(self, SafeAssertableResult) :: unknown) :: SafeAssertableResult
end
ExecuteUtilities.SafeExecute = SafeExecute

return table.freeze(ExecuteUtilities)


--!optimize 2
--!strict

local Debug = require("@packages/Debug")
local PathFileSystem = require("@packages/PathFileSystem")

local Path = PathFileSystem.Path

export type AsPath = PathFileSystem.AsPath
export type Contents = PathFileSystem.Contents
export type Metadata = PathFileSystem.Metadata
export type MetadataKind = PathFileSystem.MetadataKind
export type MetadataPermissions = PathFileSystem.MetadataPermissions
export type Path = PathFileSystem.Path
export type WriteOptions = PathFileSystem.WriteOptions

local FileSystemUtilities = {}

local function AsPathToString(asPath: AsPath): string
	if type(asPath) == "string" then
		return asPath
	end

	return Path.from(asPath):toString()
end

local function GetFileDescendants(directoryPath: AsPath): {string}
	local contents = PathFileSystem.ReadDirectory(directoryPath)
	if not contents then
		return {}
	end

	local descendants = {}
	local length = 0
	for _, filePath in contents do
		local childPath = AsPathToString(directoryPath) .. "/" .. filePath
		if PathFileSystem.IsDirectory(childPath) then
			for _, descendant in GetFileDescendants(childPath) do
				length += 1
				descendants[length] = descendant
			end
		else
			length += 1
			descendants[length] = childPath
		end
	end

	return descendants
end
FileSystemUtilities.GetFileDescendants = GetFileDescendants

function FileSystemUtilities.FindFirstFileOfExtension(directoryPath: AsPath, extension: string): string?
	if not PathFileSystem.IsDirectory(directoryPath) then
		return nil
	end

	if string.byte(extension, 1) ~= 46 then
		extension = "." .. extension
	end

	extension = `{Debug.EscapeString(extension)}$`
	for _, filePath in PathFileSystem.ReadDirectory(directoryPath) do
		if not PathFileSystem.IsFile(filePath) then
			continue
		end

		if string.match(filePath, extension) then
			return filePath
		end
	end

	return nil
end
function FileSystemUtilities.FindFirstFileOfExtensionPath(directoryPath: AsPath, extension: string): Path?
	if not PathFileSystem.IsDirectory(directoryPath) then
		return nil
	end

	if string.byte(extension) ~= 46 then
		extension = "." .. extension
	end

	extension = `{Debug.EscapeString(extension)}$`
	for _, filePath in PathFileSystem.ReadDirectory(directoryPath) do
		if not PathFileSystem.IsFile(filePath) then
			continue
		end

		if string.match(filePath, extension) then
			return Path.new(filePath)
		end
	end

	return nil
end

function FileSystemUtilities.JustFileName(path: AsPath): string
	local castPath = AsPathToString(path)
	return string.match(string.match(castPath, "^(.+)%..+$") or castPath, "([^\\/]+)$") or castPath
end
function FileSystemUtilities.JustFileNameWithExtension(path: AsPath): string
	local castPath = AsPathToString(path)
	return string.match(castPath, "([^\\/]+)$") or castPath
end

local function WithoutExtension(path: AsPath): string
	local castPath = AsPathToString(path)
	return string.match(castPath, "^(.+)%..+$") or castPath
end
FileSystemUtilities.WithoutExtension = WithoutExtension

local function GetExtension(path: AsPath): string?
	return string.match(AsPathToString(path), "%.([^%.]+)$")
end
FileSystemUtilities.GetExtension = GetExtension

type CreatorFunction = (path: AsPath, contents: string?) -> ()
type SafeCreate =
	((name: AsPath, fileType: "File", contents: string) -> string)
	& ((name: AsPath, fileType: "Directory") -> string)

local function GetNewFilePath(withoutExtension: string, index: number, extension: string?): string
	return if extension then `{withoutExtension} ({index}).{extension}` else `{withoutExtension} ({index})`
end

local function SafeCreate(name: AsPath, fileType: "File" | "Directory", contents: string?)
	local checkerFunction: typeof(PathFileSystem.IsDirectory) = if fileType == "File"
		then PathFileSystem.IsFile
		else PathFileSystem.IsDirectory

	local creatorFunction = (
		if fileType == "File" then PathFileSystem.WriteFile else PathFileSystem.WriteDirectory
	) :: CreatorFunction

	if not checkerFunction(name) then
		creatorFunction(name, contents)
		return name
	end

	local withoutExtension = WithoutExtension(name)
	local extension = GetExtension(name)
	local index = 1

	local newFilePath = GetNewFilePath(withoutExtension, index, extension)
	while checkerFunction(newFilePath) do
		index += 1
		newFilePath = GetNewFilePath(withoutExtension, index, extension)
	end

	creatorFunction(newFilePath, contents)
	return newFilePath
end
FileSystemUtilities.SafeCreate = SafeCreate :: SafeCreate

function FileSystemUtilities.GetOrCreateDirectory(name: AsPath): ()
	if not PathFileSystem.IsDirectory(name) then
		SafeCreate(name, "Directory")
	end
end

function FileSystemUtilities.RecursivelyCreateDirectory(path: AsPath): ()
	for _, component in string.split(AsPathToString(path), PathFileSystem.PathSeparator) do
		PathFileSystem.WriteDirectory(component)
	end
end

return table.freeze(FileSystemUtilities)


--!optimize 2
--!strict

local ExecuteUtilities = require("@utilities/ExecuteUtilities")
local GreenTea = require("@packages/GreenTea")
local GreenTeaAssert = require("@functions/GreenTeaAssert")
local GreenTeaUtilities = require("@packages/GreenTeaUtilities")
local LuauPolyfill = require("@packages/LuauPolyfill")
local OperatingSystemUtilities = require("@utilities/OperatingSystemUtilities")
local Option = require("@packages/Option")
local PathFileSystem = require("@packages/PathFileSystem")
local PromiseUtilities = require("@packages/PromiseUtilities")
local RegEx = require("@lune/RegEx")
local ThreadUtilities = require("@packages/ThreadUtilities")
local TypeError = require("@errors/TypeError")
local UniqueIdUtilities = require("@utilities/UniqueIdUtilities")
-- local process = require("@lune/process")

local console = LuauPolyfill.console
type Option<T> = Option.Option<T>
type Path = PathFileSystem.Path

local FormatUtilities = {}

local StyLuaType = table.freeze({
	Standard = "Standard" :: "Standard",
	React = "React" :: "React",
	Roact = "React" :: "React",
})
FormatUtilities.StyLuaType = StyLuaType
export type StyLuaType = "Standard" | "React"

local IsFormatOptionsRaw = {
	FormatAmount = GreenTea.optional(GreenTea.number({
		integer = true,
		range = "[1, inf]",
	})),
	FormatWith = GreenTea.optional(GreenTea.string()),
}

local IsMaybeFormatOptions = GreenTea.build(GreenTea.optional(IsFormatOptionsRaw))
local IsString = GreenTea.build(GreenTea.string())
local IsStyLuaType = GreenTea.build(GreenTeaUtilities.SmartLiteralValues(StyLuaType))

local TYPE_TO_EXECUTABLE_NAMES: {[StyLuaType]: {string}} = {
	[StyLuaType.Standard] = {"stylua2", "stylua2-dg", "stylua"},
	[StyLuaType.React] = {"stylua2React", "stylua2React-dg", "stylua2Roact", "stylua2Roact-dg", "stylua"},
}
local styluaTypeToExecutable: {[StyLuaType]: Option<string>} = {}

type ExecResult = ExecuteUtilities.ExecResult
local function DoesExecutableExist(executableName: string): boolean
	local result = ExecuteUtilities.Execute({executableName, "--version"})
	return result.ok and result.code == 0
end

local function GetExecutableName(styluaType: StyLuaType): Option<string>
	GreenTeaAssert(IsStyLuaType, styluaType, GetExecutableName, TypeError)
	local cached = styluaTypeToExecutable[styluaType]
	if cached then
		return cached
	end

	local executableNames = TYPE_TO_EXECUTABLE_NAMES[styluaType]
	if not executableNames then
		styluaTypeToExecutable[styluaType] = Option.None
		return Option.None
	end

	for _, executableName in executableNames do
		if DoesExecutableExist(executableName) then
			local option = Option.Some(executableName)
			styluaTypeToExecutable[styluaType] = option
			return option
		end
	end

	return Option.None
end
FormatUtilities.GetExecutableName = GetExecutableName

export type FormatOptions = typeof(IsFormatOptionsRaw)
local function AsFormatOptions(formatOptions: FormatOptions): FormatOptions
	return formatOptions
end

local REACT_REGEXP = RegEx.new(
	"(createElement|createContext|oneChild|createBinding|createFragment|useBinding|useCallback|useContext|useDebugValue|useEffect|useImperativeHandle|useLayoutEffect|useMemo|useReducer|useRef|useState)\\("
)

local function GetStyLuaType(source: string): StyLuaType
	GreenTeaAssert(IsString, source, GetStyLuaType, TypeError)
	return if REACT_REGEXP:isMatch(source) then StyLuaType.React else StyLuaType.Standard
end
FormatUtilities.GetStyLuaType = GetStyLuaType

local function Format(code: string, options: FormatOptions?): (boolean, string)
	GreenTeaAssert(IsString, code, Format, TypeError)
	GreenTeaAssert(IsMaybeFormatOptions, options, Format, TypeError)

	local formatOptions = AsFormatOptions(options or {})
	local formatAmount = formatOptions.FormatAmount or 1
	local formatWith = formatOptions.FormatWith or GetStyLuaType(code) or "stylua2"

	-- write file

	local temporaryFile = OperatingSystemUtilities.TemporaryDirectory():join(`{UniqueIdUtilities.NextUniqueId()}.luau`)
	PathFileSystem.WriteFile(temporaryFile, code)

	console.log(`Formatting {formatAmount}`)

	local shouldKill = false

	local arguments = {formatWith, "-f", "stylua.toml", "--verify", temporaryFile:toString()}
	if formatAmount == 1 then
		ExecuteUtilities.SafeExecute(arguments):AssertWithMessage(function(message: string): ()
			shouldKill = true
			console.warn(`Failed to format - {message}`)
		end)
	else
		for _ = 1, formatAmount do
			if shouldKill then
				return false, code
			end

			ExecuteUtilities.SafeExecute(arguments):AssertWithMessage(function(message: string): ()
				shouldKill = true
				console.warn(`Failed to format - {message}`)
			end)
		end
	end

	if shouldKill then
		return false, code
	end

	local formatted = PathFileSystem.ReadFile(temporaryFile)
	ThreadUtilities.FastSpawn(function(): ()
		ThreadUtilities.FastDelay(0.2, function(): ()
			PathFileSystem.RemoveFile(temporaryFile)
		end)
	end)

	return true, formatted
end
FormatUtilities.Format = Format
FormatUtilities.FormatAsync = PromiseUtilities.FastAsyncMulti(Format)

return table.freeze(FormatUtilities)


--!optimize 2
--!strict

local function GenerateGuid(): string
	return string.format(
		"%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
		math.random(0, 255),
		math.random(0, 255),
		math.random(0, 255),
		math.random(0, 255),
		math.random(0, 255),
		math.random(0, 255),
		bit32.bor(bit32.band(math.random(0, 255), 0x0F), 0x40),
		bit32.bor(bit32.band(math.random(0, 255), 0x3F), 0x80),
		math.random(0, 255),
		math.random(0, 255),
		math.random(0, 255),
		math.random(0, 255),
		math.random(0, 255),
		math.random(0, 255),
		math.random(0, 255),
		math.random(0, 255)
	)
end

return GenerateGuid


--!optimize 2
--!strict

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

local function GetAverageWaitTime(runs: number, duration: number?): number
	local total = 0
	for _ = 1, runs do
		total += task.wait(duration)
	end
	return total / runs
end

return GetAverageWaitTime


--!optimize 2
--!strict

local FileSystemUtilities = require("@utilities/FileSystemUtilities")
local Option = require("@packages/Option")
local PathFileSystem = require("@packages/PathFileSystem")
local serde = require("@lune/serde")

local Path = PathFileSystem.Path

type VsCodeSettings = {["luau-lsp.sourcemap.rojoProjectFile"]: string?}
type CodeWorkspaceData = {settings: VsCodeSettings?}

local VSCODE_SETTINGS = PathFileSystem.CWD:join(".vscode"):join("settings.json")

type PathOption = Option.Option<PathFileSystem.Path>

local function MakeJsonSafeForSerde(jsonString: string): string
	return (string.gsub(string.gsub(string.gsub(jsonString, "/%*.-%*/", ""), "//[^\n]*", ""), ",%s*([}%]])", "%1"))
end

local function GetVsCodeSettings(): PathOption
	if not PathFileSystem.IsFile(VSCODE_SETTINGS) then
		return Option.None
	end

	local vsCodeSettings: VsCodeSettings? = nil
	pcall(function()
		vsCodeSettings = serde.decode("json", MakeJsonSafeForSerde(PathFileSystem.ReadFile(VSCODE_SETTINGS)))
	end)

	if not vsCodeSettings then
		return Option.None
	end

	local rojoProjectFile = vsCodeSettings["luau-lsp.sourcemap.rojoProjectFile"]
	if not rojoProjectFile then
		return Option.None
	end

	return Option.Some(Path.new(rojoProjectFile))
end
local function ThenMutate(projectFile: PathFileSystem.Path): PathOption
	if not PathFileSystem.IsFile(projectFile) then
		return Option.None
	end

	return Option.Some(projectFile)
end

local DEFAULT_PROJECT_FILE = Option.Some(Path.new("default.project.json"))

local function GetProjectFile(): PathOption
	local projectFileOption: PathOption = Option.None

	local codeWorkspace = FileSystemUtilities.FindFirstFileOfExtensionPath(PathFileSystem.CWD, "code-workspace")
	if codeWorkspace then
		local codeWorkspaceData: CodeWorkspaceData? = nil
		pcall(function()
			codeWorkspaceData = serde.decode("json", MakeJsonSafeForSerde(PathFileSystem.ReadFile(codeWorkspace)))
		end)

		if codeWorkspaceData then
			local settings = codeWorkspaceData.settings
			if settings then
				local rojoProjectFile = settings["luau-lsp.sourcemap.rojoProjectFile"]
				if rojoProjectFile then
					projectFileOption = Option.Some(Path.new(rojoProjectFile))
				end
			end
		end
	end

	return projectFileOption:OrElse(GetVsCodeSettings):Or(DEFAULT_PROJECT_FILE):AndThen(ThenMutate)
end

return GetProjectFile


--!optimize 2
--!strict

local LuauPolyfill = require("@packages/LuauPolyfill")
local Option = require("@packages/Option")
local PathFileSystem = require("@packages/PathFileSystem")
local process = require("@lune/process")

local Path = PathFileSystem.Path
local String = LuauPolyfill.String
local console = LuauPolyfill.console

type PathOption = Option.Option<PathFileSystem.Path>

local IS_WINDOWS = process.os == "windows"

local function MutatePath(path: PathFileSystem.Path): PathOption
	if not PathFileSystem.IsFileSafe(path) then
		return Option.None
	end

	local result = process.exec(path:toString(), {"-V"})
	if not result.ok then
		console.warn(`Command failed:\n{result.stderr}\n{result.stdout}`)
		process.exit(result.code)
	end

	if string.sub(result.stdout, 1, 4) ~= "Rojo" then
		return Option.None
	end

	return Option.Some(path)
end

local function GetRojoLocation(): PathOption
	if IS_WINDOWS then
		local result = process.exec("where", {"rojo"})
		if not result.ok then
			console.warn(`Command failed:\n{result.stderr}\n{result.stdout}`)
			process.exit(result.code)
		end

		return Option.Wrap(Path.from(String.trim(result.stdout))):AndThen(MutatePath)
	end

	local result = process.exec("whereis", {"rojo"})
	if not result.ok then
		console.warn(`Command failed:\n{result.stderr}\n{result.stdout}`)
		process.exit(result.code)
	end

	local path = string.match(String.trim(result.stdout), "^%w+:%s*(.+)$")
	return if path then Option.Wrap(Path.from(path)):AndThen(MutatePath) else Option.None
end

return GetRojoLocation


--!optimize 2
--!strict

local Error = require("@packages/Error")
local GreenTea = require("@packages/GreenTea")
local GreenTeaUtilities = require("@packages/GreenTeaUtilities")
local HttpPromise = require("@promises/HttpPromise")
local MissingError = require("@errors/MissingError")
local PathFileSystem = require("@packages/PathFileSystem")
local PromiseUtilities = require("@packages/PromiseUtilities")

local Await = PromiseUtilities.Await

local LUAURC_PATH = PathFileSystem.CWD:join(".luaurc")

local LuauConfigurationUtilities = {}
LuauConfigurationUtilities.LUAURC_PATH = LUAURC_PATH

export type LanguageMode = "strict" | "nonstrict" | "nocheck"

local IsLuauConfigurationRaw = {
	aliases = GreenTea.optional({[GreenTea.string()] = GreenTea.string()});
	globals = GreenTea.optional(GreenTea.array(GreenTea.string()));
	languageMode = GreenTea.optional(GreenTeaUtilities.EasyLiteral("strict", "nonstrict", "nocheck")) :: LanguageMode?;
	lint = GreenTea.optional({[GreenTea.string()] = GreenTea.boolean()});
	lintErrors = GreenTea.optional(GreenTea.boolean());
	typeErrors = GreenTea.optional(GreenTea.boolean());
}
export type LuauConfiguration = typeof(IsLuauConfigurationRaw)

local function DoesConfigurationExist(): boolean
	return PathFileSystem.IsFile(LUAURC_PATH)
end
LuauConfigurationUtilities.DoesConfigurationExist = DoesConfigurationExist

local function GetConfiguration(): LuauConfiguration
	if not DoesConfigurationExist() then
		local exception = MissingError.new("No .luaurc file found.")
		Error.captureStackTrace(exception, GetConfiguration)
		error(exception)
	end

	return Await(HttpPromise.PromiseDecode(PathFileSystem.ReadFile(LUAURC_PATH), IsLuauConfigurationRaw))
end
LuauConfigurationUtilities.GetConfigurationAsync = PromiseUtilities.FastAsync(GetConfiguration)

return table.freeze(LuauConfigurationUtilities)


--!optimize 2
--!strict

local DotEnv = require("@packages/DotEnv")
local ExecuteUtilities = require("@utilities/ExecuteUtilities")
local LuauPolyfill = require("@packages/LuauPolyfill")
local PathFileSystem = require("@packages/PathFileSystem")
local process = require("@lune/process")
DotEnv.Load()

local Boolean = LuauPolyfill.Boolean
local Path = PathFileSystem.Path
local Set = LuauPolyfill.Set
type Path = PathFileSystem.Path
type Set<T> = LuauPolyfill.Set<T>

local OperatingSystemUtilities = {}

local function GetUserAccountName(): string
	local environment = process.env
	local username = environment.USER or environment.USERNAME
	if Boolean.toJSBoolean(username) then
		return username :: string
	end

	local whoAmI = ExecuteUtilities.Execute({"whoami"})
	if not whoAmI.ok then
		return "Unknown"
	end

	local stdout = whoAmI.stdout
	return string.match(stdout, "[^\\]+$") or stdout or "Unknown"
end
OperatingSystemUtilities.GetUserAccountName = GetUserAccountName

local GetHowManySmallAccounts = (function()
	local set: Set<string> = Set.new()

	local function getHowManySmallAccounts(): Set<string>
		local stringValue = process.env.HOWMANYSMALL_ACCOUNTS
		if stringValue then
			for _, username in string.split(stringValue, ",") do
				set:add(username)
			end
		end

		return set
	end

	return getHowManySmallAccounts
end)()

function OperatingSystemUtilities.IsHowManySmall(): boolean
	local accountUser = process.env.ACCOUNT_USER
	local boolean = GetHowManySmallAccounts():has(string.lower(GetUserAccountName()))
	return if accountUser then string.lower(accountUser) == "howmanysmall" or boolean else boolean
end

function OperatingSystemUtilities.TemporaryDirectory(): Path
	local env = process.env
	if process.os == "windows" then
		local tmpValue = env.TMP
		if tmpValue then
			return Path.new(tmpValue)
		end

		local tempValue = env.TEMP
		if tempValue then
			return Path.new(tempValue)
		end

		local userProfileValue = env.USERPROFILE
		if userProfileValue then
			return Path.new(userProfileValue):join("AppData"):join("Local"):join("Temp")
		end

		return Path.new("C:/Windows/Temp")
	end

	local tmpDirValue = env.TMPDIR
	return if tmpDirValue then Path.new(tmpDirValue) else Path.new("/tmp")
end

return table.freeze(OperatingSystemUtilities)


--!optimize 2
--!strict

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

local PathUtilities = {}
local SEPARATOR = if process.os == "windows" then "\\" else "/"
PathUtilities.Separator = SEPARATOR

--- Fixes the path so that it's properly formatted to the local operating system
--- @param path The path to format
--- @return A fixed path matching the OS format
function PathUtilities.OsPath(path: string): string
	local newPath = ""

	if process.os == "windows" then
		local index = 1
		while index <= #path do
			local character = string.sub(path, index, index)
			if character == "/" then
				character = "\\"
			end

			newPath ..= character
			index += 1
		end

		newPath = string.gsub(newPath, "\\+", "\\")
	else
		local index = 1
		while index <= #path do
			local character = string.sub(path, index, index)
			if character == "\\" then
				character = "/"
			end

			newPath ..= character
			index += 1
		end

		newPath = string.gsub(newPath, "/+", "/")
	end

	return newPath
end

--- Join multiple path parts into a combined path
--- @return A joined path
function PathUtilities.Join(...: string): string
	return PathUtilities.OsPath(table.concat({...}, SEPARATOR))
end

--- Gets the extension of the given path (if applicable) - otherwise returns nil
--- @param path The path to get the extension of
--- @return The path extension or nil
function PathUtilities.ExtensionName(path: string): string?
	return string.match(path, ".(%.[A-z0-9_-]+)$")
end

--- Gets the full directory path of the given path
--- @param path The path
--- @return The full directory path without the end item
function PathUtilities.DirectoryName(path: string): string
	local parts = string.split(PathUtilities.OsPath(path), SEPARATOR)
	table.remove(parts)
	return table.concat(parts, SEPARATOR)
end

function PathUtilities.FileName(path: string): string
	return string.match(path, `[^{PathUtilities.Separator}]+$`) or path
end
function PathUtilities.FileNameNoExtension(path: string): string?
	local extension = PathUtilities.ExtensionName(path)
	local fileName = string.match(path, `[^{PathUtilities.Separator}]+$`)
	if extension then
		return if fileName then string.gsub(fileName, `{extension}$`, "") else nil
	end

	return fileName
end

return table.freeze(PathUtilities)


--!optimize 2
--!strict

type ProgressOptions = {
	Fill: string?,
	Length: number?,
	PrintEnd: string?,
	PrintStart: string?,
	Surrounding: string?,
}
local BAR = utf8.char(9608)

local function ProgressBar(iteration: number, total: number, options: ProgressOptions?): string
	local trueOptions: ProgressOptions = if options then options else {}
	local length = trueOptions.Length or 100
	local surrounding = trueOptions.Surrounding or "|"

	local filledLength = length * iteration // total
	return `{trueOptions.PrintStart or "\r"}{surrounding}{string.rep(trueOptions.Fill or BAR, filledLength)}{string.rep(
		"-",
		length - filledLength
	)}{surrounding}{trueOptions.PrintEnd or "\r"}`
end

return ProgressBar


--!optimize 2
--!strict

local Debug = require("@packages/Debug")
local StringUtilities = require("@utilities/StringUtilities")
local stdio = require("@lune/stdio")

export type CanPromptFor = boolean | number | string
type Types = "boolean" | "number" | "string"
local CAN_PROMPT_FOR = {
	boolean = true;
	number = true;
	string = true;
}

local function CreatePrompt<T>(key: string, value: unknown, setter: (newValue: unknown) -> ()): () -> ()
	if type(value) == "number" then
		return function(): ()
			local newValue = stdio.prompt("text", `What should the {key} be?`, tostring(value))
			setter(StringUtilities.GetNumberFromString(newValue, value))
		end
	end

	if type(value) == "boolean" then
		return function(): ()
			local newValue = stdio.prompt("confirm", `Do you want to enable {key}?`, not not value)
			setter(newValue)
		end
	end

	if type(value) == "string" then
		return function(): ()
			setter(stdio.prompt("text", `What should the {key} be?`, tostring(value)))
		end
	end

	error(`Unknown value in exhaustive match: {Debug.Inspect(value)}`)
end

local function PromptFor(object: {[string]: unknown}): ()
	local prompts = {}
	local length = 0

	for key, value in Debug.AlphabeticalOrder(object) do
		local typeOf = type(value)
		if CAN_PROMPT_FOR[typeOf] then
			length += 1
			prompts[length] = CreatePrompt(key, value, function(newValue: unknown): ()
				object[key] = newValue
			end)
		end
	end

	for _, callback in prompts do
		callback()
	end
end

return PromptFor


--!optimize 2
--!strict

--[[
MIT License

Copyright (c) 2024 BusyCityGuy

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
]]

--[[
	Lune instances are tables containing a reference to the instance, but Lune creates
	different tables even for the same instance. Jest's caching behavior depends
	on the instance always being the same to look it up by key, so this function serves to
	always return the same "wrapped" instance when given the same instance. It has to also
	override the FindFirstChild, WaitForChild, and Instance property getter to ensure they
	also return a "wrapped" instance.
--]]

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

local ReducedInstance = {}
local cache = {}

local function Once(instance: roblox.Instance): roblox.Instance
	assert(instance, "Instance is nil")

	local debugId = instance:GetDebugId()
	if cache[debugId] then
		return cache[debugId]
	end

	local self = setmetatable({
		instance = instance;
	}, {
		-- selene: allow(shadowing)
		__index = function(self, key): any?
			local value = self.instance[key]
			if type(value) == "function" then
				if key == "FindFirstChild" then
					return function(self, ...): roblox.Instance?
						local child = self.instance:FindFirstChild(...)
						if child then
							return Once(child :: roblox.Instance)
						end
						return
					end
				elseif key == "WaitForChild" then
					return function(self, ...)
						return Once(self.instance:WaitForChild(...) :: roblox.Instance)
					end
				else
					return function(self, ...)
						return value(self.instance, ...)
					end
				end
			elseif typeof(value) == "Instance" then
				return Once((value :: never) :: roblox.Instance)
			end

			return value
		end;
		-- selene: allow(shadowing)
		__newindex = function(self, key, value)
			self.instance[key] = value
		end;
		__metatable = false;
	})

	cache[debugId] = self

	return (self :: any) :: roblox.Instance
end
ReducedInstance.Once = Once

return table.freeze(ReducedInstance)


--!optimize 2
--!strict

local GreenTea = require("@packages/GreenTea")
local GreenTeaAssert = require("@functions/GreenTeaAssert")
local GreenTeaUtilities = require("@packages/GreenTeaUtilities")
local LuauPolyfill = require("@packages/LuauPolyfill")
local TypeError = require("@errors/TypeError")
local roblox = require("@lune/roblox")

local console = LuauPolyfill.console

local RobloxEnumUtilities = {}

local ReturnType = table.freeze({
	Array = "Array" :: "Array";
	Dictionary = "Dictionary" :: "Dictionary";
})
export type ReturnType = "Array" | "Dictionary"
RobloxEnumUtilities.ReturnType = ReturnType

local IsReturnTypeRaw = GreenTeaUtilities.SmartLiteralValues(ReturnType) :: ReturnType

local IsMaybeReturnType = GreenTea.build(GreenTea.optional(IsReturnTypeRaw) :: ReturnType?)

-- local function AsReturnType(returnType: ReturnType): ReturnType
-- 	return returnType
-- end

export type FakeEnumItem = {
	Name: string,
	Value: number,
}
local function AsFakeEnumItem(fakeEnumItem: FakeEnumItem): FakeEnumItem
	return if table.isfrozen(fakeEnumItem) then fakeEnumItem else table.freeze(fakeEnumItem)
end
local function AsFakeEnumItems(fakeEnumItems: {FakeEnumItem}): {FakeEnumItem}
	return if table.isfrozen(fakeEnumItems) then fakeEnumItems else table.freeze(fakeEnumItems)
end
export type FakeEnum = {
	EnumItems: {FakeEnumItem},
	Name: string,
}
local function AsFakeEnum(fakeEnum: FakeEnum): FakeEnum
	return if table.isfrozen(fakeEnum) then fakeEnum else table.freeze(fakeEnum)
end

local function Sort<T>(array: {T}, callback: nil | (a: T, b: T) -> boolean): {T}
	local new = table.clone(array)
	table.sort(new, callback)
	return new
end
local function SortByValue(a: FakeEnumItem, b: FakeEnumItem): boolean
	return a.Value < b.Value
end
local function ExhaustiveMatch(value: never): never
	error(`Unknown value in exhaustive match: {value}`)
end

type GenerateEnumsOffline =
	& ((returnType: "Array") -> {FakeEnum})
	& ((returnType: "Dictionary") -> {[string]: {FakeEnumItem}})
	& ((returnType: ReturnType?) -> {[string]: {FakeEnumItem}})

local cachedEnums: {[ReturnType]: {FakeEnum} | {[string]: {FakeEnumItem}}} = {}

local function GetEnumItems(items: {[string]: number}): {FakeEnumItem}
	local enumItems: {FakeEnumItem} = {}
	local enumItemsLength = 0

	for name, value in items do
		enumItemsLength += 1
		enumItems[enumItemsLength] = AsFakeEnumItem({
			Name = name;
			Value = value;
		})
	end

	return AsFakeEnumItems(Sort(enumItems, SortByValue))
end

local function GenerateEnumsOffline(returnType: ReturnType?): {FakeEnum} | {[string]: {FakeEnumItem}}
	GreenTeaAssert(IsMaybeReturnType, returnType, GenerateEnumsOffline, TypeError)

	local trueReturnType: ReturnType = returnType or ReturnType.Dictionary
	local cached = cachedEnums[trueReturnType]
	if cached then
		return cached
	end

	local reflectionDatabase = roblox.getReflectionDatabase()
	local enumNames = Sort(reflectionDatabase:GetEnumNames())

	if trueReturnType == ReturnType.Array then
		local enums: {FakeEnum} = table.create(#enumNames)
		local enumsLength = 0

		for _, enumName in enumNames do
			local enum = reflectionDatabase:GetEnum(enumName)
			if not enum then
				console.error(`Failed to get enum '{enumName}'?`)
				continue
			end

			enumsLength += 1
			enums[enumsLength] = AsFakeEnum({
				EnumItems = GetEnumItems(enum.Items);
				Name = enumName;
			})
		end

		cachedEnums[trueReturnType] = table.freeze(enums)
		return enums
	end

	if trueReturnType == ReturnType.Dictionary then
		local enums: {[string]: {FakeEnumItem}} = {}
		for _, enumName in enumNames do
			local enum = reflectionDatabase:GetEnum(enumName)
			if not enum then
				console.error(`Failed to get enum '{enumName}'?`)
				continue
			end
			enums[enumName] = GetEnumItems(enum.Items)
		end

		cachedEnums[trueReturnType] = table.freeze(enums)
		return enums
	end

	ExhaustiveMatch(trueReturnType)
	error("unreachable")
end
RobloxEnumUtilities.GenerateEnumsOffline = GenerateEnumsOffline :: GenerateEnumsOffline

return table.freeze(RobloxEnumUtilities)


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

--[[
MIT License

Copyright (c) 2024 BusyCityGuy

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
]]

--[[
	Provides a connection to a loop that runs every frame. This is used
	in the custom Heartbeat implementation in lune/test.luau
--]]

local ThreadUtilities = require("@packages/ThreadUtilities")
local task = require("@lune/task")

type Subscription = {
	Connected: boolean,
	Function: (deltaTime: number) -> (),
}
export type Connection = {
	Connected: boolean,
	Disconnect: (self: Connection) -> (),
}

local Runtime = {}

local isRunning = false
local subscriptions: {Subscription} = {}

local function Loop(): ()
	if isRunning then
		return
	end

	isRunning = true
	while #subscriptions > 0 do
		local deltaTime = task.wait()
		for _, subscription in subscriptions do
			if subscription.Connected then
				subscription.Function(deltaTime)
			end
		end

		for index = #subscriptions, 1, -1 do
			if not subscriptions[index].Connected then
				table.remove(subscriptions, index)
			end
		end
	end
	isRunning = false
end

function Runtime.Connect(_: unknown, callback: (deltaTime: number) -> ()): Connection
	local subscription: Subscription = {
		Connected = true;
		Function = callback;
	}
	table.insert(subscriptions, subscription)
	ThreadUtilities.FastSpawn(Loop)

	return {
		Connected = true;
		Disconnect = function(self: Connection): ()
			self.Connected = false
			subscription.Connected = false
		end;
	}
end

return table.freeze(Runtime)


--!optimize 2
--!strict

local Chalk = require("@packages/Chalk")
local ProgressBar = require("@utilities/ProgressBar")

local boldGreen = Chalk.Bold .. Chalk.GreenBright
local boldRed = Chalk.Bold .. Chalk.RedBright
local boldYellow = Chalk.Ansi(33)
local blue = Chalk.Ansi256(39)
local boldBlue = Chalk.Bold .. blue

--[=[
	A function that benchmarks functions against each other.
	@class SpeedTester
	@ignore
]=]

type TimeEntry = {
	Function: () -> (),
	Name: string,
	Time: number,
}

local function SortByTime(a: TimeEntry, b: TimeEntry): boolean
	return a.Time < b.Time
end

--[=[
	@interface IOptions
	.BarGraphMultiplier? number -- The multiplier for the bar graph, a higher number means longer bars which is obviously more precise. Defaults to 50.
	.TimeThreshold? number -- The time at which the function benchmarking stops. Defaults to 1.
	.TitleText? string -- The title of the benchmark. Defaults to "No Title Given".
	@within SpeedTester
]=]
type Options = {
	BarGraphMultiplier: number?,
	TimeThreshold: number?,
	TitleText: string?,
}

--[=[
	Performs a benchmark.

	@function SpeedTester
	@param FunctionsTable {[string]: () -> ()} -- The functions you are speed testing.
	@param Options IOptions? -- The options for the benchmark.
	@within SpeedTester
]=]
local function SpeedTester(functionsTable: {[string]: () -> ()}, options: Options?): ()
	if not functionsTable or type(functionsTable) ~= "table" then
		error("FunctionsTable doesn't exist or isn't a table.")
	end

	local trueOptions: Options = if options then options else {}
	local barGraphMultiplier = trueOptions.BarGraphMultiplier or 50
	local timeThreshold = trueOptions.TimeThreshold or 1
	local titleText = trueOptions.TitleText or "No Title Given"

	local barOptions = {
		Length = barGraphMultiplier;
		PrintEnd = "";
		PrintStart = "";
		Surrounding = "";
	}

	do
		local timesArray: {TimeEntry} = {}
		local totalFunctions = 0

		for name, benchmark in functionsTable do
			totalFunctions += 1
			timesArray[totalFunctions] = {
				Function = benchmark;
				Name = name;
				Time = 0;
			}
		end

		repeat
			local longestTime = 0
			for _, timeEntry in timesArray do
				local benchmark = timeEntry.Function
				local previousTime = timeEntry.Time

				local startTime = os.clock()
				benchmark()
				local time = os.clock() - startTime + previousTime

				if time > longestTime then
					longestTime = time
				end

				timeEntry.Time = time
			end
		until longestTime > timeThreshold

		table.sort(timesArray, SortByTime)
		local slowestFunction = timesArray[totalFunctions]

		print(
			`In {Chalk.Bold(titleText)}, {boldGreen(timesArray[1].Name)} is {boldBlue(
				string.format("%.2f%%", 100 - 100 * (timesArray[1].Time / slowestFunction.Time))
			)} faster than {boldRed(slowestFunction.Name)}`
		)

		local barLengths: {number} = table.create(totalFunctions)
		local maxLength = -1
		for order, timeEntry in timesArray do
			local length = timeEntry.Time * barGraphMultiplier / slowestFunction.Time
			maxLength = math.max(maxLength, length)
			barLengths[order] = length
		end

		for order, timeEntry in timesArray do
			print(
				`\t[{blue(tostring(order))}] {boldYellow(timeEntry.Name)}:\n\t{ProgressBar(
					barLengths[order],
					maxLength,
					barOptions
				)}`
			)
		end
	end
end

local Metatable = {}
function Metatable:__call(functionsTable: {[string]: () -> ()}, options: Options?): ()
	return SpeedTester(functionsTable, options)
end

return setmetatable({SpeedTester = SpeedTester}, Metatable)


--!optimize 2
--!strict

local GreenTea = require("@packages/GreenTea")
local GreenTeaAssert = require("@functions/GreenTeaAssert")
local StrictTypeError = require("@errors/StrictTypeError")

local StringUtilities = {}
local IS_DEVELOPER_MODE = _G.__DEV__

local IsNumberRaw = GreenTea.number({})
local IsStringRaw = GreenTea.string({})
local IsNumberOrStringRaw = GreenTea.union(IsNumberRaw, IsStringRaw)

local IsValidCharacters = GreenTea.build(GreenTea.optional(GreenTea.union(IsStringRaw, GreenTea.array(IsStringRaw))))
local IsMaybeNumber = GreenTea.build(GreenTea.optional(IsNumberRaw))
local IsMaybeNumberOrString = GreenTea.build(GreenTea.optional(IsNumberOrStringRaw))
local IsPositiveInteger = GreenTea.build(GreenTea.number({integer = true, range = "(0, inf]"}))
local IsString = GreenTea.build(IsStringRaw)

local function ToPascalCase(value: string): string
	if IS_DEVELOPER_MODE then
		GreenTeaAssert(IsString, value, ToPascalCase, StrictTypeError)
	end

	return (
		string.gsub(
			string.gsub(string.gsub(string.lower(value), "[ _-](%a)", string.upper), "^%a", string.upper),
			"%p",
			""
		)
	)
end
StringUtilities.ToPascalCase = ToPascalCase

local function GetNumberFromString(value: nil | number | string, defaultValue: number?): number
	if IS_DEVELOPER_MODE then
		GreenTeaAssert(IsMaybeNumberOrString, value, GetNumberFromString, StrictTypeError)
		GreenTeaAssert(IsMaybeNumber, defaultValue, GetNumberFromString, StrictTypeError)
	end

	local trueDefaultValue: number = if defaultValue then defaultValue else 0

	if value == nil or type(value) ~= "string" then
		return if value ~= nil and type(value) == "number" then value else trueDefaultValue
	end

	local number = tonumber(value)
	if number then
		return number
	end

	local match = string.match(value, "%-?%d+%.?%d*")
	return if match then tonumber(match) or trueDefaultValue else trueDefaultValue
end
StringUtilities.GetNumberFromString = GetNumberFromString

local CHARACTERS =
	string.split("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz~`!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?", "")

local function GetRandomString(size: number, characters: {string} | nil | string): string
	if IS_DEVELOPER_MODE then
		GreenTeaAssert(IsPositiveInteger, size, GetRandomString, StrictTypeError)
		GreenTeaAssert(IsValidCharacters, characters, GetRandomString, StrictTypeError)
	end

	local stringBuilder = table.create(size)

	if characters then
		local array = if type(characters) == "string" then string.split(characters, "") else characters
		local length = #array

		for index = 1, size do
			stringBuilder[index] = array[math.random(length)]
		end
	else
		for index = 1, size do
			stringBuilder[index] = CHARACTERS[math.random(94)]
		end
	end

	return table.concat(stringBuilder)
end
StringUtilities.GetRandomString = GetRandomString

local function GetRandomStringConcat(size: number, characters: {string} | nil | string): string
	if IS_DEVELOPER_MODE then
		GreenTeaAssert(IsPositiveInteger, size, GetRandomStringConcat, StrictTypeError)
		GreenTeaAssert(IsValidCharacters, characters, GetRandomStringConcat, StrictTypeError)
	end

	local randomString = ""

	if characters then
		local array = if type(characters) == "string" then string.split(characters, "") else characters
		local length = #array

		for _ = 1, size do
			randomString ..= array[math.random(length)]
		end
	else
		for _ = 1, size do
			randomString ..= CHARACTERS[math.random(94)]
		end
	end

	return randomString
end
StringUtilities.GetRandomStringConcat = GetRandomStringConcat

return table.freeze(StringUtilities)


--!optimize 2
--!strict

local GreenTea = require("@packages/GreenTea")
local GreenTeaAssert = require("@functions/GreenTeaAssert")
local TypeError = require("@errors/TypeError")

local UniqueIdUtilities = {}

-- Int64 implementation for maximum speed (library slow)
local ONE = vector.create(0, 0, 1)
local ZERO = vector.create(0, 0, 0)
local Y_AXIS = vector.create(0, 1, 0)
local X_AXIS = vector.create(1, 0, 0)

local function FromPair(most: number, least: number): vector
	return vector.create(
		bit32.rshift(most, 10),
		bit32.bor(bit32.extract(least, 22, 10), bit32.lshift(bit32.band(most, 0b1111_1111_11), 10)),
		bit32.band(least, 0b1111_1111_1111_1111_1111_11)
	)
end
local function FromF64(f64: number): vector
	return FromPair(bit32.bor(f64 // 2 ^ 32), bit32.bor(f64 % 2 ^ 32))
end
local function FromU32(u32: number): vector
	return vector.create(0, bit32.rshift(u32, 22), bit32.band(u32, 0b1111_1111_1111_1111_1111_11))
end
local function And(lhs: vector, rhs: vector): vector
	return vector.create(bit32.band(lhs.x, rhs.x), bit32.band(lhs.y, rhs.y), bit32.band(lhs.z, rhs.z))
end
local function Or(lhs: vector, rhs: vector): vector
	return vector.create(bit32.bor(lhs.x, rhs.x), bit32.bor(lhs.y, rhs.y), bit32.bor(lhs.z, rhs.z))
end
local function Normalize(u64: vector): vector
	return vector.create(
		bit32.band(u64.x, 0b1111_1111_1111_1111_1111_11),
		bit32.band(u64.y, 0b1111_1111_1111_1111_1111),
		bit32.band(u64.z, 0b1111_1111_1111_1111_1111_11)
	)
end
local function Add(augend: vector, addend: vector): vector
	local prime = augend + addend
	prime += if prime.z > (2 ^ 22 - 1) then Y_AXIS else ZERO
	prime += if prime.y > (2 ^ 20 - 1) then X_AXIS else ZERO
	return Normalize(prime)
end
local function ToPair(u64: vector): (number, number)
	local y = u64.y
	local most = bit32.bor(bit32.lshift(u64.x, 10), bit32.rshift(y, 10))
	local least = bit32.bor(bit32.lshift(y, 22), u64.z)
	return most, least
end
local function LeftShift(u64: vector, shift: number): vector
	if shift >= 64 then
		return ZERO
	end

	if shift == 0 then
		return u64
	end

	local most, least = ToPair(u64)
	if shift < 32 then
		local remainder = 32 - shift
		return FromPair(
			bit32.replace(bit32.rshift(least, remainder), most, shift, remainder),
			bit32.lshift(least, shift)
		)
	end

	return FromPair(bit32.lshift(least, shift - 32), 0)
end
local function ToF64(u64: vector): number
	local most, least = ToPair(u64)
	return most * 2 ^ 32 + least
end
local function Subtract(minuend: vector, subtrahend: vector): vector
	local prime = minuend - subtrahend

	local adjustZ = prime.z
	local adjustY = prime.y
	local adjustX = prime.x

	local borrowZ = adjustZ < 0
	if borrowZ then
		adjustZ += 2 ^ 22
	end

	if borrowZ then
		adjustY -= 1
	end
	local borrowY = adjustY < 0
	if borrowY then
		adjustY += 2 ^ 20
	end

	if borrowY then
		adjustX -= 1
	end
	local borrowX = adjustX < 0
	if borrowX then
		adjustX += 2 ^ 22
	end

	return vector.create(adjustX, adjustY, adjustZ)
end
local function ToQuartet(u64: vector): (number, number, number, number)
	local most, least = ToPair(u64)
	local q1, q2 = bit32.rshift(most, 16), bit32.band(most, 0xffff)
	local q3, q4 = bit32.rshift(least, 16), bit32.band(least, 0xffff)
	return q1, q2, q3, q4
end
local function GreaterThan(lhs: vector, rhs: vector): boolean
	local mostLeft, leastLeft = ToPair(lhs)
	local rightMost, rightLeast = ToPair(rhs)
	return if mostLeft == rightMost then leastLeft > rightLeast else mostLeft > rightMost
end
local function GreaterThanEqualTo(lhs: vector, rhs: vector): boolean
	return lhs == rhs or GreaterThan(lhs, rhs)
end
local function CountLeftZeroes(u64: vector): number
	local most, least = ToPair(u64)
	return if most == 0 then bit32.countlz(least) + 32 else bit32.countlz(most)
end

local CHARACTERS = string.split("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "")
local CHARACTERS_LENGTH = FromU32(#CHARACTERS)
local LARGE_VALUE = FromU32(0xFFFFFF)

local CHARACTERS_LEFT_ZEROES = CountLeftZeroes(CHARACTERS_LENGTH)
local CHARACTERS_LENGTH_64 = ToF64(CHARACTERS_LENGTH)
local CHARACTERS_MOST, CHARACTERS_LEAST = ToPair(CHARACTERS_LENGTH)
local CHARACTERS_E, CHARACTERS_F, CHARACTERS_G, CHARACTERS_H = ToQuartet(CHARACTERS_LENGTH)
local ONE_MOST, ONE_LEAST = ToPair(ONE)

local IsMaybeNumber = GreenTea.build(GreenTea.optional(GreenTea.number()))

local function GetRandomSeed(): number
	return math.floor((os.clock() % 1 * 1E7) * 1000000)
end
UniqueIdUtilities.GetRandomSeed = GetRandomSeed

function UniqueIdUtilities.NextUniqueId(timestamp: number?): string
	GreenTeaAssert(IsMaybeNumber, timestamp, UniqueIdUtilities.NextUniqueId, TypeError)

	local timePart = And(FromF64(timestamp or GetRandomSeed()), LARGE_VALUE)
	local randomPart = FromU32(math.random(0, 0xFFFFFF))
	local saferId = Or(LeftShift(randomPart, 24), timePart)

	local result = ""
	while GreaterThan(saferId, ZERO) do
		local moduloDivideResult = ZERO
		if CountLeftZeroes(saferId) >= 11 and CHARACTERS_LEFT_ZEROES >= 11 then
			moduloDivideResult = FromF64(ToF64(saferId) // CHARACTERS_LENGTH_64)
		else
			local divideRemainder = saferId

			-- Doing extra iterations of this is very bad!
			for index = math.abs(CountLeftZeroes(saferId) - CHARACTERS_LEFT_ZEROES), 0, -1 do
				local shiftedDivisor: vector = nil
				if index >= 64 then
					shiftedDivisor = ZERO
				elseif index == 0 then
					shiftedDivisor = CHARACTERS_LENGTH
				elseif index < 32 then
					local shiftRemainder = 32 - index
					shiftedDivisor = FromPair(
						bit32.replace(
							bit32.rshift(CHARACTERS_LEAST, shiftRemainder),
							CHARACTERS_MOST,
							index,
							shiftRemainder
						),
						bit32.lshift(CHARACTERS_LEAST, index)
					)
				else
					shiftedDivisor = FromPair(bit32.lshift(CHARACTERS_LEAST, index - 32), 0)
				end

				if GreaterThanEqualTo(divideRemainder, shiftedDivisor) then
					divideRemainder = Subtract(divideRemainder, shiftedDivisor)
					if index >= 64 then
						moduloDivideResult = Or(moduloDivideResult, ZERO)
					elseif index == 0 then
						moduloDivideResult = Or(moduloDivideResult, ONE)
					elseif index < 32 then
						local shiftRemainder = 32 - index
						moduloDivideResult = Or(
							moduloDivideResult,
							FromPair(
								bit32.replace(bit32.rshift(ONE_LEAST, shiftRemainder), ONE_MOST, index, shiftRemainder),
								bit32.lshift(ONE_LEAST, index)
							)
						)
					else
						moduloDivideResult = Or(moduloDivideResult, FromPair(bit32.lshift(ONE_LEAST, index - 32), 0))
					end
				end
			end
		end

		local a, b, c, d = ToQuartet(moduloDivideResult)
		local product4 = d * CHARACTERS_H
		local product3 = bit32.rshift(product4, 16) + c * CHARACTERS_H
		local product2 = bit32.rshift(product3, 16)
		product3 = bit32.band(product3, 0xFFFF) + d * CHARACTERS_G

		product2 += bit32.rshift(product3, 16) + b * CHARACTERS_H
		local product1 = bit32.rshift(product2, 16)

		product2 = bit32.band(product2, 0xFFFF) + c * CHARACTERS_G
		product1 += bit32.rshift(product2, 16)
		product2 = bit32.band(product2, 0xFFFF) + d * CHARACTERS_F
		product1 += bit32.rshift(product2, 16) + a * CHARACTERS_H + b * CHARACTERS_G + c * CHARACTERS_F + d * CHARACTERS_E

		local subtractBy =
			FromPair(bit32.replace(product2, product1, 16, 16), bit32.replace(product4, product3, 16, 16))
		local moduloResult = Subtract(saferId, subtractBy)

		local added = Add(moduloResult, ONE)
		local remainder = ToF64(added)
		result = CHARACTERS[remainder] .. result

		local divideResult = ZERO
		if CountLeftZeroes(saferId) >= 11 and CHARACTERS_LEFT_ZEROES >= 11 then
			divideResult = FromF64(ToF64(saferId) // CHARACTERS_LENGTH_64)
		else
			local divideRemainder = saferId

			-- Doing extra iterations of this is very bad!
			for index = math.abs(CountLeftZeroes(saferId) - CHARACTERS_LEFT_ZEROES), 0, -1 do
				local shiftedDivisor: vector = nil
				if index >= 64 then
					shiftedDivisor = ZERO
				elseif index == 0 then
					shiftedDivisor = CHARACTERS_LENGTH
				elseif index < 32 then
					local shiftRemainder = 32 - index
					shiftedDivisor = FromPair(
						bit32.replace(
							bit32.rshift(CHARACTERS_LEAST, shiftRemainder),
							CHARACTERS_MOST,
							index,
							shiftRemainder
						),
						bit32.lshift(CHARACTERS_LEAST, index)
					)
				else
					shiftedDivisor = FromPair(bit32.lshift(CHARACTERS_LEAST, index - 32), 0)
				end

				if GreaterThanEqualTo(divideRemainder, shiftedDivisor) then
					divideRemainder = Subtract(divideRemainder, shiftedDivisor)
					if index >= 64 then
						divideResult = Or(divideResult, ZERO)
					elseif index == 0 then
						divideResult = Or(divideResult, ONE)
					elseif index < 32 then
						local shiftRemainder = 32 - index
						divideResult = Or(
							divideResult,
							FromPair(
								bit32.replace(bit32.rshift(ONE_LEAST, shiftRemainder), ONE_MOST, index, shiftRemainder),
								bit32.lshift(ONE_LEAST, index)
							)
						)
					else
						divideResult = Or(divideResult, FromPair(bit32.lshift(ONE_LEAST, index - 32), 0))
					end
				end
			end
		end
		saferId = divideResult
	end

	return result
end

local function GetResult(result: {string}): string
	return string.reverse(table.concat(result))
end

function UniqueIdUtilities.NextUniqueIdGuarantee8(timestamp: number?): string
	GreenTeaAssert(IsMaybeNumber, timestamp, UniqueIdUtilities.NextUniqueIdGuarantee8, TypeError)

	local timePart = And(FromF64(timestamp or GetRandomSeed()), LARGE_VALUE)
	local randomPart = FromU32(math.random(0, 0xFFFFFF))
	local saferId = Or(LeftShift(randomPart, 24), timePart)

	local result = table.create(8)
	local length = 0
	while GreaterThan(saferId, ZERO) do
		local moduloDivideResult = ZERO
		if CountLeftZeroes(saferId) >= 11 and CHARACTERS_LEFT_ZEROES >= 11 then
			moduloDivideResult = FromF64(ToF64(saferId) // CHARACTERS_LENGTH_64)
		else
			local divideRemainder = saferId

			-- Doing extra iterations of this is very bad!
			for index = math.abs(CountLeftZeroes(saferId) - CHARACTERS_LEFT_ZEROES), 0, -1 do
				local shiftedDivisor: vector = nil
				if index >= 64 then
					shiftedDivisor = ZERO
				elseif index == 0 then
					shiftedDivisor = CHARACTERS_LENGTH
				elseif index < 32 then
					local shiftRemainder = 32 - index
					shiftedDivisor = FromPair(
						bit32.replace(
							bit32.rshift(CHARACTERS_LEAST, shiftRemainder),
							CHARACTERS_MOST,
							index,
							shiftRemainder
						),
						bit32.lshift(CHARACTERS_LEAST, index)
					)
				else
					shiftedDivisor = FromPair(bit32.lshift(CHARACTERS_LEAST, index - 32), 0)
				end

				if GreaterThanEqualTo(divideRemainder, shiftedDivisor) then
					divideRemainder = Subtract(divideRemainder, shiftedDivisor)
					if index >= 64 then
						moduloDivideResult = Or(moduloDivideResult, ZERO)
					elseif index == 0 then
						moduloDivideResult = Or(moduloDivideResult, ONE)
					elseif index < 32 then
						local shiftRemainder = 32 - index
						moduloDivideResult = Or(
							moduloDivideResult,
							FromPair(
								bit32.replace(bit32.rshift(ONE_LEAST, shiftRemainder), ONE_MOST, index, shiftRemainder),
								bit32.lshift(ONE_LEAST, index)
							)
						)
					else
						moduloDivideResult = Or(moduloDivideResult, FromPair(bit32.lshift(ONE_LEAST, index - 32), 0))
					end
				end
			end
		end

		local a, b, c, d = ToQuartet(moduloDivideResult)
		local product4 = d * CHARACTERS_H
		local product3 = bit32.rshift(product4, 16) + c * CHARACTERS_H
		local product2 = bit32.rshift(product3, 16)
		product3 = bit32.band(product3, 0xFFFF) + d * CHARACTERS_G

		product2 += bit32.rshift(product3, 16) + b * CHARACTERS_H
		local product1 = bit32.rshift(product2, 16)

		product2 = bit32.band(product2, 0xFFFF) + c * CHARACTERS_G
		product1 += bit32.rshift(product2, 16)
		product2 = bit32.band(product2, 0xFFFF) + d * CHARACTERS_F
		product1 += bit32.rshift(product2, 16) + a * CHARACTERS_H + b * CHARACTERS_G + c * CHARACTERS_F + d * CHARACTERS_E

		local subtractBy =
			FromPair(bit32.replace(product2, product1, 16, 16), bit32.replace(product4, product3, 16, 16))
		local moduloResult = Subtract(saferId, subtractBy)

		local added = Add(moduloResult, ONE)
		local remainder = ToF64(added)
		length += 1
		result[length] = CHARACTERS[remainder]
		-- result = CHARACTERS[remainder] .. result

		if length == 8 then
			return GetResult(result)
		end

		local divideResult = ZERO
		if CountLeftZeroes(saferId) >= 11 and CHARACTERS_LEFT_ZEROES >= 11 then
			divideResult = FromF64(ToF64(saferId) // CHARACTERS_LENGTH_64)
		else
			local divideRemainder = saferId

			-- Doing extra iterations of this is very bad!
			for index = math.abs(CountLeftZeroes(saferId) - CHARACTERS_LEFT_ZEROES), 0, -1 do
				local shiftedDivisor: vector = nil
				if index >= 64 then
					shiftedDivisor = ZERO
				elseif index == 0 then
					shiftedDivisor = CHARACTERS_LENGTH
				elseif index < 32 then
					local shiftRemainder = 32 - index
					shiftedDivisor = FromPair(
						bit32.replace(
							bit32.rshift(CHARACTERS_LEAST, shiftRemainder),
							CHARACTERS_MOST,
							index,
							shiftRemainder
						),
						bit32.lshift(CHARACTERS_LEAST, index)
					)
				else
					shiftedDivisor = FromPair(bit32.lshift(CHARACTERS_LEAST, index - 32), 0)
				end

				if GreaterThanEqualTo(divideRemainder, shiftedDivisor) then
					divideRemainder = Subtract(divideRemainder, shiftedDivisor)
					if index >= 64 then
						divideResult = Or(divideResult, ZERO)
					elseif index == 0 then
						divideResult = Or(divideResult, ONE)
					elseif index < 32 then
						local shiftRemainder = 32 - index
						divideResult = Or(
							divideResult,
							FromPair(
								bit32.replace(bit32.rshift(ONE_LEAST, shiftRemainder), ONE_MOST, index, shiftRemainder),
								bit32.lshift(ONE_LEAST, index)
							)
						)
					else
						divideResult = Or(divideResult, FromPair(bit32.lshift(ONE_LEAST, index - 32), 0))
					end
				end
			end
		end
		saferId = divideResult
	end

	if length < 8 then
		-- local deltaAmount = 8 - #result
		-- local newResult = ""
		while true do
			saferId = Or(LeftShift(randomPart, 24), timePart)
			while GreaterThan(saferId, ZERO) do
				local moduloDivideResult = ZERO
				if CountLeftZeroes(saferId) >= 11 and CHARACTERS_LEFT_ZEROES >= 11 then
					moduloDivideResult = FromF64(ToF64(saferId) // CHARACTERS_LENGTH_64)
				else
					local divideRemainder = saferId

					-- Doing extra iterations of this is very bad!
					for index = math.abs(CountLeftZeroes(saferId) - CHARACTERS_LEFT_ZEROES), 0, -1 do
						local shiftedDivisor: vector = nil
						if index >= 64 then
							shiftedDivisor = ZERO
						elseif index == 0 then
							shiftedDivisor = CHARACTERS_LENGTH
						elseif index < 32 then
							local shiftRemainder = 32 - index
							shiftedDivisor = FromPair(
								bit32.replace(
									bit32.rshift(CHARACTERS_LEAST, shiftRemainder),
									CHARACTERS_MOST,
									index,
									shiftRemainder
								),
								bit32.lshift(CHARACTERS_LEAST, index)
							)
						else
							shiftedDivisor = FromPair(bit32.lshift(CHARACTERS_LEAST, index - 32), 0)
						end

						if GreaterThanEqualTo(divideRemainder, shiftedDivisor) then
							divideRemainder = Subtract(divideRemainder, shiftedDivisor)
							if index >= 64 then
								moduloDivideResult = Or(moduloDivideResult, ZERO)
							elseif index == 0 then
								moduloDivideResult = Or(moduloDivideResult, ONE)
							elseif index < 32 then
								local shiftRemainder = 32 - index
								moduloDivideResult = Or(
									moduloDivideResult,
									FromPair(
										bit32.replace(
											bit32.rshift(ONE_LEAST, shiftRemainder),
											ONE_MOST,
											index,
											shiftRemainder
										),
										bit32.lshift(ONE_LEAST, index)
									)
								)
							else
								moduloDivideResult =
									Or(moduloDivideResult, FromPair(bit32.lshift(ONE_LEAST, index - 32), 0))
							end
						end
					end
				end

				local a, b, c, d = ToQuartet(moduloDivideResult)
				local product4 = d * CHARACTERS_H
				local product3 = bit32.rshift(product4, 16) + c * CHARACTERS_H
				local product2 = bit32.rshift(product3, 16)
				product3 = bit32.band(product3, 0xFFFF) + d * CHARACTERS_G

				product2 += bit32.rshift(product3, 16) + b * CHARACTERS_H
				local product1 = bit32.rshift(product2, 16)

				product2 = bit32.band(product2, 0xFFFF) + c * CHARACTERS_G
				product1 += bit32.rshift(product2, 16)
				product2 = bit32.band(product2, 0xFFFF) + d * CHARACTERS_F
				product1 += bit32.rshift(product2, 16) + a * CHARACTERS_H + b * CHARACTERS_G + c * CHARACTERS_F + d * CHARACTERS_E

				local subtractBy =
					FromPair(bit32.replace(product2, product1, 16, 16), bit32.replace(product4, product3, 16, 16))
				local moduloResult = Subtract(saferId, subtractBy)

				local added = Add(moduloResult, ONE)
				local remainder = ToF64(added)
				length += 1
				result[length] = CHARACTERS[remainder]
				-- result = CHARACTERS[remainder] .. result

				if length == 8 then
					return GetResult(result)
				end

				local divideResult = ZERO
				if CountLeftZeroes(saferId) >= 11 and CHARACTERS_LEFT_ZEROES >= 11 then
					divideResult = FromF64(ToF64(saferId) // CHARACTERS_LENGTH_64)
				else
					local divideRemainder = saferId

					-- Doing extra iterations of this is very bad!
					for index = math.abs(CountLeftZeroes(saferId) - CHARACTERS_LEFT_ZEROES), 0, -1 do
						local shiftedDivisor: vector = nil
						if index >= 64 then
							shiftedDivisor = ZERO
						elseif index == 0 then
							shiftedDivisor = CHARACTERS_LENGTH
						elseif index < 32 then
							local shiftRemainder = 32 - index
							shiftedDivisor = FromPair(
								bit32.replace(
									bit32.rshift(CHARACTERS_LEAST, shiftRemainder),
									CHARACTERS_MOST,
									index,
									shiftRemainder
								),
								bit32.lshift(CHARACTERS_LEAST, index)
							)
						else
							shiftedDivisor = FromPair(bit32.lshift(CHARACTERS_LEAST, index - 32), 0)
						end

						if GreaterThanEqualTo(divideRemainder, shiftedDivisor) then
							divideRemainder = Subtract(divideRemainder, shiftedDivisor)
							if index >= 64 then
								divideResult = Or(divideResult, ZERO)
							elseif index == 0 then
								divideResult = Or(divideResult, ONE)
							elseif index < 32 then
								local shiftRemainder = 32 - index
								divideResult = Or(
									divideResult,
									FromPair(
										bit32.replace(
											bit32.rshift(ONE_LEAST, shiftRemainder),
											ONE_MOST,
											index,
											shiftRemainder
										),
										bit32.lshift(ONE_LEAST, index)
									)
								)
							else
								divideResult = Or(divideResult, FromPair(bit32.lshift(ONE_LEAST, index - 32), 0))
							end
						end
					end
				end
				saferId = divideResult
			end
		end

		-- result ..= newResult
	end

	return GetResult(result)
end

return table.freeze(UniqueIdUtilities)


--!optimize 2
--!strict

local PromptFor = require("@utilities/PromptFor")
local SpeedTester = require("@utilities/SpeedTester")

local Benchmark = {}
Benchmark.ArraySize = 1E5
Benchmark.BarGraphMultiplier = 70
Benchmark.TimeThreshold = 1

Benchmark.Configure = PromptFor

function Benchmark.Run(): number
	local benchmarks = {}

	local arraySize = Benchmark.ArraySize
	local array = table.create(arraySize)
	for index = 1, arraySize do
		array[index] = index
	end

	local function WriteU16(_: number): () end
	local function ValueWrite(_: number): () end

	benchmarks["generalized"] = function(): ()
		local length = #array
		WriteU16(length)
		for _, value in array do
			ValueWrite(value)
		end
	end

	benchmarks["numeric"] = function(): ()
		local length = #array
		WriteU16(length)
		for index = 1, length do
			ValueWrite(array[index])
		end
	end

	SpeedTester.SpeedTester(benchmarks, {
		BarGraphMultiplier = Benchmark.BarGraphMultiplier;
		TimeThreshold = Benchmark.TimeThreshold;
		TitleText = "Array Iteration Benchmark";
	})

	return 0
end

return Benchmark


--!optimize 2
--!strict

local HttpPromise = require("@promises/HttpPromise")
local PromiseUtilities = require("@packages/PromiseUtilities")
local PromptFor = require("@utilities/PromptFor")
local RegEx = require("@lune/regex")
local SpeedTester = require("@utilities/SpeedTester")

local Benchmark = {}
Benchmark.BarGraphMultiplier = 70
Benchmark.TimeThreshold = 1
Benchmark.UseStartAt = false

local function StringFind(value: string, pattern: RegEx.Regex, startAt: number?): (number?, number?)
	if startAt then
		value = string.sub(value, startAt)
	end

	local match = pattern:find(value)
	if match then
		return match.start, match.finish
	end

	return nil :: number?, nil :: number?
end

local Find = RegEx.new(".").find
local function StringFindCached(value: string, pattern: RegEx.Regex, startAt: number?): (number?, number?)
	if startAt then
		value = string.sub(value, startAt)
	end

	local match = Find(pattern, value)
	if match then
		return match.start, match.finish
	end

	return nil :: number?, nil :: number?
end

local FIND_PATTERN = RegEx.new("<([^>]+)>([^<]*)")

Benchmark.Configure = PromptFor

function Benchmark.Run(): number
	local html = PromiseUtilities.Await(HttpPromise.PromiseGet("https://example.org/"))
	local startAt = if Benchmark.UseStartAt then 2 else nil

	local benchmarks = {}

	benchmarks["RegEx (inline)"] = function(): (number?, number?)
		local match = FIND_PATTERN:find(if startAt then string.sub(html, startAt) else html)
		if match then
			return match.start, match.finish
		end

		return nil :: number?, nil :: number?
	end
	benchmarks["RegEx (inline, cached)"] = function(): (number?, number?)
		local match = Find(FIND_PATTERN, if startAt then string.sub(html, startAt) else html)
		if match then
			return match.start, match.finish
		end
		return nil :: number?, nil :: number?
	end

	benchmarks["RegEx (function)"] = function(): (number?, number?)
		return StringFind(html, FIND_PATTERN, startAt)
	end
	benchmarks["RegEx (function, cached)"] = function(): (number?, number?)
		return StringFindCached(html, FIND_PATTERN, startAt)
	end

	benchmarks["string.find"] = function(): (number?, number?, string?, string?)
		return string.find(html, "<([^>]+)>([^<]*)", startAt)
	end

	SpeedTester.SpeedTester(benchmarks, {
		BarGraphMultiplier = Benchmark.BarGraphMultiplier;
		TimeThreshold = Benchmark.TimeThreshold;
		TitleText = "RegEx vs string.find Benchmark";
	})

	return 0
end

return Benchmark


--!optimize 2
--!strict
--[=[ lunar
	about = "Allows you to create and run benchmarks for Lune specific code."
]=]

local GreenTea = require("@packages/GreenTea")
local LuauPolyfill = require("@packages/LuauPolyfill")
local PathFileSystem = require("@packages/PathFileSystem")
local process = require("@lune/process")
local stdio = require("@lune/stdio")

local console = LuauPolyfill.console
type Path = PathFileSystem.Path

local IsBenchmark = GreenTea.build({
	BarGraphMultiplier = GreenTea.number({integer = true});
	TimeThreshold = GreenTea.number();
	Configure = GreenTea.optional(GreenTea.anyfn());
	Run = GreenTea.anyfn();
})

type Benchmark = typeof(IsBenchmark:type())

type BenchmarkEntry = {
	Benchmark: Benchmark,
	Name: string,
}

local benchmarks: {BenchmarkEntry} = {}
local length = 0
do
	local BASE_PATH = PathFileSystem.CWD:join(".lune"):join("benchmarks")
	if not PathFileSystem.IsDirectory(BASE_PATH) then
		console.warn("No benchmarks directory found!")
		process.exit(1)
	end

	local function GetPath(filePath: PathFileSystem.Path, isFolder: boolean): Path
		return if isFolder then filePath:join("init") else filePath
	end

	for _, file in PathFileSystem.GetChildren(BASE_PATH) do
		local fileName = file:fileName()
		if not fileName then
			console.warn(`File name not found for {file}?`)
			continue
		end

		if not string.match(string.lower(fileName), "^benchmark") then
			continue
		end

		-- local file = BASE_PATH:join(potentialBenchmark)
		local filePath = GetPath(file, PathFileSystem.IsDirectory(file))
		local requirePath = filePath:stripPrefix(BASE_PATH)
		if not requirePath then
			console.warn(`{filePath} could not be stripped of the prefix.`)
			continue
		end

		local success, value = pcall(function(): any
			return require(`./{requirePath:toString()}`) :: any
		end)

		if not success then
			console.warn(`{file} failed to load - {value}`)
			continue
		end

		local typeSuccess, typeError = IsBenchmark:matches(value)
		if not typeSuccess then
			console.warn(`{filePath} failed to check - {typeError}`)
			continue
		end

		length += 1
		benchmarks[length] = {
			Benchmark = value;
			Name = string.gsub(fileName, "%.luau?$", "");
		}
	end

	table.sort(benchmarks, function(a: BenchmarkEntry, b: BenchmarkEntry): boolean
		return a.Name < b.Name
	end)
end

local names: {string} = table.create(length)
for index, benchmark in benchmarks do
	names[index] = benchmark.Name
end

local index = stdio.prompt("select", "Which benchmark do you want to run?", names)
if not index then
	process.exit(1)
end

local benchmarkEntry = benchmarks[index]
if not benchmarkEntry then
	console.warn("Invalid benchmark")
	process.exit(1)
end

local benchmark = benchmarkEntry.Benchmark
local configure = benchmark.Configure
if configure then
	configure(benchmark)
end

benchmark.Run()


--!optimize 2
--!strict
--[=[ lunar
	about = "Changes the file extensions of files in a directory."
	args = "<from-extension> <to-extension> <targets...>"
]=]

local LuauPolyfill = require("@packages/LuauPolyfill")
local PathFileSystem = require("@packages/PathFileSystem")
local process = require("@lune/process")

local Path = PathFileSystem.Path
local console = LuauPolyfill.console
type Path = PathFileSystem.Path

local arguments = table.clone(process.args)
if #arguments < 3 then
	console.warn("You must pass in at least three arguments.")
	console.log("Usage: lunar change-file-extensions <from-extension> <to-extension> <targets...>")
	process.exit(1)
end

local function Extensionify(value: string): string
	return if string.byte(value) == 46 then string.sub(value, 2) else value
end
local function Matchify(value: string): string
	return "%." .. value .. "$"
end

local fromExtension = Extensionify(table.remove(arguments, 1) :: string)
local toExtension = Extensionify(table.remove(arguments, 1) :: string)

local FixFile: (path: Path) -> ()
local FixDirectory: (path: Path) -> ()

local function FixUnknown(path: Path)
	if PathFileSystem.IsFileSafeSilent(path) then
		FixFile(path)
	else
		FixDirectory(path)
	end
end

function FixFile(path: Path): ()
	local extension = path:extension()
	if not extension or extension ~= fromExtension then
		return
	end

	local fileName = path:fileName()
	if not fileName then
		console.warn("Janky garbage library failure for fileName? %*", path:toString())
		return
	end

	local parent = path:parent()
	if not parent then
		console.warn("Janky garbage library failure for parent? %*", path:toString())
		return
	end

	PathFileSystem.Move(path, parent:join((string.gsub(fileName, Matchify(fromExtension), "." .. toExtension))))
end

function FixDirectory(path: Path): ()
	for _, file in PathFileSystem.ReadDirectory(path) do
		FixUnknown(path:join(file))
	end
end

for _, argument in arguments do
	FixUnknown(Path.new(argument))
end


--!optimize 2
--!strict
--[=[ lunar
	about = "Cleans up any .gitkeeps that we don't need anymore."
]=]

local LuauPolyfill = require("@packages/LuauPolyfill")
local PathFileSystem = require("@packages/PathFileSystem")

local Path = PathFileSystem.Path
local console = LuauPolyfill.console

local allGitKeeps: {PathFileSystem.Path} = {}
local length = 0

do
	for _, descendant in PathFileSystem.GetDescendants(Path.new("src")) do
		if descendant:fileName() == ".gitkeep" then
			length += 1
			allGitKeeps[length] = descendant
		end
	end
	for _, descendant in PathFileSystem.GetDescendants(Path.new("Vendor")) do
		if descendant:fileName() == ".gitkeep" then
			length += 1
			allGitKeeps[length] = descendant
		end
	end
	for _, descendant in PathFileSystem.GetDescendants(Path.new("VendorServer")) do
		if descendant:fileName() == ".gitkeep" then
			length += 1
			allGitKeeps[length] = descendant
		end
	end
	for _, descendant in PathFileSystem.GetDescendants(Path.new(".lune")) do
		if descendant:fileName() == ".gitkeep" then
			length += 1
			allGitKeeps[length] = descendant
		end
	end
end

for _, gitKeepPath in allGitKeeps do
	local parent = gitKeepPath:parent()
	if not parent then
		console.warn(`{gitKeepPath:toString()} has no parent?`)
		continue
	end

	if #PathFileSystem.ReadDirectory(parent) == 1 then
		-- console.log(`Cannot remove {parent:toString()}`)
		continue
	end

	console.log(`Removing {gitKeepPath:toString()}`)
	PathFileSystem.RemoveFile(gitKeepPath)
end


--!optimize 2
--!strict
--[=[ lunar
	about = "Creates a benchmark file."
	args = "[-h] [--luau] [--interp] [--init] <name>"
]=]

local ArgParse = require("@packages/ArgParse")
local BenchmarkCreators = require("@utilities/BenchmarkCreators")

local parser = ArgParse("create-benchmark", "Creates a benchmark file.")

parser:flag("--luau", "Create a Luau benchmark.")
parser:flag("--interp", "Make the benchmark interpreted mode.")
parser:flag("--init", "Make the benchmark use the init format.")
parser:argument("name", "The name of the benchmark", nil, nil)

local arguments: {
	luau: boolean?,
	interp: boolean?,
	init: boolean?,
	name: string,
} = parser:parse()

local creator: typeof(BenchmarkCreators.Luau) = if arguments.luau
	then BenchmarkCreators.Luau
	else BenchmarkCreators.TypeScript

creator(arguments.name, not arguments.interp, not not arguments.init)


--!optimize 2
--!strict
--[=[ lunar
	about = "Enables FFlagEnableLoadModule in ClientAppSettings.json for Jest testing."
]=]

local DotEnv = require("@packages/DotEnv")
local ExecuteUtilities = require("@utilities/ExecuteUtilities")
local LuauPolyfill = require("@packages/LuauPolyfill")
local PathFileSystem = require("@packages/PathFileSystem")
local process = require("@lune/process")
local serde = require("@lune/serde")
DotEnv.Load()

local Path = PathFileSystem.Path
local console = LuauPolyfill.console

local environment = process.env
local operatingSystem: process.OS = process.os

local function ExhaustiveMatch(value: never): never
	error(`Unknown value in exhaustive match: {value}`)
end

if operatingSystem == "windows" then
	local appDataPath = environment.LOCALAPPDATA
	local robloxVersionsPath = Path.new(appDataPath):join("Roblox"):join("Versions")

	if not PathFileSystem.IsDirectory(robloxVersionsPath) then
		error(`Roblox versions folder not found. Please install Roblox. Checked here: {robloxVersionsPath}`)
	end

	local mostRecentStudioVersionPath: PathFileSystem.Path? = nil
	for _, versionDirectoryPath in PathFileSystem.GetChildren(robloxVersionsPath) do
		if not PathFileSystem.IsDirectory(versionDirectoryPath) then
			console.log(`Skipping {versionDirectoryPath} because it is not a directory`)
			continue
		end

		if not PathFileSystem.IsFile(versionDirectoryPath:join("RobloxStudioBeta.exe")) then
			console.log(`Skipping {versionDirectoryPath} because it does not contain RobloxStudioBeta.exe`)
			continue
		end

		mostRecentStudioVersionPath = versionDirectoryPath
		break
	end

	if mostRecentStudioVersionPath == nil then
		error(`Roblox Studio not found. Please install Roblox. Checked here: {robloxVersionsPath}`)
	end

	local clientSettingsDirectory = mostRecentStudioVersionPath:join("ClientSettings")
	if not PathFileSystem.IsDirectory(clientSettingsDirectory) then
		PathFileSystem.WriteDirectory(clientSettingsDirectory)
	end

	local clientAppSettingsPath = clientSettingsDirectory:join("ClientAppSettings.json")
	local clientAppSettings

	if PathFileSystem.IsFile(clientAppSettingsPath) then
		clientAppSettings = serde.decode("json", PathFileSystem.ReadFile(clientAppSettingsPath))

		local metadata = PathFileSystem.Metadata(clientAppSettingsPath)
		if metadata.permissions and metadata.permissions.readOnly then
			local result = process.exec("attrib", {"-r", clientAppSettingsPath:toString()})
			assert(result.ok, `Failed to remove read-only attribute:\n{result.stderr}`)
		end
	else
		clientAppSettings = {}
	end

	clientAppSettings.FFlagEnableLoadModule = true
	PathFileSystem.WriteFile(clientAppSettingsPath, serde.encode("json", clientAppSettings))

	local result = process.exec("attrib", {"+r", clientAppSettingsPath:toString()})
	assert(result.ok, `Failed to add read-only attribute:\n{result.stderr}`)

	console.log(`Wrote FFlagEnableLoadModule to ClientAppSettings.json at {clientAppSettingsPath}`)
	process.exit(0)
elseif operatingSystem == "macos" then
	ExecuteUtilities.Execute({"./scripts/enable-loadmodule.sh"}):Assert()
elseif operatingSystem == "linux" then
	console.warn(`No support for le reddit {process.os}!`)
	process.exit(1)
else
	ExhaustiveMatch(operatingSystem)
end


--!optimize 2
--!strict

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

if process.env.windir ~= nil then
	-- Windows
	local appDataPath = process.env.LOCALAPPDATA
	local robloxVersionsPath = `{appDataPath}\\Roblox\\Versions`

	if not fs.isDir(robloxVersionsPath) then
		error(`Roblox versions folder not found. Please install Roblox. Checked here: {robloxVersionsPath}`)
	end

	local mostRecentStudioVersionPath
	for _, versionDirName in fs.readDir(robloxVersionsPath) do
		local versionDirPath = `{robloxVersionsPath}\\{versionDirName}`

		if not fs.isDir(versionDirPath) then
			continue
		end
		if not fs.isFile(`{versionDirPath}\\RobloxStudioBeta.exe`) then
			continue
		end

		mostRecentStudioVersionPath = versionDirPath
		break
	end

	if mostRecentStudioVersionPath == nil then
		error(`Roblox Studio not found. Please install Roblox. Checked here: {robloxVersionsPath}`)
	end

	local clientSettingsDir = `{mostRecentStudioVersionPath}\\ClientSettings`
	if not fs.isDir(clientSettingsDir) then
		fs.writeDir(clientSettingsDir)
	end

	local clientAppSettingsPath = `{clientSettingsDir}\\ClientAppSettings.json`
	local clientAppSettings

	if fs.isFile(clientAppSettingsPath) then
		clientAppSettings = serde.decode("json", fs.readFile(clientAppSettingsPath))

		local metadata = fs.metadata(clientAppSettingsPath)
		if metadata.permissions and metadata.permissions.readOnly then
			local result = process.exec("attrib", {"-r", clientAppSettingsPath})
			assert(result.ok, `Failed to remove read-only attribute:\n{result.stderr}`)
		end
	else
		clientAppSettings = {}
	end

	clientAppSettings.FFlagEnableLoadModule = true

	fs.writeFile(clientAppSettingsPath, serde.encode("json", clientAppSettings))

	local result = process.exec("attrib", {"+r", clientAppSettingsPath})
	assert(result.ok, `Failed to add read-only attribute:\n{result.stderr}`)

	print(`Wrote FFlagEnableLoadModule to ClientAppSettings.json at {clientAppSettingsPath}`)
else
	error(
		"This script only supports windows. Please implement support for your OS. I can't because I only have a Windows machine, sorry."
	)
end


--!optimize 2
--!strict

local ArgParse = require("@packages/ArgParse")
local LuauPolyfill = require("@packages/LuauPolyfill")
local PathFileSystem = require("@packages/PathFileSystem")
local process = require("@lune/process")

local console = LuauPolyfill.console

local parser = ArgParse("make-lune-error", "Makes Lune Error classes.")
parser:argument("names", "The names of the Error classes.", nil, nil, "+")

local arguments: {names: {string}} = parser:parse()

local ERRORS_FOLDER = PathFileSystem.CWD:join(".lune"):join("Classes"):join("Errors")
if not PathFileSystem.IsDirectory(ERRORS_FOLDER) then
	console.error(`Missing folder at {ERRORS_FOLDER}!`)
	process.exit(1)
end

local BASE_STRING = [==[--!optimize 2
--!strict

local Error = require("@packages/Error")

export type __ERROR_NAME__ = {
	name: "__ERROR_NAME__",
	message: string,
	stack: string?,
}
type Static = typeof(Error) & {
	new: (message: string) -> __ERROR_NAME__,
}

local __ERROR_NAME__: __ERROR_NAME__ & Static = setmetatable({}, {__index = Error}) :: any
(__ERROR_NAME__ :: any).__index = __ERROR_NAME__;
(__ERROR_NAME__ :: any).__tostring = function(self: __ERROR_NAME__): string
	local stack = self.stack
	if stack ~= nil and stack ~= "" then
		return stack
	end

	local message = self.message
	if message then
		return string.format("%*: %*", self.name, tostring(message))
	end

	return self.name
end

(__ERROR_NAME__ :: any).new = function(message: string): __ERROR_NAME__
	local self = Error.new(message)
	self.name = "__ERROR_NAME__"
	return (setmetatable(self, __ERROR_NAME__) :: unknown) :: __ERROR_NAME__
end

return __ERROR_NAME__
]==]

for _, errorName in arguments.names do
	if string.sub(errorName, -5) ~= "Error" then
		errorName ..= "Error"
	end

	local fileName = ERRORS_FOLDER:join(`{errorName}.luau`)
	if PathFileSystem.IsFile(fileName) then
		continue
	end

	PathFileSystem.WriteFile(fileName, (string.gsub(BASE_STRING, "__ERROR_NAME__", errorName)))
end


--!optimize 2
--!strict
--[=[ lunar
	about = "Regenerates the Enums for our unit testing."
]=]

local FormatUtilities = require("@utilities/FormatUtilities")
local LuauPolyfill = require("@packages/LuauPolyfill")
local PathFileSystem = require("@packages/PathFileSystem")
local PromiseUtilities = require("@packages/PromiseUtilities")
local RobloxEnumUtilities = require("@utilities/RobloxEnumUtilities")

local console = LuauPolyfill.console

RobloxEnumUtilities.GenerateEnumsOffline(RobloxEnumUtilities.ReturnType.Array)

local PATH_TO_ENUMS = PathFileSystem.CWD:join(".lune/Packages/Enum/init.luau")

local function CreateItemType(enumName: string, enumNames: {string}): string
	local stringBuilder = table.create(#enumNames)
	for index, enumValue in enumNames do
		stringBuilder[index] = string.format("\t| EnumItem<number, %*, %q>", enumName, enumValue)
	end
	return `export type {enumName} =\n{table.concat(stringBuilder, "\n")}`
end
local function CreateEnumType(enumName: string, enumNames: {string}): string
	local stringBuilder = table.create(#enumNames)
	for index, enumValue in enumNames do
		stringBuilder[index] = string.format("\t%*: EnumItem<number, %*, %q>,", enumValue, enumName, enumValue)
	end
	return `export type {enumName}Root = EnumObject<\{\n{table.concat(stringBuilder, "\n")}\n}, {enumName}>`
end

local function Map<T, U>(array: {T}, callback: (value: T) -> U): {U}
	local new = table.create(#array)
	for index, value in array do
		new[index] = callback(value)
	end
	return new
end
local function Sort<T>(array: {T}, callback: nil | (a: T, b: T) -> boolean): {T}
	local new = table.clone(array)
	table.sort(new, callback)
	return new
end

type FakeEnumItem = RobloxEnumUtilities.FakeEnumItem

local function SortByValue(a: FakeEnumItem, b: FakeEnumItem): boolean
	return a.Value < b.Value
end
local function ReturnName(value: FakeEnumItem): string
	return value.Name
end

local function HasIncrementalValue(array: {FakeEnumItem}): boolean
	if #array == 0 then
		return false
	end

	local newArray = Sort(array, SortByValue)
	local lastValue = (table.remove(newArray, 1) :: FakeEnumItem).Value
	if lastValue ~= 0 then
		return false
	end

	for _, entry in newArray do
		local value = entry.Value
		if lastValue + 1 ~= value then
			return false
		end
		lastValue = value
	end

	return true
end

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

local function IsNumber(value: string): boolean
	local byte = string.byte(value, 1, 1)
	return byte > 47 and byte < 58
end
local function IsNotLuauIdentifier(value: unknown): boolean
	return not (
		type(value) ~= "string"
		or #value == 0
		or string.find(value, "[^%d%a_]")
		or IsNumber(value)
		or LUAU_KEYWORDS[value]
	)
end

local function CreateEnumSetter(enumName: string, enumValues: {FakeEnumItem}, enumNames: {string}): string
	local stringBuilder = table.create(#enumValues)
	local enumString: string = nil
	local beforeAfterBraces = ""

	if HasIncrementalValue(enumValues) then
		for index, enumValue in enumNames do
			stringBuilder[index] = string.format("%q", enumValue)
		end
		enumString = table.concat(stringBuilder, ", ")
	else
		for index, enumValue in enumValues do
			local name = enumValue.Name
			if IsNotLuauIdentifier(name) then
				stringBuilder[index] = `\t{name} = {enumValue.Value};`
			else
				stringBuilder[index] = string.format("\t%q = %*;", name, enumValue.Value)
			end
		end
		enumString = table.concat(stringBuilder, "\n")
		beforeAfterBraces = "\n"
	end

	return `{CreateItemType(enumName, enumNames)}\n{CreateEnumType(enumName, enumNames)}\nEnum.{enumName} = \{{beforeAfterBraces}{enumString}{beforeAfterBraces}}`
end

local enums = RobloxEnumUtilities.GenerateEnumsOffline(RobloxEnumUtilities.ReturnType.Array)

local size = #enums
local stringBuilder = table.create(size + 1)

local allEnumNames = table.create(size)
local returnStringBuilder = {"return Enum :: Enums<{"}

console.log("Starting!")

for index, enum in enums do
	local enumValues = Sort(enum.EnumItems, SortByValue)
	local names = Map(enumValues, ReturnName)
	allEnumNames[index] = `{enum}Root`
	stringBuilder[index] = CreateEnumSetter(enum.Name, enumValues, names)
	returnStringBuilder[index + 1] = `\t{enum}: {enum}Root,`
end
returnStringBuilder[size + 1] = `}, {table.concat(allEnumNames, " | ")}>`
stringBuilder[size + 1] = table.concat(returnStringBuilder, "\n")

local ROOT_STRING = [[--!nocheck
--!optimize 2

local Enum = require("@self/Enum")

type SimpleResult<T, E> = {
	Success: true,
	Value: T,
} | {
	Success: false,
	Error: E,
}

type EnumItem<Value, Type, Name = string> = {
	EnumType: Type,
	Name: Name,
	Value: Value,
}
type EnumObject<Values, ArrayValues> = {
	Cast: ((self: EnumObject<Values, ArrayValues>, check: unknown) -> ArrayValues)
		& ((self: EnumObject<Values, ArrayValues>, check: unknown) -> (false, string)),
	CastResult: (self: EnumObject<Values, ArrayValues>, check: unknown) -> SimpleResult<ArrayValues, string>,
	GetEnumItems: (self: EnumObject<Values, ArrayValues>) -> {ArrayValues},
} & Values

type Enums<Values, ArrayValues> = {
	GetEnums: (self: Enums<Values, ArrayValues>) -> {ArrayValues},
} & Values

%*]]

console.log("Formatting...")
local success, source = PromiseUtilities.AwaitMulti(
	FormatUtilities.FormatAsync(string.format(ROOT_STRING, table.concat(stringBuilder, "\n\n")), {
		FormatAmount = 2;
		FormatWith = "stylua2-dg";
	})
)
console.log("Writing...")

if success then
	PathFileSystem.WriteFile(PATH_TO_ENUMS, source)
else
	console.error(`Failed to format the enums - {source}`)
end


--!optimize 2
--!strict
--[=[ lunar
	about = "Regenerates the Enums for our unit testing."
]=]

local ApiDumpUtilities = require("@utilities/ApiDumpUtilities")
local FormatUtilities = require("@utilities/FormatUtilities")
local GetGlobalTypesAsync = require("@promises/GetGlobalTypesAsync")
local LuauPolyfill = require("@packages/LuauPolyfill")
local PathFileSystem = require("@packages/PathFileSystem")
local PromiseUtilities = require("@packages/PromiseUtilities")

local Await = PromiseUtilities.Await
local console = LuauPolyfill.console

local PATH_TO_ENUMS = PathFileSystem.CWD:join(".lune/Packages/Enum/init.luau")

local apiDump = Await(ApiDumpUtilities.GetEnumsFromDumpAsync())
local globalTypes = Await(GetGlobalTypesAsync(true))

local function CreateItemType(enumName: string, enumNames: {string}): string
	local stringBuilder = table.create(#enumNames)
	for index, enumValue in enumNames do
		stringBuilder[index] = string.format("\t| EnumItem<number, %*, %q>", enumName, enumValue)
	end
	return `export type {enumName} =\n{table.concat(stringBuilder, "\n")}`
end
local function CreateEnumType(enumName: string, enumNames: {string}): string
	local stringBuilder = table.create(#enumNames)
	for index, enumValue in enumNames do
		stringBuilder[index] = string.format("\t%*: EnumItem<number, %*, %q>,", enumValue, enumName, enumValue)
	end
	return `export type {enumName}Root = EnumObject<\{\n{table.concat(stringBuilder, "\n")}\n}, {enumName}>`
end

local function Map<T, U>(array: {T}, callback: (value: T) -> U): {U}
	local new = table.create(#array)
	for index, value in array do
		new[index] = callback(value)
	end
	return new
end
local function Sort<T>(array: {T}, callback: nil | (a: T, b: T) -> boolean): {T}
	local new = table.clone(array)
	table.sort(new, callback)
	return new
end

type EnumItem = {
	Name: string,
	Value: number,
}

local function SortByValue(a: EnumItem, b: EnumItem): boolean
	return a.Value < b.Value
end
local function ReturnName(value: EnumItem): string
	return value.Name
end

local function HasIncrementalValue(array: {EnumItem}): boolean
	if #array == 0 then
		return false
	end

	local newArray = Sort(array, SortByValue)
	local lastValue = (table.remove(newArray, 1) :: EnumItem).Value
	if lastValue ~= 0 then
		return false
	end

	for _, entry in newArray do
		local value = entry.Value
		if lastValue + 1 ~= value then
			return false
		end
		lastValue = value
	end

	return true
end

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

local function IsNumber(value: string): boolean
	local byte = string.byte(value, 1, 1)
	return byte > 47 and byte < 58
end
local function IsNotLuauIdentifier(value: unknown): boolean
	return not (
		type(value) ~= "string"
		or #value == 0
		or string.find(value, "[^%d%a_]")
		or IsNumber(value)
		or LUAU_KEYWORDS[value]
	)
end

local function CreateEnumSetter(enumName: string, enumValues: {EnumItem}, enumNames: {string}): string
	local stringBuilder = table.create(#enumValues)
	local enumString: string = nil
	local beforeAfterBraces = ""

	if HasIncrementalValue(enumValues) then
		for index, enumValue in enumNames do
			stringBuilder[index] = string.format("%q", enumValue)
		end
		enumString = table.concat(stringBuilder, ", ")
	else
		for index, enumValue in enumValues do
			local name = enumValue.Name
			if IsNotLuauIdentifier(name) then
				stringBuilder[index] = `\t{name} = {enumValue.Value};`
			else
				stringBuilder[index] = string.format("\t%q = %*;", name, enumValue.Value)
			end
		end
		enumString = table.concat(stringBuilder, "\n")
		beforeAfterBraces = "\n"
	end

	return `{CreateItemType(enumName, enumNames)}\n{CreateEnumType(enumName, enumNames)}\nEnum.{enumName} = \{{beforeAfterBraces}{enumString}{beforeAfterBraces}}`
end

local function Filter<T>(array: {T}, predicate: (value: T) -> boolean): {T}
	local new = table.create(#array)
	local length = 0

	for _, value in array do
		if predicate(value) then
			length += 1
			new[length] = value
		end
	end

	return new
end

local enums = Sort(
	Filter(apiDump.Enums, function(enum)
		return string.find(globalTypes, `Enum{enum.Name}`) ~= nil
	end),
	function(a, b): boolean
		return a.Name < b.Name
	end
)

local size = #enums
local stringBuilder = table.create(size + 1)

local allEnumNames = table.create(size)
local returnStringBuilder = {"return Enum :: Enums<{"}

for index, enum in enums do
	local enumValues = Sort(enum.Items, SortByValue)
	local enumName = enum.Name
	local enumNames = Map(enumValues, ReturnName)
	allEnumNames[index] = `{enumName}Root`
	stringBuilder[index] = CreateEnumSetter(enumName, enumValues, enumNames)
	returnStringBuilder[index + 1] = `\t{enumName}: {enumName}Root,`
end
returnStringBuilder[size + 1] = `}, {table.concat(allEnumNames, " | ")}>`
stringBuilder[size + 1] = table.concat(returnStringBuilder, "\n")

local ROOT_STRING = [[--!nocheck
--!optimize 2

local Enum = require("@self/Enum")

type SimpleResult<T, E> = {
	Success: true,
	Value: T,
} | {
	Success: false,
	Error: E,
}

type EnumItem<Value, Type, Name = string> = {
	EnumType: Type,
	Name: Name,
	Value: Value,
}
type EnumObject<Values, ArrayValues> = {
	Cast: ((self: EnumObject<Values, ArrayValues>, check: unknown) -> ArrayValues)
		& ((self: EnumObject<Values, ArrayValues>, check: unknown) -> (false, string)),
	CastResult: (self: EnumObject<Values, ArrayValues>, check: unknown) -> SimpleResult<ArrayValues, string>,
	GetEnumItems: (self: EnumObject<Values, ArrayValues>) -> {ArrayValues},
} & Values

type Enums<Values, ArrayValues> = {
	GetEnums: (self: Enums<Values, ArrayValues>) -> {ArrayValues},
} & Values

%*]]

local success, source = PromiseUtilities.AwaitMulti(
	FormatUtilities.FormatAsync(string.format(ROOT_STRING, table.concat(stringBuilder, "\n\n")), {
		FormatAmount = 2;
		FormatWith = "stylua2-dg";
	})
)
if success then
	PathFileSystem.WriteFile(PATH_TO_ENUMS, source)
else
	console.error(`Failed to format the enums - {source}`)
end


--!optimize 2
--!strict
--[=[ lunar
	about = "Removes zone identifier files."
	args = "<targets...>"
]=]

local ArgParse = require("@packages/ArgParse")
local LuauPolyfill = require("@packages/LuauPolyfill")
local PathFileSystem = require("@packages/PathFileSystem")

local Path = PathFileSystem.Path
local console = LuauPolyfill.console

type Path = PathFileSystem.Path

local parser = ArgParse("remove-identifiers", "Removes zone identifier files.")

parser:argument("targets", "The target folders to remove the files from.", nil, nil, "+")

local arguments: {targets: {string}} = parser:parse()

for _, target in arguments.targets do
	local success, path = pcall(Path.from, target)
	if not success then
		console.error("Could not parse path %*", target)
		continue
	end

	if not PathFileSystem.IsDirectorySafe(path) then
		continue
	end

	for _, descendant in PathFileSystem.GetDescendants(path) do
		if not PathFileSystem.IsFileSafeSilent(descendant) then
			continue
		end

		if descendant:extension() == "Identifier" then
			PathFileSystem.RemoveFile(descendant)
		end
	end
end