return {
	id = "dark",
	name = "Dark",
	mode = "dark",
	colors = {
		accent = {
			default = Color3.fromHex("ffdf35"),
			hovered = Color3.fromHex("ffed96"),
			pressed = Color3.fromHex("c2a92a"),
			disabled = Color3.fromHex("222222"),
		},
		onAccent = {
			default = Color3.new(),
			hovered = Color3.fromHex("1a1a1a"),
			pressed = Color3.new(),
			disabled = Color3.fromHex("757575"),
		},
		button = {
			default = Color3.fromHex("3c3c3c"),
			hovered = Color3.fromHex("4a4a4a"),
			pressed = Color3.fromHex("1e1e1e"),
			disabled = Color3.fromHex("222222"),
		},
		onButton = {
			default = Color3.fromHex("d3d3d3"),
			hovered = Color3.fromHex("d3d3d3"),
			pressed = Color3.fromHex("d3d3d3"),
			disabled = Color3.fromHex("757575"),
		},
		buttonBorder = {
			default = Color3.fromHex("2e2e2e"),
			hovered = Color3.fromHex("2e2e2e"),
			pressed = Color3.fromHex("2e2e2e"),
			disabled = Color3.fromHex("2e2e2e"),
		},
		textbox = {
			default = Color3.fromHex("1e1e1e"),
			hovered = Color3.fromHex("3c3c3c"),
			focused = Color3.fromHex("1e1e1e"),
			disabled = Color3.fromHex("222222"),
		},
		onTextbox = {
			default = Color3.fromHex("d3d3d3"),
			hovered = Color3.fromHex("d3d3d3"),
			focused = Color3.fromHex("d3d3d3"),
			disabled = Color3.fromHex("757575"),
		},
		textboxBorder = {
			default = Color3.fromHex("2e2e2e"),
			hovered = Color3.fromHex("2e2e2e"),
			focused = Color3.fromHex("ffdf35"),
			disabled = Color3.fromHex("2e2e2e"),
		},
		surface = Color3.new(),
		onSurface = Color3.new(1, 1, 1),
		onSurfaceVariant = Color3.fromHex("afafaf"),
		error = Color3.fromHex("ff3053"),
		onError = Color3.new(1, 1, 1),
	},
	typography = {
		title = Font.new("rbxassetid://16658237174", Enum.FontWeight.Bold),
		body = Font.new("rbxasset://fonts/families/BuilderSans.json"),
		mono = Font.new("rbxassetid://16658246179"),
	},
	variables = {
		spacing = 8,
		cornerRadius = UDim.new(0, 5),
		borderWidth = 1,
		scrollbarWidth = 8,
		scrollbarTransparency = 0.5,
		textSize = 16,
		surfaceTransparency = 0.1,
		tooltipMaxSize = Vector2.new(320, 320),
		popoverMaxSize = Vector2.new(320, 320),
	},
	elevation = {
		popover = 900,
		tooltip = 1000,
	},
}


--!strict
local DarkTheme = require(script.Dark)

local themeMap = table.freeze({
	[DarkTheme.id] = DarkTheme,
})

return themeMap


local Selection = game:GetService("Selection")

local React = require("@pkgs/React")

local IconMap = require("@root/Assets/IconMap")
local LocalStore = require("@root/Stores/LocalStore")
local Preferences = require("@root/Stores/Preferences")

local ResurfaceApi = require("@root/ResurfaceApi")

local Button = require("../Components/Button")
local ContextualActionContainer = require("../Components/ContextualActionContainer")
local Tooltip = require("../Components/Tooltip")

local useValue = require("@root/Lib/Store").useValue
local useTheme = require("../Hooks/useTheme")

local createNextOrder = require("../Lib/createNextOrder")
local e = React.createElement

local NO_MATERIAL_VARIANTS = {
	[Enum.SurfaceType.Motor] = true,
	[Enum.SurfaceType.SteppingMotor] = true,
	[Enum.SurfaceType.Hinge] = true,
}

local function ContextualToolbar()
	local nextOrder = createNextOrder()
	local theme = useTheme()

	local selectedSurface = useValue(Preferences.selectedSurface)
	local applyToAllFaces = useValue(Preferences.applyToAllfaces)
	local applyToAllFacesOverride = useValue(LocalStore.applyToAllfacesOverride)
	local useMaterialVariants = useValue(Preferences.useMaterialVariants)
	local hasActionableSelection = useValue(LocalStore.hasActionableSelection)

	local isApplyToAllFaces = if applyToAllFacesOverride
		then not applyToAllFaces
		else applyToAllFaces

	return e("Frame", {
		AutomaticSize = Enum.AutomaticSize.XY,
		BackgroundTransparency = 1,
	}, {
		layout = e("UIListLayout", {
			FillDirection = Enum.FillDirection.Horizontal,
			Padding = UDim.new(0, theme.variables.spacing / 2),
			SortOrder = Enum.SortOrder.LayoutOrder,
			HorizontalAlignment = Enum.HorizontalAlignment.Center,
			VerticalAlignment = Enum.VerticalAlignment.Center,
		}),
	}, {
		applyToSelection = e(ContextualActionContainer, {
			visible = isApplyToAllFaces and hasActionableSelection,
			layoutOrder = nextOrder(),
		}, {
			tooltip = e(Tooltip, {
				title = "Apply to Selection",
				text = "Apply the selected surface to all faces of the current selection.",
			}, {
				button = e(Button, {
					rounded = true,
					text = "Selection",
					icon = IconMap.Icons.target,
					onClick = function()
						local selection = Selection:Get()

						ResurfaceApi.apply(selection, {
							surfaceType = Preferences.selectedSurface(),
							useMaterialVariants = Preferences.useMaterialVariants(),
							useMaterialVariantsMeshUnion = Preferences.useMaterialVariantsMeshUnion(),
							setMaterialToPlastic = Preferences.setMaterialToPlastic(),
						})

						if Preferences.deactivatePluginAfterOperation() then
							LocalStore.isPluginActive(false)
						end
					end,
				}),
			}),
		}),

		useMaterialVariants = e(ContextualActionContainer, {
			visible = isApplyToAllFaces and not NO_MATERIAL_VARIANTS[selectedSurface],
			layoutOrder = nextOrder(),
		}, {
			tooltip = e(Tooltip, {
				title = "Use MaterialVariants",
				text = "Emulate surface inputs on all faces by using a MaterialVariant.",
			}, {
				button = e(Button, {
					rounded = true,
					selected = useMaterialVariants,
					text = "MaterialVariants",
					icon = IconMap.Icons.material,
					onClick = function()
						Preferences.useMaterialVariants(function(value)
							return not value
						end)
					end,
				}),
			}),
		}),
	})
end

return ContextualToolbar


local React = require("@pkgs/React")

local IconMap = require("@root/Assets/IconMap")
local LocalStore = require("@root/Stores/LocalStore")
local Preferences = require("@root/Stores/Preferences")

local Button = require("../Components/Button")
local ComponentGroup = require("../Components/ComponentGroup")
local Divider = require("../Components/Divider")
local SettingsWidget = require("../Widgets/SettingsWidget")
local SurfacePickerWidget = require("../Widgets/SurfacePickerWidget")
local SurfacePreviewIcon = require("../Components/SurfacePreview/SurfacePreviewIcon")
local Tooltip = require("../Components/Tooltip")
local WigglyUnderlineButton = require("../Components/WigglyUnderlineButton")

local useValue = require("@root/Lib/Store").useValue
local useTheme = require("../Hooks/useTheme")
local useRef = React.useRef

local createNextOrder = require("../Lib/createNextOrder")
local createUniqueKey = require("../../Lib/createUniqueKey")
local formatEnumName = require("../Lib/formatEnumName")
local formatKeyCode = require("../Lib/formatKeyCode")
local e = React.createElement
local memo = React.memo

local function PrimaryToolbar()
	local theme = useTheme()

	local uniqueKey = createUniqueKey()
	local nextOrder = createNextOrder()

	local surfaceSelectorButtonRef = useRef(nil :: GuiBase2d?)
	local settingsButtonRef = useRef(nil :: GuiBase2d?)

	local selectedSurface = useValue(Preferences.selectedSurface)
	local applyToAllfaces = useValue(Preferences.applyToAllfaces)
	local applyToAllfacesOverride = useValue(LocalStore.applyToAllfacesOverride)
	local setMaterialToPlastic = useValue(Preferences.setMaterialToPlastic)
	local currentWidget = useValue(LocalStore.currentWidget)
	local useMaterialVariants = useValue(Preferences.useMaterialVariants)
	local useMaterialVariantsMeshUnion =
		useValue(Preferences.useMaterialVariantsMeshUnion)
	local transparencyScale = useValue(Preferences.transparencyScale)
	local applyToAllfacesOverrideKeycode =
		useValue(Preferences.keybindApplyToAllFacesOverride)
	local surfacePickerWidgetKeycode = useValue(Preferences.keybindSurfacePickerWidget)

	local selectedSurfaceTitle = formatEnumName(selectedSurface.Name)
	local isApplyToAllFaces = if applyToAllfacesOverride
		then not applyToAllfaces
		else applyToAllfaces

	return e("TextButton", {
		AutoButtonColor = false,
		AutomaticSize = Enum.AutomaticSize.XY,
		BackgroundColor3 = theme.colors.surface,
		BackgroundTransparency = theme.variables.surfaceTransparency
			* transparencyScale,
		BorderSizePixel = 0,
		Text = "",
	}, {
		corners = e("UICorner", {
			CornerRadius = theme.variables.cornerRadius
				+ UDim.new(0, theme.variables.spacing),
		}),
		padding = e("UIPadding", {
			PaddingTop = UDim.new(0, theme.variables.spacing),
			PaddingBottom = UDim.new(0, theme.variables.spacing),
			PaddingLeft = UDim.new(0, theme.variables.spacing * 1.5),
			PaddingRight = UDim.new(0, theme.variables.spacing),
		}),
		layout = e("UIListLayout", {
			FillDirection = Enum.FillDirection.Horizontal,
			HorizontalAlignment = Enum.HorizontalAlignment.Center,
			VerticalAlignment = Enum.VerticalAlignment.Center,
			SortOrder = Enum.SortOrder.LayoutOrder,
			Padding = UDim.new(0, theme.variables.spacing),
		}),
	}, {
		surfaceSelector = e(Tooltip, {
			rootRef = surfaceSelectorButtonRef,
			title = "Active Surface",
			text = `Applies "{selectedSurfaceTitle}" during surface change operations.`,
			keycode = formatKeyCode(surfacePickerWidgetKeycode),
			native = {
				LayoutOrder = nextOrder(),
			},
		}, {
			button = e(WigglyUnderlineButton, {
				text = formatEnumName(selectedSurface.Name),
				onClick = function()
					LocalStore.currentWidget(function(current)
						if current == "surfacePicker" then
							return nil
						end

						return "surfacePicker"
					end)
				end,
			}, {
				previewIcon = e(SurfacePreviewIcon, {
					surfaceType = selectedSurface,
				}),
			}),

			widget = e(SurfacePickerWidget, {
				anchorRef = surfaceSelectorButtonRef,
			}),
		}),

		[uniqueKey("divider")] = e(Divider, {
			orientation = "vertical",
			native = {
				LayoutOrder = nextOrder(),
			},
		}) :: never,

		[uniqueKey("componentGroup")] = e(ComponentGroup, {
			native = {
				LayoutOrder = nextOrder(),
			},
		}, {
			allFacesToggle = e(Tooltip, {
				title = "Apply to All Faces",
				text = `Click a part{useMaterialVariantsMeshUnion and ", mesh or union" or ""} to apply "{selectedSurfaceTitle}" to all of its faces while active.`,
				keycode = formatKeyCode(applyToAllfacesOverrideKeycode),
				native = {
					LayoutOrder = nextOrder(),
				},
			}, {
				button = e(Button, {
					text = "All Faces",
					icon = IconMap.Icons.cube,
					selected = isApplyToAllFaces,
					onClick = function()
						Preferences.applyToAllfaces(function(current)
							return not current
						end)
					end,
					native = {
						LayoutOrder = nextOrder(),
					},
				}),
			}),

			setPlasticToggle = e(Tooltip, {
				title = "Set to Plastic",
				text = "Set the selected part's material to Plastic when applying surface inputs.",
				native = {
					LayoutOrder = nextOrder(),
				},
			}, {
				button = e(Button, {
					text = "Set Plastic",
					icon = IconMap.Icons.fabric,
					selected = setMaterialToPlastic,
					disabled = isApplyToAllFaces and useMaterialVariants,
					onClick = function()
						Preferences.setMaterialToPlastic(function(current)
							return not current
						end)
					end,
					native = {
						LayoutOrder = nextOrder(),
					},
				}),
			}),
		}) :: never,

		[uniqueKey("divider")] = e(Divider, {
			orientation = "vertical",
			native = {
				LayoutOrder = nextOrder(),
			},
		}) :: never,

		[uniqueKey("componentGroup")] = e(ComponentGroup, {
			native = {
				LayoutOrder = nextOrder(),
			},
		}, {
			settingsToggle = e(Tooltip, {
				title = "Settings",
				rootRef = settingsButtonRef,
				native = {
					LayoutOrder = nextOrder(),
				},
			}, {
				button = e(Button, {
					icon = IconMap.Icons.settings,
					selected = currentWidget == "settings",
					onClick = function()
						LocalStore.currentWidget(function(current)
							if current == "settings" then
								return nil
							end

							return "settings"
						end)
					end,
					native = {
						LayoutOrder = nextOrder(),
					},
				}),

				widget = e(SettingsWidget, {
					anchorRef = settingsButtonRef,
				}),
			}),
		}) :: never,
	})
end

return (memo(PrimaryToolbar) :: never) :: typeof(PrimaryToolbar)


local plugin: Plugin = plugin or script:FindFirstAncestorOfClass("Plugin")

local StudioService = game:GetService("StudioService")
local RuntimeScript = script.Parent.Parent.Parent

local React = require("@pkgs/React")

local LocalStore = require("@root/Stores/LocalStore")
local Manifest = require("@root/Manifest")
local Preferences = require("@root/Stores/Preferences")

local Button = require("../Components/Button")
local ComponentGroup = require("../Components/ComponentGroup")
local Divider = require("../Components/Divider")
local Popover = require("../Components/Popover")
local Switch = require("../Components/Switch")

local useDefaultProps = require("../Hooks/useDefaultProps")
local useValue = require("@root/Lib/Store").useValue
local useTheme = require("../Hooks/useTheme")

local createNextOrder = require("../Lib/createNextOrder")
local createUniqueKey = require("../../Lib/createUniqueKey")
local formatEnumName = require("../Lib/formatEnumName")
local withFont = require("../Lib/withFont")
local e = React.createElement
local memo = React.memo

local studioUserId = StudioService:GetUserId()
local buildSessionId = RuntimeScript:GetAttribute("BuildSessionId")
local sourceCommitHash = RuntimeScript:GetAttribute("SourceCommitHash")
local isLocalPlugin = plugin.Parent or plugin.Name:match("%.rbxmx?$")
local pluginSource = isLocalPlugin and "model_file" or "creator_store"

local function ControlGroup(props: {
	title: string,
	description: string?,
	disabled: boolean?,
	native: { [string]: any }?,
	children: React.Node?,
})
	local theme = useTheme()
	local nextOrder = createNextOrder()

	local nativeProps = useDefaultProps({
		AutomaticSize = Enum.AutomaticSize.Y,
		Size = UDim2.fromScale(1, 0),
	}, props.native)

	return e(ComponentGroup, {
		orientation = "horizontal",
		spacing = UDim.new(0, theme.variables.spacing * 3),
		native = nativeProps,
		layoutNative = {
			HorizontalFlex = Enum.UIFlexAlignment.Fill,
		},
	}, {
		control = props.children and e("Frame", {
			AutomaticSize = Enum.AutomaticSize.XY,
			BackgroundTransparency = 1,
			LayoutOrder = nextOrder(),
		}, props.children),

		labels = e(ComponentGroup, {
			orientation = "vertical",
			spacing = UDim.new(),
			native = {
				AutomaticSize = Enum.AutomaticSize.Y,
				LayoutOrder = nextOrder(),
				Size = UDim2.fromScale(1, 0),
			},
		}, {
			title = e("TextLabel", {
				AutomaticSize = Enum.AutomaticSize.Y,
				BackgroundTransparency = 1,
				Size = UDim2.new(1, 0, 0, theme.variables.textSize),
				LayoutOrder = nextOrder(),
				FontFace = withFont(theme.typography.body, Enum.FontWeight.SemiBold),
				Text = props.title,
				TextColor3 = props.disabled and theme.colors.onSurfaceVariant
					or theme.colors.onSurface,
				TextSize = theme.variables.textSize,
				TextWrapped = true,
				TextXAlignment = Enum.TextXAlignment.Left,
			}),

			description = props.description and e("TextLabel", {
				AutomaticSize = Enum.AutomaticSize.Y,
				BackgroundTransparency = 1,
				Size = UDim2.new(1, 0, 0, theme.variables.textSize),
				LayoutOrder = nextOrder(),
				FontFace = theme.typography.body,
				Text = props.description,
				TextColor3 = theme.colors.onSurfaceVariant,
				TextSize = theme.variables.textSize,
				TextWrapped = true,
				TextXAlignment = Enum.TextXAlignment.Left,
			}),
		}),
	})
end

local function SettingsWidget(props: {
	anchorRef: { current: GuiBase2d? },
})
	local theme = useTheme()

	local nextOrder = createNextOrder()
	local uniqueKey = createUniqueKey()

	local selectedSurface = useValue(Preferences.selectedSurface)
	local applyToAllfaces = useValue(Preferences.applyToAllfaces)
	local applyToAllfacesOverride = useValue(LocalStore.applyToAllfacesOverride)

	local setMaterialToPlastic = useValue(Preferences.setMaterialToPlastic)
	-- local useTextures = useValue(Preferences.useTextures)
	local useMaterialVariants = useValue(Preferences.useMaterialVariants)
	local useMaterialVariantsMeshUnion =
		useValue(Preferences.useMaterialVariantsMeshUnion)

	local deactivatePluginAfterOperation =
		useValue(Preferences.deactivatePluginAfterOperation)
	local useClassicMenuOnLaunch = useValue(Preferences.useClassicMenuOnLaunch)
	local autoFocusSurfacePickerSearch =
		useValue(Preferences.autoFocusSurfacePickerSearch)

	local isReducedMotion = useValue(Preferences.isReducedMotion)
	local transparencyScale = useValue(Preferences.transparencyScale)
	local useBounceAnimations = useValue(Preferences.useBounceAnimations)

	local isVisible = useValue(LocalStore.currentWidget) == "settings"

	local selectedSurfaceTitle = selectedSurface
			and formatEnumName(selectedSurface.Name)
		or "undefined"
	local isAllFacesEnabled = if applyToAllfacesOverride
		then not applyToAllfaces
		else applyToAllfaces

	return e(Popover, {
		visible = isVisible,
		anchorRef = props.anchorRef,
		stickyHeader = e("TextLabel", {
			AutomaticSize = Enum.AutomaticSize.XY,
			BackgroundTransparency = 1,
			LayoutOrder = nextOrder(),
			FontFace = withFont(theme.typography.title, Enum.FontWeight.ExtraBold),
			Text = "Settings",
			TextColor3 = theme.colors.onSurface,
			TextSize = theme.variables.textSize * 1.25,
			TextWrapped = true,
			TextXAlignment = Enum.TextXAlignment.Left,
		}) :: never,
	}, {
		layout = e("UIListLayout", {
			Padding = UDim.new(0, theme.variables.spacing),
			SortOrder = Enum.SortOrder.LayoutOrder,
			HorizontalFlex = Enum.UIFlexAlignment.Fill,
		}),
		padding = e("UIPadding", {
			PaddingRight = UDim.new(
				0,
				theme.variables.scrollbarWidth + theme.variables.spacing
			),
		}),
	}, {
		selection = e(ComponentGroup, {
			orientation = "vertical",
			spacing = UDim.new(0, theme.variables.spacing),
			native = {
				LayoutOrder = nextOrder(),
			},
		}, {
			heading = e("TextLabel", {
				AutomaticSize = Enum.AutomaticSize.XY,
				BackgroundTransparency = 1,
				LayoutOrder = nextOrder(),
				FontFace = withFont(theme.typography.title, Enum.FontWeight.ExtraBold),
				Text = "Selection",
				TextColor3 = theme.colors.onSurface,
				TextSize = theme.variables.textSize,
				TextWrapped = true,
			}),

			allFaces = e(ControlGroup, {
				title = "Apply All Faces",
				description = `Click a part to apply "{selectedSurfaceTitle}" to all of its faces while active.`,
				native = {
					LayoutOrder = nextOrder(),
				},
			}, {
				switch = e(Switch, {
					selected = isAllFacesEnabled,
					disabled = applyToAllfacesOverride,
					onClick = function()
						Preferences.applyToAllfaces(function(current)
							return not current
						end)
					end,
				}),
			}),

			useMaterialVariants = e(ControlGroup, {
				title = "Use MaterialVariants",
				description = "Emulate surface inputs on all faces by using a MaterialVariant.",
				disabled = not isAllFacesEnabled,
				native = {
					LayoutOrder = nextOrder(),
				},
			}, {
				switch = e(Switch, {
					selected = useMaterialVariants,
					disabled = not isAllFacesEnabled,
					onClick = function()
						Preferences.useMaterialVariants(function(current)
							return not current
						end)
					end,
				}),
			}),

			useMaterialVariantsMeshUnion = e(ControlGroup, {
				title = "Force MaterialVariants for Meshes and Unions",
				description = 'Apply MaterialVariants to Meshes and Unions, regardless of the "Use Material Variants" setting.',
				disabled = not isAllFacesEnabled,
				native = {
					LayoutOrder = nextOrder(),
				},
			}, {
				switch = e(Switch, {
					selected = useMaterialVariantsMeshUnion,
					disabled = not isAllFacesEnabled,
					onClick = function()
						Preferences.useMaterialVariantsMeshUnion(function(current)
							return not current
						end)
					end,
				}),
			}),

			setPlastic = e(ControlGroup, {
				title = "Set to Plastic",
				description = "Set the selected part's material to Plastic when applying surface inputs.",
				disabled = isAllFacesEnabled and useMaterialVariants,
				native = {
					LayoutOrder = nextOrder(),
				},
			}, {
				switch = e(Switch, {
					selected = setMaterialToPlastic,
					disabled = isAllFacesEnabled and useMaterialVariants,
					onClick = function()
						Preferences.setMaterialToPlastic(function(current)
							return not current
						end)
					end,
				}),
			}),

			-- 	useTextures = e(ControlGroup, {
			-- 		title = "Always Use Textures",
			-- 		description = "Always emulate surface inputs by applying texture instances to the selected part's surface.",
			-- 		disabled = isAllFacesEnabled and useMaterialVariants,
			-- 		native = {
			-- 			LayoutOrder = nextOrder(),
			-- 		},
			-- 	}, {
			-- 		switch = e(Switch, {
			-- 			selected = useTextures,
			-- 			disabled = isAllFacesEnabled and useMaterialVariants,
			-- 			onClick = function()
			-- 				Preferences.useTextures(function(current)
			-- 					return not current
			-- 				end)
			-- 			end,
			-- 		}),
			-- 	}),
		}),

		-- General options
		[uniqueKey("divider")] = e(Divider, {
			orientation = "horizontal",
			native = {
				LayoutOrder = nextOrder(),
			},
		}) :: never,

		general = e(ComponentGroup, {
			orientation = "vertical",
			spacing = UDim.new(0, theme.variables.spacing),
			native = {
				LayoutOrder = nextOrder(),
			},
		}, {
			heading = e("TextLabel", {
				AutomaticSize = Enum.AutomaticSize.XY,
				BackgroundTransparency = 1,
				LayoutOrder = nextOrder(),
				FontFace = withFont(theme.typography.title, Enum.FontWeight.ExtraBold),
				Text = "General",
				TextColor3 = theme.colors.onSurface,
				TextSize = theme.variables.textSize,
				TextWrapped = true,
			}),

			autofocusSurfacePickerSearch = e(ControlGroup, {
				title = "Auto-focus Search",
				description = "Automatically focus the search field when opening the surface picker.",
				native = {
					LayoutOrder = nextOrder(),
				},
			}, {
				switch = e(Switch, {
					selected = autoFocusSurfacePickerSearch,
					onClick = function()
						Preferences.autoFocusSurfacePickerSearch(function(current)
							return not current
						end)
					end,
				}),
			}),

			classicLaunchMenu = e(ControlGroup, {
				title = "Classic Menu on Launch",
				description = "Show the classic surface picker menu from Surface Tool when clicking the plugin button.",
				native = {
					LayoutOrder = nextOrder(),
				},
			}, {
				switch = e(Switch, {
					selected = useClassicMenuOnLaunch,
					onClick = function()
						Preferences.useClassicMenuOnLaunch(function(current)
							return not current
						end)
					end,
				}),
			}),

			closeAfterOperation = e(ControlGroup, {
				title = "Close After Operation",
				description = "Automatically deactivate the plugin after applying a surface.",
				native = {
					LayoutOrder = nextOrder(),
				},
			}, {
				switch = e(Switch, {
					selected = deactivatePluginAfterOperation,
					onClick = function()
						Preferences.deactivatePluginAfterOperation(function(current)
							return not current
						end)
					end,
				}),
			}),
		}),

		-- Accessibility options
		[uniqueKey("divider")] = e(Divider, {
			orientation = "horizontal",
			native = {
				LayoutOrder = nextOrder(),
			},
		}) :: never,

		accessibility = e(ComponentGroup, {
			orientation = "vertical",
			spacing = UDim.new(0, theme.variables.spacing),
			native = {
				LayoutOrder = nextOrder(),
			},
		}, {
			heading = e("TextLabel", {
				AutomaticSize = Enum.AutomaticSize.XY,
				BackgroundTransparency = 1,
				LayoutOrder = nextOrder(),
				FontFace = withFont(theme.typography.title, Enum.FontWeight.ExtraBold),
				Text = "Accessibility",
				TextColor3 = theme.colors.onSurface,
				TextSize = theme.variables.textSize,
				TextWrapped = true,
			}),

			bounceAnimations = e(ControlGroup, {
				title = "Bounce Animations",
				description = "Use playful springy animations throughout the UI (glitchy!).",
				disabled = isReducedMotion,
				native = {
					LayoutOrder = nextOrder(),
				},
			}, {
				switch = e(Switch, {
					selected = useBounceAnimations,
					disabled = isReducedMotion,
					onClick = function()
						Preferences.useBounceAnimations(function(current)
							return not current
						end)
					end,
				}),
			}),

			reducedMotion = e(ControlGroup, {
				title = "Reduce Motion",
				description = "Reduce motion during animations.",
				native = {
					LayoutOrder = nextOrder(),
				},
			}, {
				switch = e(Switch, {
					selected = isReducedMotion,
					onClick = function()
						Preferences.isReducedMotion(function(current)
							return not current
						end)
					end,
				}),
			}),

			reducedTransparency = e(ControlGroup, {
				title = "Reduce Transparency",
				description = "Reduce transparency throughout the UI.",
				native = {
					LayoutOrder = nextOrder(),
				},
			}, {
				switch = e(Switch, {
					selected = transparencyScale ~= 1,
					onClick = function()
						Preferences.transparencyScale(function(current)
							return current == 1 and 0 or 1
						end)
					end,
				}),
			}),
		}),

		-- Diagnostics
		[uniqueKey("divider")] = e(Divider, {
			orientation = "horizontal",
			native = {
				LayoutOrder = nextOrder(),
			},
		}) :: never,

		diagnostics = e(ComponentGroup, {
			orientation = "vertical",
			spacing = UDim.new(0, theme.variables.spacing),
			native = {
				LayoutOrder = nextOrder(),
			},
		}, {
			heading = e("TextLabel", {
				AutomaticSize = Enum.AutomaticSize.XY,
				BackgroundTransparency = 1,
				LayoutOrder = nextOrder(),
				FontFace = withFont(theme.typography.title, Enum.FontWeight.ExtraBold),
				Text = "Diagnostics",
				TextColor3 = theme.colors.onSurface,
				TextSize = theme.variables.textSize,
				TextWrapped = true,
			}),

			version = e(ControlGroup, {
				title = "Version",
				description = `{Manifest.package.version}{sourceCommitHash and `+{sourceCommitHash
					:sub(1, 7)
					:lower()}` or ""}{buildSessionId and " (DEV)" or " (PROD)"}`,
				native = {
					LayoutOrder = nextOrder(),
				},
			}),

			userId = studioUserId and e(ControlGroup, {
				title = "User ID",
				description = studioUserId,
				native = {
					LayoutOrder = nextOrder(),
				},
			}),

			acquisitionSource = e(ControlGroup, {
				title = "Acquisition Source",
				description = pluginSource == "creator_store" and "Creator Store"
					or "Model file",
				native = {
					LayoutOrder = nextOrder(),
				},
			}),
		}),

		-- Footer
		[uniqueKey("divider")] = e(Divider, {
			orientation = "horizontal",
			native = {
				LayoutOrder = nextOrder(),
			},
		}) :: never,

		footer = e("Frame", {
			AutomaticSize = Enum.AutomaticSize.XY,
			BackgroundTransparency = 1,
			LayoutOrder = nextOrder(),
		}, {
			resetAllSettings = e(Button, {
				text = "Reset All Settings",
				native = {
					LayoutOrder = nextOrder(),
				},
				onClick = function()
					for _, preference in pairs(Preferences) do
						preference.reset()
					end

					LocalStore.currentWidget(nil)
				end,
			}),
		}),
	})
end

return (memo(SettingsWidget) :: never) :: typeof(SettingsWidget)


local UserInputService = game:GetService("UserInputService")

local Array = require("@root/Lib/Array")
local React = require("@pkgs/React")

local IconMap = require("@root/Assets/IconMap")
local LocalStore = require("@root/Stores/LocalStore")
local Preferences = require("@root/Stores/Preferences")

local Button = require("../Components/Button")
local ComponentGroup = require("../Components/ComponentGroup")
local Popover = require("../Components/Popover")
local SurfacePickerGridView = require("../Components/SurfacePickerGridView")
local SurfacePickerListView = require("../Components/SurfacePickerListView")
local TextBox = require("../Components/TextBox")

local useValue = require("@root/Lib/Store").useValue
local useTheme = require("../Hooks/useTheme")
local useEffect = React.useEffect
local useState = React.useState
local useMemo = React.useMemo
local useRef = React.useRef

local createNextOrder = require("../Lib/createNextOrder")
local e = React.createElement

local SURFACE_TYPES: { Enum.SurfaceType } = Enum.SurfaceType:GetEnumItems()
local SURFACE_TYPE_NAMES = Array.map(SURFACE_TYPES, function(surfaceType)
	return surfaceType.Name:lower()
end)

local function calculateSearchInputInset(theme: typeof(useTheme()), numButtons: number)
	return -(
		(theme.variables.spacing * 2 + theme.variables.textSize) * numButtons
		+ (theme.variables.spacing / 2) * numButtons
	)
end

local function SurfacePickerWidget(props: {
	anchorRef: { current: GuiBase2d? },
})
	local theme = useTheme()
	local nextOrder = createNextOrder()

	local scrollingFrameRef = useRef(nil :: ScrollingFrame?)
	local textBoxRef = useRef(nil :: TextBox?)

	local isVisible = useValue(LocalStore.currentWidget) == "surfacePicker"
	local isInListView = useValue(Preferences.isSurfacePickerInListView)
	local autoFocusSurfacePickerSearch =
		useValue(Preferences.autoFocusSurfacePickerSearch)

	local searchQuery, setSearchQuery = useState("")

	local surfaceTypes: { Enum.SurfaceType } = useMemo(function()
		if searchQuery == "" then
			return SURFACE_TYPES
		end

		local surfaces = {}

		for _, surfaceType: Enum.SurfaceType in SURFACE_TYPES do
			if surfaceType.Name:lower():match(searchQuery) then
				table.insert(surfaces, surfaceType)
			end
		end

		return surfaces
	end, { searchQuery })

	local quickSelectSurfaceType = useMemo(function()
		local quickSelectItem: Enum.SurfaceType?

		if #surfaceTypes == 1 then
			quickSelectItem = surfaceTypes[1]
		end

		if not quickSelectItem then
			local exactMatchIndex = table.find(SURFACE_TYPE_NAMES, searchQuery)

			if exactMatchIndex then
				quickSelectItem = SURFACE_TYPES[exactMatchIndex]
			end
		end

		return quickSelectItem
	end, { surfaceTypes } :: { any })

	useEffect(function()
		local connection = UserInputService.InputEnded:Connect(
			function(input, robloxProcessed)
				if robloxProcessed or not isVisible then
					return
				end

				if textBoxRef.current and input.KeyCode == Enum.KeyCode.Slash then
					textBoxRef.current:CaptureFocus()
				end
			end
		)

		return function()
			connection:Disconnect()
		end
	end, { isVisible })

	return e(Popover, {
		visible = isVisible,
		anchorRef = props.anchorRef,
		scrollingFrameRef = scrollingFrameRef,
		stickyHeader = e(ComponentGroup, {
			native = {
				AutomaticSize = Enum.AutomaticSize.Y,
				Size = UDim2.fromScale(1, 0),
				LayoutOrder = nextOrder(),
			},
			layoutNative = {
				HorizontalFlex = Enum.UIFlexAlignment.Fill,
			},
		}, {
			search = e(TextBox, {
				textBoxRef = textBoxRef,
				fullWidth = true,
				autofocus = autoFocusSurfacePickerSearch,
				placeholder = "Search",
				icon = IconMap.Icons.search,
				text = searchQuery,
				native = {
					AutomaticSize = Enum.AutomaticSize.Y,
					LayoutOrder = nextOrder(),
					Size = UDim2.new(1, calculateSearchInputInset(theme, 2), 0, 0),
				},
				onChange = function(_, text)
					local trimmed = text:gsub("[^%w]", "")
					setSearchQuery(trimmed:lower())
				end,
				onFocusLost = function(_, _, enterPressed)
					if not enterPressed then
						return
					end

					local selectIndex = 1

					if #surfaceTypes ~= 1 then
						local foundIndex = table.find(SURFACE_TYPE_NAMES, searchQuery)

						if not foundIndex then
							return
						end

						selectIndex = foundIndex
					end

					if surfaceTypes[selectIndex] or quickSelectSurfaceType then
						Preferences.selectedSurface(
							surfaceTypes[selectIndex] or quickSelectSurfaceType
						)

						LocalStore.currentWidget(nil)
					end
				end,
			}),

			viewModeToggles = e(ComponentGroup, {
				native = {
					LayoutOrder = nextOrder(),
				},
			}, {
				gridView = e(Button, {
					icon = IconMap.Icons.view_grid,
					selected = not isInListView,
					onClick = function()
						Preferences.isSurfacePickerInListView(false)
					end,
				}),

				listView = e(Button, {
					icon = IconMap.Icons.view_list,
					selected = isInListView,
					onClick = function()
						Preferences.isSurfacePickerInListView(true)
					end,
				}),
			}),
		}) :: never,
	}, {
		view = isInListView and e(SurfacePickerListView, {
			surfaceTypes = surfaceTypes,
			highlightedSurfaceType = quickSelectSurfaceType,
			scrollingFrameRef = scrollingFrameRef,
		}) or e(SurfacePickerGridView, {
			surfaceTypes = surfaceTypes,
			highlightedSurfaceType = quickSelectSurfaceType,
			scrollingFrameRef = scrollingFrameRef,
		}),
	})
end

return SurfacePickerWidget


--!strict
local function filter<T>(array: { T }, predicate: (value: T, index: number) -> any): { T }
	local output = {}

	for index, value in array do
		if predicate(value, index) then
			table.insert(output, value)
		end
	end

	return output
end

local function map<T, U>(array: { T }, mapper: (value: T, index: number) -> U?): { U }
	local output = {}

	for index, value in array do
		local newValue = mapper(value, index)

		if newValue ~= nil then
			table.insert(output, newValue)
		end
	end

	return output
end

return {
	filter = filter,
	map = map,
}


export type Value<T> =
	((value: (T | (current: T) -> T)?, bypassSignals: boolean?) -> T)
	& {
		changed: RBXScriptSignal<T, T>,
		initialValue: T,
		destroy: () -> (),
		reset: () -> (),
		onChange: (prev: T, next: T) -> () -> (),
		onReset: (() -> ())?,
	}

local function Value<T>(
	value: T,
	api: {
		validate: ((value: T) -> boolean)?,
	}?
)
	local changedSignal = Instance.new("BindableEvent")
	local cachedValue = value

	local proxy: any
	proxy = setmetatable({
		changed = changedSignal.Event,
		initialValue = value,
		onChange = function(_, callback)
			local connection = changedSignal.Event:Connect(function(...)
				callback(...)
			end)

			return function()
				connection:Disconnect()
			end
		end,
		destroy = function()
			changedSignal:Destroy()

			changedSignal = nil :: never
			cachedValue = nil :: never
		end,
		reset = function()
			local currentValue = cachedValue
			cachedValue = value

			changedSignal:Fire(currentValue, value)

			if proxy.onReset then
				proxy.onReset()
			end
		end,
		onReset = nil :: () -> ()?,
	}, {
		__call = function(_, ...)
			if select("#", ...) == 0 then
				return cachedValue
			end

			local proposedValue = select(1, ...)

			if typeof(proposedValue) == "function" then
				proposedValue = proposedValue(cachedValue)
			end

			if cachedValue == proposedValue then
				return cachedValue
			end

			if api and api.validate and not api.validate(proposedValue) then
				return cachedValue
			end

			if select(2, ...) ~= false then
				changedSignal:Fire(cachedValue, proposedValue)
			end

			cachedValue = proposedValue
			return proposedValue
		end,
	})

	return proxy :: Value<T>
end

local hooks = {}
do
	local React = require("@pkgs/React")

	local useEffect = React.useEffect
	local useState = React.useState

	function hooks.useValue<T, V>(value: Value<T>): T
		local currentValue, setCurrentValue = useState(value())

		useEffect(function()
			local valueChanged = value.changed:Connect(function(_, newValue)
				setCurrentValue(function(current)
					if current ~= newValue then
						return newValue
					end

					return current
				end)
			end)

			return function()
				valueChanged:Disconnect()
			end
		end, { value.changed } :: { any })

		return currentValue
	end
end

return {
	Value = Value,
	useValue = hooks.useValue,
}


local function createUniqueKey(existingNames: { any }?)
	local names = {}

	if existingNames then
		for _, name in existingNames do
			local stringified = tostring(name)

			if names[stringified] == nil then
				names[stringified] = 1
			else
				names[stringified] += 1
			end
		end
	end

	return function(name: any): string
		name = tostring(name)

		if names[name] == nil then
			names[name] = 1
			return name
		end

		while true do
			names[name] += 1
			local finalName = `{name}_{names[name]}`

			if names[finalName] == nil then
				return finalName
			end
		end
	end
end

return createUniqueKey


local WallyManifest = require(script.Parent.WallyPackageManifest) :: WallyManifest

type WallyManifest = {
	package: {
		name: string,
		realm: string,
		registry: string,
		version: string,
	},
	dependencies: {
		[string]: string,
	},
	product: {
		description: string,
		display_name: string,
	},
}

return WallyManifest


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

local MaterialVariants = require("@root/Assets/MaterialVariants")

local createUniqueKey = require("@root/Lib/createUniqueKey")

local NORMAL_IDS: { Enum.NormalId } = Enum.NormalId:GetEnumItems()

local SURFACE_NORMAL_MAP = {
	[Enum.NormalId.Top] = "TopSurface",
	[Enum.NormalId.Bottom] = "BottomSurface",
	[Enum.NormalId.Left] = "LeftSurface",
	[Enum.NormalId.Right] = "RightSurface",
	[Enum.NormalId.Front] = "FrontSurface",
	[Enum.NormalId.Back] = "BackSurface",
}

local SURFACE_NO_VARIANTS = {
	[Enum.SurfaceType.Smooth] = true,
	[Enum.SurfaceType.SmoothNoOutlines] = true,
	[Enum.SurfaceType.SteppingMotor] = true,
	[Enum.SurfaceType.Motor] = true,
}

local function findExistingMaterialVariant(
	variant: MaterialVariants.Variant
): MaterialVariant?
	for _, object in MaterialService:GetDescendants() do
		if object:IsA("MaterialVariant") then
			if object.ColorMap ~= variant.colorMap then
				continue
			end

			if object.NormalMap ~= variant.normalMap then
				continue
			end

			if object.StudsPerTile ~= variant.studsPerTile then
				continue
			end

			return object
		end
	end

	return
end

local function getMaterialVariant(surfaceType: Enum.SurfaceType): MaterialVariant?
	local materialVariant

	for _, variant in MaterialVariants do
		if variant.surfaceType == surfaceType then
			materialVariant = variant
			break
		end
	end

	if not materialVariant then
		return
	end

	local existingVariant = findExistingMaterialVariant(materialVariant)

	if existingVariant then
		return existingVariant
	end

	local existingVariantNames = {}

	for _, variant in MaterialService:GetChildren() do
		table.insert(existingVariantNames, variant.Name)
	end

	local uniqueName = createUniqueKey(existingVariantNames)

	local newVariant = Instance.new("MaterialVariant")
	newVariant.Name = uniqueName(surfaceType.Name)
	newVariant.BaseMaterial = Enum.Material.Plastic
	newVariant.ColorMap = materialVariant.colorMap
	newVariant.NormalMap = materialVariant.normalMap
	newVariant.StudsPerTile = materialVariant.studsPerTile
	newVariant.Parent = MaterialService

	return newVariant
end

local function extractBaseParts(objects: { Instance })
	local partMap = {}
	local count = 0

	for _, object in objects do
		if object == workspace or object == workspace.Terrain then
			continue
		end

		if object:IsA("BasePart") then
			if not partMap[object] then
				partMap[object] = true
				count += 1
			end

			continue
		end

		if object:IsA("Folder") or object:IsA("PVInstance") then
			local descendants = object:GetDescendants()

			for _, descendant in descendants do
				if descendant:IsA("BasePart") then
					if not partMap[descendant] then
						partMap[descendant] = true
						count += 1
					end
				end
			end
		end
	end

	return partMap, count
end

local function containsAllNormalIds(array: { Enum.NormalId }?)
	if not array then
		return false
	end

	for _, normalId in NORMAL_IDS do
		if not table.find(array, normalId) then
			return false
		end
	end

	return true
end

local function pluralize(word: string, count: number)
	if count == 1 or word:sub(-1):lower() == "s" then
		return word
	end

	return `{word}s`
end

local function resetAllSurfaces(
	part: BasePart,
	props: {
		material: Enum.Material?,
		surfaceType: Enum.SurfaceType?,
	}?
)
	local material = props and props.material or Enum.Material.Plastic
	local surfaceType = props and props.surfaceType or Enum.SurfaceType.Smooth

	part.Material = material
	part.MaterialVariant = ""

	for _, normal in NORMAL_IDS do
		local property = SURFACE_NORMAL_MAP[normal];
		(part :: any)[property] = surfaceType
	end
end

local function apply(
	objects: { Instance },
	props: {
		surfaceType: Enum.SurfaceType,
		normalIds: { Enum.NormalId }?,
		useMaterialVariants: boolean?,
		useMaterialVariantsMeshUnion: boolean?,
		setMaterialToPlastic: boolean?,
	}
)
	local normalIds: { Enum.NormalId } = props.normalIds or NORMAL_IDS
	local partMap, partCount = extractBaseParts(objects)

	if #normalIds == 0 or partCount == 0 then
		return
	end

	local useMaterialVariants = props.useMaterialVariants
	local useMaterialVariantsMeshUnion = props.useMaterialVariantsMeshUnion
	local setMaterialToPlastic = props.setMaterialToPlastic

	local applyAllFaces = normalIds == NORMAL_IDS or containsAllNormalIds(normalIds)

	local ok, recordingId = pcall(function()
		return ChangeHistoryService:TryBeginRecording(
			`apply surface "{props.surfaceType.Name}" to {partCount} {pluralize(
				"part",
				partCount
			)}`
		)
	end)

	if not ok or not recordingId then
		error(
			`[Resurface] ChangeHistoryService failed to provide a recording id{recordingId and `: {recordingId}` or ""}.`
		)
	end

	for part in partMap do
		if applyAllFaces then
			local processMaterialVariants = useMaterialVariants

			if part:IsA("TriangleMeshPart") and not useMaterialVariants then
				processMaterialVariants = useMaterialVariantsMeshUnion
			end

			if processMaterialVariants then
				if SURFACE_NO_VARIANTS[props.surfaceType] then
					local isNoOutlines = props.surfaceType
						== Enum.SurfaceType.SmoothNoOutlines

					resetAllSurfaces(part, {
						material = isNoOutlines and Enum.Material.SmoothPlastic
							or Enum.Material.Plastic,
						surfaceType = isNoOutlines
								and Enum.SurfaceType.SmoothNoOutlines
							or Enum.SurfaceType.Smooth,
					})

					continue
				end

				local materialVariant = getMaterialVariant(props.surfaceType)

				if materialVariant then
					resetAllSurfaces(part, {
						material = materialVariant.BaseMaterial,
						surfaceType = Enum.SurfaceType.Smooth,
					})

					part.MaterialVariant = materialVariant.Name
				end

				continue
			end
		end

		if part:IsA("TriangleMeshPart") then
			continue
		end

		if setMaterialToPlastic then
			part.Material = Enum.Material.Plastic
			part.MaterialVariant = ""
		end

		for _, normal in normalIds do
			local property = SURFACE_NORMAL_MAP[normal];
			(part :: any)[property] = props.surfaceType
		end
	end

	ChangeHistoryService:FinishRecording(
		recordingId,
		Enum.FinishRecordingOperation.Commit
	)
end

return {
	apply = apply,
}


local plugin: Plugin = plugin or script:FindFirstAncestorOfClass("Plugin")

local Store = require("@root/Lib/Store")

local Value = Store.Value
type Value<T> = Store.Value<T>

local validWidgetIds = {
	settings = true,
	surfacePicker = true,
}

local store = {
	isPluginActive = Value(false),
	currentWidget = Value(nil, {
		validate = function(value: string?)
			return not value or validWidgetIds[value]
		end,
	}) :: Value<nil | "settings" | "surfacePicker">,
	isListeningForKeybinds = Value(false),
	applyToAllfacesOverride = Value(false),
	isLegacyPluginTransition = Value(false),
	hasActionableSelection = Value(false),
}

store.isLegacyPluginTransition.changed:Connect(function(_, value)
	if not value then
		plugin:SetSetting("currentVersion", nil)
	end
end)

store.isPluginActive.changed:Connect(function(_, isActive)
	if isActive then
		return
	end

	store.currentWidget(nil)
end)

return store


local Store = require("@root/Lib/Store")

local Value = Store.Value
type Value<T> = Store.Value<T>

type Keybind = Enum.KeyCode | "CTRL" | "SHIFT" | "ALT"

local sessionSyncedPreferences = {
	isReducedMotion = true,
	transparencyScale = true,
}

local store = {
	selectedSurface = Value(Enum.SurfaceType.Studs),

	applyToAllfaces = Value(false),
	setMaterialToPlastic = Value(true),

	-- useTextures = Value(false),
	useMaterialVariants = Value(true),
	useMaterialVariantsMeshUnion = Value(true),

	isSurfacePickerInListView = Value(false),
	autoFocusSurfacePickerSearch = Value(true),

	useClassicMenuOnLaunch = Value(false),
	deactivatePluginAfterOperation = Value(false),

	preferredThemeId = Value(nil) :: Value<string?>,
	useBounceAnimations = Value(true),

	isReducedMotion = Value(false),
	transparencyScale = Value(1),

	keybindSurfacePickerWidget = Value(Enum.KeyCode.B) :: Value<Keybind>,
	keybindApplyToAllFacesOverride = Value("CTRL") :: Value<Keybind>,
}

do -- Persistence
	local plugin: Plugin = plugin or script:FindFirstAncestorOfClass("Plugin")
	local RunService = game:GetService("RunService")

	local Manifest = require("@root/Manifest")

	local nextRunTimestamp = tick() + 5
	local isProcessing = false

	local function serialize(value: any)
		local valueType = typeof(value)
		local serializedValue = value
		local extraData

		if valueType == "EnumItem" then
			serializedValue = value.Name
			extraData = tostring(value.EnumType)
		elseif valueType == "table" then
			serializedValue = table.clone(value)

			for k, v in serializedValue do
				serializedValue[k] = serialize(v)
			end
		end

		return { v = serializedValue, t = valueType, x = extraData }
	end

	local function deserialize(data: { v: { v: any, t: any, x: any } })
		local valueType = data.v.t
		local serializedValue = data.v.v
		local extraData = data.v.x

		if valueType == "EnumItem" then
			return (Enum :: any)[extraData][serializedValue]
		elseif valueType == "table" then
			serializedValue = table.clone(serializedValue)

			for k, v in serializedValue do
				serializedValue[k] = deserialize(v)
			end
		end

		return serializedValue
	end

	local function toSafeString(value: string)
		local formatted = value:gsub("[^%w]", "_")
		return formatted
	end

	local keyPrefix = toSafeString(Manifest.package.name)
	local function toSafeId(id: string)
		return `{keyPrefix}__{toSafeString(id)}`
	end

	local valueChangeTimestamps = {}

	for id, value in store :: { [string]: Value<any> } do
		local safeId = toSafeId(id)
		local cachedValue = plugin:GetSetting(safeId)

		if cachedValue then
			local deserializedValue = deserialize(cachedValue)
			value(deserializedValue)
		end

		value.changed:Connect(function()
			local now = tick()

			valueChangeTimestamps[id] = now
			nextRunTimestamp = now + 5
		end)

		value.onReset = function()
			plugin:SetSetting(safeId, nil)
		end
	end

	local function doSave()
		isProcessing = true

		for id, value in store :: { [string]: Value<any> } do
			if not sessionSyncedPreferences[id] and not valueChangeTimestamps[id] then
				continue
			end

			local safeId = toSafeId(id)

			local cachedValue = plugin:GetSetting(safeId)
			local currentValue = value()

			local localValueTimestamp = valueChangeTimestamps[id]
			valueChangeTimestamps[id] = nil

			if cachedValue then
				local deserializedValue = deserialize(cachedValue)

				if deserializedValue == currentValue then
					continue
				end

				if not localValueTimestamp or cachedValue.t >= localValueTimestamp then
					if sessionSyncedPreferences[id] then
						value(deserializedValue)
					end

					continue
				end
			end

			if not cachedValue and value.initialValue == currentValue then
				continue
			end

			plugin:SetSetting(safeId, {
				v = serialize(currentValue),
				t = tick(),
			})
		end

		isProcessing = false
	end

	local updateSignal = RunService.Heartbeat:Connect(function()
		if tick() < nextRunTimestamp or isProcessing then
			return
		end

		doSave()
		nextRunTimestamp = tick() + 5
	end)

	plugin.Unloading:Connect(function()
		updateSignal:Disconnect()
		doSave()
	end)
end

do
	if store.selectedSurface() == nil then
		store.selectedSurface.reset()
	end
end

return store


local CoreGui = game:GetService("CoreGui")

local Janitor = require("@pkgs/Janitor")
local React = require("@pkgs/React")
local ReactRoblox = require("@pkgs/ReactRoblox")

local LocalStore = require("./Stores/LocalStore")

-- Migrations to ease transition from Surface Tool
if plugin:GetSetting("currentVersion") ~= nil then
	LocalStore.isLegacyPluginTransition(true)
end

local App = require("./Frontend/App")

local janitor = Janitor.new()
local robloxCoreGui: ScreenGui? = CoreGui:FindFirstChild("RobloxGui")

local buildSessionId = script:GetAttribute("BuildSessionId")
	or script:GetAttribute("SourceCommitHash")
	or math.floor(tick() + 0.5)
buildSessionId = tostring(buildSessionId):sub(1, 7):lower()

local container = Instance.new("ScreenGui")
container.Name = `ResurfaceViewportWidget<{buildSessionId}>`
container.ZIndexBehavior = Enum.ZIndexBehavior.Sibling
container.IgnoreGuiInset = true
container.DisplayOrder = robloxCoreGui and robloxCoreGui.DisplayOrder + 100 or 1

local root = ReactRoblox.createRoot(container)
local app = React.createElement(App, {
	plugin = plugin,
})

local pluginActiveConnection = LocalStore.isPluginActive.changed:Connect(
	function(prevActive, isActive)
		if prevActive == isActive then
			return
		end

		if isActive then
			plugin:Activate(true)
		elseif plugin:IsActivated() then
			plugin:Deactivate()
		end
	end
)

local pluginDeactivationConnection = plugin.Deactivation:Connect(function()
	LocalStore.isPluginActive(false)
end)

local pluginUnloadingConnection = plugin.Unloading:Connect(function()
	if LocalStore.isPluginActive() or plugin:IsActivated() then
		plugin:Deactivate()
		plugin:SelectRibbonTool(Enum.RibbonTool.Select, UDim2.new())
	end

	janitor:Destroy()
end)

janitor:Add(root, "unmount")
janitor:Add(container, "Destroy")

janitor:Add(pluginActiveConnection, "Disconnect")

janitor:Add(pluginDeactivationConnection, "Disconnect")
janitor:Add(pluginUnloadingConnection, "Disconnect")

root:render(app)

container.Parent = CoreGui


--> Task to typecheck packages using luau-lsp

local task = require("./task")

task.new("luau-lsp")
	:addArgument("analyze")
	:addArgument("--base-luaurc=.luaurc")
	:addArgument("--settings=.vscode/settings.json")
	:addArgument("--defs=globalTypes.d.luau")
	:addArgument("--ignore=**/luau_packages/**")
	:addArgument("--ignore=**/lune_packages/**")
	:addArgument("--ignore=**/.lune**")
	:addArgument("--ignore=**/bundled.luau")
	:addArgument("packages")
	:execute()


--> Task to typecheck packages using luau-lsp

local task = require("./task")

task.new("pesde"):addArgument("install"):setcwd("packages/api_types"):execute()
task.new("lune"):addArgument("run"):addArgument("bundle"):setcwd("packages/api_types"):execute()


--> Task to execute StyLua for checking validity of formatting

local task = require("./task")

task.new("stylua")
	:addArgument("--check")
	:addArgument("--output-format")
	:addArgument("Summary")
	:addArgument("packages")
	:execute()


--> Task for linting packages using selene

local task = require("./task")

task.new("selene"):addArgument("packages"):execute()


--> run a script, scripts can be found under ./lune/scripts

-- scripts are generally just luau files that are quickly put together in order to achieve something quickly -
-- such as automating files

-- NOTE: we're using the `Lune` runtime in the CI/CD environment, so we're not going to be using the `@std-polyfills`
-- package that the discord-luau package uses.

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

local task = require("./task")

local processArguments = table.clone(process.args)
local scriptName = table.remove(processArguments, 1)
local scriptPath = `.lune/scripts/{scriptName}.luau`

assert(fs.isFile(scriptPath), `Script '{scriptName}' not found! Unable to execute script!`)

local taskObject = task.new("lune"):addArgument("run"):addArgument(scriptPath)

for _, argument in processArguments do
	taskObject:addArgument(argument)
end

taskObject:execute()


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

local packageDir = process.args[1]

local function patchFile(dir, fileName)
	local fileData = fs.readFile(`{dir}/{fileName}`)

	for requirePath: string in fileData.gmatch(fileData, 'local %S+ = require%("(%S+)"%)') do
		if string.sub(requirePath, 1, 1) == "@" then
			print(`{dir}/{fileName}`)
		end
	end
end

local function readFolder(dir)
	for _, object in fs.readDir(dir) do
		if fs.isDir(`{dir}/{object}`) then
			readFolder(`{dir}/{object}`)
		else
			patchFile(dir, object)
		end
	end
end

readFolder(packageDir)


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

local luaTableSource = "{\n"
local packageDir = process.args[1]

local function readFile(dir, fileName)
	local fileName = string.sub(fileName, 1, -6)
	local source = `{dir}/{fileName}`

	luaTableSource = `local {fileName} = require("{string.gsub(source, packageDir .. "/", "")}")\n` .. luaTableSource
	luaTableSource ..= `{fileName} = {fileName},\n`
end

local function openFolder(dir)
	local dirSplit = string.split(dir, "/")
	local dirName = dirSplit[#dirSplit]

	luaTableSource ..= `{dirName} = \{\n`
end

local function closeFolder()
	luaTableSource ..= `},\n`
end

local function readFolder(dir)
	for _, object in fs.readDir(dir) do
		if fs.isDir(`{dir}/{object}`) then
			openFolder(`{dir}/{object}`)
			readFolder(`{dir}/{object}`)
			closeFolder()
		else
			readFile(dir, object)
		end
	end
end

readFolder(packageDir)

luaTableSource ..= "}\n"

print(luaTableSource)


--> Runs tests located in the tests/ directory

-- NOTE: we're using the `Lune` runtime in the CI/CD environment, so we're not going to be using the `@std-polyfills`
-- package that the discord-luau package uses.

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

local requireTests
local requireFile

function requireFile(fullPath: string)
	local success, result = pcall(require, fullPath)

	if not success then
		print(`\n{stdio.color("red")}Failed to require unit-test at \`{fullPath}\`, error:`)
		print(`{stdio.style("dim")}{result}`)

		process.exit(1)
	else
		result()
	end
end

function requireTests(path: string)
	for _, object in fs.readDir(path) do
		if fs.isDir(`{path}/{object}`) then
			if object == "luau_packages" or object == "lune_packages" then
				continue
			end

			requireTests(`{path}/{object}`)
		else
			if not string.find(object, ".spec.luau") then
				continue
			end

			requireFile(`{path}/{object}`)
		end
	end
end

return requireTests


--> Task for running unit-tests using frktest

local frktest = require("../lune_packages/frktest")
local reporter = frktest._reporters.lune_console_reporter

local requireTests = require("./task/requireTests")

requireTests("./packages")

reporter.init()

if not frktest.run() then
	print("--------------------")
	error(`See above for test failures.`, -1)
end


require("@self/luau-lsp").config({
	server = {
		settings = {
			["luau-lsp"] = {
				inlayHints = {
					variableTypes = true,
					functionReturnTypes = true,
					parameterNames = "all",
					typeHintMaxLength = 20,
				},
				require = {
					mode = "relativeToFile",
					directoryAliases = {},
				},
				completion = {
					imports = {
						enabled = true,
					},
				},
			},
		},
	},
	platform = {
		type = "standard",
	},
})


declare function warn<T...>(...: T...)

declare game: any
declare workspace: any


--> Task to execute Darklua for bundling the various packages apart of Discord-Luau

local task = require("../../../.lune/task")

task.new("darklua")
	:addArgument("process")
	:addArgument("src/init.luau")
	:addArgument("bundled.luau")
	:addArgument(`-cdarklua.json`)
	:execute()

task.new("stylua"):addArgument("bundled.luau"):execute()
task.new("lune"):addArgument("run"):addArgument("fixTypes"):execute()
task.new("stylua"):addArgument("bundled.luau"):execute()


--[=[
	@class Types.application
]=]

local util = require("../luau_packages/util")

local MembershipState = table.freeze(util.tableReflect({
	Invited = 1,
	Accepted = 2,
}))

local IntegrationTypesConfig = table.freeze(util.tableReflect({
	GuildInstall = 0,
	UserInstall = 1,
}))

export type MembershipState = "Invited" | "Accepted"
export type TeamRole = "Owner" | "Admin" | "Developer" | "ReadOnly"

export type IntegrationTypesConfig = "GuildInstall" | "UserInstall"

export type OAuth2Scopes =
	"activities.read"
	| "activities.write"
	| "applications.builds.read"
	| "applications.builds.upload"
	| "applications.commands"
	| "applications.commands.update"
	| "applications.commands.permissions.update"
	| "applications.entitlements"
	| "applications.store.update"
	| "bot"
	| "connections"
	| "dm_channels.read"
	| "email"
	| "gdm.join"
	| "guilds"
	| "guilds.join"
	| "guilds.members.read"
	| "identify"
	| "messages.read"
	| "relationships.read"
	| "role_connections.write"
	| "rpc"
	| "rpc.activities.write"
	| "rpc.notifications.read"
	| "rpc.voice.read"
	| "rpc.voice.write"
	| "voice"
	| "webhook.incoming"

return {
	MembershipState = MembershipState,
	IntegrationTypesConfig = IntegrationTypesConfig,
}


--[=[
	@class Types.channel
]=]

local util = require("../luau_packages/util")

local apiTypes = require("./apiTypes")

local ChannelTypes = table.freeze(util.tableReflect({
	GuildText = 0,
	DM = 1,
	GuildVoice = 2,
	GroupDM = 3,
	GuildCategory = 4,
	GuildAnnouncement = 5,
	AnnouncementThread = 10,
	PublicThread = 11,
	PrivateThread = 12,
	GuildStageVoice = 13,
	GuildDirectory = 14,
	GuildForum = 15,
	GuildMedia = 16,
}))

local MediaSortOrder = table.freeze(util.tableReflect({
	LatestActivity = 0,
	CreationDate = 1,
}))

local ForumSortOrder = table.freeze(util.tableReflect({
	LatestActivity = 0,
	CreationDate = 1,
}))

local ForumLayout = table.freeze(util.tableReflect({
	NotSet = 0,
	ListView = 1,
	GalleryView = 2,
}))

local VideoQualityMode = table.freeze(util.tableReflect({
	Auto = 1,
	Full = 2,
}))

local SelectMenuType = table.freeze(util.tableReflect({
	String = 3,
	Text = 4,
	User = 5,
	Role = 6,
	Mentionable = 7,
	Channel = 8,
}))

export type ChannelType =
	"GuildText"
	| "DM"
	| "GuildVoice"
	| "GroupDM"
	| "GuildCategory"
	| "GuildAnnouncement"
	| "AnnouncementThread"
	| "PublicThread"
	| "PrivateThread"
	| "GuildStageVoice"
	| "GuildDirectory"
	| "GuildForum"
	| "GuildMedia"

export type MediaSortOrder = "LatestActivity" | "CreationDate"
export type ForumSortOrder = "LatestActivity" | "CreationDate"
export type ForumLayout = "NotSet" | "ListView" | "GalleryView"

export type VideoQualityMode = "Auto" | "Full"

export type SelectMenuType = "String" | "Text" | "User" | "Role" | "Mentionable" | "Channel"

export type AbstractChannel = {
	id: apiTypes.Snowflake?,
	type: ChannelType,
	guild_id: apiTypes.Snowflake?,
	position: number,
	permission_overwrites: { apiTypes.OverwriteObject },
	name: string,
	topic: string?,
	nsfw: boolean?,
	last_message_id: apiTypes.Snowflake?,
	bitrate: number?,
	user_limit: number?,
	rate_limit_per_user: number?,
	recipients: { apiTypes.UserObject }?,
	icon: string?,
	owner_id: apiTypes.Snowflake?,
	application_id: apiTypes.Snowflake?,
	managed: boolean?,
	parent_id: apiTypes.Snowflake?,
	last_pin_timestamp: string?,
	rtc_region: string?,
	video_quality_mode: VideoQualityMode?,
	message_count: number?,
	member_count: number?,
	thread_metadata: apiTypes.ThreadMetadataObject?,
	member: apiTypes.ThreadMemberObject?,
	default_auto_archive_duration: number?,
	permissions: string?,
	flags: apiTypes.ChannelFlags?,
	total_message_sent: number?,
	available_tags: { apiTypes.ForumTagObject }?,
	applied_tags: { apiTypes.Snowflake }?,
	default_reaction_emoji: apiTypes.DefaultReactionObject?,
	default_thread_rate_limit_per_user: number?,
	default_sort_order: apiTypes.SortOrderType?,
	default_forum_layout: apiTypes.ForumLayoutType?,
}

return {
	ChannelTypes = ChannelTypes,
	MediaSortOrder = MediaSortOrder,
	ForumSortOrder = ForumSortOrder,
	ForumLayout = ForumLayout,
	VideoQualityMode = VideoQualityMode,
	SelectMenuType = SelectMenuType,
}


--[=[
	@class Types.client
]=]

local util = require("../luau_packages/util")

local ActivityType = table.freeze(util.tableReflect({
	Game = 0,
	Streaming = 1,
	Listening = 2,
	Watching = 3,
	Competing = 5,
}))

local Status = table.freeze(util.tableReflect({
	Online = "online",
	DoNotDisturb = "dnd",
	Idle = "idle",
	Invisible = "invisible",
	Offline = "offline",
}))

export type ActivityType = "Game" | "Streaming" | "Listening" | "Watching" | "Competing"
export type Status = "Online" | "DoNotDisturb" | "Idle" | "Invisible" | "Offline"

return {
	ActivityType = ActivityType,
	Status = Status,
}


--[=[
	@class Types.embed
]=]

local util = require("../luau_packages/util")

local EmbedType = table.freeze(util.tableReflect({
	Rich = "rich",
	Image = "image",
	Video = "video",
	Gifv = "gifv",
	Article = "article",
	Link = "link",
	PollResult = "poll_result",
}))

export type EmbedType = "Rich" | "Image" | "Video" | "Gifv" | "Article" | "Link" | "PollResult"

return {
	EmbedType = EmbedType,
}


--[=[
	@class Types.entitlement
]=]

local util = require("../luau_packages/util")

local EntitlementType = table.freeze(util.tableReflect({
	Purchase = 1,
	PremiumSubscription = 2,
	DeveloperGift = 3,
	TestModePurchase = 4,
	FreePurchase = 5,
	UserGift = 6,
	PremiumPurchase = 7,
	ApplicationSubscription = 8,
}))

export type EntitlementType =
	"Purchase"
	| "PremiumSubscription"
	| "DeveloperGift"
	| "TestModePurchase"
	| "FreePurchase"
	| "UserGift"
	| "PremiumPurchase"
	| "ApplicationSubscription"

return {
	EntitlementType = EntitlementType,
}


--[=[
	@class Types.gateway.closeCodes

	https://discord.com/developers/docs/topics/opcodes-and-status-codes#gateway-gateway-close-event-codes
]=]

local CloseCodes = table.freeze({
	UnknownError = 4000,
	UnknownOpcode = 4001,
	DecodeError = 4002,
	NotAuthenticated = 4003,
	AuthenticationFailed = 4004,
	AlreadyAuthenticated = 4005,
	InvalidSeq = 4007,
	Ratelimited = 4008,
	SessionTimedOut = 4009,
	InvalidShard = 4010,
	ShardingRequired = 4011,
	InvalidAPIVersion = 4012,
	InvalidIntents = 4013,
	DisallowedIntents = 4014,
})

export type CloseCode =
	"UnknownError"
	| "UnknownOpcode"
	| "DecodeError"
	| "NotAuthenticated"
	| "AuthenticationFailed"
	| "AlreadyAuthenticated"
	| "InvalidSeq"
	| "Ratelimited"
	| "SessionTimedOut"
	| "InvalidShard"
	| "ShardingRequired"
	| "InvalidAPIVersion"
	| "InvalidIntents"
	| "DisallowedIntents"

return CloseCodes


--[=[
	@class Types.gateway.intents

	https://discord.com/developers/docs/topics/gateway#list-of-intents
]=]

local Intents = table.freeze({
	Guilds = 0,
	GuildMembers = 1,
	GuildModeration = 2,
	GuildEmojisAndStickers = 3,
	GuildIntegrations = 4,
	GuildWebhooks = 5,
	GuildInvites = 6,
	GuildVoiceStates = 7,
	GuildPresences = 8,
	GuildMessages = 9,
	GuildMessageReactions = 10,
	GuildMessageTyping = 11,
	DirectMessages = 12,
	DirectMessageReactions = 13,
	DirectMessageTyping = 14,
	MessageContent = 15,
	GuildScheduleEvents = 16,
	AutoModerationConfiguration = 20,
	AutoModerationExecution = 21,
	GuildMessagePolls = 24,
	DirectMessagePolls = 25,
})

export type Intent =
	"Guilds"
	| "GuildMembers"
	| "GuildModeration"
	| "GuildEmojisAndStickers"
	| "GuildIntegrations"
	| "GuildWebhooks"
	| "GuildInvites"
	| "GuildVoiceStates"
	| "GuildPresences"
	| "GuildMessages"
	| "GuildMessageReactions"
	| "GuildMessageTyping"
	| "DirectMessages"
	| "DirectMessageReactions"
	| "DirectMessageTyping"
	| "MessageContent"
	| "GuildScheduleEvents"
	| "AutoModerationConfiguration"
	| "AutoModerationExecution"
	| "GuildMessagePolls"
	| "DirectMessagePolls"

return Intents


--[=[
	@class Types.gateway.opcodes

	https://discord.com/developers/docs/topics/opcodes-and-status-codes#gateway-gateway-opcodes
]=]

local Opcodess = table.freeze({
	Dispatch = 0,
	Heartbeat = 1,
	Identify = 2,
	PresenceUpdate = 3,
	VoiceStateUpdate = 4,
	Resume = 6,
	Reconnect = 7,
	RequestGuildMembers = 8,
	InvalidSession = 9,
	Hello = 10,
	HeartbeatACK = 11,
})

export type Opcode =
	"Dispatch"
	| "Heartbeat"
	| "Identify"
	| "PresenceUpdate"
	| "VoiceStateUpdate"
	| "Resume"
	| "Reconnect"
	| "RequestGuildMembers"
	| "InvalidSession"
	| "Hello"
	| "HeartbeatACK"

return Opcodess


--[=[
	@class Types.gateway.receiveEvents

	https://discord.com/developers/docs/topics/gateway-events#receive-events

	these enums map to the `T` event name given in a Discord payload;
		https://discord.com/developers/docs/topics/gateway-events#payload-structure
]=]

local ReceiveEvents = table.freeze({
	Hello = "HELLO",
	Ready = "READY",
	Resumed = "RESUMED",
	Reconnect = "RECONNECT",
	InvalidSession = "INVALID_SESSION",
	ApplicationCommandPermissionsUpdate = "APPLICATION_COMMAND_PERMISSIONS_UPDATE",
	AutoModerationRuleCreate = "AUTO_MODERATION_RULE_CREATE",
	AutoModerationRuleUpdate = "AUTO_MODERATION_RULE_UPDATE",
	AutoModerationRuleDelete = "AUTO_MODERATION_RULE_DELETE",
	AutoModerationActionExecution = "AUTO_MODERATION_ACTION_EXECUTION",
	ChannelCreate = "CHANNEL_CREATE",
	ChannelUpdate = "CHANNEL_UPDATE",
	ChannelDelete = "CHANNEL_DELETE",
	ChannelPinsUpdate = "CHANNEL_PINS_UPDATE",
	ThreadCreate = "THREAD_CREATE",
	ThreadUpdate = "THREAD_UPDATE",
	ThreadDelete = "THREAD_DELETE",
	ThreadListSync = "THREAD_LIST_SYNC",
	ThreadMemberUpdate = "THREAD_MEMBER_UPDATE",
	ThreadMembersUpdate = "THREAD_MEMBERS_UPDATE",
	EntitlementCreate = "ENTITLEMENT_CREATE",
	EntitlementUpdate = "ENTITLEMENT_UPDATE",
	EntitlementDelete = "ENTITLEMENT_DELETE",
	GuildCreate = "GUILD_CREATE",
	GuildUpdate = "GUILD_UPDATE",
	GuildDelete = "GUILD_DELETE",
	GuildAuditLogEntryCreate = "GUILD_AUDIT_LOG_ENTRY_CREATE",
	GuildBanAdd = "GUILD_BAN_ADD",
	GuildBanRemove = "GUILD_BAN_REMOVE",
	GuildEmojisUpdate = "GUILD_EMOJIS_UPDATE",
	GuildStickersUpdate = "GUILD_STICKERS_UPDATE",
	GuildIntegrationsUpdate = "GUILD_INTEGRATIONS_UPDATE",
	GuildMemberAdd = "GUILD_MEMBER_ADD",
	GuildMemberRemove = "GUILD_MEMBER_REMOVE",
	GuildMemberUpdate = "GUILD_MEMBER_UPDATE",
	GuildMembersChunk = "GUILD_MEMBERS_CHUNK",
	GuildRoleCreate = "GUILD_ROLE_CREATE",
	GuildRoleDelete = "GUILD_ROLE_DELETE",
	GuildRoleUpdate = "GUILD_ROLE_UPDATE",
	GuildScheduledEventCreate = "GUILD_SCHEDULED_EVENT_CREATE",
	GuildScheduledEventUpdate = "GUILD_SCHEDULED_EVENT_UPDATE",
	GuildScheduledEventDelete = "GUILD_SCHEDULED_EVENT_DELETE",
	GuildScheduledEventUserAdd = "GUILD_SCHEDULED_EVENT_USER_ADD",
	GuildScheduledEventUserRemove = "GUILD_SCHEDULED_EVENT_USER_REMOVE",
	IntegrationCreate = "INTEGRATION_CREATE",
	IntegrationUpdate = "INTEGRATION_UPDATE",
	IntegrationDelete = "INTEGRATION_DELETE",
	InteractionCreate = "INTERACTION_CREATE",
	InviteCreate = "INVITE_CREATE",
	InviteDelete = "INVITE_DELETE",
	MessageCreate = "MESSAGE_CREATE",
	MessageUpdate = "MESSAGE_UPDATE",
	MessageDelete = "MESSAGE_DELETE",
	MessageDeleteBulk = "MESSAGE_DELETE_BULK",
	MessageReactionAdd = "MESSAGE_REACTION_ADD",
	MessageReactionRemove = "MESSAGE_REACTION_REMOVE",
	MessageReactionRemoveAll = "MESSAGE_REACTION_REMOVE_ALL",
	MessageReactionRemoveEmoji = "MESSAGE_REACTION_REMOVE_EMOJI",
	PresenceUpdate = "PRESENCE_UPDATE",
	StageInstanceCreate = "STAGE_INSTANCE_CREATE",
	StageInstanceUpdate = "STAGE_INSTANCE_UPDATE",
	StageInstanceDelete = "STAGE_INSTANCE_DELETE",
	TypingStart = "TYPING_START",
	UserUpdate = "USER_UPDATE",
	VoiceStateUpdate = "VOICE_STATE_UPDATE",
	VoiceServerUpdate = "VOICE_SERVER_UPDATE",
	WebhooksUpdate = "WEBHOOKS_UPDATE",
	MessagePollVoteAdd = "MESSAGE_POLL_VOTE_ADD",
	MessagePollVoteRemove = "MESSAGE_POLL_VOTE_REMOVE",
})

export type ReceiveEvent =
	"Hello"
	| "Ready"
	| "Resumed"
	| "Reconnect"
	| "InvalidSession"
	| "ApplicationCommandPermissionsUpdate"
	| "AutoModerationRuleCreate"
	| "AutoModerationRuleUpdate"
	| "AutoModerationRuleDelete"
	| "AutoModerationActionExecution"
	| "ChannelCreate"
	| "ChannelUpdate"
	| "ChannelDelete"
	| "ChannelPinsUpdate"
	| "ThreadCreate"
	| "ThreadUpdate"
	| "ThreadDelete"
	| "ThreadListSync"
	| "ThreadMemberUpdate"
	| "ThreadMembersUpdate"
	| "EntitlementCreate"
	| "EntitlementUpdate"
	| "EntitlementDelete"
	| "GuildCreate"
	| "GuildUpdate"
	| "GuildDelete"
	| "GuildAuditLogEntryCreate"
	| "GuildBanAdd"
	| "GuildBanRemove"
	| "GuildEmojisUpdate"
	| "GuildStickersUpdate"
	| "GuildIntegrationsUpdate"
	| "GuildMemberAdd"
	| "GuildMemberRemove"
	| "GuildMemberUpdate"
	| "GuildMembersChunk"
	| "GuildRoleCreate"
	| "GuildRoleDelete"
	| "GuildRoleUpdate"
	| "GuildScheduledEventCreate"
	| "GuildScheduledEventUpdate"
	| "GuildScheduledEventDelete"
	| "GuildScheduledEventUserAdd"
	| "GuildScheduledEventUserRemove"
	| "IntegrationCreate"
	| "IntegrationUpdate"
	| "IntegrationDelete"
	| "InteractionCreate"
	| "InviteCreate"
	| "InviteDelete"
	| "MessageCreate"
	| "MessageUpdate"
	| "MessageDelete"
	| "MessageDeleteBulk"
	| "MessageReactionAdd"
	| "MessageReactionRemove"
	| "MessageReactionRemoveAll"
	| "MessageReactionRemoveEmoji"
	| "PresenceUpdate"
	| "StageInstanceCreate"
	| "StageInstanceUpdate"
	| "StageInstanceDelete"
	| "TypingStart"
	| "UserUpdate"
	| "VoiceStateUpdate"
	| "VoiceServerUpdate"
	| "WebhooksUpdate"
	| "MessagePollVoteAdd"
	| "MessagePollVoteRemove"

return ReceiveEvents


--[=[
	@class Types.gateway.sendEvents

	https://discord.com/developers/docs/topics/gateway-events#send-events

	these enums map to the `opcodes` module under `@api-types/gateway/optcodes.luau`
]=]

local SendEvents = table.freeze({
	Identify = "Identify",
	Resume = "Resume",
	Heartbeat = "Heartbeat",
	RequestGuildMembers = "RequestGuildMembers",
	UpdateVoiceState = "UpdateVoiceState",
	UpdatePresence = "UpdatePresence",
})

export type SendEvent = "Identify" | "Resume" | "Heartbeat" | "RequestGuildMembers" | "UpdateVoiceState" | "UpdatePresence"

return SendEvents


--[=[
	@class Types.guild
]=]

local util = require("../luau_packages/util")

local VerificationLevel = table.freeze(util.tableReflect({
	None = 0,
	Low = 1,
	Medium = 2,
	High = 3,
	VeryHigh = 4,
}))

local DefaultMessageNotification = table.freeze(util.tableReflect({
	AllMessages = 0,
	OnlyMentions = 1,
}))

local ExplicitContentFilterLevel = table.freeze(util.tableReflect({
	Disabled = 0,
	MembersWithoutRoles = 1,
	AllMembers = 2,
}))

local NSFWLevel = table.freeze(util.tableReflect({
	Default = 0,
	Explicit = 1,
	Safe = 2,
	AgeRestricted = 3,
}))

local PremiumTier = table.freeze(util.tableReflect({
	None = 0,
	Tier1 = 1,
	Tier2 = 2,
	Tier3 = 3,
}))

local MFALevel = table.freeze(util.tableReflect({
	None = 0,
	Elevated = 1,
}))

local InviteType = table.freeze(util.tableReflect({
	Guild = 0,
	GroupDM = 1,
	Friend = 2,
}))

local InviteTargetType = table.freeze(util.tableReflect({
	Stream = 1,
	EmbeddedApplication = 2,
}))

local ScheduledEventPrivacyLevel = table.freeze(util.tableReflect({
	GuildOnly = 2,
}))

local SchedledEventEntityType = table.freeze(util.tableReflect({
	StageInstance = 1,
	Voice = 2,
	External = 3,
}))

local SchedledEventStatus = table.freeze(util.tableReflect({
	Scheduled = 1,
	Active = 2,
	Completed = 3,
	Cancelled = 4,
}))

local IntegrationType = table.freeze(util.tableReflect({
	Twitch = "twitch",
	YouTube = "youtube",
	Discord = "discord",
	GuildSubscription = "guild_subscription",
}))

local IntegrationExpireBehaviour = table.freeze(util.tableReflect({
	RemoveRole = 0,
	Kick = 1,
}))

local OnboardingPromptType = table.freeze(util.tableReflect({
	MultipleChoice = "MULTIPLE_CHOICE",
	Dropdown = "DROPDOWN",
}))

local OnboardingMode = table.freeze(util.tableReflect({
	Default = "ONBOARDING_DEFAULT",
	Advanced = "ONBOARDING_ADVANCED",
}))

local OverwriteType = table.freeze(util.tableReflect({
	Role = 0,
	Member = 1,
}))

local TriggerTypes = table.freeze(util.tableReflect({
	Keyword = 1,
	Spam = 3,
	KeywordPreset = 4,
	MentionSpam = 5,
	MemberProfile = 6,
}))

local KeywordPresetType = table.freeze(util.tableReflect({
	Profanity = 1,
	SexualContent = 2,
	Slurs = 3,
}))

local EventTypes = table.freeze(util.tableReflect({
	MessageSend = 1,
	MemberUpdate = 2,
}))

local ActionTypes = table.freeze(util.tableReflect({
	BlockMessage = 1,
	SendAlertMessage = 2,
	Timeout = 3,
	BlockMemberInteraction = 4,
}))

local AuditLogEvent = table.freeze(util.tableReflect({
	GuildUpdate = 1,
	ChannelCreate = 10,
	ChannelUpdate = 11,
	ChannelDelete = 12,
	ChannelOverwriteCreate = 13,
	ChannelOverwriteUpdate = 14,
	ChannelOverwriteDelete = 15,
	MemberKick = 20,
	MemberPrune = 21,
	MemberBanAdd = 22,
	MemberBanRemove = 23,
	MemberUpdate = 24,
	MemberRoleUpdate = 25,
	MemberMove = 26,
	MemberDisconnect = 27,
	BotAdd = 28,
	RoleCreate = 30,
	RoleUpdate = 31,
	RoleDelete = 32,
	InviteCreate = 40,
	InviteUpdate = 41,
	InviteDelete = 42,
	WebhookCreate = 50,
	WebhookUpdate = 51,
	WebhookDelete = 52,
	EmojiCreate = 60,
	EmojiUpdate = 61,
	EmojiDelete = 62,
	MessageDelete = 72,
	MessageBulkDelete = 73,
	MessagePin = 74,
	MessageUnpin = 75,
	IntegrationCreate = 80,
	IntegrationUpdate = 81,
	IntegrationDelete = 82,
	StageInstanceCreate = 83,
	StageInstanceUpdate = 84,
	StageInstanceDelete = 85,
	StickerCreate = 90,
	StickerUpdate = 91,
	StickerDelete = 92,
	GuildScheduledEventCreate = 100,
	GuildScheduledEventUpdate = 101,
	GuildScheduledEventDelete = 102,
	ThreadCreate = 110,
	ThreadUpdate = 111,
	ThreadDelete = 112,
	ApplicationCommandPermissionUpdate = 121,
	SoundboardSoundCreate = 130,
	SoundboardSoundUpdate = 131,
	SoundboardSoundDelete = 132,
	AutoModerationRuleCreate = 140,
	AutoModerationRuleUpdate = 141,
	AutoModerationRuleDelete = 142,
	AutoModerationBlockMessage = 143,
	AutoModerationFlagToChannel = 144,
	AutoModerationUserCommunicationDisabled = 145,
	CreatorMonetizationRequestCreated = 150,
	CreatorMonetizationTermsAccepted = 151,
	OnboardingPromptCreate = 163,
	OnboardingPromptUpdate = 164,
	OnboardingPromptDelete = 165,
	OnboardingCreate = 166,
	OnboardingUpdate = 167,
	HomeSettingsCreate = 190,
	HomeSettingsUpdate = 191,
}))

export type ActionTypes = "BlockMessage" | "SendAlertMessage" | "Timeout" | "BlockMemberInteraction"
export type EventTypes = "MessageSend" | "MemberUpdate"
export type KeywordPresetType = "Profanity" | "SexualContent" | "Slurs"
export type TriggerTypes = "Keyword" | "Spam" | "KeywordPreset" | "MentionSpam" | "MemberProfile"
export type VerificationLevel = "None" | "Low" | "Medium" | "High" | "VeryHigh"
export type DefaultMessageNotification = "AllMessages" | "OnlyMentions"
export type ExplicitContentFilterLevel = "Disabled" | "MembersWithoutRoles" | "AllMembers"
export type NSFWLevel = "Default" | "Explicit" | "Safe" | "AgeRestricted"
export type PremiumTier = "None" | "Tier1" | "Tier2" | "Tier3"
export type MFALevel = "None" | "Elevated"
export type InviteType = "Guild" | "GroupDM" | "Friend"
export type InviteTargetType = "Stream" | "EmbeddedApplication"
export type ScheduledEventPrivacyLevel = "GuildOnly"
export type SchedledEventEntityType = "StageInstance" | "Voice" | "External"
export type SchedledEventStatus = "Scheduled" | "Active" | "Completed" | "Canceled"
export type IntegrationType = "Twitch" | "YouTube" | "Discord" | "GuildSubscription"
export type IntegrationExpireBehaviour = "RemoveRole" | "Kick"
export type OnboardingPromptType = "MultipleChoice" | "Dropdown"
export type OnboardingMode = "Default" | "Advanced"
export type OverwriteType = "Role" | "Member"
export type AuditLogEvent =
	"GuildUpdate"
	| "ChannelCreate"
	| "ChannelUpdate"
	| "ChannelDelete"
	| "MessageCreate"
	| "MessageUpdate"
	| "MessageDelete"
	| "ReactionAdd"
	| "ReactionRemove"
	| "PresenceUpdate"
	| "VoiceStateUpdate"
	| "TypingStart"

return {
	AuditLogEvent = AuditLogEvent,
	TriggerTypes = TriggerTypes,
	KeywordPresetType = KeywordPresetType,
	EventTypes = EventTypes,
	ActionTypes = ActionTypes,
	VerificationLevel = VerificationLevel,
	DefaultMessageNotification = DefaultMessageNotification,
	ExplicitContentFilterLevel = ExplicitContentFilterLevel,
	NSFWLevel = NSFWLevel,
	PremiumTier = PremiumTier,
	MFALevel = MFALevel,
	InviteType = InviteType,
	InviteTargetType = InviteTargetType,
	ScheduledEventPrivacyLevel = ScheduledEventPrivacyLevel,
	SchedledEventEntityType = SchedledEventEntityType,
	SchedledEventStatus = SchedledEventStatus,
	IntegrationType = IntegrationType,
	IntegrationExpireBehaviour = IntegrationExpireBehaviour,
	OnboardingPromptType = OnboardingPromptType,
	OnboardingMode = OnboardingMode,
	OverwriteType = OverwriteType,
}


--[=[
	@class Types.interaction
]=]

local util = require("../luau_packages/util")

local InteractionType = table.freeze(util.tableReflect({
	Ping = 1,
	ApplicationCommand = 2,
	MessageComponent = 3,
	ApplicationCommandAutocomplete = 4,
	ModalSubmit = 5,
}))

local ApplicationCommandOptionType = table.freeze(util.tableReflect({
	SubCommand = 1,
	SubCommandGroup = 2,
	String = 3,
	Integer = 4,
	Boolean = 5,
	User = 6,
	Channel = 7,
	Role = 8,
	Mentionable = 9,
	Number = 10,
	Attachment = 11,
}))

local InteractionContextType = table.freeze(util.tableReflect({
	Guild = 0,
	BotDm = 1,
	PrivateChannel = 2,
}))

local ApplicationCommandType = table.freeze(util.tableReflect({
	ChatInput = 1,
	User = 2,
	Message = 3,
	PrimaryEntryPoiant = 4,
}))

local ComponentTypes = table.freeze(util.tableReflect({
	ActionRow = 1,
	Button = 2,
	StringSelect = 3,
	TextInput = 4,
	UserSelect = 5,
	RoleSelect = 6,
	MentionableSelect = 7,
	ChannelSelect = 8,
}))

export type InteractionType =
	"Ping"
	| "ApplicationCommand"
	| "MessageComponent"
	| "ApplicationCommandAutocomplete"
	| "ModalSubmit"

export type ComponentTypes =
	"ActionRow"
	| "Button"
	| "StringSelect"
	| "TextInput"
	| "UserSelect"
	| "RoleSelect"
	| "MentionableSelect"
	| "ChannelSelect"

export type ApplicationCommandOptionType =
	"SubCommand"
	| "SubCommandGroup"
	| "String"
	| "Integer"
	| "Boolean"
	| "User"
	| "Channel"
	| "Role"
	| "Mentionable"
	| "Number"
	| "Attachment"

export type ApplicationCommandType = "ChatInput" | "User" | "Message" | "PrimaryEntryPoiant"

export type InteractionContextType = "Guild" | "BotDm" | "PrivateChannel"

return {
	InteractionType = InteractionType,
	ApplicationCommandOptionType = ApplicationCommandOptionType,
	InteractionContextType = InteractionContextType,
	ApplicationCommandType = ApplicationCommandType,
	ComponentTypes = ComponentTypes,
}


--[=[
	@class Types.permission
]=]

local util = require("../luau_packages/util")

local bit = require("../luau_packages/bit")

local Permissions = table.freeze(util.tableReflect({
	CreateInstantInvite = bit.lshift(1, 0),
	KickMembers = bit.lshift(1, 1),
	BanMembers = bit.lshift(1, 2),
	Administrator = bit.lshift(1, 3),
	ManageChannels = bit.lshift(1, 4),
	ManageGuild = bit.lshift(1, 5),
	AddReactions = bit.lshift(1, 6),
	ViewAuditLog = bit.lshift(1, 7),
	PrioritySpeaker = bit.lshift(1, 8),
	Stream = bit.lshift(1, 9),
	ViewChannel = bit.lshift(1, 10),
	SendMessages = bit.lshift(1, 11),
	SendTTSMessages = bit.lshift(1, 12),
	ManageMessages = bit.lshift(1, 13),
	EmbedLinks = bit.lshift(1, 14),
	AttachFiles = bit.lshift(1, 15),
	ReadMessageHistory = bit.lshift(1, 16),
	MentionEveryone = bit.lshift(1, 17),
	UseExternalEmojis = bit.lshift(1, 18),
	ViewGuildInsights = bit.lshift(1, 19),
	Connect = bit.lshift(1, 20),
	Speak = bit.lshift(1, 21),
	MuteMembers = bit.lshift(1, 22),
	DeafenMembers = bit.lshift(1, 23),
	MoveMembers = bit.lshift(1, 24),
	UseVAD = bit.lshift(1, 25),
	ChangeNickname = bit.lshift(1, 26),
	ManageNicknames = bit.lshift(1, 27),
	ManageRoles = bit.lshift(1, 28),
	ManageWebhooks = bit.lshift(1, 29),
	ManageGuildExpressions = bit.lshift(1, 30),
	UseApplicationCommands = bit.lshift(1, 31),
	RequestToSpeak = bit.lshift(1, 32),
	ManageEvents = bit.lshift(1, 33),
	ManageThreads = bit.lshift(1, 34),
	CreatePublicThreads = bit.lshift(1, 35),
	CreatePrivateThreads = bit.lshift(1, 36),
	UseExternalStickers = bit.lshift(1, 37),
	SendMessagesInThreads = bit.lshift(1, 38),
	UseEmbeddedActivities = bit.lshift(1, 39),
	ModerateMembers = bit.lshift(1, 40),
	ViewCreatorMonetizationAnalytics = bit.lshift(1, 41),
	UseSoundboard = bit.lshift(1, 42),
	UseExternalSounds = bit.lshift(1, 45),
	SendVoiceMessages = bit.lshift(1, 46),
	SendPolls = bit.lshift(1, 49),
	UseExternalApps = bit.lshift(1, 50),
}))

export type Permissions =
	"CreateInstantInvite"
	| "KickMembers"
	| "BanMembers"
	| "Administrator"
	| "ManageChannels"
	| "ManageGuild"
	| "AddReactions"
	| "ViewAuditLog"
	| "PrioritySpeaker"
	| "Stream"
	| "ViewChannel"
	| "SendMessages"
	| "SendTtsMessages"
	| "ManageMessages"
	| "EmbedLinks"
	| "AttachFiles"
	| "ReadMessageHistory"
	| "MentionEveryone"
	| "UseExternalEmojis"
	| "ViewGuildInsights"
	| "Connect"
	| "Speak"
	| "MuteMembers"
	| "DeafenMembers"
	| "MoveMembers"
	| "UseVad"
	| "ChangeNickname"
	| "ManageNicknames"
	| "ManageRoles"
	| "ManageWebhooks"
	| "ManageEmojisAndStickers"
	| "UseApplicationCommands"
	| "RequestToSpeak"
	| "ManageEvents"
	| "ManageThreads"
	| "CreatePublicThreads"
	| "CreatePrivateThreads"
	| "UseExternalStickers"
	| "SendMessagesInThreads"
	| "UseEmbeddedActivities"
	| "ModerateMembers"
	| "ViewCreatorMonetizationAnalytics"
	| "UseSoundboard"
	| "CreateGuildExpressions"
	| "CreateEvents"
	| "UseExternalSounds"
	| "SendVoiceMessages"
	| "SendPolls"
	| "UseExternalApps"

return {
	Permissions = Permissions,
}


--[=[
	@class Types.poll
]=]

export type PollLayoutType = "Default"

return "DISCORD_V10_APPLICATION_TYPES"


--[=[
	@class Types.sticker
]=]

local util = require("../luau_packages/util")

local StickerTypes = table.freeze(util.tableReflect({
	Standard = 1,
	Guild = 2,
}))

local StickerFormatTypes = table.freeze(util.tableReflect({
	PNG = 1,
	APNG = 2,
	LOTTIE = 3,
	GIF = 4,
}))

export type StickerType = "Standard" | "Guild"
export type StickerFormatType = "PNG" | "APNG" | "LOTTIE" | "GIF" | "Unknown"

return {
	StickerTypes = StickerTypes,
	StickerFormatTypes = StickerFormatTypes,
}


--[=[
	@class Types.user
]=]

local util = require("../luau_packages/util")

local NitroType = table.freeze(util.tableReflect({
	None = 0,
	NitroClassic = 1,
	Nitro = 2,
	NitroBasic = 3,
}))

export type NitroType = "None" | "NitroClassic" | "Nitro" | "NitroBasic"

return {
	NitroType = NitroType,
}


--[=[
	@class Types.webhook
]=]

local util = require("../luau_packages/util")

local WebhookType = table.freeze(util.tableReflect({
	Incoming = 1,
	ChannelFollower = 2,
	Application = 3,
}))

export type WebhookType = "Incoming" | "ChannelFollower" | "Application"

return {
	WebhookType = WebhookType,
}


--[[
	An optimized 64-bit library to provide 64-bit binary operations in Luau
	using 32-bit operations and `bit32` for improved performance.
]]
local Bit64 = {}

--[[
	Splits a 64-bit integer into high and low 32-bit integers
]]
local function split64(value: number): (number, number)
	local low = bit32.band(value, 0xFFFFFFFF)
	local high = math.floor(value / 4294967296)

	return low, high
end

--[[
	Combines high and low 32-bit integers into a 64-bit integer
]]
local function combine64(low: number, high: number): number
	return (high * 4294967296) + low
end

--[[
	Binary NOT operation
]]
function Bit64.bnot(value: number): number
	local low, high = split64(value)
	local newLow = bit32.bnot(low)
	local newHigh = bit32.bnot(high)

	return combine64(newLow, newHigh)
end

--[[
	Binary OR operation
]]
function Bit64.bor(value0: number, value1: number): number
	local low0, high0 = split64(value0)
	local low1, high1 = split64(value1)
	local newLow = bit32.bor(low0, low1)
	local newHigh = bit32.bor(high0, high1)

	return combine64(newLow, newHigh)
end

--[[
	Binary AND operation
]]
function Bit64.band(value0: number, value1: number): number
	local low0, high0 = split64(value0)
	local low1, high1 = split64(value1)
	local newLow = bit32.band(low0, low1)
	local newHigh = bit32.band(high0, high1)

	return combine64(newLow, newHigh)
end

--[[
	Binary SHIFT operation
]]
function Bit64.shift(value: number, displacement: number): number
	if displacement == 0 then
		return value
	end

	local low, high = split64(value)

	if displacement > 0 then
		-- Left shift
		if displacement >= 32 then
			high = bit32.lshift(low, displacement - 32)
			low = 0
		else
			high = bit32.bor(bit32.lshift(high, displacement), bit32.rshift(low, 32 - displacement))
			low = bit32.lshift(low, displacement)
		end
	else
		-- Right shift
		displacement = -displacement
		if displacement >= 32 then
			low = bit32.rshift(high, displacement - 32)
			high = 0
		else
			low = bit32.bor(bit32.rshift(low, displacement), bit32.lshift(high, 32 - displacement))
			high = bit32.rshift(high, displacement)
		end
	end

	low = bit32.band(low, 0xFFFFFFFF)
	high = bit32.band(high, 0xFFFFFFFF)

	return combine64(low, high)
end

return Bit64


--[=[
	@class Bit

	A Bit wrapper library that supports both 64-bit integers and 32-bit integers.
	Primarily required in the calculation for snowflake IDs that the Discord API will give us.
]=]

local Bit64 = require("@self/bit64")
local Bit = {}

--[=[
	@within Bit

	Returns true if the given value is a 32-bit integer.
]=]
function Bit.is32Bit(value: number): boolean
	return value >= 0 and value <= 0xFFFFFFFF
end

--[=[
	@within Bit

	Binary NOT operation for integers
]=]
function Bit.bnot(value: number): number
	if Bit.is32Bit(value) then
		return bit32.bnot(value)
	else
		return Bit64.bnot(value)
	end
end

--[=[
	@within Bit

	Binary OR operation for integers
]=]
function Bit.bor(value0: number, value1: number): number
	if Bit.is32Bit(value0) and Bit.is32Bit(value1) then
		return bit32.bor(value0, value1)
	else
		return Bit64.bor(value0, value1)
	end
end

--[=[
	@within Bit

	Binary AND operation for integers
]=]
function Bit.band(value0: number, value1: number): number
	if Bit.is32Bit(value0) and Bit.is32Bit(value1) then
		return bit32.band(value0, value1)
	else
		return Bit64.band(value0, value1)
	end
end

--[=[
	@within Bit

	Binary SHIFT operation for integers
]=]
function Bit.shift(value: number, displacement: number): number
	if Bit.is32Bit(value) then
		if displacement >= 0 then
			return bit32.lshift(value, displacement)
		else
			return bit32.rshift(value, -displacement)
		end
	else
		return Bit64.shift(value, displacement)
	end
end

--[=[
	@within Bit

	Binary LEFT SHIFT operation for integers
]=]
function Bit.lshift(value: number, displacement: number): number
	return Bit.shift(value, displacement)
end

--[=[
	@within Bit

	Binary RIGHT SHIFT operation for integers
]=]
function Bit.rshift(value: number, displacement: number): number
	return Bit.shift(value, -displacement)
end

return Bit


--[=[
	@class Builders.Activity
	@__index Prototype

	Implementation of Discord Activity as a Luau builder.

	https://discord.com/developers/docs/events/gateway-events#activity-object

	Note: Only YouTube and Twitch URLs are allowed for streaming.
]=]

local apiTypes = require("../luau_packages/api_types")

local Activity = {}

Activity.Prototype = {}
Activity.Interface = {}

--[=[
	@within Builders.Activity

	Sets the name of the activity.
]=]
function Activity.Prototype.setActivityName(self: Activity, name: string): Activity
	self.name = name

	return self
end

--[=[
	@within Builders.Activity

	Sets the type of the activity.
]=]
function Activity.Prototype.setActivityType(self: Activity, activityType: apiTypes.ActivityType): Activity
	assert(
		apiTypes.client.ActivityType[activityType],
		`Expected ActivityType to be one of Game, Streaming, Listening, Watching or Competing`
	)

	self.activityType = activityType

	return self
end

--[=[
	@within Builders.Activity

	Sets the streaming URL of the activity. Only YouTube and Twitch URLs are allowed.
]=]
function Activity.Prototype.setStreamingURL(self: Activity, streamURL: string): Activity
	local hasYouTube = string.find(streamURL, "youtube.com") ~= nil
	local hasTwitch = string.find(streamURL, "twitch.tv") ~= nil

	assert(hasYouTube or hasTwitch, `Expected either Twitch or YouTube for streaming URLs!`)

	self.streamingURL = streamURL

	return self
end

--[=[
	@within Builders.Activity

	Builds the activity JSON payload that can be parsed by the Discord API.
]=]
function Activity.Prototype.build(self: Activity): JSON
	assert(self.activityType ~= nil, `Activity requires ActivityType`)
	assert(self.name ~= nil, `Activity requires Name`)

	return {
		name = self.name,
		url = self.streamingURL,
		type = apiTypes.client.ActivityType[self.activityType],
	}
end

--[=[
	@within Builders.Activity

	Creates a new Activity.

	```luau
	local activity = Activity.new({
		name = "Playing a game",
		activityType = "Game",
		streamingURL = "https://twitch.tv/example"
	})
	```
]=]
function Activity.Interface.new(resource: {
	name: string?,
	activityType: apiTypes.ActivityType?,
	streamingURL: string?,
}?): Activity
	local self = setmetatable(
		{
			--[=[
				@prop name string
				@within Builders.Activity
				The name of the activity that is displayed.
			]=]
			name = nil,

			--[=[
				@prop activityType api_types.ActivityType
				@within Builders.Activity
				The type of activity being performed.
			]=]
			activityType = nil,

			--[=[
				@prop streamingURL string?
				@within Builders.Activity
				The URL of the stream, only YouTube and Twitch URLs are supported.
			]=]
			streamingURL = nil,
		} :: Activity,
		{ __index = Activity.Prototype }
	)

	if resource then
		if resource.name then
			self:setActivityName(resource.name)
		end
		if resource.activityType then
			self:setActivityType(resource.activityType)
		end
		if resource.streamingURL then
			self:setStreamingURL(resource.streamingURL)
		end
	end

	return self
end

export type Activity = typeof(Activity.Prototype) & {
	name: string,
	activityType: apiTypes.ActivityType,
	streamingURL: string?,
}

export type JSON = typeof(Activity.Prototype.build(nil :: any))

return Activity.Interface


--[=[
	@class Builders.AutomoderationRule
	@__index Prototype

	Implementation of a discord automoderation rule object as a luau builder.

	https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-rule-object
]=]

local apiTypes = require("../../luau_packages/api_types")
local triggerMetadata = require("./trigger/metadata")
local action = require("./action/action")

local AutomoderationRule = {}

AutomoderationRule.Prototype = {}
AutomoderationRule.Interface = {}

--[=[
	@within Builders.AutomoderationRule

	Sets the name of the automoderation rule.
]=]
function AutomoderationRule.Prototype.setName(self: AutomoderationRule, name: string): AutomoderationRule
	self.name = name

	return self
end

--[=[
	@within Builders.AutomoderationRule

	Sets the event type for the automoderation rule.
]=]
function AutomoderationRule.Prototype.setEventType(
	self: AutomoderationRule,
	eventType: apiTypes.EventTypes
): AutomoderationRule
	self.eventType = eventType

	return self
end

--[=[
	@within Builders.AutomoderationRule

	Sets the trigger type for the automoderation rule.
]=]
function AutomoderationRule.Prototype.setTriggerType(
	self: AutomoderationRule,
	triggerType: apiTypes.TriggerTypes
): AutomoderationRule
	self.triggerType = triggerType

	return self
end

--[=[
	@within Builders.AutomoderationRule

	Sets the trigger metadata for the automoderation rule.
]=]
function AutomoderationRule.Prototype.setTriggerMetadata(
	self: AutomoderationRule,
	triggerMetadata: triggerMetadata.JSON
): AutomoderationRule
	self.triggerMetadata = triggerMetadata

	return self
end

--[=[
	@within Builders.AutomoderationRule

	Sets the actions for the automoderation rule.
]=]
function AutomoderationRule.Prototype.setActions(self: AutomoderationRule, actions: { action.JSON }): AutomoderationRule
	self.actions = actions

	return self
end

--[=[
	@within Builders.AutomoderationRule

	Sets whether the automoderation rule is enabled.
]=]
function AutomoderationRule.Prototype.setEnabled(self: AutomoderationRule, enabled: boolean): AutomoderationRule
	self.enabled = enabled

	return self
end

--[=[
	@within Builders.AutomoderationRule

	Sets the exempt roles for the automoderation rule.
]=]
function AutomoderationRule.Prototype.setExemptRoles(
	self: AutomoderationRule,
	exemptRoles: { apiTypes.Snowflake }
): AutomoderationRule
	self.exemptRoles = exemptRoles

	return self
end

--[=[
	@within Builders.AutomoderationRule

	Sets the exempt channels for the automoderation rule.
]=]
function AutomoderationRule.Prototype.setExemptChannels(
	self: AutomoderationRule,
	exemptChannels: { apiTypes.Snowflake }
): AutomoderationRule
	self.exemptChannels = exemptChannels

	return self
end

--[=[
	@within Builders.AutomoderationRule

	Builds the automoderation rule object.
]=]
function AutomoderationRule.Prototype.build(self: AutomoderationRule): JSON
	return {
		name = self.name,
		event_type = apiTypes.guild.EventTypes[self.eventType],
		trigger_type = apiTypes.guild.TriggerTypes[self.triggerType],
		trigger_metadata = self.triggerMetadata,
		actions = self.actions,
		enabled = self.enabled,
		exempt_roles = self.exemptRoles,
		exempt_channels = self.exemptChannels,
	}
end

--[=[
	@within Builders.AutomoderationRule

	Constructs a new automoderation rule builder.

	```luau
	local rule = AutomoderationRule.new({
		name = "My Rule",
		eventType = "MESSAGE_SEND",
		triggerType = "KEYWORD",
		actions = {
			{
				type = "BLOCK_MESSAGE",
				metadata = {}
			}
		}
	})
	```
]=]
function AutomoderationRule.Interface.new(resource: {
	name: string?,
	eventType: apiTypes.EventTypes?,
	triggerType: apiTypes.TriggerTypes?,
	triggerMetadata: triggerMetadata.JSON?,
	actions: { action.JSON }?,
	enabled: boolean?,
	exemptRoles: { apiTypes.Snowflake }?,
	exemptChannels: { apiTypes.Snowflake }?,
}?): AutomoderationRule
	local self = setmetatable(
		{
			--[=[
				@prop name string
				@within Builders.AutomoderationRule
				The name of the automoderation rule.
			]=]
			name = nil,

			--[=[
				@prop eventType Builders.EventTypes
				@within Builders.AutomoderationRule
				The event type that will trigger the rule.
			]=]
			eventType = nil,

			--[=[
				@prop triggerType Builders.TriggerTypes
				@within Builders.AutomoderationRule
				The type of content which can trigger the rule.
			]=]
			triggerType = nil,

			--[=[
				@prop triggerMetadata Builders.TriggerMetadata
				@within Builders.AutomoderationRule
				The trigger metadata for the rule.
			]=]
			triggerMetadata = nil,

			--[=[
				@prop actions {Builders.Action}
				@within Builders.AutomoderationRule
				The actions which will execute when the rule is triggered.
			]=]
			actions = nil,

			--[=[
				@prop enabled boolean
				@within Builders.AutomoderationRule
				Whether the rule is enabled.
			]=]
			enabled = nil,

			--[=[
				@prop exemptRoles {string}
				@within Builders.AutomoderationRule
				The role ids that should not be affected by the rule.
			]=]
			exemptRoles = nil,

			--[=[
				@prop exemptChannels {string}
				@within Builders.AutomoderationRule
				The channel ids that should not be affected by the rule.
			]=]
			exemptChannels = nil,
		} :: AutomoderationRule,
		{
			__index = AutomoderationRule.Prototype,
		}
	)

	if resource then
		if resource.name then
			self:setName(resource.name)
		end

		if resource.eventType then
			self:setEventType(resource.eventType)
		end

		if resource.triggerType then
			self:setTriggerType(resource.triggerType)
		end

		if resource.triggerMetadata then
			self:setTriggerMetadata(resource.triggerMetadata)
		end

		if resource.actions then
			self:setActions(resource.actions)
		end

		if resource.enabled then
			self:setEnabled(resource.enabled)
		end

		if resource.exemptRoles then
			self:setExemptRoles(resource.exemptRoles)
		end

		if resource.exemptChannels then
			self:setExemptChannels(resource.exemptChannels)
		end
	end

	return self
end

export type AutomoderationRule = typeof(AutomoderationRule.Prototype) & {
	name: string,
	eventType: apiTypes.EventTypes,
	triggerType: apiTypes.TriggerTypes,
	triggerMetadata: triggerMetadata.JSON?,
	actions: { action.JSON },
	enabled: boolean?,
	exemptRoles: { apiTypes.Snowflake }?,
	exemptChannels: { apiTypes.Snowflake }?,
}

export type JSON = typeof(AutomoderationRule.Prototype.build(nil :: any))

return AutomoderationRule.Interface