local HttpService = game:GetService("HttpService")

local fusion = require("@packages/fusion")
local Spring = fusion.Spring

local Children = fusion.Children
local Cleanup = fusion.Cleanup
local New = fusion.New
local Out = fusion.Out

local Clean = fusion.cleanup
local Computed = fusion.Computed
local Observer = fusion.Observer
local Value = fusion.Value

local fusionComponents = require("@packages/fusionComponents")
local button = fusionComponents.common.button
local breadcrumb = fusionComponents.common.breadcrumb
local input = fusionComponents.common.input
local modal = fusionComponents.common.modal
local popover = fusionComponents.common.popover
local contextMenu = fusionComponents.common.contextMenu
local menuEntry = fusionComponents.utility.menuEntry
local text = fusionComponents.base.text
local icon = fusionComponents.base.icon
local frame = fusionComponents.base.frame
local scrollFrame = fusionComponents.common.scrollingFrame

local tailwind = require("@packages/tailwind")

local fusionUtils = require("@packages/fusionUtils")
local awaitLayer = fusionUtils.awaitLayer

local virtualScroller = require("@components/virtualScroller")

local textureImage = require("./textureImage")
local treeButton = require("./treeButton")
local createFolderModal = require("./modals/createFolder")
local modifyTextureModal = require("./modals/modifyTexture")
local moveItemModal = require("./modals/moveItem")
local confirmDeleteModal = require("./modals/confirmDelete")
local copyContentModal = require("./modals/copyContent")
local importInstancesModal = require("./modals/importInstances")
local importJsonModal = require("./modals/importJson")

local settingsManager = require("@src/lib/settingsManager")
local theme = require("@src/theme")

local function traverseTextureList(textureList: { textureList }, path: { string }): { texture | textureList }
	local items = textureList

	for _, label in path do
		if label == "" then
			continue
		end

		for _, item in items do
			if item.name == label then
				items = item.content
				break
			end
		end
	end

	return items
end

local function tableFind(t: { any }, predicate: (any) -> boolean): any?
	for _, value in t do
		if predicate(value) then
			return value
		end
	end

	return nil
end

-- natural sort, from https://stackoverflow.com/questions/27909784
local function stringCompare(a, b)
	a = tostring(a)
	b = tostring(b)
	local patt = "^(.-)%s*(%d+)$"
	local _, _, col1, num1 = a:find(patt)
	local _, _, col2, num2 = b:find(patt)
	if (col1 and col2) and col1 == col2 then
		return tonumber(num1) < tonumber(num2)
	end
	return a < b
end

type texture = {
	name: string,
	id: string,
	flipbookLayout: nil | "2x2" | "4x4" | "8x8",
}

export type textureList = {
	name: string,
	content: { texture | textureList },
}

type props = {
	Value: fusion.Value<{
		Texture: string,
		FlipbookLayout: Enum.ParticleFlipbookLayout,
	}>?,
	Path: { string },
	IsSelectingImage: boolean,
}

local SIDEBAR_WIDTH = 200
local EDITOR_BUTTON_SIZE = 96

local function editor(props: props)
	local useColor = theme:get("TexturePicker", "gray", "default", "Base")

	local searchQuery = Value("")
	local currentPath = Value(props.Path or { "Home" })

	local textures = settingsManager.getReactiveWithFallback("textures", {})

	local currentItems: fusion.Computed<{ texture | textureList }> = Computed(function()
		local path = currentPath:get() or ""
		local usedTextures = textures and textures:get()

		if not usedTextures then
			return {}
		end

		if path == "" then
			return usedTextures
		end

		local items = traverseTextureList(usedTextures, path)

		-- sort items; we want folders first, then textures
		local query = searchQuery:get():lower()

		table.sort(items, function(a, b)
			if not a then
				return false
			elseif not b then
				return true
			end

			if query ~= "" then
				local aMatchesQuery = string.lower(a.name):find(query)
				local bMatchesQuery = string.lower(b.name):find(query)

				if aMatchesQuery and not bMatchesQuery then
					return true
				elseif not aMatchesQuery and bMatchesQuery then
					return false
				elseif aMatchesQuery and bMatchesQuery then
					return aMatchesQuery < bMatchesQuery
				end
			end

			if a.id == nil and b.id == nil then
				return stringCompare(a.name, b.name)
			elseif a.id == nil then
				return true
			elseif b.id == nil then
				return false
			end

			return stringCompare(a.name, b.name)
		end)

		return items
	end)

	local breadcrumbLabels = Value({
		{
			Text = "Home",
			Icon = "home",
			OnClick = function()
				currentPath:set({ "Home" })
			end,
		},
	})

	local breadcrumbSize = Value(Vector2.new())
	local containerSize = Value(Vector2.new())
	local contentSize = Value(Vector2.new())

	local isContextMenuVisible = Value(false)
	local isModalVisible = Value(false)
	local isPopoverVisible = Value(false)
	local modalContent = Value({})

	local selectedIndices = Value({})
	local isCtrlHeld = Value(false)
	local isShiftHeld = Value(false)

	local connections = {}

	local function deleteIds(textureList: { texture })
		local path = currentPath:get(false) or ""
		local usedTextures = textures and textures:get(false)

		if not usedTextures then
			return
		end

		local items = traverseTextureList(usedTextures, path)

		for _, texture in textureList do
			for i, item in items do
				if item.name == texture.name then
					table.remove(items, i)
					break
				end
			end
		end

		settingsManager.set("textures", usedTextures)
	end

	local function updateBreadcrumb()
		local path = currentPath:get(false) or {}
		local iteratedPath = {}

		local labels = breadcrumbLabels:get(false)

		local mappedCount = {}

		for _, label in path do
			if label == "" then
				continue
			end

			if not mappedCount[label] then
				mappedCount[label] = 0
			end

			mappedCount[label] += 1
		end

		local labelCount = {}

		for i = #labels, 1, -1 do
			local label = labels[i]
			local labelPath = label.Text

			if labelPath ~= "Home" then
				-- remove the amount of times this label appears in the path at MOST
				if not labelCount[labelPath] then
					labelCount[labelPath] = 0
				end

				labelCount[labelPath] += 1

				if mappedCount[labelPath] == nil or labelCount[labelPath] >= mappedCount[labelPath] then
					table.remove(labels, i)
				end
			end
		end

		local currentCount = {}

		for _, label in path do
			if label == "" then
				continue
			end

			table.insert(iteratedPath, label)
			local currentIteratedPath = table.clone(iteratedPath)

			local found = false

			for _, existingLabel in labels do
				if existingLabel.Text == label then
					if
						mappedCount[label] > 1
						and (currentCount[label] == nil or currentCount[label] + 1 < mappedCount[label])
					then
						if not currentCount[label] then
							currentCount[label] = 0
						end

						currentCount[label] += 1
						break
					end

					found = true
					break
				end
			end

			if not found then
				table.insert(labels, {
					Text = label,
					OnClick = function()
						currentPath:set(currentIteratedPath)
					end,
				})
			end
		end

		breadcrumbLabels:set(labels)
	end

	local component = New("Frame")({
		Name = "TexturePicker",
		BackgroundTransparency = 1,
		Size = UDim2.new(1, 0, 1, 0),
		[Cleanup] = {
			connections,
			Observer(currentPath):onChange(updateBreadcrumb),
		},
		[Children] = {
			frame({
				Name = "Sidebar",
				Appearance = useColor("SidebarBackground", true),
				Size = UDim2.new(0, SIDEBAR_WIDTH, 1, 0),
				CornerRadius = UDim.new(0, 0),
				Content = {
					New("UIPadding")({
						PaddingBottom = UDim.new(0, 4),
						PaddingLeft = UDim.new(0, 4),
						PaddingRight = UDim.new(0, 4),
						PaddingTop = UDim.new(0, 4),
					}),
					New("UIListLayout")({
						FillDirection = Enum.FillDirection.Vertical,
						HorizontalAlignment = Enum.HorizontalAlignment.Center,
						Padding = UDim.new(0, 4),
						SortOrder = Enum.SortOrder.LayoutOrder,
						VerticalAlignment = Enum.VerticalAlignment.Top,
					}),
					input({
						Color = "gray",
						Variant = "default",
						Placeholder = "Search..",
						Icon = "search",
						TextXAlignment = Enum.TextXAlignment.Left,
						Size = UDim2.new(1, 0, 0, 24),
						OnTextChanged = function(inputText)
							searchQuery:set(inputText)
						end,
					}),
					scrollFrame({
						Name = "FolderList",
						BackgroundTransparency = 1,
						Size = UDim2.new(1, 0, 1, -28),
						ScrollingFrameProps = {
							VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar,
							AutomaticCanvasSize = Enum.AutomaticSize.Y,
						},
						Content = {
							New("UIListLayout")({
								FillDirection = Enum.FillDirection.Vertical,
								HorizontalAlignment = Enum.HorizontalAlignment.Left,
								HorizontalFlex = Enum.UIFlexAlignment.Fill,
								Padding = UDim.new(0, 2),
								SortOrder = Enum.SortOrder.LayoutOrder,
								VerticalAlignment = Enum.VerticalAlignment.Top,
							}),
							treeButton({
								Name = "Home",
								Content = textures,
								CurrentPath = currentPath,
								OnClick = function(path)
									selectedIndices:set({})
									currentPath:set(path)
								end,
								Path = { "Home" },
								Query = searchQuery,
							}),
						},
					}),
				},
			}),
			New("Frame")({
				Name = "Content",
				BackgroundTransparency = 1,
				Position = UDim2.new(0, SIDEBAR_WIDTH, 0, 0),
				Size = UDim2.new(1, -SIDEBAR_WIDTH, 1, 0),
				[Children] = {
					New("UIListLayout")({
						FillDirection = Enum.FillDirection.Vertical,
						HorizontalAlignment = Enum.HorizontalAlignment.Center,
						Padding = UDim.new(0, 4),
						SortOrder = Enum.SortOrder.LayoutOrder,
						VerticalAlignment = Enum.VerticalAlignment.Top,
					}),
					New("UIPadding")({
						PaddingBottom = UDim.new(0, 2),
						PaddingLeft = UDim.new(0, 4),
						PaddingRight = UDim.new(0, 4),
						PaddingTop = UDim.new(0, 2),
					}),
					New("Frame")({
						Name = "Topbar",
						BackgroundTransparency = 1,
						Position = UDim2.new(0, 0, 0, 0),
						Size = UDim2.new(1, 0, 0, 28),
						ClipsDescendants = true,
						[Children] = {
							New("UIPadding")({
								PaddingLeft = UDim.new(0, 2),
								PaddingRight = UDim.new(0, 2),
								PaddingTop = UDim.new(0, 2),
								PaddingBottom = UDim.new(0, 2),
							}),
							New("UIListLayout")({
								FillDirection = Enum.FillDirection.Horizontal,
								HorizontalAlignment = Enum.HorizontalAlignment.Center,
								Padding = UDim.new(0, 4),
								VerticalAlignment = Enum.VerticalAlignment.Center,
							}),
							New("ScrollingFrame")({
								Name = "BreadcrumbContainer",
								AutomaticCanvasSize = Enum.AutomaticSize.X,
								AutomaticSize = Enum.AutomaticSize.Y,
								BackgroundTransparency = 1,
								CanvasPosition = Spring(
									Computed(function()
										-- this tries to scroll as far right as possible
										local canvasSize = containerSize:get()
										local viewportSize = breadcrumbSize:get()

										if canvasSize == nil or viewportSize == nil then
											return Vector2.new(0, 0)
										end

										return Vector2.new(viewportSize.X - canvasSize.X, 0)
									end),
									30,
									1
								),
								CanvasSize = UDim2.new(0, 0, 0, 0),
								ClipsDescendants = true,
								ScrollBarThickness = 0,
								ScrollingEnabled = false,
								Size = UDim2.new(1, 0, 0, 24),
								[Out("AbsoluteSize")] = containerSize,
								[Children] = {
									New("UIFlexItem")({
										FlexMode = Enum.UIFlexMode.Fill,
									}),
									New("UIListLayout")({
										FillDirection = Enum.FillDirection.Horizontal,
										HorizontalAlignment = Enum.HorizontalAlignment.Left,
										Padding = UDim.new(0, 4),
										SortOrder = Enum.SortOrder.LayoutOrder,
										VerticalAlignment = Enum.VerticalAlignment.Center,
										[Out("AbsoluteContentSize")] = breadcrumbSize,
									}),
									breadcrumb({
										Color = "blue",
										Labels = breadcrumbLabels,
									}),
								},
							}),
							(function()
								local isThisPopoverVisible = Value(false)

								local createdButton = button({
									Color = "gray",
									Variant = "solid",
									Icon = "plus",
									AutomaticSize = Enum.AutomaticSize.XY,
									OnClick = function()
										local currentPopoverVisible = isPopoverVisible:get(false)

										if
											currentPopoverVisible ~= false
											and currentPopoverVisible ~= isThisPopoverVisible
										then
											isPopoverVisible:get(false):set(false)
										end

										isThisPopoverVisible:set(not isThisPopoverVisible:get(false))
										isPopoverVisible:set(isThisPopoverVisible)

										if isContextMenuVisible:get(false) then
											isContextMenuVisible:get(false):set(false)
										end
									end,
								})

								local lastContent = nil
								local contentObserver = Observer(modalContent):onChange(function()
									if lastContent then
										lastContent:destroy()
									end

									lastContent = modalContent:get()
								end)

								return modal(
									popover(createdButton, {
										Variant = "default",
										Visible = isThisPopoverVisible,
										Placement = "bottom",
										RelativePosition = UDim2.new(0, -50, 0, 0),
										Content = {
											New("UIListLayout")({
												FillDirection = Enum.FillDirection.Vertical,
												HorizontalAlignment = Enum.HorizontalAlignment.Center,
												HorizontalFlex = Enum.UIFlexAlignment.Fill,
												Padding = UDim.new(0, 4),
												SortOrder = Enum.SortOrder.LayoutOrder,
												VerticalAlignment = Enum.VerticalAlignment.Top,
											}),
											menuEntry({
												Option = {
													Label = "Create Folder",
													Icon = "folder-plus",
													Color = "gray",
												},
												OnClick = function()
													isModalVisible:set(true)
													isThisPopoverVisible:set(false)
													isPopoverVisible:set(false)

													modalContent:set(createFolderModal({
														useColor = useColor,
														OnCreate = function(name: string)
															local path = currentPath:get(false) or ""
															local usedTextures = textures and textures:get(false)

															if not usedTextures then
																return
															end

															local items = traverseTextureList(usedTextures, path)

															-- if there is already a folder with this name, add (x) to the end
															local newName = name
															local i = 1

															while
																tableFind(items, function(item)
																	return item.name == newName
																end)
															do
																i = i + 1
																newName = name .. " (" .. i .. ")"
															end

															table.insert(items, {
																name = newName,
																content = {},
															})

															settingsManager.set("textures", usedTextures)
															isModalVisible:set(false)
														end,
														OnClose = function()
															isModalVisible:set(false)
														end,
													}))
												end,
											}),
											menuEntry({
												Option = {
													Label = "Create Texture",
													Icon = "image-plus",
													Color = "gray",
												},
												OnClick = function()
													isModalVisible:set(true)
													isThisPopoverVisible:set(false)

													modalContent:set(modifyTextureModal({
														VerbUsed = "Create",
														useColor = useColor,
														OnCreate = function(name, id, flipbookLayout)
															local path = currentPath:get(false) or ""
															local usedTextures = textures and textures:get(false)

															if not usedTextures then
																return
															end

															local items = traverseTextureList(usedTextures, path)

															-- prevent duplicate names, duplicate names will be suffixed with (x)
															local newName = name
															local i = 1

															while
																tableFind(items, function(item)
																	return item.name == newName
																end)
															do
																i = i + 1
																newName = name .. " (" .. i .. ")"
															end

															table.insert(items, {
																name = newName,
																id = id,
																flipbookLayout = flipbookLayout,
															})

															settingsManager.set("textures", usedTextures)
															isModalVisible:set(false)
														end,
														OnClose = function()
															isModalVisible:set(false)
														end,
													}))
												end,
											}),
										},
										[Cleanup] = contentObserver,
									}),
									{
										Visible = isModalVisible,
										Padding = UDim.new(0, 8),
										OnClickBesides = function()
											isModalVisible:set(false)
										end,
										Content = modalContent,
									}
								)
							end)(),
							(function()
								local isThisPopoverVisible = Value(false)

								return popover(
									button({
										Color = "gray",
										Variant = "solid",
										Icon = "download",
										AutomaticSize = Enum.AutomaticSize.XY,
										OnClick = function()
											local currentPopoverVisible = isPopoverVisible:get(false)

											if
												currentPopoverVisible ~= false
												and currentPopoverVisible ~= isThisPopoverVisible
											then
												isPopoverVisible:get(false):set(false)
											end

											isThisPopoverVisible:set(not isThisPopoverVisible:get(false))
											isPopoverVisible:set(isThisPopoverVisible)

											if isContextMenuVisible:get(false) then
												isContextMenuVisible:get(false):set(false)
											end
										end,
									}),
									{
										Variant = "default",
										Visible = isThisPopoverVisible,
										Placement = "bottom",
										RelativePosition = UDim2.new(0, -50, 0, 0),
										Content = {
											New("UIListLayout")({
												FillDirection = Enum.FillDirection.Vertical,
												HorizontalAlignment = Enum.HorizontalAlignment.Center,
												HorizontalFlex = Enum.UIFlexAlignment.Fill,
												Padding = UDim.new(0, 4),
												SortOrder = Enum.SortOrder.LayoutOrder,
												VerticalAlignment = Enum.VerticalAlignment.Top,
											}),
											menuEntry({
												Option = {
													Label = "Import instances",
													Icon = "image-up",
													Color = "gray",
												},
												OnClick = function()
													isModalVisible:set(true)
													modalContent:set(importInstancesModal({
														useColor = useColor,
														BaseTextures = textures:get(false),
														OnClose = function()
															isModalVisible:set(false)
														end,
														OnConfirm = function(newTextures)
															settingsManager.set("textures", newTextures)
															isModalVisible:set(false)
														end,
														Path = currentPath:get(false),
													}))
												end,
											}),
											menuEntry({
												Option = {
													Label = "Import JSON data",
													Icon = "file-json",
													Color = "gray",
												},
												OnClick = function()
													isModalVisible:set(true)
													modalContent:set(importJsonModal({
														useColor = useColor,
														BaseTextures = textures:get(false),
														OnClose = function()
															isModalVisible:set(false)
														end,
														OnConfirm = function(newTextures)
															settingsManager.set("textures", newTextures)
															isModalVisible:set(false)
														end,
														Path = currentPath:get(false),
													}))
												end,
											}),
										},
									}
								)
							end)(),
							(function()
								local isThisPopoverVisible = Value(false)

								return popover(
									button({
										Color = "gray",
										Variant = "solid",
										Icon = "upload",
										AutomaticSize = Enum.AutomaticSize.XY,
										OnClick = function()
											local currentPopoverVisible = isPopoverVisible:get(false)

											if
												currentPopoverVisible ~= false
												and currentPopoverVisible ~= isThisPopoverVisible
											then
												isPopoverVisible:get(false):set(false)
											end

											isThisPopoverVisible:set(not isThisPopoverVisible:get(false))
											isPopoverVisible:set(isThisPopoverVisible)

											if isContextMenuVisible:get(false) then
												isContextMenuVisible:get(false):set(false)
											end
										end,
									}),
									{
										Variant = "default",
										Visible = isThisPopoverVisible,
										Placement = "bottom",
										RelativePosition = UDim2.new(0, -50, 0, 0),
										Content = {
											New("UIListLayout")({
												FillDirection = Enum.FillDirection.Vertical,
												HorizontalAlignment = Enum.HorizontalAlignment.Center,
												HorizontalFlex = Enum.UIFlexAlignment.Fill,
												Padding = UDim.new(0, 4),
												SortOrder = Enum.SortOrder.LayoutOrder,
												VerticalAlignment = Enum.VerticalAlignment.Top,
											}),
											menuEntry({
												Option = {
													Label = "Export folder",
													Icon = "file-down",
													Color = "gray",
												},
												OnClick = function()
													local path = currentPath:get(false) or ""
													local usedTextures = textures and textures:get(false)

													if not usedTextures then
														return
													end

													local items = traverseTextureList(usedTextures, path)
													isModalVisible:set(true)
													modalContent:set(copyContentModal({
														useColor = useColor,
														ClipboardContent = HttpService:JSONEncode(items),
														OnClose = function()
															isModalVisible:set(false)
														end,
													}))
												end,
											}),
											menuEntry({
												Option = {
													Label = "Export all",
													Icon = "folder-down",
													Color = "gray",
												},
												OnClick = function()
													isModalVisible:set(true)
													modalContent:set(copyContentModal({
														useColor = useColor,
														ClipboardContent = HttpService:JSONEncode(textures:get(false)),
														OnClose = function()
															isModalVisible:set(false)
														end,
													}))
												end,
											}),
											Computed(function()
												if #selectedIndices:get() == 0 then
													return nil
												end

												return menuEntry({
													Option = {
														Label = "Export selected",
														Icon = "image-down",
														Color = "gray",
													},
													OnClick = function()
														local path = currentPath:get(false) or ""
														local usedTextures = textures and textures:get(false)

														if not usedTextures then
															return
														end

														local items = traverseTextureList(usedTextures, path)
														local selectedItems = {}

														for i, isSelected in selectedIndices:get(false) do
															if isSelected then
																table.insert(selectedItems, items[i])
															end
														end

														isModalVisible:set(true)
														modalContent:set(copyContentModal({
															useColor = useColor,
															ClipboardContent = HttpService:JSONEncode(selectedItems),
															OnClose = function()
																isModalVisible:set(false)
															end,
														}))
													end,
												})
											end, Clean),
										},
									}
								)
							end)(),
						},
					}),
					New("Frame")({
						Name = "Content",
						BackgroundTransparency = 1,
						Position = UDim2.new(0, 0, 0, 0),
						Size = UDim2.new(1, 0, 1, -34),
						[Out("AbsoluteSize")] = contentSize,
						[Children] = {
							virtualScroller({
								Size = UDim2.new(1, 0, 1, 0),
								ItemCount = Computed(function()
									return #currentItems:get()
								end),
								ItemHeight = Value(EDITOR_BUTTON_SIZE + 4), -- +4 for padding
								ItemWidth = Value(EDITOR_BUTTON_SIZE + 4),
								CanvasPosition = Value(Vector2.new()),
								RenderItem = function(index: number)
									-- this seems like a really bad way to do this, but I can't think of anything better
									-- it's pretty creative I suppose?
									local doNotRefresh

									local function doRefresh()
										doNotRefresh = math.random()

										return doNotRefresh
									end

									local lastItem
									local didItemChange = Computed(function()
										local currentItem = currentItems:get()[index]

										if currentItem == nil then
											return doRefresh()
										end

										if
											lastItem
											and (
												currentItem.name ~= lastItem.name
												or (
													currentItem.id ~= lastItem.id
													or currentItem.content ~= lastItem.content
												)
											)
										then
											lastItem = table.clone(currentItem)
											return doRefresh()
										end

										lastItem = table.clone(currentItem)
										return doNotRefresh
									end)

									return Computed(function()
										didItemChange:get()

										local item = currentItems:get(false)[index]

										if item == nil then
											return nil
										end

										local isThisContextMenuVisible = Value(false)
										local options = {
											{
												Label = "Edit",
												Icon = "pencil",
												Color = "gray",
												Disabled = Computed(function()
													return next(selectedIndices:get()) ~= nil
												end),
												Callback = function()
													if isModalVisible:get(false) then
														return
													end

													isThisContextMenuVisible:set(false)
													modalContent:set(modifyTextureModal({
														OverwriteDefaults = {
															ID = item.id,
															Name = item.name,
															["Flipbook Layout"] = item.flipbookLayout
																	and Enum.ParticleFlipbookLayout["Grid" .. item.flipbookLayout]
																or Enum.ParticleFlipbookLayout.None,
														},
														IsFolder = item.id == nil,
														VerbUsed = "Edit",
														useColor = useColor,
														OnCreate = function(name, id, flipbookLayout)
															local path = currentPath:get(false) or ""
															local usedTextures = textures and textures:get(false)

															if not usedTextures then
																return
															end

															local items = traverseTextureList(usedTextures, path)

															-- make sure this is not a duplicate name, suffix with (x). filter on whether it's the current item though
															local newName = name
															local count = 1

															while
																tableFind(items, function(texture)
																	return texture.name == newName and texture.id ~= id
																end)
															do
																count = count + 1
																newName = name .. " (" .. count .. ")"
															end

															for idx, texture in items do
																if texture.name == item.name then
																	item.name = newName
																	item.id = id
																	item.flipbookLayout = flipbookLayout

																	items[idx] = item
																	break
																end
															end

															settingsManager.set("textures", usedTextures)
															isModalVisible:set(false)
														end,
														OnClose = function()
															isModalVisible:set(false)
														end,
													}))
													isModalVisible:set(true)
												end,
											} or nil,
											{
												Label = "Move",
												Icon = "folder-tree",
												Color = "gray",
												Callback = function()
													isModalVisible:set(true)
													isThisContextMenuVisible:set(false)
													modalContent:set(moveItemModal({
														IsFolder = item.content ~= nil,
														useColor = useColor,
														CurrentPath = currentPath:get(false) or { "Home" },
														Textures = Value(textures:get(false)),
														OnClose = function()
															isModalVisible:set(false)
														end,
														OnMove = function(newPath: { string })
															local usedTextures = textures and textures:get(false)

															if not usedTextures then
																return
															end

															local items = traverseTextureList(
																usedTextures,
																currentPath:get(false) or ""
															)

															if selectedIndices:get(false)[index] then
																local selectedItems = {}

																for i, isSelected in selectedIndices:get(false) do
																	if isSelected then
																		table.insert(selectedItems, items[i])
																	end
																end

																deleteIds(selectedItems)

																local newItems =
																	traverseTextureList(usedTextures, newPath)

																for _, selectedItem in selectedItems do
																	table.insert(newItems, selectedItem)
																end

																selectedIndices:set({})
															else
																deleteIds({ item })

																local newItems =
																	traverseTextureList(usedTextures, newPath)

																table.insert(newItems, item)
															end

															settingsManager.set("textures", usedTextures)
															isModalVisible:set(false)

															-- remove self from selected indices, and if we're in between any selected indices, move them forward by 1
															local newSelectedIndices = selectedIndices:get(false)

															for i, isSelected in newSelectedIndices do
																if isSelected then
																	if i == index then
																		newSelectedIndices[i] = nil
																	elseif i > index then
																		newSelectedIndices[i - 1] = true
																		newSelectedIndices[i] = nil
																	end
																end
															end

															selectedIndices:set(newSelectedIndices)
														end,
													}))
												end,
											},
											{
												Label = "Delete",
												Icon = "trash",
												Color = "gray",
												Callback = function()
													local function delete()
														local path = currentPath:get(false) or ""
														local usedTextures = textures and textures:get(false)

														if not usedTextures then
															return
														end

														local items = traverseTextureList(usedTextures, path)

														if selectedIndices:get(false)[index] then
															local selectedItems = {}

															for i, isSelected in selectedIndices:get(false) do
																if isSelected then
																	table.insert(selectedItems, items[i])
																end
															end

															deleteIds(selectedItems)
															selectedIndices:set({})
														else
															deleteIds({ item })
														end

														settingsManager.set("textures", usedTextures)
													end

													if
														(item.content and #item.content > 0)
														or selectedIndices:get(false)[index]
													then
														local textureChildren, folderChildren = 0, 0

														local function iterateItem(subItem)
															for _, child in subItem do
																if child.id then
																	textureChildren += 1
																else
																	folderChildren += 1
																	iterateItem(child.content)
																end
															end
														end

														local items = currentItems:get(false)

														for _, v in items do
															if v.name == item.name then
																iterateItem(v.content)
															end
														end

														for idx, _ in selectedIndices:get(false) do
															local child = items[idx]

															if child.id then
																textureChildren += 1
															else
																folderChildren += 1
																iterateItem(child.content)
															end
														end

														if folderChildren > 0 or textureChildren > 1 then
															isModalVisible:set(true)
															isThisContextMenuVisible:set(false)
															modalContent:set(confirmDeleteModal({
																useColor = useColor,
																ContentCount = {
																	Folders = folderChildren,
																	Textures = textureChildren,
																},
																OnClick = function(confirmed)
																	isModalVisible:set(false)

																	if confirmed then
																		delete()
																	end
																end,
															}))

															return
														end
													end

													delete()
												end,
											},
										}
										local function customVisibleLogic(toOpen)
											if toOpen then
												if isContextMenuVisible:get(false) then
													isContextMenuVisible:get(false):set(false)
												end

												isContextMenuVisible:set(isThisContextMenuVisible)
												isThisContextMenuVisible:set(true)
											else
												if isContextMenuVisible:get(false) == isThisContextMenuVisible then
													isContextMenuVisible:set(false)
												end

												isThisContextMenuVisible:set(false)
											end
										end

										local appearanceOverride = Computed(function()
											local color = theme.global.isDark:get() and tailwind.neutral[700]
												or tailwind.neutral[200]

											if selectedIndices:get()[index] then
												color = tailwind.blue[500]
											end

											return {
												_global = {
													Stroke = color,
												},
											}
										end)

										return contextMenu(
											button({
												Color = "gray",
												Variant = "solid",
												AppearanceOverride = appearanceOverride,
												LayoutOrder = index,
												Size = UDim2.new(1, -4, 1, -4), -- -4 for padding
												AutomaticSize = Enum.AutomaticSize.None,
												Position = UDim2.new(0, 2, 0, 2), -- +2 to center
												OnClick = function()
													if isCtrlHeld:get(false) then
														local newSelectedIndices = selectedIndices:get(false)
														newSelectedIndices[index] = not newSelectedIndices[index] or nil
														selectedIndices:set(newSelectedIndices)
													elseif isShiftHeld:get(false) then
														local newSelectedIndices = selectedIndices:get(false)
														local lastSelectedIndex = index

														for i, isSelected in newSelectedIndices do
															if isSelected then
																lastSelectedIndex = i
															end
														end

														if lastSelectedIndex then
															for i = math.min(index, lastSelectedIndex), math.max(index, lastSelectedIndex) do
																newSelectedIndices[i] = true
															end
														end

														selectedIndices:set(newSelectedIndices)
													elseif selectedIndices:get(false)[index] then
														selectedIndices:set({})
													else
														if item.id then
															-- texture
															if props.Value and props.IsSelectingImage then
																local flipbookLayout = item.flipbookLayout
																		and "Grid" .. item.flipbookLayout
																	or "None"
																props.Value:set(`{item.id},{flipbookLayout}`)
															end
														else
															-- folder
															local oldPath = table.clone(currentPath:get(false))
															table.insert(oldPath, item.name)

															currentPath:set(oldPath)
														end
													end
												end,
												Content = {
													New("UIListLayout")({
														FillDirection = Enum.FillDirection.Vertical,
														HorizontalAlignment = Enum.HorizontalAlignment.Center,
														Padding = UDim.new(0, 4),
														SortOrder = Enum.SortOrder.LayoutOrder,
														VerticalAlignment = Enum.VerticalAlignment.Center,
													}),
													item.id and textureImage({
														Image = item.id,
														FlipbookMode = item.flipbookLayout,
														Size = UDim2.new(0, 48, 0, 48),
														[Children] = {
															New("UICorner")({
																CornerRadius = UDim.new(0, 4),
															}),
														},
													}) or icon({
														Icon = {
															Name = "folder",
															Size = 48,
														},
														Color = useColor("Title", true),
													}),
													text({
														Appearance = useColor("Title", true),
														Text = {
															Label = item.name,
															Font = theme.global.font,
															TextSize = 14,
														},
														Size = UDim2.new(1, 0, 1, -56),
														TextWrapped = true,
														ClipsDescendants = true,
													}),
												},
											}),
											{
												Color = "gray",
												Variant = "default",
												Enabled = true,
												Visible = isThisContextMenuVisible,
												VisibleLogic = customVisibleLogic,
												Options = {
													options,
												},
											}
										)
									end, Clean)
								end,
							}),
						},
					}),
				},
			}),
		},
	})

	if not props.IsSelectingImage then
		awaitLayer(component, function(layer)
			local inputFrame = layer.contentFrame

			table.insert(
				connections,
				inputFrame.InputBegan:Connect(function(inputObject)
					if inputObject.UserInputType == Enum.UserInputType.Keyboard then
						if
							inputObject.KeyCode == Enum.KeyCode.LeftControl
							or inputObject.KeyCode == Enum.KeyCode.RightControl
						then
							isCtrlHeld:set(true)
						elseif
							inputObject.KeyCode == Enum.KeyCode.LeftShift
							or inputObject.KeyCode == Enum.KeyCode.RightShift
						then
							isShiftHeld:set(true)
						end
					end
				end)
			)

			table.insert(
				connections,
				inputFrame.InputEnded:Connect(function(inputObject)
					if inputObject.UserInputType == Enum.UserInputType.Keyboard then
						if
							inputObject.KeyCode == Enum.KeyCode.LeftControl
							or inputObject.KeyCode == Enum.KeyCode.RightControl
						then
							isCtrlHeld:set(false)
						elseif
							inputObject.KeyCode == Enum.KeyCode.LeftShift
							or inputObject.KeyCode == Enum.KeyCode.RightShift
						then
							isShiftHeld:set(false)
						end
					end
				end)
			)
		end)
	end

	updateBreadcrumb()

	return component
end

return editor


local HttpService = game:GetService("HttpService")

local cachedImages = {}

-- Function to get the width and height from a PNG buffer
local function getPngDimensions(pngBuffer)
	local header = pngBuffer:sub(1, 8)
	if header == "\137PNG\r\n\26\n" then
		local width = pngBuffer:byte(17) * 256 ^ 3
			+ pngBuffer:byte(18) * 256 ^ 2
			+ pngBuffer:byte(19) * 256
			+ pngBuffer:byte(20)
		local height = pngBuffer:byte(21) * 256 ^ 3
			+ pngBuffer:byte(22) * 256 ^ 2
			+ pngBuffer:byte(23) * 256
			+ pngBuffer:byte(24)
		return Vector2.new(width, height)
	end

	return nil
end

local function requestImage(id)
	if cachedImages[id] then
		return cachedImages[id]
	end

	local rawId = id:match("rbxassetid://(%d+)")

	if not rawId then
		return Vector2.new(0, 0)
	end

	local success, image = pcall(function()
		return HttpService:RequestAsync({
			Url = "https://assetdelivery.roproxy.com/v1/asset?id=" .. rawId,
			Method = "GET",
		})
	end)

	if not success then
		-- HTTP requests are disabled
		warn("HTTP requests are disabled! Cannot fetch image dimensions.")

		image = {
			Success = false,
		}
	end

	if image.Success then
		local dimensions = getPngDimensions(image.Body)
		if dimensions then
			cachedImages[id] = dimensions
		end

		return cachedImages[id]
	else
		return Vector2.new(0, 0)
	end
end

return requestImage


local HttpService = game:GetService("HttpService")

local fusion = require("@packages/fusion")
local Children = fusion.Children
local Cleanup = fusion.Cleanup

local Computed = fusion.Computed
local DoNothing = fusion.doNothing
local Value = fusion.Value

local fusionComponents = require("@packages/fusionComponents")
local button = fusionComponents.common.button

local fusionUtils = require("@packages/fusionUtils")
local topLayerProvider = fusionUtils.topLayerProvider

local widget = require("@components/widget")
local editor = require("./editor")

local settingsManager = require("@src/lib/settingsManager")

type props = {
	Instance: Instance,
	PropertyName: string,
	Value: fusion.Value<{
		Texture: string,
		FlipbookLayout: Enum.ParticleFlipbookLayout,
	}>,
	LayoutOrder: number,
}

local function texturePickerPropertyField(props: props)
	local isWidgetEnabled = Value(false)
	local wasEnabled = false
	local lastReturn = nil

	local textures: fusion.Value<editor.textureList>? = settingsManager.getReactiveWithFallback("textures", {})

	local textureName = Value()
	local texturePath = Computed(function()
		textureName:set(nil)

		local data = props.Value:get()
		local id = data and data:split(",")[1]

		if textures and textures:get() then
			local function iterate(textureList: editor.textureList, path: { string }): boolean?
				for _, texture in textureList do
					if texture.id == nil then
						local result = iterate(texture.content, path)

						if result then
							-- push down into table
							table.insert(path, texture.name)
							return result
						end
					elseif texture.id == id then
						textureName:set(texture.name)
						return true
					end
				end

				return nil
			end

			local path = {}
			iterate(textures:get(), path)

			-- invert path
			local newPath = { "Home" }

			for i = #path, 1, -1 do
				table.insert(newPath, path[i])
			end

			return newPath
		end

		return nil
	end)

	local createdWidget

	local imageButton = button({
		Color = "white",
		Variant = "solid",
		Icon = {
			Name = "image",
			Size = 16,
		},
		ButtonText = Computed(function()
			local path = texturePath:get()

			if not props.Value:get() or props.Value:get().Texture == "" or not textureName:get() then
				return "None"
			end

			if not path then
				return "Unknown"
			end

			return textureName:get()
		end),
		Padding = 2,
		Size = UDim2.new(0, 16, 0, 16),
		LayoutOrder = props.LayoutOrder,
		OnClick = function()
			if createdWidget == nil then
				createdWidget = widget({
					Name = `Texture Picker ({props.Instance}.{props.PropertyName})`,
					Id = HttpService:GenerateGUID(),
					InitialDockTo = Enum.InitialDockState.Float,
					InitialEnabled = false,
					ForceInitialEnabled = true,
					FloatingSize = Vector2.new(575, 408),
					MinimumSize = Vector2.new(400, 300),
					Enabled = isWidgetEnabled,
					[Children] = {
						Computed(function()
							local widgetEnabled = isWidgetEnabled:get()

							if widgetEnabled and not wasEnabled then
								wasEnabled = true

								lastReturn = ({
									topLayerProvider.new(editor({
										Value = props.Value,
										Instance = props.Instance,
										IsSelectingImage = true,
										Path = texturePath:get(false),
									})),
								})[1]
							end

							if not widgetEnabled then
								wasEnabled = false
							end

							return { lastReturn }
						end, DoNothing),
					},
				})
			end

			isWidgetEnabled:set(true)
		end,
		[Cleanup] = {
			function()
				-- https://devforum.roblox.com/t/2853087
				isWidgetEnabled:set(false)

				if createdWidget then
					createdWidget:Destroy()
				end
			end,
		},
	})

	return imageButton
end

return texturePickerPropertyField


local fusion = require("@packages/fusion")
local Children = fusion.Children
local New = fusion.New

local fusionComponents = require("@packages/fusionComponents")
local text = fusionComponents.base.text
local button = fusionComponents.common.button

local theme = require("@src/theme")

local function concatMultilineString(...: string)
	return table.concat({ ... }, "\n")
end

type props = {
	useColor: theme.useColorFunction,
	OnClick: (confirmed: boolean) -> (),
	ContentCount: {
		Textures: number,
		Folders: number,
	},
}

local function createFolderModal(props: props)
	local operationDone = false

	return New("Frame")({
		AutomaticSize = Enum.AutomaticSize.Y,
		BackgroundTransparency = 1,
		Size = UDim2.new(0, 300, 0, 0),
		[Children] = {
			New("UIListLayout")({
				FillDirection = Enum.FillDirection.Vertical,
				HorizontalAlignment = Enum.HorizontalAlignment.Center,
				Padding = UDim.new(0, 8),
				SortOrder = Enum.SortOrder.LayoutOrder,
				VerticalAlignment = Enum.VerticalAlignment.Top,
			}),
			text({
				Appearance = props.useColor("Title", true),
				Text = {
					Label = "Confirm deletion",
					Font = Font.new(theme.global.font.Family, Enum.FontWeight.SemiBold),
					TextSize = 18,
				},
				AnchorPoint = Vector2.new(0, 0.5),
				Position = UDim2.new(0, 0, 0.5, 0),
				Size = UDim2.new(1, 0, 0, 0),
				AutomaticSize = Enum.AutomaticSize.Y,
				TextXAlignment = Enum.TextXAlignment.Center,
			}),
			text({
				Appearance = props.useColor("Description", true),
				Text = {
					Label = concatMultilineString(
						"Are you sure that you want to remove these items and <b>irreversibly</b> remove all of their contents?",
						`\n<b>When deleted, {props.ContentCount.Folders} folder(s) and {props.ContentCount.Textures} texture(s) will be lost.</b>`
					),
					TextSize = 14,
					Font = theme.global.font,
				},
				Size = UDim2.new(0, 0, 0, 0),
				AutomaticSize = Enum.AutomaticSize.XY,
				TextWrapped = true,
				TextXAlignment = Enum.TextXAlignment.Left,
				RichText = true,
			}),
			New("Frame")({
				AutomaticSize = Enum.AutomaticSize.Y,
				BackgroundTransparency = 1,
				Size = UDim2.new(1, 0, 0, 0),
				[Children] = {
					New("UIListLayout")({
						FillDirection = Enum.FillDirection.Horizontal,
						HorizontalAlignment = Enum.HorizontalAlignment.Right,
						Padding = UDim.new(0, 4),
						SortOrder = Enum.SortOrder.LayoutOrder,
						VerticalAlignment = Enum.VerticalAlignment.Center,
					}),
					button({
						Color = "gray",
						Variant = "ghost",
						ButtonText = "Cancel",
						AutomaticSize = Enum.AutomaticSize.XY,
						Padding = 6,
						OnClick = function()
							if operationDone then
								return
							end

							props.OnClick(false)
							operationDone = true
						end,
					}),
					button({
						Color = "red",
						Variant = "solid",
						ButtonText = "Delete",
						AutomaticSize = Enum.AutomaticSize.XY,
						Padding = 6,
						OnClick = function()
							if operationDone then
								return
							end

							props.OnClick(true)
							operationDone = true
						end,
					}),
				},
			}),
		},
	})
end

return createFolderModal


local fusion = require("@packages/fusion")
local Children = fusion.Children
local New = fusion.New
local Cleanup = fusion.Cleanup

local fusionComponents = require("@packages/fusionComponents")
local text = fusionComponents.base.text
local input = fusionComponents.common.input
local button = fusionComponents.common.button

local theme = require("@src/theme")

type props = {
	useColor: theme.useColorFunction,
	ClipboardContent: string,
	OnClose: () -> (),
}

local function copyContentModal(props: props)
	local connections = {}

	local component = New("Frame")({
		AutomaticSize = Enum.AutomaticSize.Y,
		BackgroundTransparency = 1,
		Size = UDim2.new(0, 300, 0, 0),
		[Cleanup] = connections,
		[Children] = {
			New("UIListLayout")({
				FillDirection = Enum.FillDirection.Vertical,
				HorizontalAlignment = Enum.HorizontalAlignment.Center,
				Padding = UDim.new(0, 8),
				SortOrder = Enum.SortOrder.LayoutOrder,
				VerticalAlignment = Enum.VerticalAlignment.Top,
			}),
			New("Frame")({
				Name = "TopItems",
				AutomaticSize = Enum.AutomaticSize.Y,
				BackgroundTransparency = 1,
				Size = UDim2.new(1, 0, 0, 0),
				[Children] = {
					text({
						Appearance = props.useColor("Title", true),
						Text = {
							Label = "Copy to clipboard",
							Font = Font.new(theme.global.font.Family, Enum.FontWeight.SemiBold),
							TextSize = 18,
						},
						AnchorPoint = Vector2.new(0, 0.5),
						Position = UDim2.new(0, 0, 0.5, 0),
						Size = UDim2.new(1, 0, 0, 0),
						AutomaticSize = Enum.AutomaticSize.Y,
						TextXAlignment = Enum.TextXAlignment.Center,
					}),
					button({
						Color = "gray",
						Variant = "ghost",
						Icon = "x",
						AutomaticSize = Enum.AutomaticSize.XY,
						Position = UDim2.new(1, -24, 0, 0),
						OnClick = props.OnClose,
					}),
				},
			}),
			text({
				Appearance = props.useColor("Description", true),
				Text = {
					Label = "Click the text box below and press <b>Ctrl+C</b> to copy the following content:",
					TextSize = 14,
					Font = theme.global.font,
				},
				Size = UDim2.new(0, 0, 0, 0),
				AutomaticSize = Enum.AutomaticSize.XY,
				TextWrapped = true,
				TextXAlignment = Enum.TextXAlignment.Left,
				RichText = true,
			}),
			input({
				Color = "gray",
				Variant = "default",
				Size = UDim2.new(0, 200, 0, 24),
				Text = props.ClipboardContent,
			}),
		},
	})

	-- there is *no* reason for this to be as hard as it is but roblox for some reason requires active input in order to copy text
	local box: TextBox = component:FindFirstChild("InputBox", true)
	box.TextEditable = false

	table.insert(
		connections,
		box:GetPropertyChangedSignal("CursorPosition"):Connect(function()
			box.SelectionStart = 1
			box.CursorPosition = #props.ClipboardContent + 1
		end)
	)

	table.insert(
		connections,
		box:GetPropertyChangedSignal("SelectionStart"):Connect(function()
			box.SelectionStart = 1
			box.CursorPosition = #props.ClipboardContent + 1
		end)
	)

	box:CaptureFocus()

	return component
end

return copyContentModal


local fusion = require("@packages/fusion")
local Children = fusion.Children
local New = fusion.New

local Computed = fusion.Computed
local Value = fusion.Value

local fusionComponents = require("@packages/fusionComponents")
local text = fusionComponents.base.text
local button = fusionComponents.common.button

local theme = require("@src/theme")

local inputPropertyField = require("@components/propertyFields/input")
local modalOptionWrapper = require("@components/modalOptionWrapper")

type folder = {}

type props = {
	useColor: theme.useColorFunction,
	OnCreate: (name: string) -> (),
	OnClose: () -> (),
}

local function createFolderModal(props: props)
	local hasCreated = false

	local inputValue = Value("")
	local isConfirmDisabled = Computed(function()
		return not inputValue:get():match("^[%w%s-()]+$") or inputValue:get():match("^%s*$")
	end)

	return New("Frame")({
		AutomaticSize = Enum.AutomaticSize.Y,
		BackgroundTransparency = 1,
		Size = UDim2.new(0, 200, 0, 0),
		[Children] = {
			New("UIListLayout")({
				FillDirection = Enum.FillDirection.Vertical,
				HorizontalAlignment = Enum.HorizontalAlignment.Center,
				Padding = UDim.new(0, 8),
				SortOrder = Enum.SortOrder.LayoutOrder,
				VerticalAlignment = Enum.VerticalAlignment.Top,
			}),
			New("Frame")({
				Name = "TopItems",
				AutomaticSize = Enum.AutomaticSize.Y,
				BackgroundTransparency = 1,
				Size = UDim2.new(1, 0, 0, 0),
				[Children] = {
					text({
						Appearance = props.useColor("Title", true),
						Text = {
							Label = "Create Folder",
							Font = Font.new((theme.global.font).Family, Enum.FontWeight.SemiBold),
							TextSize = 18,
						},
						AnchorPoint = Vector2.new(0, 0.5),
						Position = UDim2.new(0, 0, 0.5, 0),
						Size = UDim2.new(1, 0, 0, 0),
						AutomaticSize = Enum.AutomaticSize.Y,
						TextXAlignment = Enum.TextXAlignment.Center,
					}),
					button({
						Color = "gray",
						Variant = "ghost",
						Icon = "x",
						AutomaticSize = Enum.AutomaticSize.XY,
						Position = UDim2.new(1, -24, 0, 0),
						OnClick = props.OnClose,
					}),
				},
			}),
			modalOptionWrapper({
				OptionName = "Name",
				Content = {
					inputPropertyField({
						Value = inputValue,
						LayoutOrder = 1,
					}),
				},
			}),
			button({
				Color = "gray",
				Variant = "solid",
				ButtonText = "Create folder",
				AutomaticSize = Enum.AutomaticSize.XY,
				Disabled = isConfirmDisabled,
				OnClick = function()
					if hasCreated then
						return
					end

					props.OnCreate(inputValue:get())
					hasCreated = true
				end,
			}),
		},
	})
end

return createFolderModal


local Selection = game:GetService("Selection")

local fusion = require("@packages/fusion")
local Children = fusion.Children
local Cleanup = fusion.Cleanup
local New = fusion.New

local Computed = fusion.Computed
local Value = fusion.Value

local fusionComponents = require("@packages/fusionComponents")
local text = fusionComponents.base.text
local button = fusionComponents.common.button
local frame = fusionComponents.base.frame
local scrollFrame = fusionComponents.common.scrollingFrame

local theme = require("@src/theme")

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

local classDataHandler = require("@src/lib/classDataHandler")

type texture = {
	name: string,
	id: string,
	flipbookLayout: nil | "2x2" | "4x4" | "8x8",
}

export type textureList = {
	name: string,
	content: { texture | textureList },
}

local function traverseTextureList(textureList: { textureList }, path: { string }): { texture | textureList }
	local items = textureList

	for _, label in path do
		if label == "" then
			continue
		end

		for _, item in items do
			if item.name == label then
				items = item.content
				break
			end
		end
	end

	return items
end

local function deepCopy<T>(t: T & {}): T
	local copy = {}

	for k, v in t :: { any } do
		if type(v) == "table" then
			copy[k] = deepCopy(v)
		else
			copy[k] = v
		end
	end

	return copy :: any
end

local function concatMultilineString(...: string)
	return table.concat({ ... }, "\n")
end

type props = {
	useColor: theme.useColorFunction,
	BaseTextures: { textureList },
	Path: { string },
	OnConfirm: (textures: { textureList }) -> (),
	OnClose: () -> (),
}

local function importInstancesModal(props: props)
	local operationHandled = false
	local selectedItems = Value(Selection:Get())

	local visualizedTextures = Value({}) -- same as textures, but the new textures are visualized here

	local currentTextures = Computed(function()
		local textures = deepCopy(props.BaseTextures)
		local visualizationTextures = deepCopy(textures)

		--[[

            If it is an instance that has properties, and one of those is Texture, then add it to the list
            If the item is a folder, nest it's children
            If the instance name is it's classname, use the parent name instead

        ]]

		local function insertIntoTable(t, content)
			for _, v in content do
				-- if t contains an item whose name is the item that we're trying to insert, we need to add a suffix of (x) to the name
				local newName = v.name
				local i = 1

				while true do
					local found = false

					for _, item in t do
						if item.name == newName then
							found = true
							break
						end
					end

					if not found then
						break
					end

					i = i + 1
					newName = v.name .. " (" .. i .. ")"
				end

				v.name = newName

				table.insert(t, v)
			end
		end

		local function iterateChildren(children: { Instance }): { texture | textureList }
			local result: { texture | textureList } = {}
			local visualizationResult: { texture | textureList } = {}

			for _, child in children do
				local categories = classDataHandler.getProperties(child)
				local hasInserted = false

				if categories then
					for _, category in categories do
						local properties = category.properties and category.properties or { category }

						for _, property in properties do
							if property.name == "Texture" then
								local name = child.Name

								if name == child.ClassName then
									name = child.Parent.Name
								end

								name = name:gsub("[^%w%s%-()]", "")

								table.insert(result, {
									name = name,
									id = child.Texture,
									flipbookLayout = child:IsA("ParticleEmitter") and child.FlipbookLayout.Name:split(
										"Grid"
									)[2] or nil,
								})

								table.insert(visualizationResult, {
									name = name,
									content = {},
									isItem = true,
									icon = theme.getClassIcon(child.ClassName):get(),
								})

								hasInserted = true
							end
						end
					end
				end

				if child:IsA("Decal") then
					local name = child.Name

					if name == child.ClassName then
						name = child.Parent.Name
					end

					name = name:gsub("[^%w%s%-()]", "")

					table.insert(result, {
						name = name,
						id = child.Texture,
					})

					table.insert(visualizationResult, {
						name = name,
						content = {},
						isItem = true,
						icon = theme.getClassIcon(child.ClassName):get(),
					})

					hasInserted = true
				end

				-- if nothing has been found yet, then iterate through the children
				if not hasInserted then
					if child:IsA("Folder") or child:IsA("Model") then
						local data, visualizationData = iterateChildren(child:GetChildren())

						if #data > 0 then
							table.insert(result, {
								name = child.Name,
								content = data,
							})

							table.insert(visualizationResult, {
								name = child.Name,
								content = visualizationData,
							})
						end
					else
						local data, visualizationData = iterateChildren(child:GetChildren())

						if #data > 0 then
							insertIntoTable(result, data)
							insertIntoTable(visualizationResult, visualizationData)
						end
					end
				end
			end

			return result, visualizationResult
		end

		local targetTable = traverseTextureList(textures, props.Path)
		local targetVisualizationTable = traverseTextureList(visualizationTextures, props.Path)

		local newContent, visualizationContent = iterateChildren(selectedItems:get())

		if #newContent > 0 then
			if targetTable.content then
				insertIntoTable(targetTable.content, newContent)
				insertIntoTable(targetVisualizationTable, visualizationContent)
			else
				insertIntoTable(targetTable, newContent)
				insertIntoTable(targetVisualizationTable, visualizationContent)
			end
		end

		visualizedTextures:set(traverseTextureList(visualizationTextures, props.Path))
		return textures
	end)

	local isConfirmDisabled = Computed(function()
		-- compare basetextures with currentTextures
		-- if they match, then disable the confirm button
		-- they are both tables however, so compare their contents recursively
		local function compareTables(a, b)
			if type(a) ~= type(b) then
				return false
			end
			if type(a) ~= "table" then
				return a == b
			end
			for k, v in a do
				if not compareTables(v, b[k]) then
					return false
				end
			end
			return true
		end

		return compareTables(currentTextures:get(), props.BaseTextures)
	end)

	if props.Path[1] == "" then
		props.Path[1] = "Home"
	end

	return New("Frame")({
		AutomaticSize = Enum.AutomaticSize.Y,
		BackgroundTransparency = 1,
		Size = UDim2.new(0, 300, 0, 0),
		[Cleanup] = {
			Selection.SelectionChanged:Connect(function()
				selectedItems:set(Selection:Get())
			end),
		},
		[Children] = {
			New("UIListLayout")({
				FillDirection = Enum.FillDirection.Vertical,
				HorizontalAlignment = Enum.HorizontalAlignment.Center,
				Padding = UDim.new(0, 8),
				SortOrder = Enum.SortOrder.LayoutOrder,
				VerticalAlignment = Enum.VerticalAlignment.Top,
			}),
			New("Frame")({
				Name = "TopItems",
				AutomaticSize = Enum.AutomaticSize.Y,
				BackgroundTransparency = 1,
				Size = UDim2.new(1, 0, 0, 0),
				[Children] = {
					text({
						Appearance = props.useColor("Title", true),
						Text = {
							Label = "Import from Selection",
							Font = Font.new(theme.global.font.Family, Enum.FontWeight.SemiBold),
							TextSize = 18,
						},
						AnchorPoint = Vector2.new(0, 0.5),
						Position = UDim2.new(0, 0, 0.5, 0),
						Size = UDim2.new(1, 0, 0, 0),
						AutomaticSize = Enum.AutomaticSize.Y,
						TextXAlignment = Enum.TextXAlignment.Center,
					}),
					button({
						Color = "gray",
						Variant = "ghost",
						Icon = "x",
						AutomaticSize = Enum.AutomaticSize.XY,
						Position = UDim2.new(1, -24, 0, 0),
						OnClick = props.OnClose,
					}),
				},
			}),
			text({
				Appearance = props.useColor("Description", true),
				Text = {
					Label = concatMultilineString(
						"This feature attempts to import instances that are selected in the explorer into the texture picker.",
						"Please note that this feature is experimental and may not work as expected.",
						"\n<b>Double-check the imported textures before confirming.</b>"
					),
					TextSize = 14,
					Font = theme.global.font,
				},
				Size = UDim2.new(0, 0, 0, 0),
				AutomaticSize = Enum.AutomaticSize.XY,
				TextWrapped = true,
				TextXAlignment = Enum.TextXAlignment.Left,
				RichText = true,
			}),
			frame({
				Appearance = props.useColor("LighterBackground", true),
				Stroke = props.useColor("Line", true),
				Size = UDim2.new(0, 200, 0, 200),
				Content = {
					New("UIPadding")({
						PaddingBottom = UDim.new(0, 4),
						PaddingLeft = UDim.new(0, 4),
						PaddingRight = UDim.new(0, 4),
						PaddingTop = UDim.new(0, 4),
					}),
					scrollFrame({
						Content = {
							New("UIPadding")({
								PaddingRight = UDim.new(0, 4),
							}),
							New("UIListLayout")({
								FillDirection = Enum.FillDirection.Vertical,
								HorizontalAlignment = Enum.HorizontalAlignment.Left,
								HorizontalFlex = Enum.UIFlexAlignment.Fill,
								Padding = UDim.new(0, 2),
								SortOrder = Enum.SortOrder.LayoutOrder,
								VerticalAlignment = Enum.VerticalAlignment.Top,
							}),
							treeButton({
								Name = props.Path[#props.Path],
								CurrentPath = Value(props.Path),
								Content = visualizedTextures,
								Path = table.clone(props.Path),
							}),
						},
						ScrollingFrameProps = {
							VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar,
							AutomaticCanvasSize = Enum.AutomaticSize.Y,
						},
						Size = UDim2.new(1, 0, 1, 0),
					}),
				},
			}),
			button({
				Color = "gray",
				Variant = "solid",
				ButtonText = "Import Items",
				AutomaticSize = Enum.AutomaticSize.XY,
				Disabled = isConfirmDisabled,
				OnClick = function()
					if operationHandled then
						return
					end

					operationHandled = true

					props.OnConfirm(currentTextures:get())
				end,
			}),
		},
	})
end

return importInstancesModal


local HttpService = game:GetService("HttpService")
local Selection = game:GetService("Selection")

local fusion = require("@packages/fusion")
local Children = fusion.Children
local Cleanup = fusion.Cleanup
local New = fusion.New

local Computed = fusion.Computed
local Value = fusion.Value

local fusionComponents = require("@packages/fusionComponents")
local text = fusionComponents.base.text
local button = fusionComponents.common.button
local frame = fusionComponents.base.frame
local scrollFrame = fusionComponents.common.scrollingFrame

local theme = require("@src/theme")

local treeButton = require("../treeButton")
local modalOptionWrapper = require("@components/modalOptionWrapper")
local inputPropertyField = require("@components/propertyFields/input")

type texture = {
	name: string,
	id: string,
	flipbookLayout: nil | "2x2" | "4x4" | "8x8",
}

export type textureList = {
	name: string,
	content: { texture | textureList },
}

local function traverseTextureList(textureList: { textureList }, path: { string }): { texture | textureList }
	local items = textureList

	for _, label in path do
		if label == "" then
			continue
		end

		for _, item in items do
			if item.name == label then
				items = item.content
				break
			end
		end
	end

	return items
end

local function deepCopy<T>(t: T & {}): T
	local copy = {}

	for k, v in t :: { any } do
		if type(v) == "table" then
			copy[k] = deepCopy(v)
		else
			copy[k] = v
		end
	end

	return copy :: any
end

type props = {
	useColor: theme.useColorFunction,
	BaseTextures: { textureList },
	Path: { string },
	OnConfirm: (textures: { textureList }) -> (),
	OnClose: () -> (),
}

local function importInstancesModal(props: props)
	local operationHandled = false
	local selectedItems = Value(Selection:Get())
	local inputValue = Value("")

	local visualizedTextures = Value({}) -- same as textures, but the new textures are visualized here

	local currentTextures = Computed(function()
		local textures = deepCopy(props.BaseTextures)
		local visualizationTextures = deepCopy(textures)

		local function insertIntoTable(t, content: { textureList | texture })
			for _, v in content do
				-- if t contains an item whose name is the item that we're trying to insert, we need to add a suffix of (x) to the name
				local newName = v.name
				local i = 1

				while true do
					local found = false

					for _, item in t do
						if item.name == newName then
							found = true
							break
						end
					end

					if not found then
						break
					end

					i = i + 1
					newName = v.name .. " (" .. i .. ")"
				end

				v.name = newName
				table.insert(t, v)
			end
		end

		local targetTable = traverseTextureList(textures, props.Path)
		local targetVisualizationTable = traverseTextureList(visualizationTextures, props.Path)

		-- parse json
		local succ, parsed = pcall(function()
			return HttpService:JSONDecode(inputValue:get())
		end)

		if succ and typeof(parsed) == "table" then
			local parsedCloned = deepCopy(parsed)
			-- this is an array of textures or texture lists.
			-- we need to insert them into the target table
			insertIntoTable(targetTable, parsed)

			-- for visualization, we need to tag all texture ITEMS (so the ones that have .id) with .isItem = true. this should be recursive
			local function tagItems(content)
				for _, v in content do
					if v.id then
						v.isItem = true
						v.content = {}
					else
						tagItems(v.content)
					end
				end
			end

			tagItems(parsedCloned)

			-- now we need to insert the visualization textures into the visualization table
			insertIntoTable(targetVisualizationTable, parsed)
		end

		visualizedTextures:set(traverseTextureList(visualizationTextures, props.Path))
		return textures
	end)

	local isConfirmDisabled = Computed(function()
		-- compare basetextures with currentTextures
		-- if they match, then disable the confirm button
		-- they are both tables however, so compare their contents recursively
		local function compareTables(a, b)
			if type(a) ~= type(b) then
				return false
			end
			if type(a) ~= "table" then
				return a == b
			end
			for k, v in a do
				if not compareTables(v, b[k]) then
					return false
				end
			end
			return true
		end

		return compareTables(currentTextures:get(), props.BaseTextures)
	end)

	return New("Frame")({
		AutomaticSize = Enum.AutomaticSize.Y,
		BackgroundTransparency = 1,
		Size = UDim2.new(0, 300, 0, 0),
		[Cleanup] = {
			Selection.SelectionChanged:Connect(function()
				selectedItems:set(Selection:Get())
			end),
		},
		[Children] = {
			New("UIListLayout")({
				FillDirection = Enum.FillDirection.Vertical,
				HorizontalAlignment = Enum.HorizontalAlignment.Center,
				Padding = UDim.new(0, 8),
				SortOrder = Enum.SortOrder.LayoutOrder,
				VerticalAlignment = Enum.VerticalAlignment.Top,
			}),
			New("Frame")({
				Name = "TopItems",
				AutomaticSize = Enum.AutomaticSize.Y,
				BackgroundTransparency = 1,
				Size = UDim2.new(1, 0, 0, 0),
				[Children] = {
					text({
						Appearance = props.useColor("Title", true),
						Text = {
							Label = "Import from JSON",
							Font = Font.new(theme.global.font.Family, Enum.FontWeight.SemiBold),
							TextSize = 18,
						},
						AnchorPoint = Vector2.new(0, 0.5),
						Position = UDim2.new(0, 0, 0.5, 0),
						Size = UDim2.new(1, 0, 0, 0),
						AutomaticSize = Enum.AutomaticSize.Y,
						TextXAlignment = Enum.TextXAlignment.Center,
					}),
					button({
						Color = "gray",
						Variant = "ghost",
						Icon = "x",
						AutomaticSize = Enum.AutomaticSize.XY,
						Position = UDim2.new(1, -24, 0, 0),
						OnClick = props.OnClose,
					}),
				},
			}),
			text({
				Appearance = props.useColor("Description", true),
				Text = {
					Label = Computed(function()
						local baseString =
							"Insert JSON in the text box below. You can see the new file structure below the text box."

						if isConfirmDisabled:get() and #inputValue:get() > 0 then
							return baseString
								.. ' \n\nThe JSON is invalid, or the file structure is unchanged.'
						end

						return baseString .. "\n\nMake sure the changes are correct before importing."
					end),
					TextSize = 14,
					Font = theme.global.font,
				},
				Size = UDim2.new(0, 0, 0, 0),
				AutomaticSize = Enum.AutomaticSize.XY,
				TextWrapped = true,
				TextXAlignment = Enum.TextXAlignment.Left,
				RichText = true,
			}),
			modalOptionWrapper({
				OptionName = "JSON",
				Content = inputPropertyField({
					Value = inputValue,
					LayoutOrder = 1,
				}),
			}),
			frame({
				Appearance = props.useColor("LighterBackground", true),
				Stroke = props.useColor("Line", true),
				Size = UDim2.new(0, 200, 0, 200),
				Content = {
					New("UIPadding")({
						PaddingBottom = UDim.new(0, 4),
						PaddingLeft = UDim.new(0, 4),
						PaddingRight = UDim.new(0, 4),
						PaddingTop = UDim.new(0, 4),
					}),
					scrollFrame({
						Content = {
							New("UIPadding")({
								PaddingRight = UDim.new(0, 4),
							}),
							New("UIListLayout")({
								FillDirection = Enum.FillDirection.Vertical,
								HorizontalAlignment = Enum.HorizontalAlignment.Left,
								HorizontalFlex = Enum.UIFlexAlignment.Fill,
								Padding = UDim.new(0, 2),
								SortOrder = Enum.SortOrder.LayoutOrder,
								VerticalAlignment = Enum.VerticalAlignment.Top,
							}),
							treeButton({
								Name = props.Path[#props.Path],
								CurrentPath = Value(props.Path),
								Content = visualizedTextures,
								Path = props.Path,
							}),
						},
						ScrollingFrameProps = {
							VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar,
							AutomaticCanvasSize = Enum.AutomaticSize.Y,
						},
						Size = UDim2.new(1, 0, 1, 0),
					}),
				},
			}),
			button({
				Color = "gray",
				Variant = "solid",
				ButtonText = "Import Items",
				AutomaticSize = Enum.AutomaticSize.XY,
				Disabled = isConfirmDisabled,
				OnClick = function()
					if operationHandled then
						return
					end

					operationHandled = true

					props.OnConfirm(currentTextures:get())
				end,
			}),
		},
	})
end

return importInstancesModal


local fusion = require("@packages/fusion")
local New = fusion.New
local Children = fusion.Children

local Value = fusion.Value

local fusionComponents = require("@packages/fusionComponents")
local text = fusionComponents.base.text
local button = fusionComponents.common.button
local frame = fusionComponents.base.frame
local scrollFrame = fusionComponents.common.scrollingFrame

local theme = require("@src/theme")

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

type props = {
	useColor: theme.useColorFunction,
	OnMove: (path: { string }) -> (),
	OnClose: () -> (),
	Textures: fusion.Value<any>, -- no need to type this, it's just to pass to a child component
	CurrentPath: { string },
	IsFolder: boolean,
}

local function moveItemModal(props: props)
	local selectedPath = Value(props.CurrentPath)
	local isConfirmDisabled = Value(true)

	return New("Frame")({
		AutomaticSize = Enum.AutomaticSize.Y,
		BackgroundTransparency = 1,
		Size = UDim2.new(0, 200, 0, 0),
		[Children] = {
			New("UIListLayout")({
				FillDirection = Enum.FillDirection.Vertical,
				HorizontalAlignment = Enum.HorizontalAlignment.Center,
				Padding = UDim.new(0, 8),
				SortOrder = Enum.SortOrder.LayoutOrder,
				VerticalAlignment = Enum.VerticalAlignment.Top,
			}),
			New("Frame")({
				Name = "TopItems",
				AutomaticSize = Enum.AutomaticSize.Y,
				BackgroundTransparency = 1,
				Size = UDim2.new(1, 0, 0, 0),
				[Children] = {
					text({
						Appearance = props.useColor("Title", true),
						Text = {
							Label = "Move Item",
							Font = Font.new(theme.global.font.Family, Enum.FontWeight.SemiBold),
							TextSize = 18,
						},
						AnchorPoint = Vector2.new(0, 0.5),
						Position = UDim2.new(0, 0, 0.5, 0),
						Size = UDim2.new(1, 0, 0, 0),
						AutomaticSize = Enum.AutomaticSize.Y,
						TextXAlignment = Enum.TextXAlignment.Center,
					}),
					button({
						Color = "gray",
						Variant = "ghost",
						Icon = "x",
						AutomaticSize = Enum.AutomaticSize.XY,
						Position = UDim2.new(1, -24, 0, 0),
						OnClick = props.OnClose,
					}),
				},
			}),
			frame({
				Appearance = props.useColor("LighterBackground", true),
				Stroke = props.useColor("Line", true),
				Size = UDim2.new(0, 200, 0, 200),
				Content = {
					scrollFrame({
						Content = {
							New("UIPadding")({
								PaddingBottom = UDim.new(0, 4),
								PaddingLeft = UDim.new(0, 4),
								PaddingRight = UDim.new(0, 4),
								PaddingTop = UDim.new(0, 4),
							}),
							New("UIListLayout")({
								FillDirection = Enum.FillDirection.Vertical,
								HorizontalAlignment = Enum.HorizontalAlignment.Left,
								HorizontalFlex = Enum.UIFlexAlignment.Fill,
								Padding = UDim.new(0, 2),
								SortOrder = Enum.SortOrder.LayoutOrder,
								VerticalAlignment = Enum.VerticalAlignment.Top,
							}),
							treeButton({
								Name = "Home",
								CurrentPath = selectedPath,
								Content = props.Textures,
								DisabledCascades = props.IsFolder,
								DisabledPath = props.CurrentPath,
								OnClick = function(path)
									selectedPath:set(path)
									isConfirmDisabled:set(false)
								end,
								Path = { "Home" },
							}),
						},
						ScrollingFrameProps = {
							VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar,
							AutomaticCanvasSize = Enum.AutomaticSize.Y,
						},
						Size = UDim2.new(1, 0, 1, 0),
					}),
				},
			}),
			button({
				Color = "gray",
				Variant = "solid",
				ButtonText = "Move item",
				AutomaticSize = Enum.AutomaticSize.XY,
				Disabled = isConfirmDisabled,
				OnClick = function()
					props.OnMove(selectedPath:get())
				end,
			}),
		},
	})
end

return moveItemModal


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

local fusion = require("@packages/fusion")
local Children = fusion.Children
local Cleanup = fusion.Cleanup
local Hydrate = fusion.Hydrate
local New = fusion.New

local Computed = fusion.Computed
local Value = fusion.Value

local fusionUtils = require("@packages/fusionUtils")
local propertyProvider = fusionUtils.propertyProvider

local requestImage = require("../getImageResolution")

type props = {
	Image: string,
	FlipbookMode: "2x2" | "4x4" | "8x8",
}

local FLIPBOOK_MAPS = {
	["2x2"] = Vector2.new(2, 2),
	["4x4"] = Vector2.new(4, 4),
	["8x8"] = Vector2.new(8, 8),
}

local function flipbook(props: props)
	local imageSize = Vector2.new(0, 0)

	local resolution = FLIPBOOK_MAPS[props.FlipbookMode].X * FLIPBOOK_MAPS[props.FlipbookMode].Y
	local cleanup = {}

	local editableImage = Value(nil)
	local didFinish = Value(false)

	local component

	component = New("ImageLabel")({
		BackgroundTransparency = 1,
		Image = Computed(function()
			if didFinish:get() and editableImage:get() == nil then
				return props.Image
			end

			return ""
		end),
		ImageTransparency = 0,
		ScaleType = Enum.ScaleType.Fit,
		[Cleanup] = cleanup,
		[Children] = {
			editableImage,
		},
	})

	task.spawn(function()
		pcall(function()
			editableImage:set(AssetService:CreateEditableImageAsync(props.Image))
		end)

		didFinish:set(true)

		if editableImage:get(false) == nil then
			imageSize = requestImage(props.Image)

			if imageSize == nil then
				return
			end

			component.ImageRectSize = Vector2.new(
				imageSize.X / FLIPBOOK_MAPS[props.FlipbookMode].X,
				imageSize.Y / FLIPBOOK_MAPS[props.FlipbookMode].Y
			)
		else
			imageSize = editableImage:get(false).Size

			component.ImageRectSize = Vector2.new(
				imageSize.X / FLIPBOOK_MAPS[props.FlipbookMode].X,
				imageSize.Y / FLIPBOOK_MAPS[props.FlipbookMode].Y
			)
		end
	end)

	local timer = 0
	table.insert(
		cleanup,
		RunService.RenderStepped:Connect(function(dt)
			if imageSize == nil then
				return
			end

			timer += dt

			local frame = math.floor(timer * resolution) % resolution
			local x = frame % FLIPBOOK_MAPS[props.FlipbookMode].X
			local y = math.floor(frame / FLIPBOOK_MAPS[props.FlipbookMode].X)

			local offset = Vector2.new(
				x * imageSize.X / FLIPBOOK_MAPS[props.FlipbookMode].X,
				y * imageSize.Y / FLIPBOOK_MAPS[props.FlipbookMode].Y
			)

			component.ImageRectOffset = offset
		end)
	)

	return Hydrate(component)(propertyProvider.getPropsAndSpecialKeys(props, "ImageLabel"))
end

return flipbook


local flipbook = require("./flipbook")
local texture = require("./texture")

type props = {
	Image: string,
	FlipbookMode: nil | "2x2" | "4x4" | "8x8",
}

local function textureImage(props: props)
	if props.FlipbookMode then
		-- for some reason props.FlipbookMode does not evaluate to not nil after this, so we typecast props to any
		return flipbook(props :: any)
	else
		return texture(props)
	end
end

return textureImage


local AssetService = game:GetService("AssetService")

local fusion = require("@packages/fusion")
local Children = fusion.Children
local Hydrate = fusion.Hydrate
local New = fusion.New

local Computed = fusion.Computed
local Value = fusion.Value

local fusionUtils = require("@packages/fusionUtils")
local propertyProvider = fusionUtils.propertyProvider

type props = {
	Image: string,
	FlipbookMode: nil | "2x2" | "4x4" | "8x8",
}

local function texture(props: props)
	local editableImage = Value(nil)
	local didFinish = Value(false)

	task.spawn(function()
		pcall(function()
			editableImage:set(AssetService:CreateEditableImageAsync(props.Image))
		end)

		didFinish:set(true)
	end)

	local component = New("ImageLabel")({
		BackgroundTransparency = 1,
		Image = Computed(function()
			if didFinish:get() and editableImage:get() == nil then
				return props.Image
			end

			return ""
		end),
		ImageTransparency = 0,
		ScaleType = Enum.ScaleType.Fit,
		[Children] = {
			editableImage,
		},
	})

	return Hydrate(component)(propertyProvider.getPropsAndSpecialKeys(props, "ImageLabel"))
end

return texture


local fusion = require("@packages/fusion")
local Children = fusion.Children
local Cleanup = fusion.Cleanup
local New = fusion.New

local Computed = fusion.Computed
local Observer = fusion.Observer
local Value = fusion.Value

local fusionComponents = require("@packages/fusionComponents")
local menuEntry = fusionComponents.utility.menuEntry
local baseText = fusionComponents.base.text
local icon = fusionComponents.base.icon

local fusionUtils = require("@packages/fusionUtils")
local use = fusionUtils.use

type textureList = {
	name: string,
	content: textureList?,
}

local function tableEq(a, b)
	if #a ~= #b then
		return false
	end

	for i = 1, #a do
		if a[i] ~= b[i] then
			return false
		end
	end

	return true
end

type props = {
	Name: string,
	CurrentPath: fusion.StateObject<string>,
	Content: fusion.CanBeState<{ textureList }>, -- no need to type it entirely, we're only interested in the folders anyways
	OnClick: ((path: { string }) -> nil)?,
	Path: { string },
	Query: fusion.Value<string>?,
	DisabledPath: { string }?,
	DisabledCascades: boolean?,
	IsSelfDisabled: boolean?,
	Icon: string?,
	IsItem: boolean?, -- the isItem tag is used to determine if the treeButton is a texture or a folder - we want to show treebuttons for textures sometimes, but not always
}

local function treeButton(props: props)
	local isDisabled = props.DisabledPath and tableEq(props.Path, props.DisabledPath) or props.IsSelfDisabled

	local subButtons = Value({})

	local function refreshData(content)
		local newSubButtons = {}

		for index, value in content do
			if value.content then
				local clonedPath = table.clone(props.Path)
				table.insert(clonedPath, value.name)

				newSubButtons[index] = treeButton({
					Name = value.name,
					Icon = value.icon,
					CurrentPath = props.CurrentPath,
					Content = value.content,
					OnClick = props.OnClick,
					Path = clonedPath,
					Query = props.Query,
					DisabledPath = props.DisabledPath,
					DisabledCascades = props.DisabledCascades,
					IsSelfDisabled = props.DisabledCascades and isDisabled,
					IsItem = value.isItem,
				})
			end
		end

		subButtons:set(newSubButtons)
	end

	local contentObserver
	if typeof(props.Content) == "table" and (props.Content :: any).get then
		local content = props.Content :: fusion.Value<{ textureList }>
		contentObserver = Observer(props.Content):onChange(function()
			refreshData(content:get())
		end)

		refreshData(content:get())
	else
		refreshData(props.Content)
	end

	local amountOfTextures = Computed(function()
		local count = 0
		local query = props.Query and props.Query:get():lower() or ""

		for _, value in use(props.Content) do
			if not value.content and (query == "" or value.name:lower():find(query)) or value.isItem then
				count += 1
			end
		end

		return count
	end)

	local buttonIcon = typeof(props.Icon) == "string" and {
		Image = props.Icon,
	} or props.Icon or {}

	return New("Frame")({
		Name = props.Name,
		AutomaticSize = Enum.AutomaticSize.Y,
		BackgroundTransparency = 1,
		Size = UDim2.new(1, 0, 0, 0),
		Visible = Computed(function()
			-- iterate through ALL children to see if any of them match the query
			-- this is recursive, and also iterates through descendants
			local query = props.Query and props.Query:get():lower() or ""

			local function iterate(content: { textureList }): boolean
				for _, value in content do
					if value.name:lower():find(query) then
						return true
					end

					if value.content and iterate(value.content) then
						return true
					end
				end

				return false
			end

			return query == "" or props.Name:lower():find(query) or iterate(use(props.Content))
		end),
		[Cleanup] = {
			contentObserver,
		},
		[Children] = {
			New("UIListLayout")({
				FillDirection = Enum.FillDirection.Vertical,
				HorizontalAlignment = Enum.HorizontalAlignment.Right,
				Padding = Computed(function()
					if next(subButtons:get()) then
						return UDim.new(0, 4)
					end

					return UDim.new(0, 0)
				end),
				SortOrder = Enum.SortOrder.LayoutOrder,
				VerticalAlignment = Enum.VerticalAlignment.Top,
			}),
			New("Frame")({
				Name = "MenuEntryContainer",
				AutomaticSize = Enum.AutomaticSize.Y,
				BackgroundTransparency = 1,
				ClipsDescendants = true,
				Size = UDim2.new(1, 0, 0, 0),
				[Children] = {
					menuEntry({
						Option = {
							Label = props.Name,
							Color = "gray",
							Icon = props.Name == "Home" and "home" or buttonIcon.Image,
							Disabled = isDisabled,
							Slots = {
								Left = buttonIcon.Image and function(useSlotColor)
									return icon({
										Color = useSlotColor("Text", true),
										Icon = props.Name == "Home" and "home" or buttonIcon.Image,
										ImageRectSize = buttonIcon.ImageRectSize,
										ImageRectOffset = buttonIcon.ImageRectOffset,
									})
								end or nil,
								Center = function(useSlotColor)
									return baseText({
										Name = "Label",
										Appearance = useSlotColor("Text", true),
										Text = Computed(function()
											return `{props.Name} <font transparency="0.5">({props.IsItem ~= true and amountOfTextures:get() or "+"})</font>`
										end),
										RichText = true,
										AutomaticSize = Enum.AutomaticSize.XY,
										BackgroundTransparency = 1,
										TextXAlignment = Enum.TextXAlignment.Left,
										TextSize = 16,
									})
								end,
							},
						},
						OnClick = function()
							if props.OnClick then
								props.OnClick(props.Path)
							end
						end,
						Selected = Computed(function()
							if props.CurrentPath:get() == "" and props.Name == "Home" then
								return true
							end

							return tableEq(props.Path, props.CurrentPath:get())
						end),
						Size = UDim2.new(1, 0, 0, 0),
					}),
				},
			}),
			New("Frame")({
				Name = "SubContent",
				AutomaticSize = Enum.AutomaticSize.Y,
				BackgroundTransparency = 1,
				Size = UDim2.new(1, -12, 0, 0),
				[Children] = {
					New("UIListLayout")({
						FillDirection = Enum.FillDirection.Vertical,
						HorizontalAlignment = Enum.HorizontalAlignment.Left,
						HorizontalFlex = Enum.UIFlexAlignment.Fill,
						Padding = UDim.new(0, 4),
						SortOrder = Enum.SortOrder.LayoutOrder,
						VerticalAlignment = Enum.VerticalAlignment.Top,
					}),
					subButtons,
				},
			}),
		},
	})
end

return treeButton


-- this component exists due to the necessity of rendering components conditionally based on a scrolling frame, while they don't fit into a grid/list/whatever
-- it's important to realize that this component should not be used for just anything, but only things where the tradeoff is significant enough to warrant the extra complexity
-- it's primarily made for property fields that take up a lot of instances and lag the treeview
-- this way, performance is consistent across instances of different sizes
-- this component could probably be optimized further by batching the calculations

local fusion = require("@packages/fusion")
local Children = fusion.Children
local Hydrate = fusion.Hydrate
local New = fusion.New
local Clean = fusion.cleanup

local Computed = fusion.Computed
local DoNothing = fusion.doNothing
local Observer = fusion.Observer
local Value = fusion.Value

local fusionUtils = require("@packages/fusionUtils")
local onDestroy = fusionUtils.onDestroy
local use = fusionUtils.use

local MIN_RERENDER_DISTANCE = 8
local POOLED_INSTANCES: {
	[Instance]: {
		[number]: {
			size: fusion.Value<Vector2>,
			position: fusion.Value<Vector2>,
			isRendered: fusion.Value<boolean>,
			enabled: fusion.Value<boolean>?,
			children: fusion.Value<any>,
			render: () -> Instance,
		}, -- table with gaps
	},
} =
	{}

type standaloneScrollerEntryProps = {
	FrameProperties: {
		[string]: any,
	}?,
	Enabled: fusion.CanBeState<boolean>?,
	Visible: fusion.CanBeState<boolean>?,
	Render: () -> Instance,
}

local function standaloneScrollingFrame(frame: ScrollingFrame)
	-- this manages the updating from canvas
	-- other instances will manage for themselves
	local scrollingFrame = frame

	if not scrollingFrame:IsA("ScrollingFrame") then
		scrollingFrame = frame:FindFirstChildWhichIsA("ScrollingFrame", true) :: ScrollingFrame

		if not scrollingFrame then
			warn("standaloneScrollingFrame: no ScrollingFrame found")
			return scrollingFrame
		end
	end

	if not POOLED_INSTANCES[scrollingFrame] then
		POOLED_INSTANCES[scrollingFrame] = {}

		local lastUpdatePosition = nil

		local scrollingFrameAbsolutePosition = Value(Vector2.new())
		local scrollingFrameWindowSize = Value(Vector2.new())
		local scrollingFramePosition = Value(Vector2.new())

		local function updateChildren()
			local windowSize = scrollingFrameWindowSize:get(false)
			local scrollPosition = scrollingFrameAbsolutePosition:get(false)

			if POOLED_INSTANCES[scrollingFrame] == nil then
				return
			end

			for _, instance in POOLED_INSTANCES[scrollingFrame] do
				if instance.enabled and not instance.enabled:get(false) then
					continue
				end

				-- i have no idea if localizing these will make it faster
				local instancePosition = instance.position
				local instanceSize = instance.size
				local isRendered = instance.isRendered

				local selfPositionTopBound = instancePosition:get(false)
				local selfPositionBottomBound = instancePosition:get(false) + instanceSize:get(false)

				local rerenderDist = Vector2.new(MIN_RERENDER_DISTANCE, MIN_RERENDER_DISTANCE)

				local topBound = selfPositionTopBound - scrollPosition - rerenderDist
				local bottomBound = selfPositionBottomBound - scrollPosition + rerenderDist
				local inWindow = (topBound.X >= 0 and topBound.Y >= 0 or bottomBound.X >= 0 and bottomBound.Y >= 0)
					and (
						topBound.X <= windowSize.X and topBound.Y <= windowSize.Y
						or bottomBound.X <= windowSize.X and bottomBound.Y <= windowSize.Y
					)

				if inWindow and not isRendered:get(false) then
					local children = instance.children
					local render = instance.render

					if children:get(false) == nil then
						children:set({ render() })
					end

					isRendered:set(true)
				elseif not inWindow and isRendered:get(false) then
					isRendered:set(false)
				end
			end
		end

		onDestroy(frame, {
			scrollingFrame:GetPropertyChangedSignal("AbsoluteWindowSize"):Connect(function()
				scrollingFrameWindowSize:set(scrollingFrame.AbsoluteWindowSize)
				updateChildren()
			end),
			scrollingFrame:GetPropertyChangedSignal("CanvasPosition"):Connect(function()
				local canvasPosition = scrollingFrame.CanvasPosition

				if lastUpdatePosition and (canvasPosition - lastUpdatePosition).Magnitude < MIN_RERENDER_DISTANCE then
					return
				end

				lastUpdatePosition = canvasPosition
				scrollingFramePosition:set(canvasPosition)
				updateChildren()
			end),
			scrollingFrame:GetPropertyChangedSignal("AbsolutePosition"):Connect(function()
				scrollingFrameAbsolutePosition:set(scrollingFrame.AbsolutePosition)
				updateChildren()
			end),
			scrollingFrame:GetPropertyChangedSignal("AbsoluteCanvasSize"):Connect(function()
				task.delay(0.25, updateChildren)
			end),
			function()
				POOLED_INSTANCES[scrollingFrame] = nil
				scrollingFrame = nil
			end,
		})
	end

	return frame
end

local function standaloneScrollerEntry(props: standaloneScrollerEntryProps)
	local scrollingFrame = Value()
	local scrollerEntryChildren = Value()
	local isRendered = Value(false)
	local scrollerEntry = New("Frame")({
		Name = "ScrollerEntry",
		BackgroundTransparency = 1,
		Size = UDim2.new(),
		Visible = Computed(function()
			return (props.Visible == nil or use(props.Visible)) and (props.Enabled == nil or use(props.Enabled))
		end),
		[Children] = Computed(function()
			if isRendered:get() and (props.Enabled == nil or use(props.Enabled)) then
				return scrollerEntryChildren:get()
			end

			return nil
		end, DoNothing),
	})

	if props.FrameProperties then
		scrollerEntry = Hydrate(scrollerEntry)(props.FrameProperties)
	end

	local selfSize = Value(Vector2.new())
	local selfPosition = Value(Vector2.new())

	local function updateChild()
		if props.Enabled ~= nil and not use(props.Enabled) or not scrollingFrame:get(false) then
			return
		end

		local windowSize = scrollingFrame:get(false).AbsoluteWindowSize
		local scrollPosition = scrollingFrame:get(false).AbsolutePosition

		-- we determine where in the canvas we are ourselves first -- we find this by subtracing the scrollingframe's position from our own position
		local selfPositionTopBound = selfPosition:get(false)
		local selfPositionBottomBound = selfPosition:get(false) + selfSize:get(false)
		local topBound = selfPositionTopBound - scrollPosition
		local bottomBound = selfPositionBottomBound - scrollPosition
		-- see if we are in the window (so if relativeposition is between 0,0 and windowSize)
		local inWindow = (topBound.X >= 0 and topBound.Y >= 0 or bottomBound.X >= 0 and bottomBound.Y >= 0)
			and (
				topBound.X <= windowSize.X and topBound.Y <= windowSize.Y
				or bottomBound.X <= windowSize.X and bottomBound.Y <= windowSize.Y
			)

		if inWindow and not isRendered:get(false) then
			if scrollerEntryChildren:get(false) == nil then
				scrollerEntryChildren:set({ props.Render() })
			end

			isRendered:set(true)
		elseif not inWindow and isRendered:get(false) then
			isRendered:set(false)
		end
	end

	local function updateParent()
		local foundFrame = scrollerEntry:FindFirstAncestorWhichIsA("ScrollingFrame")
		scrollingFrame:set(foundFrame)

		if foundFrame and POOLED_INSTANCES[foundFrame] then
			table.insert(POOLED_INSTANCES[foundFrame], {
				instance = scrollerEntry,
				size = selfSize,
				position = selfPosition,
				isRendered = isRendered,
				enabled = props.Enabled,
				children = scrollerEntryChildren,
				render = props.Render,
			})

			updateChild()
		end
	end

	onDestroy(scrollerEntry, {
		scrollerEntry:GetPropertyChangedSignal("Parent"):Connect(updateParent),
		scrollerEntry:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
			selfSize:set(scrollerEntry.AbsoluteSize)
			updateChild()
		end),
		scrollerEntry:GetPropertyChangedSignal("AbsolutePosition"):Connect(function()
			selfPosition:set(scrollerEntry.AbsolutePosition)
		end),
		props.Enabled and Observer(props.Enabled):onChange(function()
			-- position is not updated immediately; we need to wait for the next frame
			task.delay(0, updateChild)
		end),
		function()
			if POOLED_INSTANCES[scrollingFrame:get(false)] then
				local pooledInstances = POOLED_INSTANCES[scrollingFrame:get(false)]

				-- remove self from pooled instances
				for i, instance in pooledInstances do
					if instance.isRendered == isRendered then
						Clean(instance.children)

						for idx in instance do
							instance[idx] = nil
						end

						pooledInstances[i] = nil
						break
					end
				end
			end
		end,
	})

	task.delay(0.1, updateParent)

	return scrollerEntry
end

return {
	frame = standaloneScrollingFrame,
	entry = standaloneScrollerEntry,
}


local fusion = require("@packages/fusion")
local Children = fusion.Children
local Hydrate = fusion.Hydrate
local New = fusion.New
local Out = fusion.Out

local Clean = fusion.cleanup
local Computed = fusion.Computed
local ForValues = fusion.ForValues
local Value = fusion.Value

local fusionUtils = require("@packages/fusionUtils")
local use = fusionUtils.use
local propertyProvider = fusionUtils.propertyProvider

local function convertTo3D(value: number, minBound: number, maxBound: number, offset: number): number
	return (-offset / 2) + ((offset / 2 - (-offset / 2)) * (value - minBound) / (maxBound - minBound))
end

local function convertToVectorBase(value1: number, value2: number, bounds: bounds, offset: Vector2): Vector3
	local xPos = convertTo3D(value1, bounds.x.min, bounds.x.max, offset.X)
	local yPos = convertTo3D(value2, bounds.y.min, bounds.y.max, offset.Y)
	return Vector3.new(xPos, yPos, 0)
end

export type convertToVector = ((value: Vector2) -> Vector3) & ((value: number) -> number)

export type bounds = {
	x: {
		min: number,
		max: number,
	},
	y: {
		min: number,
		max: number,
	},
}

type boundsReactive = {
	x: {
		min: fusion.CanBeState<number>,
		max: fusion.CanBeState<number>,
	},
	y: {
		min: fusion.CanBeState<number>,
		max: fusion.CanBeState<number>,
	},
}

type props = {
	BackgroundColor: Color3,
	Bounds: boundsReactive,
	Corner: fusion.CanBeState<UDim>,
	Children: fusion.CanBeState<
		{
			(
				convertToVector: convertToVector,
				bounds: fusion.Computed<bounds>,
				containerSize: fusion.Value<Vector2>
			) -> { Instance }
		}
	>,
}

local FOV_CONSTANT = 0.017454177141189575
--local CAMERA_ORIENTATION = CFrame.new(Vector3.new(), Vector3.new(0, 0, -1))

local function viewport2D(props: props)
	local containerSize = Value(Vector2.new(0, 0))
	local processedBounds = Computed(function()
		return {
			x = {
				min = use(props.Bounds.x.min),
				max = use(props.Bounds.x.max),
			},
			y = {
				min = use(props.Bounds.y.min),
				max = use(props.Bounds.y.max),
			},
		}
	end)

	local component = New("ViewportFrame")({
		Name = "Viewport2D",
		Ambient = Color3.new(1, 1, 1),
		BackgroundColor3 = props.BackgroundColor,
		BackgroundTransparency = 1,
		CurrentCamera = New("Camera")({
			CFrame = Computed(function()
				local rangeWidth = use(props.Bounds.x.max) - use(props.Bounds.x.min)
				return CFrame.new(0, 0, rangeWidth / FOV_CONSTANT)
			end),
			FieldOfView = 1,
		}),
		LightColor = Color3.new(1, 1, 1),
		LightDirection = Vector3.new(1, 1, 1),
		[Out("AbsoluteSize")] = containerSize,
		[Children] = {
			if props.Corner
				then New("UICorner")({
					CornerRadius = use(props.Corner),
				})
				else nil,
			New("WorldModel")({
				[Children] = {
					ForValues(props.Children, function(value)
						if containerSize:get() == nil then
							return
						end

						return use(value)(function(position)
							local rangeWidth = use(props.Bounds.x.max) - use(props.Bounds.x.min)
							local absoluteSize = containerSize:get()

							local aspectRatio = absoluteSize.X / absoluteSize.Y
							local horizontalScale = rangeWidth * aspectRatio
							local verticalScale = rangeWidth / 2 * 2

							local offset = Vector2.new(horizontalScale, verticalScale)

							local bounds = processedBounds:get()
							local usedPosition = use(position)

							if typeof(usedPosition) == "number" then
								return convertTo3D(usedPosition, bounds.y.min, bounds.y.max, offset.Y)
							end

							return convertToVectorBase(usedPosition.X, usedPosition.Y, bounds, offset)
						end, processedBounds, containerSize)
					end, Clean),
				},
			}),
		},
	})

	props.BackgroundColor = (nil :: any) :: Color3 -- i love this language

	return Hydrate(component)(propertyProvider.getPropsAndSpecialKeys(props, "ViewportFrame"))
end

return viewport2D


local Selection = game:GetService("Selection")

local fusion = require("@packages/fusion")
local Children = fusion.Children
local New = fusion.New

local Value = fusion.Value

local fusionUtils = require("@packages/fusionUtils")
local topLayerProvider = fusionUtils.topLayerProvider

local app = require("@components/app")

--[[
local screenGui = game.Players.LocalPlayer:WaitForChild("PlayerGui"):WaitForChild("VFXEditor")
app({ Items = Value({require("@src/stories/generateInstance")()}) }).Parent = screenGui.Frame

leaving this code in here in case I need to find memory leaks again
]]

local toolbar = plugin:CreateToolbar("VFX Editor")
local button =
	toolbar:CreateButton("Toggle VFX Editor", "Open/close the editor", "rbxassetid://18834248930", "VFX Editor")
button.ClickableWhenViewportHidden = true

local widget

button.Click:Connect(function()
	if widget == nil then
		widget = plugin:CreateDockWidgetPluginGui(
			"VFXEditor",
			DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Float, true, false, 350, 550, 150, 150)
		)

		widget.Title = "VFX Editor"
		widget.ZIndexBehavior = Enum.ZIndexBehavior.Sibling

		local layer = topLayerProvider.new(New("Frame")({
			BackgroundTransparency = 1,
			Parent = widget,
			Size = UDim2.new(1, 0, 1, 0),
			[Children] = {
				app({ Items = Value(Selection:Get()) }),
			},
		}))

		layer.Parent = widget
	else
		widget.Enabled = not widget.Enabled
	end
end)