--[[
	textchat
	utility for TextChatService, making adding/removing, and muting TextSources easier
	alongside providing some utilities for chat visuals
]]

local TextChatService = game:GetService("TextChatService")
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")

type UserId = number

type PlayerOrUserId = Player | UserId

local CHANNEL_TO_SOURCES = {} :: { [TextChannel]: { [UserId]: TextSource } }
local USER_SOURCES = {} :: { [UserId]: { TextSource } }
local CHAT_COLORS = table.freeze({
	 -- BrickColor.new("Bright red").Color,
	Color3.new(253 / 255, 41 / 255, 67 / 255),
	 -- BrickColor.new("Bright blue").Color,
	Color3.new(1 / 255, 162 / 255, 255 / 255),
	 -- BrickColor.new("Earth green").Color,
	Color3.new(2 / 255, 184 / 255, 87 / 255),
	BrickColor.new("Bright violet").Color,
	BrickColor.new("Bright orange").Color,
	BrickColor.new("Bright yellow").Color,
	BrickColor.new("Light reddish violet").Color,
	BrickColor.new("Brick yellow").Color,
})

local function GET_ID_FROM_PLAYER_OR_USER_ID(player_or_user_id: PlayerOrUserId): number
	return if type(player_or_user_id) == "number" then
			player_or_user_id
		else
			player_or_user_id.UserId
end

local function GET_COLOR_FOR_NAME(name: string): Color3
	local sub_number = if #name % 2 == 1 then -1 else 0
	local name_value = 0

	for index, codepoint in { string.byte(name, 1, #name) } do
		name_value += if (((#name - index) + 1) - sub_number) % 4 >= 2 then
				-codepoint
			else
				codepoint
	end

	return CHAT_COLORS[(name_value % #CHAT_COLORS) + 1]
end

local function GET_CHAT_PREFIX(
	display: string,
	name: string?,
	is_verified: boolean?,
	prefix: string?
): string
	return string.format(
		if is_verified then
			"\u{E000}%*%*"
		else
			"%*%*",
		GET_COLOR_FOR_NAME(name or display):ToHex(),
		if name then `{display}(@{name})` else display,
		prefix or ": "
	)
end

local textchat = {
	get_prefix = GET_CHAT_PREFIX
}

--[[
	Gives the chat color for a specific `player_or_name`,
	if provided a player it uses `Player.Name` to give a chat color.
]]
function textchat.get_color(player_or_name: Player | string): Color3
	return GET_COLOR_FOR_NAME(
		if type(player_or_name) == "string" then player_or_name else player_or_name.Name
	)
end

function textchat.get_prefix_for_player(
	player: Player, include_name: boolean?, prefix: string?
): string
	return GET_CHAT_PREFIX(
		player.DisplayName, if include_name then player.Name else nil,
		player.HasVerifiedBadge, prefix
	)
end

--[[
	Removes the given `player_or_user_id` from the `channel`
]]
function textchat.remove_from_channel(
	player_or_user_id: PlayerOrUserId, channel: TextChannel
)
	local source = CHANNEL_TO_SOURCES[channel][GET_ID_FROM_PLAYER_OR_USER_ID(player_or_user_id)]

	if source then
		source:Destroy()
	end
end

--[[
	Adds the given `player` or `user_id` to the `channel`
]]
function textchat.add_to_channel(
	player_or_user_id: PlayerOrUserId, channel: TextChannel
): (TextSource?, boolean)
	return channel:AddUserAsync(
		GET_ID_FROM_PLAYER_OR_USER_ID(player_or_user_id)
	)
end

--[[
	Sets if a `player` or `user_id` is muted in all channels, or `channel` if provided
]]
function textchat.set_mute(
	player_or_user_id: PlayerOrUserId, is_muted: boolean, channel: TextChannel?
)
	local user_id = GET_ID_FROM_PLAYER_OR_USER_ID(player_or_user_id)

	if channel then
		local source = CHANNEL_TO_SOURCES[channel][user_id]

		if source then
			source.CanSend = is_muted
		end
	else
		for channel, sources in CHANNEL_TO_SOURCES do
			local source = CHANNEL_TO_SOURCES[channel][user_id]

			if source then
				source.CanSend = is_muted
			end
		end
	end
end

--[[
	Checks if the given `player_or_user_id` is muted in all channels, or a specific `channel`
]]
function textchat.is_muted(
	player_or_user_id: PlayerOrUserId, channel: TextChannel?
): boolean
	local user_id = GET_ID_FROM_PLAYER_OR_USER_ID(player_or_user_id)

	if channel then
		local source = CHANNEL_TO_SOURCES[channel][user_id]
		return source and source.CanSend
	else
		local sources = USER_SOURCES[user_id]
		local muted_count = 0

		for _, source in sources do
			if source.CanSend then
				muted_count += 1
			end
		end
		return #sources == muted_count
	end
end

if RunService:IsServer() then

	local function on_descendant_added(descendant: Instance)
		if descendant:IsA("TextSource") then
			local user_id = descendant.UserId
			local sources = USER_SOURCES[user_id]

			if sources then
				table.insert(sources, descendant)
			else
				USER_SOURCES[user_id] = { descendant }
			end

			-- text sources will only ever have TextChannels as their parents
			local channel_sources = CHANNEL_TO_SOURCES[descendant.Parent :: any]

			if channel_sources then
				if not channel_sources[user_id] then
					channel_sources[user_id] = descendant
				end
			end
		elseif descendant:IsA("TextChannel") then
			local sources = {}

			for _, child in descendant:GetChildren() do
				if child:IsA("TextSource") then
					sources[child.UserId] = child
				end
			end

			CHANNEL_TO_SOURCES[descendant] = sources
		end
	end

	TextChatService.DescendantRemoving:Connect(function(descendant)
		if descendant:IsA("TextSource") then
			local sources = USER_SOURCES[descendant.UserId]

			if sources then
				local index = table.find(sources, descendant)

				if index then
					if #sources == 1 then
						USER_SOURCES[descendant.UserId] = nil
					else
						sources[index] = sources[#sources]
						sources[#sources] = nil
					end
				end
			end
		elseif descendant:IsA("TextChannel") then
			CHANNEL_TO_SOURCES[descendant] = nil
		end
	end)

	TextChatService.DescendantAdded:Connect(on_descendant_added)

	Players.PlayerRemoving:Connect(function(player)
		USER_SOURCES[player.UserId] = nil
	end)

	task.spawn(function()
		for _, descendant in TextChatService:GetDescendants() do
			on_descendant_added(descendant)
		end
	end)

end

return table.freeze(textchat)


--!native

--[[
	url
	utility functions for encoding and decoding urls
]]

export type SpaceEncoding = "+" | "%20"

local FORMAT = string.format
local GMATCH = string.gmatch
local GSUB = string.gsub

local function CHAR_TO_CODE(char: string): string
    return FORMAT("%%%02X", string.byte(char))
end

local function CODE_TO_CHAR(code: string): string
    return string.char(tonumber(code, 16) :: any)
end

-- modified from:
-- https://devforum.roblox.com/t/233570/2
local function DECODE(url: string): string
	return (GSUB(GSUB(url, "+", " "), "%%(%x%x)", CODE_TO_CHAR))
end

local function ENCODE(url: string, encode_spaces_as: SpaceEncoding?): string
	local space_encode_char = if encode_spaces_as == "+" then "+" else "%%20"
	local chars_encoded = (GSUB(url, "([^%w _%%%-%.~])", CHAR_TO_CODE))
	local new_line_encoded = (GSUB(chars_encoded, "\n", "\r\n"))

	return (GSUB(new_line_encoded, "%s+", space_encode_char))
end

local url = {
	encode = ENCODE,
	decode = DECODE,
}

-- modified from: https://stackoverflow.com/questions/28916182/parse-parameters-out-of-url-in-lua
function url.read_query(url: string): { [string]: string }
	local query_tbl = {}

	for key, value: any in GMATCH(DECODE(url), "([^&=?]-)=([^&=?]+)") do
		query_tbl[key] = value
	end
	return query_tbl
end

function url.queryify(t: { [string]: string }, encode_spaces_as: SpaceEncoding?): string
	local query_strings = {} :: { string }

	for key, value in t do
		local encoded_value = ENCODE(value, encode_spaces_as)
		local encoded_key = ENCODE(key, encode_spaces_as)
		table.insert(query_strings, `{encoded_key}={encoded_value}`)
	end
	return `?{table.concat(query_strings, "&")}`
end

return table.freeze(url)


local testkit = require("@testkit")
local url = require("./init")

local TEST, CASE, CHECK, FINISH = testkit.test()

TEST("url", function()
	do CASE("encode %%20")
		CHECK(url.encode("Je suis allé au cinéma.") == "Je%20suis%20all%C3%A9%20au%20cin%C3%A9ma.")
	end

	do CASE("encode +")
		CHECK(url.encode("Je suis allé au cinéma.", "+") == "Je+suis+all%C3%A9+au+cin%C3%A9ma.")
	end

	do CASE("encode + char")
		CHECK(url.encode("+") == "%2B")
	end

	do CASE("decode %%20")
		CHECK(url.decode("Je%20suis%20all%C3%A9%20au%20cin%C3%A9ma.") == "Je suis allé au cinéma.")
	end

	do CASE("decode + char")
		CHECK(url.decode("%2B") == "+")
	end

	do CASE("decode +")
		CHECK(url.decode("Je+suis+all%C3%A9+au+cin%C3%A9ma.") == "Je suis allé au cinéma.")
	end

	do CASE("queryify")
		local query = url.queryify({
			meow = "mrrp",
			mrrp = "meow"
		})

		CHECK(query == "?meow=mrrp&mrrp=meow")
	end

	do CASE("read_query")
		local query = "?meow=mrrp&mrrp=meow"
		local tbl = url.read_query(query)

		CHECK(tbl.meow == "mrrp" and tbl.mrrp == "meow")
	end

end)


--[[
	deep iter dir
	recursively iterates over a directory and
	calls a callback that can then optionally move or change the contents of the file
]]

local read_dir = require("@scripts/read_dir")
local remove = require("@scripts/remove")
local fs = require("@lune/fs")

type BaseFileInfo<N, P> = {
	contents: (string | buffer)?,
	path: P,
	name: N,
}

export type OptionalFileInfo =
	| BaseFileInfo<string, nil>
	| BaseFileInfo<nil, string>
	| BaseFileInfo<nil, nil>

type IterDirCallback = ((name: string, path: string, contents: string, depth: number) -> (OptionalFileInfo | "break" | "continue")?) |
	((name: string, path: string, contents: string, depth: number) -> ())

type DirInfo = {
	depth: number,
	path: string,
}

local DIRQUEUE = {} :: { DirInfo }

local function deep_iter_dir(path: string, f: IterDirCallback)
	local queue_len = 1
	DIRQUEUE[1] = {
		depth = #string.split(path, "/"),
		path = path,
	}

	while queue_len ~= 0 do
        local dir_info = remove(DIRQUEUE, queue_len)
		local next_depth = dir_info.depth + 1
		local dir_path = dir_info.path
        queue_len -= 1

        for _, entry in read_dir(dir_path) do
            local path = `{dir_path}/{entry}`

			if fs.isFile(path) then
				local contents = fs.readFile(path)
				local result = f(
					string.match(entry, "[^%.]+") or "",
					path, contents, next_depth
				)

				if result == "break" then
					return
				elseif type(result) == "table" then
					if result.contents then
						contents = result.contents
					end

					if result.name then
						fs.removeFile(path)
						path = `{dir_path}/{result.name}`
					elseif result.path then
						fs.removeFile(path)
						path = result.path
					end

					fs.writeFile(path, contents)
				end
			else
				queue_len += 1
				DIRQUEUE[queue_len] = {
					depth = next_depth,
					path = path,
				}
			end
        end
    end

	table.clear(DIRQUEUE)
end

return deep_iter_dir


--[[
	deep iter scripts
	recursively iterates over every luau file in a directory
	and calls a callback that can then optionally overwrite the file
]]

local deep_iter_dir = require("@scripts/deep_iter_dir")

type IterScriptsCallback = ((path: string, src: string, depth: number) -> string?) |
	((path: string, src: string, depth: number) -> ())

local function deep_iter_scripts(dir: string, f: IterScriptsCallback)
	deep_iter_dir(dir, function(_, path, content, depth)
		if string.sub(path, #path - 3, #path) == "luau" then
			local new_src = f(path, content, depth)

			if new_src then
				return {
					contents = new_src
				}
			end
		end
		return nil :: any
	end :: any)
end

return deep_iter_scripts


--[[
	dep graph
	module for generating a dependency graph for all libraries
]]

local deep_iter_scripts = require("@scripts/deep_iter_scripts")
local require_pattern = require("@scripts/require_pattern")
local read_dir = require("@scripts/read_dir")
local remove = require("@scripts/remove")
local process = require("@lune/process")

export type DependencyInfo = {
	shallow: { string },
	has_deps: boolean,
	deep: { string },
}

local GRAPH = {} :: { [string]: DependencyInfo }
local FOUND = {} :: { [string]: boolean }
local DEPQUEUE = {} :: { string }

for _, lib in read_dir("libs") do
	GRAPH[lib] = {
		has_deps = false,
		shallow = {},
		deep = {},
	}
end

for lib, depinfo in GRAPH do
	local shallow = depinfo.shallow

	deep_iter_scripts(`libs/{lib}`, function(_, src)
		for dep_lib in string.gmatch(src, require_pattern.libs_alias) do
			if not table.find(shallow, dep_lib) then
				table.insert(shallow, dep_lib)
			end
		end
	end)
end

for lib, depinfo in GRAPH do
	local shallow = depinfo.shallow
	local deep = depinfo.deep

	if #shallow ~= 0 then
		local queue_len = #shallow
    	table.move(shallow, 1, queue_len, 1, DEPQUEUE)

   		while queue_len ~= 0 do
        	local current = remove(DEPQUEUE, queue_len)
			queue_len -= 1

        	if not FOUND[current] then
				table.insert(deep, current)
           	 	FOUND[current] = true

            	-- Add the shallow dependencies of the current item to the queue
           		for _, dep in GRAPH[current].shallow do
					queue_len += 1
					DEPQUEUE[queue_len] = dep
            	end
        	end
    	end

		depinfo.has_deps = true
		table.clear(DEPQUEUE)
		table.clear(FOUND)
	end

	if #deep == #shallow then
		depinfo.deep = table.freeze(shallow)
	else
		table.freeze(shallow)
		table.freeze(deep)
	end

	table.freeze(depinfo)
end

if process.args[1] == "print" then
	local pretty_graph = {} :: { [string]: { shallow: { string }, deep: { string } } | { string } }

	for lib, depinfo in GRAPH do
		local shallow = depinfo.shallow
		local deep = depinfo.deep

		if depinfo.has_deps then
			if shallow ~= deep then
				pretty_graph[lib] = {
					shallow = shallow,
					deep = deep,
				}
			else
				pretty_graph[lib] = shallow
			end
		end
	end
	print(pretty_graph)
end

return table.freeze(GRAPH)


--[[
	depricate
	makes a library depricated
]]

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

local ARGS = process.args
local ALT_FORMAT = fs.readFile("scripts/depricate/ALT_FORMAT.md")
local FORMAT = fs.readFile("scripts/depricate/FORMAT.md")
local LIB = ARGS[1]
local INSTEAD

for index, arg in ARGS do
	if arg == "--instead" or arg == "--i" then
		if index == 1 then
			LIB = ARGS[3]
		end

		INSTEAD = ARGS[index + 1]
		break
	end
end

local readme = if INSTEAD then
		string.format(ALT_FORMAT, INSTEAD, INSTEAD, LIB)
	else
		string.format(FORMAT, LIB)

fs.writeFile(`libs/{LIB}/README.md`, readme)


--[[
	docs
	generates sidebar, and homepage
]]

local deep_iter_dir = require("@scripts/deep_iter_dir")
local read_dir = require("@scripts/read_dir")
local gsub = require("@scripts/gsub")
local serde = require("@lune/serde")
local fs = require("@lune/fs")

type SidebarItem = { text: string, link: string }

type LibdocInfo = {
	items: { LibdocInfo }?,
	link: string,
	text: string,
}

local IGNORE = table.freeze({ ".vitepress", "public", "index.md", "installing.md" })
local FEATURE_FORMAT = "  - title: %*\n    details: %*\n    link: %*\n"
local HOME_SRC = fs.readFile("docs/index.md")
local DOCDESCS = {} :: { [string]: string }
local FANCY_URL_PATTERN = "%[(.-)%]%(.-%)"
local LIBDOCS = {} :: { LibdocInfo }
local DESC_PATTERN = "\n\n(.-)\n\n"
local UPPER = string.upper
local MATCH = string.match
local FIND = string.find
local SUB = string.sub

local function NAME_FROM_FILE_NAME(file_name: string): string
	local rest_of_name = SUB(
		file_name, 2, (FIND(file_name, ".", 1, true) :: any) - 1
	)

	return gsub(
		UPPER(SUB(file_name, 1, 1)) .. rest_of_name,
		"%-%a", function(dash_and_char)
			return " " .. UPPER(SUB(dash_and_char, 2, 2))
		end
	)
end

local function GET_SHORTENED_DESC(file: string): string
	local desc = MATCH(gsub(file, "\r", ""), DESC_PATTERN)

	if desc then
		return gsub(desc, FANCY_URL_PATTERN, function(url_name)
			return gsub(url_name, "`", "")
		end)
	else
		return "n/a"
	end
end

for _, lib_doc in read_dir("docs") do
	if table.find(IGNORE, lib_doc) then
		continue
	end
	local name = NAME_FROM_FILE_NAME(lib_doc)
	local path = `docs/{lib_doc}`

	if fs.isFile(path) then
		DOCDESCS[name] = GET_SHORTENED_DESC(fs.readFile(path))
		table.insert(LIBDOCS, table.freeze({
			link = `/{lib_doc}`,
			text = name,
		}))
	else
		local index_path = `{path}/index.md`

		if fs.isFile(index_path) then
			local relative_index_path = `/{lib_doc}/index.md`
			DOCDESCS[name] = GET_SHORTENED_DESC(fs.readFile(index_path))
			local items = {} :: { LibdocInfo }
			local libdoc_info = {
				link = relative_index_path,
				items = items,
				text = name,
			}

			deep_iter_dir(path, function(name, file_path)
				if file_path ~= index_path then
					table.insert(items, {
						text = NAME_FROM_FILE_NAME(name),
						link = MATCH(file_path, "docs(.+)") or "",
					})
				end
			end :: any)

			table.insert(LIBDOCS, libdoc_info)
		else
			error("cannot have docs dir without an index.md file")
		end
	end
end

table.sort(LIBDOCS, function(a, b)
	return a.text > b.text
end)

local before_features = MATCH(HOME_SRC, "((.+)features:)")
local features = table.create(#LIBDOCS)

for index, libdoc in LIBDOCS do
	local name = libdoc.text
	features[index] = string.format(
		FEATURE_FORMAT, name, DOCDESCS[name], libdoc.link
	)
end

fs.writeFile("docs/.vitepress/sidebar-libs.json", serde.encode("json", LIBDOCS))
fs.writeFile("docs/index.md", `{before_features}\n{table.concat(features)}---`)


--[[
	get depth for path
	gives how deep a path is (the amount of backslashes it has)
]]

local function get_depth_for_path(path: string)
	return #string.split(path, "/")
end

return get_depth_for_path


--[[
	gsub
	a function that only gives the first result of string.gsub so the typechecker doesnt scream
]]

type GsubSubsitute =
	| { [string]: string }
	| ((string) -> string)
	| string

--[[
	Returns a copy of `s` in which all or the first `n` occurrences of the pattern are replaced with the given replacement.
]]
local function gsub(
	s: string, p: string, f: GsubSubsitute, maxs: number?
): string
	return (string.gsub(s, p, f, maxs))
end

return gsub


--[[
	libinfo
	utility func for reading LIBRARY.toml files and providing types for LIBINFOs
]]

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

type RuntimeInfo = {
	optional: { string }?,
	main: string,
}?

type BaseLibInfo<R> = {
	tags: { string },
	version: string,
	runtime: R,
}

type InternalLibInfo = BaseLibInfo<RuntimeInfo | string>

type LibInfo = BaseLibInfo<RuntimeInfo?>

local CACHED_INFOS = {} :: { [string]: LibInfo }

local function libinfo(lib: string): LibInfo
	local cached = CACHED_INFOS[lib]

	if cached then
		return cached
	else
		local raw = serde.decode("toml", fs.readFile(`libs/{lib}/LIBRARY.toml`)) :: InternalLibInfo

		if raw.runtime then
			local runtime = raw.runtime

			if type(runtime) == "string" then
				raw.runtime = { main = runtime }
			else
				if runtime.optional then
					table.freeze(runtime.optional)
				end
			end

			table.freeze(raw.runtime)
		else
			error(`no runtime specified for library '{lib}'`)
		end

		if not string.match(raw.version, "%d+%.%d+%.%d+[%a%-]*") then
			error(`version is not vaild semver for library '{lib}'`)
		end

		CACHED_INFOS[lib] = table.freeze(raw) :: any
		return raw :: any
	end
end

return libinfo


--[[
	libs changed
	map of each library thats changed based on if it has a release for its latest version or not
]]

local get_latest_commit = require("@scripts/get_latest_commit")
local richterm = require("@scripts/richterm")
local read_dir = require("@scripts/read_dir")
local libinfo = require("@scripts/libinfo")
local summon = require("@scripts/summon")

local LIBS_CHANGED = {} :: { string }

for _, lib in read_dir("libs") do
	local latest_commit = get_latest_commit(`libs/{lib}`)

	-- lib isn't in the main branch (ie not released)
	if not latest_commit then
		continue
	elseif summon(`git tag -n {lib}@{libinfo(lib).version}`) == "" then
		table.insert(LIBS_CHANGED, lib)
	end
end

table.sort(LIBS_CHANGED)
print(
	richterm.bold(richterm.bgGreen("LIBRARIES CHANGED")) .. "\n"
	.. table.concat(LIBS_CHANGED, "\n")
)

return table.freeze(LIBS_CHANGED)


--[[
	license
	gives the license file as a code comment
]]

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

local new_line_char = if process.os == "windows" then "\r\n" else "\n"
local tabbed_license = string.gsub(fs.readFile("LICENSE"), new_line_char, "\n\t")

return `--[[\n\t{string.sub(tabbed_license, 1, #tabbed_license - 1)}]]`


--[[
	modulify
	converts a folder to a module script
]]

local read_dir = require("@scripts/read_dir")
local remove = require("@scripts/remove")
local roblox = require("@lune/roblox")
local fs = require("@lune/fs")

type ModuleScript = roblox.Instance & {
	Source: string,
}

type DirInfo = {
	instance: roblox.Instance,
	path: string,
}

local CACHED_MODULES = {} :: { [string]: ModuleScript? }
local CREATE_INSTANCE = roblox.Instance.new
local DIRQUEUE = {} :: { DirInfo }

local function CREATE_MODULE_SCRIPT(name: string, source: string, p: roblox.Instance?): ModuleScript
	local module = CREATE_INSTANCE("ModuleScript") :: ModuleScript
	module.Name = string.gsub(name, "%-", "_")
	module.Source = source
	module.Parent = p
	return module
end

local function CREATE_FOLDER(name: string, p: roblox.Instance?): roblox.Instance
	local folder = CREATE_INSTANCE("Folder")
	folder.Name = string.gsub(name, "%-", "_")
	folder.Parent = p
	return folder
end

local function MODULIFY_ITER_DIR(
	queue_len: number, path: string, parent: roblox.Instance
): number
	for _, sub_name in read_dir(path) do
		local sub_path = `{path}/{sub_name}`

		if fs.isDir(sub_path) then
			local init_path = `{sub_path}/init.luau`
			local instance = if fs.isFile(init_path) then
					CREATE_MODULE_SCRIPT(sub_name, fs.readFile(init_path), parent)
				else
					CREATE_FOLDER(sub_name, parent)
			queue_len += 1

			DIRQUEUE[queue_len] = {
				instance = instance,
				path = sub_path,
			}
		elseif sub_name ~= "init.luau" then
			CREATE_MODULE_SCRIPT(sub_name, fs.readFile(sub_path), parent)
		end
	end

	return queue_len
end

local function modulify(lib: string, path: string, ignore_cache: boolean?): ModuleScript
	if not ignore_cache then
		local module = CACHED_MODULES[lib]

		if module then
			return module
		end
	end

	local module = CREATE_MODULE_SCRIPT(lib, fs.readFile(`{path}/init.luau`))
	local queue_len = 0

	queue_len = MODULIFY_ITER_DIR(queue_len, path, module)

	while #DIRQUEUE ~= 0 do
		local dir_info = remove(DIRQUEUE)

		queue_len = MODULIFY_ITER_DIR(
			queue_len, dir_info.path, dir_info.instance
		)
	end

	CACHED_MODULES[path] = module
	table.clear(DIRQUEUE)
	return module
end

return modulify


--[[
	pull requests
	gets pull requests after a given unix timestamp
]]

local read_dir = require("@scripts/read_dir")
local datetime = require("@lune/datetime")
local summon = require("@scripts/summon")
local serde = require("@lune/serde")

export type PullRequestInfo = {
	merged_at: number,
	number: number,
	author: string,
	title: string,
}

local LIBS_TO_PRS = {} :: { [string]: { PullRequestInfo } }

local function get_after_time(
	lib: string, prs: { PullRequestInfo }, time: number
): { PullRequestInfo }
	local prs_after_time = {}

	for _, pr in prs do
		if pr.merged_at > time then
			table.insert(prs_after_time, pr)
		end
	end
	return table.freeze(prs_after_time)
end

local pull_requests = {}

function pull_requests.get_all_after_time(time: number): { [string]: { PullRequestInfo }? }
	local libs_to_prs_after_time = {}

	for lib, prs in LIBS_TO_PRS do
		local prs_after_time = get_after_time(lib, prs, time)

		if #prs_after_time ~= 0 then
			libs_to_prs_after_time[lib] = prs_after_time
		end
	end
	return table.freeze(libs_to_prs_after_time)
end

function pull_requests.get_after_time(lib: string, time: number): { PullRequestInfo }
	return (get_after_time(lib, LIBS_TO_PRS[lib], time))
end

do

	local pull_requests = serde.decode(
		"json",
		summon("gh pr list --json author,mergedAt,files,title,number --limit 100 --state merged")
	)

	for _, lib in read_dir("libs") do
		LIBS_TO_PRS[lib] = {}
	end

	for _, pr in pull_requests do
		for _, file in pr.files do
			local lib = string.match(file.path, "^libs/([^/]+)") or
				string.match(file.path, "^docs/([^/%.]+)")

			if lib then
				local prs = LIBS_TO_PRS[lib]

				if prs then
					table.insert(prs, {
						merged_at = datetime.fromIsoDate(pr.mergedAt).unixTimestamp,
						author = pr.author.login,
						number = pr.number,
						title = pr.title,
					})
				end
			end
		end
	end

end

return table.freeze(pull_requests)


--[[
	read dir
	its the same except it removes .DS_Store
]]

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

local function read_dir(path: string): { string }
	local contents = fs.readDir(path)
	local index = table.find(contents, ".DS_Store")

	if index then
		remove(contents, index)
	end
	return contents
end

return read_dir


--[[
	remove
	swap removal func for removing the first index,
	if the value doesnt exist it'll error
]]

local function remove<V>(t: { V }, i: number?): V
	if not i then
		i = 1
	end
	local v = t[i :: number]

	if v ~= nil then
		if #t ~= 1 then
			t[i :: number] = t[#t]
			t[#t] = nil
		else
			t[1] = nil
		end

		return v
	else
		error("table is empty", 2)
	end
end

return remove


--[[
	require pattern
	module for storing require pattern info, and provides a create function for making a require pattern
]]

local START = "local%s*%w+%s*=%s*require%s*%([\"'`]"
local ENDING = "[\"'`]%)"

local function CREATE(path_pattern: string?): string
	return `{START}{path_pattern or "(.-)"}{ENDING}`
end

return table.freeze({
	libs_alias = CREATE("@libs/(%w+).-"),
	create = CREATE,
	ending = ENDING,
	start = START,
})


--[[
	summon
	small wrapper around lunes process.spawn, that makes it actually error
	and sets the shell to the default shell of the os (except macos because it still has bash and bash works fine enough)
]]

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

local SHELL = if process.os == "windows" then "powershell" else "bash"
local ENV = {}
local OPTS = {
	shell = SHELL,
	env = ENV
}

local function summon(
	program: string, env: { [string]: string }?, protected: boolean?
): string?
	local result = process.exec(
		program, nil, (if env then { shell = SHELL, env = env } else OPTS) :: any
	)

	if result.ok then
		return result.stdout
	elseif protected then
		return nil
	else
		error(result.stderr, 2)
	end
end

do

	for key, value in process.env do
		ENV[key] = value
	end

end

return summon :: ((program: string, env: { [string]: string }?, protected: true) -> string?) &
	((program: string, env: { [string]: string }?, protected: false?) -> string) &
	-- these exist because of the solver being pee pee poo poo in reguards to singleton inferrence for function args
	((program: string, env: { [string]: string }?) -> string) &
	((program: string) -> string)


--[[
	test runner
	script for running tests for ci
]]

local libs_changed = require("@scripts/libs_changed")
local read_dir = require("@scripts/read_dir")
local depgraph = require("@scripts/depgraph")
local libinfo = require("@scripts/libinfo")
local process = require("@lune/process")
local fs = require("@lune/fs")

local function GET_LIBS_TO_TEST(): { string }
	if process.args[1] == "all" then
		return read_dir("libs")
	end
	local libs_to_test

	if #process.args ~= 0 then
		libs_to_test = {}

		for _, lib in process.args do
			local lib_path = `libs/{lib}`

			if fs.isFile(`{lib_path}/test.luau`) or fs.isDir(`{lib_path}/test`) then
				table.insert(libs_to_test, lib)
			else
				error(`library {lib} doesn't exist or doesnt have a subdir named 'test' or a 'test.luau' file`)
			end
		end
	else
		libs_to_test = table.clone(libs_changed)
	end

	for lib, info in depgraph do
		for _, lib_dep in info.deep do
			if table.find(libs_to_test, lib_dep) then
				table.insert(libs_to_test, lib)
				break
			end
		end
	end

	return libs_to_test
end

for _, lib in GET_LIBS_TO_TEST() do
	--TODO: make me not skip roblox libraries, via switching to using the open cloud execution api stuffs
	local libinfo = libinfo(lib)

	if libinfo.runtime and libinfo.runtime.main == "roblox" then
		continue
	end

	local path = `libs/{lib}/test`
	local file_path = `{path}.luau`
	local real_path: string
	local contents: string

	if fs.isDir(path) then
		local init_path = `{path}/init.luau`

		if fs.isFile(init_path) then
			contents = fs.readFile(init_path)
			real_path = init_path
		else
			error(`library {lib} has a test dir with no init.luau file`)
		end
	elseif fs.isFile(file_path) then
		contents = fs.readFile(file_path)
		real_path = file_path
	else
		continue
	end

	-- test not implemented and its just a placeholder as a reminder
	if #contents == 0 then
		continue
	end

	fs.writeFile(real_path, `{contents}\n\nassert(FINISH())\nprint()`)
	--TODO: replace w lunes luau library
	;(require)(`@libs/{lib}/test`)
	fs.writeFile(real_path, contents)
end


--[[
	zip
	utility for zipping folders within lune
]]

local summon = require("@scripts/summon")

local function zip(path: string): string
	return summon(`tar -c -z --exclude *.DS_Store {path} `)
end

return zip


--!nocheck
--!nolint

-------------------------------------------------------------------------------
-- testkit.luau
-- v0.7.3
--------------------------------------------------------------------------------

local color = {
	white_underline = function(s: string)
		return `\27[1;4m{s}\27[0m`
	end,

	white = function(s: string)
		return `\27[37;1m{s}\27[0m`
	end,

	green = function(s: string)
		return `\27[32;1m{s}\27[0m`
	end,

	red = function(s: string)
		return `\27[31;1m{s}\27[0m`
	end,

	yellow =  function(s: string)
		return `\27[33;1m{s}\27[0m`
	end,

	red_highlight = function(s: string)
		return `\27[41;1;30m{s}\27[0m`
	end,

	green_highlight = function(s: string)
		return `\27[42;1;30m{s}\27[0m`
	end,

	gray = function(s: string)
		return `\27[30;1m{s}\27[0m`
	end,
}

local function convert_units(unit: string, value: number): (number, string)
	local prefix_colors = {
		[4] = color.red,
		[3] = color.red,
		[2] = color.yellow,
		[1] = color.yellow,
		[0] = color.green,
		[-1] = color.red,
		[-2] = color.yellow,
		[-3] = color.green,
		[-4] = color.red
	}

	local prefixes = {
		[4] = "T",
		[3] ="G",
		[2] ="M",
		[1] = "k",
		[0] = " ",
		[-1] = "m",
		[-2] = "u",
		[-3] = "n",
		[-4] = "p"
	}

	local order = 0

	while value >= 1000 do
		order += 1
		value /= 1000
	end

	while value ~= 0 and value < 1 do
		order -= 1
		value *= 1000
	end

	if value >= 100 then
		value = math.floor(value)
	elseif value >= 10 then
		value = math.floor(value * 1e1) / 1e1
	elseif value >= 1 then
		value = math.floor(value * 1e2) / 1e2
	end

	return value, prefix_colors[order](prefixes[order] .. unit)
end

local WALL = color.white("│")

--------------------------------------------------------------------------------
-- Testing
--------------------------------------------------------------------------------

type Test = {
	name: string,
	case: Case?,
	cases: { Case },
	duration: number,
	error: {
		message: string,
		trace: string
	}?
}

type Case = {
	name: string,
	result: number,
	line: number?
}

local PASS, FAIL, NONE, ERROR = 1, 2, 3, 4

local skip: string?
local test: Test?
local tests: { Test } = {}

local function output_test_result(test: Test)
	print(color.white_underline(string.lower(test.name)))

	for _, case in test.cases do
		local status = ({
			[PASS] = color.green "PASS",
			[FAIL] = color.red "FAIL",
			[NONE] = color.yellow "NONE",
			[ERROR] = color.red "FAIL"
		})[case.result]

		local line = case.result == FAIL and color.red(`{case.line}:`) or ""

		print(`{status}{WALL} {line}{color.white(case.name)}`)
	end

	if test.error then
		print(color.white "error: " .. color.red(test.error.message))
		print(color.white "trace: " .. color.red(test.error.trace))
	else
		print()
	end
end

local function CASE(name: string)
	assert(test, "no active test")

	local case = {
		name = name,
		result = NONE
	}

	test.case = case
	table.insert(test.cases, case)
end

local function CHECK<T>(value: T, stack: number?): T
	assert(test, "no active test")
	local case = test.case

	if not case then
		CASE ""
		case = test.case
	end

	assert(case, "no active case")

	if case.result ~= FAIL then
		case.result = value and PASS or FAIL
		case.line = debug.info(stack and stack + 1 or 2, "l")
	end

	return value
end

local function TEST(name: string, fn: () -> ())
	if skip and name ~= skip then return end

	local active = test
	assert(not active, "cannot start test while another test is in progress")

	test = {
		name = name,
		cases = {},
		duration = 0
	}; assert(test)

	table.insert(tests, test)

	local start = os.clock()
	local err
	local success = xpcall(fn, function(m: string)
		err = { message = m, trace = debug.traceback(nil, 2) }
	end)
	test.duration = os.clock() - start

	if not test.case then CASE "" end
	assert(test.case, "no active case")

	if not success then
		test.case.result = ERROR
		test.error = err
	end

	test = nil
end

local function FINISH(): boolean
	local success = true
	local total_cases = 0
	local passed_cases = 0
	local duration = 0

	for _, test in tests do
		duration += test.duration
		for _, case in test.cases do
			total_cases += 1
			if case.result == PASS or case.result == NONE then
				passed_cases += 1
			else
				success = false
			end
		end

		output_test_result(test)
	end

	print(color.white(string.format(
		`{passed_cases}/{total_cases} test cases passed in %.3f ms.`,
		duration*1e3
	)))

	local fails = total_cases - passed_cases

	print(
		(
			fails > 0
			and color.red
			or color.green
		)(`{fails} {fails == 1 and "fail" or "fails"}`)
	)

	return success, table.clear(tests)
end

local function SKIP(name: string)
	assert(not test, "cannot skip during test")
	skip = name
end

--------------------------------------------------------------------------------
-- Benchmarking
--------------------------------------------------------------------------------

type Bench = {
	time_start: number?,
	memory_start: number?,
	iterations: number?
}

local bench: Bench?

function START(iter: number?): number
	local n = iter or 1
	assert(n > 0, "iterations must be greater than 0")
	assert(bench, "no active benchmark")
	assert(not bench.time_start, "clock was already started")

	bench.iterations = n
	bench.memory_start = gcinfo()
	bench.time_start = os.clock()
	return n
end

local function BENCH(name: string, fn: () -> ())
	local active = bench
	assert(not active, "a benchmark is already in progress")

	bench = {}; assert(bench)

	;(collectgarbage :: any)("collect")

	local mem_start = gcinfo()
	local time_start = os.clock()
	local err_msg: string?

	local success = xpcall(fn, function(m: string)
		err_msg = m .. debug.traceback(nil, 2)
	end)

	local time_stop = os.clock()
	local mem_stop = gcinfo()

	if not success then
		print(`{WALL}{color.red("ERROR")}{WALL} {name}`)
		print(color.gray(err_msg :: string))
	else
		time_start = bench.time_start or time_start
		mem_start = bench.memory_start or mem_start

		local n = bench.iterations or 1
		local d, d_unit = convert_units("s", (time_stop - time_start) / n)
		local a, a_unit = convert_units("B", math.round((mem_stop - mem_start) / n * 1e3))

		local function round(x: number): string
			return x > 0 and x < 10 and (x - math.floor(x)) > 0
				and string.format("%2.1f", x)
				or  string.format("%3.f", x)
		end

		print(string.format(
			`%s %s %s %s{WALL} %s`,
			color.gray(round(d)),
			d_unit,
			color.gray(round(a)),
			a_unit,
			color.gray(name)
		))
	end

	bench = nil
end

--------------------------------------------------------------------------------
-- Printing
--------------------------------------------------------------------------------

local function print2(v: unknown)
	type Buffer = { n: number, [number]: string }
	type Cyclic = { n: number, [{}]: number }

	-- overkill concatenationless string buffer
	local function tos(value: any, stack: number, str: Buffer, cyclic: Cyclic)
		local TAB = "    "
		local indent = table.concat(table.create(stack, TAB))

		if type(value) == "string" then
			local n = str.n
			str[n + 1] = "\""
			str[n + 2] = value
			str[n + 3] = "\""
			str.n = n + 3
		elseif type(value) ~= "table" then
			local n = str.n
			str[n + 1] = value == nil and "nil" or tostring(value)
			str.n = n + 1
		elseif next(value) == nil then
			local n = str.n
			str[n + 1] = "{}"
			str.n = n + 1
		else -- is table
			local tabbed_indent = indent .. TAB

			if cyclic[value] then
				str.n += 1
				str[str.n] = color.gray(`CYCLIC REF {cyclic[value]}`)
				return
			else
				cyclic.n += 1
				cyclic[value] = cyclic.n
			end

			str.n += 3
			str[str.n - 2] = "{ "
			str[str.n - 1] = color.gray(tostring(cyclic[value]))
			str[str.n - 0] = "\n"

			local i, v = next(value, nil)
			while v ~= nil do
				local n = str.n
				str[n + 1] = tabbed_indent

				if type(i) ~= "string" then
					str[n + 2] = "["
					str[n + 3] = tostring(i)
					str[n + 4] = "]"
					n += 4
				else
					str[n + 2] = tostring(i)
					n += 2
				end

				str[n + 1] = " = "
				str.n = n + 1

				tos(v, stack + 1, str, cyclic)

				i, v = next(value, i)

				n = str.n
				str[n + 1] = v ~= nil and ",\n" or "\n"
				str.n = n + 1
			end

			local n = str.n
			str[n + 1] = indent
			str[n + 2] = "}"
			str.n = n + 2
		end
	end

	local str = { n = 0 }
	local cyclic = { n = 0 }
	tos(v, 0, str, cyclic)
	print(table.concat(str))
end

--------------------------------------------------------------------------------
-- Equality
--------------------------------------------------------------------------------

local function shallow_eq(a: {}, b: {}): boolean
	if #a ~= #b then return false end

	for i, v in next, a do
		if b[i] ~= v then
			return false
		end
	end

	for i, v in next, b do
		if a[i] ~= v then
			return false
		end
	end

	return true
end

local function deep_eq(a: {}, b: {}): boolean
	if #a ~= #b then return false end

	for i, v in next, a do
		if type(b[i]) == "table" and type(v) == "table" then
			if deep_eq(b[i], v) == false then return false end
		elseif b[i] ~= v then
			return false
		end
	end

	for i, v in next, b do
		if type(a[i]) == "table" and type(v) == "table" then
			if deep_eq(a[i], v) == false then return false end
		elseif a[i] ~= v then
			return false
		end
	end

	return true
end

return table.freeze({
	test = function()
		return TEST, CASE, CHECK, FINISH, SKIP
	end,
	benchmark = function()
		return BENCH, START
	end,

	seq = shallow_eq,
	print = print2,
	deq = deep_eq,
	color = color
})


--!strict
--!native

--[[
MAD STUDIO

-[Sera]---------------------------------------

	Low-level schematized serialization library

	Members:

		Sera.Boolean

		Sera.Uint8
		Sera.Uint16
		Sera.Uint32
		Sera.Int8
		Sera.Int16
		Sera.Int32
		Sera.Float32
		Sera.Float64

		Sera.CFrame -- 48 bytes; Full precision
		Sera.LossyCFrame -- 28 bytes; Less than 0.0005 degree precision error for rotation
		Sera.Vector3
		Sera.Color3
		Sera.ColorV3 -- Input and output are Vector3's with [0, 1] color values

		Sera.String8  -- Max 255 size
		Sera.String16 -- Max 65,535 size
		Sera.String32 -- Max 4,294,967,295 size

		Sera.Buffer8  -- Max 255 size
		Sera.Buffer16 -- Max 65,535 size
		Sera.Buffer32 -- Max 4,294,967,295 size

		Sera.Angle8 -- (Unsigned) Radian representation in one byte while preserving cardinal direction accuracy (Every 45 degrees)

	Functions:

		Sera.Schema(params): Schema -- Maximum 255 fields
			params:
				{
					["FieldName"]: SeraType,
					...
				}

		-- "Serialize", "Push", "DeltaSerialize" and "DeltaPush" will return first argument if succeeded,
		-- Or return an error message as the second argument if failed.

		-- When all fields are present (Non-schema fields are ignored; Missing fields will throw an error):

		Sera.Deserialize(schema: Schema, b: buffer, offset: number?): ({[string]: any}, offset: number)
		Sera.Serialize(schema: Schema, t: {[string]: any}): (buffer?, err: string?)
		Sera.Push(schema: Schema, t: {[string]: any}, b: buffer, offset: number?): (offset: number?, err: string?)

		-- When only some fields are present (Non-schema fields will throw an error):

		Sera.DeltaDeserialize(schema: Schema, b: buffer, offset: number?): ({[string]: any}, offset: number)
		Sera.DeltaSerialize(schema: Schema, t: {[string]: any}): (buffer?, err: string?)
		Sera.DeltaPush(schema: Schema, t: {[string]: any}, b: buffer, offset: number?): (offset: number?, err: string?)

		-- Delta serialization has an additional size cost of (n + 1) bytes where "n" is the number of fields present
		-- in a table that's being serialized (1 byte for present field count, 1 enumerating byte for each field)

--]]

local BIG_BUFFER_SIZE = 1000000 -- "Serialize" and "DeltaSerialize" will fail if resulting buffer size is larger than this value

local SIZE_8 = 2 ^ 8 - 1
local SIZE_16 = 2 ^ 16 - 1
local SIZE_32 = 2 ^ 32 - 1

local MAX_FIELDS = 2 ^ 8 - 1

local BB = buffer.create(BIG_BUFFER_SIZE) -- Big reusable buffer

----- Public -----

export type Ser = (b: buffer, offset: number, value: any) -> (number)
export type Des = (b: buffer, offset: number) -> (any, number)

export type SeraType = {
	Name: string,
	Ser: Ser,
	Des: Des,
}

type Field = {Key: string, Name: string, Ser: Ser, Des: Des, Index: number}

export type Schema = {
	Numeric: {Field},
	String: {[string]: Field},
}

local Sera = {
	BB = BB,
}

Sera.Boolean = table.freeze({
	Name = "Boolean",
	Ser = function(b: buffer, offset: number, value: boolean): number
		if value == true then
			buffer.writeu8(b, offset, 1)
		elseif value == false then
			buffer.writeu8(b, offset, 0)
		else
			error("Expected boolean")
		end
		return offset + 1
	end,
	Des = function(b: buffer, offset: number): (boolean, number)
		return buffer.readu8(b, offset) == 1, offset + 1
	end,
})

Sera.Uint8 = table.freeze({
	Name = "Uint8",
	Ser = function(b: buffer, offset: number, value: number): number
		buffer.writeu8(b, offset, value)
		return offset + 1
	end,
	Des = function(b: buffer, offset: number): (number, number)
		return buffer.readu8(b, offset), offset + 1
	end,
})

Sera.Uint16 = table.freeze({
	Name = "Uint16",
	Ser = function(b: buffer, offset: number, value: number): number
		buffer.writeu16(b, offset, value)
		return offset + 2
	end,
	Des = function(b: buffer, offset: number): (number, number)
		return buffer.readu16(b, offset), offset + 2
	end,
})

Sera.Uint32 = table.freeze({
	Name = "Uint32",
	Ser = function(b: buffer, offset: number, value: number): number
		buffer.writeu32(b, offset, value)
		return offset + 4
	end,
	Des = function(b: buffer, offset: number): (number, number)
		return buffer.readu32(b, offset), offset + 4
	end,
})

Sera.Int8 = table.freeze({
	Name = "Int8",
	Ser = function(b: buffer, offset: number, value: number): number
		buffer.writei8(b, offset, value)
		return offset + 1
	end,
	Des = function(b: buffer, offset: number): (number, number)
		return buffer.readi8(b, offset), offset + 1
	end,
})

Sera.Int16 = table.freeze({
	Name = "Int16",
	Ser = function(b: buffer, offset: number, value: number): number
		buffer.writei16(b, offset, value)
		return offset + 2
	end,
	Des = function(b: buffer, offset: number): (number, number)
		return buffer.readi16(b, offset), offset + 2
	end,
})

Sera.Int32 = table.freeze({
	Name = "Int32",
	Ser = function(b: buffer, offset: number, value: number): number
		buffer.writei32(b, offset, value)
		return offset + 4
	end,
	Des = function(b: buffer, offset: number): (number, number)
		return buffer.readi32(b, offset), offset + 4
	end,
})

Sera.Float32 = table.freeze({
	Name = "Float32",
	Ser = function(b: buffer, offset: number, value: number): number
		buffer.writef32(b, offset, value)
		return offset + 4
	end,
	Des = function(b: buffer, offset: number): (number, number)
		return buffer.readf32(b, offset), offset + 4
	end,
})

Sera.Float64 = table.freeze({
	Name = "Float64",
	Ser = function(b: buffer, offset: number, value: number): number
		buffer.writef64(b, offset, value)
		return offset + 8
	end,
	Des = function(b: buffer, offset: number): (number, number)
		return buffer.readf64(b, offset), offset + 8
	end,
})

Sera.CFrame = table.freeze({
	Name = "CFrame",
	Ser = function(b: buffer, offset: number, value: CFrame): number
		local v0, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11 = value:GetComponents()
		buffer.writef32(b, offset, v0)
		buffer.writef32(b, offset + 4, v1)
		buffer.writef32(b, offset + 8, v2)
		buffer.writef32(b, offset + 12, v3)
		buffer.writef32(b, offset + 16, v4)
		buffer.writef32(b, offset + 20, v5)
		buffer.writef32(b, offset + 24, v6)
		buffer.writef32(b, offset + 28, v7)
		buffer.writef32(b, offset + 32, v8)
		buffer.writef32(b, offset + 36, v9)
		buffer.writef32(b, offset + 40, v10)
		buffer.writef32(b, offset + 44, v11)
		return offset + 48
	end,
	Des = function(b: buffer, offset: number): (CFrame, number)
		return CFrame.new(
			buffer.readf32(b, offset),
			buffer.readf32(b, offset + 4),
			buffer.readf32(b, offset + 8),
			buffer.readf32(b, offset + 12),
			buffer.readf32(b, offset + 16),
			buffer.readf32(b, offset + 20),
			buffer.readf32(b, offset + 24),
			buffer.readf32(b, offset + 28),
			buffer.readf32(b, offset + 32),
			buffer.readf32(b, offset + 36),
			buffer.readf32(b, offset + 40),
			buffer.readf32(b, offset + 44)
		), offset + 48
	end,
})

Sera.LossyCFrame = table.freeze({
	Name = "LossyCFrame",
	Ser = function(b: buffer, offset: number, value: CFrame): number
		local axis, angle = value:ToAxisAngle()
		buffer.writef32(b, offset, value.X)
		buffer.writef32(b, offset + 4, value.Y)
		buffer.writef32(b, offset + 8, value.Z)
		buffer.writef32(b, offset + 12, axis.X)
		buffer.writef32(b, offset + 16, axis.Y)
		buffer.writef32(b, offset + 20, axis.Z)
		buffer.writef32(b, offset + 24, angle)
		return offset + 28
	end,
	Des = function(b: buffer, offset: number): (CFrame, number)
		return CFrame.fromAxisAngle(
			Vector3.new(
				buffer.readf32(b, offset + 12),
				buffer.readf32(b, offset + 16),
				buffer.readf32(b, offset + 20)
			),
			buffer.readf32(b, offset :: number + 24)
			)
			+ Vector3.new(
				buffer.readf32(b, offset),
				buffer.readf32(b, offset + 4),
				buffer.readf32(b, offset + 8)
			), offset + 28
	end,
})

Sera.Vector3 = table.freeze({
	Name = "Vector3",
	Ser = function(b: buffer, offset: number, value: Vector3): number
		buffer.writef32(b, offset, value.X)
		buffer.writef32(b, offset + 4, value.Y)
		buffer.writef32(b, offset + 8, value.Z)
		return offset + 12
	end,
	Des = function(b: buffer, offset: number): (Vector3, number)
		return Vector3.new(
			buffer.readf32(b, offset),
			buffer.readf32(b, offset + 4),
			buffer.readf32(b, offset + 8)
		), offset + 12
	end,
})

Sera.Color3 = table.freeze({
	Name = "Color3",
	Ser = function(b: buffer, offset: number, value: Color3): number
		buffer.writeu8(b, offset, value.R * 255)
		buffer.writeu8(b, offset + 1, value.G * 255)
		buffer.writeu8(b, offset + 2, value.B * 255)
		return offset + 3
	end,
	Des = function(b: buffer, offset: number): (Color3, number)
		return Color3.fromRGB(
			buffer.readu8(b, offset),
			buffer.readu8(b, offset + 1),
			buffer.readu8(b, offset + 2)
		), offset + 3
	end,
})

Sera.ColorV3 = table.freeze({
	Name = "ColorV3",
	Ser = function(b: buffer, offset: number, value: Vector3): number
		buffer.writeu8(b, offset, value.X * 255)
		buffer.writeu8(b, offset + 1, value.Y * 255)
		buffer.writeu8(b, offset + 2, value.Z * 255)
		return offset + 3
	end,
	Des = function(b: buffer, offset: number): (Vector3, number)
		return Vector3.new(
			buffer.readu8(b, offset) / 255,
			buffer.readu8(b, offset + 1) / 255,
			buffer.readu8(b, offset + 2) / 255
		), offset + 3
	end,
})

Sera.String8 = table.freeze({
	Name = "String8",
	Ser = function(b: buffer, offset: number, value: string): number
		local length = value:len()
		if length > SIZE_8 then
			error(`String too long`)
		end
		buffer.writeu8(b, offset, length)
		buffer.writestring(b, offset + 1, value)
		return offset + 1 + length
	end,
	Des = function(b: buffer, offset: number): (string, number)
		local length = buffer.readu8(b, offset)
		return buffer.readstring(b, offset + 1, length), offset + 1 + length
	end,
})

Sera.String16 = table.freeze({
	Name = "String16",
	Ser = function(b: buffer, offset: number, value: string): number
		local length = value:len()
		if length > SIZE_16 then
			error(`String too long`)
		end
		buffer.writeu16(b, offset, length)
		buffer.writestring(b, offset + 2, value)
		return offset + 2 + length
	end,
	Des = function(b: buffer, offset: number): (string, number)
		local length = buffer.readu16(b, offset)
		return buffer.readstring(b, offset + 2, length), offset + 2 + length
	end,
})

Sera.String32 = table.freeze({
	Name = "String32",
	Ser = function(b: buffer, offset: number, value: string): number
		local length = value:len()
		if length > SIZE_32 then
			error(`String too long`)
		end
		buffer.writeu32(b, offset, length)
		buffer.writestring(b, offset + 4, value)
		return offset + 4 + length
	end,
	Des = function(b: buffer, offset: number): (string, number)
		local length = buffer.readu32(b, offset)
		return buffer.readstring(b, offset + 4, length), offset + 4 + length
	end,
})

Sera.Buffer8 = table.freeze({
	Name = "Buffer8",
	Ser = function(b: buffer, offset: number, value: buffer): number
		local length = buffer.len(value)
		if length > SIZE_8 then
			error(`Buffer too long`)
		end
		buffer.writeu8(b, offset, length)
		buffer.copy(b, offset + 1, value)
		return offset + 1 + length
	end,
	Des = function(b: buffer, offset: number): (buffer, number)
		local length = buffer.readu8(b, offset)
		local value = buffer.create(length)
		buffer.copy(value, 0, b, offset + 1, length)
		return value, offset + 1 + length
	end,
})

Sera.Buffer16 = table.freeze({
	Name = "Buffer16",
	Ser = function(b: buffer, offset: number, value: buffer): number
		local length = buffer.len(value)
		if length > SIZE_16 then
			error(`Buffer too long`)
		end
		buffer.writeu16(b, offset, length)
		buffer.copy(b, offset + 2, value)
		return offset + 2 + length
	end,
	Des = function(b: buffer, offset: number): (buffer, number)
		local length = buffer.readu16(b, offset)
		local value = buffer.create(length)
		buffer.copy(value, 0, b, offset + 2, length)
		return value, offset + 2 + length
	end,
})

Sera.Buffer32 = table.freeze({
	Name = "Buffer32",
	Ser = function(b: buffer, offset: number, value: buffer): number
		local length = buffer.len(value)
		if length > SIZE_32 then
			error(`Buffer too long`)
		end
		buffer.writeu32(b, offset, length)
		buffer.copy(b, offset + 4, value)
		return offset + 4 + length
	end,
	Des = function(b: buffer, offset: number): (buffer, number)
		local length = buffer.readu32(b, offset)
		local value = buffer.create(length)
		buffer.copy(value, 0, b, offset + 4, length)
		return value, offset + 4 + length
	end,
})

do

	local rad2 = math.pi * 2
	local angle8range = math.rad(359)

	local function DegEncode(deg: number): number
		return math.round((deg % rad2) * 255 / angle8range)
	end

	local DegDecodeLookup: {number} = {}

	for i = 0, 255 do
		DegDecodeLookup[i] = i * angle8range / 255
	end

	for i = 0, 315, 45 do
		local r = math.rad(i)
		local index = DegEncode(r)
		DegDecodeLookup[index] = r
	end

	Sera.Angle8 = table.freeze({
		Name = "Angle8",
		Ser = function(b: buffer, offset: number, value: number): number
			buffer.writeu8(b, offset, DegEncode(value))
			return offset + 1
		end,
		Des = function(b: buffer, offset: number): (number, number)
			return DegDecodeLookup[buffer.readu8(b, offset)], offset + 1
		end,
	})

end

----

-- Returns dictionary of deserialized schema.
function Sera.Deserialize(schema: Schema, b: buffer, offset: number?): ({[string]: any}, number)

	offset = offset or 0
	local result = {}

	for _, field in schema.Numeric do
		result[field.Key], offset = field.Des(b, offset :: number)
	end

	return result, offset :: number

end

local function SerializeUnsafe(schema: Schema, t: {[string]: any}): number

	local offset = 0

	for _, field in schema.Numeric do
		offset = field.Ser(BB, offset, t[field.Key])
	end

	return offset

end

-- Returns new buffer of serialized schema or error message if fail.
function Sera.Serialize(schema: Schema, t: any): (buffer?, string?)

	local success, offset = pcall(SerializeUnsafe, schema, t)

	if not success then

		local ser_success
		offset = 0

		for _, field in schema.Numeric do

			local value = t[field.Key]

			if value ~= nil then
				ser_success, offset = pcall(field.Ser, BB, offset, value)
				if not ser_success then
					return nil, `Couldn't serialize field "{field.Key}" (Expected: {field.Name};Received: "{typeof(value)}"); Message: {offset}`
				end
			else
				return nil, `Missing field "{field.Key}"`
			end

		end

		return nil, `Unknown error`

	end

	local b = buffer.create(offset)
	buffer.copy(b, 0, BB, 0, offset)

	return b

end

local function PushUnsafe(schema: Schema, t: {[string]: any}, b: buffer, offset: number): number

	for _, field in schema.Numeric do
		offset = field.Ser(b, offset, t[field.Key])
	end

	return offset

end

-- Pushes serialized schema to an existing buffer; Returns new buffer offset or error message if fail.
function Sera.Push(schema: Schema, t: any, b: buffer, offset: number?): (number?, string?)

	offset = offset or 0
	local original_offset = offset :: number
	local success
	success, offset = pcall(PushUnsafe, schema, t, b, offset :: number)

	if not success then

		local ser_success
		offset = original_offset

		for _, field in schema.Numeric do

			local value = t[field.Key]

			if value ~= nil then
				ser_success, offset = pcall(field.Ser, b, offset :: number, value)
				if not ser_success then
					return nil, `Couldn't serialize field "{field.Key}" (Expected: {field.Name};Received: "{typeof(value)}"); Message: {offset}`
				end
			else
				return nil, `Missing field "{field.Key}"`
			end

		end

		return nil, `Unknown error`

	end

	return offset

end

-- Returns dictionary of deserialized deltas.
function Sera.DeltaDeserialize(schema: Schema, b: buffer, offset: number?): ({[string]: any}, number)

	offset = (offset or 0) + 1
	local field_count = buffer.readu8(b, offset :: number - 1)
	local result = {}

	local lookup = schema.Numeric

	for _ = 1, field_count do
		local field = lookup[buffer.readu8(b, offset :: number)]
		result[field.Key], offset = field.Des(b, offset :: number + 1)
	end

	return result, offset :: number

end

local function DeltaSerializeUnsafe(schema: Schema, t: {[string]: any}): number

	local field_count = 0
	local offset = 1

	local lookup = schema.String

	for key in t do
		field_count += 1
		local field = lookup[key]
		buffer.writeu8(BB, offset, field.Index)
		offset = field.Ser(BB, offset + 1, t[field.Key])
	end

	buffer.writeu8(BB, 0, field_count)

	return offset

end

-- Returns new buffer of serialized deltas or error message if fail.
function Sera.DeltaSerialize(schema: Schema, t: any): (buffer?, string?)

	local success, offset = pcall(DeltaSerializeUnsafe, schema, t)

	if not success then

		local ser_success
		offset = 1

		local lookup = schema.String

		for key, value in t do

			local field = lookup[key]

			if field == nil then
				return nil, `Key "{key}" not specified in schema`
			end

			buffer.writeu8(BB, offset, field.Index)
			ser_success, offset = pcall(field.Ser, BB, offset + 1, value)
			if not ser_success then
				return nil, `Couldn't serialize field "{field.Key}" (Expected: {field.Name};Received: "{typeof(value)}"); Message: {offset}`
			end

		end

		return nil, `Unknown error`

	end

	local b = buffer.create(offset)
	buffer.copy(b, 0, BB, 0, offset)

	return b

end

local function DeltaPushUnsafe(schema: Schema, t: {[string]: any}, b: buffer, offset: number): number

	local field_count = 0
	local start_offset = offset
	offset += 1

	local lookup = schema.String

	for key in t do
		field_count += 1
		local field = lookup[key]
		buffer.writeu8(b, offset, field.Index)
		offset = field.Ser(b, offset + 1, t[field.Key])
	end

	buffer.writeu8(b, start_offset, field_count)

	return offset

end

-- Pushes serialized deltas to an existing buffer; Returns new buffer offset or error message if fail.
function Sera.DeltaPush(schema: Schema, t: any, b: buffer, offset: number?): (number?, string?)

	offset = offset or 0
	local success
	success, offset = pcall(DeltaPushUnsafe, schema, t, b, offset :: number)

	if not success then

		local ser_success
		offset = 1

		local lookup = schema.String

		for key, value in t do

			local field = lookup[key]

			if field == nil then
				return nil, `Key "{key}" not specified in schema`
			end

			buffer.writeu8(b, offset :: number, field.Index)
			ser_success, offset = pcall(field.Ser, b, offset :: number + 1, value)
			if not ser_success then
				return nil, `Couldn't serialize field "{field.Key}" (Expected: {field.Name};Received: "{typeof(value)}"); Message: {offset}`
			end

		end

		return nil, `Unknown error`

	end

	return offset

end

function Sera.Schema(params: {[string]: SeraType}): Schema

	local fields: {Field} = {}

	for key, sera_type: SeraType in params do
		if typeof(key) ~= "string" then
			error(`[{script.Name}]: Expected string for field name; Received "{typeof(key)}"`)
		end
		if typeof(sera_type) ~= "table" or typeof(sera_type.Ser) ~= "function"
			or typeof(sera_type.Des) ~= "function" or typeof(sera_type.Name) ~= "string" then
			error(`[{script.Name}]: Expected SeraType for field "{key}"`)
		end
		table.insert(fields, {Key = key, Name = sera_type.Name, Ser = sera_type.Ser, Des = sera_type.Des, Index = 0})
	end

	if #fields == 0 then
		error(`[{script.Name}]: Schema must have fields`)
	end

	if #fields > MAX_FIELDS then
		error(`[{script.Name}]: Schema exceeded {MAX_FIELDS} fields; Received {#fields} fields`)
	end

	table.sort(fields, function(a, b)
		return a.Key < b.Key
	end)

	local string_fields: {[string]: Field} = {}

	for index, field in fields do
		field.Index = index
		string_fields[field.Key] = field
	end

	return table.freeze({
		Numeric = fields,
		String = string_fields,
	})

end

return Sera


--!strict

local Sera = require(game.ReplicatedStorage.Sera)

-- Defining a schema:

local CustomType = table.freeze({
	Name = "TwoNumbers",
	Ser = function(b: buffer, offset: number, value: {number}): number
		buffer.writef64(b, offset, value[1])
		buffer.writef64(b, offset + 8, value[2])
		return offset + 16
	end,
	Des = function(b: buffer, offset: number): ({number}, number)
		return {
			buffer.readf64(b, offset),
			buffer.readf64(b, offset + 8)
		}, offset + 16
	end,
})

local TestSchema = Sera.Schema({
	Health = Sera.Int16,
	Position = Sera.Vector3,
	Name = Sera.String8,
	TwoNumbers = CustomType
})

-- Trying out serialization of a dictionary with all fields present:

do

	local serialized, error_message = Sera.Serialize(TestSchema, {
		Health = 150,
		Position = Vector3.new(1001, 69, 0.1),
		Name = "loleris",
		TwoNumbers = {1234.56, 0.001},
	})

	if error_message ~= nil then
		error(error_message)
	end

	local deserialized1, offset = Sera.Deserialize(TestSchema, serialized :: buffer)

	local b = buffer.create(offset + 10)
	Sera.Push(TestSchema, deserialized1, b, 10)

	local deserialized2 = Sera.Deserialize(TestSchema, b, 10)

	print(`All field serialization:`)
	print(`Serialized buffer size: {buffer.len(serialized :: buffer)} bytes`)
	print(`Deserialized result #1:`, deserialized1)
	print(`Deserialized result #2:`, deserialized2)

end

-- Trying out serialization of a dictionary with only some fields present:

do

	local serialized, error_message = Sera.DeltaSerialize(TestSchema, {
		Position = Vector3.new(900, 60, 3),
		TwoNumbers = {-20, -1.5},
	})

	if error_message ~= nil then
		error(error_message)
	end

	local deserialized1, offset = Sera.DeltaDeserialize(TestSchema, serialized :: buffer)

	local b = buffer.create(offset + 10)
	Sera.DeltaPush(TestSchema, deserialized1, b, 10)

	local deserialized2 = Sera.DeltaDeserialize(TestSchema, b, 10)

	print(`Partial field serialization:`)
	print(`Serialized buffer size: {buffer.len(serialized :: buffer)} bytes`)
	print(`Deserialized result #1:`, deserialized1)
	print(`Deserialized result #2:`, deserialized2)

end


-- This file was @generated by Tarmac. It is not intended for manual editing.
return {
	["MauiLogo-DarkMode"] = "rbxassetid://12558365376",
	["MauiLogo-LightMode"] = "rbxassetid://12558365512",
}


print("Should be running on the server, but we removed context checks via the flag!")


return {
    FormatVersion = 1,

    Output = {
        Directory = function()
            return script.Parent
        end,
        ScriptName = "MauiGeneratedOutput"
    }
}


return {
    FormatVersion = 1,

    Output = {
        Directory = script.Parent,
        ScriptName = "MauiGeneratedOutput"
    }
}


local RunService = game:GetService("RunService")
local Src = script:FindFirstAncestor("ultimate-list")

local DataSources = require(Src.DataSources)
local Dimensions = require(Src.Dimensions)
local React = require(Src.Parent.React)
local ReactBindingsRenderer = require(Src.Renderers.ReactBindingsRenderer)
local ReactStateRenderer = require(Src.Renderers.ReactStateRenderer)
local Renderers = require(Src.Renderers)
local createVirtualizedListController = require(Src.createVirtualizedListController)
local exhaustiveMatch = require(Src.Util.exhaustiveMatch)

local e = React.createElement

local function ScrollingFrame<T>(
	props: {
		dataSource: DataSources.DataSource<T>,
		dimensions: Dimensions.Dimensions<T>,
		renderer: Renderers.Renderer<T>,

		-- Will use index as key otherwise, which is fine for append-only cases.
		getKey: ((value: T, index: number) -> string)?,

		direction: "x" | "y",

		scrollingFrameRef: React.Ref<ScrollingFrame>?,
		onAbsoluteWindowSizeChanged: ((newWindowSize: Vector2) -> ())?,
		onScrollAxisChanged: ((newScrollAxis: number) -> ())?,

		tag: string?,
		native: { [any]: any }?,
	}
)
	local virtualizedListController = React.useState(function()
		return createVirtualizedListController(props.dataSource, props.dimensions, props.direction)
	end)

	React.useEffect(function()
		return function()
			return virtualizedListController.destroy()
		end
	end, {})

	local canvasSizeBinding, setCanvasSize = React.useBinding(virtualizedListController.getCanvasSize())
	local windowSizeBinding, setWindowSize = React.useBinding(Vector2.zero)
	local scrollAxisBinding, setScrollAxis = React.useBinding(0)

	React.useEffect(function()
		return virtualizedListController.bindToUpdate(function()
			setCanvasSize(virtualizedListController.getCanvasSize())
		end)
	end, {})

	local onAbsoluteWindowSizeChanged = React.useCallback(function(instance: ScrollingFrame)
		setWindowSize(instance.AbsoluteWindowSize)
		virtualizedListController.setWindowSize(instance.AbsoluteWindowSize)

		if props.onAbsoluteWindowSizeChanged ~= nil then
			props.onAbsoluteWindowSizeChanged(instance.AbsoluteWindowSize)
		end
	end, { props.direction, props.onAbsoluteWindowSizeChanged } :: { unknown })

	local onCanvasPositionChanged = React.useCallback(function(instance: ScrollingFrame)
		local scrollAxis = if props.direction == "x"
			then instance.CanvasPosition.X
			elseif props.direction == "y" then instance.CanvasPosition.Y
			else exhaustiveMatch(props.direction)

		-- In deferred signals, position properties update only on the frame *after*,
		-- which causes visible flickering.
		RunService.Heartbeat:Once(function()
			virtualizedListController.setScrollAxis(scrollAxis)
			setScrollAxis(scrollAxis)

			if props.onScrollAxisChanged ~= nil then
				props.onScrollAxisChanged(scrollAxis)
			end
		end)
	end, { props.direction, props.onScrollAxisChanged } :: { unknown })

	React.useEffect(function()
		virtualizedListController.setDataSource(props.dataSource)
	end, { props.dataSource })

	React.useEffect(function()
		virtualizedListController.setDimensions(props.dimensions)
	end, { props.dimensions })

	local scrollingFrameProps = {
		Size = UDim2.fromScale(1, 1),

		CanvasSize = canvasSizeBinding,

		-- Luau: No multiple indexers
		[React.Change.AbsoluteWindowSize] = onAbsoluteWindowSizeChanged :: any,
		[React.Change.CanvasPosition] = onCanvasPositionChanged :: any,
		[React.Tag] = props.tag,

		ref = props.scrollingFrameRef,
	}

	if props.native ~= nil then
		for key, value in props.native do
			if scrollingFrameProps[key] ~= nil then
				error(`{key} is already used by UltimateList`)
			end

			scrollingFrameProps[key] = value
		end
	end

	return e("Frame", {
		BackgroundTransparency = 1,
		Size = UDim2.fromScale(1, 1),
	}, {
		ScrollingFrame = e("ScrollingFrame", scrollingFrameProps),

		-- This is outside of the scrolling frame because CanvasPosition updates a bit too late even
		-- in the case of bindings, showing you emptiness before we have a chance to react.
		RendererOverlay = e("Frame", {
			BackgroundTransparency = 1,
			ClipsDescendants = true,
			-- Match window size so you can still see scroll bar
			Size = windowSizeBinding:map(function(windowSize: Vector2)
				return UDim2.fromOffset(windowSize.X, windowSize.Y)
			end),
			ZIndex = 2,
		}, {
			Renderer = if props.renderer.type == "byState"
				then e(ReactStateRenderer, {
					virtualizedListController = virtualizedListController,

					config = props.renderer.config,

					dimensions = props.dimensions,
					direction = props.direction,

					callback = props.renderer.callback,
					getKey = props.getKey,

					scrollAxisBinding = scrollAxisBinding,
				})
				elseif props.renderer.type == "byBinding" then e(ReactBindingsRenderer, {
					virtualizedListController = virtualizedListController,
					scrollAxisBinding = scrollAxisBinding,

					dimensions = props.dimensions,
					direction = props.direction,

					callback = props.renderer.callback,
					getKey = props.getKey,
				})
				else exhaustiveMatch(props.renderer.type),
		}),
	})
end

return ScrollingFrame


local ScrollingFrame = require(script.ScrollingFrame)

local Components = {}

Components.ScrollingFrame = ScrollingFrame

return Components


local Src = script:FindFirstAncestor("ultimate-list")

local DataSources = require(script.Parent)
local exhaustiveMatch = require(Src.Util.exhaustiveMatch)

local DataSourceMethods = {}

function DataSourceMethods.get<T>(
	dataSource: DataSources.DataSource<T>,
	startIndex: number
): DataSources.DataSourceCursor<T>?
	assert(startIndex >= 1, "index < 1")

	if dataSource.type == "array" then
		local function get(index: number): DataSources.DataSourceCursor<T>?
			if index < 1 or index > #dataSource.array then
				return nil
			end

			return {
				before = function()
					return get(index - 1)
				end,

				value = dataSource.array[index],

				after = function()
					return get(index + 1)
				end,
			}
		end

		return get(startIndex)
	elseif dataSource.type == "mutableSource" then
		return dataSource.methods.get(startIndex)
	else
		return exhaustiveMatch(dataSource.type)
	end
end

function DataSourceMethods.getByRange<T>(dataSource: DataSources.DataSource<T>, range: Vector3): { T }
	if dataSource.type == "array" then
		return table.move(dataSource.array, range.X, range.Y, 1, {})
	elseif dataSource.type == "mutableSource" then
		if dataSource.methods.getByRange == nil then
			local items = {}

			local cursor = dataSource.methods.get(range.X)
			for _ = 0, range.Y - range.X do
				if cursor == nil then
					break
				end

				table.insert(items, cursor.value)
				cursor = cursor.after()
			end

			return items
		end

		return dataSource.methods.getByRange(range.X, range.Y)
	else
		return exhaustiveMatch(dataSource.type)
	end
end

function DataSourceMethods.back<T>(dataSource: DataSources.DataSource<T>): T?
	if dataSource.type == "array" then
		return dataSource.array[#dataSource.array]
	elseif dataSource.type == "mutableSource" then
		if dataSource.methods.back == nil then
			local cursor = dataSource.methods.get(dataSource.methods.length())
			return cursor and cursor.value
		end

		return dataSource.methods.back()
	else
		return exhaustiveMatch(dataSource.type)
	end
end

function DataSourceMethods.length<T>(dataSource: DataSources.DataSource<T>): number
	if dataSource.type == "array" then
		return #dataSource.array
	elseif dataSource.type == "mutableSource" then
		return dataSource.methods.length()
	else
		return exhaustiveMatch(dataSource.type)
	end
end

-- Shallow equals, basically just to make sure you can use `UltimateList.DataSources.array(array)` inline
function DataSourceMethods.equals<T>(left: DataSources.DataSource<T>, right: DataSources.DataSource<T>): boolean
	if left.type ~= right.type then
		return false
	end

	if left.type == "array" then
		assert(right.type == "array", "Luau")
		return left.array == right.array
	elseif left.type == "mutableSource" then
		assert(right.type == "mutableSource", "Luau")
		return left.methods == right.methods
	else
		return exhaustiveMatch(left.type)
	end
end

return DataSourceMethods


local DataSources = {}

export type DataSource<T> =
	{
		type: "array",
		array: { T },
	}
	| {
		type: "mutableSource",
		methods: MutableDataSourceMethods<T>,
	}

export type MutableDataSourceMethods<T> = {
	-- Get a cursor to the nth element
	get: (startIndex: number) -> DataSourceCursor<T>?,

	-- Get the total length of the data source
	length: () -> number,

	-- Is given a callback to run when the data source changes.
	-- Returns a destructor to disconnect the function.
	bindToChanged: (callback: () -> ()) -> () -> (),

	-- The following have default definitions, but can be specialized to be more efficient.

	-- Get the final element (defaults to get(length()))
	back: (() -> T?)?,

	-- Get the elements from startIndex to and including endIndex.
	-- Defaults to using the `after` in your cursors.
	getByRange: ((startIndex: number, endIndex: number) -> { T })?,
}

export type DataSourceCursor<T> = {
	before: () -> DataSourceCursor<T>?,
	value: T,
	after: () -> DataSourceCursor<T>?,
}

function DataSources.array<T>(array: { T }): DataSource<T>
	return {
		type = "array",
		array = array,
	}
end

function DataSources.mutableSource<T>(methods: MutableDataSourceMethods<T>): DataSource<T>
	return {
		type = "mutableSource",
		methods = methods,
	}
end

DataSources.utilities = {}

-- Creates a wrapper around a simple `(index: number) -> T?` to get
-- a function that returns a DataStoreCursor<T>? for use in `get`.
function DataSources.utilities.createGetSimpleCursor<T>(
	-- Intentionally not `T?` because we expect length to be correct
	get: (index: number) -> T,
	getLength: () -> number
): (startIndex: number) -> DataSourceCursor<T>?
	return function(startIndex: number)
		local length = getLength()

		local function getWithCursor(index: number): DataSourceCursor<T>?
			if index < 1 or index > length then
				return nil
			end

			return {
				before = function()
					return getWithCursor(index - 1)
				end,

				value = get(index),

				after = function()
					return getWithCursor(index + 1)
				end,
			}
		end

		return getWithCursor(startIndex)
	end
end

return DataSources


-- TODO: Throughout this, assert that UDims are what is expected (scale only on opposite direction)
local Src = script:FindFirstAncestor("ultimate-list")

local DataSourceMethods = require(Src.DataSources.DataSourceMethods)
local DataSources = require(Src.DataSources)
local Dimensions = require(script.Parent)
local exhaustiveMatch = require(Src.Util.exhaustiveMatch)

local DimensionsMethods = {}

function DimensionsMethods.getCanvasSize<T>(
	dimensions: Dimensions.Dimensions<T>,
	dataSource: DataSources.DataSource<T>,
	windowSize: Vector2,
	direction: "x" | "y"
): UDim2
	local back = DataSourceMethods.back(dataSource)
	if back == nil then
		return UDim2.new()
	end

	local udimRect =
		DimensionsMethods.getUDimRect(dimensions, back, DataSourceMethods.length(dataSource), windowSize, direction)
	return if direction == "x"
		then UDim2.fromOffset(udimRect.position.X.Offset + udimRect.size.X.Offset, 0)
		elseif direction == "y" then UDim2.fromOffset(0, udimRect.position.Y.Offset + udimRect.size.Y.Offset)
		else exhaustiveMatch(direction)
end

local function getUDimSizeForConsistentSize(size: number, direction: "x" | "y"): UDim2
	if direction == "x" then
		return UDim2.new(0, size, 1, 0)
	elseif direction == "y" then
		return UDim2.new(1, 0, 0, size)
	else
		return exhaustiveMatch(direction)
	end
end

local function getPositionForConsistentSize(size: number, index: number, direction: "x" | "y"): UDim2
	if direction == "x" then
		return UDim2.fromOffset(size * (index - 1), 0)
	elseif direction == "y" then
		return UDim2.fromOffset(0, size * (index - 1))
	else
		return exhaustiveMatch(direction)
	end
end

local function getPositionForConsistentUDim2(
	udim2: UDim2,
	index: number,
	windowSize: Vector2,
	direction: "x" | "y"
): UDim2
	local amountPerNonDominant = DimensionsMethods.getAmountPerNonDominantInGrid(udim2, windowSize, direction)

	if direction == "x" then
		return UDim2.new(
			0,
			(math.ceil(index / amountPerNonDominant) - 1) * udim2.X.Offset,
			udim2.Y.Scale * ((index - 1) % amountPerNonDominant),
			udim2.Y.Offset * ((index - 1) % amountPerNonDominant)
		)
	elseif direction == "y" then
		return UDim2.new(
			udim2.X.Scale * ((index - 1) % amountPerNonDominant),
			udim2.X.Offset * ((index - 1) % amountPerNonDominant),
			0,
			(math.ceil(index / amountPerNonDominant) - 1) * udim2.Y.Offset
		)
	else
		return exhaustiveMatch(direction)
	end
end

-- If direction == x: the number of rows per column.
-- If direction == y: the number of columsn per row.
function DimensionsMethods.getAmountPerNonDominantInGrid(
	udim2: UDim2,
	windowSize: Vector2,
	direction: "x" | "y"
): number
	if direction == "x" then
		return windowSize.Y // (udim2.Y.Offset + udim2.Y.Scale * windowSize.Y)
	elseif direction == "y" then
		return windowSize.X // (udim2.X.Offset + udim2.X.Scale * windowSize.X)
	else
		return exhaustiveMatch(direction)
	end
end

function DimensionsMethods.getDominantAxis(vector: Vector2, direction: "x" | "y"): number
	if direction == "x" then
		return vector.X
	elseif direction == "y" then
		return vector.Y
	else
		return exhaustiveMatch(direction)
	end
end

function DimensionsMethods.getUDimRect<T>(
	dimensions: Dimensions.Dimensions<T>,
	item: T,
	index: number,
	windowSize: Vector2,
	direction: "x" | "y"
): Dimensions.UDimRect
	if dimensions.type == "consistentSize" then
		return {
			size = getUDimSizeForConsistentSize(dimensions.size, direction),
			position = getPositionForConsistentSize(dimensions.size, index, direction),
		}
	elseif dimensions.type == "consistentUDim2" then
		return {
			size = dimensions.udim2,
			position = getPositionForConsistentUDim2(dimensions.udim2, index, windowSize, direction),
		}
	elseif dimensions.type == "getter" then
		return dimensions.callback(item, index)
	elseif dimensions.type == "spaced" then
		if dimensions.inner.type == "consistentSize" then
			return {
				size = getUDimSizeForConsistentSize(dimensions.inner.size, direction),
				position = getPositionForConsistentSize(dimensions.inner.size + dimensions.spacing, index, direction),
			}
		elseif dimensions.inner.type == "consistentUDim2" then
			return {
				size = dimensions.inner.udim2,
				position = getPositionForConsistentUDim2(
					dimensions.inner.udim2
						+ UDim2.fromOffset(
							if direction == "x" then dimensions.spacing else 0,
							if direction == "y" then dimensions.spacing else 0
						),
					index,
					windowSize,
					direction
				),
			}
		elseif dimensions.inner.type == "getter" or dimensions.inner.type == "spaced" then
			error("Unsupported spaced dimensions")
		else
			return exhaustiveMatch(dimensions.inner.type)
		end
	else
		return exhaustiveMatch(dimensions.type)
	end
end

local function getElementsDisplayedForUDim2(udim2: UDim2, windowSize: Vector2, direction: "x" | "y"): number
	local amountPerNonDominant = DimensionsMethods.getAmountPerNonDominantInGrid(udim2, windowSize, direction)
	if direction == "x" then
		local columns = windowSize.X // udim2.Y.Offset + 2
		return amountPerNonDominant * columns
	elseif direction == "y" then
		local rows = windowSize.Y // udim2.X.Offset + 2
		return amountPerNonDominant * rows
	else
		return exhaustiveMatch(direction)
	end
end

function DimensionsMethods.elementsDisplayedHint<T>(
	dimensions: Dimensions.Dimensions<T>,
	windowSize: Vector2,
	direction: "x" | "y"
): number?
	-- Uses of +2 are for supporting top and bottom clipped off
	if dimensions.type == "consistentSize" then
		return DimensionsMethods.getDominantAxis(windowSize, direction) // dimensions.size + 2
	elseif dimensions.type == "consistentUDim2" then
		return getElementsDisplayedForUDim2(dimensions.udim2, windowSize, direction)
	elseif dimensions.type == "getter" then
		return nil
	elseif dimensions.type == "spaced" then
		if dimensions.inner.type == "consistentSize" then
			return DimensionsMethods.getDominantAxis(windowSize, direction)
					// (dimensions.inner.size + dimensions.spacing)
				+ 2
		elseif dimensions.inner.type == "consistentUDim2" then
			return getElementsDisplayedForUDim2(
				dimensions.inner.udim2
					+ UDim2.fromOffset(
						if direction == "x" then dimensions.spacing else 0,
						if direction == "y" then dimensions.spacing else 0
					),
				windowSize,
				direction
			)
		elseif dimensions.inner.type == "spaced" or dimensions.inner.type == "getter" then
			error("Unsupported spaced dimensions")
		else
			return exhaustiveMatch(dimensions.inner.type)
		end
	else
		return exhaustiveMatch(dimensions.type)
	end
end

-- Shallow equals.
function DimensionsMethods.equals<T>(left: Dimensions.Dimensions<T>, right: Dimensions.Dimensions<T>): boolean
	if left.type ~= right.type then
		return false
	end

	if left.type == "getter" then
		assert(right.type == "getter", "Luau")
		return left.callback == right.callback
	elseif left.type == "consistentSize" then
		assert(right.type == "consistentSize", "Luau")
		return left.size == right.size
	elseif left.type == "consistentUDim2" then
		assert(right.type == "consistentUDim2", "Luau")
		return left.udim2 == right.udim2
	elseif left.type == "spaced" then
		assert(right.type == "spaced", "Luau")
		return left.spacing == right.spacing
	else
		return exhaustiveMatch(left.type)
	end
end

return DimensionsMethods


local Src = script:FindFirstAncestor("ultimate-list")

local exhaustiveMatch = require(Src.Util.exhaustiveMatch)

local function adjustPositionToScrollAxis(position: UDim2, scrollAxis: number, direction: "x" | "y"): UDim2
	if direction == "x" then
		return UDim2.new(0, position.X.Offset - scrollAxis, position.Y.Scale, position.Y.Offset)
	elseif direction == "y" then
		return UDim2.new(position.X.Scale, position.X.Offset, 0, position.Y.Offset - scrollAxis)
	else
		return exhaustiveMatch(direction)
	end
end

return adjustPositionToScrollAxis


local Dimensions = {}

export type Dimensions<T> =
	{
		type: "getter",
		callback: (value: T, index: number) -> UDimRect,
	}
	| {
		type: "consistentSize",
		size: number,
	}
	| {
		type: "consistentUDim2",
		udim2: UDim2,
	}
	| {
		type: "spaced",
		spacing: number,
		inner: Dimensions<T>,
	}

export type UDimRect = {
	size: UDim2,
	position: UDim2,
}

function Dimensions.getter<T>(callback: (value: T, index: number) -> UDimRect): Dimensions<T>
	return {
		type = "getter",
		callback = callback,
	}
end

function Dimensions.consistentSize<T>(size: number): Dimensions<T>
	return {
		type = "consistentSize",
		size = size,
	}
end

function Dimensions.consistentUDim2<T>(udim2: UDim2): Dimensions<T>
	return {
		type = "consistentUDim2",
		udim2 = udim2,
	}
end

function Dimensions.withSpacing<T>(inner: Dimensions<T>, spacing: number): Dimensions<T>
	assert(
		inner.type ~= "getter",
		"withSpacing does not support getter. Instead, include your padding as part of your returned position."
	)

	if inner.type == "spaced" then
		return Dimensions.withSpacing(inner.inner, spacing)
	end

	return {
		type = "spaced",
		spacing = spacing,
		inner = inner,
	}
end

return Dimensions


local Src = script:FindFirstAncestor("ultimate-list")

local Dimensions = require(Src.Dimensions)
local DimensionsMethods = require(Src.Dimensions.DimensionsMethods)
local React = require(Src.Parent.React)
local adjustPositionToScrollAxis = require(Src.Dimensions.adjustPositionToScrollAxis)
local createDebugLogger = require(Src.Util.createDebugLogger)
local createVirtualizedListController = require(Src.createVirtualizedListController)
local joinAndMapBindings = require(Src.Util.joinAndMapBindings)

local e = React.createElement

local log = createDebugLogger("ReactBindingsRenderer")

local function getKeyDefault<T>(_value: T, index: number): string
	return tostring(index)
end

type LocatedItem<T> = {
	contents: {
		type: "vacant",
	} | {
		type: "full",
		index: number,
		key: string,
		value: T,
	},
}

type BindingAndSetter<T> = {
	binding: React.Binding<T>,
	set: (T) -> (),
}

local function ReactBindingsRenderer<T>(props: {
	virtualizedListController: createVirtualizedListController.VirtualizedListController<T>,
	scrollAxisBinding: React.Binding<number>,

	dimensions: Dimensions.Dimensions<T>,
	direction: "x" | "y",

	callback: (React.Binding<T?>) -> React.Node,
	getKey: ((value: T, index: number) -> string)?,
})
	-- NOT in order.
	-- This is to ensure that elements keep identity even when scrolled.
	local itemBindingsAndSetters: { BindingAndSetter<LocatedItem<T>> }, setItemBindingsAndSetters = React.useState(
		function(): { BindingAndSetter<LocatedItem<T>> }
			local total = {}

			local countHint = DimensionsMethods.elementsDisplayedHint(
				props.dimensions,
				props.virtualizedListController.getWindowSize(),
				props.direction
			)
			if countHint == nil then
				return total
			end

			for _ = 1, countHint do
				local binding, setBinding = React.createBinding({
					contents = {
						type = "vacant",
					},
				} :: LocatedItem<T>)

				table.insert(total, {
					binding = binding,
					set = setBinding,
				})
			end

			return total
		end
	)

	local getKey: (value: T, index: number) -> string = props.getKey or getKeyDefault

	React.useEffect(function()
		return props.virtualizedListController.bindToUpdate(function()
			local newView = props.virtualizedListController.get()
			local newRange = props.virtualizedListController.getRange()

			-- Try to preserve identity of bindings so that a scroll to show one new item only affects the binding
			-- of the no longer visible item.

			local newKeyToItemIndexInView: { [string]: number? } = {}
			for index, itemInNewView in newView do
				newKeyToItemIndexInView[getKey(itemInNewView, index + (newRange.X - 1))] = index
			end

			local deadSetters = {}
			local vacantSetters = {}
			local newBindingsAndSetters: { BindingAndSetter<LocatedItem<T>> } = {}

			for _, bindingAndSetter in itemBindingsAndSetters do
				local locatedItem = bindingAndSetter.binding:getValue()
				if locatedItem.contents.type == "vacant" then
					table.insert(vacantSetters, bindingAndSetter.set)
					continue
				end

				local key = locatedItem.contents.key
				local itemIndexInView = newKeyToItemIndexInView[key]
				if itemIndexInView == nil then
					log("%s is now dead", key)
					table.insert(deadSetters, bindingAndSetter.set)
				else
					newKeyToItemIndexInView[key] = nil

					local newValue = newView[itemIndexInView]
					if locatedItem.contents.value ~= newValue then
						log("%s existed, but changed", key)
						bindingAndSetter.set({
							contents = {
								type = "full",
								index = itemIndexInView + (newRange.X - 1),
								key = key,
								value = newValue,
							},
						})
					end
				end
			end

			log(
				"%d total setters, %d dead setters, %d vacant setters",
				#itemBindingsAndSetters,
				#deadSetters,
				#vacantSetters
			)

			-- `newKeyToItemIndexInView` now only contains keys that we haven't seen.
			for newKey, indexInView in newKeyToItemIndexInView do
				assert(indexInView ~= nil, "Luau")
				local value = newView[indexInView]

				local newLocatedItem: LocatedItem<T> = {
					contents = {
						type = "full",
						index = indexInView + (newRange.X - 1),
						key = newKey,
						value = value,
					},
				}

				-- Dead setters (referring to an older value that is no longer visible) go first
				-- because they will be transformed into vacants at the end otherwise anyway.
				local deadSetter = table.remove(deadSetters)
				if deadSetter ~= nil then
					log("%s is filling dead spot", newKey)
					deadSetter(newLocatedItem)
					continue
				end

				local vacantSetter = table.remove(vacantSetters)
				if vacantSetter ~= nil then
					log("%s is filling vacant spot", newKey)
					vacantSetter(newLocatedItem)
					continue
				end

				-- Out of bindings, we'll need to render more
				local binding, setBinding = React.createBinding(newLocatedItem)
				table.insert(newBindingsAndSetters, {
					binding = binding,
					set = setBinding,
				})
			end

			if #newBindingsAndSetters > 0 then
				log(
					"Needed to create %d more bindings to fit, there will be %d setters",
					#newBindingsAndSetters,
					#newBindingsAndSetters + #itemBindingsAndSetters
				)
				setItemBindingsAndSetters(function(total)
					total = table.clone(total)
					table.move(newBindingsAndSetters, 1, #newBindingsAndSetters, #total + 1, total)
					return total
				end)
			end

			for _, deadSetter in deadSetters do
				deadSetter({
					contents = {
						type = "vacant",
					},
				})
			end
		end)
	end, { props.virtualizedListController, getKey, itemBindingsAndSetters } :: { unknown })

	local children: { [string]: React.Node } = {}
	for index, itemBindingAndSetter in itemBindingsAndSetters do
		-- Luau: Not sure what is going on here
		local binding: React.Binding<LocatedItem<T>> = itemBindingAndSetter.binding :: any

		local udimRectBinding: React.Binding<Dimensions.UDimRect> = binding:map(function(locatedItem: LocatedItem<T>)
			if locatedItem.contents.type == "vacant" then
				return {
					size = UDim2.new(),
					position = UDim2.new(),
				}
			end

			return DimensionsMethods.getUDimRect(
				props.dimensions,
				locatedItem.contents.value,
				locatedItem.contents.index,
				props.virtualizedListController.getWindowSize(),
				props.direction
			)
		end)

		children[`Item{index}`] = e("Frame", {
			BackgroundTransparency = 1,

			Position = joinAndMapBindings(function(udimRect: Dimensions.UDimRect, scrollAxis: number)
				return adjustPositionToScrollAxis(udimRect.position, scrollAxis, props.direction)
			end, udimRectBinding, props.scrollAxisBinding),

			Size = udimRectBinding:map(function(udimRect: Dimensions.UDimRect)
				return udimRect.size
			end),

			Visible = binding:map(function(locatedItem: LocatedItem<T>)
				return locatedItem.contents.type == "full"
			end),
		}, {
			Content = props.callback(binding:map(function(locatedItem: LocatedItem<T>)
				return if locatedItem.contents.type == "full" then locatedItem.contents.value else nil
			end)),
		})
	end

	return e(React.Fragment, {}, children)
end

ReactBindingsRenderer = React.memo(ReactBindingsRenderer) :: never

return ReactBindingsRenderer


local Src = script:FindFirstAncestor("ultimate-list")

local Dimensions = require(Src.Dimensions)
local DimensionsMethods = require(Src.Dimensions.DimensionsMethods)
local React = require(Src.Parent.React)
local Renderers = require(Src.Renderers)
local adjustPositionToScrollAxis = require(Src.Dimensions.adjustPositionToScrollAxis)
local createVirtualizedListController = require(Src.createVirtualizedListController)
local joinAndMapBindings = require(Src.Util.joinAndMapBindings)

local e = React.createElement

local function getKeyDefault<T>(_value: T, index: number): string
	return tostring(index)
end

-- Lock the scroll axis in place during updates to avoid flickering
type ScrollLock<T> = {
	lockedTo: { T },
	scrollAxis: number,
}

local function ReactStateRenderer<T>(props: {
	virtualizedListController: createVirtualizedListController.VirtualizedListController<T>,

	config: Renderers.StateRendererConfig,

	dimensions: Dimensions.Dimensions<T>,
	direction: "x" | "y",

	callback: (T) -> React.Node,
	getKey: ((value: T, index: number) -> string)?,

	scrollAxisBinding: React.Binding<number>,
})
	local items, setItems = React.useState(props.virtualizedListController.get)
	local scrollLockBinding, setScrollLock = React.useBinding(nil :: ScrollLock<T>?)

	React.useEffect(function()
		local lastItems = props.virtualizedListController.get()
		setItems(lastItems)

		return props.virtualizedListController.bindToUpdate(function()
			setScrollLock({
				lockedTo = lastItems,
				scrollAxis = props.scrollAxisBinding:getValue(),
			})

			lastItems = props.virtualizedListController.get()
			setItems(lastItems)
		end)
	end, { props.virtualizedListController, props.scrollAxisBinding } :: { unknown })

	local children: { [string]: React.Node } = {}
	local getKey: (value: T, index: number) -> string = props.getKey or getKeyDefault
	local range = props.virtualizedListController.getRange()

	for relativeIndex, item in items do
		local index = range.X + (relativeIndex - 1)

		local dimensionsRect = DimensionsMethods.getUDimRect(
			props.dimensions,
			item,
			index,
			props.virtualizedListController.getWindowSize(),
			props.direction
		)

		children[getKey(item, index)] = e("Frame", {
			BackgroundTransparency = 1,
			Position = joinAndMapBindings(function(scrollAxis: number, scrollLock: ScrollLock<T>?)
				if
					props.config.freezeViewWhileScrolling ~= false
					and scrollLock ~= nil
					and scrollLock.lockedTo == items
				then
					return adjustPositionToScrollAxis(dimensionsRect.position, scrollLock.scrollAxis, props.direction)
				else
					return adjustPositionToScrollAxis(dimensionsRect.position, scrollAxis, props.direction)
				end
			end, props.scrollAxisBinding, scrollLockBinding),
			Size = dimensionsRect.size,
		}, props.callback(item))
	end

	return e(React.Fragment, nil, children)
end

return ReactStateRenderer


local Src = script:FindFirstAncestor("ultimate-list")

local React = require(Src.Parent.React)

local Renderers = {}

export type Renderer<T> = {
	type: "byState",
	callback: (T) -> React.Node,
	config: StateRendererConfig,
} | {
	type: "byBinding",
	callback: (React.Binding<T?>) -> React.Node,
}

export type StateRendererConfig = {
	freezeViewWhileScrolling: boolean?,
}

function Renderers.byState<T>(callback: (T) -> React.Node, config: StateRendererConfig?): Renderer<T>
	return {
		type = "byState",
		callback = callback,
		config = config or {},
	}
end

function Renderers.byBinding<T>(callback: (React.Binding<T?>) -> React.Node): Renderer<T>
	return {
		type = "byBinding",
		callback = callback,
	}
end

return Renderers


-- Intentionally using string formatting instead of taking strings directly to avoid
-- expensive string formatting in production.
local Src = script:FindFirstAncestor("ultimate-list")

local DebugFlags = require(Src.DebugFlags)

local function createDebugLogger(key: string): <A...>(format: string | (A...) -> (string, ...any), A...) -> ()
	return function(format, ...)
		if not DebugFlags.shouldLog then
			return
		end

		if typeof(format) == "string" then
			print(`[{key}] {string.format(format, ...)}`)
		else
			print(`[{key}]`, format(...))
		end
	end
end

return createDebugLogger


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

return exhaustiveMatch


local Src = script:FindFirstAncestor("ultimate-list")

local React = require(Src.Parent.React)

-- A version of React.joinBindings(data):map(fn) that preserves types.
local function joinAndMapBindings<Ret, T1, T2, T3>(
	fn: (T1, T2, T3) -> Ret,
	binding1: React.Binding<T1>,
	binding2: React.Binding<T2>,
	binding3: React.Binding<T3>?
): React.Binding<Ret>
	return React.joinBindings({ binding1, binding2, binding3 } :: { any }):map(function(data: any)
		return fn(unpack(data))
	end)
end

return joinAndMapBindings


local Src = script:FindFirstAncestor("ultimate-list")

local DataSourceMethods = require(Src.DataSources.DataSourceMethods)
local DataSources = require(Src.DataSources)
local Dimensions = require(Src.Dimensions)
local exhaustiveMatch = require(Src.Util.exhaustiveMatch)

local function binarySearchIndexRangeInView<T>(
	dataSource: DataSources.DataSource<T>,
	getUDimRect: (value: T, index: number) -> Dimensions.UDimRect,

	scrollAxis: number,
	windowAxis: number,

	direction: "x" | "y"
): Vector3
	type Comparison = "before" | "inside" | "after"
	local function compareInView(udimRect: Dimensions.UDimRect): Comparison
		local position = if direction == "x"
			then udimRect.position.X.Offset
			elseif direction == "y" then udimRect.position.Y.Offset
			else exhaustiveMatch(direction)

		local size = if direction == "x"
			then udimRect.size.X.Offset
			elseif direction == "y" then udimRect.size.Y.Offset
			else exhaustiveMatch(direction)

		if position + size < scrollAxis then
			return "before"
		elseif position > scrollAxis + windowAxis then
			return "after"
		else
			return "inside"
		end
	end

	local low = 1

	local length = DataSourceMethods.length(dataSource)
	local high = length

	while low <= high do
		local mid = (low + high) // 2

		local cursor = DataSourceMethods.get(dataSource, mid)
		assert(cursor ~= nil, "get() returned nil, meaning the length is inaccurate")

		local cursorUDimRect = getUDimRect(cursor.value, mid)
		local comparison: Comparison = compareInView(cursorUDimRect)

		if comparison == "after" then
			high = mid - 1
		elseif comparison == "before" then
			low = mid + 1
		elseif comparison == "inside" then
			local min = mid
			local minCursor = cursor

			while min > 1 do
				local nextCursor = minCursor.before()
				if nextCursor == nil then
					break
				end

				if compareInView(getUDimRect(nextCursor.value, min - 1)) ~= "inside" then
					break
				end

				min -= 1
				minCursor = nextCursor
			end

			local max = mid
			local maxCursor = cursor

			while max < length do
				local nextCursor = maxCursor.after()
				if nextCursor == nil then
					break
				end

				if compareInView(getUDimRect(nextCursor.value, max + 1)) ~= "inside" then
					break
				end

				max += 1
				maxCursor = nextCursor
			end

			return Vector3.new(min, max)
		else
			exhaustiveMatch(comparison)
		end
	end

	return Vector3.zero
end

return binarySearchIndexRangeInView