local UI = require(script.Parent:WaitForChild("UI"))

local Bans = {}
Bans.__index = Bans

function Bans.new(_K)
	local BANS = _K.Data.bans

	local escape = _K.Util.String.escapeRichText

	local function close(self)
		self.Activated(false)
	end

	local newBanDialog, unbanDialog
	unbanDialog = UI.new "Dialog" {
		LayoutOrder = 9,
		BackgroundTransparency = 0.875,
		BackgroundColor3 = UI.Theme.Secondary,
		AutomaticSize = Enum.AutomaticSize.Y,
		Size = UDim2.fromScale(1, 0),
		ActionText = "Unban this user?",
		Action = true,
		LeftAction = true,
		RightAction = true,
		Visible = false,
		Close = function(self)
			task.defer(close, self)
		end,

		[UI.Hook] = {
			Action = function(v)
				unbanDialog.Visible(false)
				if v and unbanDialog.userId then
					_K.Remote.Unban:Fire(tonumber(unbanDialog.userId))
				end
				unbanDialog.userId = nil
			end,
		},
	}
	unbanDialog._content:Destroy()

	local inputSize = function()
		return UDim2.fromOffset(0, UI.Theme.FontSize + UI.Theme.PaddingWithStroke().Offset)
	end

	local banType = UI.state("Server")
	local banTypes = UI.state({ "Forever", "Timed", "Server" })
	local banTarget = UI.state()
	local banReason = nil
	local banDuration = 1
	local banDurationUnit = UI.new "Select" {
		AutomaticSize = Enum.AutomaticSize.X,
		Value = "Hour",
		Choices = { "Hour", "Day", "Week", "Month", "Year" },
	}

	local banTargetInput
	local function resetbanTargetInput()
		banTargetInput.Value("")
	end

	banTargetInput = UI.new "Input" {
		AutomaticSize = Enum.AutomaticSize.X,
		FontFace = UI.Theme.FontMono,
		FontSize = UI.Theme.FontSize,
		MaxChars = 20,
		Placeholder = "UserId or Username",
		Size = inputSize,
		Value = "",

		UI.new "UIFlexItem" {
			FlexMode = Enum.UIFlexMode.Shrink,
		},

		[UI.Hook] = {
			Value = function(value)
				local userId
				if tonumber(value) then
					userId = tonumber(value)
				else
					local ok, result = pcall(_K.Service.Players.GetUserIdFromNameAsync, _K.Service.Players, value)
					userId = ok and result
				end

				local fauxArg = {
					_K = _K,
					definition = _K.Registry.commands.ban.args[1],
					command = {
						from = _K.LocalPlayer.UserId,
						fromRank = _K.Auth.getRank(_K.LocalPlayer.UserId),
						fromRole = {},
						rank = _K.Auth.hasCommand(_K.LocalPlayer.UserId, "ban") or 0,
					},
				}

				_K.Data.targetLimits[_K.LocalPlayer.UserId] = 0
				if userId and not _K.Auth.targetUserArgument(fauxArg, userId, userId) then
					userId = nil
					task.defer(resetbanTargetInput)
				end
				banTarget(userId)
			end,
		},
	}

	local banReasonInput = UI.new "Input" {
		AutomaticSize = Enum.AutomaticSize.X,
		FontFace = UI.Theme.FontMono,
		FontSize = UI.Theme.FontSize,
		MaxChars = 400,
		Placeholder = "No reason.",
		Size = inputSize,
		Value = "",
		[UI.Hook] = {
			Value = function(value)
				banReason = value ~= "" and value
			end,
		},
	}

	local function createItem(self)
		local tooltip = UI.new "Tooltip" {
			FontFace = UI.Theme.FontMono,
			Text = "Loading...",
		}

		local cleanup = { tooltip }
		local itemRef = {
			userId = 0,
			reason = nil,
			term = nil,
			name = "",
			tipText = "",
		}

		local editBanButton = UI.new "Button" {
			LayoutOrder = 9,
			Size = UDim2.new(1, 0, 1, 0),
			SizeConstraint = Enum.SizeConstraint.RelativeYY,
			Icon = UI.Theme.Image.Edit,
			IconProperties = {
				ImageColor3 = UI.Theme.PrimaryText,
				Size = UDim2.fromScale(0.625, 0.625),
			},
			Text = "",
			Activated = function()
				banType(if itemRef.term == "Server" or itemRef.term == "Forever" then itemRef.term else "Timed")
				banTarget(tonumber(itemRef.userId))
				banTargetInput.Value(itemRef.userId)
				banReason = itemRef.reason
				banReasonInput.Value(itemRef.reason or "")
				newBanDialog.Title("Edit Ban")
				newBanDialog.Visible(true)
			end,
		}
		table.insert(
			cleanup,
			UI.new "Tooltip" { Parent = editBanButton, Text = "Edit Ban", Hovering = editBanButton._hovering }
		)

		local unbanButton = UI.new "Button" {
			LayoutOrder = 9,
			BackgroundColor3 = UI.Theme.Invalid,
			Size = UDim2.new(1, 0, 1, 0),
			SizeConstraint = Enum.SizeConstraint.RelativeYY,
			Icon = UI.Theme.Image.Close_Bold,
			IconProperties = {
				ImageColor3 = UI.Theme.PrimaryText,
				Size = UDim2.fromScale(0.5, 0.5),
			},
			Text = "",
			Activated = function()
				unbanDialog.userId = itemRef.userId
				unbanDialog.ActionText(`Unban {itemRef.name} [{itemRef.userId}]?`)
				unbanDialog.Visible(true)
			end,
		}
		table.insert(
			cleanup,
			UI.new "Tooltip" { Parent = unbanButton, Text = "Remove Ban", Hovering = unbanButton._hovering }
		)

		UI.edit(editBanButton._instance.UICorner, { CornerRadius = UI.Theme.CornerPadded })
		UI.edit(unbanButton._instance.UICorner, { CornerRadius = UI.Theme.CornerPadded })

		itemRef._instance = UI.new "Frame" {
			BackgroundTransparency = 1,
			Size = self.ItemSize,

			UI.new "UIPadding" {
				PaddingBottom = UI.Theme.PaddingWithStroke,
			},

			UI.new "UIListLayout" {
				VerticalAlignment = Enum.VerticalAlignment.Center,
				FillDirection = Enum.FillDirection.Horizontal,
				SortOrder = Enum.SortOrder.LayoutOrder,
				Padding = UI.Theme.PaddingWithStroke,
			},

			UI.new "ImageLabel" {
				BackgroundTransparency = UI.Theme.TransparencyHeavy,
				BackgroundColor3 = UI.Theme.PrimaryText,
				Size = UDim2.new(1, 0, 1, 0),
				SizeConstraint = Enum.SizeConstraint.RelativeYY,

				UI.new "UICorner" {
					CornerRadius = UI.Theme.CornerPadded,
				},
				UI.new "Stroke" {},
			},

			UI.new "TextLabel" {
				Name = "Left",
				AutoLocalize = false,
				AutomaticSize = Enum.AutomaticSize.X,
				BackgroundTransparency = 1,
				Size = UDim2.new(0, 0, 1, 0),
				RichText = true,
				FontFace = UI.Theme.Font,
				TextSize = UI.Theme.FontSize,
				TextColor3 = UI.Theme.PrimaryText,
				TextStrokeColor3 = UI.Theme.Primary,
				TextStrokeTransparency = UI.Theme.TextStrokeTransparency,
				TextTruncate = Enum.TextTruncate.SplitWord,
				TextXAlignment = Enum.TextXAlignment.Left,
			},
			UI.new "TextLabel" {
				Name = "Right",
				AutoLocalize = false,
				AutomaticSize = Enum.AutomaticSize.X,
				BackgroundTransparency = 1,
				Size = UDim2.new(0, 0, 1, 0),
				RichText = true,
				FontFace = UI.Theme.Font,
				TextSize = UI.Theme.FontSize,
				TextColor3 = UI.Theme.PrimaryText,
				TextStrokeColor3 = UI.Theme.Primary,
				TextStrokeTransparency = UI.Theme.TextStrokeTransparency,
				TextTruncate = Enum.TextTruncate.SplitWord,
				TextXAlignment = Enum.TextXAlignment.Right,

				UI.new "UIFlexItem" { FlexMode = Enum.UIFlexMode.Fill },
			},

			editBanButton,
			unbanButton,
			tooltip,
			[UI.Clean] = cleanup,
		}

		table.insert(
			cleanup,
			tooltip.Visible:Connect(function()
				if tooltip.Visible._value then
					tooltip.Text(itemRef.tipText)
				end
			end)
		)

		return itemRef
	end

	local function renderItem(self, itemData, userId)
		local ban = _K.Data.bans[userId]
		if ban and ban.renderTextLeft then
			local item = itemData._instance
			item.Left.Text = ban.renderTextLeft
			item.Right.Text = ban.renderTextRight
			item.ImageLabel.Image = `rbxthumb://type=AvatarHeadShot&id={ban.userId}&w=48&h=48`

			itemData.name = ban[1]
			itemData.tipText = ban.tipText
			itemData.userId = ban.userId
			itemData.reason = ban.reason
			itemData.term = ban.term
		end
	end

	local emptyBanListButton = UI.new "Button" {
		Label = "Create a new ban",

		Activated = function()
			if not newBanDialog.Visible._value then
				banTarget(nil)
				banTargetInput.Value("")
				banReason = nil
				banReasonInput.Value("")
				newBanDialog.Title("New Ban")
				UI.Sound.Hover03:Play()
			end
			newBanDialog.Visible(not newBanDialog.Visible._value)
		end,
	}

	emptyBanListButton._instance.Visible = _K.Util.Table.countKeys(_K.client.bans._value) == 0
	_K.client.bans:Connect(function()
		emptyBanListButton._instance.Visible = _K.Util.Table.countKeys(_K.client.bans._value) == 0
	end)

	local scroller = UI.new "ScrollerFast" {
		Name = "Bans",
		List = _K.client.bans,
		Enabled = false,
		DictList = true,
		FilterInput = true,
		Visible = false,
		CreateItem = createItem,
		RenderItem = renderItem,

		emptyBanListButton,
		unbanDialog,
	}

	scroller._filterUpdate = true
	local function filterTest(self, list)
		local filter = string.lower(scroller._input._input.Text)
		self._filter = filter

		local noFilter = string.find(filter, "^%s*$")

		local new = {}
		for _, userId in list do
			local ban = _K.Data.bans[userId]
			if not ban then
				continue
			end

			if not ban.filterText then
				ban.userId = userId
				ban.reason = if ban[2] and _K.Util.String.trim(ban[2]) ~= "" then ban[2] else "No reason."
				ban.term = if ban[3] and ban[3] ~= 0
					then (if ban[3] > 0 and ban[3] ~= math.huge then os.date("%y-%m-%d %X", ban[3]) else "Forever")
					else "Server"

				ban.rawTextLeft = `@{ban[1] or "UNKNOWN"} [{ban.userId}]}`
				ban.rawTextRight = `{ban.term}`

				ban.richTextLeft = `<b>@{if ban[1] then escape(ban[1]) else "UNKNOWN"}</b> [{userId}]`
				ban.richTextRight = `<font transparency="0.5">{ban.term}</font>`

				ban.filterTextLeft = string.lower(ban.rawTextLeft)
				ban.filterTextRight = string.lower(ban.rawTextRight)
				ban.filterText = `{ban.filterTextLeft}\t{ban.filterTextRight}`

				ban.tipText = `<b>Reason:</b> {escape(ban.reason)}`

				task.spawn(function()
					local name = if ban[4] then _K.Util.getUserInfo(ban[4]).Username else "SYSTEM"
					ban.tipText = `<b>Author:</b> @{name}\n{ban.tipText}`
					scroller:render(true)
				end)
			end

			if noFilter then
				ban.renderTextLeft = ban.richTextLeft
				ban.renderTextRight = ban.richTextRight
				table.insert(new, userId)
			elseif string.find(ban.filterText, filter, 1, true) then
				local filterLeft = string.find(ban.filterTextLeft, self._filter, 1, true)
				local filterRight = string.find(ban.filterTextRight, self._filter, 1, true)

				if filterLeft then
					local pre = escape(string.sub(ban.rawTextLeft, 1, filterLeft - 1))
					local query = escape(string.sub(ban.rawTextLeft, filterLeft, filterLeft + #self._filter - 1))
					local post = escape(string.sub(ban.rawTextLeft, filterLeft + #self._filter))

					ban.renderTextLeft =
						`<font transparency="0.5">{pre}</font><b>{query}</b><font transparency="0.5">{post}</font>`
				else
					ban.renderTextLeft = `<font transparency="0.5">{ban.rawTextLeft}</font>`
				end

				if filterRight then
					local pre = escape(string.sub(ban.rawTextRight, 1, filterRight - 1))
					local query = escape(string.sub(ban.rawTextRight, filterRight, filterRight + #self._filter - 1))
					local post = escape(string.sub(ban.rawTextRight, filterRight + #self._filter))

					ban.renderTextRight =
						`<font transparency="0.5">{pre}</font><b>{query}</b><font transparency="0.5">{post}</font>`
				else
					ban.renderTextRight = `<font transparency="0.5">{ban.rawTextRight}</font>`
				end

				table.insert(new, userId)
			end
		end
		return new
	end

	task.defer(scroller.filter, scroller, filterTest)

	local function termWeight(ban)
		return if ban and ban[3] and ban[3] ~= 0 then (if ban[3] > 0 and ban[3] ~= math.huge then 1000 else 2000) else 0
	end

	task.defer(scroller.sort, scroller, function(a, b)
		a, b = BANS[a], BANS[b]
		local weightA, weightB = termWeight(a), termWeight(b)
		if weightA == weightB then
			return (a and b and a[1] and b[1]) and a[1] < b[1]
		else
			return weightA < weightB
		end
	end)

	local banTypeListItem = UI.new "ListItem" {
		Text = "Type",
		UI.new "Select" {
			Value = banType,
			Choices = banTypes,
			[UI.Hook] = {
				Value = function(value)
					banType(value)
				end,
			},
		},
	}

	local function updateAuth()
		local savedBans = _K.Auth.hasPermission(_K.LocalPlayer.UserId, "banasync")
		if savedBans then
			banType("Forever")
		else
			banType("Server")
		end
		banTypeListItem._instance.Visible = savedBans
	end
	_K.Hook.authChanged:Connect(updateAuth)
	updateAuth()

	newBanDialog = UI.new "Dialog" {
		Parent = UI.LayerTop,
		BackgroundTransparency = 0,
		Draggable = true,
		Title = "New Ban",
		Visible = false,
		ExitButton = true,
		Modal = true,
		Close = function()
			newBanDialog.Visible(false)
		end,

		UI.new "UIPadding" {
			PaddingTop = UI.Theme.PaddingWithStroke,
			PaddingBottom = UI.Theme.PaddingWithStroke,
			PaddingLeft = UI.Theme.PaddingWithStroke,
			PaddingRight = UI.Theme.PaddingWithStroke,
		},

		UI.new "ListItem" {
			Text = "User",
			UI.new "ImageLabel" {
				BackgroundTransparency = UI.Theme.TransparencyHeavy,
				BackgroundColor3 = UI.Theme.PrimaryText,
				Size = inputSize,
				Image = function()
					return `rbxthumb://type=AvatarHeadShot&id={banTarget()}&w=48&h=48`
				end,
				UI.new "UIAspectRatioConstraint" {
					AspectType = Enum.AspectType.ScaleWithParentSize,
					DominantAxis = Enum.DominantAxis.Height,
				},
				UI.new "UICorner" {
					CornerRadius = UI.Theme.CornerPadded,
				},
			},
			banTargetInput,
		},
		UI.new "ListItem" {
			Text = "Reason",
			banReasonInput,
		},

		banTypeListItem,

		UI.new "ListItem" {
			Text = "Duration",
			Visible = function()
				return banType() == "Timed"
			end,
			UI.new "Input" {
				AutomaticSize = Enum.AutomaticSize.X,
				FontFace = UI.Theme.FontMono,
				FontSize = UI.Theme.FontSize,
				MaxChars = 3,
				NumberOnly = true,
				NumberRange = NumberRange.new(1, 999),
				Placeholder = "",
				Size = inputSize,
				Value = 1,
				[UI.Hook] = {
					Value = function(value)
						banDuration = value
					end,
				},
			},
			banDurationUnit,
		},

		UI.new "Button" {
			Label = "Ban",
			Activated = function()
				if banTarget._value then
					newBanDialog.Visible(false)
					local duration: number = if banType._value == "Server"
						then 0
						elseif banType._value == "Forever" then -1
						else banDuration * _K.Util.Time.fromUnit(banDurationUnit.Value._value)
					_K.Remote.Ban:Fire(banTarget._value, banReason, duration)
				end
			end,
		},
	}

	local newBanButton = UI.new "Button" {
		LayoutOrder = 3,
		ActiveSound = false,
		Icon = UI.Theme.Image.Add,
		IconProperties = {
			ImageColor3 = UI.Theme.PrimaryText,
		},
		Size = UDim2.new(1, 0, 1, 0),
		SizeConstraint = Enum.SizeConstraint.RelativeYY,

		UI.new "UIPadding" {
			PaddingTop = UI.Theme.Padding,
			PaddingBottom = UI.Theme.Padding,
			PaddingLeft = UI.Theme.Padding,
			PaddingRight = UI.Theme.Padding,
		},

		Activated = function()
			if not newBanDialog.Visible._value then
				banTarget(nil)
				banTargetInput.Value("")
				banReason = nil
				banReasonInput.Value("")
				newBanDialog.Title("New Ban")
				UI.Sound.Hover03:Play()
			end
			newBanDialog.Visible(not newBanDialog.Visible._value)
		end,
	}
	UI.new "Tooltip" { Parent = newBanButton, Text = "New Ban", Hovering = newBanButton._hovering }
	UI.edit(newBanButton._instance.UICorner, { CornerRadius = UI.Theme.CornerPadded })

	scroller._input._instance = UI.new "Frame" {
		Parent = scroller,
		Name = "FilterInput",

		BackgroundTransparency = 1,
		Size = function()
			return UDim2.new(0, 0, 0, UI.Theme.FontSize() + UI.Theme.PaddingDouble().Offset)
		end,
		UI.new "UIListLayout" {
			FillDirection = Enum.FillDirection.Horizontal,
			Padding = UI.Theme.PaddingWithStroke,
			SortOrder = Enum.SortOrder.LayoutOrder,
		},
		UI.edit(scroller._input, { UI.new "UIFlexItem" { FlexMode = Enum.UIFlexMode.Fill } }),
		newBanButton,
	}

	return scroller
end

return Bans


local Players = game:GetService("Players")

local Package = script.Parent.Parent.Parent

local Command = require(Package.Shared.Process:WaitForChild("Command"))

local LocalPlayer = Players.LocalPlayer

return function(_K, text, cursorPosition)
	local from = LocalPlayer.UserId
	local data = {
		commands = {},
		message = text,
		cursorPosition = cursorPosition,
		invalid = {
			-- {feedback, argPos, rawArg}
		},
	}

	local prefix = _K.getCommandPrefix(from)

	if text == prefix then
		data.argPos = 2
		data.argIndex = 0
		data.rawArg = ""
		data.query = ""
		data.suggestionType = "History"
		return data
	end

	local rawCommands =
		_K.Process.rawParse(text, { prefix, unpack(_K.Data.settings.prefix) }, _K.Data.settings.splitKey or " ")
	if not rawCommands then
		table.insert(data.invalid, { "Invalid command string", 2, string.sub(text, 2) })
		return data -- invalid command string, show valid example
	end

	for _, commandArray in rawCommands do
		local commandPos, commandText = unpack(commandArray[1])
		local commandAlias = string.lower(commandText)
		local commandDefinition = _K.Registry.commands[commandAlias]

		local lastArgDefinition = commandDefinition and commandDefinition.args[#commandDefinition.args]
		local lastArg = commandArray[#commandArray]
		local commandEnd = lastArg[1] + #lastArg[2]
		local greedy = lastArgDefinition and (lastArgDefinition.type == "string" or lastArgDefinition.type == "strings")

		if cursorPosition < commandArray[1][1] or cursorPosition > commandEnd then --outside current query
			if not commandDefinition and not greedy then
				data.argIndex = 1
				data.argPos = commandPos
				data.rawArg = commandText
				data.query = commandText
				local feedback =
					`Invalid command: {commandText}, did you mean "{_K.Registry.suggestCommandAlias(commandAlias)}"?`
				table.insert(data.invalid, { feedback, commandPos, commandText })
				return data
			elseif commandDefinition then
				local command = Command.new(_K, commandAlias, commandDefinition, commandArray, from, text)
				table.insert(data.commands, command)
				if not commandDefinition.LocalPlayerAuthorized then
					local message = `"{commandText}" command restricted to {commandDefinition.RestrictedToRole.name}`
					table.insert(data.invalid, { message, commandPos, commandText })
					continue
				end
				command:validate()
				table.insert(data.commands, command)
				if command.invalidArg then
					table.insert(
						data.invalid,
						{ command.argMissing or command.invalidMessage, command.invalidPos, command.invalidArg }
					)
					return data
				end
			end
			continue -- cursor not within commandArray
		end

		if cursorPosition <= commandPos + #commandText then -- within command suggestion
			data.argIndex = 1
			data.argPos = commandPos
			data.rawArg = commandText
			data.query = string.sub(commandText, 1, cursorPosition - commandPos)
			data.suggestionType = _K.Registry.types.command
			data.suggestions =
				_K.Util.Suggest.query(data.query, _K.Registry.types.command.suggestions(data.query, from))

			local rawSuggestions = data.suggestions
			data.suggestions = table.create(#data.suggestions)
			local invalidSuggestions = {}
			for _, suggestionData in rawSuggestions do
				local _name, _display, cmd = unpack(suggestionData)
				if cmd then
					if cmd.LocalPlayerAuthorized then
						table.insert(data.suggestions, suggestionData)
						data.validSuggestion = true
					else
						suggestionData[4] = true -- invalid
						table.insert(invalidSuggestions, suggestionData)
					end
				end
			end

			if #invalidSuggestions > 0 then
				table.move(invalidSuggestions, 1, #invalidSuggestions, #data.suggestions + 1, data.suggestions)
			end

			data.commandDefinition = commandDefinition or data.suggestions[1][3]

			if commandDefinition then
				if commandDefinition.LocalPlayerAuthorized then
					local command = Command.new(_K, commandAlias, commandDefinition, commandArray, from, text)
					command:validate()
					table.insert(data.commands, command)
				elseif
					#data.suggestions == 0
					or not string.find(string.lower(data.suggestions[1][1]), string.lower(data.query), 1, true)
				then
					local message = `"{commandText}" command restricted to {commandDefinition.RestrictedToRole.name}`
					table.insert(data.invalid, { message, commandPos, commandText })
				end
			end

			return data
		end

		if not commandDefinition then
			local feedback =
				`Invalid command: {commandText}, did you mean "{_K.Registry.suggestCommandAlias(commandAlias)}"?`
			table.insert(data.invalid, { feedback, commandPos, commandText })
			return data
		elseif not commandDefinition.LocalPlayerAuthorized then
			local message = `"{commandText}" command restricted to {commandDefinition.RestrictedToRole.name}`
			table.insert(data.invalid, { message, commandPos, commandText })
			return data
		end

		data.commandDefinition = commandDefinition

		local command = Command.new(_K, commandAlias, commandDefinition, commandArray, from, text)
		local ok, result = command:validate()
		table.insert(data.commands, command)

		-- argument suggestions
		for i, arg in command.args do
			local isGreedy = greedy and i == #commandDefinition.args
			if cursorPosition < arg.argPos or (not isGreedy and cursorPosition > arg.argPos + #arg.rawArg) then
				continue
			end
			data.arg = arg
			data.argIndex = i + 1
			data.argPos = arg.argPos
			data.rawArg = arg.rawArg
			data.rawQuery = string.sub(arg.rawArg, 1, cursorPosition - arg.argPos)
			data.query = _K.Util.String.stripQuotes(data.rawQuery)
			data.argDefinition = arg.definition
			data.suggestionType = arg.rawType

			if
				arg.skipped
				and cursorPosition >= arg.argPos
				and (isGreedy or cursorPosition < arg.argPos + #arg.rawArg)
			then
				data.skipped = true
				data.rawQuery = string.sub(arg.skipped, 1, cursorPosition - arg.argPos)
				data.query = _K.Util.String.stripQuotes(data.rawQuery)
				local suggestions, values = arg.rawType.suggestions, nil
				if type(suggestions) == "function" then
					suggestions, values = arg.rawType.suggestions(data.query, from, arg.definition)
				end
				data.suggestions = suggestions and _K.Util.Suggest.query(data.query, suggestions, values)
				if data.suggestions and #data.suggestions > 0 then
					break
				end
			end

			local transformPos = arg.argPos
			for k, rawArg in arg.rawArgs do
				local transformedArg = arg.transformedArgs[k]
				local transformedType = arg.transformedTypes[k]
				local pos = transformPos
				transformPos += #rawArg + 1

				if cursorPosition < pos or (not isGreedy and cursorPosition > transformPos) then
					continue
				end

				data.argPos = if arg.prefix then pos + #arg.prefix else pos
				data.rawArg = rawArg
				data.rawQuery = string.sub(rawArg, 1, cursorPosition - pos)
				data.query = _K.Util.String.stripQuotes(data.rawQuery)
				data.transformedArg = transformedArg
				data.suggestionType = transformedType or data.suggestionType
				local suggestions, values = transformedType and transformedType.suggestions, nil
				if type(suggestions) == "function" then
					suggestions, values = transformedType.suggestions(data.query, from, arg.definition)
				end
				data.suggestions = suggestions and _K.Util.Suggest.query(data.query, suggestions, values)
			end
		end

		if
			not ok
			and not (
				data.suggestionType
				and (
					data.rawArg == ""
					or (
						data.suggestions
						and #data.suggestions > 1
						and if type(data.suggestions[1]) == "table"
							then data.suggestions[1][1] ~= data.rawArg
							else data.suggestions[1] ~= data.rawArg
					)
				)
			)
		then
			command.Error = result
			if command.invalidArg then
				if not command.argMissing then
					table.insert(data.invalid, { result, command.invalidPos, command.invalidArg })
				end
			else
				table.insert(data.invalid, { result, commandPos, string.sub(text, commandPos, commandEnd) })
			end
			return data
		end
	end

	return data
end


local UI = require(script.Parent:WaitForChild("UI"))

local Commands = {}
Commands.__index = Commands

local filterRichFormat = `<font transparency="0.5">%s</font><b>%s</b><font transparency="0.5">%s</font>`

local function _argsString(command)
	if not command.args or #command.args == 0 then
		return ""
	end
	local argTypes = {}
	for _, arg in command.args do
		table.insert(argTypes, arg.type)
	end
	return " <" .. table.concat(argTypes, "> <") .. ">"
end

function Commands.new(_K)
	local escape = _K.Util.String.escapeRichText

	local function createItem(self, command)
		local itemRef = {
			command = UI.state(command),
		}

		local textColor = function()
			return if itemRef.command().LocalPlayerAuthorized
				then UI.Theme.PrimaryText()
				else UI.Theme.PrimaryText():Lerp(UI.Theme.Primary(), 0.5)
		end

		local tooltip = UI.new "Tooltip" {
			Text = "Loading...",
		}

		local cleanup = { tooltip }

		itemRef._instance = UI.new "TextLabel" {
			AutoLocalize = false,
			BackgroundTransparency = 1,
			Size = self.ItemSize,
			RichText = true,
			FontFace = UI.Theme.Font,
			TextColor3 = textColor,
			TextSize = UI.Theme.FontSize,
			TextStrokeColor3 = UI.Theme.Primary,
			TextStrokeTransparency = UI.Theme.TextStrokeTransparency,
			TextTruncate = Enum.TextTruncate.SplitWord,
			TextXAlignment = Enum.TextXAlignment.Left,
			TextYAlignment = Enum.TextYAlignment.Top,

			UI.new "TextLabel" {
				AutoLocalize = false,
				BackgroundTransparency = 1,
				Name = "Description",
				RichText = true,
				FontFace = UI.Theme.Font,
				TextColor3 = textColor,
				TextSize = UI.Theme.FontSize,
				TextStrokeColor3 = UI.Theme.Primary,
				TextStrokeTransparency = UI.Theme.TextStrokeTransparency,
				TextTruncate = Enum.TextTruncate.SplitWord,
				TextXAlignment = Enum.TextXAlignment.Left,
				TextYAlignment = Enum.TextYAlignment.Top,
				Size = function()
					local size = UI.Theme.FontSize()
					return UDim2.new(1, -size, 0, size)
				end,
				Position = function()
					local size = UI.Theme.FontSize()
					return UDim2.new(0, size, 0, size)
				end,
			},
			tooltip,

			[UI.Clean] = cleanup,
		}

		table.insert(
			cleanup,
			tooltip.Visible:Connect(function()
				if tooltip.Visible._value then
					tooltip.Text(itemRef.command._value.tipText)
				end
			end)
		)

		return itemRef
	end

	local function renderItem(self, itemData, command)
		if command.renderText then
			local item = itemData._instance
			item.Text = command.renderText
			item.Description.Text = command.renderDesc
			itemData.command(command)
		end
	end

	local purchase = { gamepass = nil, role = nil }
	local purchaseData = {}
	local function updatePurchaseData(list)
		table.clear(purchaseData)
		local current
		for index, command in list do
			if not command.RestrictedToRole then
				for rank, role in _K.Data.rolesList do
					if _K.Auth.roleCanUseCommand(role._key, command) then
						command.RestrictedToRole = role
						break
					end
				end
			end
			if command.LocalPlayerAuthorized then
				continue
			end
			local role = command.RestrictedToRole
			if current then
				if role == current then
					purchaseData[current].lastIndex = index
				end
			end
			if not current and role and (role._key == "vip" or role.gamepasses) then
				current = role
				purchaseData[role] = {
					firstIndex = index - 1,
					lastIndex = index,
				}
			end
		end
	end

	-- TODO: update text/price based on current view, find bounds of the canvas position items

	local purchaseButton = UI.new "Button" {
		Label = "Loading...",
		LayoutOrder = 9,
		Icon = "rbxasset://textures/ui/common/robux.png",
		IconProperties = { ImageColor3 = UI.Theme.PrimaryText },
		IconRightAlign = true,
		Padding = UI.Theme.PaddingDouble,
		TextSize = UI.Theme.FontSizeLarge,
		TextXAlignment = Enum.TextXAlignment.Left,
		Size = function()
			return UDim2.new(1, 0, 0, UI.Theme.FontSizeLarge() + UI.Theme.PaddingDouble().Offset)
		end,
		Visible = false,

		UI.new "TextLabel" {
			Name = "Price",
			LayoutOrder = 8,
			AutoLocalize = false,
			AutomaticSize = Enum.AutomaticSize.XY,
			BackgroundTransparency = 1,
			FontFace = UI.Theme.FontMono,
			TextSize = UI.Theme.FontSizeLarge,
			TextColor3 = UI.Theme.PrimaryText,
			TextStrokeColor3 = UI.Theme.Primary,
			TextStrokeTransparency = UI.Theme.TextStrokeTransparency,
			TextWrapped = true,
			Size = function()
				return UDim2.new(0, UI.Theme.FontSizeLarge(), 1, 0)
			end,
			Text = `<b>?</b>`,
			RichText = true,
		},

		Activated = function()
			if purchase.role == "vip" and _K.Data.settings.vip then
				_K.PromptPurchaseVIP()
			elseif purchase.gamepass then
				_K.Service.Marketplace:PromptGamePassPurchase(_K.LocalPlayer, purchase.gamepass)
			end
		end,
	}

	local function updatePurchasePrice()
		local ok, result = pcall(function()
			local productType = if purchase.role == "vip" and _K.Data.settings.vip
				then Enum.InfoType.Asset
				else Enum.InfoType.GamePass
			return _K.Service.Marketplace:GetProductInfoAsync(purchase.gamepass, productType).PriceInRobux
		end)
		purchaseButton._content.Price.Text = `<b>{ok and result or "?"}</b>`
	end

	purchaseButton._content.Price.Text = `<b>799</b>`

	local function sortCommandsList(a, b)
		local rankA, rankB = math.huge, math.huge
		for rank, role in _K.Data.rolesList do
			if rankA > rank and _K.Auth.roleCanUseCommand(role._key, a) then
				rankA = rank
			end
			if rankB > rank and _K.Auth.roleCanUseCommand(role._key, b) then
				rankB = rank
			end
		end
		if rankA == rankB then
			return a.name < b.name
		else
			return rankA < rankB
		end
	end

	local commandsList = UI.state(_K.Registry.commandsList)
	local function updateCommandsList()
		local list
		if _K.client.settings.onlyShowUsableCommands._value then
			list = {}
			for _, command in _K.Registry.commandsList do
				local added
				for role, roleData in _K.Data.roles do
					if
						(
							(roleData.assets and next(roleData.assets))
							or (roleData.gamepasses and next(roleData.gamepasses))
							or (roleData.subscriptions and next(roleData.subscriptions))
							or (role == "vip" and _K.Data.settings.vip)
						) and _K.Auth.roleCanUseCommand(role, command)
					then
						table.insert(list, command)
						added = true
						break
					end
				end
				if not added and _K.Auth.hasCommand(_K.LocalPlayer.UserId, command) then
					table.insert(list, command)
				end
			end
		else
			list = _K.Registry.commandsList
		end
		table.sort(list, sortCommandsList)
		commandsList(list)
		updatePurchaseData(list)
	end

	local debounceUpdateCommandsList = _K.Util.Function.debounce(0.2, updateCommandsList)
	_K.client.settings.onlyShowUsableCommands:Connect(debounceUpdateCommandsList)
	_K.Hook.authChanged:Connect(debounceUpdateCommandsList)
	debounceUpdateCommandsList()

	local scroller = UI.new "ScrollerFast" {
		Name = "Commands",
		List = commandsList,
		Enabled = false,
		FilterInput = true,
		Visible = false,
		ItemSize = function()
			return UDim2.new(1, 0, 0, UI.Theme.FontSize * 2 + UI.Theme.Padding().Offset)
		end,
		CreateItem = createItem,
		RenderItem = renderItem,
		purchaseButton,
	}

	local function updatePurchaseButton()
		local itemHeight = UI.Theme.FontSize._value * 2
		local windowHeight = scroller._scroller.AbsoluteWindowSize.Y
		local top = scroller._scroller.CanvasPosition.Y
		local middle = top + windowHeight / 2
		local bottom = top + windowHeight

		local role
		for roleData, data in purchaseData do
			local first = data.firstIndex * itemHeight
			local last = data.lastIndex * itemHeight
			if first < bottom and last > middle then
				role = roleData
			end
		end

		if role and ((role._key == "vip" and _K.Data.settings.vip) or role.gamepasses) then
			purchase.role = role._key
			purchase.gamepass = if role._key == "vip" and _K.Data.settings.vip
				then 110019084552349
				else role.gamepasses[1]
			if not purchase.gamepass then
				return
			end
			purchaseButton.Label(`<b>Unlock <font color="{role.color}">{role.name or role._key}</font> Commands</b>`)
			purchaseButton._instance.Visible = true
			task.spawn(updatePurchasePrice)
			return
		end

		purchaseButton._instance.Visible = false
	end

	_K.Hook.authChanged:Connect(updatePurchaseButton)
	UI.Theme.FontSize:Connect(updatePurchaseButton)
	scroller._scroller:GetPropertyChangedSignal("CanvasPosition"):Connect(updatePurchaseButton)

	local function updateRenderParameters(command)
		if not command.aliasText then
			command.aliasText = if command.aliases and #command.aliases ~= 0
				then table.concat(command.aliases, ", ")
				else ""

			command.argNames = table.create(#command.args)
			command.argTooltip = table.create(#command.args)

			if #command.args > 0 then
				table.insert(command.argTooltip, "")
			end

			for _, arg in command.args do
				table.insert(command.argNames, if arg.optional then arg.name .. "?" else arg.name)
				table.insert(
					command.argTooltip,
					`\n<sc>{arg.name}</sc> <{arg.type}{if arg.optional
						then `?`
						else ""}>\n{arg.description or "No description."}`
				)
				if arg.ignoreSelf then
					table.insert(command.argTooltip, `\t<font transparency="0.5"><b>ignoreSelf:</b> true</font>`)
				end
				if arg.lowerRank then
					table.insert(command.argTooltip, `\t<font transparency="0.5"><b>lowerRank:</b> true</font>`)
				end
				if arg.shouldRequest and _K.Data.settings.commandRequests ~= false then
					table.insert(command.argTooltip, `\t<font transparency="0.5"><b>shouldRequest:</b> true</font>`)
				end
			end

			command.argNames = table.concat(command.argNames, " ")
			command.argTooltip = table.concat(command.argTooltip, "\n")

			local roles = {}
			for role, _roleData in _K.Data.roles do
				if _K.Auth.roleCanUseCommand(role, command) then
					table.insert(roles, role)
				end
			end

			table.sort(roles, function(a, b)
				return _K.Data.roles[a]._rank < _K.Data.roles[b]._rank
			end)

			-- non-breaking spaces
			local narrowSpace = " "
			local thinSpace = " "

			local roleColor = if #roles > 0 then _K.Data.roles[roles[1]].color else UI.Theme.Primary._value:ToHex()
			local roleCircle = `<font color="{roleColor}">●</font>{narrowSpace}`
			command.roleCircle = roleCircle

			for i, roleId in roles do
				local role = _K.Data.roles[roleId]
				roles[i] = `<font color="{role.color}">●</font>{narrowSpace}{string.gsub(role.name, " ", thinSpace)}`
			end

			command.roleText = table.concat(roles, ", ")

			local credit = command.credit or "Kohl @Scripth"
			local description = command.description or "No description."

			command.creditText = if type(credit) == "table" then table.concat(credit, ", ") else credit
			command.tipText =
				`<b>Restricted to:</b> {command.roleText}{command.argTooltip}\n\n<font transparency="0.5"><b>Credit:</b> {command.creditText}</font>`

			command.rawTextNoAlias = `{_K.getCommandPrefix()}{command.name} {command.argNames}`
			command.rawText = `{command.rawTextNoAlias} {command.aliasText}`

			command.richText = `{roleCircle}{escape(command.rawTextNoAlias)} {escape(
				command.aliasText
			)}`
			command.richDesc = `<font transparency="0.33">{description}</font>`

			command.filterTest = string.lower(`{command.rawText} {description}`)
			command.filterText = string.lower(command.rawText)
			command.filterDesc = string.lower(description)
		end

		command.colorAuth = if command.LocalPlayerAuthorized
			then UI.Theme.PrimaryText._value
			else UI.Theme.PrimaryText._value:Lerp(UI.Theme.Primary._value, 0.5)
	end

	local function filterTest(self, list)
		local filter = string.lower(scroller._input._input.Text)
		self._filter = filter

		local noFilter = string.find(self._filter, "^%s*$")

		local new = {}

		if string.find(filter, "role=", 1, true) == 1 then
			filter = string.sub(filter, 6)
			for _, command in list do
				updateRenderParameters(command)
				if string.find(string.lower(command.roleText), filter, 1, true) then
					command.renderText = command.richText
					command.renderDesc = command.richDesc
					table.insert(new, command)
				end
			end

			return new
		end

		for _, command in list do
			updateRenderParameters(command)

			if noFilter then
				command.renderText = command.richText
				command.renderDesc = command.richDesc
				table.insert(new, command)
			else
				if string.find(command.filterTest, filter, 1, true) then
					local filterFound = string.find(command.filterText, self._filter, 1, true)
					command.renderText = if filterFound
						then command.roleCircle .. string.format(
							filterRichFormat,
							escape(string.sub(command.rawText, 1, filterFound - 1)),
							escape(string.sub(command.rawText, filterFound, filterFound + #self._filter - 1)),
							escape(string.sub(command.rawText, filterFound + #self._filter))
						)
						else `{command.roleCircle}<font transparency="0.5">{command.rawText}</font>`

					local description = command.description or "No description."
					local descFound = string.find(command.filterDesc, self._filter, 1, true)
					command.renderDesc = if descFound
						then string.format(
							filterRichFormat,
							escape(string.sub(description, 1, descFound - 1)),
							escape(string.sub(description, descFound, descFound + #self._filter - 1)),
							escape(string.sub(description, descFound + #self._filter))
						)
						else `<font transparency="0.5">{description}</font>`

					table.insert(new, command)
				end
			end
		end

		return new
	end

	task.defer(function()
		scroller:filter(filterTest)
	end)

	return scroller
end

return Commands


return {
	{
		Title = "Contributors",
		Users = {
			{ Id = 44391621, Tooltip = "Project Founder & Architect. Driving the vision forward. 👑" }, -- kohl
			{ Id = 15554260, Tooltip = "Web Development lead and design infrastructure. 🌐" }, -- zeejaym
			{ Id = 3286467, Tooltip = "Critical input on design and system planning. 🛠" }, -- lejam7890
			{ Id = 521855368, Tooltip = "Strategist and Designer. Expert advice and planning. 💡" }, -- rfov

			-- major contributors
			{ Id = 1022854840, Tooltip = "3D Modeling and UGC Design specialist. 🎨" }, -- JJJ
			{ Id = 15303771, Tooltip = "Design planning and Quality Assurance lead. ✅" }, -- Fragility
			{ Id = 131233603, Tooltip = "Design, direction, and leading the community. 😎" }, -- Geed
			{ Id = 201989656, Tooltip = "Committed to finding solutions! 💡" }, -- iiRealistic_Dev
			{ Id = 14658110, Tooltip = "Transforming chaos into clarity! 💫" }, -- Rietria
			{ Id = 448137594, Tooltip = "Serving up great ideas! 🌮" }, -- Lucatacos

			-- support team
			{ Id = 2773879457, Tooltip = "Making your issues disappear! 🪄" }, -- therealfaylix
			{ Id = 1224156917, Tooltip = "Always ready to help! 🤝" }, -- dalien
			{ Id = 195261420, Tooltip = "Ensuring user satisfaction! 😊" }, -- exobyte
			{ Id = 208790398, Tooltip = "Here for your questions! ❓" }, -- chan
			{ Id = 3357116228, Tooltip = "Offering valuable support! 🌟" }, -- lemon
			{ Id = 1686847974, Tooltip = "Helping the community thrive! 🌱" }, -- stone
			{ Id = 3470429081, Tooltip = "Guiding users with expertise! 🧭" }, -- Mercury
			{ Id = 1142396048, Tooltip = "A pillar of support! 🏛" }, -- Franklin_Surten (H)
			{ Id = 483215789, Tooltip = "Ensuring smooth experiences! ⚙" }, -- seil
			{ Id = 345072461, Tooltip = "Assisting with dedication! 🎖" }, -- raenlua
			{ Id = 130170862, Tooltip = "Consistently improving! 💪" }, -- matsagor1
			{ Id = 77608258, Tooltip = "Soaring to provide support! 🕊" }, -- icarus
			{ Id = 444317184, Tooltip = "Providing excellent assistance! 🛡" }, -- vonials

			-- contributors
			{ Id = 41336494, Tooltip = "Calculating improvements! 🧮" }, -- Calculatous
			{ Id = 35818691, Tooltip = "Growing the project! 🌱" }, -- greeny
			{ Id = 83478451, Tooltip = "Swimming in good ideas! 🐠" }, -- stevendahfish
			{ Id = 2014793803, Tooltip = "Building with Lua! 💀" }, -- SkellyLua
			{ Id = 1219753275, Tooltip = "Adding awesome features! 🚀" }, -- DZIDZIO999
			{ Id = 7034077053, Tooltip = "Shredding with numbers! 🎸" }, -- johnny_klebitz1
			{ Id = 127757866, Tooltip = "Coding with flair! 💻" }, -- iam_danny
			{ Id = 190022817, Tooltip = "Blazing through contributions! 🔥" }, -- emblazes
			{ Id = 197640401, Tooltip = "I love animation! 🎬" }, -- swibes
		},
	},
	{
		Title = "Testers",
		Users = {
			{ Id = 75214485, Tooltip = "Squashing bugs! 🐞" }, -- ericance
			{ Id = 1560379748, Tooltip = "Ensuring quality! 💯" }, -- mosha
			{ Id = 200565345, Tooltip = "Finding those tricky issues! 🕵" }, -- easternbloxxer
			{ Id = 107192912, Tooltip = "Keeping things on track! 🚂" }, -- britishtrainspotting
			{ Id = 2258246, Tooltip = "Powering through tests! ⚡" }, -- emp
			{ Id = 129134882, Tooltip = "Purrfecting the experience! 🐱" }, -- horracecat
			{ Id = 146573772, Tooltip = "Polishing to a shine! ✨" }, -- metallochrome
			{ Id = 496062049, Tooltip = "Whacking those bugs! 🔨" }, -- pachiswack
			{ Id = 1279022258, Tooltip = "Uncovering hidden flaws! 🔮" }, -- arcane
			{ Id = 2297254, Tooltip = "Keeping it minimal and bug-free! 👌" }, -- min
			{ Id = 672360774, Tooltip = "X-raying for errors! 🧐" }, -- xaz
			{ Id = 9345226, Tooltip = "Casting spells to find bugs! 🪄" }, -- tomspell
			{ Id = 3230955923, Tooltip = "Napping on bugs (after finding them)! 😴" }, -- napiw
			{ Id = 516417505, Tooltip = "Landing on solutions! 🛬" }, -- landon
			{ Id = 105503420, Tooltip = "A legendary bug hunter! 🏆" }, -- general_legendary
			{ Id = 4847574, Tooltip = "Providing valuable insights! 🔎" }, -- mets
		},
	},
	{
		Title = "Inspiration",
		Users = {
			{
				Id = 1237666, -- Sceleratis
				Tooltip = "Pioneered influential forks; inspired by Adonis & EISS. 🍴",
			},
			{
				Id = 214258, -- Person299
				Tooltip = "The original spark. Creator of Person299's Admin Commands. ✨",
			},
			{
				Id = 82347291, -- ForeverHD
				Tooltip = "Creator of TopbarPlus. Inspiration via HD Admin. 👈",
			},
			{
				Id = 576473, -- lpg
				Tooltip = "Master of Rojo. A crucial tool for modern development. 👑",
			},
			{
				Id = 92658764, -- evaera
				Tooltip = "Creator of Moonwave and Cmdr. 🌙",
			},
			{
				Id = 484140, -- GigsD4x
				Tooltip = "Creator of the indispensable Building Tools by F3X. 🔨",
			},
			{
				Id = 2722909519, -- sona
				Tooltip = "Innovator of Squash. Inspiration via data-oriented magic. 🪄",
			},
			{
				Id = 4812582, -- Sharksie
				Tooltip = "Designer of the iconic Classic Sword. 🗡",
			},
			{
				Id = 648020, -- trick555
				Tooltip = "Introduced many to the world of Roblox group development. 👥",
			},
			{
				Id = 1670764, -- Elttob
				Tooltip = "Inspired Flux development via the Fusion UI framework. 🔥",
			},
		},
	},
	{
		Title = "Special Thanks",
		Users = {
			{
				Id = 70540486, -- reru
				Tooltip = "Lord of the bytes - a master of efficiency and optimization! 🤖",
			},
			{
				Id = 46236725, -- Doroku
				Tooltip = "Always there to lend support (and a comfy chair)! 🪑",
			},
			{
				Id = 16, -- cassel
				Tooltip = "For inspiring a generation of creators and players. ❤",
			},
		},
	},
}


local _K = require(script.Parent.Parent)
local UI = require(script.Parent:WaitForChild("UI"))

local About = require(script.Parent:WaitForChild("About"))
local Bans = require(script.Parent:WaitForChild("Bans"))
local Commands = require(script.Parent:WaitForChild("Commands"))
local Logs = require(script.Parent:WaitForChild("Logs"))
local Members = require(script.Parent:WaitForChild("Members"))
local Servers = require(script.Parent:WaitForChild("Servers"))
local Settings = require(script.Parent:WaitForChild("Settings"))
local Market = require(script.Parent:WaitForChild("Market"))

local clockStart = os.clock()

_K.client.dashboard = {}
_K.client.dashboard.About = About.new(_K)
_K.client.dashboard.Bans = Bans.new(_K)
_K.client.dashboard.Commands = Commands.new(_K)
_K.client.dashboard.Members = Members.new(_K)
_K.client.dashboard.Servers = Servers.new(_K)
_K.client.dashboard.Logs = Logs.new(_K)
_K.client.dashboard.Settings = Settings.new(_K)
_K.client.dashboard.Market = Market.new(_K)

local tabs = UI.new "Tabs" {
	CurrentPage = _K.client.dashboard.About,
	Tabs = {
		{ _K.client.dashboard.Commands },
		{ _K.client.dashboard.Bans },
		{ _K.client.dashboard.Members },
		{ _K.client.dashboard.Servers },
		{ _K.client.dashboard.Logs },
		{ _K.client.dashboard.Settings },
		{ _K.client.dashboard.About },
		{ _K.client.dashboard.Market },
	},
	Vertical = true,

	[UI.Event] = {
		AbsoluteSize = function()
			local size = _K.client.dashboard.Window._instance.AbsoluteSize
			_K.client.dashboard.Tabs.Vertical(size.x > size.y)
		end,
	},
}
_K.client.dashboard.Tabs = tabs

local notPaddedWhenVertical = UI.compute(function()
	return if tabs.Vertical() then UDim.new() else UI.Theme.Padding
end)

local paddedWhenVertical = UI.compute(function()
	return if tabs.Vertical() then UI.Theme.Padding else UDim.new()
end)

local paddedWhenVerticalWithStroke = UI.compute(function()
	return if tabs.Vertical() then UI.Theme.PaddingWithStroke else UI.Theme.PaddingStroke
end)

UI.edit(tabs._content, {
	BackgroundColor3 = UI.Theme.Secondary,
	BackgroundTransparency = function()
		return if tabs.Vertical() then UI.Theme.TransparencyMax else 1
	end,
	UI.new "UICorner" { CornerRadius = UI.Theme.CornerPadded },
	UI.new "UIPadding" {
		PaddingRight = paddedWhenVertical,
		PaddingLeft = paddedWhenVertical,
	},
})

UI.new "UIPadding" {
	Parent = tabs._instance,
	PaddingRight = paddedWhenVertical,
}

local dashboard = UI.new "Window" {
	Parent = UI.LayerTopInset,
	Icon = "rbxassetid://71961243872230",
	IconProperties = {
		Size = function()
			return UDim2.fromOffset(UI.Theme.FontSize(), UI.Theme.FontSize())
		end,
		UI.new "Tooltip" {
			Text = "Kohl's Admin",
		},
	},
	Position = UDim2.new(0.5, -320, 0.5, -240),
	Size = UDim2.fromOffset(648, 492),
	SizeBounds = Rect.new(128, 128, 9e9, 9e9),
	Title = "",
	Visible = false,
	tabs,
}
_K.client.dashboard.Window = dashboard
dashboard._exitButton.ActiveSound(false)
dashboard._content.UIPadding:Destroy()

task.spawn(function()
	repeat
		task.wait(1)
		dashboard.Title(DateTime.now():FormatLocalTime("LT", "en-us"))
	until nil
end)

local titleBar = dashboard._instance.Frame.TitleBar

UI.edit(titleBar.UIPadding, {
	PaddingLeft = UI.Theme.PaddingDouble,
})

UI.edit(titleBar.Left.Title, {
	TextSize = UI.Theme.FontSize,
	TextTransparency = 0.5,
})

local timeTooltip = UI.new "Tooltip" {
	Parent = titleBar.Left.Title,
}
task.spawn(function()
	repeat
		task.wait(1)
		timeTooltip.Text(DateTime.now():FormatLocalTime("dddd, LL", UI.LocalPlayer.LocaleId))
	until nil
end)

local parented = {}
for _, page in
	{
		_K.client.dashboard.Bans,
		_K.client.dashboard.Commands,
		_K.client.dashboard.Logs,
		_K.client.dashboard.Members,
		_K.client.dashboard.Settings,
	}
do
	local instance = if type(page) == "table" then page._instance else page
	local moveToTitleBar = UI.state(false)

	local function update()
		local parent = instance.Parent
		local move = parent
			and parent:IsDescendantOf(dashboard._instance)
			and tabs._pages.CurrentPage == parent
			and tabs.Vertical()

		moveToTitleBar(move, true)
	end

	instance:GetPropertyChangedSignal("Parent"):Connect(update)
	tabs._pages:GetPropertyChangedSignal("CurrentPage"):Connect(function()
		local page = tabs._pageCache[tabs._pages.CurrentPage]
		if page == instance then
			update()
		end
	end)
	tabs.Vertical:Connect(update)
	if tabs._pageCache[tabs._pages.CurrentPage] == instance then
		update()
	end

	UI.new "UIFlexItem" {
		Parent = page._input._instance,
		FlexMode = function()
			return if moveToTitleBar() then Enum.UIFlexMode.Fill else Enum.UIFlexMode.None
		end,
	}
	UI.edit(page._input, {
		Size = function()
			local y = if moveToTitleBar() then 0 else 1
			return UDim2.new(y, 0, 0, UI.Theme.FontSize() + UI.Theme.Padding().Offset * 2)
		end,
		LayoutOrder = function()
			return if moveToTitleBar() then 4 else -1
		end,
		Parent = function()
			local move = moveToTitleBar()
			parented[page] = if move then true else nil
			local spacer = titleBar:FindFirstChild("Spacer")
			if spacer then
				spacer.Visible = next(parented) == nil
			end
			return if move then titleBar else instance
		end,
	})
	UI.edit(page._scroller.UIPadding, {
		PaddingTop = paddedWhenVerticalWithStroke,
		PaddingBottom = paddedWhenVerticalWithStroke,
	})
end

UI.edit(_K.client.dashboard.Logs._scroller.UIPadding, {
	PaddingTop = UDim.new(),
})

UI.edit(tabs._instance.Bar.UIPadding, {
	PaddingBottom = notPaddedWhenVertical,
})

UI.edit(tabs._instance.Bar.Tabs, {
	UI.new "Spacer" {
		LayoutOrder = 9e8 - 1,
		Visible = tabs.Vertical,
	},
})

UI.edit(tabs._instance.Bar.Tabs.Market, {
	LayoutOrder = 9e8,
})

UI.edit(tabs._instance.Bar.Tabs.Market.UIContent.Label, {
	Name = "Title",
	UI.new "UIGradient" {},
})

tabs._instance.Bar.Tabs.Market.UIContent:AddTag("_Keternal")

local tabBarSize = UI.state(tabs._instance.Bar, "AbsoluteSize")

UI.edit(titleBar.Left, {
	Size = function()
		return if tabs.Vertical()
			then UDim2.fromOffset(tabBarSize().X - UI.Theme.Padding().Offset * 3, 0)
			else UDim2.new()
	end,
})

local statusLabelPosition = UI.compute(function()
	return UDim2.fromOffset(UI.Theme.FontSize() + UI.Theme.PaddingHalf().Offset, 0)
end)

local function statusIconLabel(name, imageDefinition, labelDefinition, tooltipDefinition)
	return UI.new "Frame" {
		AutomaticSize = Enum.AutomaticSize.X,
		BackgroundTransparency = 1,
		Name = name,
		Size = UDim2.new(0, 0, 1, 0),
		UI.edit(
			UI.new "ImageLabel" {
				BackgroundTransparency = 1,
				Image = UI.Theme.Image.Signal,
				ImageColor3 = UI.Theme.PrimaryText,
				ImageTransparency = 0.5,
				Size = UDim2.new(1, 0, 1, 0),
				SizeConstraint = Enum.SizeConstraint.RelativeYY,
			},
			imageDefinition or {}
		),
		UI.edit(
			UI.new "TextLabel" {
				Name = "Label",
				AutomaticSize = Enum.AutomaticSize.X,
				BackgroundTransparency = 1,
				Position = statusLabelPosition,
				Size = UDim2.fromScale(0, 1),
				FontFace = UI.Theme.FontMono,
				Text = "?",
				TextColor3 = UI.Theme.PrimaryText,
				TextSize = UI.Theme.FontSize,
				TextTransparency = 0.5,
			},
			labelDefinition or {}
		),
		UI.edit(UI.new "Tooltip" { FontFace = UI.Theme.FontMono, Text = name }, tooltipDefinition or {}),
	}
end

local function getAvgLow(buffer, avg, low, sum, nextAvgUpdate)
	local now = tick()
	if now > nextAvgUpdate then
		nextAvgUpdate = now + 2
		table.sort(buffer)
		avg, low = math.round(sum / #buffer) * 2, 0
		local lowCount = math.ceil(#buffer / 100)
		for i = 1, lowCount do
			low += buffer[i]
		end
		low //= lowCount
		table.clear(buffer)
	end
	return avg, low, 0, nextAvgUpdate
end

local lerpGreenRed
do
	local hueGreen = UI.compute(function()
		return UI.Color.Oklab.toHCL(UI.Color.Oklab.fromLinear(UI.Theme.Valid())).X
	end)
	local hueRed = UI.compute(function()
		return UI.Color.Oklab.toHCL(UI.Color.Oklab.fromLinear(UI.Theme.Invalid())).X
	end)
	lerpGreenRed = function(alpha: number)
		local hcl = Vector3.new(math.lerp(hueGreen() :: number, hueRed() :: number, alpha), 1, 1)
		return UI.Color.Oklab.toLinear(UI.Color.Oklab.fromHCL(hcl))
	end
end

UI.new "Frame" {
	Name = "StatusBar",
	Parent = dashboard._instance.Frame,
	LayoutOrder = 2 ^ 31 - 1,
	AnchorPoint = Vector2.new(0, 1),
	BackgroundTransparency = 1,
	ClipsDescendants = true,
	Position = UDim2.fromScale(0, 1),
	Size = function()
		return UDim2.new(1, 0, 0, UI.Theme.FontSizeSmall + UI.Theme.PaddingDouble().Offset)
	end,

	UI.new "Frame" {
		Name = "Content",
		BackgroundTransparency = 1,
		Size = UDim2.fromScale(1, 1),
		UI.new "UIListLayout" {
			SortOrder = Enum.SortOrder.LayoutOrder,
			FillDirection = Enum.FillDirection.Horizontal,
			VerticalAlignment = Enum.VerticalAlignment.Center,
			Padding = UI.Theme.PaddingDouble,
		},
		UI.new "UIPadding" {
			PaddingLeft = UI.Theme.Padding,
			PaddingRight = UI.Theme.Padding,
			PaddingTop = UI.Theme.Padding,
			PaddingBottom = UI.Theme.Padding,
		},
		statusIconLabel("Ping", {
			Image = UI.Theme.Image.SignalSheet,
			ImageRectSize = Vector2.new(86, 86),
		}, {
			UI.action(function(instance)
				local ping, max, avg, nFrames, sum = 0, 0, 0, 0, 0
				local nextAvgUpdate = tick()
				_K.Service.Run.Heartbeat:Connect(function()
					ping = math.round(_K.Service.Players.LocalPlayer:GetNetworkPing() * 1000)
					max = math.min(math.max(max, ping), 30000)
					nFrames += 1
					sum += ping
				end)
				repeat
					task.wait(1)
					if tick() > nextAvgUpdate then
						nextAvgUpdate = tick() + 2
						avg = sum // nFrames
						sum, nFrames = 0, 0
					end

					local alpha = math.clamp(ping / 350, 0, 1)
					local color = lerpGreenRed(alpha)
					instance.Parent.ImageLabel.ImageRectOffset =
						Vector2.new(math.round((1 - alpha) * 4) * 90 + math.ceil(alpha), 0)
					instance.Parent.ImageLabel.ImageColor3 = color
					instance.TextColor3 = color
					instance.Text = `{avg}ms`

					local tooltip = UI.Class.Tooltip.Cache[instance.Parent]
					if tooltip then
						tooltip.Text(`Ping\n\n<b>Avg:</b>     {avg}ms\n<b>Max:</b>     {max}ms`)
					end
				until nil
			end),
		}),
		statusIconLabel("FPS", {
			Image = UI.Theme.Image.Pace,
		}, {
			UI.action(function(instance)
				local fps, max, avg, low, sum = 0, 0, 0, 0, 0
				local buffer, nextAvgUpdate = {}, tick()
				_K.Service.Run.Heartbeat:Connect(function()
					fps = math.round(1 / _K.Service.Stats.FrameTime)
					max = math.clamp(fps, max, 240)
					sum += fps
					table.insert(buffer, fps)
				end)
				repeat
					task.wait(1)
					avg, low, sum, nextAvgUpdate = getAvgLow(buffer, avg, low, sum, nextAvgUpdate)
					avg = math.clamp(avg, 0, 240)

					local alpha = math.clamp(fps / max, 0, 1)
					local color = lerpGreenRed(1 - alpha)
					instance.Parent.ImageLabel.ImageColor3 = color
					instance.TextColor3 = color
					instance.Text = `{avg} fps`

					local tooltip = UI.Class.Tooltip.Cache[instance.Parent]
					if tooltip then
						tooltip.Text(
							`Frames Per Second\n\n<b>1% Low:</b>  {low} fps\n<b>Avg:</b>     {avg} fps\n<b>Max:</b>     {max} fps`
						)
					end
				until nil
			end),
		}),
		UI.new "Spacer" {},
		statusIconLabel("Uptime", {
			Image = UI.Theme.Image.Hourglass_Bottom,
		}, {
			UI.action(function(instance)
				repeat
					task.wait(1)
					local uptime = math.round(workspace:GetServerTimeNow() - _K.script:GetAttribute("ServerStartTime"))
					instance.Text = _K.Util.Time.clock(uptime)

					local tooltip = UI.Class.Tooltip.Cache[instance.Parent]
					if tooltip then
						tooltip.Text(
							`Uptime\n\n<b>Server:</b>  {instance.Text}\n<b>Client:</b>  {_K.Util.Time.clock(time())}`
						)
					end
				until nil
			end),
		}),
		statusIconLabel("Kohl's Admin Version", {
			Image = UI.Theme.Image.Graph1,
			ImageTransparency = UI.Theme.TransparencyStrong,
		}, {
			Text = `{_K.VERSION}`,
			TextTransparency = UI.Theme.TransparencyStrong,
		}),
	},
}

_K.client.commandBarVisible = UI.state(_K.client.CommandBar.Bar, "Visible")

local titleBarButtonSize = UI.compute(function()
	local size = UI.Theme.FontSizeLargest()
	return UDim2.fromOffset(size, size)
end)

local function toggleCommandBar()
	if _K.client.CommandBar.Bar.Visible then
		_K.client.CommandBar.hide()
	else
		_K.client.CommandBar.show()
	end
end

-- add a command bar toggle button next to the exit button
_K.client.toggleCommandBar = UI.new "Button" {
	BackgroundTransparency = 1,
	HoverTransparency = 1,
	Size = titleBarButtonSize,
	Icon = UI.Theme.Image.Terminal,
	IconProperties = {
		ImageColor3 = UI.Theme.PrimaryText,
		Size = UDim2.new(0.75, 0, 0.75, 0),
	},
	Text = "",
	Parent = dashboard._instance.Frame.TitleBar,
	LayoutOrder = 8,

	Activated = toggleCommandBar,
}
_K.client.toggleCommandBar._instance:FindFirstChildOfClass("UIStroke"):Destroy()
UI.edit(_K.client.toggleCommandBar._content.IconFrame.Icon, {
	ImageTransparency = function()
		return if _K.client.commandBarVisible() then 0 elseif _K.client.toggleCommandBar._hovering() then 0.5 else 0.75
	end,
})

UI.new "Tooltip" {
	Parent = _K.client.toggleCommandBar,
	Hovering = _K.client.toggleCommandBar._hovering,
	Text = "Command Bar",
}

local isSelected = UI.state(false)
local function dashboardToggle(button)
	if not _K.client.dashboardEnabled then
		return
	end
	isSelected(button.isSelected)
	if button.isSelected then
		UI.Sound.Hover03:Play()
	else
		UI.Sound.Hover01:Play()
		UI.clearActiveStates()
	end
	local container = tabs._pages.CurrentPage
	local page = container and container:FindFirstChildWhichIsA("GuiObject")
	if page then
		page.Visible = button.isSelected
	end
	dashboard._instance.Visible = button.isSelected
	task.delay(4, _K.client.VIPNotification)
end

local function toggleCommandBarWithSound()
	if _K.client.CommandBar.Bar.Visible then
		UI.Sound.Hover03:Play()
		_K.client.CommandBar.hide()
	else
		UI.Sound.Hover01:Play()
		_K.client.CommandBar.show()
		UI.Haptic.Hover:Play()
	end
end

local dashboardButtonPadding = UI.spring(function()
	return if isSelected() then UDim.new(0, 10) else UDim.new(0, 12)
end, 16, 0.5)

local fakeButton = { isSelected = false }
local dashboardButton = UI.new "TextButton" {
	Parent = UI.TopbarFrame,
	Name = "KADashboardButton",
	BackgroundColor3 = Color3.fromRGB(18, 18, 21),
	BackgroundTransparency = 0.08,
	Size = UDim2.fromScale(1, 1),
	SizeConstraint = Enum.SizeConstraint.RelativeYY,
	Text = "",
	Visible = false,

	UI.new "UICorner" { CornerRadius = UDim.new(1, 0) },
	UI.new "UIPadding" {
		PaddingLeft = dashboardButtonPadding,
		PaddingRight = dashboardButtonPadding,
		PaddingTop = dashboardButtonPadding,
		PaddingBottom = dashboardButtonPadding,
	},

	UI.new "ImageLabel" {
		BackgroundTransparency = 1,
		Image = "rbxassetid://71961243872230",
		Size = UDim2.fromScale(1, 1),
	},

	Activated = function()
		fakeButton.isSelected = not fakeButton.isSelected
		dashboardToggle(fakeButton)
	end,
	MouseButton2Click = toggleCommandBarWithSound,
	TouchLongPress = toggleCommandBarWithSound,
}

_K.client.dashboardButton = dashboardButton

local function commandsWindow(visible: boolean)
	local context = _K.Registry.commands.cmds
	if visible then
		UI.edit(context.env.window, {
			Visible = true,
			_K.client.dashboard.Commands,
		})
		_K.client.dashboard.Commands._instance.Visible = true
	else
		context.env.window._instance.Visible = false
		_K.UI.edit(_K.client.dashboard.Tabs, {
			_K.client.dashboard.Commands,
		})
	end
end

_K.client.commandsWindowVisible = commandsWindow

local function toggleCommandsWindow()
	commandsWindow(_K.client.dashboard.Commands._instance.Parent ~= _K.Registry.commands.cmds.env.window._content)
end

local commandsButton = UI.new "TextButton" {
	Parent = UI.TopbarFrame,
	Name = "KACommandsButton",
	BackgroundColor3 = Color3.fromRGB(18, 18, 21),
	BackgroundTransparency = 0.08,
	Size = UDim2.fromScale(1, 1),
	SizeConstraint = Enum.SizeConstraint.RelativeYY,
	Text = "",
	Visible = true,

	UI.new "UICorner" { CornerRadius = UDim.new(1, 0) },
	UI.new "UIPadding" {
		PaddingLeft = UDim.new(0, 12),
		PaddingRight = UDim.new(0, 12),
		PaddingTop = UDim.new(0, 12),
		PaddingBottom = UDim.new(0, 12),
	},

	UI.new "ImageLabel" {
		BackgroundTransparency = 1,
		Image = UI.Theme.Image.Terminal,
		Size = UDim2.fromScale(1, 1),
	},

	Activated = toggleCommandsWindow,
	MouseButton2Click = toggleCommandBarWithSound,
	TouchLongPress = toggleCommandBarWithSound,
}

_K.client.commandsButton = commandsButton

task.spawn(function()
	repeat
		task.wait()
	until _K.client.TopbarPlus

	dashboardButton:Destroy()
	dashboardButton = _K.client.TopbarPlus
		.new()
		:setName("KADashboardButton")
		:setImage(71961243872230)
		:setImageScale(1)
		:setOrder(-1)
		:setEnabled(_K.client.dashboardButtonEnabled)
		:bindEvent("toggled", dashboardToggle)

	local image = dashboardButton:getInstance("IconImage")
	if image then
		UI.new "UIPadding" {
			Parent = image.Parent,
			PaddingLeft = dashboardButtonPadding,
			PaddingRight = dashboardButtonPadding,
			PaddingTop = dashboardButtonPadding,
			PaddingBottom = dashboardButtonPadding,
		}
	end

	_K.client.dashboardButton = dashboardButton

	commandsButton:Destroy()
	commandsButton = _K.client.TopbarPlus
		.new()
		:setName("KACommandsButton")
		:setImage(string.match(UI.Theme.Image.Terminal._value, "%d+$"))
		:setImageScale(1)
		:setOrder(-1)
		:setEnabled(_K.client.commandsButtonEnabled)
		:bindEvent("toggled", toggleCommandsWindow)

	_K.client.commandsButton = commandsButton

	local image2 = commandsButton:getInstance("IconImage")
	if image2 then
		UI.new "UIPadding" {
			Parent = image2.Parent,
			PaddingLeft = UDim.new(0, 12),
			PaddingRight = UDim.new(0, 12),
			PaddingTop = UDim.new(0, 12),
			PaddingBottom = UDim.new(0, 12),
		}
	end

	for _, button in { commandsButton, dashboardButton } do
		if button.modifyTheme then
			button:modifyTheme({ "Widget", "BorderSize", 0 })
		end

		local clickRegion: TextButton? = button:getInstance("ClickRegion")
		if clickRegion then
			clickRegion.MouseButton2Click:Connect(toggleCommandBarWithSound)
			clickRegion.TouchLongPress:Connect(toggleCommandBarWithSound)
		end

		local iconSpot = button:getInstance("IconSpot")
		if iconSpot then
			iconSpot.Size = UDim2.fromOffset(44, 44)
		end
	end
end)

_K.client.hotkeys.dashboard.callback = function()
	if not _K.client.dashboardEnabled then
		return
	end
	if _K.client.dashboardButtonEnabled and _K.client.TopbarPlus then
		if dashboardButton.isSelected then
			dashboardButton:deselect()
		else
			dashboardButton:select()
		end
	else
		fakeButton.isSelected = not fakeButton.isSelected
		dashboardToggle(fakeButton)
	end
end

dashboard._exitButton._instance.Activated:Connect(function()
	fakeButton.isSelected = false
	if _K.client.TopbarPlus then
		dashboardButton:deselect()
	else
		dashboardToggle(fakeButton)
	end
end)

task.spawn(_K.log, `Dashboard loaded in {math.round((os.clock() - clockStart) * 1000)} ms`, "DEBUG")

return _K.client.dashboard


local _K = require(script.Parent.Parent)
local UI = require(script.Parent:WaitForChild("UI"))
local ExplorerOrder = require(script:WaitForChild("ExplorerOrder"))

local ContentProvider = game:GetService("ContentProvider")
local Reflection = game:GetService("ReflectionService")

local DragScope = {
	Active = false,
	StartPos = Vector3.zero,
	Ghost = nil :: Instance?,
	TargetNode = nil :: any,
}

local CLIPBOARD = {}
local SELECTION = {}
local SHOW_HIDDEN = false
local NODES = setmetatable({ [game] = { AncestorExpanded = true, Expanded = true, Instance = game } }, { __mode = "k" })

local scroller, updateListThrottled

local function isService(instance: Instance)
	local class = Reflection:GetClass(instance.ClassName)
	return class and class.Permits and class.Permits.GetService
end

local function setAncestorExpanded(node, value)
	local v = if node.Expanded then value else false
	for _, child in node.Instance:GetChildren() do
		local childNode = NODES[child]
		if childNode then
			childNode.AncestorExpanded = v
			setAncestorExpanded(childNode, v)
		end
	end
end

local function getFallbackIcon(instance: Instance)
	local path = if isService(instance)
		then "InsertableObjects/Dark/Standard/Service"
		else "Placeholder/Dark/Standard/Placeholder"

	return `rbxasset://studio_svg_textures/Shared/{path}.png`
end

local classIcons = {}
local function getClassIcon(instance: Instance)
	local existing = classIcons[instance.ClassName]
	if existing ~= nil then
		return existing or getFallbackIcon(instance)
	end

	return `rbxasset://studio_svg_textures/Shared/InsertableObjects/Dark/Standard/{instance.ClassName}.png`
end

local function setClassIcon(item, instance: Instance)
	local existing = classIcons[instance.ClassName]
	if existing ~= nil then
		item._instance.IconFrame.ImageLabel.Image = existing or getFallbackIcon(instance)
		return
	end

	item._instance.IconFrame.ImageLabel.Image =
		"rbxasset://studio_svg_textures/Shared/Placeholder/Dark/Standard/Placeholder.png"

	local texture = `rbxasset://studio_svg_textures/Shared/InsertableObjects/Dark/Standard/{instance.ClassName}.png`
	task.spawn(
		ContentProvider.PreloadAsync,
		ContentProvider,
		{ texture },
		function(path: string, Status: Enum.AssetFetchStatus)
			if Status == Enum.AssetFetchStatus.Success then
				classIcons[instance.ClassName] = path
				if item.node.Instance == instance then
					item._instance.IconFrame.ImageLabel.Image = path
				end
			elseif Status == Enum.AssetFetchStatus.Failure or Status == Enum.AssetFetchStatus.TimedOut then
				classIcons[instance.ClassName] = false
				if item.node.Instance == instance then
					item._instance.IconFrame.ImageLabel.Image = getFallbackIcon(instance)
				end
			end
		end
	)
end

local function sortChildren(a, b)
	local orderA = ExplorerOrder[a.ClassName]
	local orderB = ExplorerOrder[b.ClassName]
	if orderA and orderB and orderA ~= orderB then
		return orderA < orderB
	end
	if a.ClassName == b.ClassName then
		return a.Name < b.Name
	end
	return a.ClassName < b.ClassName
end

local function sortNodes(A: Instance, B: Instance)
	local a, b = NODES[A], NODES[B]
	if not a or not b then
		return false
	end
	return a.Index < b.Index
end

local function traverse(instance: Instance, index: number, traverseAll: boolean?)
	local node = NODES[instance]
	if not node or not (node.AncestorExpanded or traverseAll) then
		return index
	end

	node.Index = index

	local children = instance:GetChildren()
	table.sort(children, sortChildren)

	for _, child in children do
		index = traverse(child, index + 1, traverseAll)
	end

	return index
end

local function instanceRemoving(instance: Instance)
	local parentNode = instance.Parent and NODES[instance.Parent]
	if parentNode and parentNode.Expanded then
		updateListThrottled(scroller)
	end
end

local function check(instance: Instance)
	return instance.AncestryChanged
end

local function isAncestorExpanded(instance: Instance)
	if instance == game then
		return true
	end

	local parent = instance.Parent
	while parent do
		if parent == game then
			return true
		end
		local node = NODES[parent]
		if node and not node.Expanded then
			return false
		end
		parent = parent.Parent
	end

	return false
end

local function removeNode(instance)
	local node = NODES[instance]
	if node then
		if _K and _K.Flux and _K.Flux.cleanup then
			_K.Flux.cleanup(node)
		end
		NODES[instance] = nil
	end

	for _, descendant in instance:GetDescendants() do
		local descNode = NODES[descendant]
		if descNode then
			if _K and _K.Flux and _K.Flux.cleanup then
				_K.Flux.cleanup(descNode)
			end
			NODES[descendant] = nil
		end
	end
end

local function instanceAdded(instance: Instance, parent: Instance?)
	if not pcall(check, instance) then
		return
	end

	if not parent then
		parent = instance.Parent
	end

	if not parent then
		local node = NODES[instance]
		if node then
			removeNode(instance)
			if isAncestorExpanded(instance) then
				updateListThrottled(scroller)
			end
		end
		return
	end

	local tree, currParent = { instance }, parent
	while currParent do
		table.insert(tree, currParent)
		currParent = currParent.Parent
	end

	for i = #tree - 1, 1, -1 do
		local obj = tree[i]
		local objNode = NODES[obj]
		if not objNode then
			objNode = {
				AncestorExpanded = isAncestorExpanded(obj),
				Expanded = false,
				Selected = false,
				Instance = obj,
				Index = 0,
			}
			NODES[obj] = objNode
		end
		objNode.Depth = (#tree - i) - 1
	end

	local node = NODES[instance]
	if node then
		node.AncestorExpanded = isAncestorExpanded(instance)
		if not node.Connection then
			node.Connection = true

			instance.AncestryChanged:Connect(instanceAdded)
			instance.Destroying:Connect(function()
				local parentNode = instance.Parent and NODES[instance.Parent]
				if parentNode and parentNode.Expanded then
					updateListThrottled(scroller)
				end
				removeNode(instance)
			end)
		end
	end

	local parentNode = NODES[parent]
	if parentNode and parentNode.Expanded then
		updateListThrottled(scroller)
	end
end

local function parentInstanceUnsafe(instance: Instance, parent: Instance?)
	instance.Parent = parent
end

local function cloneInstanceUnsafe(instance, parent: Instance?)
	local clone = instance:Clone()
	clone.Parent = parent

	return clone
end

local function COPY()
	table.clear(CLIPBOARD)
	for _, node in SELECTION do
		if pcall(cloneInstanceUnsafe, node.Instance) then
			table.insert(CLIPBOARD, node.Instance)
		end
	end
end

local function DELETE()
	local instances = table.create(#SELECTION)
	for _, node in SELECTION do
		table.insert(instances, node.Instance)
	end
	_K.Remote.Explorer:Fire("DELETE", instances)

	for _, node in SELECTION do
		if node.Instance:IsA("Player") then
			continue
		end
		pcall(game.Destroy, node.Instance)
	end
	table.clear(SELECTION)
end

local function PASTE()
	local selected = SELECTION[1]
	_K.Remote.Explorer:Fire("PASTE", CLIPBOARD, selected.Instance)

	if not selected.Expanded then
		selected.Expanded = true
		setAncestorExpanded(selected, true)
	end

	updateListThrottled(scroller)
end

local titleBarHeight = UI.compute(function()
	return UI.Theme.FontSize() + UI.Theme.Padding().Offset * 2
end)

local titleBarButtonSize = function()
	return UDim2.fromOffset(titleBarHeight(), titleBarHeight())
end

local function Initialize()
	local Explorer = UI.new "Window" {
		Parent = UI.LayerTopInset,
		Icon = "rbxassetid://71961243872230",
		Name = "Explorer",
		Title = "Explorer",

		UI.new "UIListLayout" {
			Padding = UI.Theme.Padding,
		},
	}

	local titleBar = Explorer._instance.Frame.TitleBar

	local deleteButton = UI.new "Button" {
		Parent = titleBar,
		LayoutOrder = 4,
		Icon = UI.Theme.Image.Delete,
		IconProperties = {
			ImageColor3 = UI.Theme.PrimaryText,
			Size = UDim2.fromScale(0.625, 0.625),
		},
		Size = titleBarButtonSize,

		Activated = DELETE,
	}
	UI.new "Tooltip" { Parent = deleteButton, Hovering = deleteButton._hovering, Text = "Delete" }

	local copyButton = UI.new "Button" {
		Parent = titleBar,
		LayoutOrder = 6,
		Icon = UI.Theme.Image.Copy,
		IconProperties = {
			ImageColor3 = UI.Theme.PrimaryText,
			Size = UDim2.fromScale(0.625, 0.625),
		},
		Size = titleBarButtonSize,

		Activated = COPY,
	}
	UI.new "Tooltip" { Parent = copyButton, Hovering = copyButton._hovering, Text = "Copy" }

	local pasteButton = UI.new "Button" {
		Parent = titleBar,
		LayoutOrder = 7,
		Icon = UI.Theme.Image.Paste,
		IconProperties = {
			ImageColor3 = UI.Theme.PrimaryText,
			Size = UDim2.fromScale(0.625, 0.625),
		},
		Size = titleBarButtonSize,

		Activated = PASTE,
	}
	UI.new "Tooltip" { Parent = pasteButton, Hovering = pasteButton._hovering, Text = "Paste" }

	local itemSize = UDim2.new(1, 0, 0, 20)

	local function createGhost()
		if DragScope.Ghost then
			DragScope.Ghost:Destroy()
		end

		local ghost = UI.new "Frame" {
			Parent = UI.LayerTop,
			BackgroundTransparency = 1,
			Active = false,
			AutomaticSize = Enum.AutomaticSize.XY,
			Size = UDim2.new(),

			UI.new "UIListLayout" {
				SortOrder = Enum.SortOrder.LayoutOrder,
			},
		}

		for i, node in SELECTION do
			if node.Depth > 0 then
				UI.new "Frame" {
					LayoutOrder = i,
					Parent = ghost,
					AutomaticSize = Enum.AutomaticSize.X,
					BackgroundTransparency = 1,
					Size = UDim2.new(0, 0, 0, 20),

					UI.new "UIListLayout" {
						FillDirection = Enum.FillDirection.Horizontal,
						SortOrder = Enum.SortOrder.LayoutOrder,
						VerticalAlignment = Enum.VerticalAlignment.Center,
					},

					UI.new "Frame" {
						Name = "IconFrame",
						BackgroundTransparency = 1,
						Size = UDim2.fromOffset(20, 20),

						UI.new "ImageLabel" {
							BackgroundTransparency = 1,
							AnchorPoint = Vector2.new(0, 0.5),
							Position = UDim2.fromScale(0, 0.5),
							Size = UDim2.fromOffset(16, 16),
							Image = getClassIcon(node.Instance),
							ImageTransparency = 0.25,
						},
					},

					UI.new "TextLabel" {
						AutomaticSize = Enum.AutomaticSize.X,
						BackgroundTransparency = 1,
						Font = Enum.Font.SourceSans,
						Text = node.Instance.Name,
						TextSize = 16,
						TextColor3 = UI.Theme.PrimaryText,
						TextStrokeColor3 = UI.Theme.Primary,
						TextStrokeTransparency = UI.Theme.TextStrokeTransparency,
						TextTransparency = 0.25,
						TextTruncate = Enum.TextTruncate.SplitWord,
						Size = UDim2.new(0, 0, 1, 0),
						TextXAlignment = Enum.TextXAlignment.Left,
					},
				}
			end
		end

		DragScope.Ghost = ghost
	end

	local function updateGhost(position: Vector3)
		if DragScope.Ghost then
			DragScope.Ghost.Position = UDim2.fromOffset(position.X + 6, position.Y - 10 + UI.TopbarInset._value.Height)
		end
	end

	local function executeReparent()
		if not DragScope.TargetNode or not DragScope.TargetNode.Instance then
			return
		end

		local target = DragScope.TargetNode.Instance
		local changed = false

		for _, node in SELECTION do
			local inst = node.Instance
			if inst ~= target and not isService(inst) and not target:IsDescendantOf(inst) and inst.Parent ~= target then
				pcall(parentInstanceUnsafe, inst, target)
				changed = true
			end
		end

		if changed and DragScope.TargetNode then
			DragScope.TargetNode.Expanded = true
			setAncestorExpanded(DragScope.TargetNode, DragScope.TargetNode.Expanded)
			updateListThrottled(scroller)
		end
	end

	UI.UserInputService.InputChanged:Connect(function(input, processed)
		if input.UserInputType == Enum.UserInputType.MouseMovement then
			if DragScope.Active then
				updateGhost(input.Position)
			end
		end
	end)

	UI.UserInputService.InputEnded:Connect(function(input)
		if input.UserInputType == Enum.UserInputType.MouseButton1 then
			if DragScope.Active then
				executeReparent()
				DragScope.Active = false
				DragScope.TargetNode = nil
				if DragScope.Ghost then
					DragScope.Ghost:Destroy()
					DragScope.Ghost = nil
				end
			end
		end
	end)

	UI.UserInputService.InputBegan:Connect(function(input: InputObject)
		if input.UserInputType == Enum.UserInputType.Keyboard then
			local ctrl = UI.UserInputService:IsKeyDown(Enum.KeyCode.LeftControl)
			if input.KeyCode == Enum.KeyCode.C and ctrl then
				COPY()
			elseif input.KeyCode == Enum.KeyCode.V and ctrl then
				PASTE()
			elseif input.KeyCode == Enum.KeyCode.Delete then
				DELETE()
			end
		end
	end)

	scroller = UI.new "ScrollerFast" {
		Parent = Explorer,
		List = NODES,
		DictList = true,
		Enabled = true,
		FilterInput = true,
		Visible = true,
		ItemSize = itemSize,
		CreateItem = function(self)
			local item = {} :: any
			local lastActivate = 0
			item._instance = UI.new "TextButton" {
				Parent = Explorer._content,
				AutoButtonColor = false,
				BackgroundColor3 = UI.Theme.Secondary,
				BackgroundTransparency = 1,
				BorderSizePixel = 0,
				TextTransparency = 1,
				Size = itemSize,

				UI.new "UIPadding" {},

				UI.new "UIListLayout" {
					FillDirection = Enum.FillDirection.Horizontal,
					SortOrder = Enum.SortOrder.LayoutOrder,
					VerticalAlignment = Enum.VerticalAlignment.Center,
				},

				UI.new "TextButton" {
					Name = "Expand",
					BackgroundTransparency = 1,
					TextTransparency = 1,
					Size = UDim2.fromOffset(20, 20),

					UI.new "ImageLabel" {
						AnchorPoint = Vector2.new(0.5, 0.5),
						Position = UDim2.fromScale(0.5, 0.5),
						BackgroundTransparency = 1,
						Size = UDim2.fromOffset(8, 8),
						Image = UI.Theme.Image.Down,
						Rotation = -90,
					},

					Activated = function()
						if item._instance.Expand.ImageLabel.Visible then
							item.node.Expanded = not item.node.Expanded
							setAncestorExpanded(item.node, item.node.Expanded)

							scroller:refreshList()
						end
					end,
				},

				UI.new "Frame" {
					Name = "IconFrame",
					BackgroundTransparency = 1,
					Size = UDim2.fromOffset(20, 20),

					UI.new "ImageLabel" {
						BackgroundTransparency = 1,
						AnchorPoint = Vector2.new(0, 0.5),
						Position = UDim2.fromScale(0, 0.5),
						Size = UDim2.fromOffset(16, 16),
						Image = UI.Theme.Image.Add,
					},
				},

				UI.new "TextLabel" {
					BackgroundTransparency = 1,
					Font = Enum.Font.SourceSans,
					TextSize = 16,
					TextColor3 = UI.Theme.PrimaryText,
					TextStrokeColor3 = UI.Theme.Primary,
					TextStrokeTransparency = UI.Theme.TextStrokeTransparency,
					TextTruncate = Enum.TextTruncate.SplitWord,
					Size = UDim2.new(1, -40, 1, 0),
					TextXAlignment = Enum.TextXAlignment.Left,
				},

				-- LOGIC START --
				Activated = function()
					if item._instance.Expand.ImageLabel.Visible then
						local last = lastActivate
						lastActivate = os.clock()
						if lastActivate - last < 0.3 then
							item.node.Expanded = not item.node.Expanded
							setAncestorExpanded(item.node, item.node.Expanded)
							scroller:refreshList()
							return
						end
					end

					local ctrl = UI.UserInputService:IsKeyDown(Enum.KeyCode.LeftControl)
					local shift = UI.UserInputService:IsKeyDown(Enum.KeyCode.LeftShift)

					if ctrl or shift then -- Toggle selection
						item.node.Selected = not item.node.Selected
						if item.node.Selected then
							table.insert(SELECTION, item.node)
							item._instance.BackgroundTransparency = 0.75
						else
							local idx = table.find(SELECTION, item.node)
							if idx then
								table.remove(SELECTION, idx)
								item._instance.BackgroundTransparency = 0.9
							end
						end
					else -- Single select
						for _, v in scroller._lineItemCache do
							v._instance.BackgroundTransparency = 1
						end
						for _, v in scroller._lineItemFree do
							v._instance.BackgroundTransparency = 1
						end
						for _, v in SELECTION do
							v.Selected = false
						end
						table.clear(SELECTION)

						item.node.Selected = true
						table.insert(SELECTION, item.node)
						item._instance.BackgroundTransparency = 0.75
					end
				end,

				TouchLongPress = function(input, processed)
					DragScope.StartPos = input.Position
				end,

				InputBegan = function(input, processed)
					if input.UserInputType == Enum.UserInputType.MouseButton1 then
						DragScope.StartPos = input.Position
					end
				end,

				InputChanged = function(input, processed)
					if input.UserInputType == Enum.UserInputType.MouseMovement then
						if
							not DragScope.Active
							and UI.UserInputService:IsMouseButtonPressed(Enum.UserInputType.MouseButton1)
							and item.node
							and item.node.Depth > 0
						then
							local delta = (input.Position - DragScope.StartPos).Magnitude
							if delta > 5 then
								DragScope.Active = true

								if item.node and not item.node.Selected then
									for _, v in SELECTION do
										v.Selected = false
									end
									table.clear(SELECTION)
									item.node.Selected = true
									table.insert(SELECTION, item.node)
									scroller:refreshList()
								end

								createGhost()
								updateGhost(input.Position)
							end
						end
					end
				end,
			}

			item.hovering = UI.newSinkHoverState(item._instance)
			UI.edit(item._instance, {
				BackgroundTransparency = UI.compute(function()
					local hover = item.hovering()
					if hover and DragScope.Active then
						DragScope.TargetNode = item.node
					end
					if item.node and item.node.Selected then
						return 0.75
					end
					return if hover then 0.875 else 1
				end),
			})

			return item
		end,
		RenderItem = function(self, item, instance)
			local node = NODES[instance]
			if node then
				item.node = node
				item._instance.TextLabel.Text = instance.Name
				item._instance.BackgroundTransparency = if node.Selected then 0.75 else 1

				setClassIcon(item, instance)
				item._instance.Expand.ImageLabel.Visible = #instance:GetChildren() > 0
				item._instance.Expand.ImageLabel.Rotation = if node.Expanded then 0 else -90
				item._instance.UIPadding.PaddingLeft = UDim.new(0, node.Depth * 10)
			end
		end,
	}

	UI.new "Tooltip" {
		Parent = scroller._input._instance.ImageLabel,
		Text = "Search uses game:QueryDescendants()",
	}

	updateListThrottled = _K.Util.Function.throttle(1, scroller.updateList)

	scroller:filter(function(self, list)
		local new = {}
		local rawFilter = scroller._input._input.Text
		local filter = rawFilter
		self._filter = filter

		local noFilter = string.find(self._filter, "^%s*$")
		local query

		if noFilter then
			for _, instance in list do
				if not ExplorerOrder[instance.ClassName] and not SHOW_HIDDEN then
					continue
				end
				local node = NODES[instance]
				if node and node.AncestorExpanded and node.Depth then
					table.insert(new, instance)
				end
			end
		else
			-- prefix names with "#"
			if string.find(filter, "^%w$") and not Reflection:GetClass(filter) then
				filter = "#" .. filter
			end

			local cache, count, ok = {}, 0, nil
			ok, query = pcall(game.QueryDescendants, game, filter)
			if not ok then
				query = {}
			end

			if #query > 0 then
				for _, instance in query do
					if not ExplorerOrder[instance.ClassName] and not SHOW_HIDDEN then
						continue
					end
					local node = NODES[instance]
					if node and node.Depth then
						count += 1
						cache[instance] = true
						local parent = instance.Parent
						while parent and parent ~= game and not cache[parent] do
							cache[parent] = true
							parent = parent.Parent
						end
					end
				end
			else
				filter = string.lower(rawFilter)
				for _, instance in list do
					if not ExplorerOrder[instance.ClassName] and not SHOW_HIDDEN then
						continue
					end
					local node = NODES[instance]
					if node and node.Depth and string.find(string.lower(instance.Name), filter, 1, true) then
						count += 1
						cache[instance] = true
						local parent = instance.Parent
						while parent and parent ~= game and not cache[parent] do
							cache[parent] = true
							parent = parent.Parent
						end
					end
				end
			end
			if count > 32 then
				new = table.create(count)
			end
			for instance in cache do
				table.insert(new, instance)
			end
		end

		traverse(game, 0, not noFilter)
		table.sort(new, sortNodes)

		return new
	end)

	UI.edit(Explorer, { Visible = true })

	game.DescendantAdded:Connect(instanceAdded)
	game.DescendantRemoving:Connect(instanceRemoving)
	for _, instance in game:GetDescendants() do
		task.spawn(instanceAdded, instance)
	end

	return Explorer
end

local initialized = false

return {
	Show = function()
		if not initialized then
			initialized = Initialize()
		else
			UI.edit(initialized, { Visible = true })
		end
	end,
}


local _K = require(script.Parent.Parent)
local UI = _K.UI

task.defer(function()
	local ADMIN_MODEL_ID = 172732271
	local ok, result = _K.Util.Retry(function()
		return _K.Service.Marketplace:PlayerOwnsAssetAsync(_K.LocalPlayer, ADMIN_MODEL_ID)
	end)
	local ownsModel = ok and result

	if (_K.LocalPlayer:GetAttribute("_KDonationLevel") or 0) > 3 then
		return
	end

	local dialog
	local adminModelButton = UI.new "Button" {
		LayoutOrder = 10,
		Name = "adminModelButton",
		Icon = "rbxasset://textures/ui/common/robux.png",
		IconProperties = { ImageColor3 = UI.Theme.PrimaryText },
		IconRightAlign = true,
		Label = "Get Kohl's Admin",
		TextXAlignment = Enum.TextXAlignment.Left,
		Size = UDim2.new(0.75, 0, 0, 32),

		UI.new "Frame" {
			BackgroundTransparency = 1,
			AnchorPoint = Vector2.new(0, 0.5),
			Position = UDim2.new(0, 0, 0.5, 0),
			Size = UDim2.new(0, 28, 0, 28),

			UI.new "ImageLabel" {
				BackgroundTransparency = 1,
				Size = UDim2.new(0, 28, 0, 28),
				Image = "rbxasset://textures/ui/InspectMenu/ico_favorite@2x.png",
				ImageColor3 = Color3.new(0, 0, 0),
				ImageTransparency = 0.875,
			},
			UI.new "ImageButton" {
				BackgroundTransparency = 1,
				Position = UDim2.new(0, 2, 0, 2),
				Size = UDim2.new(0, 24, 0, 24),
				Image = "rbxasset://textures/ui/InspectMenu/ico_favorite@2x.png",
				ImageColor3 = Color3.new(1, 0.8, 0),
				Activated = function()
					pcall(function()
						_K.Service.AvatarEditor:PromptSetFavorite(ADMIN_MODEL_ID, 1, true)
					end)
				end,
				UI.new "Tooltip" {
					Text = "Add to Favorites",
				},
			},
		},

		UI.new "TextLabel" {
			Name = "Price",
			AutoLocalize = false,
			AutomaticSize = Enum.AutomaticSize.XY,
			BackgroundTransparency = 1,
			FontFace = UI.Theme.FontMono,
			TextSize = UI.Theme.FontSizeLarge,
			TextColor3 = UI.Theme.PrimaryText,
			TextStrokeColor3 = UI.Theme.Primary,
			TextStrokeTransparency = UI.Theme.TextStrokeTransparency,
			TextWrapped = true,
			Size = function()
				return UDim2.new(0, UI.Theme.FontSizeLarge(), 0, 32)
			end,
			Text = if ownsModel then "OWNED" else "FREE",
			RichText = true,
		},

		Activated = if ownsModel
			then nil
			else function()
				_K.client.lastAttemptedPurchase = true
				if _K.client.AllowThirdPartySales then
					_K.Service.Marketplace:PromptPurchase(_K.LocalPlayer, ADMIN_MODEL_ID)
				else
					_K.client.purchaseKADialog.Visible(true)
				end
			end,
	}

	local donations = UI.new "Frame" {
		Name = "Donations",
		AutomaticSize = Enum.AutomaticSize.Y,
		BackgroundTransparency = 1,
		Size = UDim2.new(1, -2, 0, 0),
		LayoutOrder = 11,

		UI.new "UIListLayout" {
			SortOrder = Enum.SortOrder.LayoutOrder,
			FillDirection = Enum.FillDirection.Horizontal,
			HorizontalFlex = Enum.UIFlexAlignment.SpaceEvenly,
			HorizontalAlignment = Enum.HorizontalAlignment.Center,
			Padding = UI.Theme.PaddingWithStroke,
			Wraps = true,
		},
	}

	_K.client.generateDonationButtons(donations)

	dialog = UI.new "Dialog" {
		Parent = UI.LayerTop,
		Title = "❤ Kohl's Admin?",
		Text = `Stop playing. Take command. Get the same powerful tools used here for your own games.\nTotal control, advanced security, and professional moderation are yours for free.`,
		Visible = false,
		Draggable = true,
		ExitButton = true,
		BackgroundTransparency = 0,
		AnchorPoint = Vector2.new(0.5, 0.5),
		Position = UDim2.new(0.5, 0, 0.5, 0),
		Size = UDim2.new(0, 480, 0, 0),
		Modal = true,
		ZIndex = 999,

		adminModelButton,
		donations,
	}

	local attempt, connection = 0, nil
	connection = _K.Hook.runPreparedCommands:Connect(function()
		if _K.Data.settings.getKohlsAdminPopup == false or _K.LocalPlayer:GetAttribute("_KDonationLevel") > 3 then
			connection:Disconnect()
			dialog:Destroy()
		end

		attempt += 1
		if attempt == 4 then
			connection:Disconnect()
			dialog.Visible(true)
		end
	end)
end)

return true


local UI = require(script.Parent:WaitForChild("UI"))

local Logs = {}
Logs.__index = Logs

local GLOBAL_KEY = "GLOBAL 🌐"

local clientTag = " CLIENT"
local serverTag = " SERVER"
local logTypes = {
	{ "DEBUG", "#888" },
	{ "INFO", "#ccc" },
	{ "WARN", "#ff0" },
	{ "ERROR", "#f00" },
	{ "CHAT", "#0f8" },
	{ "COMMAND", "#80f" },
	{ "JOIN", "#0f0" },
	{ "LEAVE", "#080" },
	{ "KILL", "#a00" },
	{ "DEATH", "#800" },
	{ "DAMAGE", "#a80" },
	{ "PURCHASE", "#0ff" },
	{ GLOBAL_KEY, "#fff" },
}

local logTypeColors = {}
for _, array in logTypes do
	local logType, color = unpack(array)
	logTypeColors[logType] = color
end

function Logs.new(_K)
	local function createItem(self, log)
		local tooltip = UI.new "Tooltip" {
			Text = "Loading...",
		}

		local cleanup = { tooltip }

		local itemRef = {
			_instance = UI.new "TextBox" {
				AutoLocalize = false,
				BackgroundColor3 = UI.Theme.PrimaryText,
				BackgroundTransparency = 1,
				Size = self.ItemSize,
				TextEditable = false,
				ClearTextOnFocus = false,
				FontFace = UI.Theme.FontMono,
				Text = "",
				TextSize = UI.Theme.FontSize,
				TextColor3 = UI.Theme.Primary,
				TextTransparency = 0.5,
				TextTruncate = Enum.TextTruncate.SplitWord,
				TextXAlignment = Enum.TextXAlignment.Left,
				TextYAlignment = Enum.TextYAlignment.Center,

				tooltip,

				UI.new "TextLabel" {
					AutoLocalize = false,
					BackgroundTransparency = 1,
					Size = self.ItemSize,
					RichText = true,
					FontFace = UI.Theme.FontMono,
					Text = "Log failed to render.",
					TextSize = UI.Theme.FontSize,
					TextColor3 = UI.Theme.PrimaryText,
					TextStrokeColor3 = UI.Theme.Primary,
					TextStrokeTransparency = UI.Theme.TextStrokeTransparency,
					TextTransparency = 0,
					TextTruncate = Enum.TextTruncate.SplitWord,
					TextXAlignment = Enum.TextXAlignment.Left,
					TextYAlignment = Enum.TextYAlignment.Center,
				},
			},

			tipText = "",

			[UI.Clean] = cleanup,
		}

		table.insert(
			cleanup,
			tooltip.Visible:Connect(function()
				if tooltip.Visible._value then
					tooltip.Text(itemRef.tipText)
				end
			end)
		)

		return itemRef
	end

	local escape = _K.Util.String.escapeRichText

	local function renderItem(self, itemData, log)
		if not log.filterText or #log < 3 then
			return
		end

		local item = itemData._instance
		local date = os.date("%y-%m-%d", log[3])
		local timeOfDay = os.date("%X", log[3])
		local level = _K.Logger:decode(log[2])
		local global = if log[6] == false then " 🌐" else ""

		local preText =
			`{escape(timeOfDay)}{global} {level}{if log[6]
				then clientTag
				elseif not log[5] and log[6] == nil then serverTag
				else ""}`

		local postText = `{if log[5] then `{log[5]}: ` else ""}{escape(log[1] or "")}`

		itemData.tipText = `<font transparency="0.5">{date}</font> {preText}\n{postText}`

		local rawText = `{timeOfDay}{global} {level}{if log[6]
			then " CLIENT"
			elseif not log[5] and log[6] == nil then " SERVER"
			else ""} {if log[5] then log[5] .. ": " else ""}{log[1]}`

		if self._noFilter then
			item.Text = rawText
			item.TextLabel.Text = `{preText} {postText}`
		else
			local filterIndex, filterEnd = string.find(log.filterText, self._filter, 1, true)
			rawText = `{date} {rawText}`

			item.Text = `{timeOfDay}{global} {level}{if log[6]
				then " CLIENT"
				elseif not log[5] and log[6] == nil then " SERVER"
				else ""} {if log[5] then log[5] .. ": " else ""}{log[1]}`

			item.TextLabel.Text =
				`{escape(string.sub(rawText, 1, filterIndex - 1))}{escape(
					string.sub(rawText, filterIndex, filterEnd)
				)}{escape(string.sub(rawText, filterEnd + 1))}`
		end
	end

	local scroller = UI.new "ScrollerFast" {
		Name = "Logs",
		List = _K.client.logs,
		Enabled = false,
		FilterInput = true,
		ReverseOrder = true,
		Visible = false,
		CreateItem = createItem,
		RenderItem = renderItem,
		ItemSize = function()
			return UDim2.new(1, 0, 0, UI.Theme.FontSize() + UI.Theme.Padding().Offset)
		end,
	}

	local logTypeFilter = {}
	for _, array in logTypes do
		logTypeFilter[array[1]] = UI.state(array[1] ~= "DEBUG" or _K.DEBUG)
	end
	scroller.logTypeFilter = logTypeFilter
	-- TODO: load filter options from client datastore profile

	local function filterTest(self, list)
		local filter = string.lower(self._input._input.Text)
		self._filter = filter

		local noFilter = string.find(filter, "^%s*$")
		self._noFilter = noFilter

		local showGlobal = logTypeFilter[GLOBAL_KEY]._value

		local new = {}
		for i, log in list do
			local level = _K.Logger:decode(log[2])

			if not log.filterText then
				local date = os.date("%y-%m-%d", log[3])
				local timeOfDay = os.date("%X", log[3])

				log.filterText = string.lower(
					`{date} {timeOfDay}{if log[6] == false then " 🌐" else ""} {level}{if log[6]
						then " CLIENT"
						elseif not log[5] and log[6] == nil then " SERVER"
						else ""} {if log[5] then log[5] .. ": " else ""}{log[1]}`
				)
			end

			if not logTypeFilter[level]._value then
				continue
			end

			if log[6] == false and not showGlobal then
				continue
			end

			if noFilter then
				table.insert(new, log)
			elseif string.find(log.filterText, filter, 1, true) then
				table.insert(new, log)
			end
		end

		return new
	end

	task.defer(scroller.filter, scroller, filterTest)

	local filterMenu
	local filterButton = UI.new "Button" {
		LayoutOrder = 3,
		ActiveSound = false,
		Icon = UI.Theme.Image.Filter,
		IconProperties = {
			ImageColor3 = UI.Theme.PrimaryText,
		},
		Size = UDim2.new(1, 0, 1, 0),
		SizeConstraint = Enum.SizeConstraint.RelativeYY,

		UI.new "UIPadding" {
			PaddingTop = UI.Theme.Padding,
			PaddingBottom = UI.Theme.Padding,
			PaddingLeft = UI.Theme.Padding,
			PaddingRight = UI.Theme.Padding,
		},

		Activated = function()
			UI.toggleState(filterMenu.Visible, "floating")
			local value = UI.raw(filterMenu.Visible)
			if value then
				UI.Sound.Hover03:Play()
			else
				UI.Sound.Hover01:Play()
			end
		end,
	}

	UI.new "Tooltip" { Parent = filterButton, Text = "Filter Logs", Hovering = filterButton._hovering }
	UI.edit(filterButton._instance.UICorner, { CornerRadius = UI.Theme.CornerPadded })

	local listItemFrame = UI.new "Frame" {
		AutomaticSize = Enum.AutomaticSize.Y,
		BackgroundTransparency = 1,
		Size = UDim2.new(1, 0, 0, 0),

		UI.new "UIListLayout" {
			FillDirection = Enum.FillDirection.Horizontal,
			Padding = UI.Theme.Padding,
			SortOrder = Enum.SortOrder.LayoutOrder,
			VerticalAlignment = Enum.VerticalAlignment.Top,
			Wraps = true,
		},
	}

	filterMenu = UI.new "Menu" {
		Adornee = filterButton._instance,
		AutomaticSize = Enum.AutomaticSize.Y,
		RightAlign = true,
		Size = function()
			local padding = UI.Theme.Padding().Offset
			return UDim2.fromOffset(256 + padding * 3, 0)
		end,

		UI.new "UIPadding" {
			PaddingTop = UI.Theme.Padding,
			PaddingBottom = UI.Theme.Padding,
			PaddingLeft = UI.Theme.Padding,
			PaddingRight = UI.Theme.Padding,
		},

		listItemFrame,
	}

	filterMenu._content.AutomaticSize = Enum.AutomaticSize.Y

	UI.edit(filterMenu._list, {
		FillDirection = Enum.FillDirection.Horizontal,
		Padding = UI.Theme.Padding,
		Wraps = true,
	})

	local lineItemSize = UI.compute(function()
		return UDim2.fromOffset(128, UI.Theme.FontSizeLarger())
	end)

	for index, array in logTypes do
		local logType = array[1]
		UI.new "ListItem" {
			Parent = listItemFrame,
			LayoutOrder = index,
			Text = logType,
			Size = lineItemSize,

			UI.new "Checkbox" {
				Value = logTypeFilter[logType],

				[UI.Hook] = {
					Value = function(value)
						scroller._filterUpdate = true
						logTypeFilter[logType](value)
						scroller:refreshList()
					end,
				},
			},
		}
	end

	for label, value in { SELECT = true, DESELECT = false } do
		UI.new "Button" {
			Parent = filterMenu._content,
			LayoutOrder = #logTypes + 1,
			Label = `<b>{label} ALL</b>`,
			Size = lineItemSize,

			Activated = function()
				scroller._filterUpdate = true
				for index, array in logTypes do
					local logType = array[1]
					logTypeFilter[logType](value)
					scroller:refreshList()
				end
			end,
		}
	end

	scroller._input._instance = UI.new "Frame" {
		Parent = scroller,
		Name = "FilterInput",

		BackgroundTransparency = 1,
		Size = function()
			return UDim2.new(1, 0, 0, UI.Theme.FontSize() + UI.Theme.PaddingDouble().Offset)
		end,

		UI.new "UIListLayout" {
			FillDirection = Enum.FillDirection.Horizontal,
			Padding = UI.Theme.PaddingWithStroke,
			SortOrder = Enum.SortOrder.LayoutOrder,
		},
		UI.edit(scroller._input, { UI.new "UIFlexItem" { FlexMode = Enum.UIFlexMode.Fill } }),
		filterButton,
	}

	return scroller
end

return Logs