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

local Error = require(script.Parent.Parent.Parent.Error)
local GreenTea = require(script.Parent.Parent.Parent.GreenTea)

local Types = require(script.Parent.Parent.Types)

type ComparisonFunction<T> = Types.ComparisonFunction<T>
type HeapEntry<T> = Types.HeapEntry<T>

export type MaxPriorityQueue<T> = typeof(setmetatable(
	{} :: {
		Array: {HeapEntry<T>},
		Length: number,

		InsertWithPriority: (self: MaxPriorityQueue<T>, value: T, priority: number) -> number,
		Insert: (self: MaxPriorityQueue<T>, value: T, priority: number) -> number,

		ChangePriority: (
			self: MaxPriorityQueue<T>,
			value: T,
			newPriority: number,
			equalityCheck: ComparisonFunction<T>?
		) -> number,

		RemovePriority: (self: MaxPriorityQueue<T>, priority: number) -> (),
		RemoveValue: (self: MaxPriorityQueue<T>, value: T, equalityCheck: ComparisonFunction<T>?) -> (),

		GetFirstPriority: (self: MaxPriorityQueue<T>) -> number?,
		GetLastPriority: (self: MaxPriorityQueue<T>) -> number?,

		PopElement: ((self: MaxPriorityQueue<T>, onlyValue: true) -> T?)
			& ((self: MaxPriorityQueue<T>, onlyValue: false?) -> HeapEntry<T>?)
			& ((self: MaxPriorityQueue<T>, onlyValue: nil) -> HeapEntry<T>?),

		Clear: (self: MaxPriorityQueue<T>) -> MaxPriorityQueue<T>,

		Contains: (self: MaxPriorityQueue<T>, value: T, equalityCheck: ComparisonFunction<T>?) -> boolean,
	},
	{} :: {
		__len: (self: MaxPriorityQueue<T>) -> number,
		__tostring: (self: MaxPriorityQueue<T>) -> string,
	}
))

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

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 IsMaybeBoolean = GreenTea.build(GreenTea.optional(GreenTea.boolean()))
local IsMaybeFunction = GreenTea.build(GreenTea.optional(GreenTea.anyfn()))
local IsNumber = GreenTea.build(GreenTea.number())
local IsUnknown = GreenTea.build(GreenTea.unknown())

--[=[
	In a max priority queue, elements are inserted by the priority in ascending
	order and the maximum value is always removed first from the queue.

	This uses a sorted array internally for the maximum speed possible when
	inserting, popping, and removing by priority.

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

--[=[
	An element with all your elements. This is technically a sorted array.

	@readonly
	@prop Array ReadonlyArray<HeapEntry<T>>
	@within MaxPriorityQueue
]=]

--[=[
	The length of the `MaxPriorityQueue`.

	@readonly
	@prop Length integer
	@within MaxPriorityQueue
]=]

local function FindClosest<T>(array: {HeapEntry<T>}, priority: number, low: number, high: number)
	local middle = math.floor((low + high) / 2)
	if middle == 0 then
		return -1
	end

	local element = array[middle]
	while middle ~= high do
		local priority2 = element.Priority
		if priority == priority2 then
			return middle
		end

		if priority < priority2 then
			high = middle - 1
		else
			low = middle + 1
		end

		middle = math.floor((low + high) / 2)
		element = array[middle]
	end

	return middle
end

--[=[
	Creates a new MaxPriorityQueue.
	@return MaxPriorityQueue<T>
]=]
function MaxPriorityQueue.new<T>(): MaxPriorityQueue<T>
	return setmetatable({
		Array = {};
		Length = 0;
	}, MaxPriorityQueue) :: never
end

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

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

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

	@function instanceof
	@within MaxPriorityQueue

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

local function Insert<T>(self: MaxPriorityQueue<T>, value: T, priority: 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, MaxPriorityQueue.Insert)
			error(exception)
		end
		local typeSuccess1, typeError1 = IsNumber:matches(priority)
		if not typeSuccess1 then
			local exception = Error.new(tostring(typeError1))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, MaxPriorityQueue.Insert)
			error(exception)
		end
	end

	local array = self.Array
	local length = self.Length
	local position = FindClosest(array, priority, 1, length)

	if position == -1 then
		table.insert(array, {Priority = priority, Value = value})
		self.Length = length + 1
		return 1
	end

	local element = array[position]
	if element then
		position = if priority < element.Priority then position else position + 1
	else
		position = 1
	end

	table.insert(array, position, {Priority = priority, Value = value})
	self.Length = length + 1
	return position
end

--[=[
	Adds an element to the `MaxPriorityQueue` with an associated priority.

	@method Insert
	@within MaxPriorityQueue

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

	@param value T -- The value to insert.
	@param priority number -- The priority of the value.
	@return integer -- The index of the element in the `MaxPriorityQueue`.
]=]
MaxPriorityQueue.Insert = Insert
--[=[
	An alias for [MaxPriorityQueue.Insert].

	@method InsertWithPriority
	@within MaxPriorityQueue

	@param value T -- The value to insert.
	@param priority number -- The priority of the value.
	@return integer -- The index of the element in the `MaxPriorityQueue`.
]=]
MaxPriorityQueue.InsertWithPriority = Insert

--[=[
	Allows you to change the priority of an element in the `MaxPriorityQueue`.

	:::info Time Complexity
	This function has kind of a funky time complexity on account of it treating
	your entries as immutable.
	:::

	@error DebugTypeError -- Thrown when you are using debug mode and you pass an incorrect type.
	@error ValueNotFoundError -- Thrown when you try to change the priority of an element that doesn't exist.

	@param value T -- The value to change the priority of.
	@param newPriority number -- The new priority of the value.
	@param equalityCheck? ComparisonFunction<T> -- The function used to check if two values are equal. If unprovided, we'll default to `==`.
	@return integer -- The index of the element in the `MaxPriorityQueue`.
]=]
function MaxPriorityQueue:ChangePriority<T>(value: T, newPriority, equalityCheck: ComparisonFunction<T>?)
	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, MaxPriorityQueue.ChangePriority)
			error(exception)
		end
		local typeSuccess1, typeError1 = IsNumber:matches(newPriority)
		if not typeSuccess1 then
			local exception = Error.new(tostring(typeError1))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, MaxPriorityQueue.ChangePriority)
			error(exception)
		end
		local typeSuccess2, typeError2 = IsMaybeFunction:matches(equalityCheck)
		if not typeSuccess2 then
			local exception = Error.new(tostring(typeError2))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, MaxPriorityQueue.ChangePriority)
			error(exception)
		end
	end

	local array = self.Array
	if equalityCheck then
		for index, element in array do
			if equalityCheck(element.Value, value) then
				table.remove(array, index)
				self.Length -= 1
				return Insert(self, value, newPriority)
			end
		end
	else
		for index, element in array do
			if element.Value == value then
				table.remove(array, index)
				self.Length -= 1
				return Insert(self, value, newPriority)
			end
		end
	end

	local exception = Error.new("Couldn't find value in the queue?")
	exception.name = "ValueNotFoundError"
	Error.captureStackTrace(exception, MaxPriorityQueue.ChangePriority)
	error(exception)
end

--[=[
	Gets the priority of the first value in the `MaxPriorityQueue`. This is the
	value that will be removed last.
	@return number?
]=]
function MaxPriorityQueue:GetFirstPriority()
	return if self.Length == 0 then nil else self.Array[1].Priority
end

--[=[
	Gets the priority of the last value in the `MaxPriorityQueue`. This is the
	value that will be removed first.
	@return number?
]=]
function MaxPriorityQueue:GetLastPriority()
	local length = self.Length
	return if length == 0 then nil else self.Array[length].Priority
end

--[=[
	Pops an element from the `MaxPriorityQueue`. This removes the last element
	in the array.

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

	@param onlyValue? boolean -- Whether or not to return the value of the element.
	@return HeapEntry<T> | T | nil
]=]
function MaxPriorityQueue:PopElement(onlyValue: boolean?)
	if IsUltraStrictMode() then
		local typeSuccess, typeError = IsMaybeBoolean:matches(onlyValue)
		if not typeSuccess then
			local exception = Error.new(tostring(typeError))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, MaxPriorityQueue.PopElement)
			error(exception)
		end
	end

	local length = self.Length
	if length == 0 then
		return nil
	end

	local array = self.Array
	local element = array[length]
	if element then
		array[length] = nil
		self.Length = length - 1
		return if onlyValue then element.Value else element
	end

	return nil
end

--[=[
	Removes an element from the `MaxPriorityQueue` with a specific priority.
	@error DebugTypeError -- Thrown when you are using debug mode and you pass an incorrect type.
	@param priority number -- The priority of the element to remove.
]=]
function MaxPriorityQueue:RemovePriority(priority)
	if IsUltraStrictMode() then
		local typeSuccess, typeError = IsNumber:matches(priority)
		if not typeSuccess then
			local exception = Error.new(tostring(typeError))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, MaxPriorityQueue.RemovePriority)
			error(exception)
		end
	end

	local array = self.Array
	local length = self.Length

	local index = FindClosest(array, priority, 1, length)
	if index == -1 then
		return
	end

	table.remove(array, index)
	self.Length = length - 1
end

--[=[
	Removes an element from the `MaxPriorityQueue` with a specific value.

	:::warning Time Complexity
	This function does _NOT_ use the binary search algorithm. It is uses a
	linear search (may change in the future) so it has the O(n) time
	complexity.
	:::

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

	@param value T -- The value of the element to remove.
	@param equalityCheck? ComparisonFunction<T> -- The function used to check if two values are equal. If unprovided, we'll default to `==`.
]=]
function MaxPriorityQueue:RemoveValue(value, equalityCheck)
	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, MaxPriorityQueue.RemoveValue)
			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, MaxPriorityQueue.RemoveValue)
			error(exception)
		end
	end

	local array = self.Array
	if equalityCheck then
		for index, element in array do
			if equalityCheck(element.Value, value) then
				table.remove(array, index)
				self.Length -= 1
				break
			end
		end
	else
		for index, element in array do
			if element.Value == value then
				table.remove(array, index)
				self.Length -= 1
				break
			end
		end
	end
end

--[=[
	Clears the entire `MaxPriorityQueue`.
	@return MaxPriorityQueue<T>
]=]
function MaxPriorityQueue:Clear()
	table.clear(self.Array)
	self.Length = 0
	return self
end

--[=[
	Checks if the `MaxPriorityQueue` contains a specific value.

	:::warning Time Complexity
	This function does _NOT_ use the binary search algorithm. It is uses a
	linear search (may change in the future) so it has the O(n) time
	complexity.
	:::

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

	@param value T -- The value of the element to remove.
	@param equalityCheck? ComparisonFunction<T> -- The function used to check if two values are equal. If unprovided, we'll default to `==`.
	@return boolean
]=]
function MaxPriorityQueue:Contains(value, equalityCheck)
	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, MaxPriorityQueue.Contains)
			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, MaxPriorityQueue.Contains)
			error(exception)
		end
	end

	if equalityCheck then
		for _, element in self.Array do
			if equalityCheck(element.Value, value) then
				return true
			end
		end
	else
		for _, element in self.Array do
			if element.Value == value then
				return true
			end
		end
	end

	return false
end

function Private:__len()
	return self.Length
end

function Private:__tostring()
	local array = self.Array
	local stringBuilder = table.create(#array)
	for index, value in array do
		stringBuilder[index] = `\t\{Priority: {value.Priority}, Value = {value.Value}};`
	end
	return `MaxPriorityQueue<[\n{table.concat(stringBuilder, "\n")}\n]>`
end

return table.freeze(MaxPriorityQueue :: Static)


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

local Error = require(script.Parent.Parent.Parent.Error)
local GreenTea = require(script.Parent.Parent.Parent.GreenTea)

local Types = require(script.Parent.Parent.Types)

type ComparisonFunction<T> = Types.ComparisonFunction<T>
type HeapEntry<T> = Types.HeapEntry<T>

export type MinPriorityQueue<T> = typeof(setmetatable(
	{} :: {
		Array: {HeapEntry<T>},
		Length: number,

		InsertWithPriority: (self: MinPriorityQueue<T>, value: T, priority: number) -> number,
		Insert: (self: MinPriorityQueue<T>, value: T, priority: number) -> number,

		ChangePriority: (
			self: MinPriorityQueue<T>,
			value: T,
			newPriority: number,
			equalityCheck: ComparisonFunction<T>?
		) -> number,

		RemovePriority: (self: MinPriorityQueue<T>, priority: number) -> (),
		RemoveValue: (self: MinPriorityQueue<T>, value: T, equalityCheck: ComparisonFunction<T>?) -> (),

		GetFirstPriority: (self: MinPriorityQueue<T>) -> number?,
		GetLastPriority: (self: MinPriorityQueue<T>) -> number?,

		PopElement: ((self: MinPriorityQueue<T>, onlyValue: true) -> T?)
			& ((self: MinPriorityQueue<T>, onlyValue: false?) -> HeapEntry<T>?)
			& ((self: MinPriorityQueue<T>, onlyValue: nil) -> HeapEntry<T>?),

		Clear: (self: MinPriorityQueue<T>) -> MinPriorityQueue<T>,

		Contains: (self: MinPriorityQueue<T>, value: T, equalityCheck: ComparisonFunction<T>?) -> boolean,
	},
	{} :: {
		__len: (self: MinPriorityQueue<T>) -> number,
		__tostring: (self: MinPriorityQueue<T>) -> string,
	}
))

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

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 IsMaybeBoolean = GreenTea.build(GreenTea.optional(GreenTea.boolean()))
local IsMaybeFunction = GreenTea.build(GreenTea.optional(GreenTea.anyfn()))
local IsNumber = GreenTea.build(GreenTea.number())
local IsUnknown = GreenTea.build(GreenTea.unknown())

--[=[
	In a min priority queue, elements are inserted by the priority in
	descending order and the maximum value is always removed first from the
	queue.

	This uses a sorted array internally for the maximum speed possible when
	inserting, popping, and removing by priority.

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

--[=[
	An element with all your elements. This is technically a sorted array.

	@readonly
	@prop Array ReadonlyArray<HeapEntry<T>>
	@within MinPriorityQueue
]=]

--[=[
	The length of the `MinPriorityQueue`.

	@readonly
	@prop Length integer
	@within MinPriorityQueue
]=]

local function FindClosest<T>(array: {HeapEntry<T>}, priority: number, low: number, high: number)
	local middle = math.floor((low + high) / 2)
	if middle == 0 then
		return -1
	end

	local element = array[middle]
	while middle ~= high do
		local priority2 = element.Priority
		if priority == priority2 then
			return middle
		end

		if priority > priority2 then
			high = middle - 1
		else
			low = middle + 1
		end

		middle = math.floor((low + high) / 2)
		element = array[middle]
	end

	return middle
end

--[=[
	Creates a new MinPriorityQueue.
	@return MinPriorityQueue<T>
]=]
function MinPriorityQueue.new<T>(): MinPriorityQueue<T>
	return setmetatable({
		Array = {};
		Length = 0;
	}, MinPriorityQueue) :: never
end

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

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

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

	@function instanceof
	@within MinPriorityQueue

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

local function Insert<T>(self: MinPriorityQueue<T>, value: T, priority: 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, MinPriorityQueue.Insert)
			error(exception)
		end
		local typeSuccess1, typeError1 = IsNumber:matches(priority)
		if not typeSuccess1 then
			local exception = Error.new(tostring(typeError1))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, MinPriorityQueue.Insert)
			error(exception)
		end
	end

	local array = self.Array
	local length = self.Length
	local position = FindClosest(array, priority, 1, length)

	if position == -1 then
		table.insert(array, {Priority = priority, Value = value})
		self.Length = length + 1
		return 1
	end

	local element = array[position]
	if element then
		position = if priority > element.Priority then position else position + 1
	else
		position = 1
	end

	table.insert(array, position, {Priority = priority, Value = value})
	self.Length = length + 1
	return position
end

--[=[
	Adds an element to the `MinPriorityQueue` with an associated priority.

	@method Insert
	@within MinPriorityQueue

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

	@param value T -- The value to insert.
	@param priority number -- The priority of the value.
	@return integer -- The index of the element in the `MinPriorityQueue`.
]=]
MinPriorityQueue.Insert = Insert
--[=[
	An alias for [MinPriorityQueue.Insert].

	@method InsertWithPriority
	@within MinPriorityQueue

	@param value T -- The value to insert.
	@param priority number -- The priority of the value.
	@return integer -- The index of the element in the `MinPriorityQueue`.
]=]
MinPriorityQueue.InsertWithPriority = Insert

--[=[
	Allows you to change the priority of an element in the `MinPriorityQueue`.

	:::info Time Complexity
	This function has kind of a funky time complexity on account of it treating
	your entries as immutable.
	:::

	@error DebugTypeError -- Thrown when you are using debug mode and you pass an incorrect type.
	@error ValueNotFoundError -- Thrown when you try to change the priority of an element that doesn't exist.

	@param value T -- The value to change the priority of.
	@param newPriority number -- The new priority of the value.
	@param equalityCheck? ComparisonFunction<T> -- The function used to check if two values are equal. If unprovided, we'll default to `==`.
	@return integer -- The index of the element in the `MinPriorityQueue`.
]=]
function MinPriorityQueue:ChangePriority<T>(value: T, newPriority, equalityCheck: ComparisonFunction<T>?)
	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, MinPriorityQueue.ChangePriority)
			error(exception)
		end
		local typeSuccess1, typeError1 = IsNumber:matches(newPriority)
		if not typeSuccess1 then
			local exception = Error.new(tostring(typeError1))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, MinPriorityQueue.ChangePriority)
			error(exception)
		end
		local typeSuccess2, typeError2 = IsMaybeFunction:matches(equalityCheck)
		if not typeSuccess2 then
			local exception = Error.new(tostring(typeError2))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, MinPriorityQueue.ChangePriority)
			error(exception)
		end
	end

	local array = self.Array
	if equalityCheck then
		for index, element in array do
			if equalityCheck(element.Value, value) then
				table.remove(array, index)
				self.Length -= 1
				return Insert(self, value, newPriority)
			end
		end
	else
		for index, element in array do
			if element.Value == value then
				table.remove(array, index)
				self.Length -= 1
				return Insert(self, value, newPriority)
			end
		end
	end

	local exception = Error.new("Couldn't find value in the queue?")
	exception.name = "ValueNotFoundError"
	Error.captureStackTrace(exception, MinPriorityQueue.ChangePriority)
	error(exception)
end

--[=[
	Gets the priority of the first value in the `MinPriorityQueue`. This is the
	value that will be removed last.
	@return number?
]=]
function MinPriorityQueue:GetFirstPriority()
	return if self.Length == 0 then nil else self.Array[1].Priority
end

--[=[
	Gets the priority of the last value in the `MinPriorityQueue`. This is the
	value that will be removed first.
	@return number?
]=]
function MinPriorityQueue:GetLastPriority()
	local length = self.Length
	return if length == 0 then nil else self.Array[length].Priority
end

--[=[
	Pops an element from the `MinPriorityQueue`. This removes the last element
	in the array.

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

	@param onlyValue? boolean -- Whether or not to return the value of the element.
	@return HeapEntry<T> | T | nil
]=]
function MinPriorityQueue:PopElement(onlyValue: boolean?)
	if IsUltraStrictMode() then
		local typeSuccess, typeError = IsMaybeBoolean:matches(onlyValue)
		if not typeSuccess then
			local exception = Error.new(tostring(typeError))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, MinPriorityQueue.PopElement)
			error(exception)
		end
	end

	local length = self.Length
	if length == 0 then
		return nil
	end

	local array = self.Array
	local element = array[length]
	if element then
		array[length] = nil
		self.Length = length - 1
		return if onlyValue then element.Value else element
	end

	return nil
end

--[=[
	Removes an element from the `MinPriorityQueue` with a specific priority.
	@error DebugTypeError -- Thrown when you are using debug mode and you pass an incorrect type.
	@param priority number -- The priority of the element to remove.
]=]
function MinPriorityQueue:RemovePriority(priority)
	if IsUltraStrictMode() then
		local typeSuccess, typeError = IsNumber:matches(priority)
		if not typeSuccess then
			local exception = Error.new(tostring(typeError))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, MinPriorityQueue.RemovePriority)
			error(exception)
		end
	end

	local array = self.Array
	local length = self.Length

	local index = FindClosest(array, priority, 1, length)
	if index == -1 then
		return
	end

	table.remove(array, index)
	self.Length = length - 1
end

--[=[
	Removes an element from the `MinPriorityQueue` with a specific value.

	:::warning Time Complexity
	This function does _NOT_ use the binary search algorithm. It is uses a
	linear search (may change in the future) so it has the O(n) time
	complexity.
	:::

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

	@param value T -- The value of the element to remove.
	@param equalityCheck? ComparisonFunction<T> -- The function used to check if two values are equal. If unprovided, we'll default to `==`.
]=]
function MinPriorityQueue:RemoveValue(value, equalityCheck)
	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, MinPriorityQueue.RemoveValue)
			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, MinPriorityQueue.RemoveValue)
			error(exception)
		end
	end

	local array = self.Array
	if equalityCheck then
		for index, element in array do
			if equalityCheck(element.Value, value) then
				table.remove(array, index)
				self.Length -= 1
				break
			end
		end
	else
		for index, element in array do
			if element.Value == value then
				table.remove(array, index)
				self.Length -= 1
				break
			end
		end
	end
end

--[=[
	Clears the entire `MinPriorityQueue`.
	@return MinPriorityQueue<T>
]=]
function MinPriorityQueue:Clear()
	table.clear(self.Array)
	self.Length = 0
	return self
end

--[=[
	Checks if the `MinPriorityQueue` contains a specific value.

	:::warning Time Complexity
	This function does _NOT_ use the binary search algorithm. It is uses a
	linear search (may change in the future) so it has the O(n) time
	complexity.
	:::

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

	@param value T -- The value of the element to remove.
	@param equalityCheck? ComparisonFunction<T> -- The function used to check if two values are equal. If unprovided, we'll default to `==`.
	@return boolean
]=]
function MinPriorityQueue:Contains(value, equalityCheck)
	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, MinPriorityQueue.Contains)
			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, MinPriorityQueue.Contains)
			error(exception)
		end
	end

	if equalityCheck then
		for _, element in self.Array do
			if equalityCheck(element.Value, value) then
				return true
			end
		end
	else
		for _, element in self.Array do
			if element.Value == value then
				return true
			end
		end
	end

	return false
end

function Private:__len()
	return self.Length
end

function Private:__tostring()
	local array = self.Array
	local stringBuilder = table.create(#array)
	for index, value in array do
		stringBuilder[index] = `\t\{Priority: {value.Priority}, Value = {value.Value}};`
	end
	return `MinPriorityQueue<[\n{table.concat(stringBuilder, "\n")}\n]>`
end

return table.freeze(MinPriorityQueue :: Static)


--!optimize 2
--!strict

local TestService = game:GetService("TestService")
local JestGlobals = require(TestService.UnitTesting.DevPackages.JestGlobals)

local MaxPriorityQueue = require(script.Parent.Parent.MaxPriorityQueue)

local describe = JestGlobals.describe
local expect = JestGlobals.expect
local it = JestGlobals.it

type MaxPriorityQueue<T> = MaxPriorityQueue.MaxPriorityQueue<T>

-- _G.__DEV__ = true
local itDebug = (if _G.__DEV__ then it else it.skip) :: typeof(it)

describe("MaxPriorityQueue", function()
	describe("constructor", function()
		it("should return a MaxPriorityQueue", function()
			expect(MaxPriorityQueue.Is(MaxPriorityQueue.new())).toBe(true)
			expect(MaxPriorityQueue.new()).toEqual(expect.any(MaxPriorityQueue))
		end)
	end)

	describe("MaxPriorityQueue.Insert", function()
		it("should return a number", function()
			local maxPriorityQueue: MaxPriorityQueue<string> = MaxPriorityQueue.new()
			expect(maxPriorityQueue:Insert("1", 1)).toEqual(expect.any("number"))
		end)

		it("should insert into the proper location", function()
			local maxPriorityQueue: MaxPriorityQueue<string> = MaxPriorityQueue.new()
			maxPriorityQueue:Insert("2", 2)
			maxPriorityQueue:Insert("3", 3)
			expect(maxPriorityQueue:Insert("1", 1)).toBe(1)
		end)

		itDebug("should throw an error if passed incorrect types on debug mode", function()
			local maxPriorityQueue: MaxPriorityQueue<string> = MaxPriorityQueue.new()

			expect(function()
				maxPriorityQueue:Insert("1", 1)
			end).never.toThrow()

			expect(function()
				maxPriorityQueue:Insert(nil :: never, 2)
			end).toThrow()

			expect(function()
				maxPriorityQueue:Insert("2", "2" :: never)
			end).toThrow()
		end)
	end)

	describe("MaxPriorityQueue.ChangePriority", function()
		it("should return a number", function()
			local maxPriorityQueue: MaxPriorityQueue<string> = MaxPriorityQueue.new()
			maxPriorityQueue:Insert("1", 4)
			maxPriorityQueue:Insert("2", 2)
			maxPriorityQueue:Insert("3", 3)
			expect(maxPriorityQueue:ChangePriority("1", 1)).toEqual(expect.any("number"))
		end)

		it("should return adjusted location", function()
			local maxPriorityQueue: MaxPriorityQueue<string> = MaxPriorityQueue.new()
			maxPriorityQueue:Insert("1", 4)
			maxPriorityQueue:Insert("2", 2)
			maxPriorityQueue:Insert("3", 3)
			expect(maxPriorityQueue:ChangePriority("1", 1)).toEqual(1)
		end)

		it("should throw if the passed value does not exist already", function()
			expect(function()
				MaxPriorityQueue.new():ChangePriority("1", 1)
			end).toThrow()
		end)
	end)
end)

return false


--!optimize 2
--!strict

local TestService = game:GetService("TestService")
local JestGlobals = require(TestService.UnitTesting.DevPackages.JestGlobals)

local MinPriorityQueue = require(script.Parent.Parent.MinPriorityQueue)

local describe = JestGlobals.describe
local expect = JestGlobals.expect
local it = JestGlobals.it

type MinPriorityQueue<T> = MinPriorityQueue.MinPriorityQueue<T>

-- _G.__DEV__ = true
local itDebug = (if _G.__DEV__ then it else it.skip) :: typeof(it)

describe("MinPriorityQueue", function()
	describe("constructor", function()
		it("should return a MinPriorityQueue", function()
			expect(MinPriorityQueue.Is(MinPriorityQueue.new())).toBe(true)
			expect(MinPriorityQueue.new()).toEqual(expect.any(MinPriorityQueue))
		end)
	end)

	describe("MinPriorityQueue.Insert", function()
		it("should return a number", function()
			local maxPriorityQueue: MinPriorityQueue<string> = MinPriorityQueue.new()
			expect(maxPriorityQueue:Insert("1", 1)).toEqual(expect.any("number"))
		end)

		it("should insert into the proper location", function()
			local maxPriorityQueue: MinPriorityQueue<string> = MinPriorityQueue.new()
			maxPriorityQueue:Insert("2", 2)
			maxPriorityQueue:Insert("3", 3)
			expect(maxPriorityQueue:Insert("1", 1)).toBe(3)
		end)

		itDebug("should throw an error if passed incorrect types on debug mode", function()
			local maxPriorityQueue: MinPriorityQueue<string> = MinPriorityQueue.new()

			expect(function()
				maxPriorityQueue:Insert("1", 1)
			end).never.toThrow()

			expect(function()
				maxPriorityQueue:Insert(nil :: never, 2)
			end).toThrow()

			expect(function()
				maxPriorityQueue:Insert("2", "2" :: never)
			end).toThrow()
		end)
	end)

	describe("MinPriorityQueue.ChangePriority", function()
		it("should return a number", function()
			local maxPriorityQueue: MinPriorityQueue<string> = MinPriorityQueue.new()
			maxPriorityQueue:Insert("1", 4)
			maxPriorityQueue:Insert("2", 2)
			maxPriorityQueue:Insert("3", 3)
			expect(maxPriorityQueue:ChangePriority("1", 1)).toEqual(expect.any("number"))
		end)

		it("should return adjusted location", function()
			local maxPriorityQueue: MinPriorityQueue<string> = MinPriorityQueue.new()
			maxPriorityQueue:Insert("1", 4)
			maxPriorityQueue:Insert("2", 2)
			maxPriorityQueue:Insert("3", 3)
			expect(maxPriorityQueue:ChangePriority("1", 1)).toEqual(3)
		end)

		it("should throw if the passed value does not exist already", function()
			expect(function()
				MinPriorityQueue.new():ChangePriority("1", 1)
			end).toThrow()
		end)
	end)
end)

return false


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

local Error = require(script.Parent.Parent.Parent.Error)
local GreenTea = require(script.Parent.Parent.Parent.GreenTea)

export type LiveRandom = {
	Get: (self: LiveRandom) -> number,
	Peek: (self: LiveRandom) -> number,
	Rig: (self: LiveRandom, value: number) -> LiveRandom,
}
type Private = {
	Last: number,
	Offset: number,
	Prime: number,
	Range: number,
	Rigged: {number},

	Get: (self: Private) -> number,
	Peek: (self: Private) -> number,
	Rig: (self: Private, value: number) -> Private,
}

type Static = {
	ClassName: "LiveRandom",
	new: (minimum: number, maximum: number?, seed: number?) -> LiveRandom,
	Is: (value: any) -> boolean,
	instanceof: (value: any) -> boolean,
}
type PrivateStatic = Static & {__tostring: (self: LiveRandom) -> string}

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 IsIntegerRaw = GreenTea.number({integer = true})
local IsInteger = GreenTea.build(IsIntegerRaw)
local IsMaybeInteger = GreenTea.build(GreenTea.optional(IsIntegerRaw))
local IsMaybeNumber = GreenTea.build(GreenTea.optional(GreenTea.number()))

local function Gcd(a: number, b: number)
	while b > 0 do
		a, b = b, a % b
	end
	return a
end

-- cheese it
local NextInteger = Random.new().NextInteger

local function GetRandomRelativePrime(value: number, randomLibrary: Random)
	if value == 1 then
		return 1
	end

	local newValue = NextInteger(randomLibrary, 1, value - 1)
	if Gcd(value, newValue) == 1 then
		return newValue
	end

	repeat
		newValue = NextInteger(randomLibrary, 1, value - 1)
	until Gcd(newValue, value) == 1

	return newValue
end

--[=[
	## LiveRandom

	Will loop through all of the numbers in range before repeating. Does not
	use any table nor any messy bash, just math. B)

	It first finds a co-prime integer to n between 1 and (n - 1) and constantly increments a running sum.

	- This is guaranteed to touch all of the integers.

	### Complexity

	- **Storage:** `O(1)`
	- **Construction:** `O(log(n))`
	- **Get:** `O(1)`

	@tag Supports Debug Mode
	@tag Supports __tostring
	@class LiveRandom
]=]
local LiveRandom = {} :: LiveRandom & Static
local Private = LiveRandom :: Private & PrivateStatic
LiveRandom.ClassName = "LiveRandom";
(LiveRandom :: any).__index = LiveRandom

--[=[
	Creates a new `LiveRandom` instance.

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

	:::info Default Values
	Like `math.random`, if you do not provide a maximum, the maximum becomes
	the minimum and the minimum becomes 1.
	:::

	@param minimum integer -- The minimum value the random number can be.
	@param maximum? integer -- The maximum value the random number can be.
	@param seed? number -- The seed to use for the random number generator. Defaults to `os.clock()%1 * 1E7`.
	@return LiveRandom
]=]
function LiveRandom.new(minimum, maximum, seed)
	if IsUltraStrictMode() then
		local typeSuccess0, typeError0 = IsInteger:matches(minimum)
		if not typeSuccess0 then
			local exception = Error.new(tostring(typeError0))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, LiveRandom.new)
			error(exception)
		end
		local typeSuccess1, typeError1 = IsMaybeInteger:matches(maximum)
		if not typeSuccess1 then
			local exception = Error.new(tostring(typeError1))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, LiveRandom.new)
			error(exception)
		end
		local typeSuccess2, typeError2 = IsMaybeNumber:matches(seed)
		if not typeSuccess2 then
			local exception = Error.new(tostring(typeError2))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, LiveRandom.new)
			error(exception)
		end
	end

	local trueMaximum = maximum or 1
	local trueMinimum = minimum
	if not maximum then
		trueMaximum = minimum
		trueMinimum = 1
	end

	local range = trueMaximum - trueMinimum + 1
	local randomLibrary = Random.new(seed or os.clock() % 1 * 1E7)

	return setmetatable({
		Last = NextInteger(randomLibrary, 1, range);
		Offset = trueMinimum - 1;
		Prime = GetRandomRelativePrime(range, randomLibrary);
		Range = range;
		Rigged = {};
	}, LiveRandom) :: never
end

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

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

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

	@function instanceof
	@within LiveRandom

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

--[=[
	Gets a random value.

	@method Get
	@within LiveRandom

	@return integer
]=]
function Private:Get()
	local range = self.Range
	local rigged = self.Rigged

	local length = #rigged
	local riggedValue = rigged[length]
	if riggedValue then
		rigged[length] = nil
		return riggedValue
	end

	local value = self.Last + self.Prime
	local last = if value > range then value - range else value
	self.Last = last
	return last + self.Offset
end

--[=[
	Peeks at the next value. This does not cause it to roll a new value.

	@method Peek
	@within LiveRandom

	@return integer
]=]
function Private:Peek()
	local rigged = self.Rigged
	local top = rigged[#rigged]
	if top then
		return top
	end

	local value = self.Last + self.Prime
	local range = self.Range
	return (if value > range then value - range else value) + self.Offset
end

--[=[
	Rigs the random number generator, which forces the next value to be the
	given value.

	@method Rig
	@within LiveRandom

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

	@param value integer -- The value to rig the random number generator to.
	@return LiveRandom
]=]
function Private:Rig(value)
	if IsUltraStrictMode() then
		local typeSuccess, typeError = IsInteger:matches(value)
		if not typeSuccess then
			local exception = Error.new(tostring(typeError))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, LiveRandom.Rig)
			error(exception)
		end
	end

	table.insert(self.Rigged, value)
	return self
end

function Private:__tostring()
	return "LiveRandom"
end

return table.freeze(LiveRandom :: Static)


--!optimize 2
--!strict

local TestService = game:GetService("TestService")
local JestGlobals = require(TestService.UnitTesting.DevPackages.JestGlobals)

local LiveRandom = require(script.Parent.Parent.LiveRandom)

local describe = JestGlobals.describe
local expect = JestGlobals.expect
local it = JestGlobals.it

local SEED_TO_USE = 8949857.9991050064563751220703125

describe("LiveRandom", function()
	describe("constructor", function()
		it("should return a LiveRandom", function()
			local liveRandom = LiveRandom.new(1, 10)
			expect(tostring(liveRandom)).toBe("LiveRandom")
			expect(liveRandom).toEqual(expect.any(LiveRandom))
			expect(LiveRandom.Is(liveRandom)).toBe(true)
		end)
	end)

	describe("LiveRandom.Get", function()
		it("should return a unique value in the range only once until we're out, then we repeat", function()
			local liveRandom = LiveRandom.new(1, 10)
			local first = liveRandom:Peek()
			local hasSeen: {[number]: true} = {}

			for _ = 1, 10 do
				local index = liveRandom:Get()
				expect(hasSeen[index]).toBeUndefined()
				hasSeen[index] = true
			end

			expect(liveRandom:Get()).toBe(first)
		end)
	end)

	describe("LiveRandom.Peek", function()
		it("should return the correct value every time", function()
			for _ = 1, 10 do
				expect(LiveRandom.new(1, 10, SEED_TO_USE):Peek()).toBe(10)
			end
		end)

		it("should return the correct value every time, even with a constant random seed", function()
			local randomSeed = os.clock() % 1 * 1E7
			local value = LiveRandom.new(1, 10, randomSeed):Peek()
			for _ = 1, 10 do
				expect(LiveRandom.new(1, 10, randomSeed):Peek()).toBe(value)
			end
		end)

		it("should not regenerate", function()
			local liveRandom = LiveRandom.new(1, 10)
			expect(liveRandom:Peek()).toBe(liveRandom:Peek())
		end)
	end)

	describe("LiveRandom.Rig", function()
		it("should return the same LiveRandom", function()
			local liveRandom = LiveRandom.new(1, 10)
			expect(liveRandom:Rig(11)).toBe(liveRandom)
		end)

		it("should return the rigged value on next get/peek", function()
			expect(LiveRandom.new(1, 10, SEED_TO_USE):Rig(11):Peek()).toBe(11)
		end)
	end)
end)

return false


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

local Error = require(script.Parent.Parent.Parent.Error)
local GreenTea = require(script.Parent.Parent.Parent.GreenTea)

local Types = require(script.Parent.Types)

export type SimpleTrie = Types.BasicTrie

type Static = {
	ClassName: "SimpleTrie",
	new: () -> SimpleTrie,
	Is: (value: any) -> boolean,
	instanceof: (value: any) -> boolean,
}
type PrivateStatic = Static & {__tostring: (self: SimpleTrie) -> string}

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 IsString = GreenTea.build(GreenTea.string())

--[=[
	The `SimpleTrie` class implements a basic trie (prefix tree) data structure
	for efficient storage and retrieval of strings. It allows you to insert
	words, search for exact matches, and check if any words in the trie start
	with a given prefix. If you need utf8 support, use the [Utf8Trie] class.

	### Features:

	- **Efficient String Storage:** Stores strings in a hierarchical structure
	  for fast retrieval and minimal redundancy.
	- **Word Insertion:** Add words to the trie using the `Insert` method.
	- **Exact Word Search:** Determine if a word exists in the trie with the
	  `Search` method.
	- **Prefix Matching:** Use the `StartsWith` method to check if any words
	  start with a given prefix.
	- **Supports Debug Mode:** Enable debug mode by setting `_G.__DEV__ = true`
	  before requiring the module to enforce type checks and provide detailed
	  error messages.

	### Example Usage:

	```lua
	_G.__DEV__ = RunService:IsStudio()
	local DataStructures = require("DataStructures")

	-- Create a new trie
	local trie = DataStructures.SimpleTrie.new()

	-- Insert words
	trie:Insert("apple")
	trie:Insert("app")
	trie:Insert("banana")

	-- Search for exact words
	print(trie:Search("apple"))   -- Output: true
	print(trie:Search("app"))     -- Output: true
	print(trie:Search("apples"))  -- Output: false

	-- Check for prefixes
	print(trie:StartsWith("app"))   -- Output: true
	print(trie:StartsWith("ban"))   -- Output: true
	print(trie:StartsWith("bat"))   -- Output: false

	-- Check if an object is a SimpleTrie
	print(DataStructures.SimpleTrie.Is(trie))     -- Output: true
	print(DataStructures.SimpleTrie.Is({}))       -- Output: false

	-- String representation
	print(tostring(trie))          -- Output: "SimpleTrie"
	```

	### Methods:

	- **`SimpleTrie.new(): SimpleTrie`**
		- Creates a new `SimpleTrie` instance.
		- **Returns:** A new `SimpleTrie` object.

	- **`SimpleTrie:Insert(word: string): void`**
		- Inserts a word into the trie.
		- **Parameters:**
			- `word` (string): The word to insert into the trie.
		- **Errors:**
			- **`DebugTypeError`**: Thrown when debug mode is enabled and
			  `word` is not a string.

	- **`SimpleTrie:Search(word: string): boolean`**
		- Searches for an exact word in the trie.
		- **Parameters:**
			- `word` (string): The word to search for.
		- **Returns:** `true` if the word exists in the trie, `false`
		  otherwise.
		- **Errors:**
			- **`DebugTypeError`**: Thrown when debug mode is enabled and
			  `word` is not a string.

	- **`SimpleTrie:StartsWith(prefix: string): boolean`**
		- Checks if any word in the trie starts with the given prefix.
		- **Parameters:**
			- `prefix` (string): The prefix to check.
		- **Returns:** `true` if any word starts with the prefix, `false` otherwise.
		- **Errors:**
			- **`DebugTypeError`**: Thrown when debug mode is enabled and
			  `prefix` is not a string.

	- **`SimpleTrie.Is(value: any): boolean`**
		- Checks if the given value is an instance of `SimpleTrie`.
		- **Parameters:**
			- `value` (any): The value to check.
		- **Returns:** `true` if `value` is a `SimpleTrie` instance, `false`
		  otherwise.

	- **`SimpleTrie.instanceof(value: any): boolean`**
		- Alias for `SimpleTrie.Is`.

	### Tags:

	- `Supports Debug Mode`: You can set `_G.__DEV__` to `true` to enable
	  debug mode. This has to be done BEFORE requiring the module.
	- `Supports __tostring`: You can call `tostring` on the trie.

	@tag Supports Debug Mode
	@tag Supports __tostring
	@class SimpleTrie
]=]
local SimpleTrie = {} :: SimpleTrie & Static
local Private = SimpleTrie :: SimpleTrie & PrivateStatic
SimpleTrie.ClassName = "SimpleTrie";
(SimpleTrie :: any).__index = SimpleTrie

--[=[
	Creates a new SimpleTrie.
	@return SimpleTrie
]=]
function SimpleTrie.new()
	return setmetatable({
		Root = {
			Children = {};
			IsEnding = false;
		};
	}, SimpleTrie) :: never
end

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

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

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

	@function instanceof
	@within SimpleTrie

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

--[=[
	Inserts a word into the trie.

	@error DebugTypeError -- Thrown when you are using debug mode and you pass an incorrect type.
	@param word string -- The word to insert.
]=]
function SimpleTrie:Insert(word)
	if IsUltraStrictMode() then
		local typeSuccess, typeError = IsString:matches(word)
		if not typeSuccess then
			local exception = Error.new(tostring(typeError))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, SimpleTrie.Insert)
			error(exception)
		end
	end

	local current = self.Root
	for index = 1, #word do
		local byte = string.byte(word, index)
		local children = current.Children

		local nextInLine = children[byte]
		if not nextInLine then
			nextInLine = {
				Children = {};
				IsEnding = false;
			}
			children[byte] = nextInLine
		end

		current = nextInLine
	end

	current.IsEnding = true
end

--[=[
	Searches for an exact word in the trie.

	@error DebugTypeError -- Thrown when debug mode is enabled and `word` is not a string.

	@param word string -- The word to search for.
	@return boolean -- `true` if the word exists in the trie, `false` otherwise.
]=]
function SimpleTrie:Search(word)
	if IsUltraStrictMode() then
		local typeSuccess, typeError = IsString:matches(word)
		if not typeSuccess then
			local exception = Error.new(tostring(typeError))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, SimpleTrie.Search)
			error(exception)
		end
	end

	local current = self.Root
	for index = 1, #word do
		local nextInLine = current.Children[string.byte(word, index)]
		if not nextInLine then
			return false
		end
		current = nextInLine
	end

	return current.IsEnding
end

--[=[
	Checks if any word in the trie starts with the given prefix.

	@error DebugTypeError -- Thrown when debug mode is enabled and `prefix` is not a string.

	@param prefix string -- The prefix to check.
	@return boolean -- `true` if any word starts with the prefix, `false` otherwise.
]=]
function SimpleTrie:StartsWith(prefix)
	if IsUltraStrictMode() then
		local typeSuccess, typeError = IsString:matches(prefix)
		if not typeSuccess then
			local exception = Error.new(tostring(typeError))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, SimpleTrie.StartsWith)
			error(exception)
		end
	end

	local current = self.Root
	for index = 1, #prefix do
		local nextInLine = current.Children[string.byte(prefix, index)]
		if not nextInLine then
			return false
		end
		current = nextInLine
	end

	return true
end

function Private:__tostring()
	return "SimpleTrie"
end

return table.freeze(SimpleTrie :: Static)


--!optimize 2
--!strict

export type BasicTrie = {
	Root: Node,

	Insert: (self: BasicTrie, word: string) -> (),
	Search: (self: BasicTrie, word: string) -> boolean,
	StartsWith: (self: BasicTrie, prefix: string) -> boolean,
}
export type Node = {
	Children: {[number]: Node},
	IsEnding: boolean,
}

return false


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

local Error = require(script.Parent.Parent.Parent.Error)
local GreenTea = require(script.Parent.Parent.Parent.GreenTea)

local Types = require(script.Parent.Types)

export type Utf8Trie = Types.BasicTrie

type Static = {
	ClassName: "Utf8Trie",
	new: () -> Utf8Trie,
	Is: (value: any) -> boolean,
	instanceof: (value: any) -> boolean,
}
type PrivateStatic = Static & {__tostring: (self: Utf8Trie) -> string}

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 IsString = GreenTea.build(GreenTea.string())

--[=[
	The `Utf8Trie` class implements a basic trie (prefix tree) data structure
	for efficient storage and retrieval of strings. It allows you to insert
	words, search for exact matches, and check if any words in the trie start
	with a given prefix. If you need performance, use the [SimpleTrie] class.

	### Features:

	- **Efficient String Storage:** Stores strings in a hierarchical structure
	  for fast retrieval and minimal redundancy.
	- **Word Insertion:** Add words to the trie using the `Insert` method.
	- **Exact Word Search:** Determine if a word exists in the trie with the
	  `Search` method.
	- **Prefix Matching:** Use the `StartsWith` method to check if any words
	  start with a given prefix.
	- **Supports Debug Mode:** Enable debug mode by setting `_G.__DEV__ = true`
	  before requiring the module to enforce type checks and provide detailed
	  error messages.

	### Example Usage:

	```lua
	_G.__DEV__ = RunService:IsStudio()
	local DataStructures = require("DataStructures")

	-- Create a new trie
	local trie = DataStructures.Utf8Trie.new()

	-- Insert words
	trie:Insert("apple")
	trie:Insert("app")
	trie:Insert("banana")

	-- Search for exact words
	print(trie:Search("apple"))   -- Output: true
	print(trie:Search("app"))     -- Output: true
	print(trie:Search("apples"))  -- Output: false

	-- Check for prefixes
	print(trie:StartsWith("app"))   -- Output: true
	print(trie:StartsWith("ban"))   -- Output: true
	print(trie:StartsWith("bat"))   -- Output: false

	-- Check if an object is a Utf8Trie
	print(DataStructures.Utf8Trie.Is(trie))     -- Output: true
	print(DataStructures.Utf8Trie.Is({}))       -- Output: false

	-- String representation
	print(tostring(trie))          -- Output: "Utf8Trie"
	```

	### Methods:

	- **`Utf8Trie.new(): Utf8Trie`**
		- Creates a new `Utf8Trie` instance.
		- **Returns:** A new `Utf8Trie` object.

	- **`Utf8Trie:Insert(word: string): void`**
		- Inserts a word into the trie.
		- **Parameters:**
			- `word` (string): The word to insert into the trie.
		- **Errors:**
			- **`DebugTypeError`**: Thrown when debug mode is enabled and
			  `word` is not a string.

	- **`Utf8Trie:Search(word: string): boolean`**
		- Searches for an exact word in the trie.
		- **Parameters:**
			- `word` (string): The word to search for.
		- **Returns:** `true` if the word exists in the trie, `false`
		  otherwise.
		- **Errors:**
			- **`DebugTypeError`**: Thrown when debug mode is enabled and
			  `word` is not a string.

	- **`Utf8Trie:StartsWith(prefix: string): boolean`**
		- Checks if any word in the trie starts with the given prefix.
		- **Parameters:**
			- `prefix` (string): The prefix to check.
		- **Returns:** `true` if any word starts with the prefix, `false` otherwise.
		- **Errors:**
			- **`DebugTypeError`**: Thrown when debug mode is enabled and
			  `prefix` is not a string.

	- **`Utf8Trie.Is(value: any): boolean`**
		- Checks if the given value is an instance of `Utf8Trie`.
		- **Parameters:**
			- `value` (any): The value to check.
		- **Returns:** `true` if `value` is a `Utf8Trie` instance, `false`
		  otherwise.

	- **`Utf8Trie.instanceof(value: any): boolean`**
		- Alias for `Utf8Trie.Is`.

	### Tags:

	- `Supports Debug Mode`: You can set `_G.__DEV__` to `true` to enable
	  debug mode. This has to be done BEFORE requiring the module.
	- `Supports __tostring`: You can call `tostring` on the trie.

	@tag Supports Debug Mode
	@tag Supports __tostring
	@class Utf8Trie
]=]
local Utf8Trie = {} :: Utf8Trie & Static
local Private = Utf8Trie :: Utf8Trie & PrivateStatic
Utf8Trie.ClassName = "Utf8Trie";
(Utf8Trie :: any).__index = Utf8Trie

--[=[
	Creates a new Utf8Trie.
	@return Utf8Trie
]=]
function Utf8Trie.new()
	return setmetatable({
		Root = {
			Children = {};
			IsEnding = false;
		};
	}, Utf8Trie) :: never
end

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

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

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

	@function instanceof
	@within Utf8Trie

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

--[=[
	Inserts a word into the trie.

	@error DebugTypeError -- Thrown when you are using debug mode and you pass an incorrect type.
	@param word string -- The word to insert.
]=]
function Utf8Trie:Insert(word)
	if IsUltraStrictMode() then
		local typeSuccess, typeError = IsString:matches(word)
		if not typeSuccess then
			local exception = Error.new(tostring(typeError))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, Utf8Trie.Insert)
			error(exception)
		end
	end

	local current = self.Root
	for start, finish in utf8.graphemes(word) do
		local byte = utf8.codepoint(word, start, finish)
		local children = current.Children

		local nextInLine = children[byte]
		if not nextInLine then
			nextInLine = {
				Children = {};
				IsEnding = false;
			}
			children[byte] = nextInLine
		end

		current = nextInLine
	end

	current.IsEnding = true
end

--[=[
	Searches for an exact word in the trie.

	@error DebugTypeError -- Thrown when debug mode is enabled and `word` is not a string.

	@param word string -- The word to search for.
	@return boolean -- `true` if the word exists in the trie, `false` otherwise.
]=]
function Utf8Trie:Search(word)
	if IsUltraStrictMode() then
		local typeSuccess, typeError = IsString:matches(word)
		if not typeSuccess then
			local exception = Error.new(tostring(typeError))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, Utf8Trie.Search)
			error(exception)
		end
	end

	local current = self.Root
	for start, finish in utf8.graphemes(word) do
		local nextInLine = current.Children[utf8.codepoint(word, start, finish)]
		if not nextInLine then
			return false
		end
		current = nextInLine
	end

	return current.IsEnding
end

--[=[
	Checks if any word in the trie starts with the given prefix.

	@error DebugTypeError -- Thrown when debug mode is enabled and `prefix` is not a string.

	@param prefix string -- The prefix to check.
	@return boolean -- `true` if any word starts with the prefix, `false` otherwise.
]=]
function Utf8Trie:StartsWith(prefix)
	if IsUltraStrictMode() then
		local typeSuccess, typeError = IsString:matches(prefix)
		if not typeSuccess then
			local exception = Error.new(tostring(typeError))
			exception.name = "DebugTypeError"
			Error.captureStackTrace(exception, Utf8Trie.StartsWith)
			error(exception)
		end
	end

	local current = self.Root
	for start, finish in utf8.graphemes(prefix) do
		local nextInLine = current.Children[utf8.codepoint(prefix, start, finish)]
		if not nextInLine then
			return false
		end
		current = nextInLine
	end

	return true
end

function Private:__tostring()
	return "Utf8Trie"
end

return table.freeze(Utf8Trie :: Static)


--!optimize 2
--!strict

local TestService = game:GetService("TestService")
local JestGlobals = require(TestService.UnitTesting.DevPackages.JestGlobals)

local SimpleTrie = require(script.Parent.Parent.SimpleTrie)

local beforeEach = JestGlobals.beforeEach
local describe = JestGlobals.describe
local expect = JestGlobals.expect
local it = JestGlobals.it

describe("SimpleTrie", function()
	local trie: SimpleTrie.SimpleTrie = nil
	beforeEach(function()
		trie = SimpleTrie.new()
	end)

	it("should insert and search for words correctly", function()
		trie:Insert("apple")
		expect(trie:Search("apple")).toBe(true)
		expect(trie:Search("app")).toBe(false)
		expect(trie:StartsWith("app")).toBe(true)
	end)

	it("should handle insertion of multiple words with common prefixes", function()
		trie:Insert("app")
		trie:Insert("application")
		trie:Insert("apex")
		expect(trie:Search("app")).toBe(true)
		expect(trie:Search("application")).toBe(true)
		expect(trie:Search("apex")).toBe(true)
		expect(trie:Search("apple")).toBe(false)
		expect(trie:StartsWith("ap")).toBe(true)
	end)

	it("should return false for searching non-existent words", function()
		expect(trie:Search("banana")).toBe(false)
		expect(trie:StartsWith("ban")).toBe(false)
	end)

	it("should return true for startsWith on existing prefixes", function()
		trie:Insert("banana")
		expect(trie:StartsWith("ban")).toBe(true)
		expect(trie:StartsWith("bana")).toBe(true)
		expect(trie:StartsWith("band")).toBe(false)
	end)

	it("should handle empty string insertion and search", function()
		trie:Insert("")
		expect(trie:Search("")).toBe(true)
		expect(trie:StartsWith("")).toBe(true)
	end)

	it("should handle case sensitivity correctly", function()
		trie:Insert("Apple")
		expect(trie:Search("apple")).toBe(false)
		expect(trie:Search("Apple")).toBe(true)
		expect(trie:StartsWith("App")).toBe(true)
		expect(trie:StartsWith("app")).toBe(false)
	end)
end)

return false


--!optimize 2
--!strict

local TestService = game:GetService("TestService")
local JestGlobals = require(TestService.UnitTesting.DevPackages.JestGlobals)

local Utf8Trie = require(script.Parent.Parent.Utf8Trie)

local beforeEach = JestGlobals.beforeEach
local describe = JestGlobals.describe
local expect = JestGlobals.expect
local it = JestGlobals.it

describe("Utf8Trie", function()
	local trie: Utf8Trie.Utf8Trie = nil
	beforeEach(function()
		trie = Utf8Trie.new()
	end)

	it("should insert and search for words with UTF-8 characters", function()
		trie:Insert("こんにちは") -- Japanese for "Hello"
		expect(trie:Search("こんにちは")).toBe(true)
		expect(trie:Search("こん")).toBe(false)
		expect(trie:StartsWith("こん")).toBe(true)
	end)

	it("should handle emojis correctly", function()
		trie:Insert("😀")
		trie:Insert("😁😂")
		expect(trie:Search("😀")).toBe(true)
		expect(trie:Search("😁")).toBe(false)
		expect(trie:Search("😁😂")).toBe(true)
		expect(trie:StartsWith("😁")).toBe(true)
		expect(trie:StartsWith("😂")).toBe(false)
	end)

	it("should handle words with mixed ASCII and UTF-8 characters", function()
		trie:Insert("hello😀")
		trie:Insert("hello")
		expect(trie:Search("hello")).toBe(true)
		expect(trie:Search("hello😀")).toBe(true)
		expect(trie:Search("hello😁")).toBe(false)
		expect(trie:StartsWith("hell")).toBe(true)
		expect(trie:StartsWith("hello😀")).toBe(true)
	end)

	it("should return false for searching non-existent UTF-8 words", function()
		expect(trie:Search("世界")).toBe(false) -- Chinese for "World"
		expect(trie:StartsWith("世")).toBe(false)
	end)

	it("should handle insertion of multiple UTF-8 words with common prefixes", function()
		trie:Insert("😀😃😄")
		trie:Insert("😀😃")
		trie:Insert("😀")
		expect(trie:Search("😀")).toBe(true)
		expect(trie:Search("😀😃")).toBe(true)
		expect(trie:Search("😀😃😄")).toBe(true)
		expect(trie:Search("😀😄")).toBe(false)
		expect(trie:StartsWith("😀")).toBe(true)
	end)

	it("should handle complex scripts correctly", function()
		trie:Insert("हैलो") -- Hindi for "Hello"
		expect(trie:Search("हैलो")).toBe(true)
		expect(trie:StartsWith("है")).toBe(true)
		expect(trie:Search("है")).toBe(false)
	end)

	it("should handle accents and diacritics", function()
		trie:Insert("café")
		trie:Insert("cafe")
		expect(trie:Search("café")).toBe(true)
		expect(trie:Search("cafe")).toBe(true)
		expect(trie:StartsWith("caf")).toBe(true)
	end)
end)

return false


--!strict

--[=[
	All the types related to this library.
	@class Types
]=]
local Types = {}

-- Luau really, really, really needs strict arrays.
export type Tuple<K, V> = {K | V}

--[=[
	The data structure that represents an element in a [MaxPriorityQueue] or
	[MinPriorityQueue].

	@interface HeapEntry
	@field Priority number -- The priority of the element.
	@field Value T -- The value of the element.
	@within Types
]=]
export type HeapEntry<T> = {
	Priority: number,
	Value: T,
}

--[=[
	A function that compares two values.

	@type ComparisonFunction <T>(a: T, b: T) -> boolean
	@within Types
]=]
export type ComparisonFunction<T> = (a: T, b: T) -> boolean

return table.freeze(Types)


--!optimize 2
--!strict

local LRUCache = require(script.Caches.LRUCache)
local LiveRandom = require(script.Random.LiveRandom)
local MaxPriorityQueue = require(script.Queues.MaxPriorityQueue)
local MinPriorityQueue = require(script.Queues.MinPriorityQueue)
local SimpleTrie = require(script.Tries.SimpleTrie)
local SortedArray = require(script.Arrays.SortedArray)
local Types = require(script.Types)
local Utf8Trie = require(script.Tries.Utf8Trie)

local DataStructures = table.freeze({
	LiveRandom = LiveRandom;
	LRUCache = LRUCache;
	MaxPriorityQueue = MaxPriorityQueue;
	MinPriorityQueue = MinPriorityQueue;
	SimpleTrie = SimpleTrie;
	SortedArray = SortedArray;
	Utf8Trie = Utf8Trie;
})

export type ComparisonFunction<T> = Types.ComparisonFunction<T>
export type HeapEntry<T> = Types.HeapEntry<T>

export type LiveRandom = LiveRandom.LiveRandom
export type LRUCache<K, V> = LRUCache.LRUCache<K, V>
export type MaxPriorityQueue<T> = MaxPriorityQueue.MaxPriorityQueue<T>
export type MinPriorityQueue<T> = MinPriorityQueue.MinPriorityQueue<T>
export type SimpleTrie = SimpleTrie.SimpleTrie
export type SortedArray<T> = SortedArray.SortedArray<T>
export type Utf8Trie = Utf8Trie.Utf8Trie

return DataStructures


type Serialize = (
	value: unknown,
	config: unknown,
	indentation: number,
	depth: number,
	references: unknown,
	printer: Serialize
) -> string
type Serializer = {
	serialize: Serialize,
	test: (value: unknown) -> boolean,
}
type JestConfiguration = {
	clearmocks: boolean?,
	displayName: {
		color: string,
		name: string,
	} | nil | string,
	projects: {Instance}?,
	rootDir: Instance?,
	setupFiles: {ModuleScript}?,
	setupFilesAfterEnv: {ModuleScript}?,
	slowTestThreshold: number?,
	snapshotFormat: {
		callToJSON: boolean?,
		escapeRegex: boolean?,
		escapeString: boolean?,
		highlight: boolean?,
		indent: number?,
		maxDepth: number?,
		maxWidth: number?,
		min: boolean?,
		printBasicPrototype: boolean?,
		printFunctionName: boolean?,
		printInstanceDefaults: boolean?,
		theme: {[string]: string}?,
	}?,
	snapshotSerializers: {Serializer}?,
	testFailureExitCode: number?,
	testMatch: {string}?,
	testPathIgnorePatterns: {string}?,
	testRegex: {{string} | string}?,
	testTimeout: number?,
	verbose: boolean?,
}

local JestConfiguration: JestConfiguration = {
	displayName = "DataStructures";
	testMatch = {"**/__tests__/*.test"};
}
return JestConfiguration


--!optimize 2
--!strict

--[[
MIT License

Copyright (c) 2024 BusyCityGuy

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

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

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

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TestService = game:GetService("TestService")
local Jest = require(TestService.UnitTesting.DevPackages.Jest)

print("Checking for ProcessService...")
local processServiceExists, ProcessService = pcall(function()
	local processServiceName = "ProcessService"
	return game:GetService(processServiceName)
end)
print(`ProcessService {processServiceExists and "exists" or "does not exist"}`)

print("Running jest tests cli...")
local status, result = Jest.runCLI(ReplicatedStorage.Packages.DataStructures, {
	all = true;
	ci = false;
	verbose = true;
}, {ReplicatedStorage.Packages.DataStructures}):awaitStatus()
print(`Jest tests completed with status {status}`)

if status == "Rejected" then
	print(result)
end

if status == "Resolved" and result.results.numFailedTestSuites == 0 and result.results.numFailedTests == 0 then
	if processServiceExists then
		ProcessService:ExitAsync(0)
	end
end

if processServiceExists then
	ProcessService:ExitAsync(1)
end

return false


import React from "@rbxts/react";

import { Counter } from "./counter";
import { Layer } from "./layer";

export function App() {
	return (
		<Layer>
			<Counter />
		</Layer>
	);
}


import React, { useState } from "@rbxts/react";
import { usePx } from "client/hooks/use-px";
import { fonts } from "client/utils/fonts";
import { palette } from "client/utils/palette";

import { Button } from "./button";

const COLORS = [palette.purple, palette.blue, palette.green, palette.yellow, palette.red];

export function Counter() {
	const px = usePx();
	const [count, setCount] = useState(0);
	const [colorIndex, setColorIndex] = useState(0);

	return (
		<Button
			onClick={() => {
				setCount(count + 1);
				setColorIndex((colorIndex + 1) % COLORS.size());
			}}
			font={fonts.inter.medium}
			text={`👆 Clicked ${count} times`}
			textSize={px(32)}
			textColor={palette.white}
			backgroundColor={COLORS[colorIndex]}
			size={new UDim2(0, px(320), 0, px(128))}
			position={new UDim2(0.5, 0, 0.5, 0)}
			anchorPoint={new Vector2(0.5, 0.5)}
		/>
	);
}


import React from "@rbxts/react";
import { IS_PLUGIN } from "shared/constants";

interface LayerProps {
	displayOrder?: number;
	children?: React.ReactNode;
}

export function Layer({ displayOrder, children }: LayerProps) {
	return IS_PLUGIN ? (
		<frame
			BackgroundTransparency={1}
			Size={new UDim2(1, 0, 1, 0)}
			Position={new UDim2(0, 0, 0, 0)}
			ZIndex={displayOrder}
		>
			{children}
		</frame>
	) : (
		<screengui
			DisplayOrder={displayOrder}
			IgnoreGuiInset
			ResetOnSpawn={false}
			ZIndexBehavior={Enum.ZIndexBehavior.Sibling}
		>
			{children}
		</screengui>
	);
}


import { RunService } from "@rbxts/services";

declare const _G: Record<string, unknown>;

if (RunService.IsStudio()) {
	_G.__DEV__ = true;
}


import { useEventListener } from "@rbxts/pretty-react-hooks";
import { createMotion, Motion, MotionGoal } from "@rbxts/ripple";
import { Binding, useBinding, useMemo } from "@rbxts/react";
import { RunService } from "@rbxts/services";

export function useMotion(initialValue: number): LuaTuple<[Binding<number>, Motion]>;

export function useMotion<T extends MotionGoal>(initialValue: T): LuaTuple<[Binding<T>, Motion<T>]>;

export function useMotion<T extends MotionGoal>(initialValue: T) {
	const motion = useMemo(() => {
		return createMotion(initialValue);
	}, []);

	const [binding, setValue] = useBinding(initialValue);

	useEventListener(RunService.Heartbeat, (delta) => {
		const value = motion.step(delta);

		if (value !== binding.getValue()) {
			setValue(value);
		}
	});

	return $tuple(binding, motion);
}


import { useCamera, useDebounceState, useEventListener } from "@rbxts/pretty-react-hooks";
import { useMemo } from "@rbxts/react";

interface ScaleFunction {
	/**
	 * Scales `pixels` based on the current viewport size and rounds the result.
	 */
	(pixels: number): number;
	/**
	 * Scales `pixels` and rounds the result to the nearest even number.
	 */
	even: (pixels: number) => number;
	/**
	 * Scales a number based on the current viewport size without rounding.
	 */
	scale: (percent: number) => number;
	/**
	 * Scales `pixels` and rounds the result down.
	 */
	floor: (pixels: number) => number;
	/**
	 * Scales `pixels` and rounds the result up.
	 */
	ceil: (pixels: number) => number;
}

const BASE_RESOLUTION = new Vector2(1280, 832);
const MIN_SCALE = 0.75;
const DOMINANT_AXIS = 0.5;

/**
 * @see https://discord.com/channels/476080952636997633/476080952636997635/1146857136358432900
 */
function calculateScale(viewport: Vector2) {
	const width = math.log(viewport.X / BASE_RESOLUTION.X, 2);
	const height = math.log(viewport.Y / BASE_RESOLUTION.Y, 2);
	const centered = width + (height - width) * DOMINANT_AXIS;

	return math.max(2 ** centered, MIN_SCALE);
}

export function usePx(): ScaleFunction {
	const camera = useCamera();

	const [scale, setScale] = useDebounceState(calculateScale(camera.ViewportSize), {
		wait: 0.2,
		leading: true,
	});

	useEventListener(camera.GetPropertyChangedSignal("ViewportSize"), () => {
		setScale(calculateScale(camera.ViewportSize));
	});

	return useMemo(() => {
		const api = {
			even: (value: number) => math.round(value * scale * 0.5) * 2,
			scale: (value: number) => value * scale,
			floor: (value: number) => math.floor(value * scale),
			ceil: (value: number) => math.ceil(value * scale),
		};

		setmetatable(api, {
			__call: (_, value) => math.round((value as number) * scale),
		});

		return api as ScaleFunction;
	}, [scale]);
}


import "./dev";

import React, { StrictMode } from "@rbxts/react";
import { createPortal, createRoot } from "@rbxts/react-roblox";
import { Players } from "@rbxts/services";

import { App } from "./components/app";

const root = createRoot(new Instance("Folder"));
const target = Players.LocalPlayer.WaitForChild("PlayerGui");

root.render(<StrictMode>{createPortal(<App />, target)}</StrictMode>);


import "../dev";

import { hoarcekat } from "@rbxts/pretty-react-hooks";
import React from "@rbxts/react";
import { App } from "client/components/app";

export = hoarcekat(() => {
	return <App />;
});


import { RunService } from "@rbxts/services";

export const IS_PLUGIN = RunService.IsStudio() && !RunService.IsRunning();


return {
	luau = {
		languagemode = "strict",
		lint = { ["*"] = true },
		linterrors = true,
		aliases = {
			batteries = "./Packages/lute/batteries",
			src = "./src",
			std = "~/.lute/typedefs/0.1.0/std",
		},
	},
}


-- Run with: lute run generate
-- Unicode data files live in unicode-data/ and are refreshed with: lute run fetch-data

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

local config = require("./lib/config")
local emitModule = require("./lib/emit-module")
local parseProperties = require("./lib/parse-properties").parseProperties
local parseTests = require("./lib/parse-tests").parseTestData

export type Range = {
	from: number,
	to: number,
	category: number,
}

local SURROGATE_START = 0xD800
local SURROGATE_END = 0xDFFF
local BYTES_PER_RANGE = 8
local CODEPOINT_MASK = 0x1FFFFF
local CATEGORY_SHIFT = 21

local GRAPHEME_CATEGORIES = {
	"Any",
	"CR",
	"Control",
	"Extend",
	"Extended_Pictographic",
	"L",
	"LF",
	"LV",
	"LVT",
	"Prepend",
	"Regional_Indicator",
	"SpacingMark",
	"T",
	"V",
	"ZWJ",
}

local WORD_CATEGORIES = {
	"Any",
	"ALetter",
	"CR",
	"Double_Quote",
	"Extend",
	"ExtendNumLet",
	"Format",
	"Hebrew_Letter",
	"Katakana",
	"LF",
	"MidLetter",
	"MidNum",
	"MidNumLet",
	"Newline",
	"Numeric",
	"Regional_Indicator",
	"Single_Quote",
	"WSegSpace",
	"ZWJ",
}

local SENTENCE_CATEGORIES = {
	"Any",
	"ATerm",
	"CR",
	"Close",
	"Extend",
	"Format",
	"LF",
	"Lower",
	"Numeric",
	"OLetter",
	"SContinue",
	"STerm",
	"Sep",
	"Sp",
	"Upper",
}

local ALPHABETIC_CATEGORIES = {
	"Any",
	"Alphabetic",
}

local NUMERIC_CATEGORIES = {
	"Any",
	"Numeric",
}

local INCB_CATEGORIES = {
	"Any",
	"Consonant",
	"Linker",
}

local function ensureDirectory(directory: string)
	if not fs.exists(directory) then
		fs.createDirectory(directory, { makeParents = true })
	end
end

local function readUnicodeFile(relativePath: string): string
	local filePath = path.join(config.UNICODE_DATA_DIR, relativePath)
	if not fs.exists(filePath) then
		error(`Missing Unicode data file: {filePath}. Run 'lute run fetch-data' to download.`)
	end

	return fs.readFileToString(filePath)
end

local function filterSurrogates(ranges: { { from: number, to: number } }): { { from: number, to: number } }
	local filtered = {}
	for _, range in ranges do
		if range.to < SURROGATE_START or range.from > SURROGATE_END then
			table.insert(filtered, range)
		else
			if range.from < SURROGATE_START then
				table.insert(filtered, { from = range.from, to = SURROGATE_START - 1 })
			end
			if range.to > SURROGATE_END then
				table.insert(filtered, { from = SURROGATE_END + 1, to = range.to })
			end
		end
	end

	return filtered
end

local function mergeRanges(ranges: { { from: number, to: number } }): { { from: number, to: number } }
	table.sort(ranges, function(a, b)
		if a.from == b.from then
			return a.to < b.to
		end

		return a.from < b.from
	end)

	local merged = {}
	for _, range in ranges do
		local last = merged[#merged]
		if last and range.from <= (last.to + 1) then
			if range.to > last.to then
				last.to = range.to
			end
		else
			table.insert(merged, { from = range.from, to = range.to })
		end
	end

	return merged
end

local function subtractRanges(
	source: { { from: number, to: number } },
	toRemove: { { from: number, to: number } }
): { { from: number, to: number } }
	if #toRemove == 0 then
		return source
	end

	local output = {}
	local removeIndex = 1

	for _, range in source do
		local start = range.from
		local finish = range.to

		while removeIndex <= #toRemove and toRemove[removeIndex].to < start do
			removeIndex += 1
		end

		local scanIndex = removeIndex
		while scanIndex <= #toRemove and toRemove[scanIndex].from <= finish do
			local removal = toRemove[scanIndex]
			if removal.from > start then
				table.insert(output, { from = start, to = removal.from - 1 })
			end

			start = math.max(start, removal.to + 1)
			if start > finish then
				break
			end

			scanIndex += 1
		end

		if start <= finish then
			table.insert(output, { from = start, to = finish })
		end
	end

	return output
end

local function buildRanges(
	props: { [string]: { { from: number, to: number } } },
	categories: { string },
	overrides: { name: string, ranges: { { from: number, to: number } } }?
): { Range }
	local output = {}

	for index, category in categories do
		if index == 1 then
			continue
		end

		local ranges = props[category]
		if ranges == nil then
			continue
		end

		local normalized = ranges
		if overrides and category ~= overrides.name then
			normalized = subtractRanges(normalized, overrides.ranges)
		end

		for _, range in normalized do
			table.insert(output, {
				from = range.from,
				to = range.to,
				category = index - 1,
			})
		end
	end

	return output
end

local function packRanges(ranges: { Range }): buffer
	table.sort(ranges, function(a, b)
		if a.from == b.from then
			return a.to < b.to
		end

		return a.from < b.from
	end)

	local out = buffer.create(#ranges * BYTES_PER_RANGE)
	for index, range in ranges do
		local offset = (index - 1) * BYTES_PER_RANGE
		local to = bit32.band(range.to, CODEPOINT_MASK)
		local toAndCategory = bit32.bor(to, bit32.lshift(range.category, CATEGORY_SHIFT))

		buffer.writeu32(out, offset, range.from)
		buffer.writeu32(out, offset + 4, toAndCategory)
	end

	return out
end

local function lookupCategory(bufferData: buffer, codepoint: number): number
	local rangeCount = buffer.len(bufferData) // BYTES_PER_RANGE
	local low = 0
	local high = rangeCount - 1

	while low <= high do
		local mid = (low + high) // 2
		local offset = mid * BYTES_PER_RANGE
		local from = buffer.readu32(bufferData, offset)
		local toAndCategory = buffer.readu32(bufferData, offset + 4)
		local to = bit32.band(toAndCategory, CODEPOINT_MASK)

		if codepoint < from then
			high = mid - 1
		elseif codepoint > to then
			low = mid + 1
		else
			return bit32.rshift(toAndCategory, CATEGORY_SHIFT)
		end
	end

	return 0
end

local function buildAsciiTable(bufferData: buffer, defaultCategory: number?): buffer
	local ascii = buffer.create(128)
	local fallback = defaultCategory or 0

	for codepoint = 0, 127 do
		local category = lookupCategory(bufferData, codepoint)
		if category == 0 then
			category = fallback
		end
		buffer.writeu8(ascii, codepoint, category)
	end

	return ascii
end

local function emitBreakModule(outputPath: string, categories: { string }, ranges: { Range })
	local rangeBuffer = packRanges(ranges)
	local asciiBuffer = buildAsciiTable(rangeBuffer)
	emitModule.emitDataModule(outputPath, {
		categories = categories,
		rangeBuffer = rangeBuffer,
		asciiBuffer = asciiBuffer,
	})
end

local function renderTestDataBlock(name: string, cases: { { input: string, expected: { string } } }): { string }
	local lines = { "\t" .. name .. " = {" }
	for _, entry in cases do
		local expectedParts = table.create(#entry.expected)
		for index, value in entry.expected do
			expectedParts[index] = string.format("%q", value)
		end

		table.insert(lines, "\t\t{")
		table.insert(lines, string.format("\t\t\tinput = %q,", entry.input))
		table.insert(lines, "\t\t\texpected = { " .. table.concat(expectedParts, ", ") .. " },")
		table.insert(lines, "\t\t},")
	end
	table.insert(lines, "\t},")

	return lines
end

local function emitTestData(
	outputPath: string,
	grapheme: { { input: string, expected: { string } } },
	word: { { input: string, expected: { string } } },
	sentence: { { input: string, expected: { string } } }
)
	local lines = {
		"-- This file is @generated by .lute/generate.luau. Do not edit.",
		"return {",
	}

	for _, line in renderTestDataBlock("grapheme", grapheme) do
		table.insert(lines, line)
	end
	for _, line in renderTestDataBlock("word", word) do
		table.insert(lines, line)
	end
	for _, line in renderTestDataBlock("sentence", sentence) do
		table.insert(lines, line)
	end

	table.insert(lines, "}")
	fs.writeStringToFile(outputPath, table.concat(lines, "\n"))
end

local function main()
	ensureDirectory(path.join("src", "generated"))
	ensureDirectory("tests")

	local graphemeData = readUnicodeFile("auxiliary/GraphemeBreakProperty.txt")
	local emojiData = readUnicodeFile("emoji/emoji-data.txt")
	local wordData = readUnicodeFile("auxiliary/WordBreakProperty.txt")
	local sentenceData = readUnicodeFile("auxiliary/SentenceBreakProperty.txt")
	local derivedCoreData = readUnicodeFile("DerivedCoreProperties.txt")
	local derivedGeneralData = readUnicodeFile("extracted/DerivedGeneralCategory.txt")

	local graphemeProps = parseProperties(graphemeData)
	local emojiProps = parseProperties(emojiData, { "Extended_Pictographic" })
	local wordProps = parseProperties(wordData)
	local sentenceProps = parseProperties(sentenceData)
	local derivedCoreProps = parseProperties(derivedCoreData, { "InCB=Consonant", "InCB=Linker", "Alphabetic" })
	local derivedGeneralProps = parseProperties(derivedGeneralData, { "Nd", "Nl", "No" })

	if graphemeProps.Control then
		graphemeProps.Control = filterSurrogates(graphemeProps.Control)
	end

	local extendedRanges: { { from: number, to: number } } = emojiProps.Extended_Pictographic or {}
	local graphemeRanges = buildRanges(graphemeProps, GRAPHEME_CATEGORIES, {
		name = "Extended_Pictographic",
		ranges = extendedRanges,
	})

	for _, range in extendedRanges do
		table.insert(graphemeRanges, {
			from = range.from,
			to = range.to,
			category = 4,
		})
	end

	emitBreakModule(path.join("src", "generated", "grapheme-data.luau"), GRAPHEME_CATEGORIES, graphemeRanges)

	local emojiRanges = buildRanges({ Extended_Pictographic = extendedRanges }, { "Any", "Extended_Pictographic" })
	emitBreakModule(path.join("src", "generated", "emoji-data.luau"), { "Any", "Extended_Pictographic" }, emojiRanges)

	local incbRanges = buildRanges({
		Consonant = derivedCoreProps.Consonant or {},
		Linker = derivedCoreProps.Linker or {},
	}, INCB_CATEGORIES)
	emitBreakModule(path.join("src", "generated", "incb-data.luau"), INCB_CATEGORIES, incbRanges)

	local alphabeticRanges = buildRanges({ Alphabetic = derivedCoreProps.Alphabetic or {} }, ALPHABETIC_CATEGORIES)
	emitBreakModule(path.join("src", "generated", "alphabetic-data.luau"), ALPHABETIC_CATEGORIES, alphabeticRanges)

	local numericSource = {}
	for _, key in { "Nd", "Nl", "No" } do
		local ranges = derivedGeneralProps[key]
		if ranges then
			for _, range in ranges do
				table.insert(numericSource, range)
			end
		end
	end

	local numericRanges: { Range } = {}
	for _, range in mergeRanges(numericSource) do
		table.insert(numericRanges, {
			from = range.from,
			to = range.to,
			category = 1,
		})
	end

	emitBreakModule(path.join("src", "generated", "numeric-data.luau"), NUMERIC_CATEGORIES, numericRanges)

	local wordRanges = buildRanges(wordProps, WORD_CATEGORIES)
	emitBreakModule(path.join("src", "generated", "word-data.luau"), WORD_CATEGORIES, wordRanges)

	local sentenceRanges = buildRanges(sentenceProps, SENTENCE_CATEGORIES)
	emitBreakModule(path.join("src", "generated", "sentence-data.luau"), SENTENCE_CATEGORIES, sentenceRanges)

	local graphemeTests = parseTests(readUnicodeFile("auxiliary/GraphemeBreakTest.txt"))
	local wordTests = parseTests(readUnicodeFile("auxiliary/WordBreakTest.txt"))
	local sentenceTests = parseTests(readUnicodeFile("auxiliary/SentenceBreakTest.txt"))

	emitTestData(path.join("tests", "test-data.luau"), graphemeTests, wordTests, sentenceTests)
end

main()


local UNICODE_VERSION = "17.0.0"

return {
	UNICODE_VERSION = UNICODE_VERSION,
	UNICODE_BASE_URL = "https://www.unicode.org/Public/" .. UNICODE_VERSION .. "/ucd/",
	UNICODE_DATA_DIR = "unicode-data",
	UNICODE_FILES = {
		"auxiliary/GraphemeBreakProperty.txt",
		"auxiliary/WordBreakProperty.txt",
		"auxiliary/SentenceBreakProperty.txt",
		"auxiliary/GraphemeBreakTest.txt",
		"auxiliary/WordBreakTest.txt",
		"auxiliary/SentenceBreakTest.txt",
		"DerivedCoreProperties.txt",
		"extracted/DerivedGeneralCategory.txt",
		"emoji/emoji-data.txt",
	},
}


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

local base64 = require("@src/base64")

type EmitOptions = {
	categories: { string },
	rangeBuffer: buffer,
	asciiBuffer: buffer,
}

local function encodeBuffer(data: buffer): string
	return buffer.tostring(base64.encode(data))
end

local function renderDataModule(options: EmitOptions): string
	local rangeBase64 = encodeBuffer(options.rangeBuffer)
	local asciiBase64 = encodeBuffer(options.asciiBuffer)

	local lines = {
		"-- This file was @generated by .lute/generate.luau. Do not edit.",
		'local base64 = require("../base64")',
		`local rangeData = base64.decode(buffer.fromstring("{rangeBase64}"))`,
		`local asciiData = base64.decode(buffer.fromstring("{asciiBase64}"))`,
		"",
		"local categories = table.freeze({",
	}

	for index, category in options.categories do
		table.insert(lines, `\t{category} = {index - 1},`)
	end

	table.insert(lines, "})")
	table.insert(lines, "")
	table.insert(lines, "return table.freeze({")
	table.insert(lines, "\tcategories = categories,")
	table.insert(lines, "\trangeData = rangeData,")
	table.insert(lines, "\tasciiData = asciiData,")
	table.insert(lines, "})")

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

local function emitDataModule(path: string, options: EmitOptions, writer: ((string, string) -> ())?): string
	local content = renderDataModule(options)
	if writer then
		writer(path, content)
	else
		fs.writeStringToFile(path, content)
	end

	return content
end

return {
	emitDataModule = emitDataModule,
	renderDataModule = renderDataModule,
}


local base64 = require("@src/base64")
local fs = require("@std/fs")
local segmenter = require("@src")
local wordData = require("@src/generated/word-data")

local BYTES_PER_RANGE = 8
local CODEPOINT_MASK = 0x1FFFFF
local CATEGORY_SHIFT = 21

local MAX_CODEPOINT = 0x10FFFF
local SURROGATE_START = 0xD800
local SURROGATE_END = 0xDFFF

type ExhaustiveOptions = {
	mode: "exhaustive",
	maxLen: number?,
}

type SweepOptions = {
	mode: "sweep",
}

type RandomOptions = {
	mode: "random",
	maxLen: number?,
	count: number?,
	seed: number?,
}

type GeneratorOptions = ExhaustiveOptions | SweepOptions | RandomOptions

local function encodeLine(value: string): string
	if value == "" then
		return "-"
	end

	return buffer.tostring(base64.encode(buffer.fromstring(value)))
end

local function decodeLine(value: string): string
	if value == "" or value == "-" then
		return ""
	end

	local decoded = base64.decode(buffer.fromstring(value))
	return buffer.tostring(decoded)
end

local function fromCodepoints(codepoints: { number }): string
	if #codepoints == 0 then
		return ""
	end

	return utf8.char(table.unpack(codepoints))
end

local function normalizeLine(line: string): string
	if string.byte(line, -1) == 13 then
		return string.sub(line, 1, -2)
	end

	return line
end

local function forEachLine(text: string, callback: (string) -> ()): ()
	for line in text:gmatch("[^\n]+") do
		callback(normalizeLine(line))
	end
end

local corpus = {
	"",
	"a",
	"0",
	"_",
	"foo_bar",
	"can't",
	"e.g.",
	"123,456",
	"a\r\nb",
	fromCodepoints({ 0x0061, 0x0301 }),
	fromCodepoints({ 0x0061, 0x0301, 0x0301 }),
	fromCodepoints({ 0x1F1FA, 0x1F1F8 }),
	fromCodepoints({ 0x1F469, 0x200D, 0x1F4BB }),
	fromCodepoints({ 0x000D, 0x000A }),
	fromCodepoints({ 0x0061, 0x200C, 0x0301 }),
}

local function emitValue(value: string, writeLine: (string) -> ())
	writeLine(encodeLine(value))
end

local function emitCorpus(writeLine: (string) -> ())
	for _, value in corpus do
		emitValue(value, writeLine)
	end
end

local function readRange(bufferData: buffer, index: number): (number, number, number)
	local offset = (index - 1) * BYTES_PER_RANGE
	local from = buffer.readu32(bufferData, offset)
	local packed = buffer.readu32(bufferData, offset + 4)
	local to = bit32.band(packed, CODEPOINT_MASK)
	local category = bit32.rshift(packed, CATEGORY_SHIFT)
	return from, to, category
end

local function buildCategoryIds(): { number }
	local ids = {}
	local seen = {}

	local rangeCount = buffer.len(wordData.rangeData) / BYTES_PER_RANGE
	for index = 1, rangeCount do
		local _, _, category = readRange(wordData.rangeData, index)
		if not seen[category] then
			seen[category] = true
			table.insert(ids, category)
		end
	end

	table.sort(ids, function(left, right)
		return left < right
	end)

	return ids
end

local function buildRepresentatives(categoryIds: { number }): ({ [number]: { number } }, { [number]: number })
	local firstByCat = {}
	local lastByCat = {}
	local asciiByCat = {}

	local rangeCount = buffer.len(wordData.rangeData) / BYTES_PER_RANGE
	for index = 1, rangeCount do
		local from, to, category = readRange(wordData.rangeData, index)

		if firstByCat[category] == nil then
			firstByCat[category] = from
		end
		lastByCat[category] = to

		if asciiByCat[category] == nil and from <= 0x7F and to >= 0x00 then
			asciiByCat[category] = math.max(from, 0x00)
		end
	end

	local representatives = {}
	local primary = {}

	for _, category in categoryIds do
		local list = {}
		local function add(value: number?)
			if value == nil then
				return
			end
			for _, existing in list do
				if existing == value then
					return
				end
			end
			table.insert(list, value)
		end

		add(firstByCat[category])
		add(asciiByCat[category])
		add(lastByCat[category])

		representatives[category] = list

		local selected = asciiByCat[category] or firstByCat[category]
		if selected == nil then
			error(`Missing representative for category {category}`)
		end
		primary[category] = selected
	end

	return representatives, primary
end

local function emitCategorySequences(
	categoryIds: { number },
	primary: { [number]: number },
	maxLen: number,
	writeLine: (string) -> ()
)
	local sequence = table.create(maxLen) :: { number }

	local function walk(depth: number, targetLen: number)
		if depth > targetLen then
			emitValue(utf8.char(table.unpack(sequence, 1, targetLen)), writeLine)
			return
		end

		for _, category in categoryIds do
			sequence[depth] = primary[category]
			walk(depth + 1, targetLen)
		end
	end

	for length = 1, maxLen do
		walk(1, length)
	end
end

local function emitSingleCodepointSweep(writeLine: (string) -> ())
	for codepoint = 0, MAX_CODEPOINT do
		if codepoint < SURROGATE_START or codepoint > SURROGATE_END then
			emitValue(utf8.char(codepoint), writeLine)
		end
	end
end

local function randomCodepoint(): number
	local roll = math.random()
	if roll < 0.7 then
		return math.random(0x20, 0x7E)
	end

	if roll < 0.8 then
		return math.random(0x00, 0x1F)
	end

	if roll < 0.95 then
		local cp = math.random(0x80, 0xFFFF)
		while cp >= SURROGATE_START and cp <= SURROGATE_END do
			cp = math.random(0x80, 0xFFFF)
		end
		return cp
	end

	return math.random(0x10000, MAX_CODEPOINT)
end

local function randomString(maxLen: number): string
	local length = math.random(0, maxLen)
	if length == 0 then
		return ""
	end

	local codepoints = table.create(length)
	for index = 1, length do
		codepoints[index] = randomCodepoint()
	end

	return utf8.char(table.unpack(codepoints))
end

local function generate(options: GeneratorOptions, writeLine: (string) -> ())
	local mode = options.mode
	if mode == "exhaustive" then
		local maxLen = (options :: ExhaustiveOptions).maxLen or 5
		local categoryIds = buildCategoryIds()
		local _, primary = buildRepresentatives(categoryIds)

		emitCorpus(writeLine)
		emitCategorySequences(categoryIds, primary, maxLen, writeLine)
	elseif mode == "sweep" then
		emitSingleCodepointSweep(writeLine)
	else
		local randomOptions = options :: RandomOptions
		local count = randomOptions.count or 1000
		local seed = randomOptions.seed or os.time()
		local maxLen = randomOptions.maxLen or 32

		math.randomseed(seed)

		emitCorpus(writeLine)
		for _ = 1, count do
			emitValue(randomString(maxLen), writeLine)
		end
	end
end

local function parseArgs(rawArgs: { string }): GeneratorOptions
	local args = table.clone(rawArgs)
	if args[1] == "--" then
		table.remove(args, 1)
	end

	local mode = args[1]
	if mode == "exhaustive" or mode == "sweep" or mode == "random" then
		table.remove(args, 1)
	else
		mode = "random"
	end

	if mode == "exhaustive" then
		return {
			mode = "exhaustive",
			maxLen = tonumber(args[1]) or 5,
		}
	end

	if mode == "sweep" then
		return {
			mode = "sweep",
		}
	end

	return {
		mode = "random",
		count = tonumber(args[1]) or 1000,
		seed = tonumber(args[2]) or os.time(),
		maxLen = tonumber(args[3]) or 32,
	}
end

local function formatSegments(segments: { { segment: string, index: number, isWordLike: boolean } }): string
	if #segments == 0 then
		return ""
	end

	local output = table.create(#segments)
	for index, entry in segments do
		local wordLike = entry.isWordLike and "1" or "0"
		local encoded = encodeLine(entry.segment)
		output[index] = `{entry.index}:{wordLike}:{encoded}`
	end

	return table.concat(output, "|")
end

local function processLine(line: string, writeLine: (string) -> ())
	local input = decodeLine(line)
	local segments = segmenter.words(input)
	local output = formatSegments(segments)
	writeLine(line .. "\t" .. output)
end

local function processText(text: string, writeLine: (string) -> ())
	forEachLine(text, function(line)
		processLine(line, writeLine)
	end)
end

local function processFile(inputPath: string, outputPath: string)
	local contents = fs.readFileToString(inputPath)
	local handle = fs.open(outputPath, "w+")
	processText(contents, function(line)
		fs.write(handle, line .. "\n")
	end)
	fs.close(handle)
end

return {
	decodeLine = decodeLine,
	encodeLine = encodeLine,
	generate = generate,
	parseArgs = parseArgs,
	processFile = processFile,
	processText = processText,
}


local function trim(value: string): string
	return (value:gsub("^%s+", ""):gsub("%s+$", ""))
end

local function parseRangeToken(token: string): (number | nil, number | nil)
	local rangeStart, rangeEnd = token:match("^(%x+)%.%.(%x+)$")
	if rangeStart == nil then
		rangeStart = token:match("^(%x+)$")
		rangeEnd = rangeStart
	end

	if rangeStart == nil or rangeEnd == nil then
		return nil, nil
	end

	return tonumber(rangeStart, 16), tonumber(rangeEnd, 16)
end

local function mergeRanges(ranges: { { from: number, to: number } }): { { from: number, to: number } }
	table.sort(ranges, function(a, b)
		if a.from == b.from then
			return a.to < b.to
		end

		return a.from < b.from
	end)

	local merged = {}
	for _, range in ranges do
		local last = merged[#merged]
		if last and range.from <= (last.to + 1) then
			if range.to > last.to then
				last.to = range.to
			end
		else
			table.insert(merged, { from = range.from, to = range.to })
		end
	end

	return merged
end

local function isInteresting(interesting: { string }?, propKey: string, propName: string): boolean
	if not interesting then
		return true
	end

	for _, name in interesting do
		if name == propKey or name == propName then
			return true
		end
	end

	return false
end

local function parseProperties(
	data: string,
	interestingProps: { string }?
): { [string]: { { from: number, to: number } } }
	local props = {}

	for rawLine in data:gmatch("[^\n]+") do
		local line = rawLine:match("^(.-)#") or rawLine
		line = trim(line)
		if line == "" then
			continue
		end

		local rangeToken, propToken, valueToken = line:match("^([^;]+);%s*([^;]+);?%s*([^;]*)")
		if rangeToken == nil or propToken == nil then
			continue
		end

		rangeToken = trim(rangeToken)
		propToken = trim(propToken)
		valueToken = trim(valueToken or "")

		local rangeStart, rangeEnd = parseRangeToken(rangeToken)
		if rangeStart == nil or rangeEnd == nil then
			continue
		end

		local propKey = propToken
		local propValue = propToken
		if valueToken and valueToken ~= "" then
			propKey = propToken .. "=" .. valueToken
			propValue = valueToken
		end

		if not isInteresting(interestingProps, propKey, propToken) then
			continue
		end

		props[propValue] = props[propValue] or {}
		table.insert(props[propValue], { from = rangeStart, to = rangeEnd })
	end

	for key, ranges in pairs(props) do
		props[key] = mergeRanges(ranges)
	end

	return props
end

return {
	parseProperties = parseProperties,
}


local function isSurrogate(codepoint: number): boolean
	return codepoint >= 0xD800 and codepoint <= 0xDFFF
end

local function codepointsToString(codepoints: { number }): string
	local parts = table.create(#codepoints)
	for index, codepoint in codepoints do
		parts[index] = utf8.char(codepoint)
	end

	return table.concat(parts)
end

local function parseTokens(data: string): { { input: string, expected: { string } } }
	local cases = {}

	for rawLine in data:gmatch("[^\n]+") do
		if not rawLine:match("^÷") then
			continue
		end

		local line = rawLine:match("^(.-)#") or rawLine
		line = line:gsub("^%s+", ""):gsub("%s+$", "")

		if line == "" then
			continue
		end

		local segments = {}
		local current = {}
		local started = false
		local skipCase = false

		for token in line:gmatch("%S+") do
			if token == "÷" then
				if started and #current > 0 then
					table.insert(segments, current)
					current = {}
				else
					started = true
				end
			elseif token == "×" then
				continue
			else
				local codepoint = tonumber(token, 16)
				if codepoint == nil or isSurrogate(codepoint) then
					skipCase = true
					break
				end
				table.insert(current, codepoint)
			end
		end

		if skipCase then
			continue
		end

		if #current > 0 then
			table.insert(segments, current)
		end

		if #segments == 0 then
			continue
		end

		local expected = table.create(#segments)
		for index, segment in segments do
			expected[index] = codepointsToString(segment)
		end

		local input = table.concat(expected)
		table.insert(cases, {
			input = input,
			expected = expected,
		})
	end

	return cases
end

return {
	parseTestData = parseTokens,
}