--!optimize 2
--!strict

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

--[=[
	The functionality of this is meant to be the same as the `await` keyword in
	ECMAScript.

	```ts
	async function promiseGet(url: string) {
		return HttpService.GetAsync(url);
	}

	const result = await promiseGet("https://example.com");
	```

	```lua
	-- This is good practice - you don't really lose debug info this way.
	local function GetAsync(url: string)
		return HttpService:GetAsync(url)
	end

	local PromiseGet = PromiseUtilities.FastAsync(GetAsync)
	local result = PromiseUtilities.Await(PromiseGet("https://example.com"))
	```

	:::tip Behavior
	This function is almost entirely identical to `Promise.expect`. You can
	probably just use that instead!
	:::

	@function Await
	@within PromiseUtilities

	@error CancelledError -- Thrown if the Promise was cancelled.
	@error RejectedError -- Thrown if the Promise was rejected.

	@yields
	@param promiseOrValue Promise<T> | T
	@return T
]=]
local function Await<T>(promiseOrValue: Promise.Promise<T> | T): T
	if not Promise.Is(promiseOrValue) then
		return promiseOrValue :: T
	end

	local status, value = (promiseOrValue :: Promise.Promise<T>):WaitStatus()
	if status == Promise.Status.Resolved then
		return value
	end

	if status == Promise.Status.Rejected then
		error(value, 2)
	end

	error("The awaited Promise was cancelled", 2)
end

return Await


--!optimize 2
--!strict

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

local function PackRest<S, T...>(statusOrSuccess: S, ...: T...)
	-- table.pack cringes with typed varargs, so we do this instead.
	-- selene: allow(mixed_table)
	return statusOrSuccess, {
		n = select("#", ...);
		select(1, ...);
	}
end

--[=[
	The functionality of this is meant to be the same as the `await` keyword in
	ECMAScript. This allows returning multiple values!

	```lua
	local function PromiseUpperAndLower(value: string): Promise<string, string>
		return Promise.new(function(resolve)
			resolve(string.upper(value), string.lower(value))
		end)
	end
	local upper, lower = PromiseUtilities.AwaitMulti(PromiseUpperAndLower("Hello there!"))
	```

	:::tip Behavior
	This function is almost entirely identical to `Promise.expect`. You can
	probably just use that instead!
	:::

	@function AwaitMulti
	@within PromiseUtilities

	@error CancelledError -- Thrown if the Promise was cancelled.
	@error RejectedError -- Thrown if the Promise was rejected.

	@yields
	@param ... T | Promise<T...>
	@return T
]=]
local function AwaitMulti<T...>(...: any): T...
	local promise: Promise.Promise<T...> = ...
	if not Promise.Is(promise) then
		return ...
	end

	local status, values = PackRest(promise:WaitStatus())
	if status == Promise.Status.Resolved then
		return table.unpack(values, 1, values.n)
	end

	if status == Promise.Status.Rejected then
		error(values[1] or "Unknown rejection?", 2)
	end

	error("The awaited Promise was cancelled", 2)
end

type AwaitPromise = <T...>(promise: Promise.Promise<T...>) -> T...
type AwaitVariadic = <T...>(T...) -> T...

return AwaitMulti :: AwaitPromise & AwaitVariadic


--!optimize 2
--!strict

local Promise = require("@packages/Promise")
local ThreadUtilities = require("@packages/ThreadUtilities")

--[=[
	A function that turns another function into the same as an async function
	in ECMAScript. This uses a thread recycling "fast spawn" function instead
	of `coroutine.wrap`.

	```ts
	async function promiseGet(url: string) {
		return HttpService.GetAsync(url);
	}
	```

	would basically be equivalent to:

	```lua
	local function GetAsync(url: string)
		return HttpService:GetAsync(url)
	end

	local PromiseGet = PromiseUtilities.FastAsync(GetAsync)
	```

	@function FastAsync
	@within PromiseUtilities

	@param callback (Arguments...) -> Returns
	@return (Arguments...) -> Promise<Returns>
]=]
local function FastAsync<Returns, Arguments...>(callback: (Arguments...) -> Returns)
	local function fastAsyncFunction(...: Arguments...)
		local arguments = {select(1, ...)}
		local length = select("#", ...)

		return Promise.new(function(resolve, reject)
			ThreadUtilities.FastSpawn(function()
				local success, value = pcall(callback, table.unpack(arguments, 1, length))
				if success then
					resolve(value)
				else
					reject(value)
				end
			end)
		end)
	end

	return fastAsyncFunction
end

--[=[
	Basically equivalent to [PromiseUtilities.FastAsync], except you are not
	expected to have any return. This only exists because Luau's type system
	is not the greatest at handling this.

	@function FastAsyncVoid
	@within PromiseUtilities

	@param callback (Arguments...) -> void
	@return (Arguments...) -> Promise<void>
]=]

return FastAsync


--!optimize 2
--!strict

local Promise = require("@packages/Promise")
local ThreadUtilities = require("@packages/ThreadUtilities")

local function PackRest<S, T...>(statusOrSuccess: S, ...: T...)
	-- table.pack cringes with typed varargs, so we do this instead.
	-- selene: allow(mixed_table)
	return statusOrSuccess, {
		n = select("#", ...);
		select(1, ...);
	}
end

--[=[
	A function that turns another function into the same as an async function
	in ECMAScript. This supports multiple returns however.

	```ts
	async function promiseUpperAndLower(value: string) {
		return [value.toUpperCase(), value.toLowerCase()];
	}
	```

	would basically be equivalent to:

	```lua
	local function GetUpperAndLowerAsync(value: string)
		return string.upper(value), string.lower(value)
	end

	local PromiseUpperAndLower = PromiseUtilities.FastAsyncMulti(GetUpperAndLowerAsync)
	```

	@function FastAsyncMulti
	@within PromiseUtilities

	@param callback (Arguments...) -> Returns...
	@return (Arguments...) -> Promise<Returns...>
]=]
local function FastAsyncMulti<Arguments..., Returns...>(callback: (Arguments...) -> Returns...)
	local function asyncFunction(...: Arguments...): Promise.TypedPromise<Returns...>
		local arguments = {select(1, ...)}
		local length = select("#", ...)

		return Promise.new(function(resolve, reject)
			ThreadUtilities.FastSpawn(function()
				local success, values = PackRest(pcall(callback, table.unpack(arguments, 1, length)))
				if success then
					resolve(table.unpack(values, 1, values.n))
				else
					reject(table.unpack(values, 1, values.n))
				end
			end)
		end)
	end

	return asyncFunction
end

return FastAsyncMulti


--!optimize 2
--!strict

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

local Async = require("@self/Async")
local AsyncMulti = require("@self/AsyncMulti")
local Await = require("@self/Await")
local AwaitMulti = require("@self/AwaitMulti")
local FastAsync = require("@self/FastAsync")
local FastAsyncMulti = require("@self/FastAsyncMulti")

export type Error = Promise.Error
export type ErrorKind = Promise.ErrorKind
export type Status = Promise.Status

export type Promise<T...> = Promise.Promise<T...>
export type VoidPromise = Promise.VoidPromise

type VoidedAsync = <Arguments...>(callback: (Arguments...) -> ()) -> (Arguments...) -> VoidPromise

--[=[
	Utilities for working with Promises.
	@class PromiseUtilities
]=]
local PromiseUtilities = table.freeze({
	Async = Async;
	AsyncVoid = (Async :: never) :: VoidedAsync;
	AsyncMulti = AsyncMulti;
	Await = Await;
	AwaitMulti = AwaitMulti;
	FastAsync = FastAsync;
	FastAsyncVoid = (FastAsync :: never) :: VoidedAsync;
	FastAsyncMulti = FastAsyncMulti;

	Promise = Promise;
})

return PromiseUtilities


--!optimize 2
--!strict

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

local console = LuauPolyfill.console

local isWallyPackage = GreenTea.build({
	dependencies = GreenTea.optional({[GreenTea.string()] = GreenTea.string()});
	["dev-dependencies"] = GreenTea.optional({[GreenTea.string()] = GreenTea.string()});
	["server-dependencies"] = GreenTea.optional({[GreenTea.string()] = GreenTea.string()});
	package = {
		authors = GreenTea.optional(GreenTea.array(GreenTea.string()));
		description = GreenTea.optional(GreenTea.string());
		exclude = GreenTea.optional(GreenTea.array(GreenTea.string()));
		include = GreenTea.optional(GreenTea.array(GreenTea.string()));
		license = GreenTea.optional(GreenTea.union(GreenTea.literal("MIT"), GreenTea.literal("Apache-2.0"))) :: nil | "MIT" | "Apache-2.0";
		name = GreenTea.string();
		private = GreenTea.optional(GreenTea.boolean());
		realm = GreenTea.union(GreenTea.literal("shared"), GreenTea.literal("server")) :: "shared" | "server";
		registry = GreenTea.string();
		version = GreenTea.string();
	};
})
type WallyPackage = typeof(isWallyPackage:type())

local function ParsePrereleaseAndBuildWithSign(value: string): (string, string)
	local prereleaseWithSign, buildWithSign = string.match(value, "^(-[^+]+)(+.+)$")
	if prereleaseWithSign == nil or buildWithSign == nil then
		prereleaseWithSign = string.match(value, "^(-.+)$")
		buildWithSign = string.match(value, "^(+.+)$")
	end

	if prereleaseWithSign == nil and buildWithSign == nil then
		error(string.format("The parameter %q must begin with + or - to denote a prerelease or a build", value))
	end

	return prereleaseWithSign :: string, buildWithSign :: string
end

local function ParsePrerelease(prereleaseWithSign: string?): string?
	if prereleaseWithSign == nil then
		return nil
	end

	local prerelease = string.match(prereleaseWithSign, "^-(%w[%.%w-]*)$" :: string)
	if prerelease == nil then
		error(
			string.format(
				"The prerelease %q is not a slash followed by alphanumerics, dots and slashes",
				prereleaseWithSign
			)
		)
	end

	return prerelease
end

local function ParseBuild(buildWithSign: string?): string?
	if buildWithSign == nil then
		return nil
	end

	local build = string.match(buildWithSign, "^%+(%w[%.%w-]*)$" :: string)
	if build == nil then
		error(string.format("The build %q is not a + sign followed by alphanumerics, dots and slashes", buildWithSign))
	end

	return build
end

type Option = {
	Build: string,
	Prerelease: string,
} | {
	Build: nil,
	Prerelease: nil,
}

local function ParsePrereleaseAndBuild(value: string?): Option
	if not (value ~= "" and value) then
		return {}
	end

	local prereleaseWithSign, buildWithSign = ParsePrereleaseAndBuildWithSign(value)
	local prerelease = ParsePrerelease(prereleaseWithSign)
	local build = ParseBuild(buildWithSign)
	return {
		Build = build :: string;
		Prerelease = prerelease :: string;
	}
end

local function ParseVersion(value: string)
	local stringMajor, stringMinor, stringPatch, stringPrereleaseAndBuild =
		string.match(value, "^(%d+)%.?(%d*)%.?(%d*)(.-)$")
	if stringMajor == nil then
		error(string.format("Could not extract version number(s) from %q", value))
	end

	local major = tonumber(stringMajor) :: number
	local minor = tonumber(stringMinor) :: number
	local patch = tonumber(stringPatch) :: number
	local option = ParsePrereleaseAndBuild(stringPrereleaseAndBuild :: string)
	return major, minor, patch, option.Prerelease, option.Build
end

local function _Compare(a: any, b: any)
	return if a == b then 0 elseif a < b then -1 else 1
end
local Compare = (_Compare :: never) :: ((a: number, b: number) -> number) & ((a: string, b: string) -> number)

local function CompareIds(selfId: string, otherId: string)
	if selfId == otherId then
		return 0
	end

	if selfId == nil then
		return -1
	end

	if otherId == nil then
		return 1
	end

	local selfNumber = tonumber(selfId)
	local otherNumber = tonumber(otherId)
	if selfNumber and otherNumber then
		return Compare(selfNumber, otherNumber)
	end

	if selfNumber then
		return -1
	end

	if otherNumber then
		return 1
	end

	return Compare(selfId, otherId)
end

local function SmallerIdList(selfIds: {string}, otherIds: {string})
	for index, value in selfIds do
		local comparison = CompareIds(value, otherIds[index])
		if comparison ~= 0 then
			return comparison == -1
		end
	end

	return #selfIds < #otherIds
end

local function SmallerPrerelease(mine: string?, other: string?)
	if mine == other or not mine then
		return false
	end

	if not other then
		return true
	end

	return SmallerIdList(string.split(mine, "."), string.split(other, "."))
end

export type SemanticVersion = typeof(setmetatable(
	{} :: {
		Major: number,
		Minor: number,
		Patch: number,
		Prerelease: string?,
		Build: string?,

		NextMajor: (self: SemanticVersion) -> SemanticVersion,
		NextMinor: (self: SemanticVersion) -> SemanticVersion,
		NextPatch: (self: SemanticVersion) -> SemanticVersion,
	},
	{} :: {
		__eq: (self: SemanticVersion, other: SemanticVersion) -> boolean,
		__lt: (self: SemanticVersion, other: SemanticVersion) -> boolean,
		__pow: (self: SemanticVersion, other: SemanticVersion) -> boolean,
		__tostring: (self: SemanticVersion) -> string,
	}
))

type BaseStatics = {
	ClassName: "SemanticVersion",
	new: (
		major: number | string,
		minor: number?,
		patch: number?,
		prerelease: string?,
		build: string?
	) -> SemanticVersion,

	FromWally: () -> SemanticVersion?,
}
type SemanticVersionStatics = BaseStatics & {
	__eq: (self: SemanticVersion, other: SemanticVersion) -> boolean,
	__lt: (self: SemanticVersion, other: SemanticVersion) -> boolean,
	__pow: (self: SemanticVersion, other: SemanticVersion) -> boolean,
	__tostring: (self: SemanticVersion) -> string,
}

local SemanticVersion = {} :: SemanticVersion & SemanticVersionStatics
SemanticVersion.ClassName = "SemanticVersion";
(SemanticVersion :: any).__index = SemanticVersion

function SemanticVersion.new(
	major: number | string,
	minor: number?,
	patch: number?,
	prerelease: string?,
	build: string?
): SemanticVersion
	local self = setmetatable({}, SemanticVersion)
	if type(major) == "string" then
		major, minor, patch, prerelease, build = ParseVersion(major)
	end

	local trueMinor = minor or 0
	local truePatch = patch or 0

	self.Major = major
	self.Minor = trueMinor
	self.Patch = truePatch
	self.Prerelease = prerelease
	self.Build = build
	table.freeze(self)
	return (self :: any) :: SemanticVersion
end

local WALLY_TOML_PATH = PathFileSystem.Path.new("wally.toml")

function SemanticVersion.FromWally()
	if not PathFileSystem.IsFileSafe(WALLY_TOML_PATH) then
		return nil
	end

	local readSuccess, fileSource = pcall(PathFileSystem.ReadFile, WALLY_TOML_PATH)
	if not readSuccess then
		console.warn(`Failed to read {WALLY_TOML_PATH} - {fileSource}`)
		return nil
	end

	local decodeSuccess, toml: WallyPackage = pcall(serde.decode, "toml" :: "toml", fileSource)
	if not decodeSuccess then
		console.warn(`Failed to decode {WALLY_TOML_PATH} - {toml}`)
		return nil
	end

	local typeSuccess, typeError = isWallyPackage:matches(toml)
	if not typeSuccess then
		console.warn(`Failed to type check {WALLY_TOML_PATH} - {typeError}`)
		return nil
	end

	return SemanticVersion.new(toml.package.version)
end

function SemanticVersion:NextMajor(): SemanticVersion
	return SemanticVersion.new(self.Major + 1, 0, 0)
end

function SemanticVersion:NextMinor(): SemanticVersion
	return SemanticVersion.new(self.Major, self.Minor + 1, 0)
end

function SemanticVersion:NextPatch(): SemanticVersion
	return SemanticVersion.new(self.Major, self.Minor, self.Patch + 1)
end

function SemanticVersion:__eq(other)
	return self.Major == other.Major
		and self.Minor == other.Minor
		and self.Patch == other.Patch
		and self.Prerelease == other.Prerelease
end
function SemanticVersion:__lt(other)
	if self.Major ~= other.Major then
		return self.Major < other.Major
	end

	if self.Minor ~= other.Minor then
		return self.Minor < other.Minor
	end

	if self.Patch ~= other.Patch then
		return self.Patch < other.Patch
	end

	return SmallerPrerelease(self.Prerelease, other.Prerelease)
end

function SemanticVersion:__pow(other)
	return if self.Major == 0 then self == other else self.Major == other.Major and self.Minor <= other.Minor
end

function SemanticVersion:__tostring()
	local stringBuilder = {string.format("%*.%*.%*", self.Major, self.Minor, self.Patch)}
	local build = self.Build
	local prerelease = self.Prerelease

	if prerelease ~= "" and prerelease then
		table.insert(stringBuilder, `-{prerelease}`)
	end

	if build ~= "" and build then
		table.insert(stringBuilder, `+{build}`)
	end

	return table.concat(stringBuilder)
end

return table.freeze(SemanticVersion :: BaseStatics)


--!optimize 2
--!strict

local Error = require("@packages/Error")
local GreenTea = require("@packages/GreenTea")
local GreenTeaUtilities = require("@packages/GreenTeaUtilities")
local TableUtilities = require("@packages/TableUtilities")

type ComparisonFunction<T> = (a: T, b: T) -> boolean

export type SortedArray<T> = typeof(setmetatable(
	{} :: {
		Array: {T},
		Comparison: ComparisonFunction<T>?,

		-- add when luau sucks less
		-- Map: <U>(self: SortedArray<T>, callback: (value: T, index: number, array: {T}) -> U, comparison: ComparisonFunction<T>?) -> SortedArray<U>,
		-- MapFiltered: <U>(self: SortedArray<T>, callback: (value: T, index: number, array: {T}) -> U, comparison: ComparisonFunction<T>?) -> SortedArray<U>, -- NonNullable<U>

		Insert: (self: SortedArray<T>, value: T) -> number,

		Find: (
			self: SortedArray<T>,
			value: T,
			equalityCheck: ComparisonFunction<T>?,
			lessThan: ComparisonFunction<T>?,
			low: number?,
			high: number?
		) -> number?,
		IndexOf: (
			self: SortedArray<T>,
			value: T,
			equalityCheck: ComparisonFunction<T>?,
			lessThan: ComparisonFunction<T>?,
			low: number?,
			high: number?
		) -> number?,

		Copy: (self: SortedArray<T>) -> {T},
		CopyDeep: (self: SortedArray<T>) -> {T},

		Clone: (self: SortedArray<T>) -> SortedArray<T>,
		CloneDeep: (self: SortedArray<T>) -> SortedArray<T>,

		RemoveElement: (
			self: SortedArray<T>,
			signature: T,
			equalityCheck: ComparisonFunction<T>?,
			lessThan: ComparisonFunction<T>?
		) -> T?,

		RemoveIndex: (self: SortedArray<T>, index: number) -> T?,
		Pop: (self: SortedArray<T>) -> T?,
		Shift: (self: SortedArray<T>) -> T?,

		Sort: (self: SortedArray<T>) -> SortedArray<T>,
		SortIndex: (self: SortedArray<T>, index: number) -> number,
		SortElement: (
			self: SortedArray<T>,
			element: T,
			equalityCheck: ComparisonFunction<T>?,
			lessThan: ComparisonFunction<T>?
		) -> number,

		GetIntersection: (
			self: SortedArray<T>,
			other: SortedArray<T>,
			equalityCheck: ComparisonFunction<T>?,
			lessThan: ComparisonFunction<T>?
		) -> SortedArray<T>,

		IsEmpty: (self: SortedArray<T>) -> boolean,
	},
	{} :: {
		__len: (self: SortedArray<T>) -> number,
		__tostring: (self: SortedArray<T>) -> string,
	}
))

type Static = {
	ClassName: "SortedArray",
	new: <T>(baseArray: {T}?, comparison: ComparisonFunction<T>?) -> SortedArray<T>,
	Is: (value: any) -> boolean,
	instanceof: (value: any) -> boolean,
}
type PrivateStatic = Static & {
	__len: (self: SortedArray<any>) -> number,
	__tostring: (self: SortedArray<any>) -> string,
}

--[=[
	A class to create automatically sorted arrays. Must contain objects
	comparable to one another (that can use the `<` and `==` operators).
	Numbers and strings support these operators by default.

	@tag Optimization
	@tag Supports Debug Mode
	@tag Supports __len
	@tag Supports __tostring
	@class SortedArray
]=]
local SortedArray = {} :: SortedArray<any> & Static
local Private = SortedArray :: SortedArray<any> & PrivateStatic
SortedArray.ClassName = "SortedArray"
(SortedArray :: any).__index = SortedArray

local cached: boolean = nil
local function IsUltraStrictMode(): boolean
	if cached ~= nil then
		return cached
	end

	local isUltraStrictMode = not not _G.__DEV__
	cached = isUltraStrictMode
	return isUltraStrictMode
end
local function Sort<T>(array: {T}, comparison: ComparisonFunction<T>?): {T}
	table.sort(array, comparison)
	return array
end

local IsIntegerRaw = GreenTea.number({integer = true})

local IsInteger = GreenTea.build(IsIntegerRaw)
local IsMaybeArray = GreenTea.build(GreenTea.optional(GreenTea.array(GreenTea.unknown())))
local IsMaybeFunction = GreenTea.build(GreenTea.optional(GreenTea.anyfn()))
local IsMaybeInteger = GreenTea.build(GreenTea.optional(IsIntegerRaw))
local IsSortedArray = GreenTea.build(GreenTeaUtilities.IsInstanceOfLuauClassFromTable(SortedArray, "SortedArray"))
local IsUnknown = GreenTea.build(GreenTea.unknown())

--[=[
	The actual array of data. You can iterate over this or index it.

	:::warning Readonly
	_PLEASE_ treat this array as readonly. Do not mutate it directly.
	:::

	@readonly
	@prop Array ReadonlyArray<T>
	@within SortedArray
]=]

--[=[
	The comparison function of the `SortedArray`.

	@readonly
	@prop Comparison ComparisonFunction<T>?
	@within SortedArray
]=]

local function FindClosest<T>(
	array: {T},
	value: T,
	low: number,
	high: number,
	equalityCheck: ComparisonFunction<T>?,
	lessThan: ComparisonFunction<T>?
): number?
	local middle = (low + high) // 2
	if middle == 0 then
		return nil
	end

	local value2 = array[middle]
	while middle ~= high do
		if equalityCheck then
			if equalityCheck(value, value2) then
				return middle
			end
		elseif value == value2 then
			return middle
		end

		local boolean = false
		if value2 then
			boolean = if lessThan then lessThan(value, value2) else (value :: never) < value2
		end

		if boolean then
			high = middle - 1
		else
			low = middle + 1
		end

		middle = (low + high) // 2
		value2 = array[middle]
	end

	return middle
end

--[=[
	Instantiates and returns a new SortedArray, with optional parameters.

	@error DebugTypeError -- Thrown when you are using debug mode and you pass an incorrect type.

	@param baseArray? {T} -- An array of data which will be sorted upon instantiation. If this is omitted, an empty array is used.
	@param comparison? ComparisonFunction<T> -- An optional comparison function which is used to customize the element sorting, which will be given two elements `a` and `b` from the array as parameters. The function should return a boolean value specifying whether the first argument should be before the second argument in the sequence. If no comparison function is passed, the Luau-default `a < b` sorting is used.
	@return SortedArray<T>
]=]
function SortedArray.new<T>(baseArray: {T}?, comparison: ComparisonFunction<T>?): SortedArray<T>
	if IsUltraStrictMode() then
		local typeSuccess0, typeError0 = IsMaybeArray:matches(baseArray)
		if not typeSuccess0 then
			local exception = Error.new(tostring(typeError0))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, SortedArray.new)
			error(exception)
		end
		local typeSuccess1, typeError1 = IsMaybeFunction:matches(comparison)
		if not typeSuccess1 then
			local exception = Error.new(tostring(typeError1))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, SortedArray.new)
			error(exception)
		end
	end

	return setmetatable({
		Array = if baseArray then Sort(table.clone(baseArray), comparison) else {};
		Comparison = comparison;
	}, SortedArray) :: never
end

--[=[
	Checks if `value` is literally a `SortedArray`.

	```lua
	SortedArray.Is(SortedArray.new()) -- true
	SortedArray.Is(SortedArray) -- false
	SortedArray.Is({}) -- false
	SortedArray.Is(nil) -- false
	SortedArray.Is(1) -- false
	```

	@param value unknown
	@return boolean
]=]
function SortedArray.Is(value)
	return type(value) == "table" and getmetatable(value) == SortedArray
end
--[=[
	An alias for [SortedArray.Is].

	@function instanceof
	@within SortedArray

	@param value unknown
	@return boolean
]=]
SortedArray.instanceof = SortedArray.Is

-- function SortedArray:Map(callback, comparison)
-- 	if IsUltraStrictMode() then
-- 		local typeSuccess0, typeError0 = IsFunction:matches(callback)
-- 		if not typeSuccess0 then
-- 			local exception = Error.new(tostring(typeError0))
-- 			exception.name = "DebugTypeError"
-- 			Error.captureStackTrace(exception, SortedArray.Map)
-- 			error(exception)
-- 		end
-- 		local typeSuccess1, typeError1 = IsMaybeFunction:matches(comparison)
-- 		if not typeSuccess1 then
-- 			local exception = Error.new(tostring(typeError1))
-- 			exception.name = "DebugTypeError"
-- 			Error.captureStackTrace(exception, SortedArray.Map)
-- 			error(exception)
-- 		end
-- 	end

-- 	local array = self.Array
-- 	local newArray = table.create(#array)
-- 	for index, value in array do
-- 		newArray[index] = callback(value, index, array)
-- 	end
-- 	return SortedArray.new(newArray, comparison):Sort()
-- end
-- function SortedArray:MapFiltered(callback, comparison)
-- 	if IsUltraStrictMode() then
-- 		local typeSuccess0, typeError0 = IsFunction:matches(callback)
-- 		if not typeSuccess0 then
-- 			local exception = Error.new(tostring(typeError0))
-- 			exception.name = "DebugTypeError"
-- 			Error.captureStackTrace(exception, SortedArray.MapFiltered)
-- 			error(exception)
-- 		end
-- 		local typeSuccess1, typeError1 = IsMaybeFunction:matches(comparison)
-- 		if not typeSuccess1 then
-- 			local exception = Error.new(tostring(typeError1))
-- 			exception.name = "DebugTypeError"
-- 			Error.captureStackTrace(exception, SortedArray.MapFiltered)
-- 			error(exception)
-- 		end
-- 	end

-- 	local array = self.Array
-- 	local newArray = table.create(#array)
-- 	for index, value in array do
-- 		local newValue = callback(value, index, array)
-- 		if newValue ~= nil then
-- 			newArray[index] = newValue
-- 		end
-- 	end
-- 	return SortedArray.new(newArray, comparison):Sort()
-- end

local function Insert<T>(self: SortedArray<T>, value: T): number
	if IsUltraStrictMode() then
		local typeSuccess, typeError = IsUnknown:matches(value)
		if not typeSuccess then
			local exception = Error.new(tostring(typeError))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, SortedArray.Insert)
			error(exception)
		end
	end

	local array = self.Array
	local comparison = self.Comparison

	local position = FindClosest(array, value, 1, #array, nil, comparison)

	local insertAt = 1
	local value2 = array[position :: never]

	if value2 ~= nil then
		local boolean = if comparison then comparison(value, value2) else (value :: never) < value2
		insertAt = if boolean then position :: number else (position :: number) + 1
	end

	table.insert(array, insertAt, value)
	return insertAt
end

--[=[
	Inserts an element in the proper place which would preserve the array's
	sorted order. Returns the index the element was inserted.

	@error DebugTypeError -- Thrown when you are using debug mode and you pass an incorrect type.

	@method Insert
	@within SortedArray

	@param value T -- The value you want to insert.
	@return integer -- The index at which it was inserted.
]=]
SortedArray.Insert = Insert

local function Find<T>(
	self: SortedArray<T>,
	value: T,
	equalityCheck: ComparisonFunction<T>?,
	lessThan: ComparisonFunction<T>?,
	low: number?,
	high: number?
): number?
	if IsUltraStrictMode() then
		local typeSuccess0, typeError0 = IsUnknown:matches(value)
		if not typeSuccess0 then
			local exception = Error.new(tostring(typeError0))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, SortedArray.Find)
			error(exception)
		end
		local typeSuccess1, typeError1 = IsMaybeFunction:matches(equalityCheck)
		if not typeSuccess1 then
			local exception = Error.new(tostring(typeError1))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, SortedArray.Find)
			error(exception)
		end
		local typeSuccess2, typeError2 = IsMaybeFunction:matches(lessThan)
		if not typeSuccess2 then
			local exception = Error.new(tostring(typeError2))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, SortedArray.Find)
			error(exception)
		end
		local typeSuccess3, typeError3 = IsMaybeInteger:matches(low)
		if not typeSuccess3 then
			local exception = Error.new(tostring(typeError3))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, SortedArray.Find)
			error(exception)
		end
		local typeSuccess4, typeError4 = IsMaybeInteger:matches(high)
		if not typeSuccess4 then
			local exception = Error.new(tostring(typeError4))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, SortedArray.Find)
			error(exception)
		end
	end

	local array = self.Array
	local length = #array
	if length == 0 then
		return nil
	end

	if length == 1 then
		local atPosition = array[1]
		return if if equalityCheck then equalityCheck(value, atPosition) else value == atPosition then 1 else nil
	end

	local boolean = false
	local position = FindClosest(array, value, low or 1, high or length, equalityCheck, lessThan or self.Comparison)

	local atPosition = array[position :: number]
	if atPosition ~= nil then
		boolean = if equalityCheck then equalityCheck(value, atPosition) else value == atPosition
	end

	return if boolean then position else nil
end

--[=[
	Finds an element in a `SortedArray` and returns its position (or `nil` if
	non-existent).

	@method Find
	@within SortedArray

	@error DebugTypeError -- Thrown when you are using debug mode and you pass an incorrect type.

	@param value T -- The value you want to find.
	@param equalityCheck? ComparisonFunction<T> -- An optional function which checks for equality between the passed-in element and the other elements in the SortedArray.
	@param lessThan? ComparisonFunction<T> -- An optional less-than comparison function, which falls back on the comparison passed in from [SortedArray.new].
	@param low? integer -- An optional lower bound for the search. Defaults to `1`.
	@param high? integer -- An optional upper bound for the search. Defaults to the length of the SortedArray.
	@return integer? -- The index of the element, or `nil` if it doesn't exist.
]=]
SortedArray.Find = Find
--[=[
	An alias for [SortedArray.Find].

	@method IndexOf
	@within SortedArray

	@error DebugTypeError -- Thrown when you are using debug mode and you pass an incorrect type.

	@param value T -- The value you want to find.
	@param equalityCheck? ComparisonFunction<T> -- An optional function which checks for equality between the passed-in element and the other elements in the SortedArray.
	@param lessThan? ComparisonFunction<T> -- An optional less-than comparison function, which falls back on the comparison passed in from [SortedArray.new].
	@param low? integer -- An optional lower bound for the search. Defaults to `1`.
	@param high? integer -- An optional upper bound for the search. Defaults to the length of the SortedArray.
	@return integer? -- The index of the element, or `nil` if it doesn't exist.
]=]
SortedArray.IndexOf = Find

--[=[
	Makes a shallow copy of the SortedArray's array.
	@return {T} -- A shallow copy of the data.
]=]
function SortedArray:Copy()
	return table.clone(self.Array)
end

local function CopyDeep<T>(object: T): T
	local new = table.clone(object :: never)
	for index, value in next, object :: never do
		if type(value) == "table" then
			new[index] = CopyDeep(value)
		end
	end
	return new :: never
end

--[=[
	Makes a deep copy of the SortedArray's array.
	@return {T} -- A deep copy of the data.
]=]
function SortedArray:CopyDeep()
	return CopyDeep(self.Array)
end

--[=[
	Makes a shallow copy of the SortedArray and returns a new SortedArray.
	Probably would recommend [SortedArray.CloneDeep] instead because of
	funny cache misses.

	@return SortedArray<T> -- A shallow copy of the SortedArray.
]=]
function SortedArray:Clone()
	return SortedArray.new(table.clone(self.Array), self.Comparison):Sort()
end

--[=[
	Makes a deep copy of the SortedArray and returns a new SortedArray.
	@return SortedArray<T> -- A deep copy of the SortedArray.
]=]
function SortedArray:CloneDeep()
	return SortedArray.new(CopyDeep(self.Array), self.Comparison):Sort()
end

local function RemoveElement<T>(
	self: SortedArray<T>,
	signature: T,
	equalityCheck: ComparisonFunction<T>?,
	lessThan: ComparisonFunction<T>?
): T?
	if IsUltraStrictMode() then
		local typeSuccess0, typeError0 = IsUnknown:matches(signature)
		if not typeSuccess0 then
			local exception = Error.new(tostring(typeError0))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, SortedArray.RemoveElement)
			error(exception)
		end
		local typeSuccess1, typeError1 = IsMaybeFunction:matches(equalityCheck)
		if not typeSuccess1 then
			local exception = Error.new(tostring(typeError1))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, SortedArray.RemoveElement)
			error(exception)
		end
		local typeSuccess2, typeError2 = IsMaybeFunction:matches(lessThan)
		if not typeSuccess2 then
			local exception = Error.new(tostring(typeError2))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, SortedArray.RemoveElement)
			error(exception)
		end
	end

	local position = Find(self, signature, equalityCheck, lessThan)
	return if position == nil then nil else table.remove(self.Array, position)
end

--[=[
	Searches the array via [SortedArray.Find]. If found, it removes the value
	and returns the value, otherwise returns undefined. Only removes a single
	occurrence.

	@method RemoveElement
	@within SortedArray

	@error DebugTypeError -- Thrown when you are using debug mode and you pass an incorrect type.

	@param signature T -- The value you want to remove.
	@param equalityCheck? ComparisonFunction<T> -- An optional function which checks for equality between the passed-in element and the other elements in the SortedArray.
	@param lessThan? ComparisonFunction<T> -- An optional less-than comparison function, which falls back on the comparison passed in from [SortedArray.new].
	@return T? -- The removed value.
]=]
SortedArray.RemoveElement = RemoveElement

local function RemoveIndex<T>(self: SortedArray<T>, index: number): T?
	if IsUltraStrictMode() then
		local typeSuccess, typeError = IsInteger:matches(index)
		if not typeSuccess then
			local exception = Error.new(tostring(typeError))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, SortedArray.RemoveIndex)
			error(exception)
		end
	end
	return table.remove(self.Array, index)
end

--[=[
	Removes an element from the SortedArray by index. Returns the removed
	element. This is literally just `table.remove`.

	@method RemoveIndex
	@within SortedArray

	@error DebugTypeError -- Thrown when you are using debug mode and you pass an incorrect type.

	@param index integer -- The index of the element to remove.
	@return T? -- The removed element.
]=]
SortedArray.RemoveIndex = RemoveIndex

--[=[
	Pops an element from the SortedArray. This just removes the last element.
	@return T? -- The removed element.
]=]
function SortedArray:Pop()
	local array = self.Array
	local length = #array

	local value = array[length]
	array[length] = nil
	return value
end

--[=[
	Shifts element from the SortedArray. This just removes the first element.
	@return T? -- The removed element.
]=]
function SortedArray:Shift()
	return table.remove(self.Array, 1)
end

--[=[
	Does `table.sort` on the SortedArray's array. Pretty useless.
	@return SortedArray<T>
]=]
function SortedArray:Sort()
	table.sort(self.Array, self.Comparison)
	return self
end

--[=[
	Removes the value at `index` and re-inserts it. This is useful for when a
	value may have updated in a way that could change it's position in a
	`SortedArray`. Returns the new position of the element.

	@error DebugTypeError -- Thrown when you are using debug mode and you pass an incorrect type.
	@error ElementNotFoundError -- Thrown when the element you are trying to sort is not found.

	@param index integer -- The index of the element to sort.
	@return integer -- The new position of the element.
]=]
function SortedArray:SortIndex(index)
	if IsUltraStrictMode() then
		local typeSuccess, typeError = IsInteger:matches(index)
		if not typeSuccess then
			local exception = Error.new(tostring(typeError))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, SortedArray.SortIndex)
			error(exception)
		end
	end

	local removedValue = RemoveIndex(self, index)
	if removedValue == nil then
		local exception = Error.new(`Could not find element at index {index}?`)
		exception.name = "ElementNotFoundError"
		Error.captureStackTrace(exception, SortedArray.SortIndex)
		error(exception)
	end

	return Insert(self, removedValue)
end

--[=[
	Calls [SortedArray.RemoveElement]`(signature, equalityCheck, lessThan)` and
	re-inserts the value. This is useful for when a value may have updated in a
	way that could change it's position in a `SortedArray`. Returns the new
	position of the element.

	@error DebugTypeError -- Thrown when you are using debug mode and you pass an incorrect type.
	@error ElementNotFoundError -- Thrown when the element you are trying to sort is not found.

	@param signature T -- The value you want to remove.
	@param equalityCheck? ComparisonFunction<T> -- An optional function which checks for equality between the passed-in element and the other elements in the SortedArray.
	@param lessThan? ComparisonFunction<T> -- An optional less-than comparison function, which falls back on the comparison passed in from [SortedArray.new].
	@return integer -- The new position of the element.
]=]
function SortedArray:SortElement(signature, equalityCheck, lessThan)
	if IsUltraStrictMode() then
		local typeSuccess0, typeError0 = IsUnknown:matches(signature)
		if not typeSuccess0 then
			local exception = Error.new(tostring(typeError0))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, SortedArray.SortElement)
			error(exception)
		end
		local typeSuccess1, typeError1 = IsMaybeFunction:matches(equalityCheck)
		if not typeSuccess1 then
			local exception = Error.new(tostring(typeError1))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, SortedArray.SortElement)
			error(exception)
		end
		local typeSuccess2, typeError2 = IsMaybeFunction:matches(lessThan)
		if not typeSuccess2 then
			local exception = Error.new(tostring(typeError2))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, SortedArray.SortElement)
			error(exception)
		end
	end

	local removedValue = RemoveElement(self, signature, equalityCheck, lessThan)
	if removedValue == nil then
		local exception = Error.new(`Could not find element {signature}?`)
		exception.name = "ElementNotFoundError"
		Error.captureStackTrace(exception, SortedArray.SortElement)
		error(exception)
	end

	return Insert(self, removedValue)
end

--[=[
	Gets an intersection between two SortedArrays.

	@error DebugTypeError -- Thrown when you are using debug mode and you pass an incorrect type.

	@param other SortedArray<T> -- The other SortedArray to get the intersection with.
	@param equalityCheck? ComparisonFunction<T> -- An optional function which checks for equality between the passed-in element and the other elements in the SortedArray.
	@param lessThan? ComparisonFunction<T> -- An optional less-than comparison function, which falls back on the comparison passed in from [SortedArray.new].
	@return SortedArray<T> -- The intersection of the two SortedArrays. This is a new SortedArray.
]=]
function SortedArray:GetIntersection<T>(other, equalityCheck, lessThan)
	if IsUltraStrictMode() then
		local typeSuccess0, typeError0 = IsSortedArray:matches(other)
		if not typeSuccess0 then
			local exception = Error.new(tostring(typeError0))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, SortedArray.GetIntersection)
			error(exception)
		end
		local typeSuccess1, typeError1 = IsMaybeFunction:matches(equalityCheck)
		if not typeSuccess1 then
			local exception = Error.new(tostring(typeError1))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, SortedArray.GetIntersection)
			error(exception)
		end
		local typeSuccess2, typeError2 = IsMaybeFunction:matches(lessThan)
		if not typeSuccess2 then
			local exception = Error.new(tostring(typeError2))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, SortedArray.GetIntersection)
			error(exception)
		end
	end

	local commonalities: SortedArray<T> = SortedArray.new({}, self.Comparison)
	local count = 0
	local position = 0

	local selfArray = self.Array
	local selfLength = #selfArray

	local otherArray = other.Array
	local otherLength = #otherArray

	if selfLength > otherLength then
		selfLength, otherLength = otherLength, selfLength
		selfArray, otherArray = otherArray, selfArray
		self, other = other, self
	end

	for index = 1, otherLength do
		local current = selfArray[index]
		local currentPosition = other:Find(current, equalityCheck, lessThan, position, otherLength)
		if currentPosition then
			position = currentPosition
			count += 1
			commonalities.Array[count] = current
		end
	end

	return commonalities
end

--[=[
	Checks if the SortedArray is empty.
	@return boolean
]=]
function SortedArray:IsEmpty()
	return #self.Array == 0
end

function Private:__len()
	return #self.Array
end

function Private:__tostring()
	local array = self.Array
	local stringBuilder = table.create(#array)
	for index, value in array do
		stringBuilder[index] = TableUtilities.Repr(value)
	end
	return `SortedArray<[{table.concat(stringBuilder, ", ")}]>`
end

return table.freeze(SortedArray :: Static)


--!optimize 2
--!strict

-- types are wonky here, but it's cause i can assure
-- that i am right
local function FastCopyDeep<T>(object: T): T
	local newObject = table.clone(object :: never)
	for key, value in pairs(object :: never) do
		if type(value) == "table" then
			newObject[key] = FastCopyDeep(value)
		end
	end
	return newObject :: never
end

--[=[
	Does a deep copy of an object. If it is not a table, it just returns the object.

	```lua
	local dictionary = {hello = {world = "goodbye"}}

	local new = CopyDeep(dictionary) -- {hello = {world = "goodbye"}}

	print(new == dictionary) -- false
	print(new.hello == dictionary.hello) -- false
	print(new.hello.world == dictionary.hello.world) -- true
	```

	@function CopyDeep
	@within TableUtilities

	@param object T -- The object to copy.
	@return T -- The copied object.
]=]
local function CopyDeep<T>(object: T): T
	if type(object) ~= "table" then
		return object
	end

	local newObject = table.clone(object)
	for key, value in pairs(object) do
		if type(value) == "table" then
			newObject[key] = FastCopyDeep(value)
		end
	end
	return newObject :: never
end

return CopyDeep


--!optimize 2
--!strict

--[=[
	Compares two or more tables to see if they are equal.

	```lua
	local a = {hello = "world"}
	local b = {hello = "world"}

	local equal = EqualObjects(a, b) -- false
	```

	```lua
	local a, b = table.unpack(table.create(2, {}), 1, 2)
	local equal = EqualObjects(a, b) -- true
	```

	@function EqualObjects
	@within TableUtilities

	@param ... ...{[unknown]: unknown} -- The tables to compare.
	@return boolean -- Whether or not the tables are equal.
]=]
local function EqualObjects(...: {[unknown]: unknown}): boolean
	local firstItem = ...
	for index = 2, select("#", ...) do
		if firstItem ~= select(index, ...) then
			return false
		end
	end
	return true
end

return EqualObjects


--!optimize 2
--!strict

local EqualObjects = require("./EqualObjects")

--@native
local function Compare(a: unknown, b: unknown): boolean
	if type(a) ~= "table" or type(b) ~= "table" then
		return a == b
	end

	local castA = a :: {[unknown]: unknown}
	local castB = b :: {[unknown]: unknown}

	-- can't use generalized iteration because of __call
	for key, value in pairs(castA) do
		if castB[key] ~= value then
			return false
		end
	end

	for key, value in pairs(castB) do
		if castA[key] ~= value then
			return false
		end
	end

	return true
end

--[=[
	Compares two dictionaries for equality.

	```lua
	local dictionary = {hello = "world", goodbye = "world"}
	local other1 = {hello = "world", goodbye = "world"}
	local other2 = {hello = "hello", world = "goodbye"}

	local value = Equals(dictionary, other1) -- true
	local value = Equals(dictionary, other1, other2) -- false
	```

	@function Equals
	@within TableUtilities

	@param ... ...{[unknown]: unknown} -- The tables to compare.
	@return boolean -- Whether the tables are equal.
]=]
--@native
local function Equals(...: {[unknown]: unknown}): boolean
	if EqualObjects(...) then
		return true
	end

	local totalArguments = select("#", ...)
	local firstObject = ...

	for index = 2, totalArguments do
		local object = select(index, ...)
		if not Compare(firstObject, object) then
			return false
		end
	end

	return true
end

return Equals


--!optimize 2
--!strict

local EqualObjects = require("./EqualObjects")

--@native
local function CompareDeep(a: unknown, b: unknown): boolean
	if type(a) ~= "table" or type(b) ~= "table" then
		return a == b
	end

	local castA = a :: {[unknown]: unknown}
	local castB = b :: {[unknown]: unknown}

	-- can't use generalized iteration because of __call
	for key, value in pairs(castA) do
		if not CompareDeep(value, castB[key]) then
			return false
		end
	end

	for key, value in pairs(castB) do
		if not CompareDeep(value, castA[key]) then
			return false
		end
	end

	return true
end

--[=[
	Compares two tables for equality using deep comparison.

	```lua
	local dictionary = {hello = "world", goodbye = {world = "hello"}}
	local other1 = {hello = "world", goodbye = {world = "hello"}}
	local other2 = {hello = "hello", world = "goodbye"}

	local value = EqualsDeep(dictionary, other1) -- true
	local value = EqualsDeep(dictionary, other1, other2) -- false
	```

	@function EqualsDeep
	@within TableUtilities

	@param ... ...{[unknown]: unknown} -- The tables to compare.
	@return boolean -- Whether the tables are equal.
]=]
--@native
local function EqualsDeep(...: {[unknown]: unknown}): boolean
	if EqualObjects(...) then
		return true
	end

	local totalArguments = select("#", ...)
	local firstObject = ...

	for index = 2, totalArguments do
		local object = select(index, ...)
		if not CompareDeep(firstObject, object) then
			return false
		end
	end

	return true
end

return EqualsDeep


--!optimize 2
--!strict

--[=[
	A fast merge function that doesn't support a `None` value. Uses if branches
	to avoid iteration. Does NOT do any type validation, be aware of what you
	pass to it.

	@function FastMerge
	@within TableUtilities

	@param ... ...{[unknown]: unknown} -- The tables to merge.
	@return T -- The merged table.
]=]
local function FastMerge<T>(...: any): T
	local length = select("#", ...)
	if length == 0 then
		return {} :: never
	end

	if length == 1 then
		local dictionary0 = ...
		return (if dictionary0 then table.clone(dictionary0) else {}) :: never
	end

	if length == 2 then
		local dictionary0, dictionary1 = ...

		local result = (if dictionary0 then table.clone(dictionary0) else {}) :: never
		if dictionary1 then
			for key, value in pairs(dictionary1) do
				result[key] = value
			end
		end

		return result
	end

	if length == 3 then
		local dictionary0, dictionary1, dictionary2 = ...

		local result = (if dictionary0 then table.clone(dictionary0) else {}) :: never
		if dictionary1 then
			for key, value in pairs(dictionary1) do
				result[key] = value
			end
		end
		if dictionary2 then
			for key, value in pairs(dictionary2) do
				result[key] = value
			end
		end

		return result
	end

	if length == 4 then
		local dictionary0, dictionary1, dictionary2, dictionary3 = ...

		local result = (if dictionary0 then table.clone(dictionary0) else {}) :: never
		if dictionary1 then
			for key, value in pairs(dictionary1) do
				result[key] = value
			end
		end
		if dictionary2 then
			for key, value in pairs(dictionary2) do
				result[key] = value
			end
		end
		if dictionary3 then
			for key, value in pairs(dictionary3) do
				result[key] = value
			end
		end

		return result
	end

	local result = {}
	for dictionaryIndex = 1, length do
		local dictionary = select(dictionaryIndex, ...)
		if dictionary ~= nil then
			for key, value in pairs(dictionary) do
				result[key] = value
			end
		end
	end

	return result :: never
end

return FastMerge


--!optimize 2
--!strict

local CopyDeep = require("./CopyDeep")

--[=[
	Merges the given dictionaries into a single dictionary. This is recursive.
	The parameters may be any number of dictionaries or `nil`. Non-dictionaries
	will be ignored. Uses if branches to avoid iteration as much as possible.

	@function FastMergeDeep
	@within TableUtilities

	@since v1.2.0

	@param ... ...{[unknown]: unknown} -- The tables to merge.
	@return T -- The merged table.
]=]
local function FastMergeDeep<T>(...: any): T
	local length = select("#", ...)
	if length == 0 then
		return {} :: never
	end

	if length == 1 then
		local dictionary0 = ...
		return (if dictionary0 then CopyDeep(dictionary0) else {}) :: never
	end

	if length == 2 then
		local dictionary0, dictionary1 = ...

		local result = (if dictionary0 then CopyDeep(dictionary0) else {}) :: never
		if dictionary1 and type(dictionary1) == "table" then
			for key, value in pairs(dictionary1) do
				if type(value) == "table" then
					if result[key] == nil or type(result[key]) ~= "table" then
						result[key] = CopyDeep(value)
					else
						result[key] = FastMergeDeep(result[key], value)
					end
				else
					result[key] = value
				end
			end
		end

		return result
	end

	if length == 3 then
		local dictionary0, dictionary1, dictionary2 = ...

		local result = (if dictionary0 then CopyDeep(dictionary0) else {}) :: never
		if dictionary1 and type(dictionary1) == "table" then
			for key, value in pairs(dictionary1) do
				if type(value) == "table" then
					if result[key] == nil or type(result[key]) ~= "table" then
						result[key] = CopyDeep(value)
					else
						result[key] = FastMergeDeep(result[key], value)
					end
				else
					result[key] = value
				end
			end
		end
		if dictionary2 and type(dictionary2) == "table" then
			for key, value in pairs(dictionary2) do
				if type(value) == "table" then
					if result[key] == nil or type(result[key]) ~= "table" then
						result[key] = CopyDeep(value)
					else
						result[key] = FastMergeDeep(result[key], value)
					end
				else
					result[key] = value
				end
			end
		end

		return result
	end

	local result = {}
	for index = 1, length do
		local dictionary = select(index, ...)
		if not dictionary or type(dictionary) ~= "table" then
			continue
		end

		for key, value in pairs(dictionary) do
			if type(value) == "table" then
				if result[key] == nil or type(result[key]) ~= "table" then
					result[key] = CopyDeep(value)
				else
					result[key] = FastMergeDeep(result[key], value)
				end
			else
				result[key] = value
			end
		end
	end

	return result :: never
end

return FastMergeDeep


--!optimize 2
--!strict

--[=[
	Gets the length of any table. This has a really bad time complexity of O(n)
	so I would just not use this unless you absolutely have to. And if you
	absolutely do have to, I'd just suggest inlining.

	This uses `pairs` to avoid weird `__call` issues with generalized
	iteration.

	```lua
	print(GetLength({hello = "world", goodbye = "world"})) -- 2
	print(GetLength({1, 2, 3})) -- 3
	print(GetLength({bad = "practice", true})) -- 2
	```

	@function GetLength
	@within TableUtilities

	@param object {[unknown]: unknown} -- The table to get the length of.
	@return number -- The length of the table.
]=]
local function GetLength(object: {[unknown]: unknown}): number
	local length = 0
	for _ in pairs(object) do
		length += 1
	end
	return length
end

return GetLength


--!optimize 2
--!strict

--[=[
	Checks if the given value as an array.

	@function IsArray
	@within TableUtilities

	@param value unknown -- The value to check.
	@return boolean -- Whether the value is an array.
]=]
local function IsArray(value: any): boolean
	if type(value) ~= "table" then
		return false
	end

	if next(value) == nil then
		return true
	end

	if #value == 0 then
		return false
	end

	local count = 0
	-- can't use generalized iteration because of __call
	local sum = 0
	for key in pairs(value) do
		if type(key) ~= "number" or key % 1 ~= 0 or key ~= key or key < 1 then
			return false
		end

		count += 1
		sum += key
	end

	return sum == count * (count + 1) / 2
end

return IsArray


--!optimize 2
--!strict

local function FastLockDeep<T>(object: T): T
	local newObject = table.clone(object :: never)
	for key, value in pairs(object :: never) do
		if type(value) == "table" then
			newObject[key] = FastLockDeep(value)
		end
	end
	return table.freeze(newObject) :: never
end

--[=[
	Does a deep copy and lock of an object. If it is not a table, it
	just returns the object.

	```lua
	local dictionary = {hello = {world = "goodbye"}}

	local new = LockDeep(dictionary) -- {hello = {world = "goodbye"}}
	new.hello.world = "yeah" -- this will error
	```

	@function LockDeep
	@within TableUtilities

	@param object T -- The object to copy and lock.
	@return T -- The copied object.
]=]
local function LockDeep<T>(object: T): T
	if type(object) ~= "table" then
		return object
	end

	local newObject = table.clone(object)
	for key, value in pairs(object) do
		if type(value) == "table" then
			newObject[key] = FastLockDeep(value)
		end
	end
	return table.freeze(newObject) :: never
end

return LockDeep


--!optimize 2
--!strict

local DateTime = require("@lune/datetime")
local FastMergeDeep = require("./FastMergeDeep")
local IsArray = require("./IsArray")
local roblox = require("@lune/roblox")

type DateTime = DateTime.DateTime
export type Locale = DateTime.Locale

--[=[
	Indicates that [DateTime]s should be formatted using the
	[DateTime.fromLocalTime] format.

	@interface LocalTimeFormat
	@field Type "LocalTime"
	@field Format string
	@field Locale? string
	@within TableUtilities
]=]
--[=[
	Indicates that [DateTime]s should be formatted using the
	[DateTime.fromUniversalTime] format.

	@interface UniversalTimeFormat
	@field Type "UniversalTime"
	@field Format string
	@field Locale? string
	@within TableUtilities
]=]
--[=[
	Indicates how we should format [DateTime]s.

	@type DateTimeFormatType "UnixTimestamp" | "UnixTimestampMillis" | "IsoDate" | "LocalTimeTable" | "UniversalTimeTable" | LocalTimeFormat | UniversalTimeFormat
	@within TableUtilities
]=]
export type DateTimeFormatType =
	| "UnixTimestamp"
	| "UnixTimestampMillis"
	| "IsoDate"
	| "LocalTimeTable"
	| "UniversalTimeTable"
	| {
		Type: "LocalTime",
		Format: string,
		Locale: Locale?,
	}
	| {
		Type: "UniversalTime",
		Format: string,
		Locale: Locale?,
	}

--[=[
	Configures how the Repr function formats.

	@interface ReprSettings
	@field DateTimeFormatType? DateTimeFormatType -- How to format [DateTime]s.
	@field Pretty? boolean -- Whether to use pretty formatting.
	@field RobloxClassName? boolean -- Whether to include Roblox class names.
	@field RobloxDirectoryToString? boolean -- Whether or not to use the DirectoryToString function, which is a fixed version of [Instance.GetFullName].
	@field RobloxFullName? boolean -- Whether to include Roblox full names.
	@field RobloxProperFullName? boolean -- Whether to include Roblox proper full names.
	@field Semicolons? boolean -- Whether to use semicolons.
	@field SortKeys? boolean -- Whether to sort keys.
	@field Spaces? number -- How many spaces to use.
	@field Tabs? boolean -- Whether to use tabs over spaces.
	@within TableUtilities
]=]
type RequiredReprSettings = {
	DateTimeFormatType: DateTimeFormatType,
	Pretty: boolean,
	RobloxClassName: boolean,
	RobloxDirectoryToString: boolean,
	RobloxFullName: boolean,
	RobloxProperFullName: boolean,
	Semicolons: boolean,
	SortKeys: boolean,
	Spaces: number,
	Tabs: boolean,
	[any]: never,
}
export type ReprSettings = {
	DateTimeFormatType: DateTimeFormatType?,
	Pretty: boolean?,
	RobloxClassName: boolean?,
	RobloxDirectoryToString: boolean?,
	RobloxFullName: boolean?,
	RobloxProperFullName: boolean?,
	Semicolons: boolean?,
	SortKeys: boolean?,
	Spaces: number?,
	Tabs: boolean?,
	[any]: never,
}

local DEFAULT_SETTINGS: RequiredReprSettings = {
	DateTimeFormatType = "UnixTimestamp";
	Pretty = true;
	RobloxClassName = false;
	RobloxDirectoryToString = true;
	RobloxFullName = true;
	RobloxProperFullName = false;
	Semicolons = true;
	SortKeys = true;
	Spaces = 4;
	Tabs = true;
}

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

local ToLocalTime = DateTime.now().toLocalTime
local ToUniversalTime = DateTime.now().toUniversalTime
local ToIsoDate = DateTime.now().toIsoDate

local Depth = 0
local Shown: {[any]: any}
local CurrentIndent: string

type TimeTable = DateTime.DateTimeValueReturns

local function GetFloat(number: number): string
	local asString = tostring(number)
	if tonumber(asString) == number then
		return asString
	end

	for index = 15, 99 do
		asString = string.format("%." .. index .. "f", number)
		if tonumber(asString) == number then
			return asString
		end
	end

	error(`Couldn't convert {number} to a float?`, 2)
end

local function IntegerOrFloat(number: number | string)
	local trueNumber = tonumber(number) or 0
	return if trueNumber ~= trueNumber
		then "0 / 0"
		elseif trueNumber % 1 == 0 then string.format("%d", trueNumber)
		else GetFloat(trueNumber)
end

local Repr: (value: any, reprSettings: ReprSettings?) -> string
local FORMATTERS: {[string]: string | (value: any, reprOptions: RequiredReprSettings, tabs: string) -> string} = {
	string = function(newValue: string)
		return string.format("%q", newValue)
	end;
	number = function(newValue: number)
		if newValue ~= newValue then
			return "0/0"
		end

		if newValue == math.huge then
			return "math.huge"
		end

		if newValue == -math.huge then
			return "-math.huge"
		end

		return IntegerOrFloat(newValue)
	end;
	boolean = tostring;
	["nil"] = "nil";
	table = function(newValue: {[any]: any}, reprOptions, tabs)
		if Shown[newValue] then
			return "{CYCLIC}"
		end

		if type(rawget(newValue, "__tostring")) == "function" then
			return tostring(newValue)
		end

		local metatable = getmetatable(newValue :: never)
		if type(metatable) == "table" and type(rawget(metatable, "__tostring")) then
			return tostring(newValue)
		end

		Shown[newValue] = true
		local isArray = IsArray(newValue)

		local pretty = reprOptions.Pretty
		local semicolons = reprOptions.Semicolons

		if isArray then
			local length = #newValue
			if length == 0 then
				Shown[newValue] = false
				return "{}"
			end

			local reprString = {"{"}
			local count = 1

			local reprValues = table.create(length)
			local totalStringLength = 0
			local isMultiline = false

			for index, arrayValue in ipairs(newValue) do
				Depth += 1
				local reprValue = Repr(arrayValue, reprOptions)
				Depth -= 1

				reprValues[index] = reprValue
				if not isMultiline then
					totalStringLength += #reprValue
					if totalStringLength >= 120 then
						isMultiline = true
					end
				end
			end

			local separator = if isMultiline and semicolons then ";" else ","
			local first = true
			if isMultiline and pretty then
				count += 1
				reprString[count] = "\n" .. CurrentIndent .. tabs
			end

			for index, reprValue in reprValues do
				if not first then
					count += 1
					reprString[count] = if isMultiline and pretty then "\n" .. CurrentIndent .. tabs else " "
				end

				count += 1
				reprString[count] = reprValue

				if (isMultiline and pretty) or index < length then
					count += 1
					reprString[count] = separator
				end
				first = false
			end

			Shown[newValue] = false
			if isMultiline and pretty then
				reprString[count + 1] = "\n"
				reprString[count + 2] = tabs
			end
			return table.concat(reprString) .. "}"
		end

		local reprString = {"{", if pretty then "\n" .. CurrentIndent .. tabs else ""}
		local count = 2

		local keyOrder = {}
		local length = 0
		local keyValueStrings = {}
		for key, dictionaryValue in next, newValue do
			Depth += 1
			local keyString = if IsLuaIdentifier(key) then key else "[" .. Repr(key, reprOptions) .. "]"
			local valueString = Repr(dictionaryValue, reprOptions)

			length += 1
			keyOrder[length] = keyString

			keyValueStrings[keyString] = valueString
			Depth -= 1
		end

		if reprOptions.SortKeys then
			table.sort(keyOrder)
		end

		local first = true
		local separator = if semicolons then ";" else ","
		for index, keyString in keyOrder do
			if not first then
				count += 1
				reprString[count] = if pretty then "\n" .. CurrentIndent .. tabs else " "
			end

			count += 1
			reprString[count] = string.format("%* = %*", keyString, keyValueStrings[keyString])

			if pretty or index < length then
				count += 1
				reprString[count] = separator
			end

			first = false
		end

		Shown[newValue] = false
		if pretty then
			reprString[count + 1] = "\n"
			reprString[count + 2] = tabs
		end

		return table.concat(reprString) .. "}"
	end;
	userdata = function(newValue: any): string
		local metatable = getmetatable(newValue)
		return if type(metatable) == "table" and type(rawget(metatable, "__tostring")) == "function"
			then tostring(newValue)
			else "<userdata>"
	end;
	buffer = function(newValue: buffer): string
		return "<buffer:" .. buffer.tostring(newValue) .. ">"
	end;
	vector = function(newValue: vector): string
		return string.format(
			"vector(%*, %*, %*)",
			IntegerOrFloat(newValue.x),
			IntegerOrFloat(newValue.y),
			IntegerOrFloat(newValue.z)
		)
	end;

	DateTime = function(newValue: DateTime, reprOptions: RequiredReprSettings): string
		local dateTimeFormatType = reprOptions.DateTimeFormatType

		if type(dateTimeFormatType) == "table" then
			local formatType = dateTimeFormatType.Type
			local format = dateTimeFormatType.Format
			local locale: DateTime.Locale = dateTimeFormatType.Locale or "en"

			return string.format(
				"<DateTime:%*:%*>",
				formatType,
				if formatType == "LocalTime"
					then newValue:formatLocalTime(format, locale)
					else newValue:formatUniversalTime(format, locale)
			)
		end

		if dateTimeFormatType == "IsoDate" then
			return string.format("DateTime.fromIsoDate(%q)", ToIsoDate(newValue))
		end

		if dateTimeFormatType == "UnixTimestamp" then
			return string.format("DateTime.fromUnixTimestamp(%*)", newValue.unixTimestamp)
		end
		if dateTimeFormatType == "UnixTimestampMillis" then
			return string.format("DateTime.fromUnixTimestampMillis(%*)", newValue.unixTimestampMillis)
		end

		if dateTimeFormatType == "LocalTimeTable" then
			local timeTable: TimeTable = ToLocalTime(newValue) :: never
			return string.format(
				"DateTime.fromLocalTime(%*, %*, %*, %*, %*, %*, %*)",
				timeTable.year,
				timeTable.month,
				timeTable.day,
				timeTable.hour,
				timeTable.minute,
				timeTable.second,
				timeTable.millisecond
			)
		end

		local timeTable: TimeTable = ToUniversalTime(newValue) :: never
		return string.format(
			"DateTime.fromUniversalTime(%*, %*, %*, %*, %*, %*, %*)",
			timeTable.year,
			timeTable.month,
			timeTable.day,
			timeTable.hour,
			timeTable.minute,
			timeTable.second,
			timeTable.millisecond
		)
	end;

	["function"] = "<function>";
	thread = "<thread>";

	DatabaseEnum = function(newValue: roblox.DatabaseEnum, reprOptions: RequiredReprSettings): string
		return Repr({
			Item = newValue.Items;
			Name = newValue.Name;
		}, reprOptions)
	end;
}

--[=[
	Basically the same as the `repr` function from Python. Formats things in a
	a pretty way.

	@function Repr
	@within TableUtilities

	@param value unknown -- The value to format.
	@param reprSettings? ReprSettings -- The settings to use.
	@return string -- The formatted value.
]=]
function Repr(value: unknown, reprSettings: ReprSettings?): string
	local reprOptions: RequiredReprSettings = FastMergeDeep(DEFAULT_SETTINGS, reprSettings)

	if reprOptions.Tabs then
		CurrentIndent = "\t"
	else
		CurrentIndent = string.rep(" ", reprOptions.Spaces)
	end

	local tabs = string.rep(CurrentIndent, Depth)
	if Depth == 0 then
		Shown = {}
	end

	local typeOf = typeof(value)
	local stringifyOrString = FORMATTERS[typeOf]
	if stringifyOrString then
		return if type(stringifyOrString) == "function"
			then stringifyOrString(value, reprOptions, tabs)
			else stringifyOrString
	end

	local typeString = type(value)
	return if typeString == typeOf then "<" .. typeString .. ">" else "<Lune:" .. typeOf .. ">"
end

return Repr


--!optimize 2
--!strict

export type Comparison<T> = (a: T, b: T) -> boolean

--[=[
	Used to find where to insert an element into a sorted array. You will
	need to provide a compare function to use and possible an equality check.
	If you are using numbers or strings, you can get away with
	[TableUtilities.FindInsertIndexSimple] instead.

	:::danger Warning
	Your array *MUST* be already sorted because this uses a binary search. This
	function will not sort for you.
	:::

	```lua
	local array: {{Value: number}} = {}
	local function LessThan(a: {Value: number}, b: {Value: number})
		return a.Value < b.Value
	end
	local function EqualsCheck(a: {Value: number}, b: {Value: number})
		return a.Value == b.Value
	end

	local function Insert(value: {Value: number})
		local position = FindInsertIndex(array, value, LessThan, EqualsCheck)
		local value2 = array[position :: never]

		if value2 == nil then
			position = 1
		else
			position = if LessThan(value, value2) then position else (position :: number) + 1
		end

		print(string.format("Inserting %* at index %*", value.Value, position))
		table.insert(array, position :: number, value)
	end

	Insert({Value = 3}) -- Inserting 3 at index 1
	Insert({Value = 1}) -- Inserting 1 at index 1
	Insert({Value = 2}) -- Inserting 2 at index 2
	Insert({Value = 4}) -- Inserting 4 at index 4
	```

	@function FindInsertIndex
	@within TableUtilities

	@param array {T} -- The array to insert into.
	@param value T -- The value to insert.
	@param lessThanCheck Comparison<T> -- The function to use to compare values.
	@param equalCheck? Comparison<T> -- The function to use to compare equality for values.
	@return number?
]=]
local function FindInsertIndex<T>(array: {T}, value: T, lessThanCheck: Comparison<T>, equalCheck: Comparison<T>?): number?
	local low = 1
	local high = #array

	local middle = (low + high) // 2
	if middle == 0 then
		return nil
	end

	local arrayValue = array[middle]
	while middle ~= high do
		if equalCheck then
			if equalCheck(value, arrayValue) then
				return middle
			end
		elseif value == arrayValue then
			return middle
		end

		if lessThanCheck(value, arrayValue) then
			high = middle - 1
		else
			low = middle + 1
		end

		middle = (low + high) // 2
		arrayValue = array[middle]
	end

	return middle
end

return FindInsertIndex


--!optimize 2
--!strict

--[=[
	Used to find where to insert an element into a sorted array. This requires
	the elements to be either a number or a string.

	:::danger Warning
	Your array *MUST* be already sorted because this uses a binary search. This
	function will not sort for you.
	:::

	```lua
	local array: {number} = {}
	local function Insert(value: number)
		local position = FindInsertIndexSimple(array, value)
		local value2 = array[position :: never]

		if value2 == nil then
			position = 1
		else
			position = if value < value2 then position else (position :: number) + 1
		end

		print(string.format("Inserting %* at index %*", value, position))
		table.insert(array, position :: number, value)
	end

	Insert(3) -- Inserting 3 at index 1
	Insert(1) -- Inserting 1 at index 1
	Insert(2) -- Inserting 2 at index 2
	Insert(4) -- Inserting 4 at index 4
	```

	@function FindInsertIndexSimple
	@within TableUtilities

	@param array {number | string} -- The array to insert into.
	@param value number | string -- The value to insert.
	@return number?
]=]
local function FindInsertIndexSimple(array: {any}, value: any): number?
	local low = 1
	local high = #array

	local middle = (low + high) // 2
	if middle == 0 then
		return nil
	end

	local arrayValue = array[middle]
	while middle ~= high do
		if value == arrayValue then
			return middle
		end

		if value < arrayValue then
			high = middle - 1
		else
			low = middle + 1
		end

		middle = (low + high) // 2
		arrayValue = array[middle]
	end

	return middle
end

return FindInsertIndexSimple :: ((array: {number}, value: number) -> number?) & ((array: {string}, value: string) -> number?)


--!optimize 2
--!strict

local FindInsertIndex = require("./FindInsertIndex")
type Comparison<T> = FindInsertIndex.Comparison<T>

--[=[
	Inserts a value into a sorted array using a binary search. This requires
	the elements to be either a number or a string.

	Inserts a value into a sorted array using a binary search. You will
	need to provide a compare function to use and possible an equality check.
	If you are using numbers or strings, you can get away with
	[TableUtilities.SortedInsertSimple] instead.

	:::danger Warning
	Your array *MUST* be already sorted because this uses a binary search. This
	function will not sort for you.
	:::

	```lua
	local array: {{Value: number}} = {}
	local function LessThan(a: {Value: number}, b: {Value: number})
		return a.Value < b.Value
	end
	local function EqualsCheck(a: {Value: number}, b: {Value: number})
		return a.Value == b.Value
	end

	SortedInsert(array, {Value = 3}, LessThan, EqualsCheck)
	SortedInsert(array, {Value = 1}, LessThan, EqualsCheck)
	SortedInsert(array, {Value = 2}, LessThan, EqualsCheck)
	SortedInsert(array, {Value = 4}, LessThan, EqualsCheck)
	print(array)
	```

	@function SortedInsert
	@within TableUtilities

	@param array {T} -- The array to insert into.
	@param value T -- The value to insert.
	@param lessThanCheck Comparison<T> -- The function to use to compare values.
	@param equalCheck? Comparison<T> -- The function to use to compare equality for values.
	@return number -- The index where the value was inserted.
]=]
local function SortedInsert<T>(array: {T}, value: T, lessThanCheck: Comparison<T>, equalCheck: Comparison<T>?): number
	local position = FindInsertIndex(array, value, lessThanCheck, equalCheck)
	local arrayValue = array[position :: never]

	local insertIndex = 1
	if arrayValue ~= nil then
		insertIndex = if lessThanCheck(value, arrayValue) then position :: number else (position :: number) + 1
	end

	table.insert(array, insertIndex, value)
	return insertIndex
end

return SortedInsert


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

local FindInsertIndexSimple = require("./FindInsertIndexSimple")

--[=[
	Inserts a value into a sorted array using a binary search. This requires
	the elements to be either a number or a string.

	:::danger Warning
	Your array *MUST* be already sorted because this uses a binary search. This
	function will not sort for you.
	:::

	```lua
	local array: {number} = {}
	SortedInsertSimple(array, 3)
	SortedInsertSimple(array, 1)
	SortedInsertSimple(array, 2)
	SortedInsertSimple(array, 4)
	print(array)
	```

	@function SortedInsertSimple
	@within TableUtilities

	@param array {number | string} -- The array to insert into.
	@param value number | string -- The value to insert.
	@return number -- The index where the value was inserted.
]=]
local function SortedInsertSimple(array: {any}, value: any): number
	local position = FindInsertIndexSimple(array, value)
	local arrayValue = array[position :: never]

	local insertIndex = 1
	if arrayValue ~= nil then
		insertIndex = if value < arrayValue then position :: number else (position :: number) + 1
	end

	table.insert(array, insertIndex, value)
	return insertIndex
end

return SortedInsertSimple :: ((array: {number}, value: number) -> number) & ((array: {string}, value: string) -> number)


--!optimize 2
--!strict

local CopyDeep = require("@self/CopyDeep")
local EqualObjects = require("@self/EqualObjects")
local Equals = require("@self/Equals")
local EqualsDeep = require("@self/EqualsDeep")
local FastMerge = require("@self/FastMerge")
local FastMergeDeep = require("@self/FastMergeDeep")
local GetLength = require("@self/GetLength")
local IsArray = require("@self/IsArray")
local LockDeep = require("@self/LockDeep")
local Repr = require("@self/Repr")

local FindInsertIndex = require("@self/SortedArrays/FindInsertIndex")
local FindInsertIndexSimple = require("@self/SortedArrays/FindInsertIndexSimple")
local SortedInsert = require("@self/SortedArrays/SortedInsert")
local SortedInsertSimple = require("@self/SortedArrays/SortedInsertSimple")

-- TODO: When we get @native in selene (and roblox? don't know if it's on without fflag mods) we should transform some of these to @native

--[=[
	A library that contains various utilities for tables.

	@class TableUtilities
]=]
local TableUtilities = table.freeze({
	CopyDeep = CopyDeep;
	EqualObjects = EqualObjects;
	Equals = Equals;
	EqualsDeep = EqualsDeep;
	FastMerge = FastMerge;
	FastMergeDeep = FastMergeDeep;
	GetLength = GetLength;
	IsArray = IsArray;
	LockDeep = LockDeep;
	Repr = Repr;

	FindInsertIndex = FindInsertIndex;
	FindInsertIndexSimple = FindInsertIndexSimple;
	SortedInsert = SortedInsert;
	SortedInsertSimple = SortedInsertSimple;
})

export type DateTimeFormatType = Repr.DateTimeFormatType
export type Locale = Repr.Locale
export type ReprSettings = Repr.ReprSettings

return TableUtilities


--!optimize 2
--!strict

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

local FreeThreads: {thread} = table.create(500)
local function RunFunction<Arguments...>(callback: (Arguments...) -> (), thread: thread, ...: Arguments...)
	callback(...)
	table.insert(FreeThreads, thread)
end

local function Yield()
	while true do
		RunFunction(coroutine.yield())
	end
end

--[=[
	Defers a function call in a much more performant manner than `task.defer`.

	@function FastDefer
	@within ThreadUtilities

	@param callback (Arguments...) -> () -- The function to spawn.
	@param ... Arguments... -- The arguments to the function.
	@return thread
]=]
local function FastDefer<Arguments...>(callback: (Arguments...) -> (), ...: Arguments...)
	local thread: thread
	local freeAmount = #FreeThreads

	if freeAmount > 0 then
		thread = FreeThreads[freeAmount]
		FreeThreads[freeAmount] = nil
	else
		thread = coroutine.create(Yield)
		coroutine.resume(thread)
	end

	-- TODO: This might be able to throw?
	return task.defer(thread, callback, thread, ...)
end

return FastDefer


--!optimize 2
--!strict

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

local FreeThreads: {thread} = table.create(500)
local function RunFunction<Arguments...>(callback: (Arguments...) -> (), thread: thread, ...: Arguments...)
	callback(...)
	table.insert(FreeThreads, thread)
end

local function Yield()
	while true do
		RunFunction(coroutine.yield())
	end
end

--[=[
	Defers a function call in a much more performant manner than `task.defer`.
	This version is safer than [ThreadUtilities.FastDefer] as I think the use
	of varargs can cause some issues.

	@function FastDeferSafe
	@within ThreadUtilities
	@since v1.1.0

	@param callback (Arguments...) -> () -- The function to spawn.
	@param ... Arguments... -- The arguments to the function.
	@return thread
]=]
local function FastDeferSafe<Arguments...>(callback: (Arguments...) -> (), ...: Arguments...)
	local thread: thread
	local freeAmount = #FreeThreads

	if freeAmount > 0 then
		thread = FreeThreads[freeAmount]
		FreeThreads[freeAmount] = nil
	else
		thread = coroutine.create(Yield)
		coroutine.resume(thread)
	end

	local length = select("#", ...)
	if length == 0 then
		return task.defer(thread, callback, thread)
	end

	local arguments = {select(1, ...)}
	return task.defer(thread, function()
		callback(table.unpack(arguments, 1, length))
	end, thread)
end

return FastDeferSafe


--!optimize 2
--!strict

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

local FreeThreads: {thread} = table.create(500)
local function RunFunction<Arguments...>(callback: (Arguments...) -> (), thread: thread, ...: Arguments...)
	callback(...)
	table.insert(FreeThreads, thread)
end

local function Yield()
	while true do
		RunFunction(coroutine.yield())
	end
end

--[=[
	Delays a function call in a much more performant manner than `task.delay`.

	@function FastDelay
	@within ThreadUtilities

	@param delay number -- The delay before the function is executed.
	@param callback (Arguments...) -> () -- The function to spawn.
	@param ... Arguments... -- The arguments to the function.
	@return thread
]=]
local function FastDelay<Arguments...>(delay: number, callback: (Arguments...) -> (), ...: Arguments...)
	local thread: thread
	local freeAmount = #FreeThreads

	if freeAmount > 0 then
		thread = FreeThreads[freeAmount]
		FreeThreads[freeAmount] = nil
	else
		thread = coroutine.create(Yield)
		coroutine.resume(thread)
	end

	local length = select("#", ...)
	if length == 0 then
		return task.delay(delay, thread, callback, thread)
	end

	local arguments = {select(1, ...)}
	return task.delay(delay, thread, function()
		callback(table.unpack(arguments, 1, length))
	end, thread)
end

return FastDelay


--!optimize 2
--!strict

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

local FreeThreads: {thread} = table.create(500)
local function RunFunction<Arguments...>(callback: (Arguments...) -> (), thread: thread, ...: Arguments...)
	callback(...)
	table.insert(FreeThreads, thread)
end

local function Yield()
	while true do
		RunFunction(coroutine.yield())
	end
end

--[=[
	Spawns a function call in a much more performant manner than `task.spawn`
	or the `coroutine` library can provide.

	@function FastSpawn
	@within ThreadUtilities

	@param callback (Arguments...) -> () -- The function to spawn.
	@param ... Arguments... -- The arguments to the function.
	@return thread
]=]
local function FastSpawn<Arguments...>(callback: (Arguments...) -> (), ...: Arguments...)
	local thread: thread
	local freeAmount = #FreeThreads

	if freeAmount > 0 then
		thread = FreeThreads[freeAmount]
		FreeThreads[freeAmount] = nil
	else
		thread = coroutine.create(Yield)
		coroutine.resume(thread)
	end

	return task.spawn(thread, callback, thread, ...)
end

return FastSpawn


--!optimize 2
--!strict

--@native
local function ThreadHandler(thread: thread, success: boolean, ...)
	if not success then
		local exception = (...)
		if type(exception) == "string" then
			error(debug.traceback(thread, exception), 2)
		else
			error(tostring(exception), 2)
		end
	end

	if coroutine.status(thread) ~= "dead" then
		error(debug.traceback(thread, "Attempted to yield inside NoYield!"), 2)
	end

	return ...
end

--[=[
	Executes a function and throws an error if yields at all.

	```lua
	print(ThreadUtilities.NoYield(function()
		return "This", "doesn't", "yield"
	end)) -- This doesn't yield

	print(ThreadUtilities.NoYield(function()
		task.wait()
		return "Errors!!!"
	end)) -- Throws an error!
	```

	```ts
	print(NoYield(() => $tuple("This", "doesn't", "yield"))) // This doesn't yield;
	print(
		NoYield(() => {
			task.wait();
			return "Errors!!!";
		}),
	); // Throws an error!
	```

	@function NoYield
	@within ThreadUtilities

	@param callback (Arguments...) -> Returns... -- The function to execute.
	@param ... Arguments... -- The arguments to the function.
	@return Returns...
]=]
local function NoYield<Arguments..., Returns...>(callback: (Arguments...) -> Returns..., ...: Arguments...): Returns...
	local thread = coroutine.create(callback)
	return ThreadHandler(thread, coroutine.resume(thread, ...))
end

return NoYield


--!optimize 2
--!strict

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

--[=[
	Cancelling threads can sometimes cause weird issues that throw errors. This
	function is a workaround for that to make sure your threads are always
	cancelled in a safe manner.

	@function SafeThreadCancel
	@within ThreadUtilities

	@param thread thread -- The thread to cancel.
]=]
local function SafeThreadCancel(thread: thread)
	local cancelled: boolean?
	if coroutine.running() ~= thread then
		cancelled = pcall(function()
			return task.cancel(thread)
		end)
	end

	if not cancelled then
		local toCancel = thread
		thread = nil :: never

		-- Could probably convert this to use FastDefer?
		task.defer(function()
			task.cancel(toCancel)
		end)
	end
end

return SafeThreadCancel


--!optimize 2
--!strict

local FastDefer = require("@self/FastDefer")
local FastDeferSafe = require("@self/FastDeferSafe")
local FastDelay = require("@self/FastDelay")
local FastSpawn = require("@self/FastSpawn")
local NoYield = require("@self/NoYield")
local SafeThreadCancel = require("@self/SafeThreadCancel")

--[=[
	Provides utilities for working with threads.

	@class ThreadUtilities
]=]
local ThreadUtilities = table.freeze({
	FastDefer = FastDefer;
	FastDeferSafe = FastDeferSafe;
	FastDelay = FastDelay;
	FastSpawn = FastSpawn;
	NoYield = NoYield;
	SafeThreadCancel = SafeThreadCancel;
})

return ThreadUtilities


--!optimize 2
--!strict

local STRING_CASES: {[string]: string} = {
	["[Enum] Requested metatable is locked"] = "Enum";
}

local function TypeOf(value: any): string
	local typeOf = typeof(value)
	if typeOf == "userdata" or typeOf == "table" then
		local metatable = getmetatable(value)
		if metatable then
			if type(metatable) == "table" then
				local internalType = metatable.__type or metatable.ClassName
				if internalType then
					return internalType
				end
			elseif type(metatable) == "string" then
				return STRING_CASES[metatable] or metatable
			end
		end
	end

	return typeOf
end

return TypeOf


--!optimize 2
--!strict

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

export type PrimitiveTypes =
	| "nil"
	| "boolean"
	| "string"
	| "number"
	| "table"
	| "userdata"
	| "function"
	| "thread"
	| "vector"
	| "buffer"

export type CheckableTypes =
	| "nil"
	| "boolean"
	| "string"
	| "number"
	| "table"
	| "userdata"
	| "function"
	| "thread"
	| "vector"
	| "buffer"
	| "Axes"
	| "BrickColor"
	| "CFrame"
	| "Color3"
	| "ColorSequence"
	| "ColorSequenceKeypoint"
	| "DateTime"
	| "DockWidgetPluginGuiInfo"
	| "Enum"
	| "EnumItem"
	| "Enums"
	| "Faces"
	| "FloatCurveKey"
	| "Font"
	| "Instance"
	| "NumberRange"
	| "NumberSequence"
	| "NumberSequenceKeypoint"
	| "OverlapParams"
	| "PathWaypoint"
	| "PhysicalProperties"
	| "Random"
	| "Ray"
	| "RaycastParams"
	| "RaycastResult"
	| "RBXScriptConnection"
	| "RBXScriptSignal"
	| "Rect"
	| "Region3"
	| "Region3int16"
	| "TweenInfo"
	| "UDim"
	| "UDim2"
	| "Vector2"
	| "Vector2int16"
	| "Vector3"
	| "Vector3int16"

local MissingTypes = {
	CheckableTypes = GreenTeaUtilities.SmartLiteral(
		"nil",
		"boolean",
		"string",
		"number",
		"table",
		"userdata",
		"function",
		"thread",
		"vector",
		"buffer",
		"Axes",
		"BrickColor",
		"CFrame",
		"Color3",
		"ColorSequence",
		"ColorSequenceKeypoint",
		"DateTime",
		"DockWidgetPluginGuiInfo",
		"Enum",
		"EnumItem",
		"Enums",
		"Faces",
		"FloatCurveKey",
		"Font",
		"Instance",
		"NumberRange",
		"NumberSequence",
		"NumberSequenceKeypoint",
		"OverlapParams",
		"PathWaypoint",
		"PhysicalProperties",
		"Random",
		"Ray",
		"RaycastParams",
		"RaycastResult",
		"RBXScriptConnection",
		"RBXScriptSignal",
		"Rect",
		"Region3",
		"Region3int16",
		"TweenInfo",
		"UDim",
		"UDim2",
		"Vector2",
		"Vector2int16",
		"Vector3",
		"Vector3int16"
	) :: CheckableTypes;

	PrimitiveTypes = GreenTeaUtilities.SmartLiteral(
		"nil",
		"boolean",
		"string",
		"number",
		"table",
		"userdata",
		"function",
		"thread",
		"vector",
		"buffer"
	) :: PrimitiveTypes;
}

return table.freeze(MissingTypes)


--!optimize 2
--!strict

export type Result<O, E> = {
	Success: true,
	Value: O,
} | {
	Success: false,
	Error: E,
}

return false


--!optimize 2
--!strict

local FileSystemUtilities = require("@utilities/FileSystemUtilities")
local GenerateGuid = require("@utilities/GenerateGuid")
local PathFileSystem = require("@packages/PathFileSystem")

local BenchmarkCreators = {}

local BENCHMARKS_FOLDER = PathFileSystem.CWD:join("src"):join("server"):join("benchmarks")

local META_SOURCE = [[{
	"properties": {
		"Attributes": {
			"BenchId": { "String": "%*" }
		}
	}
}
]]
local TYPESCRIPT_SOURCE = [[%*//!nonstrict
//!optimize 2

import type { Benchmark, Profiler } from "%*./types";

function createBenchmark<Arguments extends Array<undefined> | Array<unknown> = Array<unknown>>(
	parameterGenerator: () => LuaTuple<Arguments extends void ? Array<undefined> : Arguments>,
	functions: Record<
		string,
		(profiler: Profiler, ...benchmarkArguments: Arguments extends void ? Array<undefined> : Arguments) => void
	>,
): Benchmark<Arguments> {
	return {
		Functions: functions,
		ParameterGenerator: parameterGenerator,
	};
}

export = createBenchmark(() => $tuple(), {
	"A": (profiler) => {},
	"B": (profiler) => {},
});
]]

function BenchmarkCreators.TypeScript(fileName: string, shouldBeNative: boolean, shouldBeFolder: boolean): ()
	if shouldBeFolder then
		local folderPath = BENCHMARKS_FOLDER:join(`{fileName}.bench`)

		FileSystemUtilities.SafeCreate(folderPath, "Directory")
		FileSystemUtilities.SafeCreate(
			folderPath:join("index.ts"),
			"File",
			string.format(TYPESCRIPT_SOURCE, if shouldBeNative then "//!native\n" else "", ".")
		)
		FileSystemUtilities.SafeCreate(
			folderPath:join("init.meta.json"),
			"File",
			string.format(META_SOURCE, string.upper(GenerateGuid()))
		)
		return
	end

	FileSystemUtilities.SafeCreate(
		BENCHMARKS_FOLDER:join(`{fileName}.bench.ts`),
		"File",
		string.format(TYPESCRIPT_SOURCE, if shouldBeNative then "//!native\n" else "", "")
	)
	FileSystemUtilities.SafeCreate(
		BENCHMARKS_FOLDER:join(`{fileName}.bench.meta.json`),
		"File",
		string.format(META_SOURCE, string.upper(GenerateGuid()))
	)
end

local LUAU_SOURCE = [[%*--!nonstrict
--!optimize 2

type Profiler = {
	Begin: (label: string) -> (),
	End: () -> (),

	Start: (label: string) -> (),
	Stop: () -> (),

	Open: (label: string) -> (),
	Close: () -> (),

	Enter: (label: string) -> (),
	Exit: () -> (),

	begin: (label: string) -> (),
	["end"]: () -> (),

	start: (label: string) -> (),
	stop: () -> (),

	open: (label: string) -> (),
	close: () -> (),

	enter: (label: string) -> (),
	exit: () -> (),
}
type Benchmark<T...> = {
	ParameterGenerator: () -> T...?,
	Functions: {[string]: (profiler: Profiler, T...) -> ()},
}

local function CreateBenchmark<T...>(
	parameterGenerator: () -> T...?,
	functions: {[string]: (profiler: Profiler, T...) -> ()}
): Benchmark<T...>
	return {
		ParameterGenerator = parameterGenerator;
		Functions = functions;
	}
end

return CreateBenchmark(function()
	return
end, {
	["A"] = function(profiler) end;
	["B"] = function(profiler) end;
})
]]

function BenchmarkCreators.Luau(fileName: string, shouldBeNative: boolean, shouldBeFolder: boolean): ()
	if shouldBeFolder then
		local folderPath = BENCHMARKS_FOLDER:join(`{fileName}.bench`)
		FileSystemUtilities.SafeCreate(folderPath, "Directory")
		FileSystemUtilities.SafeCreate(
			folderPath:join("init.luau"),
			"File",
			string.format(LUAU_SOURCE, if shouldBeNative then "--!native\n" else "")
		)
		FileSystemUtilities.SafeCreate(
			folderPath:join("init.meta.json"),
			"File",
			string.format(META_SOURCE, string.upper(GenerateGuid()))
		)
		return
	end

	FileSystemUtilities.SafeCreate(
		BENCHMARKS_FOLDER:join(`{fileName}.bench.luau`),
		"File",
		string.format(LUAU_SOURCE, if shouldBeNative then "--!native\n" else "")
	)
	FileSystemUtilities.SafeCreate(
		BENCHMARKS_FOLDER:join(`{fileName}.bench.meta.json`),
		"File",
		string.format(META_SOURCE, string.upper(GenerateGuid()))
	)
end

return table.freeze(BenchmarkCreators)


--!optimize 2
--!strict

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

local ChalkUtilities = {}

local IsUnknownRaw = GreenTea.unknown({allowNil = true})

local IsCheckableTypes = GreenTea.build(MissingTypes.CheckableTypes)
local IsColorFormatter = GreenTea.build(GreenTea.fn(GreenTea.args(IsUnknownRaw), GreenTea.returns(GreenTea.string())))
local IsUnknown = GreenTea.build(IsUnknownRaw)

export type CheckableTypes = MissingTypes.CheckableTypes
export type ColorFormatter = typeof(IsColorFormatter:type())

local function AsCheckableTypes(checkableType: CheckableTypes): CheckableTypes
	return checkableType
end
ChalkUtilities.AsCheckableType = AsCheckableTypes
ChalkUtilities.AsCheckableTypes = AsCheckableTypes

type ColorFormatters = {[CheckableTypes]: ColorFormatter}
local colorFormatters: ColorFormatters = {
	[AsCheckableTypes("boolean")] = function(value: unknown): string
		return Chalk.Yellow(tostring(value))
	end;
	[AsCheckableTypes("number")] = function(value: unknown): string
		return Chalk.Cyan(tostring(value))
	end;
	[AsCheckableTypes("string")] = function(value: unknown): string
		return Chalk.Green(`"{value}"`)
	end;
}
setmetatable(colorFormatters, {
	__index = function(self: ColorFormatters, index: MissingTypes.CheckableTypes): ColorFormatter
		self[index] = tostring
		return tostring
	end;
})
local colorFormattersCast = (colorFormatters :: unknown) :: ColorFormatters

local TypeOf = (typeof :: unknown) :: (value: unknown) -> CheckableTypes

local function FormatValue(value: unknown): string
	GreenTeaAssert(IsUnknown, value, FormatValue, TypeError)
	return colorFormatters[TypeOf(value)](value)
end
ChalkUtilities.FormatValue = FormatValue

local function Add(checkableTypes: CheckableTypes, colorFormatter: ColorFormatter): ()
	GreenTeaAssert(IsCheckableTypes, checkableTypes, Add, TypeError)
	GreenTeaAssert(IsColorFormatter, colorFormatter, Add, TypeError)

	rawset(colorFormattersCast, checkableTypes, colorFormatter)
end
ChalkUtilities.Add = Add

local function Has(checkableTypes: CheckableTypes): boolean
	GreenTeaAssert(IsCheckableTypes, checkableTypes, Has, TypeError)
	return rawget(colorFormattersCast, checkableTypes) ~= nil
end
ChalkUtilities.Has = Has

local function Remove(checkableTypes: CheckableTypes): ()
	GreenTeaAssert(IsCheckableTypes, checkableTypes, Remove, TypeError)
	rawset(colorFormattersCast, checkableTypes, (nil :: unknown) :: ColorFormatter)
end
ChalkUtilities.Remove = Remove

local function SafeAdd(checkableTypes: CheckableTypes, colorFormatter: ColorFormatter): boolean
	GreenTeaAssert(IsCheckableTypes, checkableTypes, SafeAdd, TypeError)
	GreenTeaAssert(IsColorFormatter, colorFormatter, SafeAdd, TypeError)

	if Has(checkableTypes) then
		return false
	end

	Add(checkableTypes, colorFormatter)
	return true
end
ChalkUtilities.SafeAdd = SafeAdd

return table.freeze(ChalkUtilities)


--!optimize 2
--!strict

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

local String = LuauPolyfill.String
local console = LuauPolyfill.console

local CodeTransformers = {}

type ResultingValue = {
	NewFileSource: string,
	WasChanged: true,
} | {
	WasChanged: false,
}
type TokenType = LuauLexer.TokenType

function CodeTransformers.FunctionFix(source: string, isVerbose: boolean?): ResultingValue
	local navigator = LuauLexer.Navigator()
	navigator.SetSource(source)

	local newFileSource = {}
	local length = 0
	local wasChanged = false

	while true do
		local tokenType: TokenType, content = navigator.Next()
		if tokenType == nil then
			if isVerbose then
				console.log("Breaking the navigation loop.")
			end
			break
		end

		local addContent = content

		if tokenType == "keyword" then
			local functionName: string? = nil
			local skipAmount = 0

			if content == "local " then
				local identifierTokenType: TokenType, identifierContent = navigator.Peek(1)
				if identifierTokenType == "iden" then
					functionName = string.gsub(identifierContent, "%s+$", "")
					skipAmount += 1

					local operatorTokenType: TokenType, operatorContent = navigator.Peek(2)
					if operatorTokenType == "operator" and operatorContent == "= " then
						skipAmount += 1
						local keywordTokenType: TokenType, keywordContent = navigator.Peek(3)
						if keywordTokenType == "keyword" and keywordContent == "function" then
							skipAmount += 1
						else
							skipAmount = 0
							functionName = nil
						end
					else
						skipAmount = 0
						functionName = nil
					end
				end
			end

			if skipAmount ~= 0 and functionName then
				addContent = string.format("local function %*", functionName)
				wasChanged = true
				for _ = 1, skipAmount do
					navigator.Next()
				end
			end
		end

		length += 1
		newFileSource[length] = addContent
	end

	return if wasChanged
		then {
			NewFileSource = table.concat(newFileSource);
			WasChanged = true;
		}
		else {WasChanged = false}
end

local EMPTY_STRING = "\"\""

function CodeTransformers.TableConcatFix(source: string, isVerbose: boolean?): ResultingValue
	local navigator = LuauLexer.Navigator()
	navigator.SetSource(source)

	local newFileSource = {}
	local length = 0
	local wasChanged = false

	while true do
		local tokenType: TokenType, content = navigator.Next()
		if tokenType == nil then
			if isVerbose then
				console.log("Breaking the navigation loop.")
			end
			break
		end

		local addContent = content

		if tokenType == "builtin" then
			local tableName: string? = nil
			local skipAmount = 0

			if content == "table" then
				local operatorTokenType: TokenType, operatorContent = navigator.Peek(1)
				if operatorTokenType == "operator" and operatorContent == "." then
					skipAmount += 1
					local nextBuiltInTokenType: TokenType, nextBuiltInContent = navigator.Peek(2)
					if nextBuiltInTokenType == "builtin" and nextBuiltInContent == "concat" then
						skipAmount += 1
						local nextOperatorTokenType: TokenType, nextOperatorContent = navigator.Peek(3)
						if nextOperatorTokenType == "operator" and nextOperatorContent == "(" then
							skipAmount += 1

							local identifierTokenType: TokenType, identifierContent = navigator.Peek(4)
							if identifierTokenType == "iden" then
								tableName = String.trim(identifierContent)
								skipAmount += 1

								local nextNextOperatorTokenType: TokenType, nextNextOperatorContent = navigator.Peek(5)
								if
									nextNextOperatorTokenType == "operator"
									and String.trim(nextNextOperatorContent) == ","
								then
									skipAmount += 1

									local stringTokenType: TokenType, stringContent = navigator.Peek(6)
									if stringTokenType == "string" and stringContent == EMPTY_STRING then
										skipAmount += 1

										local nextNextNextOperatorTokenType: TokenType, nextNextNextOperatorContent =
											navigator.Peek(7)
										if
											nextNextNextOperatorTokenType == "operator"
											and String.trim(nextNextNextOperatorContent) == ")"
										then
											skipAmount += 1
										else
											skipAmount = 0
											tableName = nil
										end
									else
										skipAmount = 0
										tableName = nil
									end
								else
									skipAmount = 0
									tableName = nil
								end
							else
								skipAmount = 0
								tableName = nil
							end
						else
							skipAmount = 0
							tableName = nil
						end
					else
						skipAmount = 0
						tableName = nil
					end
				else
					skipAmount = 0
					tableName = nil
				end
			end

			if skipAmount ~= 0 and tableName then
				addContent = string.format("table.concat(%*)\n", tableName)
				wasChanged = true
				for _ = 1, skipAmount do
					navigator.Next()
				end
			end
		end

		length += 1
		newFileSource[length] = addContent
	end

	return if wasChanged
		then {
			NewFileSource = table.concat(newFileSource);
			WasChanged = true;
		}
		else {WasChanged = false}
end

return table.freeze(CodeTransformers)