--!strict
--!optimize 2

local PropertyFrame = require("../PropertyFrame")
local getClampedXY = require("../../../Shared/Small/GetClampedXY")
local RawInst = require("../../../Shared/Small/RawInst")

return PropertyFrame.create(
	"Enum",
	function(info, list, item, initValue)
		local editorFrame = info.PropertyEditor.Widget:FindFirstChild("Frame") :: any
		assert(editorFrame)

		local overlay = editorFrame.Items.Overlay

		local box, dropdownList
		box, dropdownList = PropertyFrame.PluginUI.createEnumBox(list, item.name, initValue.EnumType, initValue, nil, overlay, nil, function(pos, size)
			dropdownList.Backdrop.Parent = overlay
			dropdownList.Backdrop.Size = UDim2.fromOffset(size.X, dropdownList.Backdrop.Size.Y.Offset)

			local overlayAbsPos = overlay.AbsolutePosition

			local x, y = getClampedXY(
				dropdownList.Backdrop.AbsoluteSize,
				pos,
				size,
				editorFrame.AbsolutePosition,
				editorFrame.AbsoluteSize
			)
			local relativeX = x - overlayAbsPos.X
			local relativeY = y - overlayAbsPos.Y

			return Vector2.new(0, 0), UDim2.fromOffset(relativeX, relativeY)
		end)

		return box, {
			box = box,
			dropdownList = dropdownList
		}
	end,
	function(info, item, initValue, data)
		local box = data.box

		local function onChanged(newValue: EnumItem)
			box:ReconstructEnumList(newValue.EnumType, newValue)
		end
		local function onChangedInner(newValue: EnumItem)
			onChanged(newValue)
			local id = PropertyFrame.beginRecording("Enum")
			for _, inst in info.Instances do
				RawInst.rawnewindex(inst, item.name, newValue)
				info.VersionControl.addPendingChange(inst, item.name, newValue)
			end
			PropertyFrame.finishRecording(id)
		end

		onChanged(initValue)
		local changedConn = box:OnChange(onChangedInner)

		return function()
			changedConn:Disconnect()
		end, onChanged
	end
)


--!strict
--!optimize 2

local plugin = script:FindFirstAncestorOfClass("Plugin")

local PropertyFrame = require("../PropertyFrame")
local PluginUI = PropertyFrame.PluginUI

return PropertyFrame.create(
	"Instance",
	function(info, list, item, initValue)
		local PropertyEditor, Manager = info.PropertyEditor, info.Manager

		local uiItem = PluginUI.createAttributeBox(list, item.name, initValue and initValue.Name or "")
		local valueBox = uiItem.Instance:FindFirstChild("ValueBox") :: TextBox
		assert(valueBox)

		valueBox.PlaceholderText = "Click, hold & drop to assign..."

		local button = Instance.new("TextButton")
		button.BorderSizePixel = 0
		button.Transparency = 1
		button.Size = UDim2.fromScale(1, 1)
		button.ZIndex = 10
		button.Parent = valueBox

		return uiItem, {
			button = button,
			valueBox = valueBox
		}
	end,
	function(info, item, initValue, data)
		local PropertyEditor, Manager = info.PropertyEditor, info.Manager
		local button: TextButton = data.button
		local valueBox: TextBox = data.valueBox

		local function onChanged(newValue: Instance?)
			valueBox.Text = newValue and newValue.Name or ""
		end

		local inputConnection = button.InputBegan:Connect(function(input)
			if input.UserInputState ~= Enum.UserInputState.Begin then return end
			if input.UserInputType == Enum.UserInputType.MouseButton1 then
				plugin:StartDrag(Manager.getSetPropertyDragInfo(info.Instances, item.name))
			elseif input.UserInputType == Enum.UserInputType.MouseButton2 then
				PropertyEditor.CurrentSetNilTarget = {instances = info.Instances, name = item.name}
				PropertyEditor.SetNilMenu:ShowAsync()
			end
		end)

		return function()
			inputConnection:Disconnect()
		end, onChanged
	end
)


--!strict
--!optimize 2

local PropertyFrame = require("../PropertyFrame")
local xyz = require("../../../Shared/Small/xyz")
local PluginUI = PropertyFrame.PluginUI
local RawInst = PropertyFrame.RawInst

return PropertyFrame.create(
	"Vector3",
	function(info, list, item, initValue)
		local axisToUi = {}
		local items: any = {
			function(parent)
				local box = PluginUI.createAttributeBox(nil, "X", string.format("%.3f", initValue.X))
				axisToUi["x"] = box
				return box
			end,
			function(parent)
				local box = PluginUI.createAttributeBox(nil, "Y", string.format("%.3f", initValue.Y))
				axisToUi["y"] = box
				return box
			end,
			function(parent)
				local box = PluginUI.createAttributeBox(nil, "Z", string.format("%.3f", initValue.Z))
				axisToUi["z"] = box
				return box
			end,
		}
		local box, items = PluginUI.createDropdownBox(list, item.name, `{string.format("%.3f", initValue.X)}, {string.format("%.3f", initValue.Y)}, {string.format("%.3f", initValue.Z)}`, nil, items)

		return box, {
			box = box,
			axisToUi = axisToUi
		}
	end,
	function(info, item, initValue, data)
		local box = data.box
		local axisToUi = data.axisToUi

		local function onChanged(newValue: Vector3)
			box:Set(`{string.format("%.3f", newValue.X)}, {string.format("%.3f", newValue.Y)}, {string.format("%.3f", newValue.Z)}`)
			axisToUi["x"]:Set(string.format("%.3f", newValue.X))
			axisToUi["y"]:Set(string.format("%.3f", newValue.Y))
			axisToUi["z"]:Set(string.format("%.3f", newValue.Z))
		end
		local function onChangedInner(newValue: Vector3)
			onChanged(newValue)
			local id = PropertyFrame.beginRecording("Vector3")
			for _, inst in info.Instances do
				RawInst.rawnewindex(inst, item.name, newValue)
				info.VersionControl.addPendingChange(inst, item.name, newValue)
			end
			PropertyFrame.finishRecording(id)
		end

		local conns = {
			box:OnChange(function(str: string)
				local x, y, z = xyz(str)
				local vec = Vector3.new(
					PropertyFrame.tonumber(x),
					PropertyFrame.tonumber(y),
					PropertyFrame.tonumber(z)
				)
				onChangedInner(vec)
			end),
			axisToUi["x"]:OnChange(function(newValue: any)
				local curr = box:Get()
				local x, y, z = xyz(curr)
				local str = `{string.format("%.3f", newValue)}, {string.format("%.3f", y)}, {string.format("%.3f", z)}`
				box:Set(str)
				axisToUi["x"]:Set(string.format("%.3f", newValue))

				local vec = Vector3.new(
					PropertyFrame.tonumber(newValue),
					PropertyFrame.tonumber(y),
					PropertyFrame.tonumber(z)
				)
				onChangedInner(vec)
			end),
			axisToUi["y"]:OnChange(function(newValue: any)
				local curr = box:Get()
				local x, y, z = xyz(curr)
				local str = `{string.format("%.3f", x)}, {string.format("%.3f", newValue)}, {string.format("%.3f", z)}`
				box:Set(str)
				axisToUi["y"]:Set(string.format("%.3f", newValue))

				local vec = Vector3.new(
					PropertyFrame.tonumber(x),
					PropertyFrame.tonumber(newValue),
					PropertyFrame.tonumber(z)
				)
				onChangedInner(vec)
			end),
			axisToUi["z"]:OnChange(function(newValue: any)
				local curr = box:Get()
				local x, y, z = xyz(curr)
				local str = `{string.format("%.3f", x)}, {string.format("%.3f", y)}, {string.format("%.3f", newValue)}`
				box:Set(str)
				axisToUi["z"]:Set(string.format("%.3f", newValue))

				local vec = Vector3.new(
					PropertyFrame.tonumber(x),
					PropertyFrame.tonumber(y),
					PropertyFrame.tonumber(newValue)
				)
				onChangedInner(vec)
			end)
		}
		onChanged(initValue)
		return function()
			for _, conn in conns do
				conn:Disconnect()
			end
		end, onChanged
	end
)


--!strict
--!optimize 2

local PropertyFrame = require("../PropertyFrame")

local RawInst = PropertyFrame.RawInst

return PropertyFrame.create(
	"string",
	function(info, list, item, initValue)
		local box = PropertyFrame.PluginUI.createCheckBox(list, item.name, initValue)
		return box, {
			box = box
		}
	end,
	function(info, item, initValue, data)
		local box = data.box

		box:Set(initValue)
		box:SetName(item.name)

		local function onChanged(newValue: boolean)
			box:Set(newValue)
		end
		local function onChangedInner(newValue: boolean)
			onChanged(newValue)

			local id = PropertyFrame.beginRecording("bool")
			for _, inst in info.Instances do
				RawInst.rawnewindex(inst, item.name, newValue)
				info.VersionControl.addPendingChange(inst, item.name, newValue)
			end
			PropertyFrame.finishRecording(id)
		end

		local changedConn = box:OnChange(onChangedInner)

		return function()
			changedConn:Disconnect()
		end, onChanged
	end
)


--!strict
--!optimize 2

local PropertyFrame = require("../PropertyFrame")

local RawInst = PropertyFrame.RawInst

return PropertyFrame.create(
	"number",
	function(info, list, item, initValue)
		local box = PropertyFrame.PluginUI.createAttributeBox(list, item.name, string.format("%.3f", initValue))
		return box, {
			box = box
		}
	end,
	function(info, item, initValue, data)
		local box = data.box

		box:Set(string.format("%.3f", initValue))
		box:SetName(item.name)

		local function onChanged(newValue: number)
			box:Set(string.format("%.3f", newValue))
		end
		local function onChangedInner(newValue: number)
			onChanged(newValue)

			local id = PropertyFrame.beginRecording("Change")
			for _, inst in info.Instances do
				RawInst.rawnewindex(inst, item.name, newValue)
				info.VersionControl.addPendingChange(inst, item.name, newValue)
			end
			PropertyFrame.finishRecording(id)
		end

		local changedConn = box:OnChange(function(newValue)
			onChangedInner(newValue)
		end)

		return function()
			changedConn:Disconnect()
		end, onChanged
	end
)


--!strict
--!optimize 2

local PropertyFrame = require("../PropertyFrame")

local RawInst = PropertyFrame.RawInst

return PropertyFrame.create(
	"number",
	function(info, list, item, initValue)
		local box = PropertyFrame.PluginUI.createAttributeBox(list, item.name, initValue)
		return box, {
			box = box
		}
	end,
	function(info, item, initValue, data)
		local box = data.box

		box:Set(initValue)
		box:SetName(item.name)

		local function onChanged(newValue: number)
			box:Set(newValue)
		end
		local function onChangedInner(newValue: number)
			onChanged(newValue)

			local id = PropertyFrame.beginRecording("Change")
			for _, inst in info.Instances do
				RawInst.rawnewindex(inst, item.name, newValue)
				info.VersionControl.addPendingChange(inst, item.name, newValue)
			end
			PropertyFrame.finishRecording(id)
		end

		local changedConn = box:OnChange(function(newValue)
			onChangedInner(PropertyFrame.tonumber(newValue))
		end)

		return function()
			changedConn:Disconnect()
		end, onChanged
	end
)


--!strict
--!optimize 2

local PropertyFrame = require("../PropertyFrame")

local RawInst = PropertyFrame.RawInst

return PropertyFrame.create(
	"string",
	function(info, list, item, initValue)
		local box = PropertyFrame.PluginUI.createAttributeBox(list, item.name, initValue)
		return box, {
			box = box
		}
	end,
	function(info, item, initValue, data)
		local box = data.box

		box:Set(initValue)
		box:SetName(item.name)

		local function onChanged(newValue: string)
			box:Set(newValue)
		end
		local function onChangedInner(newValue: string)
			onChanged(newValue)

			local id = PropertyFrame.beginRecording("Change")
			for _, inst in info.Instances do
				RawInst.rawnewindex(inst, item.name, newValue)
				info.VersionControl.addPendingChange(inst, item.name, newValue)
			end
			PropertyFrame.finishRecording(id)
		end

		local changedConn = box:OnChange(onChangedInner)

		return function()
			changedConn:Disconnect()
		end, onChanged
	end
)


--!strict
--!optimize 2

local plugin = script:FindFirstAncestorOfClass("Plugin")

local ReflectionService = game:GetService("ReflectionService")
local ChangeHistoryService = game:GetService("ChangeHistoryService")

local main = script.Parent.Parent

local shared = main.Shared

local UITemplates = require(shared.UITemplates)
local PropertyFrame = require(script.PropertyFrame)
local xyz = require(shared.Small.xyz)
local RawInst = require(shared.Small.RawInst)
local VersionControl = require(script.Parent.VersionControl)
local PluginUI = require(shared.PluginUI)
local ItemGroup = require(shared.Vendor.ItemGroup)
local cleanup = ItemGroup.cleanup

local Types = require(main.Types)

local rawindex = RawInst.rawindex
local rawnewindex = RawInst.rawnewindex
local getId = PluginUI.getId

local instanceConnections: {RBXScriptConnection} = {}
local instancesToClean: {Instance} = {}

-- Frame pooling system
local framePool: {[string]: {{frame: PluginUI.ValueUIItem & any, data: {[string]: any}}}} = {}
local frameMetadata: {[any]: {type: string, data: {[string]: any}, connections: {RBXScriptConnection}}} = {}

local function cleanupFrameConnections(frame: any)
	local metadata = frameMetadata[frame]
	if metadata and metadata.connections then
		for _, conn in metadata.connections do
			if typeof(conn) == "RBXScriptConnection" then
				conn:Disconnect()
			elseif typeof(conn) == "function" then
				conn()
			end
		end
		table.clear(metadata.connections)
	end
end

local function getPooledFrame(propType: string, createFn: any, ...): (PluginUI.ValueUIItem & any, {[string]: any})
	if not framePool[propType] then
		framePool[propType] = {}
	end

	local pool = framePool[propType]
	if #pool > 0 then
		local frame = table.remove(pool)
		assert(frame)

		cleanupFrameConnections(frame)
		return frame.frame, frame.data
	end
	local frame, data = createFn(...)
	if frame then
		frameMetadata[frame] = {
			type = propType,
			data = data,
			connections = {}
		}
	end
	return frame, data
end

local function clearActiveFrames()
	for frame, metadata in frameMetadata do
		if metadata.connections then
			for _, conn in metadata.connections do
				if typeof(conn) == "RBXScriptConnection" then
					conn:Disconnect()
				elseif typeof(conn) == "function" then
					conn()
				end
			end
			table.clear(metadata.connections)
		end
		if frame.Instance then
			frame.Instance.Parent = nil
		end
	end
end

local function poolFramesByType()
	-- Organize all frames back into pools
	for frame, metadata in frameMetadata do
		local propType = metadata.type
		if propType then
			if not framePool[propType] then
				framePool[propType] = {}
			end
			local pooled = {
				frame = frame,
				data = metadata.data
			}
			local found = false
			for _, v in framePool[propType] do
				if v.frame == frame then
					found = true
					break
				end
			end
			if not found then
				table.insert(framePool[propType], pooled)
			end
		end
	end
end

local typeToPropModule = {}
for _, v in script.PropertyFrames:GetChildren() do
	local name = v.Name
	local module: PropertyFrame.PropertyFrame = (require::any)(v)
	local split = string.split(name, "_")
	for _, part in split do
		typeToPropModule[part] = module
	end
end

local function isFirstCharUpper(s: string)
	local upper = string.char(string.byte(s) + 32)
	return string.upper(upper) == string.sub(s, 1, 1)
end

local DEFAULT_PRIORITY = 2
local CATEGORY_PRIORITIES = {
	"Data",
	"Behavior",
	"Tags",
	"Attributes"
}

local init = false

local Manager: Types.Manager
local PropertyEditor = {} :: Types.PropertyEditor

local function gettypestr(type: ReflectionType)
	if type.ScriptType == "Instance" then
		return "Instance"
	else
		return type.EngineType
	end
end

local function getCommonProperties(instances: {Instance}): ReflectedProperties
	if #instances == 0 then
		return {}
	end

	if #instances == 1 then
		-- Single instance, return all its properties
		local inst = instances[1]
		return ReflectionService:GetPropertiesOfClass(inst.ClassName)
	end
	local commonProps = {}
	local firstInstanceProps = ReflectionService:GetPropertiesOfClass(instances[1].ClassName)

	if not firstInstanceProps then
		return {}
	end

	-- Start with properties from the first instance
	for _, propData in firstInstanceProps do
		local propName = propData.Name
		local isCommon = true

		-- Check if this property exists in all other instances
		for i = 2, #instances do
			local instanceProps = ReflectionService:GetPropertiesOfClass(instances[i].ClassName)
			if not instanceProps then continue end

			local instancePropsSet = {}
			for _, v in instanceProps do
				instancePropsSet[v.Name] = v
			end
			if not instancePropsSet[propName] then
				isCommon = false
				break
			end

			-- Also check if the property type matches
			if gettypestr(instancePropsSet[propName].Type) ~= gettypestr(propData.Type) then
				isCommon = false
				break
			end
		end

		if isCommon then
			table.insert(commonProps, propData)
		end
	end

	return commonProps
end

function PropertyEditor.updateProperties(instances)
	if not init then return end
	debug.profilebegin("PropertyEditor::updateProperties")
	local widget = PropertyEditor.Widget
	local list = PropertyEditor.PropertyList

	-- Disconnect connections
	for _, v in instanceConnections do
		v:Disconnect()
	end
	table.clear(instanceConnections)

	-- Clean up temporary instances
	for _, v in instancesToClean do
		v:Destroy()
	end
	table.clear(instancesToClean)

	-- Clear active frames and pool them for reuse
	clearActiveFrames()
	poolFramesByType()

	list:Clear()

	local n = #instances
	if n == 0 then
		widget.Title = "JProperties"
		debug.profileend()
		return
	end
	if n > 1 then
		widget.Title = `JProperties - {n} items`
	else
		local inst = instances[1]
		assert(inst)

		widget.Title = `JProperties - {inst.ClassName} "{inst.Name}"`
	end

	local props = getCommonProperties(instances) or {}
	local categories = {}
	for _, v in props::ReflectedProperties do
		local display = v.Display
		if display and display.DeprecationMessage then
			continue
		end
		local category
		if display then
			category = display.Category
		else
			category = "Data"
		end
		if not categories[category] then
			categories[category] = {}
		end

		local item = {
			name = v.Name,
			type = v.Type,
			writable = if v.Permits.Write ~= nil then SecurityCapabilities.fromCurrent():Contains(v.Permits.Write) else false,
			readable = if v.Permits.Read ~= nil then SecurityCapabilities.fromCurrent():Contains(v.Permits.Read) else false,
		}
		table.insert(categories[category], item)
	end

	local categoriesSorted = {}
	for category, list in categories do
		table.insert(categoriesSorted, {category = category, list = list})
	end
	table.sort(categoriesSorted, function(a, b)
		local pa = table.find(CATEGORY_PRIORITIES, a.category) or DEFAULT_PRIORITY
		local pb = table.find(CATEGORY_PRIORITIES, b.category) or DEFAULT_PRIORITY

		return pa < pb
	end)

	PluginUI.clearOpenEnumLists()

	local datas: {[string]: {changed: (any) -> (), frame: PluginUI.ValueUIItem, item: Types.PropertyItem}} = {}

	local defaultValues = {
		["Color3"] = Color3.fromRGB(0, 0, 0),
		["string"] = "",
		["Instance"] = nil,
		["int8"] = 0,
		["int16"] = 0,
		["int64"] = 0,
		["int32"] = 0,
		["int"] = 0,
		["Vector3"] = Vector3.zero,
		["ContentId"] = "",
		["bool"] = false,
		["double"] = 0,
		["float"] = 0
	}

	local defaultEnumValues = require(script.defaultEnumValues)

	local dropdowns = {}
	for i, v in categoriesSorted do
		local category, propItems = v.category, v.list

		local dropdown = PluginUI.createDropDownItemList({Visible = true, Name = category}, category, false)
		dropdown.Backdrop.LayoutOrder = i
		dropdown.Container.AutomaticCanvasSize = Enum.AutomaticSize.Y

		local n = 0
		for _, propItem in propItems do
			if not isFirstCharUpper(propItem.name) then continue end
			if not propItem.readable then continue end

			local propType = propItem.type
			local typeStr = gettypestr(propType)

			local module = typeToPropModule[typeStr]
			if not module then continue end

			local allSame, firstValue = PropertyFrame.isAllValuesSame(instances, propItem.name)

			local initValue
			if allSame then
				initValue = firstValue
			else
				if typeStr == "Enum" and propType.EnumType then
					initValue = defaultEnumValues[propType.EnumType]
				else
					initValue = defaultValues[typeStr]
				end
			end

			local info = {
				PropertyEditor = PropertyEditor,
				Manager = Manager,
				InstanceCleanup = instancesToClean,
				Plugin = plugin,
				VersionControl = VersionControl,
				Instances = instances
			}
			local propFrame, data = getPooledFrame(typeStr, module.createUI, info, dropdown, propItem, initValue)
			if not propFrame then continue end

			propFrame:SetName(propItem.name)

			-- Re-parent the frame to the current dropdown
			if propFrame.Instance then
				propFrame.Instance.Parent = dropdown.Container
			end

			n += 1

			local cleanup, changedFn = module.applyBehavior(info, propItem, initValue, data)
			datas[propItem.name] = {
				changed = changedFn,
				frame = propFrame,
				item = propItem
			}
			if not allSame then
				if propFrame.SetText then
					propFrame:SetText("--")
				else
					propFrame:Set("--")
				end
			end

			propFrame:SetEditable(propItem.writable)
			-- Store the connection in metadata
			local metadata = frameMetadata[propFrame]
			if metadata then
				table.insert(metadata.connections, cleanup :: any)
			end
		end
		if n == 0 then
			dropdown:Destroy()
			continue
		end
		table.insert(dropdowns, dropdown)
		dropdown:Show()
	end
	for _, inst in instances do
		table.insert(instanceConnections, inst.Changed:Connect(function(prop)
			local data = datas[prop]
			if not data then return end

			if not data.item.writable then return end

			local allSame, firstValue = PropertyFrame.isAllValuesSame(instances, prop)
			if allSame then
				data.changed(firstValue)
			else
				data.changed(defaultValues[prop])
				data.frame:Set("--")
			end
		end))
	end

	local allHaveTags = true
	for _, inst in instances do
		if not inst.GetTags then
			allHaveTags = false
			break
		end
	end

	if allHaveTags then
		local dropdown = PluginUI.createDropDownItemList({Visible = true, Name = "Tags", LayoutOrder = 1000}, "Tags", false)
		table.insert(dropdowns, dropdown)
		dropdown.Container.AutomaticCanvasSize = Enum.AutomaticSize.Y

		local tagToFrame = {}
		local function createTagFrame(tag: string)
			local frame = PluginUI.createButtonWithIcon(dropdown, tag, "rbxassetid://92768859826736", Vector2.zero, Vector2.zero)
			frame.Instance.Icon:AddTag("JEXP_MonochromeImage")

			PluginUI.createDeleteButton(frame.Instance, function()
				frame:Destroy()
				for _, inst in instances do
					inst:RemoveTag(tag)
				end
				tagToFrame[tag] = nil
			end)
			tagToFrame[tag] = frame
		end

		local currentWidget
		local insert = PluginUI.createInsertButton(dropdown.Backdrop.DropDownHeader, function()
			if currentWidget then
				currentWidget:Destroy()
			end
			local tagBox = PluginUI.createAttributeBox(nil, "Tag", "")
			local widget, cancel, confirm, close = PluginUI.createCreatorWidget("Add Tag", {
				tagBox
			})
			currentWidget = widget
			Manager.Style:Link(widget)

			cancel:OnTrigger(function()
				close()
			end)
			confirm:OnTrigger(function()
				local tag = tagBox:Get()
				for _, inst in instances do
					inst:AddTag(tag)
				end
				close()
			end)
		end)

		local commonTags = {}
		if #instances > 0 then
			local firstInstanceTags = instances[1]:GetTags()
			for _, tag in firstInstanceTags do
				local isCommon = true
				for i = 2, #instances do
					if not table.find(instances[i]:GetTags(), tag) then
						isCommon = false
						break
					end
				end
				if isCommon then
					table.insert(commonTags, tag)
				end
			end
		end

		for _, tag in commonTags do
			createTagFrame(tag)
		end

		for _, inst in instances do
			table.insert(instanceConnections, Manager.onTagAdded(inst, function(tag)
				local isCommon = true
				for _, otherInst in instances do
					if not table.find(otherInst:GetTags(), tag) then
						isCommon = false
						break
					end
				end
				if isCommon and not tagToFrame[tag] then
					createTagFrame(tag)
				end
			end))
			table.insert(instanceConnections, Manager.onTagRemoved(inst, function(tag)
				local frame = tagToFrame[tag]
				if frame then
					frame:Destroy()
					tagToFrame[tag] = nil
				end
			end))
		end

		dropdown:Show()
	end

	for _, v in dropdowns do
		list:AddItem(v.Backdrop)
	end
	debug.profileend()
end

function PropertyEditor.init(_m)
	init = true
	Manager = _m

	local state = Manager.WidgetState
	local components = Manager.Components

	local jexplorerUi = state.JExplorerUI
	local topbar = jexplorerUi.Topbar

	local connections = state.Connections
	local instances = connections:extend(cleanup.destroy)

	local toggleButton = Manager.Toolbar:CreateButton(
		getId("ToggleProperties"), "Toggle the properties widget visibility",
		"rbxassetid://106850260964317", "Properties"
	)
	toggleButton.ClickableWhenViewportHidden = true

	local widget, mainframe = PluginUI.createEditorWidget(
		UITemplates.EditorFrame(), "PropertyEditor", "",
		DockWidgetPluginGuiInfo.new(
			Enum.InitialDockState.Right,
			false,
			false
		)
	)

	widget.ZIndexBehavior = Enum.ZIndexBehavior.Sibling
	widget.Title = "JProperties"

	PropertyEditor.Widget = widget
	Manager.Style:Link(widget)

	toggleButton.Click:Connect(function()
		widget.Enabled = not widget.Enabled
	end)

	local list = PluginUI.createItemListFromExisting(mainframe, mainframe.Items.Content, mainframe.Items)
	PropertyEditor.PropertyList = list

	local setNilMenu = plugin:CreatePluginMenu(
		getId("Jexplorer_PropertyEditor_ContextMenu_SetNil", true)
	)
	local action = setNilMenu:AddNewAction(
		getId("Jexplorer_PropertyEditor_ContextMenu_SetNilAction", true), "Set to Nil"
	) :: PluginAction
	action.Triggered:Connect(function()
		local currentTarget = PropertyEditor.CurrentSetNilTarget
		if not currentTarget then return end

		for _, inst in currentTarget.instances do
			rawnewindex(inst, currentTarget.name, nil)
		end
	end)

	PropertyEditor.SetNilMenu = setNilMenu

	connections:extend(Manager.CleanupFns.call, {
		function()
			for _, v in instanceConnections do
				v:Disconnect()
			end
		end,
	})
	connections:add(widget.PluginDragDropped:Connect(function(dragData)
		local info = Manager.unpackDragInfo(dragData)
		if info.Type == "setproperty" then
			local instances, propName = info.TargetInstances, info.PropertyName
			local id = ChangeHistoryService:TryBeginRecording("Set Instance", "Set Instance")
			for _, inst in instances do
				rawnewindex(inst, propName, nil)
			end
			if id then
				ChangeHistoryService:FinishRecording(id, Enum.FinishRecordingOperation.Commit)
			end
		end
	end))
	connections:add(mainframe.InputBegan:Connect(function()
		PluginUI.clearOpenEnumLists()
	end))
end

function PropertyEditor.clearFramePools()
	for frame, metadata in frameMetadata do
		if metadata.connections then
			for _, conn in metadata.connections do
				conn:Disconnect()
			end
		end
	end
	for propType, pool in framePool do
		for _, frame in pool do
			if frame.frame.Instance then
				frame.frame.Instance:Destroy()
			end
		end
	end

	table.clear(framePool)
	table.clear(frameMetadata)
end

return PropertyEditor


--!strict
--!optimize 2

local plugin = script:FindFirstAncestorOfClass("Plugin")

local main = script.Parent.Parent
local shared = main.Shared

local UITemplates = require(shared.UITemplates)
local Types = require(main.Types)
local PluginUI = require(shared.PluginUI)
local Style = require(shared.Style)
local DefaultSettings = require(shared.Defaults.DefaultSettings)
local JDF = require(shared.UDF)

local getId = PluginUI.getId

local init = false
local SettingEditor = {
	Configs = {}
} :: Types.SettingEditor

local settingNameToData = {}
local settingNameToFrame = {}

function SettingEditor.getSetting(Manager, name)
	if not init then return nil end
	local configs = SettingEditor.Configs

	local value = configs[name]
	if type(value) == "table" and value.__stylevar then
		value = Style.eval(value.__expr, Manager.Style.Tokens)
	end
	return value
end

function SettingEditor.setSetting(Manager, name, value)
	if not init then return nil end
	local configs = SettingEditor.Configs
	configs[name] = value

	local frame = settingNameToFrame[name]
	if frame then
		local data = settingNameToData[name]
		if data and data.Type == "boolean" then
			frame:Set(value)
		else
			frame:Set(JDF.to(value))
		end
	end

	local data = settingNameToData[name]
	if data and data.OnChange then
		data.OnChange(Manager, value)
	end
end

function SettingEditor.createSetting(list, settingName, displayName, valueType, defaultValue, onChange)
	if not init then return end
	local configs = SettingEditor.Configs

	configs[settingName] = defaultValue
	if valueType == "boolean" then
		local box = PluginUI.createCheckBox(list, displayName, defaultValue, function(newValue)
			assert(typeof(newValue) == valueType)
			configs[settingName] = newValue

			if not onChange then return end
			onChange(newValue)
		end)
		settingNameToFrame[settingName] = box
	else
		local box
		box = PluginUI.createAttributeBox(list, displayName, JDF.to(defaultValue), function(newValue)
			local realVal = JDF.from(newValue)
			if not (type(realVal) == "table" and realVal.__stylevar) and (typeof(realVal) ~= valueType) then
				configs[settingName] = defaultValue
				box:Set(JDF.to(defaultValue))
				return
			end
			configs[settingName] = realVal

			if not onChange then return end
			onChange(realVal)
		end)
		settingNameToFrame[settingName] = box
	end
end

function SettingEditor.init(Manager)
	init = true
	assert(SettingEditor.Configs, "configs must be initialized by manager")

	local state = Manager.WidgetState
	local components = Manager.Components
	local jexplorerUi = state.JExplorerUI
	local connections = state.Connections

	local widget, mainframe = PluginUI.createEditorWidget(UITemplates.EditorFrame(), "SettingEditor", "Setting Editor")
	SettingEditor.Widget = widget
	Manager.Style:Link(widget)

	local createSetting = SettingEditor.createSetting
	local behaviors = {}
	for name in DefaultSettings do
		behaviors[name] = PluginUI.createDropDownItemList({
			Name = name,
			Visible = true,
			Parent = mainframe.Items.Content
		}, name)
	end

	for behaviorName, list in DefaultSettings do
		local behavior = behaviors[behaviorName]
		for name, data in list do
			local existingValue = SettingEditor.Configs[name]
			settingNameToData[name] = data
			createSetting(
				behavior,
				name,
				data.Display,
				data.Type,
				existingValue or data.Default,
				data.OnChange ~= nil and function(newValue)
					data.OnChange(Manager, newValue)
				end or nil
			)
		end
	end
end

return SettingEditor


--!strict
--!optimize 2

local plugin = script:FindFirstAncestorOfClass("Plugin")

local main = script.Parent.Parent

local shared = main.Shared

local PluginUI = require(shared.PluginUI)
local UITemplates = require(shared.UITemplates)
local ItemGroup = require(shared.Vendor.ItemGroup)
local SavedState = require(shared.SavedState)
local Style = require(shared.Style)
local Constants = require(main.Constants)
local cleanup = ItemGroup.cleanup

local DefaultStyle = require(shared.Defaults.DefaultStyle)
local DefaultSettings = require(shared.Defaults.DefaultSettings)
local DefaultInvisible = require(shared.Defaults.DefaultInvisibleClasses)
local DefaultOrder = require(shared.Defaults.DefaultOrder)

local OrderEditor = require("./OrderEditor")
local VersionControl = require("./VersionControl")
local InstanceVisibilityEditor = require("./InstanceVisibilityEditor")

local Types = require(main.Types)

local PRINT_SAVEDSTATE_PREFIX = "SavedState: "

local StateEditor = {} :: Types.StateEditor

local function getPrintableTable(tbl): {[any]: any}
	local newtbl = {}
	for k, v in tbl do
		if typeof(v) == "table" then
			if #v > 10 then
				v = "Array"
			else
				v = getPrintableTable(v)
			end
		end
		newtbl[k] = v
	end
	return newtbl
end

function StateEditor.init(Manager)
	local state = Manager.WidgetState
	local components = Manager.Components

	local jexplorerUi = state.JExplorerUI
	local topbar = jexplorerUi.Topbar

	local world = state.World

	local connections = state.Connections
	local instances = connections:extend(cleanup.destroy)

	local widget, mainframe = PluginUI.createEditorWidget(UITemplates.EditorFrame(), "StateEditor", "State Editor")
	StateEditor.Widget = widget
	Manager.Style:Link(widget)

	local function resetStyles()
		local customRules = {}
		for _, v in Manager.Style:GetWithoutTags("Default") do
			table.insert(customRules, v)
		end
		local tokens, rules = DefaultStyle(Manager)
		local style = Style.fromRulesTokens(rules, tokens)
		for _, token in style.Tokens do
			token:AddTag(Constants.DefaultTokenTag)
		end

		style:AddRules(customRules)
		Manager.Style:Replace(style)

		local style = Manager.Style
		style:UpdateTokens()

		Manager.VirtualScroller.update(Manager, false)
	end
	local function resetSettings()
		for _, t in DefaultSettings do
			for k, v in t do
				Manager.setSetting(k, v.Default)
			end
		end

		Manager.VirtualScroller.update(Manager, false, true)
	end
	local function resetOrder()
		OrderEditor.ClassPriorities = DefaultOrder(Manager)
		OrderEditor.DefaultPriority = 999

		Manager.DeferredTaskScheduler.ChildGraphDirty = true
	end
	local function resetVersions()
		VersionControl.Versions = {}
	end
	local function resetTabs()
		for _, v in table.clone(state.Tabs) do
			v.Remove()
		end
		table.clear(state.Tabs)
	end
	local function resetIconColors()
		for entity in world:query(components.FrameIconColor) do
			world:remove(entity, components.FrameIconColor)
			local frame = world:get(entity, components.Frame)
			if frame then
				Manager.ExplorerFrame.setExplorerFrameIconColor(Manager, frame, nil)
			end
		end
	end
	local function resetRepoProxies()
		for _, v in table.clone(state.RepoProxies) do
			v.Remove()
		end
		table.clear(state.RepoProxies)
	end
	local function resetInvisible()
		local tbl = {}
		for _, class in DefaultInvisible do
			InstanceVisibilityEditor.setInvisible(class, true)
			tbl[class] = true
		end
		InstanceVisibilityEditor.InvisibleClasses = tbl
		task.wait()
		Manager.VirtualScroller.rebuildAndUpdate(Manager, false)
	end

	local mainList = PluginUI.createItemListFromExisting(mainframe, mainframe.Items.Content, mainframe.Items)

	local function onsaved()
		print(PRINT_SAVEDSTATE_PREFIX, getPrintableTable(SavedState.load(Manager)))
	end

	local function onload()
		print(PRINT_SAVEDSTATE_PREFIX, getPrintableTable(state.SavedState))
	end

	PluginUI.createHeader(mainList, `Latest DataVersion: {SavedState.data_version}`)
	PluginUI.createButton(mainList, "Save State", function()
		local saver = Manager.saveState()
		saver.all()
		saver.save()

		onsaved()
		widget.Enabled = false
	end)
	PluginUI.createButton(mainList, "Load State", function()
		local loaders = Manager.loadState(SavedState.load(Manager))
		for _, v: any in loaders do v() end

		onload()
		widget.Enabled = false
	end)
	PluginUI.createButton(mainList, "Reset All To Default", function()
		resetStyles()
		resetSettings()
		resetOrder()
		resetVersions()
		resetIconColors()
		resetTabs()
		resetRepoProxies()
		resetInvisible()
	end)

	do
		local list = PluginUI.createDropDownItemList({
			Visible = true
		}, "SavedState: Styles")
		list:Parent(mainList)

		PluginUI.createButton(list, "Reset To Default", function()
			resetStyles()
			widget.Enabled = false
		end)
		PluginUI.createButton(list, "Save", function()
			local saver = Manager.saveState()
			saver.style()
			saver.save()

			onsaved()
			widget.Enabled = false
		end)
		PluginUI.createButton(list, "Load", function()
			local loaders = Manager.loadState(SavedState.load(Manager))
			Manager.Style:Replace(loaders.style())

			onload()
			widget.Enabled = false
		end)
	end
	do
		local list = PluginUI.createDropDownItemList({
			Visible = true
		}, "SavedState: Settings")
		list:Parent(mainList)

		PluginUI.createButton(list, "Reset To Default", function()
			resetSettings()
			widget.Enabled = false
		end)
		PluginUI.createButton(list, "Save", function()
			local saver = Manager.saveState()
			saver.settings()
			saver.save()

			onsaved()
			widget.Enabled = false
		end)
		PluginUI.createButton(list, "Load", function()
			local loaders = Manager.loadState(SavedState.load(Manager))
			loaders.settings()

			onload()
			widget.Enabled = false
		end)
	end
	do
		local list = PluginUI.createDropDownItemList({
			Visible = true
		}, "SavedState: Sorting Order")
		list:Parent(mainList)

		PluginUI.createButton(list, "Reset To Default", function()
			resetOrder()
			widget.Enabled = false
		end)
		PluginUI.createButton(list, "Save", function()
			local saver = Manager.saveState()
			saver.order()
			saver.save()

			onsaved()
			widget.Enabled = false
		end)
		PluginUI.createButton(list, "Load", function()
			local loaders = Manager.loadState(SavedState.load(Manager))
			loaders.order()

			onload()
			widget.Enabled = false
		end)
	end
	do
		local list = PluginUI.createDropDownItemList({
			Visible = true
		}, "SavedState: Versions")
		list:Parent(mainList)

		PluginUI.createButton(list, "Reset To Default", function()
			resetVersions()
			widget.Enabled = false
		end)
		PluginUI.createButton(list, "Save", function()
			local saver = Manager.saveState()
			saver.versions()
			saver.save()

			onsaved()
			widget.Enabled = false
		end)
		PluginUI.createButton(list, "Load", function()
			local loaders = Manager.loadState(SavedState.load(Manager))
			VersionControl.Versions = loaders.versions()

			onload()
			widget.Enabled = false
		end)
	end
	do
		local list = PluginUI.createDropDownItemList({
			Visible = true
		}, "SavedState: Icon Colors")
		list:Parent(mainList)

		PluginUI.createButton(list, "Reset To Default", function()
			resetIconColors()
			widget.Enabled = false
		end)
		PluginUI.createButton(list, "Save", function()
			local saver = Manager.saveState()
			saver.icon_colors()
			saver.save()

			onsaved()
			widget.Enabled = false
		end)
		PluginUI.createButton(list, "Load", function()
			local loaders = Manager.loadState(SavedState.load(Manager))
			VersionControl.Versions = loaders.icon_colors()

			onload()
			widget.Enabled = false
		end)
	end
	do
		local list = PluginUI.createDropDownItemList({
			Visible = true
		}, "SavedState: Tabs")
		list:Parent(mainList)

		PluginUI.createButton(list, "Reset To Default", function()
			resetTabs()
			widget.Enabled = false
		end)
		PluginUI.createButton(list, "Save", function()
			local saver = Manager.saveState()
			saver.tabs()
			saver.save()

			onsaved()
			widget.Enabled = false
		end)
		PluginUI.createButton(list, "Load", function()
			local loaders = Manager.loadState(SavedState.load(Manager))
			VersionControl.Versions = loaders.tabs()

			onload()
			widget.Enabled = false
		end)
	end
	do
		local list = PluginUI.createDropDownItemList({
			Visible = true
		}, "SavedState: Repo Proxies")
		list:Parent(mainList)

		PluginUI.createButton(list, "Reset To Default", function()
			resetRepoProxies()
			widget.Enabled = false
		end)
		PluginUI.createButton(list, "Save", function()
			local saver = Manager.saveState()
			saver.repo_proxies()
			saver.save()

			onsaved()
			widget.Enabled = false
		end)
		PluginUI.createButton(list, "Load", function()
			local loaders = Manager.loadState(SavedState.load(Manager))
			VersionControl.Versions = loaders.repo_proxies()

			onload()
			widget.Enabled = false
		end)
	end
	do
		local list = PluginUI.createDropDownItemList({
			Visible = true
		}, "SavedState: Invisible Classes")
		list:Parent(mainList)

		PluginUI.createButton(list, "Reset To Default", function()
			resetInvisible()
			widget.Enabled = false
		end)
		PluginUI.createButton(list, "Save", function()
			local saver = Manager.saveState()
			saver.invisible()
			saver.save()

			onsaved()
			widget.Enabled = false
		end)
		PluginUI.createButton(list, "Load", function()
			local loaders = Manager.loadState(SavedState.load(Manager))
			VersionControl.Versions = loaders.invisible()

			onload()
			widget.Enabled = false
		end)
	end
end

return StateEditor


--!strict
--!optimize 2

local plugin = script:FindFirstAncestorOfClass("Plugin")

local StudioService = game:GetService('StudioService')

local main = script.Parent.Parent

local shared = main.Shared

local UITemplates = require(shared.UITemplates)
local Types = require(main.Types)

local PluginUI = require(shared.PluginUI)
local Style = require(shared.Style)
local UDF = require(shared.UDF)
local Selector = require(shared.Small.Selector)
local RawInst = require(shared.Small.RawInst)
local ItemGroup = require(shared.Vendor.ItemGroup)
local xyz = require(shared.Small.xyz)
local cleanup = ItemGroup.cleanup
local Constants = require(main.Constants)

local getId = PluginUI.getId
local rule = Style.rule

local ColorPicker = require(shared.Vendor.PluginColorPicker)

local index = RawInst.index
local newindex = RawInst.newindex

local studioSettings = settings().Studio

local init = false
local StyleEditor = {} :: Types.StyleEditor

function StyleEditor.init(Manager)
	init = true
	local sheet = Manager.Style
	local state = Manager.WidgetState
	local components = Manager.Components

	local jexplorerUi = state.JExplorerUI
	local topbar = jexplorerUi.Topbar

	local connections = state.Connections
	local widget, mainframe = PluginUI.createEditorWidget(UITemplates.EditorFrame(), "StyleEditor", "Style Editor")
	StyleEditor.Widget = widget
	sheet:Link(widget)

	if not Constants.EditorsEnabled then return end
	local function getTokensFromSheetTokens(sheetTokens: {[string]: Style.StyleToken})
		local tokens = {}
		for k, token in sheet.Tokens do
			tokens[k] = token.RawValue
		end
		return tokens
	end
	local function resolve(tokens: {[string]: any}, v: any): Color3
		if typeof(v) == "Color3" then
			return v
		elseif typeof(v) == "string" and v:sub(1, 1) == "$" then
			local value = Style.eval(v, tokens)
			return resolve(tokens, value)
		elseif typeof(v) == "table" and v.__styletoken then
			return resolve(tokens, v.RawValue)
		elseif typeof(v) == "table" and v.__stylevar then
			local value = Style.eval(v.__expr, tokens)
			return resolve(tokens, value)
		else
			error(`'{v}' cannot be converted into Color3`)
		end
	end

	local currentResetTarget: {
		box: TextBox,
		inst: any,
		name: string,
		defaultValue: any,
		type: "ruleProperty" | "styledProperty" | "token"
	}?
	local stylePropertyResetMenu do
		stylePropertyResetMenu = plugin:CreatePluginMenu(
			getId("Jexplorer_StylePropertyResetMenu", true)
		)

		local reset = stylePropertyResetMenu:AddNewAction(
			getId("Jexplorer_ContextMenu_StylePropertyResetToDefault", true), "Reset to Default"
		) :: PluginAction
		reset.Triggered:Connect(function()
			if not currentResetTarget then return end
			if currentResetTarget.type == "styledProperty" then
				currentResetTarget.box.Text = UDF.to(currentResetTarget.defaultValue)
				currentResetTarget.inst:SetProperty(currentResetTarget.name, currentResetTarget.defaultValue)
			elseif currentResetTarget.type == "ruleProperty" then
				currentResetTarget.box.Text = UDF.to(currentResetTarget.defaultValue)
				newindex(
					currentResetTarget.inst,
					currentResetTarget.name,
					currentResetTarget.defaultValue
				)
			elseif currentResetTarget.type == "token" then
				currentResetTarget.box.Text = UDF.to(currentResetTarget.defaultValue)
				currentResetTarget.inst:SetToken(currentResetTarget.name, currentResetTarget.defaultValue, true)
			end
			currentResetTarget = nil
		end)
	end

	--local warningHeader = PluginUI.createHeader(nil, "WARNING: Styles are currently not saved")
	--warningHeader.Parent = mainframe.Items.Content

	local rulesDropdown = PluginUI.createDropDownItemList(nil, "StyleRules")
	local tokensDropdown = PluginUI.createDropDownItemList(nil, "Tokens")
	rulesDropdown.Backdrop.Visible = true
	tokensDropdown.Backdrop.Visible = true

	-- WARNING: YOU HAVE TO PASS A RULE IF K IS "Image" OR "Texture"
	local function createStylePropertyBox(dropdown: any, k: string, defaultValue: any, callback: ((string) -> ())?, rule: Style.Rule?)
		local defaultStr = UDF.to(defaultValue)
		local box
		box = PluginUI.createAttributeBox(dropdown, k, defaultStr, callback or function(newValue)
			assert(rule, "unexpected error: style rule not found while creating style property box")

			local current: Style.Rule? = rule
			while current do
				current:RemoveTag("DontSaveRule")
				current = current.ParentRule
			end

			if newValue == defaultStr then
				rule:SetProperty(k, defaultValue)
				sheet:UpdateTokens()
				return
			end
			local value = UDF.from(newValue)
			rule:SetProperty(k, value)
			sheet:UpdateTokens()
		end)

		local conn2
		if k == "Image" or k == "Texture" then
			local importFromFileButton = PluginUI.createSideButtonImage("rbxassetid://13544220123", Vector2.zero, Vector2.zero, box.Instance.ValueBox)
			importFromFileButton:AddTag("JEXP_MonochromeImage")
			importFromFileButton.BackgroundTransparency = 1

			importFromFileButton.Activated:Connect(function()
				local file = StudioService:PromptImportFile({"png", "jpg", "jpeg"}) :: File?
				if not file then return end
				assert(rule)

				box:Set(file:GetTemporaryId())
				rule:SetProperty(k, box:Get())
			end)
		elseif rule and (k == "BackgroundColor3" or k == "TextColor3" or k == "ImageColor3" or k == "Color" or k == "PlaceholderTextColor3") then
			local chooseColorButton = PluginUI.createSideButtonImage("", Vector2.zero, Vector2.zero, box.Instance.ValueBox)
			chooseColorButton:AddTag("JEXP_Border")
			chooseColorButton.BackgroundColor3 = resolve(rule:GetTokens(), defaultValue)

			local function colorChanged(newColor: Color3)
				assert(rule)

				rule:SetProperty(k, newColor)
				box.Instance.ValueBox.Text = UDF.to(newColor)
				chooseColorButton.BackgroundColor3 = newColor
			end

			local last
			chooseColorButton.Activated:Connect(function()
				assert(rule)
				if last then
					last:Close()
					last:Destroy()
				end
				local picker = ColorPicker.new(plugin, getId("StyleColorPicker", true))
				last = picker

				picker:Show()
				local originalColor = resolve(rule:GetTokens(), rule:GetProperty(k))
				picker:Set(originalColor)

				picker:BindToChange(colorChanged)
				picker:BindToCancel(function()
					colorChanged(originalColor)
					picker:Close()
					picker:Destroy()
				end)
				picker:BindToConfirm(function()
					picker:Close()
					picker:Destroy()
				end)
			end)

			if rule then
				conn2 = rule.AppliedPropertyChanged:Connect(function(name, v)
					if name ~= k then return end
					chooseColorButton.BackgroundColor3 = resolve(rule:GetTokens(), v)
				end)
			end
		end

		if rule then
			local conn = rule.PropertyChanged:Connect(function(name, newValue)
				if name ~= k then return end
				box.Instance.ValueBox.Text = UDF.to(newValue)
				local current: Style.Rule? = rule
				while current do
					current:RemoveTag("DontSaveRule")
					current = current.ParentRule
				end
			end);
			(box.Instance :: GuiObject).InputBegan:Connect(function(input: InputObject)
				if input.UserInputState ~= Enum.UserInputState.Begin then return end
				if input.UserInputType ~= Enum.UserInputType.MouseButton2 then return end
				currentResetTarget = {
					box = box.Instance.ValueBox,
					inst = rule,
					name = k,
					defaultValue = defaultValue,
					type = "styledProperty"
				}
				stylePropertyResetMenu:ShowAsync()
			end)
			local button = PluginUI.createDeleteButton(box.Instance, function()
				rule:SetProperty(k, nil)
				conn:Disconnect()
				if conn2 then
					conn2:Disconnect()
				end
				box:Destroy()
			end)
			button.ZIndex = 10
			button.AnchorPoint = Vector2.new(1, 0)
			button.Position = UDim2.fromScale(1, 0)
		end

		return box
	end

	local currentCreatingRule: Style.Rule?, creatingDropdown: any?
	local function showCreateStylePropertyMenu()
		local nameBox = PluginUI.createAttributeBox(nil, "Name", "")
		local defaultValueBox = PluginUI.createAttributeBox(nil, "Default Value", "")

		local widget, cancel, confirm, close = PluginUI.createCreatorWidget("Create Style Property", {
			nameBox,
			defaultValueBox
		})
		Manager.Style:Link(widget)

		cancel:OnTrigger(function()
			close()
		end)
		confirm:OnTrigger(function()
			if not currentCreatingRule then
				close()
				return
			end
			local name = nameBox:Get()
			local defaultValue = UDF.from(defaultValueBox:Get())

			if not defaultValue then return end

			currentCreatingRule:SetProperty(name, defaultValue)
			createStylePropertyBox(creatingDropdown, name, defaultValue, nil, currentCreatingRule)
			currentCreatingRule = nil

			close()
		end)
	end

	local function showCreateNestedStyleRuleMenu()
		local selectorBox = PluginUI.createAttributeBox(nil, "Selector", "")
		local priorityBox = PluginUI.createAttributeBox(nil, "Priority", "1")

		local widget, cancel, confirm, close = PluginUI.createCreatorWidget("Create Child Style Rule", {
			selectorBox,
			priorityBox
		})
		Manager.Style:Link(widget)

		cancel:OnTrigger(function()
			close()
		end)
		confirm:OnTrigger(function()
			if not currentCreatingRule or not creatingDropdown then
				close()
				return
			end

			local selector = selectorBox:Get()
			local priority = tonumber(priorityBox:Get())
			assert(priority, "stylerule priority must be a number")

			local defaultProperties = {}
			-- Macro
			local newSelector, macroType = Selector.parseSelectorMacro(selector)
			if newSelector then selector = newSelector end
			if macroType == "icon" then
				-- WARNING: THE PRESENCE OF IMAGERECTOFFSET AND IMAGERECTSIZE IS VERY IMPORTANT TO ENSURE THIS STYLE GETS APPLIED, IDK WHY IT DOESNT WORK OTHERWISE
				defaultProperties = {
					Image = "",
					ImageRectOffset = Vector2.zero,
					ImageRectSize = Vector2.zero
				}
			end

			local styleRule = rule("CustomStyle", selector, priority, defaultProperties)

			currentCreatingRule:AddRules({styleRule})
			-- Handled in ParentRule.RuleAdded
			--dropdown:Parent(creatingDropdown)

			close()
		end)
	end

	local function createStyleRuleDropdown(rule: Style.Rule, parentDropdown: PluginUI.ItemList?)
		if rule:GetName() == nil then
			rule:SetName("StyleRule")
		end
		local dropdown = PluginUI.createDropDownItemList(nil, rule:GetName(), true)
		dropdown.Backdrop.Visible = true
		dropdown.Backdrop.DropDownHeader.HeaderLabel.FocusLost:Connect(function(enterPressed)
			if not enterPressed then return end
			rule:SetName(dropdown.Backdrop.DropDownHeader.HeaderLabel.Text)
		end)
		local boxes = {}
		local defaultSelector = rule.Rule.Selector
		local defaultPriority = rule.Rule.Priority
		local box
		box = createStylePropertyBox(dropdown, "StyleRule.Selector", defaultSelector, function(newValue)
			local newSelector, macroType = Selector.parseSelectorMacro(UDF.from(newValue))
			if newSelector then
				box.Instance.ValueBox.Text = UDF.to(newSelector)
				rule.Rule.Selector = newSelector
			else
				rule.Rule.Selector = UDF.from(newValue)
			end
		end)
		table.insert(boxes, box.Instance)
		box.Instance.ValueBox.InputBegan:Connect(function(input: InputObject)
			if input.UserInputState ~= Enum.UserInputState.Begin then return end
			if input.UserInputType ~= Enum.UserInputType.MouseButton2 then return end
			currentResetTarget = {
				box = box.Instance.ValueBox,
				inst = rule,
				name = "Selector",
				defaultValue = defaultSelector,
				type = "ruleProperty"
			}
			stylePropertyResetMenu:ShowAsync()
		end)
		local box
		box = createStylePropertyBox(dropdown, "StyleRule.Priority", defaultPriority, function(newValue)
			--if newValue == "" then
			--	rule.Priority = defaultPriority
			--	box.ValueBox.Text = UDF.to(defaultPriority)
			--	return
			--end
			local newValue = UDF.from(newValue)
			assert(type(newValue) == "number", "number expected for stylerule.priority")
			rule.Rule.Priority = newValue
		end)
		table.insert(boxes, box.Instance)
		box.Instance.ValueBox.InputBegan:Connect(function(input: InputObject)
			if input.UserInputState ~= Enum.UserInputState.Begin then return end
			if input.UserInputType ~= Enum.UserInputType.MouseButton2 then return end
			currentResetTarget = {
				box = box.Instance.ValueBox,
				inst = rule,
				name = "Priority",
				defaultValue = defaultPriority,
				type = "ruleProperty"
			}
			stylePropertyResetMenu:ShowAsync()
		end)

		for k, v in rule.RawProperties do
			local defaultValue = v
			local box = createStylePropertyBox(nil, k, defaultValue, nil, rule)
			table.insert(boxes, box.Instance)
		end
		local dropdowns = {}
		for _, childRule in rule.ChildRules do
			table.insert(dropdowns, createStyleRuleDropdown(childRule, nil).Backdrop)
		end

		dropdown:AddBulkItem(dropdowns)
		local header = dropdown.Backdrop:FindFirstChild("DropDownHeader")

		PluginUI.createInsertButton(header, function()
			currentCreatingRule = rule
			creatingDropdown = dropdown
			showCreateStylePropertyMenu()
		end)

		local createRulePos = UDim2.fromScale(0.85, 0.5)
		if not rule:HasTag("Default") then
			PluginUI.createDeleteButton(header, function()
				rule:Destroy()
				dropdown:Destroy()
			end)
			createRulePos = UDim2.fromScale(0.8, 0.5)
		end
		local createRule = PluginUI.createInsertButtonWithImage(header, "rbxassetid://85474028895485", function()
			currentCreatingRule = rule
			creatingDropdown = dropdown
			showCreateNestedStyleRuleMenu()
		end)
		createRule.Position = createRulePos

		rule.Rule.Destroying:Connect(function()
			dropdown:Destroy()
		end)
		rule.RuleAdded:Connect(function(childRule)
			createStyleRuleDropdown(childRule, dropdown)
		end)

		dropdown:AddBulkItem(boxes)
		if parentDropdown then
			parentDropdown:AddItem(dropdown.Backdrop)
		end

		return dropdown
	end

	local function showCreateTopStyleRuleMenu()
		local selectorBox = PluginUI.createAttributeBox(nil, "Selector", "")
		local priorityBox = PluginUI.createAttributeBox(nil, "Priority", "1")

		local widget, cancel, confirm, close = PluginUI.createCreatorWidget("Create Style Rule", {
			selectorBox,
			priorityBox
		})
		Manager.Style:Link(widget)

		cancel:OnTrigger(function()
			close()
		end)
		confirm:OnTrigger(function()
			local selector = selectorBox:Get()
			local priority = tonumber(priorityBox:Get())
			assert(priority, "stylerule priority must be a number")

			local defaultProperties = {}
			-- Macro
			local newSelector, macroType = Selector.parseSelectorMacro(selector)
			if newSelector then selector = newSelector end
			if macroType == "icon" then
				-- WARNING: THE PRESENCE OF IMAGERECTOFFSET AND IMAGERECTSIZE IS VERY IMPORTANT TO ENSURE THIS STYLE GETS APPLIED, IDK WHY IT DOESNT WORK OTHERWISE
				defaultProperties = {
					Image = "",
					ImageRectOffset = Vector2.zero,
					ImageRectSize = Vector2.zero
				}
			end

			local styleRule = rule("CustomStyle", selector, priority, defaultProperties)
			sheet:AddRules {styleRule}

			-- Handled in the .RuleAdded connection
			--createStyleRuleDropdown(styleRule)

			close()
		end)
	end

	local function createTokenAttribute(k: string, defaultValue: any, defaultStr: string, createDeleteButton: boolean)
		local box
		box = PluginUI.createAttributeBox(nil, k, defaultStr, function(newValue)
			if newValue == defaultStr then
				sheet:SetToken(k, defaultValue, true)
				return
			end
			local value = UDF.from(newValue)
			if not value then
				box.Instance.ValueBox.Text = defaultStr
				return
			end
			sheet:SetToken(k, value, true)
		end)

		local conn = sheet.TokenChanged:Connect(function(name, newValue)
			if name ~= k then return end
			if not newValue then return end
			box.Instance.ValueBox.Text = UDF.to(newValue.RawValue)
		end)
		local conn2
		box.Instance.ValueBox.InputBegan:Connect(function(input: InputObject)
			if input.UserInputState ~= Enum.UserInputState.Begin then return end
			if input.UserInputType ~= Enum.UserInputType.MouseButton2 then return end
			currentResetTarget = {
				box = box.Instance.ValueBox,
				inst = sheet,
				name = k,
				defaultValue = defaultValue,
				type = "token"
			}
			stylePropertyResetMenu:ShowAsync()
		end)

		if createDeleteButton then
			local button = PluginUI.createDeleteButton(box.Instance, function()
				sheet:SetToken(k, nil, true)
				conn:Disconnect()
				if conn2 then
					conn2:Disconnect()
				end
				box:Destroy()
			end)
			button.ZIndex = 10
			button.AnchorPoint = Vector2.new(1, 0)
			button.Position = UDim2.fromScale(1, 0)
		end

		local function iscolor(v: any): boolean
			local success, result = pcall(resolve, getTokensFromSheetTokens(sheet.Tokens), v)
			return success and typeof(result) == "Color3"
		end
		if iscolor(defaultValue) then
			local chooseColorButton = PluginUI.createSideButtonImage("", Vector2.zero, Vector2.zero, box.Instance.ValueBox)
			chooseColorButton:AddTag("JEXP_Border")
			chooseColorButton.BackgroundColor3 = resolve(getTokensFromSheetTokens(sheet.Tokens), defaultValue)

			local function colorChanged(newColor: Color3)
				assert(rule)

				sheet:SetToken(k, Style.token(newColor), true)
				box.Instance.ValueBox.Text = UDF.to(newColor)
				chooseColorButton.BackgroundColor3 = newColor
			end

			local last
			chooseColorButton.Activated:Connect(function()
				assert(rule)
				if last then
					last:Close()
					last:Destroy()
				end
				local picker = ColorPicker.new(plugin, getId("StyleColorPicker", true))
				last = picker

				picker:Show()
				local originalColor = resolve(getTokensFromSheetTokens(sheet.Tokens), sheet:GetToken(k))
				picker:Set(originalColor)

				picker:BindToChange(colorChanged)
				picker:BindToCancel(function()
					colorChanged(originalColor)
					picker:Close()
					picker:Destroy()
				end)
				picker:BindToConfirm(function()
					picker:Close()
					picker:Destroy()
				end)
			end)

			conn2 = sheet.AppliedTokenChanged:Connect(function(name, v)
				if name ~= k then return end
				chooseColorButton.BackgroundColor3 = resolve(getTokensFromSheetTokens(sheet.Tokens), v)
			end)
		end

		return box
	end

	local function showCreateTokenMenu()
		local nameBox = PluginUI.createAttributeBox(nil, "Name", "")
		local defaultValueBox = PluginUI.createAttributeBox(nil, "Default Value", "")

		local widget, cancel, confirm, close = PluginUI.createCreatorWidget("Create Token", {
			nameBox,
			defaultValueBox
		})
		Manager.Style:Link(widget)

		cancel:OnTrigger(function()
			close()
		end)
		confirm:OnTrigger(function()
			local name = nameBox:Get()
			local defaultStr = defaultValueBox:Get()
			local defaultValue = UDF.from(defaultStr)
			sheet:SetToken(name, defaultValue, true)

			local box = createTokenAttribute(name, defaultValue, defaultStr, true)
			tokensDropdown:AddItem(box.Instance)

			close()
		end)
	end

	tokensDropdown:SetText("Loading, please wait...")
	rulesDropdown:SetText("Loading, please wait...")
	connections:extend(task.cancel, {
		task.spawn(function()
			local tokens = {}
			for k, v in sheet.Tokens do
				local defaultValue = v.RawValue
				local defaultStr = UDF.to(v.RawValue)

				table.insert(tokens, createTokenAttribute(k, defaultValue, defaultStr, not v:HasTag(Constants.DefaultTokenTag)).Instance)
				task.wait()
			end
			tokensDropdown:AddBulkItem(tokens)
			tokensDropdown:SetText("StyleTokens")
		end),

		task.spawn(function()
			local dropdowns = {}
			for _, rule in sheet.Rules do
				table.insert(dropdowns, createStyleRuleDropdown(rule).Backdrop)
				task.wait(0.1)
			end
			rulesDropdown:AddBulkItem(dropdowns)
			rulesDropdown:SetText("StyleRules")
		end)
	})

	local rulesHeader = rulesDropdown.Backdrop:FindFirstChild("DropDownHeader")

	PluginUI.createInsertButton(rulesHeader, function()
		showCreateTopStyleRuleMenu()
	end)

	local tokensHeader = tokensDropdown.Backdrop:FindFirstChild("DropDownHeader")

	PluginUI.createInsertButton(tokensHeader, function()
		showCreateTokenMenu()
	end)

	rulesDropdown.Backdrop.Parent = mainframe.Items.Content
	tokensDropdown.Backdrop.Parent = mainframe.Items.Content

	connections:add_many({
		sheet.RuleAdded:Connect(function(newRule)
			createStyleRuleDropdown(newRule, rulesDropdown)
		end) :: any,
	})

	local instances = connections:extend(cleanup.destroy)

	instances:add(stylePropertyResetMenu)
end

return StyleEditor


--!strict

local ChangeHistoryService = game:GetService("ChangeHistoryService")

local Types = require("../Types")

local ClassIcon = require("./Small/ClassIcon")
local RawInst = require("./Small/RawInst")
local Categories = require("./Info/InstanceCategories")

local getClampedXY = require("./Small/GetClampedXY")
local rawnewindex = RawInst.rawnewindex

local PluginUI = require("./PluginUI")

local function calculateMatchScore(text: string, search: string)
	if search == "" then return 1 end

	local lowerText = text:lower()
	local lowerSearch = search:lower()
	if lowerText == lowerSearch then
		return 1000
	end

	if lowerText:sub(1, #lowerSearch) == lowerSearch then
		return 100
	end

	if lowerText:find(lowerSearch, 1, true) then
		return 50
	end

	local textIndex = 1
	local searchIndex = 1
	local matches = 0

	while textIndex <= #lowerText and searchIndex <= #lowerSearch do
		if lowerText:sub(textIndex, textIndex) == lowerSearch:sub(searchIndex, searchIndex) then
			matches = matches + 1
			searchIndex = searchIndex + 1
		end
		textIndex = textIndex + 1
	end

	if matches == #lowerSearch then
		return 10 + matches
	end

	return 0
end

type buttonItem = Types.buttonItem
local menuInfo: Types.ClassSelectionMenuInfo
local ClassSelectionMenu = {} :: Types.ClassSelectionMenu

function ClassSelectionMenu.init(Manager)
	local components = Manager.Components
	local state = Manager.WidgetState
	local world = state.World
	local connections = state.Connections
	local jexplorerUi = state.JExplorerUI
	local topbar = jexplorerUi.Topbar

	-- Frequently used classes per scope: { [scopeName]: { className, ... } }
	local frequentByScope: { [string]: { string } } = {}
	local currentScope: string = "Default"
	local FREQUENT_CATEGORY = "Suggested"

	local classSelectList do
		classSelectList = PluginUI.createItemList {
			Name = "classSelectList"
		}
		local allButtons: {buttonItem} = {}
		local categories: {[string]: Frame} = {}
		local originalOrders: {
			buttons: {[string]: number},
			headers: {[string]: number}
		} = {
			buttons = {},
			headers = {}
		}
		local currentVisibleButtons: {buttonItem} = {}

		-- Holds instances created for the Frequently Used section so we can destroy on rebuild
		local frequentInstances: { Instance } = {}

		local topFrame = Instance.new("Frame")
		topFrame:AddTag("JEXP_Border")
		topFrame:AddTag("JEXP_Background")

		topFrame.Name = "TopFrame"
		topFrame.AnchorPoint = Vector2.new(0, 1)
		topFrame.BorderSizePixel = 0
		topFrame.Size = UDim2.new(1, 0, 0, 28)
		topFrame.Parent = classSelectList.Backdrop

		local searchbar = Instance.new("TextBox")
		searchbar:AddTag("JEXP_Border")
		searchbar:AddTag("JEXP_DarkBackground")
		searchbar:AddTag("JEXP_Text")

		searchbar.Name = "SearchBar"
		searchbar.AnchorPoint = Vector2.new(0, 0.5)
		searchbar.BorderSizePixel = 0
		searchbar.Position = UDim2.new(0.025, 0, 0.5, 0)
		searchbar.Size = UDim2.fromScale(0.8, 0.6)
		searchbar.Text = ""
		searchbar.TextSize = 8
		searchbar.PlaceholderText = "Search object..."
		searchbar.TextXAlignment = Enum.TextXAlignment.Left
		searchbar.TextYAlignment = Enum.TextYAlignment.Center
		searchbar.Parent = topFrame

		local highlightFrame = Instance.new("Frame")
		highlightFrame.Size = UDim2.fromScale(1, 1)
		highlightFrame.BackgroundColor3 = Color3.new(100, 100, 100)
		highlightFrame.BackgroundTransparency = 0.8

		local function rebuildFrequentSection()
			highlightFrame.Parent = nil

			-- Destroy all previously created Frequently Used UI instances
			for _, inst in frequentInstances do
				inst:Destroy()
			end
			table.clear(frequentInstances)

			-- Remove stale allButtons entries for Frequently Used
			for i = #allButtons, 1, -1 do
				if allButtons[i].category == FREQUENT_CATEGORY then
					table.remove(allButtons, i)
				end
			end

			categories[FREQUENT_CATEGORY] = nil
			originalOrders.headers[FREQUENT_CATEGORY] = nil

			local scopeList = frequentByScope[currentScope]
			if not scopeList or #scopeList == 0 then return end

			-- Header at LayoutOrder 0, always sits above real categories (base 10000+)
			local header = PluginUI.createHeader(nil, FREQUENT_CATEGORY)
			header.LayoutOrder = 0
			header.Size = UDim2.new(1, 0, 0, header.Size.Y.Offset)
			header.Parent = classSelectList.Container
			table.insert(classSelectList.Items, header)

			categories[FREQUENT_CATEGORY] = header
			originalOrders.headers[FREQUENT_CATEGORY] = 0
			table.insert(frequentInstances, header)

			-- No limit — use the full list as provided by setScopeInfo
			for order, className in scopeList do
				local icon = ClassIcon.getIcon(className)
				if not icon then continue end

				local capturedClass = className
				local buttonItem = PluginUI.createButtonWithIcon(
					nil,
					capturedClass,
					icon.Image,
					icon.ImageRectOffset,
					icon.ImageRectSize,
					function()
						classSelectList:Hide()
						if menuInfo.currentOnSelectCallback then
							menuInfo.currentOnSelectCallback(capturedClass)
						end
					end
				)

				local button = buttonItem.Instance
				-- LayoutOrder 1..N, all below the real category base of 10000
				button.LayoutOrder = order
				button:RemoveTag("JEXP_DarkBackground")
				button:AddTag("JEXP_Background")

				button.Size = UDim2.new(1, 0, 0, button.Size.Y.Offset)
				button.Parent = classSelectList.Container
				table.insert(classSelectList.Items, button)

				local key = FREQUENT_CATEGORY .. "_" .. capturedClass
				originalOrders.buttons[key] = order
				table.insert(allButtons, {
					button = button,
					className = capturedClass,
					category = FREQUENT_CATEGORY
				})
				table.insert(frequentInstances, button)
			end
		end

		searchbar:GetPropertyChangedSignal("Text"):Connect(function()
			local searchText = searchbar.Text
			local matches = {}
			for _, buttonData in allButtons do
				local score = calculateMatchScore(buttonData.className, searchText)
				if score > 0 then
					table.insert(matches, {
						button = buttonData.button,
						score = score,
						className = buttonData.className,
						category = buttonData.category
					})
				end
			end

			table.sort(matches, function(a, b)
				return a.score > b.score
			end)
			for _, buttonData in allButtons do
				buttonData.button.Visible = false
			end
			for _, header in categories do
				header.Visible = false
			end
			if searchText == "" then
				currentVisibleButtons = {}
				for _, buttonData in allButtons do
					buttonData.button.Visible = true
					local key = buttonData.category == FREQUENT_CATEGORY
						and (FREQUENT_CATEGORY .. "_" .. buttonData.className)
						or buttonData.className
					buttonData.button.LayoutOrder = originalOrders.buttons[key]
					table.insert(currentVisibleButtons, buttonData)
				end
				table.sort(currentVisibleButtons, function(a, b)
					return a.button.LayoutOrder < b.button.LayoutOrder
				end)
				for category, header in categories do
					header.Visible = true
					header.LayoutOrder = originalOrders.headers[category]
				end
				menuInfo.currentVisibleButtons = currentVisibleButtons
				return
			end

			currentVisibleButtons = {}
			local layoutOrder = 1
			local visibleCategories = {}
			local categoryOrder = {}
			for _, match in matches do
				if not visibleCategories[match.category] then
					visibleCategories[match.category] = true
					table.insert(categoryOrder, match.category)
				end
			end
			for _, category in categoryOrder do
				if categories[category] then
					categories[category].Visible = true
					categories[category].LayoutOrder = layoutOrder
					layoutOrder = layoutOrder + 1
				end
				for _, match in matches do
					if match.category == category then
						match.button.Visible = true
						match.button.LayoutOrder = layoutOrder
						layoutOrder = layoutOrder + 1
						table.insert(currentVisibleButtons, match)
					end
				end
			end
			menuInfo.currentVisibleButtons = currentVisibleButtons
		end)

		local REAL_CATEGORY_BASE = 10000
		local layoutOrder = REAL_CATEGORY_BASE
		local buttons = {}
		for category, tbl in Categories do
			local header = PluginUI.createHeader(classSelectList, category)
			layoutOrder += 1
			header.LayoutOrder = layoutOrder

			categories[category] = header
			originalOrders.headers[category] = layoutOrder

			for _, class in tbl do
				local icon = ClassIcon.getIcon(class)
				if not icon then continue end

				local capturedClass = class
				local buttonItem = PluginUI.createButtonWithIcon(
					nil, capturedClass, icon.Image, icon.ImageRectOffset, icon.ImageRectSize,
					function()
						classSelectList:Hide()
						if menuInfo.currentOnSelectCallback then
							menuInfo.currentOnSelectCallback(capturedClass)
						end
					end
				)
				table.insert(buttons, buttonItem.Instance)
				layoutOrder += 1

				local button = buttonItem.Instance
				button.LayoutOrder = layoutOrder
				button:RemoveTag("JEXP_DarkBackground")
				button:AddTag("JEXP_Background")
				originalOrders.buttons[capturedClass] = layoutOrder
				table.insert(allButtons, {
					button = button,
					className = capturedClass,
					category = category
				})
			end
		end
		classSelectList:AddBulkItem(buttons)
		currentVisibleButtons = {}
		for _, buttonData in allButtons do
			table.insert(currentVisibleButtons, buttonData)
		end

		menuInfo = {
			currentOnSelectCallback = nil,
			currentInsertTarget = 1,
			classSelectList = classSelectList,
			currentVisibleButtons = currentVisibleButtons,
			searchbar = searchbar,
			highlightFrame = highlightFrame,
			setScopeInfo = function(scopeName: string, frequentClasses: {string})
				frequentByScope[scopeName or "Default"] = frequentClasses
			end,
			setScope = function(scopeName: string)
				currentScope = scopeName or "Default"
				rebuildFrequentSection()

				currentVisibleButtons = {}
				for _, buttonData in allButtons do
					buttonData.button.Visible = true
					local key = buttonData.category == FREQUENT_CATEGORY
						and (FREQUENT_CATEGORY .. "_" .. buttonData.className)
						or buttonData.className
					buttonData.button.LayoutOrder = originalOrders.buttons[key]
					table.insert(currentVisibleButtons, buttonData)
				end
				table.sort(currentVisibleButtons, function(a, b)
					return a.button.LayoutOrder < b.button.LayoutOrder
				end)

				menuInfo.currentVisibleButtons = currentVisibleButtons
				menuInfo.currentInsertTarget = 1
				if #currentVisibleButtons > 0 then
					highlightFrame.Parent = currentVisibleButtons[1].button
				end

				searchbar.Text = ""
			end
		}
		ClassSelectionMenu.menuInfo = menuInfo

		local function onInputBegan(input)
			if not classSelectList.Backdrop.Visible then return end
			if input.UserInputState ~= Enum.UserInputState.Begin then return end
			local visible = menuInfo.currentVisibleButtons
			if input.KeyCode == Enum.KeyCode.Return then
				classSelectList:Hide()
				if menuInfo.currentOnSelectCallback and menuInfo.currentInsertTarget > 0 and menuInfo.currentInsertTarget <= #visible then
					local selected = visible[menuInfo.currentInsertTarget]
					menuInfo.currentOnSelectCallback(selected.className)
				end
			elseif input.KeyCode == Enum.KeyCode.Up then
				if #visible > 0 then
					menuInfo.currentInsertTarget = math.max(1, menuInfo.currentInsertTarget - 1)
					highlightFrame.Parent = visible[menuInfo.currentInsertTarget].button
				end
			elseif input.KeyCode == Enum.KeyCode.Down then
				if #visible > 0 then
					menuInfo.currentInsertTarget = math.min(#visible, menuInfo.currentInsertTarget + 1)
					highlightFrame.Parent = visible[menuInfo.currentInsertTarget].button
				end
			end
		end

		connections:add_many({
			Manager.connectInputBegan(onInputBegan),
			searchbar.FocusLost:Connect(function(_, input)
				onInputBegan(input)
			end),
			searchbar.InputBegan:Connect(onInputBegan)
		})
		classSelectList.Backdrop.Parent = jexplorerUi
	end

	Manager.ItemLists = {
		InsertInstance = classSelectList
	}
end

function ClassSelectionMenu.show(Manager, pos, size, fn)
	assert(menuInfo, "ClassSelectionMenu not initialized yet")

	local state = Manager.WidgetState
	local jexplorerUi = state.JExplorerUI

	local classSelectList = menuInfo.classSelectList
	local frameSize = classSelectList.Backdrop.AbsoluteSize
	local searchbar = menuInfo.searchbar

	menuInfo.currentOnSelectCallback = fn

	classSelectList:SetAnchor(
		Vector2.new(0, 0),
		UDim2.fromOffset(
			getClampedXY(
				frameSize,
				pos,
				size,
				jexplorerUi.AbsolutePosition,
				jexplorerUi.AbsoluteSize
			)
		)
	)

	classSelectList:Show()
	searchbar:CaptureFocus()
	searchbar.Text = ""
	menuInfo.currentInsertTarget = 1
	if #menuInfo.currentVisibleButtons > 0 then
		menuInfo.highlightFrame.Parent = menuInfo.currentVisibleButtons[1].button
	end
end

return ClassSelectionMenu


--!strict

local plugin = script:FindFirstAncestorOfClass("Plugin")

local VersionControl = require("../Editors/VersionControl")
local StyleEditor = require("../Editors/StyleEditor")
local SettingEditor = require("../Editors/SettingEditor")
local OrderEditor = require("../Editors/OrderEditor")
local InstanceVisibilityEditor = require("../Editors/InstanceVisibilityEditor")
local ContextMenuEditor = require("../Editors/ContextMenuEditor")
local JextensionEditor = require("../Editors/JextensionEditor")
local StateEditor = require("../Editors/StateEditor")
local ExplorerFrameEditor = require("../Editors/ExplorerFrameEditor")

local UniPlugin = require("../UniPlugin")(plugin)

local Types = require("../Types")

local PluginUI = require("./PluginUI")
local SavedState = require("./SavedState")
local Style = require("./Style")

local studioSettings = settings().Studio

local IS_EDIT = game:GetService("RunService"):IsEdit()

return function(Manager: Types.Manager, components: Types.Components, topbar: Instance)
	local state = Manager.WidgetState
	local topbarMenuDropdown do
		topbarMenuDropdown = PluginUI.createItemList {
			Name = "TopbarMenuDropdown"
		}

		local theme = assert(studioSettings.Theme) :: StudioTheme
		local backgroundColor = theme:GetColor(Enum.StudioStyleGuideColor.MainBackground)
		local isLight = backgroundColor.R > 0.5
		local themeExtension = isLight and "Light" or "Dark"

		local WIP_COLOR = Color3.fromRGB(79, 79, 79)
		if IS_EDIT then
			PluginUI.createButtonWithIcon(
				topbarMenuDropdown,
				"Open Version Control",
				`rbxasset://studio_svg_textures/Shared/InsertableObjects/{themeExtension}/Standard/PackageLink.png`,
				Vector2.zero, Vector2.new(16, 16),
				function()
					VersionControl.Widget.Enabled = true
					topbarMenuDropdown:Hide()
				end
			)
			PluginUI.createSeparator(topbarMenuDropdown)
			PluginUI.createButtonWithIcon(
				topbarMenuDropdown,
				"Open Style Editor",
				`rbxasset://studio_svg_textures/Shared/InsertableObjects/{themeExtension}/Standard/StyleSheet.png`,
				Vector2.zero, Vector2.new(16, 16),
				function()
					StyleEditor.Widget.Enabled = true
					topbarMenuDropdown:Hide()
				end
			)
		end
		PluginUI.createButtonWithIcon(
			topbarMenuDropdown,
			"Open Setting Editor",
			`rbxasset://studio_svg_textures/Shared/InsertableObjects/{themeExtension}/Standard/Configuration.png`,
			Vector2.zero, Vector2.new(16, 16),
			function()
				SettingEditor.Widget.Enabled = true
				topbarMenuDropdown:Hide()
			end
		)
		if IS_EDIT then
			PluginUI.createButtonWithIcon(
				topbarMenuDropdown,
				"Open Instance Visibility Editor",
				`rbxasset://studio_svg_textures/Shared/InsertableObjects/{themeExtension}/Standard/SelectionBox.png`,
				Vector2.zero, Vector2.new(16, 16),
				function()
					InstanceVisibilityEditor.Widget.Enabled = true
					topbarMenuDropdown:Hide()
				end
			)
			PluginUI.createButtonWithIcon(
				topbarMenuDropdown,
				"Open Order Editor",
				`rbxasset://studio_svg_textures/Shared/InsertableObjects/{themeExtension}/Standard/UIListLayout.png`,
				Vector2.zero, Vector2.new(16, 16),
				function()
					OrderEditor.Widget.Enabled = true
					topbarMenuDropdown:Hide()
				end
			)
			PluginUI.createButtonWithIcon(
				topbarMenuDropdown,
				"Open Context Menu Editor",
				`rbxasset://studio_svg_textures/Shared/InsertableObjects/{themeExtension}/Standard/UITableLayout.png`,
				Vector2.zero, Vector2.new(16, 16),
				function()
					ContextMenuEditor.Widget.Enabled = true
					topbarMenuDropdown:Hide()
				end
			)
			PluginUI.createButtonWithIcon(
				topbarMenuDropdown,
				"Open Jextension Editor",
				`rbxasset://studio_svg_textures/Shared/InsertableObjects/{themeExtension}/Standard/Weld.png`,
				Vector2.zero, Vector2.new(16, 16),
				function()
					JextensionEditor.Widget.Enabled = true
					topbarMenuDropdown:Hide()
				end
			)
			PluginUI.createButtonWithIcon(
				topbarMenuDropdown,
				"Open State Editor",
				`rbxasset://studio_svg_textures/Shared/InsertableObjects/{themeExtension}/Standard/Actor.png`,
				Vector2.zero, Vector2.new(16, 16),
				function()
					StateEditor.Widget.Enabled = true
					topbarMenuDropdown:Hide()
				end
			)
			PluginUI.createButtonWithIcon(
				topbarMenuDropdown,
				"Open ExplorerFrame Editor",
				`rbxasset://studio_svg_textures/Shared/InsertableObjects/{themeExtension}/Standard/Frame.png`,
				Vector2.zero, Vector2.new(16, 16),
				function()
					ExplorerFrameEditor.Widget.Enabled = true
					topbarMenuDropdown:Hide()
				end
			)

			PluginUI.createSeparator(topbarMenuDropdown)
			PluginUI.createButtonWithIcon(
				topbarMenuDropdown,
				"Refresh",
				"rbxassetid://13825976376",
				Vector2.zero, Vector2.zero,
				function()
					Manager.VirtualScroller.rebuildAndUpdate(Manager, false)
				end
			)
			PluginUI.createButton(topbarMenuDropdown, "Reassign ExplorerFrames", function()
				local world = state.World
				for entity, frame in world:query(components.Frame):iter() do
					Manager.ExplorerFrame.queueForExplorerFrameReuse(Manager, frame)
				end
				task.wait(1/30)
				Manager.VirtualScroller.update(Manager, false, true)
			end)
			PluginUI.createSeparator(topbarMenuDropdown)
			PluginUI.createButton(topbarMenuDropdown, "Save State", function()
				topbarMenuDropdown:Hide()
				local saver = Manager.saveState()
				saver.all()
				saver.save()

				local PRINT_SAVEDSTATE_PREFIX = "SavedState: "
				local function getPrintableTable(tbl): {[any]: any}
					local newtbl = {}
					for k, v in tbl do
						if typeof(v) == "table" then
							if #v > 10 then
								v = "Array"
							else
								v = getPrintableTable(v)
							end
						end
						newtbl[k] = v
					end
					return newtbl
				end
				local function onsaved()
					print(PRINT_SAVEDSTATE_PREFIX, getPrintableTable(SavedState.load(Manager)))
				end
				onsaved()
			end)
			PluginUI.createButton(topbarMenuDropdown, "Kill Jexplorer", function()
				UniPlugin:close()
				plugin:Destroy()
			end)
			PluginUI.createButton(topbarMenuDropdown, "Insert Plugin Src", function()
				Manager.Script:Clone().Parent = workspace
			end)
			PluginUI.createButton(topbarMenuDropdown, "Debug Plugin", function()
				plugin.Parent = workspace
			end)
		end
		topbarMenuDropdown.Backdrop.MouseLeave:Connect(function()
			topbarMenuDropdown:Hide()
		end)
		topbarMenuDropdown.Backdrop.Parent = topbar
	end
	return topbarMenuDropdown
end


local studioSetting = settings().Studio
local pluginDebugServiceEnabled = studioSetting.PluginDebuggingEnabled

return {
	if pluginDebugServiceEnabled then nil else "PluginDebugService",
	"VirtualInputManager",
	"InstanceFileSyncService",
	"AdService",
	"InstanceExtensionsService",
	"MicroProfilerService",
	"ModerationService",
	"RecommendationService",
	"RemoteCommandService",
	"SlimContentProvider",
	"StudioTestService",
	"UniqueIdLookupService",
	"ReflectionService",
	"BadgeService",
	"ChangeHistoryService",
	"Chat",
	"CollectionService",
	"ContentProvider",
	"ContextActionService",
	"CorePackages",
	"DataStoreService",
	"Debris",
	"GamePassService",
	"GroupService",
	"GuiService",
	"HapticService",
	"HttpService",
	"InsertService",
	"JointsService",
	"KeyframeSequenceProvider",
	"LocalizationService",
	"LogService",
	"MarketplaceService",
	"MemStorageService",
	"MessagingService",
	"MouseService",
	"NetworkServer",
	"NotificationService",
	"PathfindingService",
	"PhysicsService",
	"PolicyService",
	"PointsService",
	"ProximityPromptService",
	"RunService",
	"ScriptContext",
	"Selection",
	"SocialService",
	"Stats",
	"TeleportService",
	--"TestService",
	"TextService",
	"TimerService",
	"TouchInputService",
	"TweenService",
	"UserInputService",
	"UserService",
	"VRService",
	"VersionControlService",
	"VoiceChatService",
	"AdService",
	"AnalyticsService",
	"AnimationClipProvider",
	"AssetService",
	"AvatarCreationService",
	"BrowserService",
	"CaptureService",
	"CommerceService",
	"ControllerService",
	"CookiesService",
	"DraftsService",
	"DraggerService",
	"FeatureRestrictionManager",
	"FriendService",
	"GamepadService",
	"GenerationService",
	"Geometry",
	"GuidRegistryService",
	"HSRDataContentProvider",
	"HeapProfilerService",
	"HeightmapImporterService",
	"HttpRbxApiService",
	"KeyboardService",
	"LuaWebService",
	"MeshContentProvider",
	"PackageService",
	"PermissionsService",
	"PlacesService",
	"PluginManagementService",
	"ProcessInstancePhysicsService",
	"PublishService",
	"RemoteDebuggerServer",
	"ScriptEditorService",
	"ScriptProfilerService",
	"ScriptService",
	"SelectionHighlightManager",
	"SerializationService",
	"ServiceVisibilityService",
	"SharedTableRegistry",
	"SolidModelContentProvider",
	"Stats",
	"StudioService",
	"TextBoxService",
	"UGCValidationService",
	"UIDragDetectorService",
	"VideoCaptureService",
	"VideoService",
	"Visit",
	"VisualizationModeService"
}


--!strict
local Types = require("../../Types")

return function(manager: Types.Manager)
	return {
		"Workspace",
		"Players",
		"CoreGui",
		"Lighting",
		"MaterialService",
		"NetworkClient",
		"PluginGuiService",
		"ReplicatedFirst",
		"ReplicatedStorage",
		"ServerScriptService",
		"ServerStorage",
		"StarterGui",
		"StarterPack",
		"StarterPlayer",
		"Teams",
		"SoundService",
		"TextChatService",

		"ChatWindowConfiguration",
		"ChatInputBarConfiguration",
		"ChannelTabsConfiguration",
		"BubbleChatConfiguration",

		"PackageLink",

		"Camera",
		"Terrain",
		"Folder",

		"Tool",

		"ScreenGui",
		"SurfaceGui",
		"BillboardGui",
		"DockWidgetPluginGui",
		"QWidgetPluginGui",

		"UIListLayout",
		"UIGridLayout",
		"UITableLayout",
		"UIPageLayout",
		"UIPadding",
		"UICorner",
		"UIGradient",
		"UIStroke",
		"UITextSizeConstraint",
		"UIAspectRatioConstraint",
		"UISizeConstraint",
		"UIScale",

		"AudioDeviceOutput",
		"AudioPlayer",
		"Wire",

		"Frame",
		"CanvasGroup",
		"ImageLabel",
		"ImageButton",
		"TextLabel",
		"TextBox",
		"TextButton",

		"StandalonePluginScripts",

		"BindableEvent",
		"BindableFunction",
		"RemoteEvent",
		"UnreliableRemoteEvent",
		"RemoteFunction",

		"Actor",
		"Script",
		"LocalScript",
		"ModuleScript",

		"Model",
		"Part",
		"MeshPart",
		"UnionOperation"
	}
end


--!strict
local Types = require(script.Parent.Parent.Parent.Types)
local Style = require(script.Parent.Parent.Style)

local var = Style.var

return {
	Internal = {
		EntityDeletionFlushStep = {
			Type = "number",
			Display = "Entity Deletion Flush Step",
			Default = 30
		}
	},
	Query = {
		QueryAutocompleteEnabled = {
			Type = "boolean",
			Display = "Query Autocomplete Enabled",
			Default = true
		}
	},
	Duplicate = {
		IncrementNamesWithNoNumberSuffix = {
			Type = "boolean",
			Display = "Increment Names With No Number Suffix",
			Default = false,
		},
		NormalDuplicateIncrementsName = {
			Type = "boolean",
			Display = "Normal Duplicate Increments Name",
			Default = true
		},
	},
	Keybinds = {
		KeybindEscapeTarget = {
			Type = "string",
			Display = "Escape Target",
			Default = "Escape | (Shift & Backspace)"
		},
		KeybindDeleteTarget = {
			Type = "string",
			Display = "Delete Target",
			Default = "Delete | Backspace"
		},
		KeybindDuplicateTarget = {
			Type = "string",
			Display = "Duplicate Target",
			Default = "Control & D"
		},
		KeybindDuplicateTargetWithIncrementName = {
			Type = "string",
			Display = "Duplicate Target With Increment Name",
			Default = "Control & D & E"
		},
		KeybindCopy = {
			Type = "string",
			Display = "Copy",
			Default = "Control & C"
		},
		KeybindPaste = {
			Type = "string",
			Display = "Paste",
			Default = "Control & V"
		},
		KeybindUndo = {
			Type = "string",
			Display = "Undo",
			Default = "Control & Z"
		},
		KeybindRedo = {
			Type = "string",
			Display = "Redo",
			Default = "Control & Y"
		},
	},
	Expand = {
		AutoExpandOnSelect = {
			Type = "boolean",
			Display = "Auto-expand Ancestors on Select Unexpanded Instance",
			Default = true,
		},
		StickyStacksEnabled = {
			Type = "boolean",
			Display = "Sticky Stack Breadcrumb when Ancestors Are Expanded",
			Default = true
		}
	},
	Selection = {
		JumpToSelection = {
			Type = "boolean",
			Display = "Jump to Middle of Selection",
			Default = true,
		},
		PropertyEditorUpdatedOnSelect = {
			Type = "boolean",
			Display = "Property Editor (JProperties) Updated on Selection Changed",
			Default = false
		},
		DoubleClickToOpenScriptTime = {
			Type = "number",
			Display = "Cooldown to Double Click to Open Script",
			Default = 0.2
		},
		DoubleClickToRenameTime = {
			Type = "number",
			Display = "Cooldown to Double Click to Rename",
			Default = 0.5
		}
	},
	Saving = {
		AutosaveInstChanged = {
			Type = "boolean",
			Display = "Autosave Instance Changes",
			Default = true
		},
		SaveOnClose = {
			Type = "boolean",
			Display = "Save On Close",
			Default = true
		}
	},
	Language = {
		DocumentationLanguage = {
			Type = "string",
			Display = "Documentation Language (Roblox | System | locale-id)",
			Default = "Roblox"
		}
	},
	Scrolling = {
		--SmoothScrollEnabled = {
		--	Type = "boolean",
		--	Display = "Smooth Scroll Enabled",
		--	Default = false,
		--	OnChange = function(Manager, newValue)
		--		local jexplorerUi = Manager.WidgetState.JExplorerUI
		--		jexplorerUi.InstanceTree.ScrollingEnabled = newValue
		--	end
		--},
		ScrollStep = {
			Type = "number",
			Display = "Scrolling Step (offsets)",
			Default = 20,
		},
		--ScrollThreshold = {
		--	Type = "number",
		--	Display = "Non-smooth Scroll Threshold",
		--	Default = 1,
		--},
		VirtualScrollFrameBuffer = {
			Type = "number",
			Display = "Virtual Scrolling Instance Frame Buffer",
			Default = 10,
		},
		VirtualScrollSettleTime = {
			Type = "number",
			Display = "Virtual Scrolling Scroll Settle Time",
			Default = var "1/20",
		},
		--VirtualReuseCooldown = {
		--	Type = "number",
		--	Display = "Virtual Frame Reuse Cooldown",
		--	Default = 0,
		--},
	},
	ExplorerFrame = {
		IndentWidth = {
			Type = "number",
			Display = "ExplorerFrame Children Indent Width",
			Default = 20,
			OnChange = function(Manager)
				Manager.VirtualScroller.rebuildAndUpdate(Manager, false)
			end,
		},
		FrameHeight = {
			Type = "number",
			Display = "ExplorerFrame Frame Height",
			Default = 20,
			OnChange = function(Manager)
				Manager.VirtualScroller.rebuildAndUpdate(Manager, false)
			end,
		}
	}
} :: {
	[string]: {
		[string]: {
			Type: string,
			Display: string,
			Default: any,
			OnChange: ((m: Types.Manager, newValue: any) -> ())?
		}
	}
}