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

export type Path = string

local function getModifiedAt(path: Path): number
	local meta = fs.metadata(path)
	return assert(meta.modifiedAt).unixTimestampMillis
end

local function rebuild_item(options: {
	base: Path,
	out: Path,
	file: Path
})
	local basePath = `{options.base}/{options.file}`
	local outPath = `{options.out}/{options.file}`

	local baseMeta = fs.metadata(basePath)
	if not baseMeta.exists then
		local outMeta = fs.metadata(outPath)
		if outMeta.exists then
			fs.removeFile(outPath)
		end
		return
	end

	local outParent = string.match(outPath, "(.+)[/\\][^/\\]*$")
	if outParent then
		fs.writeDir(outParent)
	end

	local baseFile = fs.readFile(basePath)
	local outFile = string.gsub(
		baseFile,
		"(%-%-%-[^\n]*)\\ *(\r?\n)",
		"%1  %2"
	)

	fs.writeFile(outPath, outFile)
end

local function rebuild_directory(options: {
	base: Path,
	out: Path,
	dir: Path,
	watch: { [Path]: number }?,
})
	for _, child in fs.readDir(`{options.base}/{options.dir}`) do
		if fs.metadata(`{options.base}/{options.dir}/{child}`).kind == "dir" then
			rebuild_directory({
				base = options.base,
				out = options.out,
				dir = `{options.dir}/{child}`,
				watch = options.watch,
			})
		else
			rebuild_item({
				base = options.base,
				out = options.out,
				file = `{options.dir}/{child}`,
			})

			if options.watch then
				options.watch[`{options.dir}/{child}`] = getModifiedAt(`{options.base}/{options.dir}/{child}`)
			end
		end
	end
end

local function watch(items: { [Path]: number }, onChanged: (Path) -> ())
	local thread = task.spawn(function()
		while true do
			for path, timestamp in items do
				local meta = fs.metadata(path)
				if meta.exists and meta.modifiedAt.unixTimestampMillis > timestamp then
					onChanged(path)
				end
				task.wait(1/60)
			end
		end
	end)
	return function()
		task.cancel(thread)
	end
end

local function buildWatchlist(options: {
	base: Path,
	out: Path,
	dir: Path,
	watch: { [Path]: number },
})
	for _, child in fs.readDir(`{options.base}/{options.dir}`) do
		if fs.metadata(`{options.base}/{options.dir}/{child}`).kind == "dir" then
			buildWatchlist({
				base = options.base,
				out = options.out,
				dir = `{options.dir}/{child}`,
				watch = options.watch,
			})
		else
			options.watch[`{options.dir}/{child}`] = getModifiedAt(`{options.base}/{options.dir}/{child}`)
		end
	end
end

local function copy(options: {
	base: Path,
	out: Path,
	items: { Path },
	watch: { [Path]: number }?,
})
	for _, item in options.items do
		fs.copy(`{options.base}/{item}`, `{options.out}/{item}`, {
			overwrite = true,
		})

		if options.watch then
			if fs.metadata(`{options.base}/{item}`).kind == "file" then
				options.watch[item] = getModifiedAt(`{options.base}/{item}`)
			else
				buildWatchlist({
					base = options.base,
					out = options.out,
					dir = item,
					watch = options.watch,
				})
			end
		end
	end
end

local codeWatchList = {}
rebuild_directory({
	base = "src",
	out = "moonwave/src",
	dir = ".",
	watch = codeWatchList,
})

local copyWatchList = {}
copy({
	base = ".",
	out = "moonwave",
	items = {
		"pages",
		"CHANGELOG.md",
		"README.md",
		"moonwave.toml",
	},
	watch = copyWatchList,
})

if process.args[1] == "dev" then
	print("watching for changes to existing files...")
	watch(codeWatchList, function(filePath)
		print(`  rebuilding {filePath}`)
		rebuild_item({
			base = "src",
			out = "moonwave/src",
			file = filePath,
		})
	end)
	watch(copyWatchList, function(filePath)
		print(`  copying {filePath}`)
		copy({
			base = ".",
			out = "moonwave",
			items = {
				filePath,
			},
		})
	end)

	process.spawn("moonwave", process.args, {
		stdio = "inherit",
		cwd = "moonwave",
		shell = true,
	})
elseif process.args[1] == "build" then
	process.spawn("moonwave", process.args, {
		stdio = "inherit",
		cwd = "moonwave",
		shell = true,
	})
else
	warn(`unknown command: {process.args[1]}`)
end


--[[
	This is a simplified Roblox -> Lune converter specifically for GreenTea.
	I have chosen this approach over DarkLua and other because...
	1. String requires are coming to Roblox soon, which makes this less necessary.
	2. DarkLua and similar add a lot of complexity for something that is really
	   simple in this case due to GreenTea being such a small library.
]]

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

local function rewrite(path: string, process: (str: string) -> string)
	return fs.writeFile(path, process(fs.readFile(path)))
end

for _, name in fs.readDir("src") do
	rewrite(`src/{name}`, function(str)
		return str:gsub('require%(([^"].-)%)', function(inner)
			local target = inner:gsub("script%.Parent%.", "./"):gsub("script%.", "./")

			return `require("{target}")`
		end)
	end)
end

rewrite(`src-t-standalone-pesde/t.luau`, function(str)
	return str:gsub("require%(script.Parent.roblox_packages.GreenTea%)", 'require("./lune_packages/GreenTea")')
end)

rewrite("pesde.toml", function(str)
	local data = serde.decode("toml", str)
	data.target.environment = "lune"
	data.target.build_files = nil

	return serde.encode("toml", data, true)
end)

rewrite("src-t-standalone-pesde/pesde.toml", function(str)
	local data = serde.decode("toml", str)
	data.target.environment = "lune"
	data.target.build_files = nil

	return serde.encode("toml", data, true)
end)


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

local wally = serde.decode("toml", fs.readFile("wally.toml"))

local lastVersion = wally.package.version
local nextVersion = assert(process.args[1], "version required!\n\nusage:\n  lune set-version -- <version>")

assert(
	string.match(nextVersion, "^[0-9]+%.[0-9]+%.[0-9]+$")
		or string.match(nextVersion, "^[0-9]+%.[0-9]+%.[0-9]+%-[%w%._]+$")
		or string.match(nextVersion, "^[0-9]+%.[0-9]+%.[0-9]+%-[%w%._]+%+[%w%._]+$")
		or string.match(nextVersion, "^[0-9]+%.[0-9]+%.[0-9]+%+[%w%._]+$"),
	"version must be a semver"
)

local function sanitizePattern(str: string)
	return string.gsub(str, "([%.%-%*%+%?%%])", "%%%1")
end

local function replaceVersion(file: string, from: string, to: string)
	fs.writeFile(file, (string.gsub(fs.readFile(file), sanitizePattern(from), to)))
end

replaceVersion("README.md", `@{lastVersion}`, `@{nextVersion}`)
replaceVersion("README.md", `version = "^{lastVersion}"`, `version = "^{nextVersion}"`)

replaceVersion("wally.toml", `version = "{lastVersion}"`, `version = "{nextVersion}"`)
replaceVersion("src-t-standalone-wally/wally.toml", `version = "{lastVersion}"`, `version = "{nextVersion}"`)
replaceVersion("src-t-standalone-wally/wally.toml", `@={lastVersion}`, `@={nextVersion}`)

replaceVersion("pesde.toml", `version = "{lastVersion}"`, `version = "{nextVersion}"`)
replaceVersion("src-t-standalone-pesde/pesde.toml", `version = "{lastVersion}"`, `version = "{nextVersion}"`)
replaceVersion("src-t-standalone-pesde/pesde.toml", `version = "={lastVersion}"`, `version = "={nextVersion}"`)


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

local function exec(cmdline: string)
	local pieces = {}
	for piece in string.gmatch(" " .. cmdline, "%s+([^%s]+)") do
		table.insert(pieces, piece)
	end

	local program = table.remove(pieces, 1)
	assert(program ~= nil, "cmdline must include a program")
	local result = process.spawn(program, pieces, {
		stdio = "inherit",
	})
	if not result.ok then
		process.exit(result.code)
	end
end

exec("wally install")
exec("rojo sourcemap -o sourcemap.json ./test.project.json")
exec("wally-package-types --sourcemap sourcemap.json ./DevPackages/")
exec("wally-patch-package")


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

export type Cause = GreenTea.Cause
export type Type = GreenTea.Type
export type TuplePacked<T...> = GreenTea.TuplePacked<T...>

return GreenTea.t


local GreenTea = require(script.Parent.GreenTea)

export type Cause = GreenTea.Cause
export type Type = GreenTea.Type
export type TuplePacked<T...> = GreenTea.TuplePacked<T...>

return GreenTea.t


local Packages = game:GetService("ReplicatedStorage").Packages
local JestGlobals = require(Packages.Dev.JestGlobals)
local JestBenchmark = require(Packages.Dev.JestBenchmark)
local describe = JestGlobals.describe
local benchmark = JestBenchmark.benchmark

return function<T>(name: string, times: number, benchFunction: (args: T) -> (), setupFunction: ((runId: number) -> T)?)
	assert(typeof(name) == "string", "name must be a string")
	assert(typeof(times) == "number", "times must be a number")
	assert(typeof(benchFunction) == "function", "benchFunction must be a function")

	local benchArgs = {}

	if setupFunction then
		for runId = 1, times do
			benchArgs[runId] = setupFunction(runId)
		end
	end

	describe(name, function()
		benchmark(`{name} ({times} times)`, function()
			for runId = 1, times do
				benchFunction(benchArgs[runId])
			end
		end)
	end)
end


local Packages = game:GetService("ReplicatedStorage").Packages
local Dash = require(Packages.Dash)
local Cryo = require(Packages.Dev.Cryo)
local benchmark = require(script.Parent.benchmark)

local DICT_A = {
	a = 5,
	b = 10,
	cherry = 800,
}
local DICT_B = {
	apple = 50,
	cherry = 80,
	fig = 100,
}
local DICT_C = {
	banana = 10,
	cherry = 100,
	durian = 1000,
}

local BENCHMARK_ITERATIONS = 10_000

benchmark("Dash.join - 2 dictionaries", BENCHMARK_ITERATIONS, function(targetTable)
	Dash.join(targetTable, DICT_A)
end, function()
	return {}
end)

benchmark("Cryo.Dictionary.union - 2 dictionaries", BENCHMARK_ITERATIONS, function(targetTable)
	Cryo.Dictionary.union(targetTable, DICT_A)
end, function()
	return {}
end)

benchmark("Dash.join - 3 dictionaries", BENCHMARK_ITERATIONS, function(targetTable)
	Dash.join(targetTable, DICT_A, DICT_B)
end, function()
	return {}
end)

benchmark("Cryo.Dictionary.union - 3 dictionaries", BENCHMARK_ITERATIONS, function(targetTable)
	Cryo.Dictionary.union(targetTable, DICT_A)
	Cryo.Dictionary.union(targetTable, DICT_B)
end, function()
	return {}
end)

benchmark("Dash.join - 4 dictionaries", BENCHMARK_ITERATIONS, function(targetTable)
	Dash.join(targetTable, DICT_A, DICT_B, DICT_C)
end, function()
	return {}
end)

benchmark("Cryo.Dictionary.union - 4 dictionaries", BENCHMARK_ITERATIONS, function(targetTable)
	Cryo.Dictionary.union(targetTable, DICT_A)
	Cryo.Dictionary.union(targetTable, DICT_B)
	Cryo.Dictionary.union(targetTable, DICT_C)
end, function()
	return {}
end)


local Packages = game:GetService("ReplicatedStorage").Packages
local Dash = require(Packages.Dash)
local Cryo = require(Packages.Dev.Cryo)
local benchmark = require(script.Parent.benchmark)

local DICT_A = {
	a = 5,
	b = 10,
	cherry = 800,
}
local DICT_B = {
	apple = 50,
	cherry = 80,
	fig = 100,
}

local BENCHMARK_ITERATIONS = 10_000

benchmark("Dash.union", BENCHMARK_ITERATIONS, function()
	Dash.union(DICT_A, DICT_B)
end)

benchmark("Cryo.Dictionary.union", BENCHMARK_ITERATIONS, function()
	Cryo.Dictionary.union(DICT_A, DICT_B)
end)


local Dash = script.Parent
local Types = require(Dash.Types)
local class = require(Dash.class)
local format = require(Dash.format) :: <T...>(string, T...) -> string
local join = require(Dash.join)

--[=[
	Creates an error object with a specified name and message.

	In native Lua, errors can only be string values. At Roblox, we can take advantage of throwing error objects to provide structured information about problems that occur.

	The tags table stores serializable information about an error which can be provided when it is thrown, and later passed to a logging endpoint.

	Throwing an error instance captures its stack trace, avoiding the need to explicitly use xpcall.

	@usage In general, errors should not be used during normal control flow.
]=]
local Error = class("Error", function(name: string, message: string, tags: Types.Table?)
	return {
		name = name,
		message = message or "An error occurred",
		tags = tags or {},
	}
end)

function Error:toString(): string
	return format("{}: {}\n{}", self.name, format(self.message, self.tags), self.stack)
end

--[=[
	Returns a new error instance containing the tags provided joined to any existing tags of the current error instance.

	@param tags Optional table of tags to join with existing tags.
	@return A new Error instance with combined tags.
]=]
function Error:joinTags(tags: Types.Table?): Error
	return Error.new(self.name, self.message, join(self.tags, tags or {}))
end

--[=[
	Throws an error.

	The stack of the error is captured and stored.

	If additional tags are provided, a new error is created with the joined tags of this instance.

	@param tags Optional table of additional tags to include.
]=]
function Error:throw(tags: Types.Table?)
	local instance = self:joinTags(tags)
	instance.stack = debug.traceback()
	error(instance)
end

-- TODO Luau: Define class types automatically
export type Error = typeof(Error.new("", ""))

return Error


local Symbol = require(script.Parent.Symbol)
local None = Symbol.new("None")

--[=[
	A symbol representing nothing, that can be used in place of nil as a key or value of a table,
	where nil is illegal.

	Utility functions can check for the None symbol and treat it like a nil value.

	@usage Use cases include:
	1. Creating an ordered list with undefined values in it
	2. Creating a map with a key pointing to a nil value
]=]
return None


local Dash = script.Parent
local class = require(Dash.class)

--[=[
	Creates a symbol with a specified name. Upper snake-case is recommended as the symbol is a constant, unless you are linking the symbol conceptually to a different string.

	Symbols are useful when you want a value that isn't equal to any other type, for example if you want to store a unique property on an object that won't be accidentally accessed with a simple string lookup.

	@param name The name for the symbol.
	@return A new Symbol instance.
	@example
	```luau
		local CHEESE = Symbol.new("CHEESE")
		local FAKE_CHEESE = Symbol.new("CHEESE")
		print(CHEESE == CHEESE) --> true
		print(CHEESE == FAKE_CHEESE) --> false
		print(tostring(CHEESE)) --> "Symbol.new(CHEESE)"
	```
]=]
local Symbol = class("Symbol", function(name: string)
	return {
		name = name,
	}
end)

function Symbol:toString(): string
	return ("Symbol(%s)"):format(self.name)
end

-- TODO Luau: Define class types automatically
export type Symbol = typeof(Symbol.new(""))

return Symbol


-- A table with keys of type _Key_ and values of type _Value_
export type Map<Key, Value> = { [Key]: Value }
-- A table with keys of a fixed type _Key_ and a boolean value representing membership of the set (default is false)
export type Set<Key> = { [Key]: boolean }
-- A table of any type
export type Table = { [any]: any }
-- A class has a constructor returning an instance of _Object_ type
export type Class<Object> = {
	new: () -> Object,
}
-- Represents a function which takes any arguments and returns any value
export type AnyFunction = (...any) -> any

return {}


type AllHandler = (any, any) -> boolean

local defaultHandler: AllHandler = function(value)
	return value
end

--[=[
	Returns `true` if all entries in the _input_ table satisfy the predicate _handler_.

	If no _handler_ is provided, the default predicate returns the truthiness of each value.
	For array inputs, elements are visited in order 1..n;

	@param input The table to test.
	@param handler Optional predicate called as `(value, key)`
	@return boolean -- `true` only if every call to _handler_ returns truthy; otherwise `false`
]=]
local function all(input: {}, handler: AllHandler?): boolean
	local allHandler = handler or defaultHandler
	for key, value in input do
		if not allHandler(value, key) then
			return false
		end
	end
	return true
end

return all


local Dash = script.Parent
local None = require(Dash.None)
local forEachArgs = require(Dash.forEachArgs)
local forEach = require(Dash.forEach)
local insert = table.insert

--[=[
	Adds new elements to the _target_ array from subsequent array arguments in left-to-right order.

	Arguments which are `nil` or `Dash.None` are skipped.

	@param target The array to append to.
	@param ... One or more arrays of items to append.
	@return The mutated _target_ array.
]=]
local function append<T>(target: { T }, ...: { T }?): { T }
	forEachArgs(function(list: { T }?)
		if list == None or list == nil then
			return
		else
			forEach(list, function(value: T)
				insert(target, value)
			end)
		end
	end, ...)
	return target
end

return append


local Dash = script.Parent

--[=[
	Performs a simple equality check and throws an error if _left_ is not equal to _right_.

	The formatted error message can be customized, which by default provides a serialization of both inputs using `Dash.pretty`.

	The `left` and `right` values are available to be referenced in the formatted message.

	@error AssertError -- when values differ; includes `left` and `right` in the error tags.
]=]
local function assertEqual(left: unknown, right: unknown, formattedErrorMessage: string?)
	if left ~= right then
		local Error = require(Dash.Error)
		local TypeError =
			Error.new("AssertError", formattedErrorMessage or `Left "{left}" does not equal right "{right}"`)
		TypeError:throw({
			left = left,
			right = right,
		})
	end
end

return assertEqual


local Dash = script.Parent
local None = require(Dash.None)
local Types = require(Dash.Types)
local forEach = require(Dash.forEach)
local forEachArgs = require(Dash.forEachArgs)

--[=[
	Adds new key/value pairs to the _target_ table from subsequent table arguments in left-to-right order.

	The `None` symbol can be used to remove existing elements in _target_. `nil` or `None` arguments are skipped.

	@param target The table to assign into.
	@param ... One or more tables whose entries will be assigned to _target_.
	@return The mutated _target_ table.

	@example
	```luau
		local characters = {
			Frodo = {
				name = "Frodo Baggins",
				team = "blue"
			},
			Boromir = {
				score = 5
			}
		}
		local otherCharacters = {
			Frodo = {
				team = "red",
				score = 10
			},
			Bilbo = {
				team = "yellow",
			},
			Boromir = {
				score = {1, 2, 3}
			}
		}
		local result = assign(characters, otherCharacters)
		print(result) --> {
			Frodo = {
				team = "red",
				score = 10
			},
			Bilbo = {
				team = "yellow"
			},
			Boromir = {
				score = {1, 2, 3}
			}
		}
	```
]=]
local function assign(target: {}, ...: {}?): Types.Table
	-- Iterate through the varags in order
	forEachArgs(function(input: {}?)
		-- Ignore items which are not defined
		if input == nil or input == None then
			return
		else
			-- Iterate through each key of the input and assign to target at the same key
			forEach(input, function(value, key)
				if value == None then
					target[key] = nil
				else
					target[key] = value
				end
			end)
		end
	end, ...)
	return target
end

return assign


-- TODO (AleksandrSl 18/08/2025): Implementation is valid, but it's impossible to cast nil into T...
--!nocheck
--[=[
	Returns a stateful iterator that yields all elements from the first iterator, then the next, until exhausted.

	@param ... One or more stateful iterator functions to chain.
	@return A stateful iterator that returns `nil` when all iterators are exhausted.

	@example
	```luau
		for x in chain(ipairs({1, 2}), ipairs({3})) do print(x) end --> 1 2 3
	```
]=]
local function chain<T...>(...: () -> T...): () -> T...
	local iterators = { ... }
	local iterators_count = #iterators
	local current_iterator_index = 1
	return function(): T...
		while current_iterator_index <= iterators_count do
			local iterator_function = iterators[current_iterator_index]
			local result = { iterator_function() }
			if result[1] ~= nil then
				return table.unpack(result)
			else
				current_iterator_index = current_iterator_index + 1
			end
		end
		return nil
	end
end

return chain


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

local function throwNotImplemented(tags: Types.Table)
	local Error = require(Dash.Error) :: { [string]: any } -- see type Error
	local NotImplemented =
		Error.new("NotImplemented", [[The method "{methodName}" is not implemented on the class "{className}"]])
	NotImplemented:throw(tags)
end

export type Constructor<T...> = (T...) -> Types.Table

local defaultConstructor: Constructor<> = function()
	return {}
end

--[=[
	Creates a class called _name_ with the specified _constructor_. The constructor should return a plain table which will be turned into an instance of _Class_ from a call to `Class.new(...)`.

	@param name The class name used for string representation and error messages.
	@param constructor A function that returns the initial instance table; defaults to an empty table.
	@return A new Class table with constructor `new` and overridable metamethods.

	@example
	```luau
		-- Create a simple Vehicle class
		local Vehicle = class("Vehicle", function(wheelCount: number) return
			{
				speed = 0,
				wheelCount = wheelCount
			}
		end)
		function Vehicle:drive(speed)
			self.speed = speed
		end
		-- Create a car instance
		local car = Vehicle.new(4)
		car.wheelCount --> 4
		car.speed --> 0
		-- Drive the car
		car:drive(10)
		car.speed --> 10
	```

	@usage When using Dash classes, private fields should be prefixed with `_` to avoid accidental access.
	@usage A private field should only be accessed by a method of the class itself, though Rodash does not restrict this in code.
	@usage Public fields are recommended when there is no complex access logic e.g. `position.x`
]=]
local function class<T...>(name: string, constructor: Constructor<T...>?)
	local classConstructor = constructor or defaultConstructor
	local Class = {
		name = name,
	}
	--[=[
		Return a new instance of the class, passing any arguments to the specified constructor.
		@example
			local Car = class("Car", function(speed)
				return {
					speed = speed
				}
			end)
			local car = Car.new(5)
			pretty(car) --> 'Car {speed = 5}'
	]=]
	function Class.new(...)
		local instance = classConstructor(...)
		setmetatable(instance, {
			__index = Class,
			__tostring = Class.toString,
			__eq = Class.equals,
			__lt = Class.__lt,
			__le = Class.__le,
			__add = Class.__add,
			__sub = Class.__sub,
			__mul = Class.__mul,
			__div = Class.__div,
			__mod = Class.__mod,
		})
		instance.Class = Class
		instance:_init(...)
		return instance
	end
	--[=[
		Run after the instance has been properly initialized, allowing methods on the instance to
		be used.
		@example
			local Vehicle = dash.class("Vehicle", function(wheelCount) return
				{
					speed = 0,
					wheelCount = wheelCount
				}
			end)
			-- Let's define a static private function to generate a unique id for each vehicle.
			function Vehicle._getNextId()
				Vehicle._nextId = Vehicle._nextId + 1
				return Vehicle._nextId
			end
			Vehicle._nextId = 0
			-- A general purpose init function may call other helper methods
			function Vehicle:_init()
				self._id = self:_generateId()
			end
			-- Assign an id to the new instance
			function Vehicle:_generateId()
				return format("#{}: {} wheels", Vehicle._getNextId(), self.wheelCount)
			end
			-- Return the id if the instance is represented as a string
			function Vehicle:toString()
				return self._id
			end
			local car = Vehicle.new(4)
			tostring(car) --> "#1: 4 wheels"
	]=]
	function Class:_init() end

	--[=[
		Returns `true` if _value_ is an instance of _Class_ or any sub-class.
		@example
			local Vehicle = dash.class("Vehicle", function(wheelCount) return
				{
					speed = 0,
					wheelCount = wheelCount
				}
			end)
			local Car = Vehicle:extend("Vehicle", function()
				return Vehicle.constructor(4)
			end)
			local car = Car.new()
			car.isInstance(Car) --> true
			car.isInstance(Vehicle) --> true
			car.isInstance(Bike) --> false
	]=]
	function Class.isInstance(value)
		local ok, isInstance = pcall(function()
			local metatable = getmetatable(value)
			while metatable do
				if metatable.__index == Class then
					return true
				end
				metatable = getmetatable(metatable.__index)
			end
			return false
		end)
		return ok and isInstance
	end

	--[=[
		Create a subclass of _Class_ with a new name _className_ that inherits the metatable of _Class_,
		optionally overriding the constructor with _classConstructor_ and providing additional _decorators_.
		The super-constructor can be accessed with `Class.constructor`.
		Super methods can be accessed using `Class.methodName` and should be called with self.
		@example
			local Vehicle = dash.class("Vehicle", function(wheelCount) return
				{
					speed = 0,
					wheelCount = wheelCount
				}
			end)
			-- Let's define a static private function to generate a unique id for each vehicle.
			function Vehicle._getNextId()
				Vehicle._nextId = Vehicle._nextId + 1
				return Vehicle._nextId
			end
			Vehicle._nextId = 0
			-- A general purpose init function may call other helper methods
			function Vehicle:_init()
				self.id = self:_generateId()
			end
			-- Assign an id to the new instance
			function Vehicle:_generateId()
				return dash.format("#{}: {} wheels", Vehicle._getNextId(), self.wheelCount)
			end
			-- Let's make a Car class which has a special way to generate ids
			local Car = Vehicle:extend("Vehicle", function()
				return Vehicle.constructor(4)
			end)
			-- Uses the super method to generate a car-specific id
			function Car:_generateId()
				self.id = dash.format("Car {}", Vehicle._generateId(self))
			end
			local car = Car.new()
			car.id --> "Car #1: 4 wheels"
	]=]
	function Class:extend(subClassName: string, subClassConstructor)
		local SubClass = class(subClassName, subClassConstructor or Class.new)
		setmetatable(SubClass, { __index = self })
		return SubClass
	end

	--[=[
		Return a string representation of the instance. By default this is the _name_ field (or the
		Class name if this is not defined), but the method can be overridden.
		@example
			local Car = class("Car", function(name)
				return {
					name = name
				}
			end)

			local car = Car.new()
			car:toString() --> 'Car'
			tostring(car) --> 'Car'
			print("Hello " .. car) -->> Hello Car
			local bob = Car.new("Bob")
			bob:toString() --> 'Bob'
			tostring(bob) --> 'Bob'
			print("Hello " .. bob) -->> Hello Bob
		@example
			local NamedCar = class("NamedCar", function(name)
				return {
					name = name
				}
			end)
			function NamedCar:toString()
				return "Car called " .. self.name
			end
			local bob = NamedCar.new("Bob")
			bob:toString() --> 'Car called Bob'
			tostring(bob) --> 'Car called Bob'
			print("Hello " .. bob) -->> Hello Car called Bob
	]=]
	function Class:toString()
		return self.name
	end

	--[=[
		Returns `true` if `self` is considered equal to _other_. This replaces the `==` operator
		on instances of this class, and can be overridden to provide a custom implementation.
	]=]
	function Class:equals(other)
		return rawequal(self, other)
	end

	--[=[
		Returns `true` if `self` is considered less than  _other_. This replaces the `<` operator
		on instances of this class, and can be overridden to provide a custom implementation.
	]=]
	function Class:__lt(_)
		throwNotImplemented({
			methodName = "__lt",
			className = name,
		})
	end

	--[=[
		Returns `true` if `self` is considered less than or equal to _other_. This replaces the
		`<=` operator on instances of this class, and can be overridden to provide a custom
		implementation.
	]=]
	function Class:__le(_)
		throwNotImplemented({
			methodName = "__le",
			className = name,
		})
	end

	function Class:__add()
		throwNotImplemented({
			methodName = "__add",
			className = name,
		})
	end
	function Class:__sub()
		throwNotImplemented({
			methodName = "__sub",
			className = name,
		})
	end
	function Class:__mul()
		throwNotImplemented({
			methodName = "__mul",
			className = name,
		})
	end
	function Class:__div()
		throwNotImplemented({
			methodName = "__div",
			className = name,
		})
	end
	function Class:__mod()
		throwNotImplemented({
			methodName = "__mod",
			className = name,
		})
	end

	return Class
end

return class


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

-- TODO (AleksandrSl 18/08/2025): Try to type again with a solver V2. Solver V1 infers types weirdly,
--  so the functions are barely usable without type casts
export type CollectHandler<Key, Value, NewKey, NewValue> = (key: Key, value: Value) -> (NewKey?, NewValue?)

--[=[
	Returns a new table by iterating over _input_ and calling _handler_ for each `(key, value)`.

	The _handler_ should return a `(newKey, newValue)` pair to insert into the result, or `nil` to skip.

	@return A new table containing only the entries for which _handler_ returned a non-`nil` key.
	@see `Dash.collectArray` to build arrays while optionally skipping values.
]=]
local function collect(input: {}, handler: CollectHandler<any, any, any, any>): Types.Table
	local result = {}
	for key, child in input do
		local outputKey, outputValue = handler(key, child)
		if outputKey ~= nil then
			result[outputKey] = outputValue
		end
	end
	return result
end

return collect


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

local insert = table.insert

-- TODO (AleksandrSl 18/08/2025): Try to type again with a solver V2. Solver V1 infers types weirdly,
--  so the functions are barely usable without type casts
type CollectHandler<Key, Value, NewValue> = (key: Key, value: Value) -> NewValue?

--[=[
	Returns a new array by iterating over _input_ and calling _handler_ for each `(key, value)`.

	The _handler_ should return a new value to be pushed onto the end of the result array, or `nil` to skip.

	@return A new array containing only the values returned by _handler_ that were not `nil`.
	@see `Dash.map` to transform values without filtering out `nil`.
]=]
local function collectArray<Key, Value, NewValue>(input: {}, handler: CollectHandler<Key, Value, NewValue>): { NewValue }
	local result = {}
	for key, child in input :: Types.Table do
		local outputValue = handler(key, child)
		if outputValue ~= nil then
			insert(result, outputValue)
		end
	end
	return result
end

return collectArray


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

type CollectHandler<Key, Value, NewValue> = (key: Key, value: Value) -> NewValue?

--[=[
	Returns a new set built from iterating over _input_ and calling _handler_ for each `(key, value)`.

	If _handler_ is not provided, the values of _input_ are used directly. Values for which the _handler_ returns `nil` are skipped.

	@param input The table to iterate over.
	@param handler Optional function called as `(key, value)` for each entry.
	@return A new set containing each non-`nil` value returned by _handler_ (or each value of _input_ if no handler).
	@see `Dash.collectArray` to collect into an array while preserving insertion order for arrays.
]=]
local function collectSet<Key, Value, NewValue>(input: {}, handler: CollectHandler<Key, Value, NewValue>?): Types.Set<Value | NewValue>
	local result: Types.Set<Value | NewValue> = {}
	for key, child in input :: Types.Table do
		local outputValue
		if handler == nil then
			outputValue = child
		else
			outputValue = handler(key, child)
		end
		if outputValue ~= nil then
			result[outputValue] = true
		end
	end
	return result
end

return collectSet


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

--[=[
	Returns a shallow copy of the _input_ table.

	Only the top level is copied; nested tables are copied by reference.

	@param input The table to copy.
	@return A new table with the same keys and values as _input_.
	@see `Dash.assign` to copy properties into an existing table.

	@deprecated -- Use `table.clone` instead
]=]
local function copy(input: Types.Table): Types.Table
	return table.clone(input)
end

return copy


type CountHandler<Key, Value> = (Value, Key) -> boolean

local defaultHandler = function()
	return true
end

--[=[
	Returns the number of elements in _input_, optionally counting only those for which _handler_ returns true on the `(value, key)` pair.

	If _handler_ is not provided, all elements are counted. Iteration order over tables is unordered.

	@param input The table to count elements from.
	@param handler Optional function called as `(value, key)` for each element; return `true` to count the element.
	@return The count of elements that match the handler (or total count if no handler).

	@example
	```luau
		Dash.count({1, 2, 3, 4, 5}) --> 5
	```

	@example
	```luau
		-- Count the number of ids that start with 1
		Dash.count(
			{[12] = true, [24] = true, [153] = true, [199] = true},
			function(value, id) return tostring(id):sub(1, 1) == "1" end
		) --> 3
	```

	@example
	```luau
		-- Count the numbers divisible by 5
		Dash.count(
			{1, 1, 2, 3, 5, 8, 13, 21, 34, 55},
			function(num) return num % 5 == 0 end
		) --> 2
	```
]=]
local function count(input: {}, handler: CountHandler<any, any>?): number
	local counter = 0
	local countHandler: CountHandler<any, any> = handler or defaultHandler
	for key, value in input do
		if countHandler(value, key) then
			counter += 1
		end
	end
	return counter
end

return count


local Dash = script.Parent
local Types = require(Dash.Types)
local includes = require(Dash.includes)
local keys = require(Dash.keys)

local sort = table.sort

export type Cycles = {
	-- A set of tables which were visited recursively
	visited: Types.Set<Types.Table>,
	-- A map from table to unique index in visit order
	refs: Types.Map<Types.Table, number>,
	-- The number to use for the next unique table visited
	nextRef: number,
	-- An array of keys which should not be visited
	omit: { any },
}

local function getDefaultCycles(): Cycles
	return {
		visited = {},
		refs = {},
		nextRef = 1,
		omit = {},
	}
end

-- TODO Luau: Improve type inference to a point that this definition does not produce so many type errors
-- TYPED: local function cycles(value: any, depth: number?, initialCycles: Cycles?): Cycles
--[=[
	Returns a Cycles object describing repeated table references found by recursively walking _input_.

	Works with cyclic structures. Keys are visited in a stable order to preserve reference numbering.
	If _depth_ is provided, traversal stops when it reaches -1; pass an existing _initialCycles_ to continue accumulation across calls.

	@param input Any value to inspect; only tables contribute cycles.
	@param depth Optional maximum recursion depth (decremented on each recursive step).
	@param initialCycles An existing Cycles object to update; if omitted a new one is created.
	@return A Cycles object for tables; `nil` for non-table inputs.
]=]
local function cycles(input: any, depth: number?, initialCycles: any): Cycles?
	if depth == -1 then
		return initialCycles
	end

	if typeof(input) == "table" then
		local childCycles = initialCycles or getDefaultCycles()

		if childCycles.visited[input] then
			-- We have already visited the table, so check if it has a reference
			if not childCycles.refs[input] then
				-- If not, create one as it is present at least twice
				childCycles.refs[input] = childCycles.nextRef
				childCycles.nextRef += 1
			end
			return nil
		else
			-- We haven't yet visited the table, so recurse
			childCycles.visited[input] = true
			-- Visit in order to preserve reference consistency
			local inputKeys = keys(input)
			sort(inputKeys, function(left, right)
				if typeof(left) == "number" and typeof(right) == "number" then
					return left < right
				else
					return tostring(left) < tostring(right)
				end
			end)
			for _, key in ipairs(inputKeys) do
				local value = input[key]
				if includes(childCycles.omit, key) then
					-- Don't visit omitted keys
					continue
				end
				-- TODO Luau: support type narrowring with "and"
				-- TYPED: cycles(key, depth and depth - 1 or nil, childCycles)
				-- TYPED: cycles(value, depth and depth - 1 or nil, childCycles)
				-- Recurse through both the keys and values of the table
				if depth then
					cycles(key, depth - 1, childCycles)
					cycles(value, depth - 1, childCycles)
				else
					cycles(key, nil, childCycles)
					cycles(value, nil, childCycles)
				end
			end
		end
		return childCycles
	else
		-- Non-tables do not have cycles
		return nil
	end
end

return cycles


local Dash = script.Parent
local assign = require(Dash.assign)

type DebounceOptions = {
	leading: boolean?,
	trailing: boolean?,
}

type DebounceOptionsInternal = {
	leading: boolean,
	trailing: boolean,
}

-- `& (...any) -> ...any` in the function type is a funky way to mimick `T extends function`
type AnyVoidFunction = (...any) -> ()
type Debounced<T> = T & AnyVoidFunction

--[=[
	Creates and returns a new debounced version of the passed function which postpones execution until after
	_wait_ seconds have elapsed since the last invocation.

	@param func The function to debounce.
	@param wait The number of seconds to delay.
	@param options Optional debounce options:
		- leading: boolean? (if true, call at the start of the delay; default false)
		- trailing: boolean? (if true, call at the end of the delay; default true)
	@return The new debounced function.
]=]

local function debounce<T>(func: T & AnyVoidFunction, wait: number, options: DebounceOptions?): Debounced<T>
	local defaultOptions: DebounceOptionsInternal = {
		leading = false,
		trailing = true,
	}

	local resolvedOptions: DebounceOptionsInternal = assign(defaultOptions, options)

	local callId = 0
	local scheduledThread: thread? = nil
	local isLeadingCalled = false
	local fn = function() end

	if resolvedOptions.trailing then
		fn = function(currentCallId: number, shouldCallLeading: boolean, ...)
			if callId == currentCallId then
				-- Reset leading flag for next cycle
				isLeadingCalled = false
				-- Only call if this wasn't already called by leading
				if not shouldCallLeading then
					(func :: AnyVoidFunction)(...)
				end
			end
		end
	-- If trailing is disabled, reset leading flag after delay
	elseif resolvedOptions.leading then
		fn = function(currentCallId: number)
			if callId == currentCallId then
				isLeadingCalled = false
			end
		end
	end

	return function(...)
		callId += 1
		local currentCallId = callId
		local shouldCallLeading = resolvedOptions.leading and not isLeadingCalled

		-- Clear existing timeout
		if scheduledThread then
			task.cancel(scheduledThread)
			scheduledThread = nil
		end

		-- Call leading if this is the first call and leading is enabled
		if shouldCallLeading then
			isLeadingCalled = true;
			(func :: AnyVoidFunction)(...)
		end

		-- Set up trailing call if enabled
		scheduledThread = task.delay(wait, fn, currentCallId, shouldCallLeading, ...)
	end :: Debounced<T>
end

return debounce


--[=[
	Checks whether _input_ ends with the string _suffix_.

	@return `true` if _input_ ends with _suffix_; otherwise `false`.
	@example
	```luau
	endsWith("Fun Roblox Games", "Games") --> true
	```
	@example
	```luau
	endsWith("Bad Roblox Memes", "Games") --> false
	```
]=]
local function endsWith(input: string, suffix: string): boolean
	return input:sub(-suffix:len()) == suffix
end

return endsWith


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

export type FilterHandler<Key, Value> = (Value, Key) -> boolean

--[=[
	Returns a new array containing the elements of the _input_ table for which _handler_ returns truthy.

	For an array input, the order of elements is preserved in the output.
	For a Map/table input, elements are visited in an arbitrary order.

	@param input The table to filter.
	@param handler Function called as `(value, key)` for each entry; return truthy to include the value.
	@return A new array with only the values for which _handler_ returned truthy.
]=]
local function filter(input: {}, handler: FilterHandler<any, any>): Types.Table
	local result = {}
	for index, child in input do
		if handler(child, index) then
			table.insert(result, child)
		end
	end
	return result
end

return filter


export type FindHandler<Key, Value> = (Value, Key) -> boolean

--[=[
	Returns the first element in the _input_ table that satisfies the _handler_ predicate.

	For an array input, the first matching element in order 1..n is returned.
	For a Map input, an arbitrary matching element is returned.

	@param input The table to search.
	@param handler Function called as `(value, key)` for each entry; return `true` to select the value.
	@return The first matching value, or `nil` if no entries satisfy the condition.
]=]
local function find(input: {}, handler: FindHandler<any, any>): any?
	for key, child in input do
		if handler(child, key) then
			return child
		end
	end
	return nil
end

return find


export type FindHandler<Value> = (Value, number) -> boolean

--[=[
	Returns the index of the first element in the _input_ array that satisfies the _handler_ predicate.

	@param input The array to search.
	@param handler Function called as `(value, index)` for each element; return `true` to select the index.
	@return The index of the first matching element, or `nil` if no elements satisfy the condition.
]=]
local function findIndex<Value>(input: { Value }, handler: FindHandler<Value>): number?
	for key, child in ipairs(input) do
		if handler(child, key) then
			return key
		end
	end
	return nil
end

return findIndex


local Dash = script.Parent
local append = require(Dash.append)
local forEach = require(Dash.forEach)

-- TODO Luau: Support function generics
--[=[
	Flattens the input array by a single level.

	Outputs a new array of elements merged from the _input_ array arguments in left-to-right order.

	@param input The array of arrays to flatten.
	@return A new array with all elements from nested arrays merged at the top level.
]=]
local function flat(input: { any }): { any }
	local result = {}
	forEach(input, function(childArray: { any })
		append(result, childArray)
	end)
	return result
end

return flat


export type ForEachHandler<Key, Value> = (Value, Key) -> ()

--[=[
	Iterates through the elements of the _input_ table and calls _handler_ for each entry.

	For an array input, the elements are visited in order 1..n.
	For a Map input, the keys are visited in an arbitrary order.

	@param input The table to iterate over.
	@param handler Function called as `(value, key)` for each entry.
]=]
local function forEach(input: {}, handler: ForEachHandler<any, any>)
	for key, value in input do
		handler(value, key)
	end
end

return forEach


export type ForEachArgsHandler<Value> = (Value, number) -> ()

--[=[
	Iterates through the tail arguments in order, including nil values up to the argument list length.

	@param handler Function called as `(value, index)` for each argument.
	@param ... Variable arguments to iterate over.
]=]
local function forEachArgs<Value>(handler: ForEachArgsHandler<Value>, ...: Value)
	for index = 1, select("#", ...) do
		handler(select(index, ...), index)
	end
end

return forEachArgs


local Dash = script.Parent
local formatValue = require(Dash.formatValue)
local splitOn = require(Dash.splitOn)
local startsWith = require(Dash.startsWith)

local concat = table.concat
local insert = table.insert

--[=[
	Returns the _format_ string with placeholders `{...}` substituted with readable representations of the subsequent arguments.
	This function is a simpler & more powerful version of `string.format`, inspired by `format!` in Rust.

	* `{}` formats and prints the next argument using `:format()` if available, or a suitable default representation depending on its type.
	* `{blah}` formats and prints the key "blah" of the 1st argument
	* `{2}` formats and prints the 2nd argument.
	* `{#2}` prints the length of the 2nd argument.

	Display parameters can be combined after a `:` in the curly braces. Any format parameters used in `string.format` can be used here, along with these extras:
	* `{:?}` formats any value using `pretty`.
	* `{:#?}` formats any value using multiline `pretty`.

	@param formatString The format string with placeholders.
	@param ... Arguments to substitute into the format string.
	@return The formatted string with placeholders replaced.
	@example
	```luau
		local props = {"teeth", "claws", "whiskers", "tail"}
		format("{:?} is in {:#?}", "whiskers", props)
		-> '"whiskers" is in {"teeth", "claws", "whiskers", "tail"}'
	```
	@example
	```luau
		format("The time is {:02}:{:02}", 2, 4) -> "The time is 02:04"
	```
	@example
	```luau
		format("The color blue is #{:06X}", 255) -> "The color blue is #0000FF"
	```
	@usage Escape `{` with `{{` and `}` similarly with `}}`.
	@usage See [https://developer.roblox.com/articles/Format-String](https://developer.roblox.com/articles/Format-String) for complete list of formatting options and further use cases.
]=]
-- TODO (AleksandrSl 18/08/2025): Check how it works, since splitOn returns only one value
local function format(formatString: string, ...)
	local args = { ... }
	local argIndex = 1
	local texts, subs = splitOn(formatString, "{[^{}]*}")
	local result = {}
	-- Iterate through possible curly-brace matches, ignoring escaped and substituting valid ones
	for i, text in texts do
		local unescaped = text:gsub("{{", "{"):gsub("}}", "}")
		insert(result, unescaped)
		local placeholder = subs[i] and subs[i]:sub(2, -2)
		if placeholder then
			-- Ensure that the curly braces have not been escaped
			local escapeMatch = text:gmatch("{+$")()
			local isEscaped = escapeMatch and #escapeMatch % 2 == 1
			if not isEscaped then
				-- Split the placeholder into left & right parts pivoting on the central ":"
				local placeholderSplit = splitOn(placeholder, ":")
				local isLength = startsWith(placeholderSplit[1], "#")
				local argString = isLength and placeholderSplit[1]:sub(2) or placeholderSplit[1]
				local nextIndex = tonumber(argString)
				local displayString = placeholderSplit[2]
				local arg = "nil"
				if nextIndex then
					-- Return the next argument
					arg = args[nextIndex]
				elseif argString:len() > 0 then
					-- Print a child key of the 1st argument
					local argChild = args[1] and args[1][argString]
					if argChild ~= nil then
						arg = argChild
					end
				else
					arg = args[argIndex]
					argIndex = argIndex + 1
				end
				if isLength then
					arg = #arg
				end
				-- Format the selected value
				insert(result, formatValue(arg, displayString or ""))
			else
				local unescapedSub = placeholder
				insert(result, unescapedSub)
			end
		end
	end
	return concat(result, "")
end

return format


local Dash = script.Parent

--[=[
	Formats a specific _value_ using the specified _displayString_.

	@param value The value to format.
	@param displayString Optional display string specifying the format.
	@return The formatted string representation of the value.
	@example
	```luau
		formatValue(255, "06X") --> "0000FF"
	```
	@example
	```luau
		formatValue(255.5) --> "255.5"
	```
	@see `format` for a full description of valid display strings.
]=]
local function formatValue(value: any, displayString: string?): string
	local _displayString = displayString or ""
	-- Inline require to prevent infinite require cycle
	local displayTypeStart, displayTypeEnd = _displayString:find("[A-Za-z#?]+")
	if displayTypeStart then
		local displayType = _displayString:sub(displayTypeStart, displayTypeEnd)
		local formatAsString = "%"
			.. _displayString:sub(1, displayTypeStart - 1)
			.. _displayString:sub(displayTypeEnd + 1)
			.. "s"
		-- Pretty print values
		local pretty = require(Dash.pretty)
		if displayType == "#?" then
			-- Multiline print a value
			return formatAsString:format(pretty(value, { multiline = true }))
		elseif displayType == "?" then
			-- Inspect a value
			return formatAsString:format(pretty(value))
		end
		return ("%" .. _displayString):format(value)
	else
		local displayType = "s"
		if type(value) == "number" then
			-- Correctly display floats or integers
			local _, fraction = math.modf(value)
			displayType = fraction == 0 and "d" or "f"
		end
		return ("%" .. _displayString .. displayType):format(tostring(value))
	end
end

return formatValue


local Dash = script.Parent
local Error = require(Dash.Error)
local format = require(Dash.format)

local ReadonlyKey =
	Error.new("ReadonlyKey", "Attempted to write to readonly key {key:?} of frozen object {objectName:?}")
local MissingKey = Error.new("MissingKey", "Attempted to read missing key {key:?} of frozen object {objectName:?}")

--[=[
	Returns a new read-only view of _object_ which prevents any values from being changed.

	@param name The name of the object for improved error message readability.
	@param object The object to freeze, must be a table.
	@param throwIfMissing If `true` then access to a missing key will also throw.
	@return A frozen proxy object that prevents modifications.

	@note
	Unfortunately you cannot iterate using `pairs` or `ipairs` on frozen objects because Luau doesn't support defining these custom iterators in metatables.

	@example
	```luau
		local drink = freeze("Ice Cream", {
			flavor = "mint",
			topping = "sprinkles"
		}, true)
		print(drink.flavor) --> "mint"
		drink.flavor = "vanilla"
		--!> ReadonlyKey: Attempt to write to readonly key "flavor" (a string) of frozen object "Ice Cream"`
		print(drink.syrup) --> nil
		--!> `MissingKey: Attempt to read missing key "syrup" (a string) of frozen object "Ice Cream"`
	```

	@deprecated -- Use `table.freeze` instead
]=]
local function freeze<T>(objectName: string, object: T, throwIfMissing: boolean?): T
	-- We create a proxy so that the underlying object is not affected
	local proxy = {}
	---- TODO (AleksandrSl 06/08/2025): Remove once we can add constraints to generic
	assert(
		type(object) == "table",
		`Attempted to call Dash.freeze with argument #2 of type {type(object)}, but it should be table `
	)
	setmetatable(proxy, {
		__index = function(_, key: any)
			local value = object[key]
			if value == nil and throwIfMissing then
				-- Tried to read a key which isn't present in the underlying object
				MissingKey:throw({
					key = key,
					objectName = objectName,
				})
			end
			return value
		end,
		__newindex = function(_, key: any)
			-- Tried to write to any key
			ReadonlyKey:throw({
				key = key,
				objectName = objectName,
			})
		end,
		__len = function()
			return #object
		end,
		__tostring = function()
			return format("Frozen({})", objectName)
		end,
		__call = function(_, ...)
			-- TODO Luau: Gated check for if a function has a __call value
			local callable: any = object
			return callable(...)
		end,
	})
	-- If we ever able to types this naturally, feel free to drop the cast.
	return (proxy :: unknown) :: T
end

return freeze


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

type FrequenciesHandler<Key, Value, NewKey> = (Value, Key) -> NewKey

--[=[
	Returns a map counting the frequency of unique values in the _input_ table.

	If no _handler_ is provided, counts occurrences of each unique value.
	If a _handler_ is provided, counts occurrences of each unique mapped key.

	@param input The table to count frequencies from.
	@param handler Optional function called as `(value, key)` to map values to keys before counting.
	@return A map with keys as unique values/mapped keys and values as their counts.
	@example
	```luau
		Dash.frequencies({"Red", "Black", "Orange", "Red"}) --> {["Black"] = 1, ["Orange"] = 1, ["Red"] = 2}
	```
	@example
	```luau
		-- Count colors by neutral vs non-neutral
		function toNeutral(color)
			return if Dash.includes({ "Black", "Gray", "White" }, color) then "Neutral" else "Non-neutral"
		end
		Dash.frequencies({ "Black", "Orange", "Blue", "Gray", "White" }, function(color)
			return toNeutral(color)
		end) --> {["Neutral"] = 3, ["Non-neutral"] = 2}
	```
]=]
local function frequencies(input: {}, handler: FrequenciesHandler<any, any, any>?): Types.Table
	return reduce(input, function(acc, value, key)
		local newKey = if handler then handler(value, key) else value
		if acc[newKey] == nil then
			acc[newKey] = 1
		else
			acc[newKey] += 1
		end
		return acc
	end, {})
end

return frequencies


--[=[
	Returns the value at _path_ from _object_. If resolution yields `nil`, _defaultValue_ is returned instead.

	@param object The table to query.
	@param path An array of keys to traverse on _object_.
	@param defaultValue The value to return when the resolved value is `nil`.
	@return The resolved value at _path_, or _defaultValue_ if any step is missing or `nil`.

	@example
	```luau
		get({ a = 1 }, { "a" }) --> 1
	```
	@example
	```luau
		get({ a = { b = 2 }}, { "a", "b" }) --> 2
	```
	@example
	```luau
		get({ a = { b = 2 }}, { "a", "c" }, 5) --> 5
	```
]=]
local function get<T>(object: { [any]: any }, path: { any }, defaultValue: T?): T?
	if object == nil then
		return defaultValue
	end

	local current = object

	for _, key in path do
		if type(current) ~= "table" then
			return defaultValue
		end

		current = current[key]

		if current == nil then
			return defaultValue
		end
	end

	return (current :: unknown) :: T?
end

return get


export type GetValueHandler<Key, Value> = ({ [Key]: Value }, Key) -> Value

--[=[
	Returns the value at _key_ from the _input_ table.

	If the key is missing, calls _getValue_ to compute a value, stores it on _input_, and returns it.

	@param input The table to read from (and potentially write to).
	@param key The key to look up.
	@param getValue A function that computes and returns the value when the key is missing.
	@return The existing value at _key_, or the newly computed value if it was missing.
]=]
local function getOrSet<Key, Value>(input: {}, key: Key, getValue: GetValueHandler<Key, Value>): Value
	if input[key] == nil then
		input[key] = getValue(input, key)
	end
	return input[key]
end

return getOrSet


--[=[
	Returns the input parameters unchanged.

	Can be used to make it clear that a handler returns its inputs unchanged.

	@param ... The input parameters to return.
	@return The input parameters unchanged.
]=]
local function identity<T...>(...: T...): T...
	return ...
end

return identity


--[=[
	Returns `true` if the _item_ exists as a value in the _input_ table.

	A nil _item_ will always return `false`.

	@param input The table to search in.
	@param item The value to search for.
	@return `true` if the item is found, `false` otherwise.
]=]
local function includes(input: {}, item: any?): boolean
	if item == nil then
		return false
	end
	for _, child in input do
		if child == item then
			return true
		end
	end
	return false
end

return includes


local Types = require(script.Types)

export type Map<Key, Value> = Types.Map<Key, Value>
export type Set<Key> = Types.Set<Key>
export type Table = Types.Table
export type Class<Object> = Types.Class<Object>
export type AnyFunction = Types.AnyFunction

local Dash = {
	all = require(script.all),
	append = require(script.append),
	assertEqual = require(script.assertEqual),
	assign = require(script.assign),
	chain = require(script.chain),
	class = require(script.class),
	collect = require(script.collect),
	collectArray = require(script.collectArray),
	collectSet = require(script.collectSet),
	copy = require(script.copy),
	count = require(script.count),
	cycles = require(script.cycles),
	debounce = require(script.debounce),
	endsWith = require(script.endsWith),
	Error = require(script.Error),
	filter = require(script.filter),
	find = require(script.find),
	findIndex = require(script.findIndex),
	flat = require(script.flat),
	forEach = require(script.forEach),
	forEachArgs = require(script.forEachArgs),
	format = require(script.format),
	formatValue = require(script.formatValue),
	freeze = require(script.freeze),
	frequencies = require(script.frequencies),
	get = require(script.get),
	getOrSet = require(script.getOrSet),
	groupBy = require(script.groupBy),
	identity = require(script.identity),
	includes = require(script.includes),
	isCallable = require(script.isCallable),
	isLowercase = require(script.isLowercase),
	isUppercase = require(script.isUppercase),
	iterable = require(script.iterable),
	iterator = require(script.iterator),
	join = require(script.join),
	joinArrays = require(script.joinArrays),
	joinDeep = require(script.joinDeep),
	keyBy = require(script.keyBy),
	keys = require(script.keys),
	last = require(script.last),
	leftPad = require(script.leftPad),
	map = require(script.map),
	mapFirst = require(script.mapFirst),
	mapLast = require(script.mapLast),
	mapNotNil = require(script.mapNotNil),
	mapOne = require(script.mapOne),
	max = require(script.max),
	memoize = require(script.memoize),
	min = require(script.min),
	None = require(script.None),
	noop = require(script.noop),
	omit = require(script.omit),
	pick = require(script.pick),
	pipe = require(script.pipe),
	pretty = require(script.pretty),
	product = require(script.product),
	reduce = require(script.reduce),
	reverse = require(script.reverse),
	rightPad = require(script.rightPad),
	shallowEqual = require(script.shallowEqual),
	slice = require(script.slice),
	some = require(script.some),
	splitOn = require(script.splitOn),
	startsWith = require(script.startsWith),
	sum = require(script.sum),
	Symbol = require(script.Symbol),
	throttle = require(script.throttle),
	trim = require(script.trim),
	Types = require(script.Types),
	union = require(script.union),
	values = require(script.values),
	zip = require(script.zip),
}

return table.freeze(Dash)


--[=[
	Returns `true` if the value can be called i.e. you can write `value(...)`.

	@param value The value to check if it's callable.
	@return `true` if the value is callable, `false` otherwise.
]=]
local function isCallable<T>(value: T): boolean
	return type(value) == "function"
		or (type(value) == "table" and getmetatable(value) and getmetatable(value).__call ~= nil)
		or false
end

return isCallable


local Dash = script.Parent
local assertEqual = require(Dash.assertEqual)

--[=[
	Returns `true` if the first character of _input_ is a lower-case character.

	Throws if the _input_ is not a string or it is the empty string.

	Our current version of Lua unfortunately does not support upper or lower-case detection outside the english alphabet. This function has been implemented to return the expected result once this has been corrected.

	@param input The string to check.
	@return `true` if the first character is lowercase, `false` otherwise.
	@error AssertError when input is an empty string.
]=]
local function isLowercase(input: string): boolean
	assertEqual(#input > 0, true, [[Attempted to call Dash.isLowercase with an empty string]])
	local firstLetter = input:sub(1, 1)
	return firstLetter == firstLetter:lower()
end
return isLowercase


local Dash = script.Parent
local assertEqual = require(Dash.assertEqual)

--[=[
	Returns `true` if the first character of _input_ is an upper-case character.

	Throws if the _input_ is not a string or it is the empty string.

	Our current version of Lua unfortunately does not support upper or lower-case detection outside the english alphabet. This function has been implemented to return the expected result once this has been corrected.

	@param input The string to check.
	@return `true` if the first character is uppercase, `false` otherwise.
	@error AssertError when input is an empty string.
]=]
local function isUppercase(input: string)
	assertEqual(#input > 0, true, [[Attempted to call Dash.isUppercase with an empty string]])
	local firstLetter = input:sub(1, 1)
	return firstLetter == firstLetter:upper()
end
return isUppercase


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

--[=[
	Creates a stateful iterator for the _input_ table, first visiting ordered numeric keys 1..n and then the remaining unordered keys in any order.

	@param input The table to create an iterator for.
	@return A stateful iterator function that yields `(key, value)` pairs.
	@see `Dash.iterator` for an iterator that can iterate over an iterable.
]=]
local function iterable(input: {}): Types.AnyFunction
	local currentIndex = 1
	local inOrderedKeys = true
	local currentKey
	local iterateFn
	iterateFn = function()
		if inOrderedKeys then
			local value = input[currentIndex]
			if value == nil then
				inOrderedKeys = false
			else
				local index = currentIndex
				currentIndex += 1
				return index :: number?, value
			end
		end
		while true do
			currentKey = next(input, currentKey)
			-- Don't re-visit ordered keys 1..n
			if
				typeof(currentKey) == "number"
				and currentKey > 0
				and currentKey < currentIndex
				and currentKey % 1 == 0
			then
				continue
			end
			if currentKey == nil then
				return nil
			else
				return currentKey, input[currentKey]
			end
		end
	end
	return iterateFn
end

return iterable


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

--[=[
	Returns an iterator for _input_: `ipairs` when it's a non-empty array, otherwise `pairs` for tables; if _input_ is a function, it is returned as-is.

	This function can be used to build behavior that iterates over both arrays and Maps.

	@return A stateful iterator function suitable for use in a generic for; may return `nil` to terminate.
	@see `Dash.iterable` if you want to iterate over a Table with numeric but un-ordered keys.
	@deprecated use generalized iteration instead.
]=]
local function iterator(input: {} | Types.AnyFunction)
	if typeof(input) == "function" then
		return input
	elseif typeof(input) == "table" then
		if #input > 0 then
			return ipairs(input)
		else
			return pairs(input)
		end
	else
		return nil
	end
end

return iterator


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

--[=[
	Returns a new Map by merging all keys from the provided Map arguments in left-to-right order.

	The `None` symbol can be used to remove existing elements.

	@param ... Any number of tables to merge.
	@return A new Map containing the merged keys and values.
]=]
local function join(...: {}?): Types.Table
	return assign({}, ...)
end

return join


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

--[=[
	Returns a new array containing the joined elements of the arrays passed.

	@param ... Any number of arrays to join.
	@return A new array containing the joined elements.
]=]
local function joinArrays(...: {}?): Types.Table
	local arrays = { ... }
	local joinedArray = {}
	for _, array in arrays do
		if array then
			table.move(array, 1, #array, #joinedArray + 1, joinedArray)
		end
	end
	return joinedArray
end

return joinArrays


local Dash = script.Parent
local None = require(Dash.None)
local Types = require(Dash.Types)
local forEach = require(Dash.forEach)

-- TODO Luau: Support typing varargs
-- TODO Luau: Support function generics
--[=[
	Creates a shallow clone of the _source_ map, and copies the values from the _delta_ map by key, like the join utility.

	However, if any of the values are tables themselves, the joinDeep function is called recursively to produce a new table at the specified key.

	The purpose of this function is to merge nested immutable data using as few table creation operations as possible, making it appropriate for updating state in a reducer.

	The None symbol can be used to remove an existing value.

	@param source The source table to clone and merge into.
	@param delta The delta table containing changes to apply.
	@return A new table with deep-merged values from source and delta.
]=]
local function joinDeep(source: {}, delta: {}): Types.Table
	local result = table.clone(source)
	-- Iterate through each key of the input and assign to target at the same key
	forEach(delta, function(value, key)
		if typeof(source[key]) == "table" and typeof(value) == "table" then
			-- Only merge tables
			result[key] = joinDeep(source[key], value)
		elseif value == None then
			-- Remove none values
			result[key] = nil
		else
			result[key] = value
		end
	end)
	return result
end

return joinDeep


local insert = table.insert

--[=[
	Returns an array of the keys in the _input_ table.

	If the input is an array, ordering is preserved.
	If the input is a Map, elements are returned in an arbitrary order.

	@param input The table to extract keys from.
	@return An array containing all keys from the input table.
]=]
local function keys(input: {})
	local result = {}
	for key, _ in input do
		insert(result, key)
	end
	return result
end

return keys


export type FindHandler<Value> = (Value, number) -> boolean

--[=[
	Returns the last element in the _input_ array that satisfies the _handler_ predicate.

	If no _handler_ is provided, returns the last element of the array.
	If no entries satisfy the condition, returns `nil`.

	@param input The array to search.
	@param handler Optional function called as `(value, index)` for each element; return `true` to select the value.
	@return The last matching value, or `nil` if no elements satisfy the condition.
]=]
local function last<Value>(input: { Value }, handler: FindHandler<Value>?): Value?
	for index = #input, 1, -1 do
		local child = input[index]
		if not handler then
			return child
		end
		if handler and handler(child, index) then
			return child
		end
	end
	return nil
end

return last


--[=[
	Makes a string of _length_ from _input_ by repeating characters from _prefix_ at the start of the string.

	@param input The string to pad.
	@param length The desired total length of the result.
	@param prefix The character(s) to use for padding; defaults to a single space.
	@return A new string padded to the specified length.
	@example
	```luau
		leftPad("toast", 6) --> " toast"
	```
	@example
	```luau
		leftPad("2", 2, "0") --> "02"
	```
	@example
	```luau
		leftPad("toast", 10, ":)") --> ":):):toast"
	```
]=]
local function leftPad(input: string, length: number, prefix: string?): string
	local definedPrefix = prefix or " "

	local padLength = length - input:len()
	local remainder = padLength % definedPrefix:len()
	local repetitions = (padLength - remainder) / definedPrefix:len()
	return string.rep(definedPrefix or " ", repetitions) .. definedPrefix:sub(1, remainder) .. input
end

return leftPad


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

export type MapHandler<Key, Value, NewValue> = (Value, Key) -> NewValue

--[=[
	Returns a new table by applying the _handler_ to each element of _input_.

	For an array input, the elements are visited in order 1..n.
	For a Map input, the elements are visited in an arbitrary order.

	Values returned by _handler_ must be defined.

	@param input The table to iterate over.
	@param handler Function called as `(value, key)` for each entry.
	@return A new table with the same keys but values replaced by handler results.
	@see `Dash.collectArray` if you want to return nil values.
]=]
local function map(input: {}, handler: MapHandler<any, any, any>): Types.Table
	local result = {}
	for key, child in input do
		local value = handler(child, key)
		assertEqual(value == nil, false, [[Returned nil from a Dash.map handler]])
		result[key] = value
	end
	return result
end

return map


export type MapHandler<Value, NewValue> = (Value, number) -> NewValue?

--[=[
	Iterates through the elements of the _input_ array in order 1..n and returns the first non-nil value from the _handler_.

	If all values returned by the _handler_ are `nil`, `nil` is returned.

	@param input The array to iterate over.
	@param handler Function called as `(value, index)` for each element.
	@return The first non-nil value returned by the handler, or `nil` if all return `nil`.
]=]
local function mapFirst<Value, NewValue>(input: { Value }, handler: MapHandler<Value, NewValue>): NewValue?
	for index, child in ipairs(input) do
		local output = handler(child, index)
		if output ~= nil then
			return output
		end
	end
	return nil
end

return mapFirst


export type MapHandler<Value, NewValue> = (Value, number) -> NewValue?

--[=[
	Iterates through the elements of the _input_ array in reverse order n..1 and returns the first non-nil value from the _handler_.

	If all values returned by the _handler_ are `nil`, `nil` is returned.

	@param input The array to iterate over.
	@param handler Function called as `(value, index)` for each element.
	@return The first non-nil value returned by the handler, or `nil` if all return `nil`.
]=]
local function mapLast<Value, NewValue>(input: { Value }, handler: MapHandler<Value, NewValue>): NewValue?
	for key = #input, 1, -1 do
		local child = input[key]
		local output = handler(child, key)
		if output ~= nil then
			return output
		end
	end
	return nil
end

return mapLast


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

export type MapHandler<Key, Value, NewValue> = (Value, Key) -> NewValue

--[=[
	Returns a new table by applying the _handler_ to each element of _input_.

	For a Map input, the elements are visited in an arbitrary order.

	If a nil value is returned from the handler, the associated key is _not_ added
	to the new table.

	@param input The table to iterate over.
	@param handler Function called as `(value, key)` for each entry.
	@return A new table with the same keys but values replaced by handler results.
	Nil results are deleted from the table.
]=]
local function mapNotNil(input: {}, handler: MapHandler<any, any, any>): Types.Table
	local result = {}
	for key, child in input do
		local value = handler(child, key)
		if value ~= nil then
			result[key] = value
		end
	end
	return result
end

return mapNotNil


export type MapHandler<Key, Value, NewValue> = (Value, Key) -> NewValue?

--[=[
	Iterates through the elements of the _input_ table in no particular order and returns the first non-nil value.

	If a _handler_ is provided, returns the first non-nil value returned by the handler.
	If no _handler_ is provided, returns the first value visited.
	If all values are `nil`, `nil` is returned.

	@param input The table to iterate over.
	@param handler Optional function called as `(value, key)` for each element.
	@return The first non-nil value, or `nil` if all values are `nil`.
]=]
local function mapOne(input: {}, handler: MapHandler<any, any, any>?): any?
	for key, child in input do
		local output
		if handler then
			output = handler(child, key)
		else
			output = child
		end
		if output ~= nil then
			return output
		end
	end
	return nil
end

return mapOne


local Dash = script.Parent
local reduce = require(Dash.reduce)

-- Return true if a > b
type MaxComparator = (any, any) -> boolean
type MaxHandler = (any, any) -> any

local defaultComparator = function(a: number | string, b: number | string)
	return a > b
end

--[=[
	Returns the maximum value in the _input_ table. By default, this returns the greatest value in the table as defined by the `>` operator.

	If a _handler_ is provided, elements will first be transformed by `handler(value, key)` before being used for comparison.

	Elements will be compared using the _comparator_ if it is provided. The comparator should take two elements and return `true` if the first one is greater than the second one, and `false` otherwise.

	If the provided _input_ has no elements, returns `nil`.

	@param input The table to find the maximum value from.
	@param handler Optional function called as `(value, key)` to transform values before comparison.
	@param comparator Optional function called as `(a, b)` to compare two values.
	@return The maximum value, or `nil` if the input is empty.
	@example
	```luau
		Dash.max({1, 2, 3}) --> 3
	```
	@example
	```luau
		Dash.max({"1", "2", "10"}) --> "2"
	```
	@example
	```luau
		Dash.max({1, 2, 3}, function(num) return -num end) --> 1
	```
	@example
	```luau
		Dash.max(
			{"blue", "red", "orange"},
			nil,
			function(word1, word2) return word1:len() > word2:len() end
		) --> "orange"
	```
]=]
local function max<T>(input: { T }, handler: MaxHandler?, comparator: MaxComparator?): T
	local comparatorFn: MaxComparator = comparator or defaultComparator

	if handler then
		local accumulator = reduce(input, function(acc, value, key)
			local newValue = handler(value, key)
			if not acc then
				return { value = newValue, original = value }
			end
			return if comparatorFn(newValue, acc.value) then { value = newValue, original = value } else acc
		end, nil)
		return accumulator.original
	end

	return reduce(input, function(acc, value)
		if not acc then
			return value
		end
		return if comparatorFn(value, acc) then value else acc
	end, nil)
end

return max


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

export type ResolverFunction = (...any) -> string

--[=[
	Creates a function that memoizes the result of _func_. The memoized function will cache results based on the arguments provided. If a resolver function is provided, it will be used to generate the cache key from the arguments.

	@param func The function to memoize.
	@param resolver Optional function to resolve the cache key.
	@return The memoized function.
	@example
	```luau
		local function add(a: number, b: number): number
			return a + b
		end
		local memoizedAdd = memoize(add)
		print(memoizedAdd(1, 2)) -- Calculates and caches: 3
		print(memoizedAdd(1, 2)) -- Returns from cache: 3
		-- With custom resolver
		local memoizedWithResolver = memoize(add, function(a: number, b: number): string
			return string.format("%d_%d", a, b)
		end)
	```
]=]
local function memoize(func: Types.AnyFunction, resolver: ResolverFunction?): Types.AnyFunction
	local cache = {}

	return function(...): any
		local args = { ... }
		local key: string

		if resolver then
			key = resolver(...)
		else
			key = table.concat(args, "|")
		end

		if cache[key] == nil then
			cache[key] = func(...)
		end

		return cache[key]
	end
end

return memoize


local Dash = script.Parent
local reduce = require(Dash.reduce)

-- Return true if a < b
type MinComparator = (any, any) -> boolean
type MinHandler = (any, any) -> any

local defaultComparator = function(a, b)
	return a < b
end

-- TODO (AleksandrSl 03/06/2024): Can be further "optimized" by writing a compare function that will do all the stuff and pass default comparator
--[=[
	Returns the minimum value in the _input_ table. By default, this returns the least value in the table as defined by the `<` operator.

	If a _handler_ is provided, elements will first be transformed by `handler(value, key)` before being used for comparison.

	Elements will be compared using the _comparator_ if it is provided. The comparator should take two elements and return `true` if the first one is less than the second one, and `false` otherwise.

	If the provided _input_ has no elements, returns `nil`.

	@param input The table to find the minimum value from.
	@param handler Optional function called as `(value, key)` to transform values before comparison.
	@param comparator Optional function called as `(a, b)` to compare two values.
	@return The minimum value, or `nil` if the input is empty.

	@example
	```luau
		Dash.min({1, 2, 3}) --> 1
	```
	@example
	```luau
		Dash.min({"2", "20", "10"}) --> "10"
	```
	@example
	```luau
		Dash.min({3, 2, 1}, function(num) return -num end) --> 3
	```
	@example
	```luau
		Dash.min(
			{"blue", "red", "orange"},
			nil,
			function(word1, word2) return word1:len() < word2:len() end
		) --> "red"
	```
]=]
local function min<T>(input: { T }, handler: MinHandler?, comparator: MinComparator?): T
	local comparatorFn: MinComparator = comparator or defaultComparator

	if handler then
		local accumulator = reduce(input, function(acc, value, key)
			local newValue = handler(value, key)
			if not acc then
				return { value = newValue, original = value }
			end
			return if comparatorFn(newValue, acc.value) then { value = newValue, original = value } else acc
		end, nil)
		return accumulator.original
	end

	return reduce(input, function(acc, value)
		if not acc then
			return value
		end
		return if comparatorFn(value, acc) then value else acc
	end, nil)
end

return min


--[=[
	A function which does nothing.

	Can be used to make it clear that a handler has no function.

	@return Always returns `nil`.
]=]
-- selene: allow(unused_variable)
local function noop(...: any) end

return noop


local Dash = script.Parent
local collectSet = require(Dash.collectSet)
local forEach = require(Dash.forEach)
local Types = require(Dash.Types)

--[=[
	Returns a new table made from entries in the _input_ table whose key is not in the _keys_ array.

	If the input is an array, ordering is preserved.
	If the input is a Map, elements are returned in an arbitrary order.

	@param input The table to filter.
	@param keys Array of keys to exclude from the result.
	@return A new table with the specified keys omitted.
]=]
local function omit(input: {}, keys: { any }): Types.Table
	local output = {}
	local keySet = collectSet(keys)
	forEach(input, function(_, key)
		if not keySet[key] then
			output[key] = input[key]
		end
	end)
	return output
end

return omit


local Dash = script.Parent
local Types = require(Dash.Types)
export type PickHandler<Key, Value> = (Value, Key) -> boolean

--[=[
	Returns a new table containing only the entries from _input_ for which the _handler_ returns truthy.

	@param input The table to filter.
	@param handler Function called as `(value, key)` for each entry; return truthy to include the value.
	@return A new table with only the selected entries.
]=]
local function pick(input: {}, handler: PickHandler<any, any>): Types.Table
	local result = {}
	for key, child in input do
		if handler(child, key) then
			result[key] = child
		end
	end
	return result
end

return pick


--[=[
	Creates a function that returns the result of passing a value through a pipeline of functions.
	Each function in the pipeline receives the result of the previous function.
	Functions are executed from left to right.

	@param f The first function in the pipeline.
	@param ... Additional functions to pipe.
	@return A function that returns the result of passing a value through the pipeline.
	@error "Expected a function" when any of the arguments is not a function.
	@example
	```luau
		local function addOne(n)
			return n + 1
		end
		local function double(n)
			return n * 2
		end
		local addThenDouble = pipe(addOne, double)
		print(addThenDouble(2)) -- prints 6 ((2 + 1) * 2)
	```
]=]
local function pipe<Args...>(f: (Args...) -> any, ...: (...any) -> any): (Args...) -> any
	local fnCount = select("#", ...)
	if fnCount == 0 then
		return f
	end
	local fns = { ... }
	return function(...)
		local result = { f(...) }
		for i = 1, fnCount do
			result = { fns[i](unpack(result)) }
		end
		return unpack(result)
	end
end

return pipe


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

local append = require(Dash.append)
local assign = require(Dash.assign)
local cycles = require(Dash.cycles)
local includes = require(Dash.includes)
local join = require(Dash.join)
local map = require(Dash.map)
local keys = require(Dash.keys)
local slice = require(Dash.slice)

local concat = table.concat
local insert = table.insert
local sort = table.sort

export type PrettyOptions = {
	-- The maximum depth of ancestors of a table to display (default = 2)
	depth: number?,
	-- An array of keys which should not be visited
	omit: { any } | nil,
	-- Whether to use multiple lines (default = false)
	multiline: boolean?,
	-- Whether to show the length of any array in front of its content
	arrayLength: boolean?,
	-- The maximum length of a line (default = 80)
	maxLineLength: number?,
	-- Whether to drop the quotation marks around strings. By default, this is true for table keys
	noQuotes: boolean?,
	-- The indent string to use (default = "\t")
	indent: string?,
	-- A set of tables which have already been visited and should be referred to by reference
	visited: Types.Set<Types.Table>?,
	-- A cycles object returned from `cycles` to aid reference display
	cycles: cycles.Cycles?,
}

local function indentLines(lines: { string }, indent: string)
	return map(lines, function(line: string)
		return indent .. line
	end)
end

local function sortObjectKeys(object: Types.Table): { any }
	local objectKeys = keys(object)
	sort(objectKeys, function(a, b)
		if type(a) == "number" and type(b) == "number" then
			return a < b
		else
			return tostring(a) < tostring(b)
		end
	end)
	return objectKeys
end

local function constructValueOptions(options: PrettyOptions, object: Types.Table): PrettyOptions
	return assign(
		{
			visited = {},
			indent = "\t",
			depth = 2,
		},
		options,
		{
			-- Depth is reduced until we shouldn't recurse any more
			depth = options.depth and options.depth - 1 or nil,
			cycles = options.cycles or cycles(object, options.depth, {
				visited = {},
				refs = {},
				nextRef = 0,
				depth = options.depth,
				omit = options.omit or {},
			}),
		}
	) :: PrettyOptions
end

local function insertClosedBracket(lines: { string }, multiline: boolean, first: boolean)
	if multiline then
		if first then
			-- An empty table is just represented as {}
			lines[#lines] = lines[#lines] .. "}"
		else
			insert(lines, "}")
		end
	else
		lines[#lines] = ("%s}"):format(lines[#lines])
	end
end

local pretty

local function prettyLines(object: any, _options: PrettyOptions?): { string }
	local options = _options or {}
	if type(object) == "table" then
		-- A table needs to be serialized recusively
		-- Construct the options for recursive calls for the table values
		local valueOptions = constructValueOptions(options, object)

		if valueOptions.depth == -1 then
			-- Indicate there is more information available beneath the maximum depth
			return { "..." }
		end
		if valueOptions.visited[object] then
			-- Indicate this table has been printed already, so print a ref number instead of
			-- printing it multiple times
			return { "&" .. valueOptions.cycles.refs[object] }
		end

		valueOptions.visited[object] = true

		local multiline = valueOptions.multiline
		local comma = multiline and "," or ", "

		-- If the table appears multiple times in the output, mark it with a ref prefix so it can
		-- be identified if it crops up later on
		local ref = valueOptions.cycles.refs[object]
		local refTag = ref and ("<%s>"):format(ref) or ""
		local lines = { refTag .. "{" }

		-- Build the options for the recursive call for the table keys
		local keyOptions = join(valueOptions, {
			noQuotes = true,
			multiline = false,
		})

		-- Compact numeric keys into a simpler array style
		local maxConsecutiveIndex = 0
		local first = true

		local objectKeys = sortObjectKeys(object)

		if #object > 0 and valueOptions.arrayLength then
			lines[1] = ("#%d %s"):format(#object, lines[1])
		end

		for _, key in ipairs(objectKeys) do
			if type(key) == "number" and key == maxConsecutiveIndex + 1 then
				maxConsecutiveIndex = key
			end

			if valueOptions.omit and includes(valueOptions.omit, key) then
				-- Don't include keys which are omitted
				continue
			end

			local value = object[key]
			local isArray = (type(key) == "number" and key >= 1 and key <= maxConsecutiveIndex)

			if first then
				first = false
			else
				lines[#lines] = lines[#lines] .. comma
			end

			if multiline then
				if isArray then
					append(lines, indentLines(prettyLines(value, valueOptions), valueOptions.indent))
				else
					local keyLines = prettyLines(key, keyOptions)
					local indentedKey = indentLines(keyLines, valueOptions.indent)
					local valueLines = prettyLines(value, valueOptions)
					local valueTail = slice(valueLines, 2)
					local indentedValueTail = indentLines(valueTail, valueOptions.indent)
					-- The last line of the key and first line of the value are concatenated together
					indentedKey[#indentedKey] = ("%s = %s"):format(indentedKey[#indentedKey], valueLines[1])
					append(lines, indentedKey)
					append(lines, indentedValueTail)
				end
			else
				if isArray then
					lines[#lines] = lines[#lines] .. pretty(value, valueOptions)
				else
					lines[#lines] = lines[#lines] .. pretty(key, keyOptions) .. " = " .. pretty(value, valueOptions)
				end
			end
		end

		insertClosedBracket(lines, multiline, first)
		return lines
	elseif type(object) == "string" and options and not options.noQuotes then
		return { ('"%s"'):format(object) }
	else
		return { tostring(object) }
	end
end

--[=[
	Returns a pretty string serialization of _object_.

	This implementation deals with cycles in tables and can neatly display metatables.

	Optionally use an indented multiline string, limit the depth of tables, omit or pick keys.

	@param object The object to serialize.
	@param options Optional configuration for the serialization.
	@return A formatted string representation of the object.
]=]
pretty = function(object: any, options: PrettyOptions?): string
	return concat(prettyLines(object, options), "\n")
end

return pretty


local Dash = script.Parent
local reduce = require(Dash.reduce)

--[=[
	Multiplies all numbers in the _input_ array.

	If the _input_ has no elements, returns `1`.

	@param input The array of numbers to multiply.
	@return The product of all numbers in the array.
	@example
	```luau
		Dash.product({3, 3, 2}) --> 18
	```
]=]
local function product(input: { number }): number
	return reduce(input, function(acc: number, num)
		return acc * num
	end, 1)
end

return product


export type ReduceHandler<Key, Value, Accumulator> = (Accumulator, Value, Key) -> Accumulator

--[=[
	Iterates through the elements of the _input_ table and calls the _handler_ for each element,
	passing the return of the previous call as the first argument.

	The _initial_ value is passed into the first call, and the final value is returned by the function.

	@param input The array to iterate over.
	@param handler Function called as `(accumulator, value, key)` for each element.
	@param initial The initial value for the accumulator.
	@return The final accumulated value.
]=]
local function reduce<Accumulator>(input: {}, handler: ReduceHandler<any, any, Accumulator>, initial: any): Accumulator
	local result = initial
	for key, value in input do
		result = handler(result, value, key)
	end
	return result
end

return reduce


local insert = table.insert

--[=[
	Reverses the order of the elements in the _input_ array.

	@param input The array to reverse.
	@return A new array with elements in reverse order.
]=]
local function reverse<T>(input: { T }): { T }
	local output: { T } = {}
	for i = #input, 1, -1 do
		insert(output, input[i])
	end
	return output
end
return reverse


--[=[
	Makes a string of _length_ from _input_ by repeating characters from _suffix_ at the end of the string.

	By default, suffix is " ".

	@param input The string to pad.
	@param length The desired total length of the result.
	@param suffix The character(s) to use for padding; defaults to a single space.
	@return A new string padded to the specified length.
	@example
	```luau
		rightPad("toast", 6) --> "toast "
	```
	@example
	```luau
		rightPad("2", 2, "!") --> "2!"
	```
	@example
	```luau
		rightPad("toast", 10, ":)") --> "toast:):):"
	```
]=]
local function rightPad(input: string, length: number, suffix: string?): string
	local definedSuffix = suffix or " "
	local padLength = length - input:len()
	local remainder = padLength % definedSuffix:len()
	local repetitions = (padLength - remainder) / definedSuffix:len()
	return input .. string.rep(suffix or " ", repetitions) .. definedSuffix:sub(1, remainder)
end

return rightPad


--[=[
	Returns `true` if the _left_ and _right_ values are equal (by the equality operator) or the inputs are tables, and all their keys are equal.

	@param left The first value to compare.
	@param right The second value to compare.
	@return `true` if the values are shallowly equal, `false` otherwise.
]=]
local function shallowEqual(left: any, right: any): boolean
	if left == right then
		return true
	end
	if typeof(left) ~= "table" or typeof(right) ~= "table" or #left ~= #right then
		return false
	end
	if left == nil or right == nil then
		return false
	end
	for key, value in left do
		if right[key] ~= value then
			return false
		end
	end
	for key, value in right do
		if left[key] ~= value then
			return false
		end
	end
	return true
end

return shallowEqual


local insert = table.insert

--[=[
	Returns a portion of the _input_ array starting with the element at the _left_ index and ending with the element at the _right_ index (i.e. an inclusive range).

	If _left_ is not defined, it defaults to 1.
	If _right_ is not defined, it defaults to the length of the array (i.e. the last element).

	If _left_ is `-n`, the slice starts with the element `n` places from the last one.
	If _right_ is `-n`, the slice ends with the element `n` places from the last one.

	An empty array is returned if the slice has no or negative length.

	@param input The array to slice.
	@param left Optional starting index; defaults to 1.
	@param right Optional ending index; defaults to array length.
	@return A new array containing the sliced elements.
]=]
local function slice<T>(input: { T }, left: number?, right: number?): { T }
	local output: { T } = {}

	-- Default values
	local l: number = left or 1
	local r: number = right or #input

	if l < 0 then
		l = #input + l
	end
	if r and r < 0 then
		r = #input + r
	end
	for i = l, r do
		insert(output, input[i])
	end
	return output
end

return slice


export type SomeHandler<Key, Value> = (Value, Key) -> boolean

--[=[
	Iterates through the elements of the _input_ table in no particular order.

	Calls the _handler_ for each entry and returns `true` if the handler returns truthy for any element which it is called with.

	@param input The table to iterate over.
	@param handler Function called as `(value, key)` for each entry; return truthy to indicate a match.
	@return `true` if any element matches the handler, `false` otherwise.
]=]
local function some(input: {}, handler: SomeHandler<any, any>): boolean
	for key, child in input do
		if handler(child, key) then
			return true
		end
	end
	return false
end

return some


local insert = table.insert

--[=[
	Splits _input_ into parts based on a _pattern_ delimiter and returns a table of the parts, followed by a table of the matched delimiters.

	@param input The string to split.
	@param pattern The delimiter pattern to split on.
	@return A tuple containing the parts array and delimiters array.
]=]
local function splitOn(input: string, pattern: string): ({ string }, { string }?)
	local parts = {}
	local delimiters = {}
	local from = 1
	if not pattern then
		for i = 1, #input do
			insert(parts, input:sub(i, i))
		end
		return parts
	end
	local delimiterStart, delimiterEnd = input:find(pattern, from)
	while delimiterStart do
		insert(delimiters, input:sub(delimiterStart, delimiterEnd))
		insert(parts, input:sub(from, delimiterStart - 1))
		from = delimiterEnd + 1
		delimiterStart, delimiterEnd = input:find(pattern, from)
	end
	insert(parts, input:sub(from))
	return parts, delimiters
end

return splitOn


--[=[
	Checks whether _input_ starts with the string _prefix_.

	@param input The string to check.
	@param prefix The prefix to check for.
	@return `true` if _input_ starts with _prefix_; otherwise `false`.
	@example
	```luau
		startsWith("Fun Roblox Games", "Fun") --> true
	```
	@example
	```luau
		startsWith("Chess", "Fun") --> false
	```
]=]
local function startsWith(input: string, prefix: string): boolean
	return input:sub(1, prefix:len()) == prefix
end

return startsWith


local Dash = script.Parent
local reduce = require(Dash.reduce)

--[=[
	Sums all numbers in the _input_ array.

	If the input array has no elements, returns `0`.

	@param input The array of numbers to sum.
	@return The sum of all numbers in the array.
	@example
	```luau
		Dash.sum({3, 2, 1}) --> 6
	```
]=]
local function sum(input: { number }): number
	return reduce(input, function(acc: number, num)
		return acc + num
	end, 0)
end

return sum


local Dash = script.Parent
local assign = require(Dash.assign)

type ThrottleOptions = {
	leading: boolean?,
	trailing: boolean?,
}

type ThrottleOptionsInternal = {
	leading: boolean,
	trailing: boolean,
}

-- `& (...any) -> ...any` in the function type is a funky way to mimick `T extends function`
type AnyVoidFunction = (...any) -> ()
type Throttled<T> = T & AnyVoidFunction

--[=[
	Creates and returns a new throttled version of the passed function which will ensure that the function is
	called at most once during the specified wait period. If called multiple times during the wait period, only
	the first call will be executed immediately, and subsequent calls will be ignored until the wait period has elapsed.

	The last call will always be done after the same delay.
	```

	@param func The function to throttle.
	@param wait The number of seconds to throttle invocations to.
	@param options Optional throttle options:
		- leading: boolean? (if true, call at the start of the window; default true)
		- trailing: boolean? (if true, call at the end of the window; default true)
	@return The new throttled function.
	@example
	```luau
		local throttled = throttle(function(v) print(v) end, 0.1)
		for i = 1, 10 do
			throttled(i)
		end
		-- Would result in:
		-- 1
		-- 10
	```
]=]
local function throttle<T>(func: T & AnyVoidFunction, wait: number, options: ThrottleOptions?): Throttled<T>
	local defaultOptions: ThrottleOptionsInternal = {
		leading = true,
		trailing = true,
	}

	local resolvedOptions = assign(defaultOptions, options) :: ThrottleOptionsInternal

	local delay = math.max(0, wait)
	local leading = resolvedOptions.leading
	local trailing = resolvedOptions.trailing

	local lastExecutionTime = -math.huge
	local scheduledThread: thread? = nil
	local lastArgs: { unknown } = {}

	local function clearScheduled()
		if scheduledThread then
			task.cancel(scheduledThread)
			scheduledThread = nil
		end
	end

	local function invoke()
		lastExecutionTime = os.clock()
		scheduledThread = nil;
		(func :: AnyVoidFunction)(table.unpack(lastArgs))
	end

	return (
		function(...)
			local now = os.clock()
			lastArgs = { ... }

			--[[
				We calculate timeSinceExecution to determine if enough time has passed since the last execution
				to allow a new "leading" call. This is important for the very first call, where
				lastExecutionTime is initialized to -math.huge, so timeSinceExecution will be very large,
				ensuring the first call is always allowed if leading is enabled.
			]]
			local timeSinceExecution = now - lastExecutionTime

			if leading and timeSinceExecution >= delay then
				clearScheduled()
				invoke()
				return
			end

			--[[
				For trailing calls, we need to schedule the function to run after the remaining time in the throttle window.
				We calculate (now - baseTime) separately because:
				  - On the very first call (lastExecutionTime == -math.huge), we want the trailing call to be scheduled
				    after the full delay from now, not some huge negative value.
				  - On subsequent calls, baseTime is lastExecutionTime, so (now - baseTime) gives us the elapsed time
				    since the last execution, and (delay - (now - baseTime)) is the remaining time to wait.
				This ensures that the trailing call is always scheduled to run at the end of the throttle window,
				regardless of when the first or last call happened.
			]]
			if trailing and not scheduledThread then
				local baseTime = (lastExecutionTime == -math.huge) and now or lastExecutionTime
				local remaining = delay - (now - baseTime)
				if remaining < 0 then
					remaining = 0
				end
				scheduledThread = task.delay(remaining, invoke)
			end
		end :: unknown
	) :: Throttled<T>
end

return throttle


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

--[=[
	Returns a new Map by merging all keys in the two Maps in left-to-right order.

	The `None` symbol can be used to remove existing elements.

	@param map1 The first Map to union
	@param map2 The second Map to union
	@return A new Map containing the merged keys and values.
]=]
local function union(map1: {}, map2: {}): Types.Table
	local unioned = table.clone(map1)

	for key, value in map2 do
		if value == None then
			unioned[key] = nil
		else
			unioned[key] = value
		end
	end

	return unioned
end

return union


local Dash = script.Parent
local Types = require(Dash.Types)
local insert = table.insert

--[=[
	Returns an array of the values in the _input_ table.

	If the input is an array, ordering is preserved.
	If the input is a Map, elements are returned in an arbitrary order.

	@param input The table to extract values from.
	@return An array containing all values from the input table.
]=]
local function values(input: {}): Types.Table
	local result = {}
	for _, value in input do
		insert(result, value)
	end
	return result
end

return values


--[=[
	Returns an iterator over two given arrays that produces pairs of elements with the same index from both arrays.

	The iterator stops when the shortest array ends, i.e. when it encounters nil in one of the arrays.

	@param array1 The first array to zip.
	@param array2 The second array to zip.
	@return An iterator that yields pairs of elements from both arrays.
]=]
local function zip<Item1, Item2>(array1: { Item1 }, array2: { Item2 }): () -> (Item1, Item2)
	local key1, key2, item1, item2
	return function()
		key1, item1 = next(array1, key1)
		key2, item2 = next(array2, key2)
		if not item1 or not item2 then
			return nil
		end
		return item1, item2
	end
end

return zip


return {
	displayName = "Dash",
	testMatch = {
		"**/benchmark/*.bench",
	},
}


local ReplicatedStorage = game:GetService("ReplicatedStorage")
local processServiceExists, ProcessService = pcall(function()
	return game:GetService("ProcessService")
end)

local Packages = ReplicatedStorage:FindFirstChild("Packages")
if not Packages or not Packages:FindFirstChild("Dev") then
	game:GetService("TestService"):Error("Invalid Package configuration. Try running `rotrieve install` to remedy.")
	ProcessService:ExitAsync(1)
end

local runCLI = require(Packages.Dev.Jest).runCLI

local status, result = runCLI(Packages.Dash, {}, { Packages.Dash }):awaitStatus()

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

local statusCode = if result.results.failureCount == 0 then 0 else 1

ProcessService:ExitAsync(statusCode)


-- Checks that toString represenation of the error includes given message
local function toThrowWithMessage(_, received, errorMessage)
	local ok, error = pcall(received)
	local pass = not ok and string.find(error:toString(), errorMessage) ~= nil
	local message
	if pass then
		message = function()
			return `expected {error} not to include {errorMessage}`
		end
	else
		message = function()
			return `expected {error} to include {errorMessage}`
		end
	end
	return { message = message, pass = pass }
end

return {
	toThrowWithMessage = toThrowWithMessage,
}