local derive = require("../reactive/derive")
local graph = require("../reactive/graph")
local result = require("../result")
local scheduler = require("../scheduler")
local types = require("../reactive/types")

type Condition<T> = (types.Readable<boolean>) -> (T, number?)

type SwitchGraph<Result> = {
	previous_effect: Condition<Result>?,
	scopes: { [Condition<Result>]: graph.StableNode<Result> },
	active_nodes: { [Condition<Result>]: graph.SourceNode<boolean> },
	delay_by: { [Condition<Result>]: number? },
	threads: { [Condition<Result>]: thread? },
	result_values: { [Condition<Result>]: Result },
}

local function switch<Key, Result>(condition: () -> Key)
	return function(
		options: { [Key]: (types.Readable<boolean>) -> (Result, number?) }
	): () -> { Result }
		graph.set_group(graph.new_group("switch_delay"))
		local parent_node = graph.assert_stable_parent()
		-- allows us to force an update on the reactive node
		local update_counter = graph.create_source_node(0)

		-- we put this into a derive to reduce the amount of updates
		local callback = derive(function(): Condition<Result>?
			return options[condition()]
		end)

		local function destroy(p: SwitchGraph<Result>, c: Condition<Result>)
			graph.destroy(p.scopes[c])
			p.scopes[c] = nil
			p.active_nodes[c] = nil
			p.delay_by[c] = nil
			p.threads[c] = nil
			p.result_values[c] = nil
		end

		local function destroy_delayed(p: SwitchGraph<Result>, c: Condition<Result>)
			destroy(p, c)
			graph.update_source_node(update_counter, update_counter.cached_value + 1)
		end

		local function update(p: SwitchGraph<Result>)
			graph.push_dependency(update_counter)

			local previous_effect = p.previous_effect
			local scopes = p.scopes
			local active_nodes = p.active_nodes
			local result_values = p.result_values
			local active_effect = callback()

			if previous_effect == active_effect then
				return p
			end

			-- previous effect isn't the same as active effect, so destroy previous effect
			if
				previous_effect
				and not p.threads[previous_effect]
				and p.scopes[previous_effect]
			then
				local delay_by = p.delay_by[previous_effect]

				graph.update_source_node(p.active_nodes[previous_effect], false)

				if delay_by then
					p.threads[previous_effect] =
						scheduler.delay(delay_by, destroy_delayed, p, previous_effect)
				else
					destroy(p, previous_effect)
				end
			end

			p.previous_effect = active_effect

			if not active_effect then
				return p
			end

			-- first see if an old node was in the process of getting destroyed.
			-- if it is, save it
			local thread = p.threads[active_effect]
			if thread then
				scheduler.cancel(thread)
				p.threads[active_effect] = nil
				graph.update_source_node(p.active_nodes[active_effect], true)
				return p
			end

			-- otherwise, create a new node
			local node = graph.create_stable_node(parent_node)
			local active = graph.create_source_node(true)

			local function read_active()
				graph.push_dependency(active)
				return active.cached_value
			end

			scopes[active_effect] = node
			active_nodes[active_effect] = active
			result_values[active_effect], p.delay_by[active_effect] =
				graph.run_as_unsafe(node, active_effect, read_active)

			return p
		end

		local reactive = graph.create_reactive_node(parent_node, update, "lazy", {
			previous_effect = nil,
			scopes = {},
			active_nodes = {},
			delay_by = {},
			threads = {},
			result_values = {},
		})

		graph.clear_group()
		return function()
			local result: result.Identity<SwitchGraph<Result>, unknown> =
				graph.evaluate_node(reactive)

			if result.success then
				local values = {}
				for _, value in result.value.result_values do
					table.insert(values, value)
				end
				return values
			else
				error(result, 0)
			end
		end
	end
end

return switch


local action = require("../instances/action")
local cleanup = require("../reactive/cleanup")
local graph = require("../reactive/graph")

local function bind_tag(instance: Instance, tag: string)
	instance:AddTag(tag)

	cleanup(function()
		instance:RemoveTag(tag)
	end)
end

local function tags(tags: { string | () -> string? }): action.Identity
	return action.create(function(instance)
		graph.set_group(graph.new_group("bind tags"))
		for _, tag in tags do
			if typeof(tag) == "string" then
				bind_tag(instance, tag)
				continue
			end
			graph.create_reactive_node(graph.assert_stable_parent(), function(): nil
				local current = tag()
				return if current then bind_tag(instance, current) else nil
			end, "deferred", nil)
		end
		graph.clear_group()
	end)
end

return tags


export type Styler = (text: string) -> string

local function createStylerFunction(opener: number, closer: number): Styler
	local open = string.format("%c[%dm", 27, opener)
	local close = string.format("%c[%dm", 27, closer)

	return function(text: string)
		return `{open}{text}{close}`
	end
end

local function combineStyles(styleArray: { (text: string) -> string }): Styler
	return function(text: string)
		local result = text

		for _, style in styleArray do
			result = style(result)
		end

		return result
	end
end

return {
	combineStyles = combineStyles,

	modifier = {
		dim = createStylerFunction(2, 22),
		reset = createStylerFunction(0, 0),
		bold = createStylerFunction(1, 22),
		italic = createStylerFunction(3, 23),
		hidden = createStylerFunction(8, 28),
		inverse = createStylerFunction(7, 27),
		underline = createStylerFunction(4, 24),
		overline = createStylerFunction(53, 55),
		strikethrough = createStylerFunction(9, 29),
	},

	color = {
		red = createStylerFunction(31, 39),
		blue = createStylerFunction(34, 39),
		cyan = createStylerFunction(36, 39),
		gray = createStylerFunction(90, 39),
		grey = createStylerFunction(90, 39),
		black = createStylerFunction(30, 39),
		green = createStylerFunction(32, 39),
		white = createStylerFunction(37, 39),
		yellow = createStylerFunction(33, 39),
		magenta = createStylerFunction(35, 39),
		redBright = createStylerFunction(91, 39),
		blueBright = createStylerFunction(94, 39),
		cyanBright = createStylerFunction(96, 39),
		blackBright = createStylerFunction(90, 39),
		greenBright = createStylerFunction(92, 39),
		whiteBright = createStylerFunction(97, 39),
		yellowBright = createStylerFunction(93, 39),
		magentaBright = createStylerFunction(95, 39),
	},
}


local oklab = require("@src/utils/oklab")

local function within(epsilon: number)
	return function(value1: vector, value2: vector): boolean
		return vector.magnitude(value1 - value2) <= epsilon
	end
end

local colors_within = within(0.01)

local function oklab_fuzz()
	for r = 1, 256 do
		for g = 1, 256 do
			for b = 1, 256 do
				local original = vector.create(r, g, b)

				assert(
					colors_within(oklab.to_srgb(oklab.from_srgb(original)), original),
					"colors not within"
				)
			end
		end
	end
end

return oklab_fuzz


--[[
	hooks to improve developer experience
]]

return {
	scheduler = require("@self/scheduler"),
	logging = require("@self/logging"),
}


--[[
	hook onto when fluid warns
	used if you wanna catch a logging.warn/logging.warn_supressed this will allow you to hook onto that
]]
local logging = require("@src/logging")

type OnWarn = (text: string) -> ()

-- user callback for when fluid warns to the stdio
-- if nothing is specified, fluid will output to the stdio
local on_warn_hook: OnWarn?

-- overwrite to accommodate the user specified hook
local original_function = logging.__warn_facade
function logging.__warn_facade(...)
	if on_warn_hook then
		on_warn_hook(...)
	else
		original_function(...)
	end
end

--[[
 	user callback for when fluid warns to the stdio
 	prevents stdio from even being written

	@example
	```lua
	local did_output_something = false

	hooks.logging.on_warn(function(err)
		did_output_something = string.find(err, "did_output_something") ~= nil
	end)

	require("@src/logging").warn("did_output_something")

	CHECK(did_output_something)

	hooks.logging.on_warn(nil) -- good practice to remove on_warn
	```
]]
local function set_on_warn(hook: OnWarn?)
	on_warn_hook = hook
end

return {
	on_warn = set_on_warn,
}


--[[
	hooks wait/delay in fluid
	this allows you to timeskip forward in tests
]]
local fluid = require("@src")
local logging = require("@src/logging")
local task = require("@lune/task")

local scheduler_interface = fluid.__SCHEDULER_INTERFACE
local step_fluid = fluid.provide_scheduler()

local step_tick = 0

local waiting_threads: { [thread]: WaitingThread } = {}
type WaitingThread =
	({ resume: number, start: number } | { resume: number })
	& { [any]: any }
	& { any }

-- scheduler.wait and scheduler.delay is overwritten to allow timeskip
function scheduler_interface.wait(time: number?): number
	waiting_threads[coroutine.running()] =
		{ resume = step_tick + (time or 0), start = step_tick } :: WaitingThread
	return coroutine.yield()
end

function scheduler_interface.delay<T...>(n: number, fn: (T...) -> (), ...: T...): thread
	local thread = coroutine.create(fn)
	local data: { [any]: any } = table.pack(...)
	data.resume = step_tick + n
	data.trace = debug.traceback(nil, 2)

	waiting_threads[thread] = data :: WaitingThread

	return thread
end

local function step_threads(delta_time: number)
	step_tick += delta_time

	local processing = waiting_threads
	waiting_threads = {}

	for thread, data in processing do
		if step_tick >= data.resume and coroutine.status(thread) == "suspended" then
			local ok, err
			if data.start then
				ok, err = coroutine.resume(thread, step_tick - data.start)
			else
				ok, err = coroutine.resume(thread, table.unpack(data, 1, data.n))
			end

			if not ok then
				logging.warn(`[fluid]: {err} origin: {data.trace}`)
			end
		else
			waiting_threads[thread] = data
		end
	end
end

--[[
	@example
	```lua
	local async = fluid.async(function(set)
		scheduler.wait(1)
		return "HTTP/2"
	end)

	hooks.scheduler.step(1) -- immediately fast forward by 1 second
	CHECK(async() == "HTTP/2")
	```
]]
local function step(delta_time: number?)
	local dt = delta_time or 1 / 60
	task.spawn(step_threads, dt)
	task.spawn(step_fluid, dt)
end

return {
	step = step,
}


--!nolint BuiltinGlobalWrite

local process = require("@lune/process")
local roblox = require("@lune/roblox")
local _, _, _, FINISH = require("@vendor/testkit").test()

local tests = {}

function tests.fuzz()
	require("@self/fuzz_tests/colors.fuzz")()
end

function tests.unit_tests()
	require("@self/unit_tests/logging.spec")
	require("@self/unit_tests/no_yield.spec")
	require("@self/unit_tests/reactive_graph.spec")
	require("@self/unit_tests/scheduler.spec")
	require("@self/unit_tests/bind.spec")
	require("@self/unit_tests/interval.spec")
	require("@self/unit_tests/spring.spec")
	require("@self/unit_tests/colors.spec")

	local ok = FINISH()
	if not ok then
		process.exit(1)
	end
end

do -- setting roblox globals
	Color3 = roblox.Color3
	CFrame = roblox.CFrame
	UDim2 = roblox.UDim2
end

return tests


local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TextService = game:GetService("TextService")

local fluid = require(ReplicatedStorage.fluid)

local random_words = fluid.source({ "hi", "bye", "meow", "mrrp" })

local function label()
	return fluid.create("TextLabel")({
		Text = "haii!",
	})
end

local function iso_date()
	local text = fluid.interval(function()
		return string.rep("W", math.random(1, 20))
	end)

	local elements = fluid.interval(function()
		local array = {}

		for i = 1, math.random(1, 5) do
			table.insert(array, label())
		end

		return array
	end, 2)

	return fluid.create("ScreenGui")({
		Name = "iso",

		fluid.tags({ "hi" }),

		fluid.create("TextButton")({
			Text = text,

			Activated = function()
				print("button pressed!")
			end,

			Size = fluid.async(function(set)
				local text_value = text()

				local params = Instance.new("GetTextBoundsParams")
				params.Text = text_value

				local bounds = TextService:GetTextBoundsAsync(params)

				return UDim2.fromOffset(bounds.X, bounds.Y)
			end),
			Position = UDim2.fromScale(0.5, 0.5),
			AnchorPoint = Vector2.new(0.5, 0.5),
		}),

		fluid.create("UIListLayout")({}),

		fluid.for_values(random_words, function(key, value)
			return fluid.create("TextLabel")({
				Name = key,
				Text = value,
			})
		end),

		fluid.create("Frame")({
			Size = UDim2.fromScale(0.2, 0.4),

			elements,
		}),
	})
end

local function list()
	local items = fluid.source({})

	local input = fluid.create("TextBox")({
		Position = UDim2.fromScale(0.5, 0.5),
		Size = UDim2.fromScale(0.25, 0.1),

		fluid.changed("Text", print),
	}) :: TextBox

	return fluid.create("ScreenGui")({
		input,

		fluid.create("TextButton")({
			Text = "enter",

			Size = UDim2.fromScale(0.25, 0.1),
			Position = UDim2.fromScale(0.5, 0.7),

			Activated = function()
				local existing_items = items()
				table.insert(existing_items, input.Text)
				items(existing_items)

				input.Text = ""
			end,

			fluid.tags({ "tag1", "tag2" }),
			fluid.tags({
				function()
					return items()[#items()]
				end,
			}),
		}),

		fluid.create("TextButton")({
			Text = "remove",

			Size = UDim2.fromScale(0.25, 0.1),
			Position = UDim2.fromScale(0.5, 0.85),

			Activated = function()
				local existing_items = items()
				table.remove(existing_items)
				items(existing_items)
			end,
		}),

		fluid.create("Frame")({
			Position = UDim2.fromScale(0.25, 0.5),
			Size = UDim2.fromOffset(125, 500),

			fluid.create("UIListLayout")({}),

			fluid.for_values(items, function(key, value)
				local color = fluid.source(Color3.new())
				local lerped = fluid.lerp(color, 0.1)
				color(Color3.new(1, 0, 0))

				return fluid.show(function()
					return true
				end, function()
					return {
						fluid.create("TextLabel")({
							Size = UDim2.fromOffset(125, 50),
							BackgroundColor3 = lerped,

							Name = key,
							Text = value,
						}),
					}
				end)
			end),
		}),
	})
end

fluid.mount(function()
	return { list() }
end, Players.LocalPlayer.PlayerGui)


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local fluid = require(ReplicatedStorage.fluid)

local create = fluid.create
local scale = UDim2.fromScale
local px = UDim2.fromOffset

fluid.mount(function()
	local color = fluid.source(Color3.new())
	local size = fluid.source(px(60, 60))

	fluid.effect(function()
		color()
		size()

		task.delay(0.2, function()
			if color() == Color3.new(1, 1, 1) then
				color(Color3.new(0, 0, 0))
				size(px(60, 60))
			end
		end)
	end)

	return create("ScreenGui")({

		create("TextButton")({
			AnchorPoint = Vector2.new(0.5, 0.5),
			Position = scale(0.5, 0.5),
			AutoButtonColor = false,

			BackgroundColor3 = color,
			Size = size,

			Text = "",

			MouseButton1Click = function()
				color(Color3.new(1, 1, 1))
				size(px(150, 60))
			end,
		}),
	})
end, game:GetService("Players").LocalPlayer.PlayerGui)


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local fluid = require(ReplicatedStorage.fluid)

local mount = fluid.mount
local source = fluid.source
local show = fluid.show
local show_delay = fluid.show_delay
local create = fluid.create
local scale = UDim2.fromScale
local px = UDim2.fromOffset

mount(function()
	local active_show_delay = source(false)
	local active = source(false)

	return create("ScreenGui")({
		ResetOnSpawn = false,

		show(active, function()
			return create("Frame")({
				AnchorPoint = Vector2.new(0.5, 0.5),
				Position = scale(0.5, 0.5) + px(-250, -60),
				Size = px(60, 60),
			})
		end),
		show_delay(active_show_delay, function()
			return create("Frame")({
				AnchorPoint = Vector2.new(0.5, 0.5),
				Position = scale(0.5, 0.5) + px(-250, 60),
				Size = px(60, 60),
				BackgroundColor3 = function()
					return active_show_delay() and Color3.new(1, 1, 1) or Color3.new(1, 0, 0)
				end,
			}),
				0.5
		end),

		create("TextButton")({
			AnchorPoint = Vector2.new(0.5, 0.5),
			Position = scale(0.5, 0.5),
			AutoButtonColor = false,

			Size = px(150, 50),

			Text = function()
				if active() then
					return "Working...."
				end
				return "Activate delay"
			end,

			MouseButton1Click = function()
				if active() then
					return
				end

				active(true)
				task.delay(0.5, active, false)
			end,
		}),
		create("TextButton")({
			AnchorPoint = Vector2.new(0.5, 0.5),
			Position = scale(0.5, 0.5) + px(0, 55),
			AutoButtonColor = false,

			Size = px(150, 50),

			Text = function()
				if active_show_delay() then
					return "Working...."
				end
				return "Activate show_delay"
			end,

			MouseButton1Click = function()
				if active_show_delay() then
					return
				end

				active_show_delay(true)
				task.delay(0.5, active_show_delay, false)
			end,
		}),
	})
end, game:GetService("Players").LocalPlayer.PlayerGui)


local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TextService = game:GetService("TextService")

local fluid = require(ReplicatedStorage.fluid)

local function main()
	local active = fluid.source(false)

	return fluid.create("ScreenGui" :: "ScreenGui")({
		fluid.create("TextButton" :: "TextButton")({

			Position = UDim2.fromScale(0.5, 0.5),
			Size = UDim2.fromOffset(200, 50),
			AnchorPoint = Vector2.new(0.5, 0.5),

			Activated = function()
				active(not active())
			end,

			fluid.show_delay(active :: () -> boolean, function(active: types.Readable<boolean>)
				return fluid.create("Frame" :: "Frame")({

					Position = UDim2.fromScale(0, 1),
					Size = UDim2.fromOffset(100, 100),
				}),
					0.5 :: number?
			end) :: () -> { Instance },
		}),
	})
end

fluid.mount(function()
	return main()
end, Players.LocalPlayer.PlayerGui)


local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local fluid = require(ReplicatedStorage.fluid)
local create = fluid.create
local mount = fluid.mount

local parent_screen = create("ScreenGui")({
	Name = "ParentScreen",
	ResetOnSpawn = false,
	Parent = Players.LocalPlayer:WaitForChild("PlayerGui"),
})

mount(function()
	local is_even_second = fluid.interval(function()
		return (os.clock() // 1) % 2 == 1
	end, 2)

	return create("Frame")({
		Size = UDim2.fromScale(1, 1),
		Position = fluid.spring(
			fluid.interval(function()
				return UDim2.fromScale(math.random(), math.random())
			end),
			1,
			1
		),

		fluid.show(function()
			return is_even_second()
		end, function()
			return Instance.new("Frame")
		end),

		BackgroundColor3 = fluid.spring(
			fluid.interval(function()
				return Color3.fromRGB(
					math.random() * 255,
					math.random() * 255,
					math.random() * 255
				)
			end, 2),
			1,
			1
		),
	})
end, parent_screen)


local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local fluid = require(ReplicatedStorage.fluid)
local create = fluid.create
local keys = fluid.for_keys
local mount = fluid.mount
local source = fluid.source

local parent_screen = create("ScreenGui")({
	Name = "ParentScreen",
	ResetOnSpawn = false,
	Parent = Players.LocalPlayer:WaitForChild("PlayerGui"),
})

local list = {
	"1",
	"2",
	"3",
	"4",
}

local list_source = source(list)

local function some_component(_, k, active): (Instance, number?)
	print("CREATED?")
	return create("TextLabel")({
		Name = k,

		Size = UDim2.fromOffset(100, 100),

		Text = function()
			return `{k}`
		end,
	}),
		2
end

mount(function()
	print("RUNNING")
	return create("Frame")({
		Size = UDim2.fromScale(1, 1),

		create("UIListLayout")({}),

		keys(list_source, some_component),
	})
end, parent_screen)

table.remove(list, 4)
list_source(list)


--!optimize 2
--!native
local task = task or require("@lune/task")

local tspawn = task.spawn
local cyield = coroutine.yield

local freeThread: thread? = nil

local function deletedSignalError()
	error("Cannot fire a deleted signal", 2)
end

local errorTable = {
	fire = deletedSignalError,
	connect = deletedSignalError,
	once = deletedSignalError,
	wait = deletedSignalError,
	disconnectAll = deletedSignalError,
}

local function run<T...>(func: (T...) -> (), ...: T...)
	local ref = freeThread
	freeThread = nil

	func(...)

	freeThread = ref
end

local function yieldLoop()
	while true do
		run(cyield())
	end
end

local signal = {}
signal.__index = signal
type InternalIdentity<T...> = setmetatable<{ (T...) -> () }, typeof(signal)>

--[=[
	@class signal

	The main class for this package. It is actually just an array with a metatable! Constructed with:
	```lua
	local signal = require(...)

	signal()
	```
]=]
local function constructor<T...>(): InternalIdentity<T...>
	return setmetatable({}, signal)
end

--[=[
	@method connect
	@within signal

	Connects a function to the signal. This will be called whenever the signal is fired.
	Connections can be "disconnected" (they won't be called when the signal is fired) by calling the function that they return.
	```lua
	local sig = signal()

	-- You can connect a function like this
	sig:connect(function(text)
		print(text)
	end)

	-- You can also directly connect the function like this.
	sig:connect(print)

	-- Will print twice, because two connections.
	sig:fire("Hello, world!")
	```

	You shouldn't connect the same function twice. This is because the function is used as a reference to disconnect when disconnecting later.
	While it *might* work for simple or specialized cases, you may find that it doesn't work as expected in some cases.

	Connections will be ran in order of connection.

	It's also worth noting that disconnecting is not an optimal operation, as it will shift the array. If you have a lot of connections to a single signal, you should avoid frequent disconnections.

	@param callback (T...) -> ()
	@return () -> ()
]=]
function signal.connect<T...>(self: InternalIdentity<T...>, callback: (T...) -> ())
	table.insert(self, callback)

	return function()
		local index = table.find(self, callback)

		if index then
			table.remove(self, index)
		end
	end
end

--[=[
	@method fire
	@within signal

	Firing a signal will run all connections in order, while using thread reusage.
	[You can find more on this optimization here](https://devforum.roblox.com/t/thread-reuse-how-it-works-why-it-works/1999166).

	You should note these things about thread reusage:
	- You cannot rely on the thread being different, or the same consistently.
	- The above means that certain "no-yield" implementations might not work correctly in connections.
	- If you have a lot of connections, you should avoid yielding in said connections. Yielding forces a new thread to be spawned, which isn't optimal, but it's still okay.

	@param data ...
	@return void
]=]
function signal.fire<T...>(self: InternalIdentity<T...>, ...: T...)
	for i = #(self :: any), 1, -1 do
		if not freeThread then
			freeThread = coroutine.create(yieldLoop)
			tspawn(freeThread :: thread)
		end

		-- Type states don't take into account assigning yet
		tspawn(freeThread :: thread, self[i], ...)
	end
end

--[=[
	@method once
	@within signal

	Connects to a signal, and disconnects after the first time it is fired.
	You can still disconnect the connection before it is fired.

	@param callback (T...) -> ()
	@return () -> ()
]=]
function signal.once<T...>(self: InternalIdentity<T...>, callback: (T...) -> ())
	local disconnect
	disconnect = self:connect(function(...)
		assert(disconnect ~= nil, "Luau")
		disconnect()

		callback(...)
	end)
end

--[=[
	@method wait
	@within signal

	Yields the current thread until the signal is fired, and returns the arguments passed.
	Will raise an error if the thread is resumed before the signal is fired.

	@return T...
]=]
function signal.wait<T...>(self: InternalIdentity<T...>): T...
	local running = coroutine.running()

	self:once(function(...)
		assert(
			coroutine.status(running) == "suspended",
			":wait() called, then another thread resumed the waiting thread. Please dont do that :("
		)

		tspawn(running, ...)
	end)

	return cyield()
end

--[=[
	@method disconnectAll
	@within signal

	Disconnects all connections to the signal. This is an efficient operation, utilizing `table.clear`.
]=]
function signal.disconnectAll<T...>(self: InternalIdentity<T...>)
	table.clear(self)
end

--[=[
	@method delete
	@within signal

	Disconnects all connections to the signal, and renders the signal unusable.
	While this technically isn't required or needed for memory concerns, you might want to use this when you're working with others.

	This will prevent and raise an error upon any attempts to use the signal, which is useful for debugging & catching leaks.
]=]
function signal.delete<T...>(self: InternalIdentity<T...>): ()
	self:disconnectAll()

	setmetatable(self, errorTable)
end

export type Identity<T...> = {
	fire: (self: Identity<T...>, T...) -> (),
	connect: (self: Identity<T...>, callback: (T...) -> ()) -> () -> (),
	once: (self: Identity<T...>, callback: (T...) -> ()) -> () -> (),
	wait: (self: Identity<T...>) -> T...,
	disconnectAll: (self: Identity<T...>) -> (),
	delete: (self: Identity<T...>) -> (),
}

return constructor :: <T...>() -> Identity<T...>


--!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)

	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,
})


local Avalog = require(script.Parent.Parent.Avalog)
local Config = require(script.Parent.Config)
local Icon = require(script.Parent.Parent.Avalog.Packages.TopbarPlus)

Avalog.Config:Update(Config)

Avalog:Start()

local AvalogIcon = Icon.new():setImage("rbxassetid://109049301067448"):setLeft():setImageScale(0.55)

AvalogIcon.toggled:Connect(function()
	Avalog:ToggleOpen()
end)


--!strict

local AvalogConfig = require(script.Parent.Parent.Avalog.Packages.Avalog.Config)

local Config: AvalogConfig.Config = {
	Featured = {
		Items = {},
	},
}

return Config


local Avalog = require(script.Parent.Parent.Avalog)
local Config = require(script.Parent.Config)

Avalog.Config:Update(Config)

Avalog:Start()


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

local function convertDependencies(pesdeDependencies)
	local wallyDependencies = {}

	for alias, dependency in pairs(pesdeDependencies) do
		if next(dependency) ~= nil then
			assert(dependency.name == nil, `Package relies on Pesde package.`)

			if dependency.wally and dependency.version then
				wallyDependencies[alias] = `{dependency.wally}@{dependency.version}`
			end
		end
	end

	return wallyDependencies
end

local function convertName(pesdeName)
	return string.gsub(string.gsub(pesdeName, "avafe/", "imavafe/"), "_", "-")
end

local function convertManifest(pesdeToml)
	local wallyInfo = {
		package = {
			name = convertName(pesdeToml.name),
			version = pesdeToml.version,

			license = pesdeToml.license,
			authors = pesdeToml.authors,

			registry = pesdeToml["wally_indices"].default,
			realm = "shared",
		},
		dependencies = convertDependencies(pesdeToml.dependencies),
	}

	return wallyInfo
end

local function convertProject(path: string)
	if fs.isFile(`{path}/pesde.toml`) then
		local pesdeToml = serde.decode("toml", fs.readFile(`{path}/pesde.toml`))
		local wallyToml = serde.encode("toml", convertManifest(pesdeToml), true)

		fs.writeFile(`{path}/wally.toml`, wallyToml)
	else
		assert(false, `pesde.toml does not exist for {path}`)
	end
end

convertProject("./")


local SOURCE = script.Parent.SourceCode
local PACKAGES = script.Parent.Parent
local CLIENT = SOURCE.Client

local States = require(SOURCE.Client.UI.States)
local Fusion = require(PACKAGES.Fusion)
local Bootloader = require(PACKAGES.Bootloader)
local Config = require(script.Parent.Config)
local ServiceProvider = require(CLIENT.ServiceProvider)
local LocalAvatar = require(CLIENT.Avatar.LocalAvatar)

local function GetModules()
	local Modules = {}

	for _, Child in ipairs(CLIENT:GetChildren()) do
		if Child:IsA("ModuleScript") then
			table.insert(Modules, Child)
		end
	end

	return Modules
end

--[=[
	@class AvalogClient

	The client-side module of Avalog.
]=]
local AvalogClient = {
	Config = Config,
	Started = false,
	_Bootloader = Bootloader(GetModules()),
	_ServiceProvider = ServiceProvider,
}

--[=[
	Starts Avalog on the client.
]=]
function AvalogClient:Start()
	assert(not self.Started, "Avalog already started.")
	self.Started = true

	AvalogClient._ServiceProvider:Setup({
		AvatarManager = LocalAvatar,
	})

	AvalogClient._Bootloader:Initialize()
	AvalogClient._Bootloader:Start()
end

--[=[
	Configures Avalog with the given config.

	@param NewConfig Config
]=]
function AvalogClient:Configure(NewConfig: Config.Config)
	AvalogClient.Config:Update(NewConfig)
end

--[=[
	Toggles whether or not Avalog is open.
]=]
function AvalogClient:ToggleOpen()
	States.Open:set(not Fusion.peek(States.Open))
end

--[=[
	Sets whether or not Avalog is open.

	@param Open boolean
]=]
function AvalogClient:SetOpen(Open: boolean)
	States.Open:set(Open)
end

return AvalogClient


local SOURCE = script.Parent.SourceCode
local PACKAGES = script.Parent.Parent
local SERVER = SOURCE.Server

local Config = require(script.Parent.Config)
local Bootloader = require(PACKAGES.Bootloader)

local function GetModules()
	local Modules = {}

	for _, Child in ipairs(SERVER:GetChildren()) do
		if Child:IsA("ModuleScript") then
			table.insert(Modules, Child)
		end
	end

	return Modules
end

--[=[
	@class AvalogServer

	The server-side module of Avalog.
]=]
local AvalogServer = {
	Config = Config,
	Bootloader = Bootloader(GetModules()),
	Started = false,
}

--[=[
	Starts Avalog on the server.
]=]
function AvalogServer:Start()
	assert(not self.Started, "Avalog already started.")
	self.Started = true

	AvalogServer.Bootloader:Initialize()
	AvalogServer.Bootloader:Start()
end

--[=[
	Configures Avalog with the given config.

	@param NewConfig Config
]=]
function AvalogServer:Configure(NewConfig: Config.Config)
	AvalogServer.Config:Update(NewConfig)
end

return AvalogServer


local RunService = game:GetService("RunService")

if RunService:IsServer() then
	return require(script.Server)
else
	return require(script.Client)
end


-- An AvatarManager represents an avatar and operations that can be performed on this avatar.

local Avalog = script.Parent.Parent.Parent.Parent
local HumanoidDescriber = require(Avalog.Parent.HumanoidDescriber)
local Signal = require(Avalog.Parent.Signal)

export type CatalogItem = {
	Id: number,
	Type: Enum.AvatarItemType,
	AssetType: Enum.AvatarAssetType,
	Name: string,
}

export type AvatarManager = {
	Character: Model?,
	Humanoid: Humanoid?,
	HumanoidDescriber: HumanoidDescriber.HumanoidDescriber?,
	Updated: Signal.Signal<nil>,

	-- Limits the number of accessories the avatar has on.
	ApplyLimits: (AvatarManager) -> (),
	UpdateScale: (AvatarManager, Property: string, Value: number) -> (),
	SetColor: (AvatarManager, Color: Color3) -> (),

	GetUnownedItems: (AvatarManager) -> (),
	ToggleCatalogItem: (AvatarManager, CatalogItem: CatalogItem) -> (),
	Reset: () -> AvatarManager,
	Apply: () -> AvatarManager,
	SetCharacter: (AvatarManager, Character: Model) -> (),
	Clear: (AvatarManager) -> (),

	--[[ The follow methods are only used on the client (not plugin) ]]

	-- Updates the avatar on the server.
	UpdateServer: (AvatarManager, Data: HumanoidDescriber.Data) -> (),

	Redo: (AvatarManager) -> (),
	Undo: (AvatarManager) -> (),
	SaveAvatar: (AvatarManager) -> (),
	PromptCheckout: (AvatarManager, Items: any) -> (),
}

return {}


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

local Avalog = script.Parent.Parent.Parent.Parent
local HumanoidDescriber = require(Avalog.Parent.HumanoidDescriber)
local Signal = require(Avalog.Parent.Signal)
local Future = require(Avalog.Parent.Future)
local Fusion = require(Avalog.Parent.Fusion)
local States = require(Avalog.SourceCode.Client.UI.States)
local ScaleLimits = require(Avalog.SourceCode.Shared.ScaleLimits)
local SquashNumber = require(Avalog.SourceCode.Shared.Utils.SquashNumber)
local CatalogItems = require(Avalog.SourceCode.Client.UI.StateExtensions.CatalogItems)
local AvatarManager = require(Avalog.SourceCode.Client.Avatar.AvatarManager)
local EnumSerDes = require(Avalog.Parent.EnumSerDes)
local Sift = require(Avalog.Parent.Sift)
local Config = require(Avalog.Config)

local Zap
if RunService:IsRunning() then
	Zap = require(Avalog.SourceCode.Shared.Zap.client)
end

export type CatalogItem = {
	Id: number,
	Type: Enum.AvatarItemType,
	AssetType: Enum.AvatarAssetType,
	Name: string,
}

local CONFIG = Config:Get()

local LocalAvatar = {
	Character = Instance.new("Model"),
	Humanoid = Instance.new("Humanoid"),
	HumanoidDescriber = HumanoidDescriber(Instance.new("HumanoidDescription")),
	Updated = Signal.new(),
}

function LocalAvatar:ApplyLimits()
	LocalAvatar.HumanoidDescriber:UpdateData(function(Data)
		if #Data.Accessories > CONFIG.Limits.Accessories then
			Data.Accessories = Sift.Array.slice(Data.Accessories, 1, CONFIG.Limits.Accessories)
		end

		return Data
	end)
end

function LocalAvatar:UpdateServer(Data: HumanoidDescriber.Data): ()
	if Data then
		if Zap ~= nil then
			Zap.UpdateAvatar.Fire(EnumSerDes.SerializeTree(Data))
		end
	end
end

function LocalAvatar:Redo()
	local UndoHistoryValue = Fusion.peek(States.Avatar.UndoHistory)
	local LastDataEntry = UndoHistoryValue[#UndoHistoryValue]

	if LastDataEntry then
		LocalAvatar.HumanoidDescriber:UpdateData(function()
			return LastDataEntry
		end)

		table.remove(UndoHistoryValue, #UndoHistoryValue)

		States.Avatar.UndoHistory:set(UndoHistoryValue)

		self:Apply()
	end
end

function LocalAvatar:Undo()
	local Data = LocalAvatar.HumanoidDescriber:GetData()
	local HistoryDataValue = Fusion.peek(States.Avatar.DataHistory)
	local LastDataEntry = HistoryDataValue[#HistoryDataValue - 1]

	if LastDataEntry then
		local UndoHistoryValue = Fusion.peek(States.Avatar.UndoHistory)

		LocalAvatar.HumanoidDescriber:UpdateData(function()
			return LastDataEntry
		end)

		table.remove(HistoryDataValue, #HistoryDataValue)
		table.insert(UndoHistoryValue, Data)

		States.Avatar.DataHistory:set(HistoryDataValue)
		States.Avatar.UndoHistory:set(UndoHistoryValue)

		self:Apply()
	end
end

function LocalAvatar:SaveAvatar()
	AvatarEditorService:PromptSaveAvatar(
		LocalAvatar.HumanoidDescriber.HumanoidDescription,
		LocalAvatar.Humanoid.RigType
	)
end

function LocalAvatar:UpdateScale(Property: string, Value: number)
	local Clamp = ScaleLimits[Property]
	if Clamp then
		Value = SquashNumber(Value, Clamp.Min, Clamp.Max)
	end

	LocalAvatar.HumanoidDescriber:UpdateData(function(Data)
		if Data.Scale[Property] ~= nil then
			Data.Scale[Property] = Value
		end

		return Data
	end)

	self:Apply()
end

function LocalAvatar:SetColor(Color: Color3)
	LocalAvatar.HumanoidDescriber:UpdateData(function(Data)
		for PartName, _ in pairs(Data.BodyPartColors) do
			Data.BodyPartColors[PartName] = Color
		end

		return Data
	end)

	self:Apply()
end

function LocalAvatar:PromptCheckout(Items)
	if #Items > 0 then
		if Zap ~= nil then
			Zap.BulkPurchaseAvatarItems.Fire(EnumSerDes.SerializeTree(Items))
		end

		return true
	else
		return false
	end
end

function LocalAvatar:GetUnownedItems()
	local Items = {}

	local AssetIdsValue = Fusion.peek(States.Avatar.EquippedAssetIds)
	local BundleIdsValue = Fusion.peek(States.Avatar.EquippedBundleIds)

	for _, AssetId in ipairs(AssetIdsValue) do
		local AssetInfo = CatalogItems:GetItem(AssetId, Enum.InfoType.Asset)

		if AssetInfo and not AssetInfo.Offsale then
			local Owned = CatalogItems:OwnsItem(AssetId, Enum.AvatarItemType.Asset)

			if Owned == false then
				table.insert(Items, {
					Id = tostring(AssetId),
					Type = Enum.MarketplaceProductType.AvatarAsset,
				})
			end
		end
	end

	for _, BundleId in ipairs(BundleIdsValue) do
		local ItemInfo = CatalogItems:GetItem(BundleId, Enum.InfoType.Bundle)

		if ItemInfo and not ItemInfo.Offsale then
			local Owned = CatalogItems:OwnsItem(BundleId, Enum.AvatarItemType.Bundle)

			if Owned == false then
				table.insert(Items, {
					Id = tostring(BundleId),
					Type = Enum.MarketplaceProductType.AvatarBundle,
				})
			end
		end
	end

	return Items
end

function LocalAvatar:ToggleCatalogItem(CatalogItem: CatalogItem)
	if CatalogItem.Type == Enum.AvatarItemType.Bundle then
		local BundleDetails = CatalogItems:GetBundle(CatalogItem.Id)
		local UserOutfit

		if BundleDetails then
			for _, BundleItem in ipairs(BundleDetails.Items) do
				if BundleItem.Type == "UserOutfit" then
					UserOutfit = BundleItem
					break
				end
			end

			if UserOutfit then
				local Success, Result = Future.Try(function()
					return Players:GetHumanoidDescriptionFromOutfitId(UserOutfit.Id)
				end):Await()

				if Success then
					local BundleDescriber = HumanoidDescriber(Result)
					local BundleData = BundleDescriber:GetData()

					if BundleDetails.BundleType == "BodyParts" then
						LocalAvatar.HumanoidDescriber:UpdateData(function(Data)
							Data.BodyParts = BundleData.BodyParts
							return Data
						end)
					elseif BundleDetails.BundleType == "AvatarAnimations" then
						LocalAvatar.HumanoidDescriber:UpdateData(function(Data)
							Data.Animations = BundleData.Animations
							return Data
						end)
					elseif BundleDetails.BundleType == "DynamicHead" then
						LocalAvatar.HumanoidDescriber:EquipAsset(
							BundleData.BodyParts.Head,
							Enum.AvatarAssetType.DynamicHead
						)
					end

					BundleDescriber:Destroy()
					Result:Destroy()
				else
					warn(Result)
				end
			end
		end
	elseif CatalogItem.Type == Enum.AvatarItemType.Asset then
		local Equipped = LocalAvatar.HumanoidDescriber:IsAssetEquipped(CatalogItem.Id, CatalogItem.AssetType)
		LocalAvatar.HumanoidDescriber:SetAssetEquipped(CatalogItem.Id, CatalogItem.AssetType, not Equipped)
	end

	self:ApplyLimits()

	self:Apply()
end

function LocalAvatar:Reset()
	local Success, Result = Future.Try(function()
		return Players:GetHumanoidDescriptionFromUserId(Players.LocalPlayer.UserId or 1)
	end):Await()

	if Success then
		LocalAvatar.HumanoidDescriber:ReadFromDescription(Result)
		self:Apply()
	else
		warn(Result)
	end
end

function LocalAvatar:Apply(UpdateServer: boolean?)
	if UpdateServer == nil then
		UpdateServer = true
	end

	local Success, Result = Future.Try(function()
		return LocalAvatar.Humanoid:ApplyDescriptionReset(LocalAvatar.HumanoidDescriber.HumanoidDescription)
	end):After(function()
		if UpdateServer then
			return LocalAvatar:UpdateServer(LocalAvatar.HumanoidDescriber:GetData())
		end

		return true
	end)

	return Success, Result
end

function LocalAvatar:SetCharacter(Character: Model)
	if Character then
		local OriginalCharacter = Character
		local OriginalHumanoid = OriginalCharacter:FindFirstChildOfClass("Humanoid")

		Character.Archivable = true
		Character = Character:Clone()
		Character:PivotTo(CFrame.new())

		local Humanoid = Character:FindFirstChildOfClass("Humanoid")
		if Humanoid and OriginalHumanoid then
			local HumanoidDescription = Humanoid:FindFirstChildOfClass("HumanoidDescription")
			local HumanoidDescriberObject = HumanoidDescriber(HumanoidDescription)

			LocalAvatar.Character = Character
			LocalAvatar.Humanoid = Humanoid
			LocalAvatar.HumanoidDescriber = HumanoidDescriberObject

			OriginalCharacter.Humanoid.ApplyDescriptionFinished:Connect(function()
				HumanoidDescriberObject:ReadFromDescription(OriginalHumanoid:GetAppliedDescription())
				self:Apply(false)
			end)

			LocalAvatar.Updated:Fire()
		end
	end
end

local function LoadExistingCharacter()
	local ExistingCharacter = Players.LocalPlayer.Character

	if ExistingCharacter then
		LocalAvatar:SetCharacter(ExistingCharacter)
	end
end

if RunService:IsRunning() then
	Players.LocalPlayer.CharacterAdded:Connect(function(Character)
		LocalAvatar:SetCharacter(Character)
	end)
	Players.LocalPlayer.CharacterAppearanceLoaded:Connect(function(Character)
		LocalAvatar:SetCharacter(Character)
	end)

	task.spawn(function()
		LoadExistingCharacter()
		task.wait(3)
		LoadExistingCharacter()
	end)
end

function LocalAvatar:Clear()
	LocalAvatar.Character = nil
	LocalAvatar.Humanoid = nil
	LocalAvatar.HumanoidDescriber = nil

	LocalAvatar.Updated:Fire()
end

return LocalAvatar :: AvatarManager.AvatarManager


local GuiService = game:GetService("GuiService")
local StarterGui = game:GetService("StarterGui")

local Avalog = script.Parent.Parent.Parent
local Fusion = require(Avalog.Parent.Fusion)
local States = require(Avalog.SourceCode.Client.UI.States)
local Config = require(Avalog.Config)

local ITEM_HANDLERS = {
	Backpack = Enum.CoreGuiType.Backpack,
	PlayerList = Enum.CoreGuiType.PlayerList,
	Emotes = Enum.CoreGuiType.EmotesMenu,
	Captures = Enum.CoreGuiType.Captures,
	Chat = Enum.CoreGuiType.Chat,
	TouchControls = function(Enabled: boolean)
		GuiService.TouchControlsEnabled = Enabled
	end,
}

local Scope = Fusion.scoped(Fusion)

local CoreUI = {}

Scope:Observer(States.Open):onChange(function()
	local OpenValue = Fusion.peek(States.Open)

	for _, CoreGuiItem in ipairs(Config:Get().HiddenCoreGui) do
		local ItemHandler = ITEM_HANDLERS[CoreGuiItem]

		if typeof(ItemHandler) == "EnumItem" then
			StarterGui:SetCoreGuiEnabled(ItemHandler, not OpenValue)
		elseif typeof(ItemHandler) == "function" then
			ItemHandler(not OpenValue)
		end
	end
end)

return CoreUI


local RunService = game:GetService("RunService")

local Avalog = script.Parent.Parent.Parent
local Version = require(Avalog.SourceCode.Shared.Version)

local Promo = {}

function Promo:Start()
	if not RunService:IsStudio() then
		print(`Running Avalog {Version} 🛠\n> https://devforum.roblox.com/t/3513102`)
	end
end

return Promo


-- Provides some service. Must call `Setup` to setup services before getting any.
-- This is how the plugin swaps its implementations.

local Avalog = script.Parent.Parent.Parent
local AvatarManager = require(Avalog.SourceCode.Client.Avatar.AvatarManager)

export type AvatarManager = AvatarManager.AvatarManager

local ServiceProvider = {
	_AvatarManager = nil :: AvatarManager.AvatarManager?,
}

export type SetupOptions = {
	AvatarManager: AvatarManager.AvatarManager,
}
function ServiceProvider:Setup(Options: SetupOptions)
	self._AvatarManager = Options.AvatarManager
end

function ServiceProvider:GetAvatarManager()
	assert(self._AvatarManager, "Missing AvatarManager. Forgot to call Setup?")
	return self._AvatarManager
end

return ServiceProvider


local MarketplaceService = game:GetService("MarketplaceService")
local RunService = game:GetService("RunService")

local Avalog = script.Parent.Parent.Parent.Parent.Parent
local OnyxUI = require(Avalog.Parent.OnyxUI)
local Fusion = require(Avalog.Parent.Fusion)
local States = require(Avalog.SourceCode.Client.UI.States)
local ServiceProvider = require(Avalog.SourceCode.Client.ServiceProvider)
local Navigation = require(Avalog.SourceCode.Client.UI.StateExtensions.Navigation)
local Config = require(Avalog.Config)

local CONFIG = Config:Get()

local Children = Fusion.Children

export type Props = {}

return function(Scope: Fusion.Scope<any>, Props: Props)
	local Scope = Fusion.innerScope(Scope, Fusion, OnyxUI.Util, OnyxUI.Components)
	local Theme = OnyxUI.Themer.Theme:now()

	local AvatarManager = ServiceProvider:GetAvatarManager()

	local ViewportCamera = Scope:New "Camera" {
		CFrame = CFrame.lookAt(Vector3.new(4, 0, -8), Vector3.new(0, 0, 0)),
		CameraType = Enum.CameraType.Scriptable,
	}

	local RotationAngle = 0
	local Dragging = false
	local LastDragPosition = Vector2.new()

	local DragDetector = Scope:New "UIDragDetector" {
		DragStyle = Enum.UIDragDetectorDragStyle.Scriptable,
	}

	DragDetector.DragStart:Connect(function(Position)
		Dragging = true
		LastDragPosition = Position
	end)
	DragDetector.DragEnd:Connect(function()
		Dragging = false
	end)
	DragDetector.DragContinue:Connect(function(Position)
		RotationAngle += (Position - LastDragPosition).X / 50
		LastDragPosition = Position
	end)

	RunService.RenderStepped:Connect(function(DeltaTime)
		if not Dragging then
			RotationAngle += DeltaTime * math.rad(30)
		end

		ViewportCamera.CFrame = CFrame.lookAt(
			Vector3.new(8 * math.cos(RotationAngle), 0, 8 * math.sin(RotationAngle)),
			Vector3.new(0, 0, 0)
		)
	end)

	return Scope:Image(OnyxUI.Util.CombineProps(Props, {
		Name = script.Name,
		MaxSize = Scope:Computed(function(Use)
			local ResolutionValue = Use(States.Screen.Resolution)

			if ResolutionValue.X < 800 then
				return Vector2.new(ResolutionValue.X / 2.5, math.huge)
			elseif ResolutionValue.X > 1000 then
				return Vector2.new(ResolutionValue.X / 3, math.huge)
			else
				return Vector2.new(16 * 24, math.huge)
			end
		end),
		BackgroundTransparency = 0,
		BackgroundColor3 = Theme.Colors.Base.Dark,
		Image = "rbxassetid://140363116814273",
		ScaleType = Enum.ScaleType.Crop,
		AutomaticSize = Enum.AutomaticSize.X,
		ListEnabled = true,
		ListHorizontalFlex = Enum.UIFlexAlignment.Fill,
		ListVerticalFlex = Enum.UIFlexAlignment.Fill,

		[Children] = {
			Scope:Frame {
				Name = "Contents",
				Padding = Scope:Computed(function(Use)
					return UDim.new(0, Use(Theme.Spacing["0.25"]))
				end),
				AutomaticSize = Enum.AutomaticSize.None,

				[Children] = {
					DragDetector,
					Scope:New "ViewportFrame" {
						Name = "Viewport",
						Size = UDim2.fromScale(1, 1),
						BackgroundTransparency = 1,
						BackgroundColor3 = Color3.fromRGB(0, 0, 0),
						CurrentCamera = ViewportCamera,

						[Children] = {
							Scope:New "WorldModel" {
								[Children] = {
									ViewportCamera,

									Scope:Computed(function(Use)
										local CharacterValue = Use(States.Avatar.Character)

										if RunService:IsRunning() then
											return CharacterValue
										else
											return nil
										end
									end),
								},
							},
						},
					},
					Scope:Frame {
						Name = "Controls",
						Size = UDim2.fromScale(1, 1),
						ListEnabled = true,
						ListHorizontalAlignment = Enum.HorizontalAlignment.Center,
						ListVerticalFlex = Enum.UIFlexAlignment.SpaceBetween,

						[Children] = {
							Scope:Card {
								Name = "TopControls",
								ListEnabled = true,
								ListFillDirection = Enum.FillDirection.Horizontal,
								ListHorizontalFlex = Enum.UIFlexAlignment.Fill,
								ListHorizontalAlignment = Enum.HorizontalAlignment.Center,
								ListPadding = Scope:Computed(function(Use)
									return UDim.new(0, Use(Theme.Spacing["0.5"]))
								end),
								CornerRadius = Scope:Computed(function(Use)
									return UDim.new(0, Use(Theme.CornerRadius["2"]))
								end),
								Padding = Scope:Computed(function(Use)
									return UDim.new(0, Use(Theme.Spacing["0.5"]))
								end),
								BackgroundColor3 = Theme.Colors.Base.Main,

								[Children] = {
									Scope:Button {
										Name = "CheckoutButton",
										Content = { "rbxassetid://13846738861", "Buy" },
										SizeVariant = "Large",
										Disabled = States.Checkout.Loading,

										OnActivated = function()
											States.Checkout.Loading:set(true)
											local UnownedItems = AvatarManager:GetUnownedItems()
											States.Checkout.Loading:set(false)

											if #UnownedItems > 0 then
												local Success = AvatarManager:PromptCheckout(UnownedItems)
												if Success then
													MarketplaceService.PromptBulkPurchaseFinished:Once(
														function(_, Status)
															if
																Status
																== Enum.MarketplaceBulkPurchasePromptStatus.Completed
															then
																task.wait(0.5)
																AvatarManager:SaveAvatar()
															end
														end
													)
												end
											else
												AvatarManager:SaveAvatar()
											end
										end,
									},
									Scope:Button {
										Content = { "rbxassetid://13846852950", "Done" },
										Color = Theme.Colors.Success.Main,
										SizeVariant = "Large",
										Visible = CONFIG.AvatarSavingAllowed,

										OnActivated = function()
											AvatarManager:UpdateServer(Fusion.peek(States.Avatar.Data))

											States.Open:set(false)
										end,
									},
								},
							},
							Scope:Card {
								Name = "BottomControls",
								ListEnabled = true,
								ListFillDirection = Enum.FillDirection.Horizontal,
								ListHorizontalFlex = Enum.UIFlexAlignment.Fill,
								ListHorizontalAlignment = Enum.HorizontalAlignment.Center,
								ListPadding = Scope:Computed(function(Use)
									return UDim.new(0, Use(Theme.Spacing["0.75"]))
								end),
								CornerRadius = Scope:Computed(function(Use)
									return UDim.new(0, Use(Theme.CornerRadius["2"]))
								end),
								Padding = Scope:Computed(function(Use)
									return UDim.new(0, Use(Theme.Spacing["0.5"]))
								end),
								BackgroundColor3 = Theme.Colors.Base.Main,

								[Children] = {
									Scope:Button {
										Content = { "rbxassetid://13846120661" },
										SizeVariant = "Large",
										Disabled = Scope:Computed(function(Use)
											local DataHistoryValue = Use(States.Avatar.DataHistory)
											local Category = Use(Navigation.Category)
											return (#DataHistoryValue <= 1) or (Category.Name == "Equipped")
										end),

										OnActivated = function()
											AvatarManager:Undo()
										end,
									},
									Scope:Button {
										Content = { "rbxassetid://13847364382" },
										SizeVariant = "Large",
										Color = Theme.Colors.Error.Main,

										OnActivated = function()
											AvatarManager:Reset()
										end,
									},
									Scope:Button {
										Content = { "rbxassetid://13846158505" },
										SizeVariant = "Large",
										Disabled = Scope:Computed(function(Use)
											local UndoHistoryValue = Use(States.Avatar.UndoHistory)
											local Category = Use(Navigation.Category)
											return (#UndoHistoryValue == 0) or (Category == "Equipped")
										end),

										OnActivated = function()
											AvatarManager:Redo()
										end,
									},
								},
							},
						},
					},
				},
			},
		},
	}))
end


local Avalog = script.Parent.Parent.Parent.Parent.Parent
local OnyxUI = require(Avalog.Parent.OnyxUI)
local Fusion = require(Avalog.Parent.Fusion)
local ServiceProvider = require(Avalog.SourceCode.Client.ServiceProvider)
local ScaleLimits = require(Avalog.SourceCode.Shared.ScaleLimits)
local StretchNumber = require(Avalog.SourceCode.Shared.Utils.StretchNumber)
local States = require(Avalog.SourceCode.Client.UI.States)

local Children = Fusion.Children
local SliderSetting = require(script.Parent.SliderSetting)

export type Props = {
	Shown: Fusion.UsedAs<boolean>?,
}

return function(Scope: Fusion.Scope<any>, Props: Props)
	local Scope = Fusion.innerScope(Scope, Fusion, OnyxUI.Util, OnyxUI.Components, {
		SliderSetting = SliderSetting,
	})
	local Theme = OnyxUI.Themer.Theme:now()
	local AvatarManager = ServiceProvider:GetAvatarManager()

	local Shown = OnyxUI.Util.Fallback(Props.Shown, false)

	local ScaleProperties = {
		{ Name = "BodyType", Value = Scope:Value(1) },
		{ Name = "Proportion", Value = Scope:Value(1) },
		{ Name = "Height", Value = Scope:Value(1) },
		{ Name = "Width", Value = Scope:Value(1) },
		{ Name = "Depth", Value = Scope:Value(1) },
		{ Name = "Head", Value = Scope:Value(1) },
	}

	for _, ScaleProperty in ipairs(ScaleProperties) do
		Scope:Observer(ScaleProperty.Value):onChange(function()
			AvatarManager:UpdateScale(ScaleProperty.Name, Fusion.peek(ScaleProperty.Value))
		end)
	end

	AvatarManager.Updated:Connect(function()
		if AvatarManager.HumanoidDescriber == nil then return end

		local Data = AvatarManager.HumanoidDescriber:GetData()

		for _, ScaleProperty in ipairs(ScaleProperties) do
			local ExistingScale = Data.Scale[ScaleProperty.Name]
			local ScaleLimit = ScaleLimits[ScaleProperty.Name]
			if ExistingScale then
				ScaleProperty.Value:set(StretchNumber(ExistingScale, ScaleLimit.Min, ScaleLimit.Max))
			end
		end
	end)

	return Scope:Scroller(OnyxUI.Util.CombineProps(Props, {
		Name = script.Name,
		ListEnabled = true,
		ListHorizontalFlex = Enum.UIFlexAlignment.Fill,
		Visible = Shown,
		CanvasSize = UDim2.new(),
		PaddingRight = Scope:Computed(function(Use)
			return UDim.new(0, Use(Theme.Spacing["1"]))
		end),
		Padding = Scope:Computed(function(Use)
			return UDim.new(0, Use(Theme.Spacing["0"]))
		end),

		[Children] = {
			Scope:ForValues(ScaleProperties, function(Use, Scope, ScaleProperty)
				return OnyxUI.Themer.Theme:is(Fusion.peek(States.Theme)):during(function()
					return Scope:SliderSetting {
						Label = ScaleProperty.Name,
						Value = ScaleProperty.Value,
					}
				end)
			end),
		},
	}))
end


local Avalog = script.Parent.Parent.Parent.Parent.Parent
local OnyxUI = require(Avalog.Parent.OnyxUI)
local Fusion = require(Avalog.Parent.Fusion)
local CatalogSearch = require(Avalog.SourceCode.Client.UI.StateExtensions.CatalogSearch)
local Navigation = require(Avalog.SourceCode.Client.UI.StateExtensions.Navigation)
local States = require(Avalog.SourceCode.Client.UI.States)
local Sift = require(Avalog.Parent.Sift)

export type Props = {
	PrefixItems: Fusion.UsedAs<{ [string]: any }>?,
	LoadItems: Fusion.UsedAs<boolean>?,
	Shown: Fusion.UsedAs<boolean>?,
}

return function(Scope: Fusion.Scope<any>, Props: Props)
	local Scope = Fusion.innerScope(Scope, Fusion, OnyxUI.Util, OnyxUI.Components, {
		ItemsTab = require(script.Parent.ItemsTab),
	})

	local Shown = OnyxUI.Util.Fallback(Props.Shown, false)
	local PrefixItems = OnyxUI.Util.Fallback(Props.PrefixItems, {})
	local LoadItems = OnyxUI.Util.Fallback(Props.LoadItems, true)

	local DeepestCategory = Scope:Computed(function(Use)
		local CategoryValue = Use(Navigation.Category)
		local SubCategoryValue = Use(Navigation.SubCategory)

		if SubCategoryValue ~= nil then
			return SubCategoryValue
		else
			return CategoryValue
		end
	end)
	local BundleTypes = Scope:Computed(function(Use)
		local DeepestCategoryValue = Use(DeepestCategory)

		if DeepestCategoryValue and DeepestCategoryValue.BundleTypes then
			return DeepestCategoryValue.BundleTypes
		else
			return {}
		end
	end)
	local AssetTypes = Scope:Computed(function(Use)
		local DeepestCategoryValue = Use(DeepestCategory)

		if DeepestCategoryValue and DeepestCategoryValue.AssetTypes then
			return DeepestCategoryValue.AssetTypes
		else
			return {}
		end
	end)

	local Search = Scope:Value(nil)
	local Results = Scope:Value({})

	local function UpdateSearch()
		if not Fusion.peek(Shown) then
			return
		end
		if not Fusion.peek(LoadItems) then
			return
		end

		local BundleTypesValue = Fusion.peek(BundleTypes)
		local AssetTypesValue = Fusion.peek(AssetTypes)
		local SearchQueryValue = Fusion.peek(States.Catalog.SearchQuery)
		local DeepestCategoryValue = Fusion.peek(DeepestCategory)

		local Searching = string.len((SearchQueryValue or "")) == 0

		local SearchObject = CatalogSearch.Search({
			BundleTypes = BundleTypesValue,
			AssetTypes = AssetTypesValue,
			SearchKeyword = SearchQueryValue,
			CategoryFilter = (Searching and DeepestCategoryValue.CategoryFilter) or nil,
		})

		SearchObject.Updated:Connect(function()
			Results:set(SearchObject.Results)
		end)

		Search:set(SearchObject)
		Results:set(SearchObject.Results)
	end
	Scope:Observer(BundleTypes):onChange(function()
		UpdateSearch()
	end)
	Scope:Observer(AssetTypes):onChange(function()
		UpdateSearch()
	end)
	Scope:Observer(States.Catalog.SearchQuery):onChange(function()
		UpdateSearch()
	end)
	UpdateSearch()

	local CombinedItems = Scope:Computed(function(Use)
		local ResultsValue = Use(Results)
		local PrefixItemsValue = Use(PrefixItems)
		local SearchQueryValue = Use(States.Catalog.SearchQuery)

		if (SearchQueryValue == nil) or (SearchQueryValue == "") then
			return Sift.Array.concat(PrefixItemsValue, ResultsValue)
		else
			return ResultsValue
		end
	end)

	return Scope:ItemsTab(OnyxUI.Util.CombineProps(Props, {
		Name = script.Name,
		Items = Scope:ForValues(CombinedItems, function(Use, Scope, Item)
			if typeof(Item.ItemType) == "string" then
				Item.ItemType = Enum.AvatarItemType[Item.ItemType]
			end

			return {
				Id = Item.Id,
				Type = Item.ItemType,
			}
		end),

		OnReachedEnd = function()
			local SearchValue = Fusion.peek(Search)

			if SearchValue ~= nil then
				SearchValue:LoadNextPage()
			end
		end,
	}))
end


local RunService = game:GetService("RunService")

local Avalog = script.Parent.Parent.Parent.Parent.Parent
local OnyxUI = require(Avalog.Parent.OnyxUI)
local Fusion = require(Avalog.Parent.Fusion)
local States = require(Avalog.SourceCode.Client.UI.States)
local Categories = require(Avalog.SourceCode.Shared.Categories)
local AvatarAssetTypes = require(Avalog.SourceCode.Shared.AvatarAssetTypes)

local Children = Fusion.Children

-- Avalog cannot develop w/o featured tab
if RunService:IsStudio() then
	if Categories.Marketplace[1].Name ~= "Featured" then
		table.insert(Categories.Marketplace, 1, {
			Name = "Featured",
			CategoryFilter = Enum.CatalogCategoryFilter.Recommended,
			AssetTypes = AvatarAssetTypes.Types.All,
			BundleTypes = {
				Enum.BundleType.Animations,
				Enum.BundleType.BodyParts,
				Enum.BundleType.DynamicHead,
				Enum.BundleType.DynamicHeadAvatar,
				Enum.BundleType.Shoes,
			},
		})
	end
end

export type Props = {
	OnExit: () -> (),
}

return function(Scope: Fusion.Scope<any>, Props: Props)
	local Scope = Fusion.innerScope(Scope, Fusion, OnyxUI.Util, OnyxUI.Components, {
		CategoryBar = require(script.Parent.CategoryBar),
		NavigationBar = require(script.Parent.NavigationBar),
		TabDisplay = require(script.Parent.TabDisplay),
	})
	local Theme = OnyxUI.Themer.Theme:now()

	return Scope:Frame(OnyxUI.Util.CombineProps(Props, {
		Name = "CatalogMenu",
		BackgroundTransparency = 0,
		BackgroundColor3 = Theme.Colors.Base.Main,
		ListEnabled = true,
		ListHorizontalFlex = Enum.UIFlexAlignment.Fill,
		ListPadding = Scope:Computed(function(Use)
			return UDim.new(0, Use(Theme.Spacing["0"]))
		end),

		[Children] = {
			Scope:NavigationBar {
				OnExit = Props.OnExit,
			},
			Scope:CategoryBar {},
			Scope:Computed(function(Use, Scope)
				-- Clean menu when closed, improving passive game performance
				if Use(States.Open) then
					return OnyxUI.Themer.Theme:is(Theme):during(function()
						return Scope:TabDisplay {}
					end)
				end
			end),
		},
	}))
end


local Avalog = script.Parent.Parent.Parent.Parent.Parent
local OnyxUI = require(Avalog.Parent.OnyxUI)
local Fusion = require(Avalog.Parent.Fusion)
local States = require(Avalog.SourceCode.Client.UI.States)
local Categories = require(Avalog.SourceCode.Shared.Categories)
local CategoriesToNames = require(Avalog.SourceCode.Shared.Categories.CategoriesToNames)
local Navigation = require(Avalog.SourceCode.Client.UI.StateExtensions.Navigation)

local Children = Fusion.Children

export type Props = {}

return function(Scope: Fusion.Scope<any>, Props: Props)
	local Scope = Fusion.innerScope(Scope, Fusion, OnyxUI.Util, OnyxUI.Components, {
		Tabs = require(script.Parent.Tabs),
	})
	local Theme = OnyxUI.Themer.Theme:now()

	Scope:Observer(States.Catalog.Mode):onChange(function()
		States.Catalog.Category:set(1)
		States.Catalog.SubCategory:set(1)
	end)

	return Scope:Frame(OnyxUI.Util.CombineProps(Props, {
		Name = script.Name,
		ListEnabled = true,
		ListPadding = Scope:Computed(function(Use)
			return UDim.new(0, Use(Theme.Spacing["0"]))
		end),
		ListHorizontalFlex = Enum.UIFlexAlignment.Fill,

		[Children] = {
			Scope:Tabs {
				Name = "Categories",
				Options = Scope:Computed(function(Use)
					local ModeValue = Use(Navigation.Mode)

					local CategoryNames = CategoriesToNames(Categories[ModeValue])

					if (ModeValue == "Marketplace") and (CategoryNames[1] ~= "Featured") then
						table.insert(CategoryNames, 1, "Featured")
					end

					return CategoryNames
				end),
				Selection = States.Catalog.Category,
				Padding = Scope:Computed(function(Use)
					return UDim.new(0, Use(Theme.Spacing["0.5"]))
				end),
				AutomaticSize = Enum.AutomaticSize.Y,
			},
			Scope:Tabs {
				Name = "SubCategories",
				Options = Scope:Computed(function(Use)
					local CategoryValue = Use(Navigation.Category)

					return CategoriesToNames(CategoryValue.SubCategories or {})
				end),
				Selection = States.Catalog.SubCategory,
				Padding = Scope:Computed(function(Use)
					return UDim.new(0, Use(Theme.Spacing["0.5"]))
				end),
				BackgroundTransparency = 0,
				BackgroundColor3 = Theme.Colors.Neutral.Main,
				AutomaticSize = Enum.AutomaticSize.Y,
				Visible = Scope:Computed(function(Use)
					local CategoryValue = Use(Navigation.Category)

					return #CategoriesToNames(CategoryValue.SubCategories or {}) > 0
				end),
			},
		},
	}))
end


local Avalog = script.Parent.Parent.Parent.Parent.Parent
local OnyxUI = require(Avalog.Parent.OnyxUI)
local Fusion = require(Avalog.Parent.Fusion)

local Util = OnyxUI.Util
local Themer = OnyxUI.Themer

export type Props = {} & { [string]: any }

return function(Scope: Fusion.Scope<any>, Props: Props)
	local Scope = Fusion.innerScope(Scope, Fusion, OnyxUI.Util, OnyxUI.Components)
	local Theme = Themer.Theme:now()

	local Color = Util.Fallback(Props.Color, Theme.Colors.Neutral.Main)

	return Scope:Button(Util.CombineProps(Props, {
		Color = Color,
		CornerRadius = Scope:Computed(function(Use)
			return UDim.new(0, Use(Theme.CornerRadius["1.5"]))
		end),
		PaddingTop = Scope:Computed(function(Use)
			return UDim.new(0, Use(Theme.Spacing["0.5"]))
		end),
		PaddingBottom = Scope:Computed(function(Use)
			return UDim.new(0, Use(Theme.Spacing["0.5"]))
		end),
		PaddingLeft = Scope:Computed(function(Use)
			return UDim.new(0, Use(Theme.Spacing["0.5"]))
		end),
		PaddingRight = Scope:Computed(function(Use)
			return UDim.new(0, Use(Theme.Spacing["0.5"]))
		end),
	}))
end


local Avalog = script.Parent.Parent.Parent.Parent.Parent
local OnyxUI = require(Avalog.Parent.OnyxUI)
local Fusion = require(Avalog.Parent.Fusion)
local States = require(Avalog.SourceCode.Client.UI.States)

local ItemsTab = require(script.Parent.ItemsTab)

export type Props = ItemsTab.Props & {}

return function(Scope: Fusion.Scope<any>, Props: Props)
	local Scope = Fusion.innerScope(Scope, Fusion, OnyxUI.Util, OnyxUI.Components, {
		ItemButton = require(script.Parent.ItemButton),
		ItemsTab = require(script.Parent.ItemsTab),
	})

	local Shown = OnyxUI.Util.Fallback(Props.Shown, false)

	local AssetIds = Scope:Value({})

	local function UpdateAssetIds()
		local EquippedAssetIdsValue = Fusion.peek(States.Avatar.EquippedAssetIds)
		AssetIds:set(EquippedAssetIdsValue)
	end

	UpdateAssetIds()

	Scope:Observer(States.Avatar.Loaded):onChange(function()
		UpdateAssetIds()
	end)

	return Scope:ItemsTab(OnyxUI.Util.CombineProps(Props, {
		Name = script.Name,
		Items = Scope:ForValues(AssetIds, function(Use, Scope, AssetId)
			return {
				Id = AssetId,
				Type = Enum.AvatarItemType.Asset,
			}
		end),
	}))
end


local Avalog = script.Parent.Parent.Parent.Parent.Parent
local OnyxUI = require(Avalog.Parent.OnyxUI)
local Fusion = require(Avalog.Parent.Fusion)

export type Props = {
	Options: Fusion.UsedAs<{ string }>?,
	Selection: Fusion.UsedAs<number>?,
	OnSwap: Fusion.UsedAs<(number) -> ()>?,
}

return function(Scope: Fusion.Scope<any>, Props: Props)
	local Scope = Fusion.innerScope(Scope, Fusion, OnyxUI.Util, OnyxUI.Components)
	local Theme = OnyxUI.Themer.Theme:now()

	local Options = OnyxUI.Util.Fallback(Props.Options, {})
	local Selection = Scope:EnsureValue(OnyxUI.Util.Fallback(Props.Selection, 1))
	local OnSwap = OnyxUI.Util.Fallback(Props.OnSwap, function() end)

	local function RotateSelection(Increment: number)
		local OptionsValue = Fusion.peek(Options)
		local SelectionValue = Fusion.peek(Selection)
		local NewSelection = SelectionValue + Increment

		if NewSelection > #OptionsValue then
			NewSelection = 1
		elseif NewSelection < 1 then
			NewSelection = #OptionsValue
		end

		Selection:set(NewSelection)
	end

	return Scope:IconButton(OnyxUI.Util.CombineProps(Props, {
		Name = script.Name,
		Style = "Ghost",
		Color = Theme.Colors.BaseContent.Main,
		Image = Scope:Computed(function(Use)
			return Use(Options)[Use(Selection)]
		end),

		OnActivated = function()
			local SelectionValue = Fusion.peek(Selection)
			local OnSwapValue = Fusion.peek(OnSwap)

			RotateSelection(1)
			OnSwapValue(SelectionValue, SelectionValue + 1)
		end,
	}))
end


local AvatarEditorService = game:GetService("AvatarEditorService")

local Avalog = script.Parent.Parent.Parent.Parent.Parent
local OnyxUI = require(Avalog.Parent.OnyxUI)
local Fusion = require(Avalog.Parent.Fusion)
local Navigation = require(Avalog.SourceCode.Client.UI.StateExtensions.Navigation)
local States = require(Avalog.SourceCode.Client.UI.States)
local Inventory = require(Avalog.SourceCode.Client.UI.StateExtensions.Inventory)

export type Props = {
	Shown: Fusion.UsedAs<boolean>?,
}

return function(Scope: Fusion.Scope<any>, Props: Props)
	local Scope = Fusion.innerScope(Scope, Fusion, OnyxUI.Util, OnyxUI.Components, {
		ItemsTab = require(script.Parent.ItemsTab),
	})

	local Shown = OnyxUI.Util.Fallback(Props.Shown, false)

	local DeepestCategory = Scope:Computed(function(Use)
		local CategoryValue = Use(Navigation.Category)
		local SubCategoryValue = Use(Navigation.SubCategory)

		if SubCategoryValue ~= nil then
			return SubCategoryValue
		else
			return CategoryValue
		end
	end)
	local AssetTypes = Scope:Computed(function(Use)
		local DeepestCategoryValue = Use(DeepestCategory)

		if DeepestCategoryValue and DeepestCategoryValue.AssetTypes then
			return DeepestCategoryValue.AssetTypes
		else
			return {}
		end
	end)

	local Search = Scope:Value(nil)
	local Results = Scope:Value({})

	local function UpdateSearch()
		if not Fusion.peek(Shown) then
			return
		end
		if not Fusion.peek(States.Inventory.AccessAllowed) then
			return
		end

		local AssetTypesValue = Fusion.peek(AssetTypes)

		local SearchObject = Inventory.Search(AssetTypesValue)

		SearchObject.Updated:Connect(function()
			Results:set(SearchObject.Results)
		end)

		Search:set(SearchObject)
		Results:set(SearchObject.Results)
	end
	Scope:Observer(AssetTypes):onChange(function()
		UpdateSearch()
	end)
	Scope:Observer(States.Inventory.AccessAllowed):onChange(function()
		UpdateSearch()
	end)
	UpdateSearch()

	local ShownResults = Scope:ForValues(Results, function(Use, Scope, Result)
		local SearchQueryValue = Use(States.Catalog.SearchQuery)

		if
			(utf8.len(SearchQueryValue) > 0)
			and (string.find(string.lower(Result.Name), string.lower(SearchQueryValue), 1, true) == nil)
		then
			return nil
		else
			return Result
		end
	end)

	local function CheckInventoryAccess()
		local ShownValue = Fusion.peek(Shown)
		local AccessAllowedValue = Fusion.peek(States.Inventory.AccessAllowed)

		if ShownValue and not AccessAllowedValue then
			AvatarEditorService:PromptAllowInventoryReadAccess()
		end
	end

	CheckInventoryAccess()

	AvatarEditorService.PromptAllowInventoryReadAccessCompleted:Connect(function(Result)
		local ShownValue = Fusion.peek(Shown)
		local ModeValue = Fusion.peek(States.Catalog.Mode)

		if ShownValue and (Result ~= Enum.AvatarPromptResult.Success) then
			States.Catalog.Mode:set(ModeValue + 1)
		end
	end)

	return Scope:ItemsTab(OnyxUI.Util.CombineProps(Props, {
		Name = script.Name,
		Items = Scope:ForValues(ShownResults, function(Use, Scope, Result)
			return {
				Id = Result.AssetId,
				Type = Enum.AvatarItemType.Asset,
				Owned = true,
			}
		end),
		OnReachedEnd = function()
			local SearchValue = Fusion.peek(Search)
			if SearchValue ~= nil then
				SearchValue:LoadNextPage()
			end
		end,
	}))
end


local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")

local Avalog = script.Parent.Parent.Parent.Parent.Parent
local OnyxUI = require(Avalog.Parent.OnyxUI)
local Fusion = require(Avalog.Parent.Fusion)
local States = require(Avalog.SourceCode.Client.UI.States)
local ServiceProvider = require(Avalog.SourceCode.Client.ServiceProvider)
local CatalogItems = require(Avalog.SourceCode.Client.UI.StateExtensions.CatalogItems)
local Future = require(Avalog.Parent.Future)

local Children = Fusion.Children

local ITEM_TYPE_INFO_TYPES = {
	[Enum.AvatarItemType.Asset] = Enum.InfoType.Asset,
	[Enum.AvatarItemType.Bundle] = Enum.InfoType.Bundle,
}

local CustomButton = require(script.Parent.CustomButton)

export type Props = CustomButton.Props & {
	Id: Fusion.UsedAs<number>?,
	Type: Fusion.UsedAs<Enum.AvatarItemType>?,
	Owned: Fusion.UsedAs<boolean>?,
}

return function(Scope: Fusion.Scope<any>, Props: Props)
	local Scope = Fusion.innerScope(Scope, Fusion, OnyxUI.Util, OnyxUI.Components, {
		CustomButton = CustomButton,
	})
	local Theme = OnyxUI.Themer.Theme:now()
	local AvatarManager = ServiceProvider:GetAvatarManager()

	local Id = Scope:EnsureValue(OnyxUI.Util.Fallback(Props.Id, nil))
	local Type = OnyxUI.Util.Fallback(Props.Type, Enum.AvatarItemType.Asset)
	local Owned = OnyxUI.Util.Fallback(Props.Owned, false)

	local ItemInfo = Scope:Value(nil)
	local function UpdateItemInfo()
		local IdValue = Fusion.peek(Id)
		local TypeValue = Fusion.peek(Type)

		task.spawn(function()
			local FetchedItemInfo = CatalogItems:GetItem(IdValue, ITEM_TYPE_INFO_TYPES[TypeValue])
			if FetchedItemInfo then
				ItemInfo:set(FetchedItemInfo)
			end
		end)
	end
	Scope:Observer(Id):onChange(function()
		UpdateItemInfo()
	end)
	UpdateItemInfo()

	local SafeItemInfo = Scope:Computed(function(Use)
		local ItemInfoValue = Use(ItemInfo)
		local IdValue = Use(Id)

		if ItemInfoValue then
			return ItemInfoValue
		else
			return {
				Id = IdValue,
				Name = "Name",
				Price = 0,
				Offsale = false,
				Type = Enum.AvatarItemType.Asset,
				AssetType = Enum.AvatarAssetType.Hat,
			}
		end
	end)
	local Equippable = Scope:Computed(function(Use)
		local SafeItemInfoValue = Use(SafeItemInfo)
		return SafeItemInfoValue.Type == Enum.AvatarItemType.Asset
	end)
	local Equipped = Scope:Computed(function(Use)
		local AssetIdsValue = Use(States.Avatar.AssetIds)
		local IdValue = Use(Id)
		local SafeItemInfoValue = Use(SafeItemInfo)

		if SafeItemInfoValue.AssetType == Enum.AvatarAssetType.EmoteAnimation and AvatarManager.HumanoidDescriber then
			return AvatarManager.HumanoidDescriber:GetEquippedEmote(SafeItemInfoValue.Name) ~= nil
		else
			return table.find(AssetIdsValue, IdValue) ~= nil
		end
	end)

	local EquippedValueState = Scope:Value(Fusion.peek(Equipped))
	Scope:Observer(Equipped):onChange(function()
		EquippedValueState:set(Fusion.peek(Equipped))
	end)

	return Scope:CustomButton(OnyxUI.Util.CombineProps(Props, {
		Name = script.Name,
		StrokeEnabled = Equipped,
		StrokeThickness = Theme.StrokeThickness["2"],
		StrokeColor = Theme.Colors.Primary.Main,
		ListEnabled = false,

		OnActivated = function()
			local ItemInfoValue = Fusion.peek(ItemInfo)

			if ItemInfoValue then
				AvatarManager:ToggleCatalogItem(ItemInfoValue)
			end
		end,

		[Children] = {
			Scope:Frame {
				Name = "Details",
				ListEnabled = true,
				ListFillDirection = Enum.FillDirection.Vertical,
				ListHorizontalFlex = Enum.UIFlexAlignment.Fill,
				ListPadding = Scope:Computed(function(Use)
					return UDim.new(0, Use(Theme.Spacing["0.5"]))
				end),
				Padding = Scope:Computed(function(Use)
					return UDim.new(0, Use(Theme.Spacing["0.5"]))
				end),
				Size = UDim2.fromScale(1, 0),

				[Children] = {
					Scope:Image {
						BackgroundTransparency = 1,
						Image = Scope:Computed(function(Use)
							local SafeItemInfoValue = Use(SafeItemInfo)
							local ItemTypeToType = {
								[Enum.AvatarItemType.Bundle] = "BundleThumbnail",
								[Enum.AvatarItemType.Asset] = "Asset",
							}

							return `rbxthumb://id={SafeItemInfoValue.Id}&type={(ItemTypeToType[SafeItemInfoValue.Type] or "Asset")}&w=150&h=150`
						end),
						ScaleType = Enum.ScaleType.Fit,
						CornerRadius = Scope:Computed(function(Use)
							return UDim.new(0, Use(Theme.CornerRadius["1"]))
						end),
						Size = Scope:Computed(function(Use)
							local Offset = Use(Theme.TextSize["1"]) * 5
							return UDim2.fromOffset(Offset, Offset)
						end),
					},
					Scope:Frame {
						Name = "Text",
						ListEnabled = true,
						ListFillDirection = Enum.FillDirection.Vertical,
						ListHorizontalFlex = Enum.UIFlexAlignment.Fill,
						ListPadding = Scope:Computed(function(Use)
							return UDim.new(0, Use(Theme.Spacing["0.5"]))
						end),

						[Children] = {
							Scope:Text {
								Text = Scope:Computed(function(Use)
									local SafeItemInfoValue = Use(SafeItemInfo)
									return SafeItemInfoValue.Name
								end),
								Size = Scope:Computed(function(Use)
									return UDim2.fromOffset(0, Use(Theme.TextSize["1"]) * 2)
								end),
								AutomaticSize = Enum.AutomaticSize.None,
								ClipsDescendants = true,
								RichText = false,
								AutoLocalize = false,
							},
							Scope:IconText {
								Name = "Price",
								Content = Scope:Computed(function(Use)
									local ItemInfoValue = Use(ItemInfo)
									if ItemInfoValue and ItemInfoValue.Price then
										return { "rbxassetid://102562927562320", ItemInfoValue.Price }
									else
										return { "Offsale" }
									end
								end),
								ListPadding = Scope:Computed(function(Use)
									return UDim.new(0, Use(Theme.Spacing["0.25"]))
								end),
								ListWraps = false,
								ContentWrapped = false,
								AutoLocalize = false,
							},
						},
					},
				},
			},
			Scope:Checkbox {
				Checked = EquippedValueState,
				Color = Scope:Computed(function(Use)
					if Use(EquippedValueState) then
						return Use(Theme.Colors.Primary.Main)
					else
						return Use(Theme.Colors.BaseContent.Dark)
					end
				end),
				AnchorPoint = Vector2.new(1, 0),
				Position = UDim2.fromScale(1, 0),
				Active = false,
				Selectable = false,
				Visible = Equippable,
			},
			Scope:Button {
				Content = { "Buy" },
				AnchorPoint = Vector2.new(0.5, 1),
				Position = UDim2.fromScale(0.5, 1),
				Size = UDim2.fromScale(1, 0),
				Color = OnyxUI.Util.Colors.Green["500"],
				Scale = Scope:Spring(
					Scope:Computed(function(Use)
						local EquippedValue = Use(Equipped)
						local OwnedValue = Use(Owned)

						if EquippedValue and not OwnedValue then
							return 1
						else
							return 0
						end
					end),
					Theme.SpringSpeed["2"],
					Theme.SpringDampening["1.5"]
				),

				OnActivated = function()
					Future.Try(function()
						local IdValue = Fusion.peek(Id)

						return MarketplaceService:PromptPurchase(Players.LocalPlayer, IdValue)
					end)
				end,
			},
		},
	}))
end


local Avalog = script.Parent.Parent.Parent.Parent.Parent
local OnyxUI = require(Avalog.Parent.OnyxUI)
local Fusion = require(Avalog.Parent.Fusion)
local States = require(Avalog.SourceCode.Client.UI.States)

local Children = Fusion.Children
local Out = Fusion.Out
local OnChange = Fusion.OnChange

export type Props = { [string]: any } & {
	Shown: Fusion.UsedAs<boolean>?,
	Items: Fusion.UsedAs<{ [string]: any }>?,
	OnReachedEnd: Fusion.UsedAs<() -> ()>?,
}

return function(Scope: Fusion.Scope<any>, Props: Props)
	local Scope = Fusion.innerScope(Scope, Fusion, OnyxUI.Util, OnyxUI.Components, {
		ItemButton = require(script.Parent.ItemButton),
	})
	local Theme = OnyxUI.Themer.Theme:now()

	local Shown = OnyxUI.Util.Fallback(Props.Shown, false)
	local Items = OnyxUI.Util.Fallback(Props.Items, {})
	local OnReachedEnd = OnyxUI.Util.Fallback(Props.OnReachedEnd, function() end)

	local AbsoluteCanvasSize = Scope:Value(Vector2.new())
	local AbsoluteWindowSize = Scope:Value(Vector2.new())

	local LastUpdated = 0

	local ItemsTab = Scope:Hydrate(Scope:Scroller(OnyxUI.Util.CombineProps(Props, {
		Name = script.Name,
		ListEnabled = true,
		ListHorizontalFlex = Enum.UIFlexAlignment.Fill,
		ListPadding = Scope:Computed(function(Use)
			return UDim.new(0, Use(Theme.Spacing["1"]))
		end),
		ScrollBarImageColor3 = Theme.Colors.Neutral.Light,
		ClipsDescendants = false,
		Visible = Shown,
		CanvasSize = UDim2.fromOffset(0, 0),
		Padding = Scope:Computed(function(Use)
			return UDim.new(0, Use(Theme.Spacing["0"]))
		end),
		PaddingRight = Scope:Computed(function(Use)
			return UDim.new(0, Use(Theme.Spacing["1"]))
		end),
	}))) {
		[OnChange "CanvasPosition"] = function(CanvasPosition: Vector2)
			local OnReachedEndValue = Fusion.peek(OnReachedEnd)
			local AbsoluteCanvasSizeValue = Fusion.peek(AbsoluteCanvasSize)
			local AbsoluteWindowSizeValue = Fusion.peek(AbsoluteWindowSize)

			local CanvasHeight = AbsoluteCanvasSizeValue.Y - AbsoluteWindowSizeValue.Y

			if (CanvasPosition.Y >= (CanvasHeight - 500)) and ((os.time() - LastUpdated) > 0.5) then
				LastUpdated = os.time()

				OnReachedEndValue()
			end
		end,
		[Out "AbsoluteCanvasSize"] = AbsoluteCanvasSize,
		[Out "AbsoluteWindowSize"] = AbsoluteWindowSize,

		[Children] = {
			Scope:Frame {
				Name = "Items",
				ListEnabled = true,
				ListFillDirection = Enum.FillDirection.Horizontal,
				ListHorizontalFlex = Enum.UIFlexAlignment.Fill,
				ListWraps = true,
				AutomaticSize = Enum.AutomaticSize.Y,

				[Children] = {
					Scope:ForPairs(Items, function(Use, Scope, Index, Item)
						return Index,
							OnyxUI.Themer.Theme:is(Use(States.Theme)):during(function()
								return Scope:ItemButton {
									Id = Item.Id,
									Type = Item.Type,
									Visible = Item.Visible,
									Owned = Item.Owned,
									LayoutOrder = Index,
									Size = UDim2.fromScale(1 / 8, 0),
									AutomaticSize = Enum.AutomaticSize.Y,
									MinSize = Scope:Computed(function(Use)
										return Vector2.new(Use(Theme.Spacing["8"]), 0)
									end),
								}
							end)
					end),
				},
			},
			Scope:Card {
				Name = "NoItemsCard",
				ListEnabled = true,
				ListHorizontalAlignment = Enum.HorizontalAlignment.Center,
				BackgroundTransparency = 1,
				AutomaticSize = Enum.AutomaticSize.Y,
				PaddingTop = Scope:Computed(function(Use)
					return UDim.new(0, Use(Theme.Spacing["8"]))
				end),
				PaddingBottom = Scope:Computed(function(Use)
					return UDim.new(0, Use(Theme.Spacing["8"]))
				end),

				[Children] = {
					Scope:Image {
						Image = "rbxassetid://10480493542",
						BackgroundTransparency = 1,
					},
					Scope:Text {
						Text = "No more items to show!\nRate limits may have been hit.",
						TextWrapped = false,
						TextXAlignment = Enum.TextXAlignment.Center,
					},
				},
			},
		},
	}

	return ItemsTab
end


local Avalog = script.Parent.Parent.Parent.Parent.Parent
local OnyxUI = require(Avalog.Parent.OnyxUI)
local Fusion = require(Avalog.Parent.Fusion)
local Config = require(Avalog.Config)
local States = require(Avalog.SourceCode.Client.UI.States)
local IconSwap = require(script.Parent.IconSwap)
local Tabs = require(script.Parent.Tabs)
local CategoryBar = require(script.Parent.CategoryBar)
local Sift = require(Avalog.Parent.Sift)

local Children = Fusion.Children

local CONFIG = Config:Get()
local EDIT_MODE_ICONS = {
	Marketplace = "rbxassetid://13850201307",
	Inventory = "rbxassetid://13846287045",
	Customize = "rbxassetid://13845862177",
}

export type Props = { [string]: any } & {
	OnExit: () -> (),
}
return function(Scope: Fusion.Scope<any>, Props: Props)
	local Scope = Fusion.innerScope(Scope, Fusion, OnyxUI.Util, OnyxUI.Components, {
		Tabs = Tabs,
		CategoryBar = CategoryBar,
		IconSwap = IconSwap,
	})
	local Theme = OnyxUI.Themer.Theme:now()
	local Modes = Config:Get().EditModes

	local SearchBarText = Scope:Value("")

	return Scope:Frame(OnyxUI.Util.CombineProps(Props, {
		Name = script.Name,
		BackgroundColor3 = Theme.Colors.Base.Main,
		BackgroundTransparency = 0,
		ListEnabled = true,
		ListFillDirection = Enum.FillDirection.Horizontal,
		ListHorizontalFlex = Enum.UIFlexAlignment.Fill,

		[Children] = {
			Scope:Frame {
				ListEnabled = true,
				ListFillDirection = Enum.FillDirection.Horizontal,
				ListHorizontalAlignment = Enum.HorizontalAlignment.Center,
				ListVerticalAlignment = Enum.VerticalAlignment.Center,
				ListPadding = Scope:Computed(function(Use)
					return UDim.new(0, Use(Theme.Spacing["0.5"]))
				end),
				AutomaticSize = Enum.AutomaticSize.Y,
				Padding = Scope:Computed(function(Use)
					return UDim.new(0, Use(Theme.Spacing["0.25"]))
				end),
				PaddingTop = Scope:Computed(function(Use)
					return UDim.new(0, Use(Theme.Spacing["0.75"]))
				end),

				[Children] = {
					Scope:IconSwap {
						Options = Sift.Array.map(CONFIG.EditModes, function(EditMode)
							return EDIT_MODE_ICONS[EditMode]
						end),
						Selection = States.Catalog.Mode,
						ContentSize = Theme.TextSize["1.5"],
						Color = Theme.Colors.NeutralContent.Main,
					},
					Scope:TextInput {
						PlaceholderText = "Search",
						Color = Theme.Colors.NeutralContent.Main,
						MaxSize = Scope:Computed(function(Use)
							return Vector2.new(Use(Theme.Spacing["16"]), math.huge)
						end),
						FlexMode = Enum.UIFlexMode.Fill,
						AutomaticSize = Enum.AutomaticSize.XY,
						Text = SearchBarText,
						CornerRadius = Scope:Computed(function(Use)
							return UDim.new(0, Use(Theme.CornerRadius["1.5"]))
						end),
						-- StrokeTransparency = 0,
						TextTransparency = 0,

						OnFocusLost = function()
							States.Catalog.SearchQuery:set(Fusion.peek(SearchBarText))
						end,
					},
				},
			},
		},
	}))
end


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

local Avalog = script.Parent.Parent.Parent.Parent.Parent
local OnyxUI = require(Avalog.Parent.OnyxUI)
local Fusion = require(Avalog.Parent.Fusion)
local States = require(Avalog.SourceCode.Client.UI.States)
local Sift = require(Avalog.Parent.Sift)
local Config = require(Avalog.Config)

local CONFIG = Config:Get()
local CURATED_SHOWN = (not RunService:IsStudio()) and not (game.CreatorId == Players.LocalPlayer.UserId) -- Only show curated items when relevant

local function CamelToPascal(Dictionary: { [string]: any })
	return Sift.Dictionary.map(Dictionary, function(Value, Key)
		if typeof(Value) == "table" then
			Value = CamelToPascal(Value)
		end
		if typeof(Key) == "string" then
			Key = Key:gsub("(%l)(%w*)", function(a, b)
				return a:upper() .. b
			end)
		end

		return Value, Key
	end)
end

local Children = Fusion.Children

local CatalogItemsTab = require(script.Parent.CatalogItemsTab)

export type Props = CatalogItemsTab.Props & {}

return function(Scope: Fusion.Scope<any>, Props: Props)
	local Scope = Fusion.innerScope(Scope, Fusion, OnyxUI.Util, OnyxUI.Components, {
		ItemButton = require(script.Parent.ItemButton),
		CatalogItemsTab = CatalogItemsTab,
		CustomButton = require(script.Parent.CustomButton),
	})
	local Theme = OnyxUI.Themer.Theme:now()

	local CloudConfigLoaded = Scope:Computed(function(Use)
		return Use(States.CloudConfig).latestVersion ~= "0.0.0"
	end)

	return Scope:CatalogItemsTab(OnyxUI.Util.CombineProps(Props, {
		Name = script.Name,
		PrefixItems = Scope:Computed(function(Use)
			if CURATED_SHOWN then
				local PinnedItems = Use(States.CloudConfig).pinnedItems

				for _, Item in PinnedItems do
					Item.Id = tonumber(Item.itemId)
					Item.ItemType = Item.itemType
				end

				return Sift.Array.merge(CONFIG.Featured.Items, PinnedItems)
			end

			return {}
		end),
		LoadItems = CloudConfigLoaded,

		[Children] = {
			Scope:Computed(function(Use)
				local CloudConfigFailedValue = Use(CloudConfigLoaded)

				if CloudConfigFailedValue then
					return
				end

				return Scope:Card {
					BackgroundColor3 = Theme.Colors.Neutral.Main,
					ListEnabled = true,
					CornerRadius = Scope:Computed(function(Use)
						return UDim.new(0, Use(Theme.CornerRadius["1.5"]))
					end),

					[Children] = {
						Scope:Text {
							Text = "Avalog requires access to HTTP requests to function properly.",
						},
						Scope:TextInput {
							Text = "https://devforum.roblox.com/t/3513102/71",
							Color = Theme.Colors.BaseContent.Main,
							TextTransparency = 0,
							TextEditable = false,
						},
					},
				}
			end),
			Scope:Computed(function(Use)
				if CURATED_SHOWN then
					return Scope:CustomButton {
						AutomaticSize = Enum.AutomaticSize.Y,
						Visible = States.Featured.MessageShown,
						ListHorizontalAlignment = Enum.HorizontalAlignment.Left,

						OnActivated = function()
							States.Featured.MessageShown:set(false)
						end,

						[Children] = {
							Scope:IconText {
								Content = {
									"rbxassetid://13850331560",
									"Contains exclusive, curated, and sponsored listings.",
								},
								ListPadding = Scope:Computed(function(Use)
									return UDim.new(0, Use(Theme.Spacing["0.5"]))
								end),
								ListVerticalAlignment = Enum.VerticalAlignment.Center,
							},
						},
					}
				end
			end),
		},
	}))
end


local Avalog = script.Parent.Parent.Parent.Parent.Parent
local ServiceProvider = require(Avalog.SourceCode.Client.ServiceProvider)
local OnyxUI = require(Avalog.Parent.OnyxUI)
local Fusion = require(Avalog.Parent.Fusion)
local States = require(Avalog.SourceCode.Client.UI.States)

local Children = Fusion.Children

local SliderSetting = require(script.Parent.SliderSetting)

export type Props = { [string]: any } & {
	Shown: Fusion.UsedAs<boolean>?,
}

return function(Scope: Fusion.Scope<any>, Props: Props)
	local Scope = Fusion.innerScope(Scope, Fusion, OnyxUI.Util, OnyxUI.Components, {
		SliderSetting = SliderSetting,
	})
	local Theme = OnyxUI.Themer.Theme:now()
	local AvatarManager = ServiceProvider:GetAvatarManager()

	local Shown = OnyxUI.Util.Fallback(Props.Shown, false)

	local Hue = Scope:Value(1)
	local Saturation = Scope:Value(1)
	local Brightness = Scope:Value(1)

	local function UpdateColor()
		local HueValue = Fusion.peek(Hue)
		local SaturationValue = Fusion.peek(Saturation)
		local BrightnessValue = Fusion.peek(Brightness)

		AvatarManager:SetColor(Color3.fromHSV(HueValue, SaturationValue, BrightnessValue))
	end
	Scope:Observer(Hue):onChange(function()
		UpdateColor()
	end)
	Scope:Observer(Saturation):onChange(function()
		UpdateColor()
	end)
	Scope:Observer(Brightness):onChange(function()
		UpdateColor()
	end)

	return Scope:Frame(OnyxUI.Util.CombineProps(Props, {
		Name = script.Name,
		ListEnabled = true,
		ListHorizontalFlex = Enum.UIFlexAlignment.Fill,
		Visible = Shown,

		[Children] = {
			OnyxUI.Themer.Theme:is(Fusion.peek(States.Theme)):during(function()
				return {
					Scope:SliderSetting {
						Label = "Hue",
						Value = Hue,
					},
					Scope:SliderSetting {
						Label = "Saturation",
						Value = Saturation,
					},
					Scope:SliderSetting {
						Label = "Brightness",
						Value = Brightness,
					},
				}
			end),
		},
	}))
end


local Avalog = script.Parent.Parent.Parent.Parent.Parent
local OnyxUI = require(Avalog.Parent.OnyxUI)
local Fusion = require(Avalog.Parent.Fusion)

local Children = Fusion.Children

export type Props = { [string]: any } & {
	Value: Fusion.UsedAs<number>?,
	Label: Fusion.UsedAs<string>?,
}

return function(Scope: Fusion.Scope<any>, Props: Props)
	local Scope = Fusion.innerScope(Scope, Fusion, OnyxUI.Util, OnyxUI.Components)
	local Theme = OnyxUI.Themer.Theme:now()

	local Label = OnyxUI.Util.Fallback(Props.Label, "Setting")
	local Value = OnyxUI.Util.Fallback(Props.Value, 1)

	return Scope:Frame(OnyxUI.Util.CombineProps(Props, {
		Name = script.Name,
		ListEnabled = true,
		ListPadding = Scope:Computed(function(Use)
			return UDim.new(0, Use(Theme.Spacing["0.75"]))
		end),
		ListHorizontalFlex = Enum.UIFlexAlignment.Fill,

		[Children] = {
			Scope:Heading {
				Text = Label,
				HeadingSize = 1.25,
				TextWrapped = false,
			},
			Scope:Slider {
				Value = Value,
			},
		},
	}))
end