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

local Class = {}
Class.__index = Class
setmetatable(Class, BaseClass)

local resizeReference = {
	{ -1, 1, 0, 0, 48 }, -- left
	{ 1, 0, 0, 0, 48 }, -- right
	{ 0, 0, -1, 1, 0 }, -- top
	{ 0, 0, 1, 0, 0 }, -- bottom
	{ -1, 1, -1, 1, 72 }, -- top left
	{ 1, 0, -1, 1, 24 }, -- top right
	{ 1, 0, 1, 0, 72 }, -- bottom right
	{ -1, 1, 1, 0, 24 }, -- bottom left
}

-- TODO: account for anchorpoint...
local function getResizeIndex(point, size, position, buffer: number): number?
	local max = position + size
	-- not inside box buffer
	if
		point.X < position.X - buffer
		or point.X > max.X + buffer
		or point.Y < position.Y - buffer
		or point.Y > max.Y + buffer
	then
		return
	end

	local edges = {
		math.abs(point.X - position.X), -- left
		math.abs(point.X - max.X), -- right
		math.abs(point.Y - position.Y), -- top
		math.abs(point.Y - max.Y), -- bottom
	}
	-- PERF: can't be close to corner if not close to edge
	if math.min(unpack(edges)) > buffer then
		return
	end

	local corners = {
		point - position, -- top left
		point - Vector2.new(max.X, position.Y), -- top right
		point - max, -- bottom right
		point - Vector2.new(position.X, max.Y), -- bottom left
	}

	for i, corner in corners do
		if math.abs(corner.X) <= buffer and math.abs(corner.Y) <= buffer then
			return resizeReference[i + 4] -- THE CURRENT CORNER
		end
	end

	for i, distance in edges do
		if distance <= buffer then
			return resizeReference[i]
		end
	end

	return
end

local windowCache = {}

local function sortZIndex(a, b)
	return a.ZIndex < b.ZIndex
end

function Class.bringToFront(self)
	local windows = {}
	for window in windowCache do
		table.insert(windows, window._instance)
	end
	table.sort(windows, sortZIndex)
	table.insert(windows, self._instance)
	for index, window in windows do
		window.ZIndex = index
	end
end

function Class.nudge(self)
	local nudgeAmount = 32
	local maxAttempts = 16
	local position = self._instance.AbsolutePosition
	local size = self._instance.AbsoluteSize
	local parentSize = self._instance.Parent.AbsoluteSize

	for i = 1, maxAttempts do
		local nudged
		for window in windowCache do
			if not window._instance.Visible or window == self then
				continue
			end
			if (position - window._instance.AbsolutePosition).Magnitude < 16 then
				local newX = math.clamp(position.X + nudgeAmount, 0, math.max(0, parentSize.X - size.X))
				local newY = math.clamp(position.Y + nudgeAmount, 0, math.max(0, parentSize.Y - size.Y))

				position += Vector2.new(nudgeAmount, nudgeAmount)
				self.Position(UDim2.fromOffset(newX, newY))
				nudged = true
				break
			end
		end
		if not nudged then
			break
		end
	end
end

local DEFAULT = {
	Exitable = true,
	Draggable = true,
	Resizable = true,
	RemoveOnClose = false,
	Close = UI.Function,
	Icon = "",
	IconProperties = {},
	Title = "Window",
	AutomaticSize = Enum.AutomaticSize.None,
	Position = UDim2.new(0.5, -210, 0.5, -210),
	Size = UDim2.new(0, 420, 0, 420),
	SizeBounds = Rect.new(128, 128, 9e9, 9e9),
}

type Properties = typeof(DEFAULT) & Frame

function Class.new(properties: Properties?)
	local self = table.clone(DEFAULT)
	self.IconProperties = {}
	UI.makeStatefulDefaults(self, properties)

	windowCache[self] = true

	local dragInput, dragStart, startOffset
	local inputPosition = UI.state(Vector2.new())
	local dragging = UI.state(false)
	local resizing = UI.state(false)
	local resizeHover = UI.state(false)
	local resizeIconRectOffset = UI.state(Vector2.new())

	local function dragUpdate(input)
		if not dragInput or resizing._value then
			return
		end

		local delta = input.Position - dragStart
		local maxPosition = self._instance.Parent.AbsoluteSize - self._instance.AbsoluteSize

		self.Position(
			UDim2.fromOffset(
				math.clamp(math.round(startOffset.X + delta.X), 0, math.max(0, maxPosition.X)),
				math.clamp(math.round(startOffset.Y + delta.Y), 0, math.max(0, maxPosition.Y))
			),
			true
		)
	end

	local function dragInputBegan(input, gameProcessed)
		if gameProcessed or not self.Draggable._value then
			return
		end
		if
			(input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch)
			and not resizing._value
			and not resizeHover._value
		then
			Class.bringToFront(self)

			startOffset = self._instance.AbsolutePosition - self._instance.Parent.AbsolutePosition
			dragInput, dragStart = input, input.Position
			dragging(true)
			local con
			con = input:GetPropertyChangedSignal("UserInputState"):Connect(function()
				if input.UserInputState == Enum.UserInputState.End then
					con:Disconnect()
					if dragInput == input then
						dragInput = nil
						dragging(false)
					end
				end
			end)
		end
	end

	local resizeBuffer = 5
	local startSize, xSize, xPos, ySize, yPos
	local function resizeUpdate(input)
		local parentSize = self._instance.Parent.AbsoluteSize
		local delta = Vector3.new(
			math.clamp(input.Position.X, 0, parentSize.X),
			math.clamp(input.Position.Y, 0, parentSize.Y),
			0
		) - dragStart
		local resizeMin = self.SizeBounds._value.Min - startSize
		local resizeMax = self.SizeBounds._value.Max - startSize
		inputPosition(input.Position)
		self.Size(
			UDim2.fromOffset(
				math.round(startSize.X + math.clamp(xSize * delta.X, resizeMin.X, math.max(resizeMin.X, resizeMax.X))),
				math.round(startSize.Y + math.clamp(ySize * delta.Y, resizeMin.Y, math.max(resizeMin.Y, resizeMax.Y)))
			)
		)

		self.Position(
			UDim2.fromOffset(
				math.round(startOffset.X - math.clamp(xPos * -delta.X, resizeMin.X, math.max(resizeMin.X, resizeMax.X))),
				math.round(startOffset.Y - math.clamp(yPos * -delta.Y, resizeMin.Y, math.max(resizeMin.Y, resizeMax.Y)))
			)
		)
	end

	local function resizeBegan(input, gameProcessed)
		if gameProcessed or not self.Resizable._value then
			return
		end
		if
			(input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch)
			and self._instance.Visible
		then
			local resize = getResizeIndex(
				Vector2.new(input.Position.X, input.Position.Y),
				self._instance.AbsoluteSize,
				self._instance.AbsolutePosition,
				resizeBuffer
			)
			if resize then
				xSize, xPos, ySize, yPos = unpack(resize, 1, 4)
				dragInput, dragStart = input, input.Position
				startOffset = self._instance.AbsolutePosition - self._instance.Parent.AbsolutePosition
				startSize = self._instance.AbsoluteSize
				resizing(true)
				local con
				con = input:GetPropertyChangedSignal("UserInputState"):Connect(function()
					if input.UserInputState == Enum.UserInputState.End then
						con:Disconnect()
						if dragInput == input then
							dragInput = nil
							resizing(false)
						end
					end
				end)
			end
		end
	end

	local titleBarHeight = UI.compute(function()
		return UI.Theme.FontSize() + UI.Theme.Padding().Offset * 2
	end)
	local buttonSize = function()
		return UDim2.fromOffset(titleBarHeight(), titleBarHeight())
	end
	local iconSize = function()
		return UDim2.fromOffset(UI.Theme.FontSize(), UI.Theme.FontSize())
	end

	self._exitButton = UI.new "Button" {
		LayoutOrder = 9,
		BackgroundColor3 = Color3.fromRGB(200, 0, 0),
		BackgroundTransparency = 1,
		HoverTransparency = 0,
		Size = buttonSize,
		Icon = UI.Theme.Image.Close,
		IconProperties = {
			ImageColor3 = UI.Theme.PrimaryText,
			Size = UDim2.fromScale(0.5, 0.5),
		},
		Text = "",
		Activated = function()
			self._instance.Visible = false
			UI.clearActiveStates()

			if self.Close and type(self.Close._value) == "function" then
				self.Close._value(self)
			elseif self.RemoveOnClose._value then
				self:Destroy()
			end
		end,
	}
	self._exitButton._instance:FindFirstChildOfClass("UIStroke"):Destroy()
	UI.edit(self._exitButton._instance.UICorner, { CornerRadius = UI.Theme.CornerPadded })

	self._content = UI.new "Frame" {
		Name = "UIContent",
		AutomaticSize = self.AutomaticSize,
		BackgroundTransparency = 1,
		Size = UDim2.new(1, 0, 1, 0),
		ClipsDescendants = true,

		UI.new "UIFlexItem" {
			FlexMode = Enum.UIFlexMode.Fill,
		},
		UI.new "UIPadding" {
			PaddingLeft = UI.Theme.Padding,
			PaddingRight = UI.Theme.Padding,
			PaddingBottom = UI.Theme.Padding,
			PaddingTop = UDim.new(0, 1),
		},
	}

	self._instance = UI.new "ImageLabel" {
		Name = "Window",
		AutomaticSize = self.AutomaticSize,
		BackgroundColor3 = function()
			return if UI.Theme.BackgroundGradientEnabled() then Color3.new(1, 1, 1) else UI.Theme.Primary()
		end,
		BackgroundTransparency = UI.Theme.Transparency,
		Image = UI.Theme.BackgroundImage,
		ImageTransparency = UI.Theme.BackgroundImageTransparency,
		ScaleType = UI.Theme.BackgroundImageScaleType,
		Position = self.Position,
		Size = self.Size,
		Visible = false,

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

		UI.new "ImageLabel" {
			Name = "ResizeIcon",
			ZIndex = 1e3,
			BackgroundTransparency = 1,
			Image = "rbxassetid://72264604015628",
			ImageRectSize = Vector2.new(24, 24),
			ImageRectOffset = resizeIconRectOffset,
			Size = UDim2.new(0, 24, 0, 24),
			Visible = resizeHover,
			Position = function()
				local inputPos = inputPosition()
				return UDim2.fromOffset(inputPos.X - 12, inputPos.Y - 12) - self.Position()
			end,

			InputBegan = resizeBegan,
		},

		UI.new "Frame" {
			AutomaticSize = self.AutomaticSize,
			BackgroundTransparency = 1,
			ClipsDescendants = true,
			Size = UDim2.fromScale(1, 1),

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

			UI.new "TextButton" {
				Name = "TitleBar",
				Active = true,
				Modal = true,
				LayoutOrder = -9e9,
				BackgroundTransparency = 1,
				ClipsDescendants = true,
				AutomaticSize = Enum.AutomaticSize.Y,
				Size = UDim2.new(1, 0, 0, 0),
				TextTransparency = 1,

				UI.new "UIListLayout" {
					FillDirection = Enum.FillDirection.Horizontal,
					SortOrder = Enum.SortOrder.LayoutOrder,
					Padding = UI.Theme.Padding,
					VerticalAlignment = Enum.VerticalAlignment.Center,
				},
				UI.new "UIPadding" {
					PaddingLeft = function()
						return UDim.new(0, UI.Theme.Padding().Offset + UI.Theme.FontSize / 2)
					end,
					PaddingRight = UI.Theme.Padding,
					PaddingTop = UI.Theme.Padding,
					PaddingBottom = UI.Theme.Padding,
				},

				UI.new "Frame" {
					Name = "Left",
					AutomaticSize = Enum.AutomaticSize.XY,
					BackgroundTransparency = 1,
					Size = UDim2.new(),

					UI.edit(
						UI.new "ImageLabel" {
							AnchorPoint = Vector2.new(0, 0.5),
							BackgroundTransparency = 1,
							Image = self.Icon,
							ImageColor3 = UI.Theme.PrimaryText,
							Position = UDim2.fromScale(0, 0.5),
							Size = iconSize,
							Visible = function()
								return if self.Icon() ~= "" then true else false
							end,
						},
						self.IconProperties()
					),

					UI.new "TextLabel" {
						AutoLocalize = false,
						Name = "Title",
						BackgroundTransparency = 1,
						AutomaticSize = Enum.AutomaticSize.XY,
						Size = UDim2.new(0, 0, 0, 0),
						FontFace = UI.Theme.Font,
						Position = function()
							local x = if self.Icon() ~= "" then UI.Theme.FontSize + UI.Theme.Padding().Offset else 0
							return UDim2.fromOffset(x, 0)
						end,
						RichText = true,
						Text = function()
							return `<b>{self.Title()}</b>`
						end,
						TextColor3 = UI.Theme.PrimaryText,
						TextStrokeColor3 = UI.Theme.Primary,
						TextStrokeTransparency = UI.Theme.TextStrokeTransparency,
						TextSize = UI.Theme.FontSizeLarger,
						TextXAlignment = Enum.TextXAlignment.Left,
						TextYAlignment = Enum.TextYAlignment.Center,
						TextTruncate = Enum.TextTruncate.SplitWord,
					},
				},
				UI.new "Spacer" {},

				self._exitButton,

				InputBegan = dragInputBegan,
			},

			self._content,
		},

		[UI.Event] = {
			Visible = function()
				if self._instance.Visible then
					Class.bringToFront(self)
					Class.nudge(self)
				end
			end,
		},
	} :: Frame

	local mouseIconWasEnabled, mouseIconChanging = UI.UserInputService.MouseIconEnabled, nil
	UI.UserInputService:GetPropertyChangedSignal("MouseIconEnabled"):Connect(function()
		if mouseIconChanging then
			return
		end
		mouseIconWasEnabled = UI.UserInputService.MouseIconEnabled
	end)

	UI.UserInputService.InputBegan:Connect(resizeBegan)
	UI.UserInputService.InputChanged:Connect(function(input)
		if not self._instance.Visible then
			return
		end

		if
			input.UserInputType == Enum.UserInputType.MouseMovement
			or input.UserInputType == Enum.UserInputType.Touch
		then
			if resizing._value then
				-- update resize
				resizeUpdate(input)
			elseif dragging._value then
				dragUpdate(input)
			elseif self.Resizable._value then
				local resize = getResizeIndex(
					Vector2.new(input.Position.X, input.Position.Y),
					self._instance.AbsoluteSize,
					self._instance.AbsolutePosition,
					resizeBuffer
				)
				if resize then
					inputPosition(input.Position)
					resizeHover(true)
					resizeIconRectOffset(Vector2.new(resize[5]))
					if UI.UserInputService.MouseIconEnabled then
						mouseIconChanging = true
						UI.UserInputService.MouseIconEnabled = false
						mouseIconChanging = false
					end
				elseif resizeHover._value then
					resizeHover(false)
					if UI.UserInputService.MouseIconEnabled ~= mouseIconWasEnabled then
						mouseIconChanging = true
						UI.UserInputService.MouseIconEnabled = mouseIconWasEnabled
						mouseIconChanging = false
					end
				end
			end
		end
	end)

	local parentConn
	self._instance:GetPropertyChangedSignal("Parent"):Connect(function()
		if parentConn then
			parentConn:Disconnect()
		end
		local parent = self._instance.Parent
		if parent and parent:IsA("GuiBase2d") then
			local function updateSize()
				if not self._instance.Visible then
					return
				end

				local parentSize = self._instance.Parent.AbsoluteSize
				local windowSize = self._instance.AbsoluteSize
				local maxPosition = parentSize - windowSize
				local position = self._instance.AbsolutePosition - self._instance.Parent.AbsolutePosition

				local padding = self._instance.Parent:FindFirstChildOfClass("UIPadding")
				local paddingX = if padding
					then padding.PaddingLeft.Offset
						+ parentSize.X * padding.PaddingLeft.Scale
						+ padding.PaddingRight.Offset
						+ parentSize.X * padding.PaddingRight.Scale
					else 0
				local paddingY = if padding
					then padding.PaddingTop.Offset
						+ parentSize.Y * padding.PaddingTop.Scale
						+ padding.PaddingBottom.Offset
						+ parentSize.Y * padding.PaddingBottom.Scale
					else 0

				self.Size(
					UDim2.fromOffset(
						math.clamp(windowSize.X, 0, math.max(0, parentSize.X - paddingX)),
						math.clamp(windowSize.Y, 0, math.max(0, parentSize.Y - paddingY))
					)
				)
				self.Position(
					UDim2.fromOffset(
						math.clamp(position.X, 0, math.max(0, maxPosition.X)),
						math.clamp(position.Y, 0, math.max(0, maxPosition.Y))
					)
				)
			end
			parentConn = parent:GetPropertyChangedSignal("AbsoluteSize"):Connect(updateSize)
			self._instance:GetPropertyChangedSignal("Visible"):Connect(updateSize)
			if self._instance.Visible then
				updateSize()
			end
		end
	end)

	return setmetatable(self, Class) :: typeof(self) & typeof(Class)
end

return Class


return function(UI)
	return {
		ImageButton = {
			HoverHapticEffect = UI.Haptic.Hover,
			PressHapticEffect = UI.Haptic.Click,
		},
		TextButton = {
			HoverHapticEffect = UI.Haptic.Hover,
			PressHapticEffect = UI.Haptic.Click,
		},
	}
end


return {
	Transparency = 0.08,
	TextStrokeTransparency = 0.875,

	BackgroundImage = "rbxassetid://104331312960285",
	BackgroundImageScaleType = Enum.ScaleType.Crop,
	BackgroundImageTransparency = 1,

	BackgroundGradientEnabled = false,
	BackgroundGradient = Color3.fromRGB(63, 0, 31),
	BackgroundGradientStop = Color3.fromRGB(31, 0, 63),
	BackgroundGradientAngle = 45,

	Primary = Color3.fromRGB(18, 18, 21),
	PrimaryText = Color3.fromRGB(255, 255, 255),

	Secondary = Color3.fromRGB(255, 255, 255),
	SecondaryText = Color3.fromRGB(0, 0, 0),

	Border = Color3.fromRGB(100, 100, 100),
	BorderStop = Color3.fromRGB(100, 100, 100),
	BorderAngle = 90,

	Valid = Color3.fromRGB(0, 255, 0),
	Invalid = Color3.fromRGB(255, 0, 0),

	FontSize = 16,
	Font = Font.fromId(12187365364),
	FontMono = Enum.Font.RobotoMono,

	CornerRadius = UDim.new(0, 12),
	Padding = UDim.new(0, 6),
	StrokeEnabled = false,

	TweenIn = TweenInfo.new(0.3, Enum.EasingStyle.Cubic, Enum.EasingDirection.In),
	TweenOut = TweenInfo.new(0.3, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out),
	TweenInOut = TweenInfo.new(0.3, Enum.EasingStyle.Cubic, Enum.EasingDirection.InOut),

	Image = {
		Add = "75463072948156",
		Check = "125053493387464",
		Close = "117385134461283",
		Check_Bold = "73008384795860",
		Close_Bold = "74789968601408",
		Cloud = "118275085320462",
		Cloud_Done = "123950085396972",
		Cloud_Save = "118034592501659",
		Circle = "89534232370062",
		Edit = "76957302873541",
		Filter = "93483633020282",
		Graph1 = "106022943008038",
		Host = "90101028950281",
		Hourglass_Bottom = "113528044873866",
		Memory = "129377673430313",
		Memory_Alt = "94473693145825",
		Pace = "109012026245646",
		Search = "82890781874034",
		Speed = "116538164057946",
		Terminal = "108121111850606",

		Copy = "124783262098670",
		Cut = "100350584715025",
		Delete = "100174268115752",
		Paste = "107985770649673",

		Down = "116491276097788",

		Play = "113952858544170",
		Stop = "94423843228984",

		SignalSheet = "79089908951427", -- 86x86 size 90 offset 5x1 sheet
	},

	SoundEnabled = true,
	TypingSounds = true,
	TypingSoundsOnEveryTextBox = true,
	Volume = 1,

	Sound = {
		Back = "10066914500",
		Call_Accept = "17208209318",
		Call_Decline = "17208214688",
		Call_Hangup = "17208220181",
		Call_Leave = "17208295577",
		Hello = "17208335138",
		Hover01 = "17208339919",
		Hover02 = "17208343514",
		Hover03 = "17208348006",
		Negative = "17208353912",
		Notification_High = "17208361335",
		Notification_Low = "17208372272",
		PopSwipe = "10128766965",
		Select01 = "10128760939",
		Select02 = "10066968815",
		Select03 = "17208396156",
		Swipe = "17208405682",
	},
}


return {
	Transparency = 0.7,
	TextStrokeTransparency = 0.9,

	Primary = Color3.fromRGB(0, 0, 0),
	PrimaryText = Color3.fromRGB(255, 255, 255),

	Secondary = Color3.fromRGB(255, 255, 255),
	SecondaryText = Color3.fromRGB(0, 0, 0),

	Border = Color3.fromRGB(100, 100, 100),

	Valid = Color3.fromRGB(0, 255, 0),
	Invalid = Color3.fromRGB(255, 0, 0),

	FontSize = 18,
	Font = Enum.Font.SourceSans,

	CornerRadius = UDim.new(0, 0),
	Padding = UDim.new(0, 4),
}


return {
	Primary = Color3.fromRGB(255, 255, 255),
	PrimaryText = Color3.fromRGB(0, 0, 0),

	Secondary = Color3.fromRGB(0, 0, 0),
	SecondaryText = Color3.fromRGB(255, 255, 255),

	Border = Color3.fromRGB(191, 191, 191),

	Valid = Color3.fromRGB(0, 200, 0),
	Invalid = Color3.fromRGB(255, 0, 0),
}


local Package = script.Parent.Parent.Parent

local Flux = require(Package:WaitForChild("Flux"))
local ThemesFolder = script:WaitForChild("Themes")
local Default = require(script:WaitForChild("Default"))

local compute = Flux.compute
local state = Flux.state

--- @within UI
--- A directory of Flux state for UI components to use as theme variables
--- @interface Theme
--- .Transparency State<number>
--- .Primary State<Color3>
--- .PrimaryText State<Color3>
--- .Secondary State<Color3>
--- .SecondaryText State<Color3>
--- .Border State<Color3>
--- .Valid State<Color3>
--- .Invalid State<Color3>
--- .FontSize State<number>
--- .Font State
--- .FontMono State
--- .FontHeavy State
--- .FontBold State
--- .FontSemiBold State
--- .FontLight State
--- .CornerRadius State<UDim>
--- .CornerHalf State<UDim>
--- .CornerDiameter State<UDim>
--- .Padding State<UDim>
--- .PaddingHalf State<UDim>
--- .PaddingDouble State<UDim>
--- .StrokeEnabled State<boolean>
--- .NormalTween State<TweenInfo>
--- .TransparencyClamped State<number>
--- .ScrollMidImage State<string>
--- .ScrollTopImage State<string>
--- .ScrollBottomImage State<string>
--- .SoundsEnabled State<boolean>
--- .TypingSounds State<boolean>
--- .TypingSoundsOnEveryTextBox State<boolean>
--- .Volume State<number>

local themes = {}

local function loadTheme(themeModule: ModuleScript)
	if themeModule:IsA("ModuleScript") then
		if themes[themeModule.Name] then
			warn("Duplicate theme found, make sure they have different names!")
		end
		local theme = require(themeModule)
		if typeof(theme.Font) == "EnumItem" then
			theme.Font = Font.fromEnum(theme.Font :: Enum.Font)
		end
		if typeof(theme.FontMono) == "EnumItem" then
			theme.FontMono = Font.fromEnum(theme.FontMono :: Enum.Font)
		end
		for _, definition in { theme.Image, theme.Sound } do
			for key, value in definition do
				definition[key] = "rbxassetid://" .. value
			end
		end
		if theme == Default then
			return
		end
		if themeModule.Name == "Default" then
			-- clone the default theme to not use overwritten values
			local default = {}
			for key, value in theme do
				default[key] = if type(value) == "table" then table.clone(value) else value
			end
			themes[themeModule.Name] = default
		else
			themes[themeModule.Name] = theme
		end
	end
end

loadTheme(script:WaitForChild("Default"))
for _, themeModule: ModuleScript in ThemesFolder:GetChildren() do
	loadTheme(themeModule)
end
task.wait()

local Theme = {
	Themes = themes,
}

local userDefault = ThemesFolder:FindFirstChild("Default") :: ModuleScript
userDefault = if userDefault then require(userDefault) else {}
for k, v in Default do
	Theme[k] = userDefault[k] or v
end

local function makeStateful(source: { [string]: any })
	for key, value in source do
		if type(value) == "table" then
			makeStateful(value)
		else
			source[key] = state(value)
		end
	end
end
makeStateful(Theme)

-- COMPUTED THEME VALUES

Theme.TransparencySlight = compute(function()
	return Theme.Transparency * 0.9375 + 0.0625
end)

Theme.TransparencyLight = compute(function()
	return Theme.Transparency * 0.875 + 0.125
end)

Theme.TransparencyMedium = compute(function()
	return Theme.Transparency * 0.75 + 0.25
end)

Theme.TransparencyBalanced = compute(function()
	return Theme.Transparency * 0.5 + 0.5
end)

Theme.TransparencyStrong = compute(function()
	return Theme.Transparency * 0.25 + 0.75
end)

Theme.TransparencyHeavy = compute(function()
	return Theme.Transparency * 0.125 + 0.875
end)

Theme.TransparencyMax = compute(function()
	return Theme.Transparency * 0.0625 + 0.9375
end)

Theme.TransparencyClamped = compute(function()
	return Theme.Transparency * 0.75
end)

Theme.FontHeavy = compute(function()
	return Font.new(Theme.Font().Family, Enum.FontWeight.Heavy)
end)

Theme.FontBold = compute(function()
	return Font.new(Theme.Font().Family, Enum.FontWeight.Bold)
end)

Theme.FontSemiBold = compute(function()
	return Font.new(Theme.Font().Family, Enum.FontWeight.SemiBold)
end)

Theme.FontLight = compute(function()
	return Font.new(Theme.Font().Family, Enum.FontWeight.Light)
end)

Theme.FontThin = compute(function()
	return Font.new(Theme.Font().Family, Enum.FontWeight.Thin)
end)

Theme.FontSizeDouble = compute(function()
	return math.round(Theme.FontSize * 2)
end)

Theme.FontSizeLargest = compute(function()
	return math.round(Theme.FontSize * 1.75)
end)

Theme.FontSizeLarger = compute(function()
	return math.round(Theme.FontSize * 1.5)
end)

Theme.FontSizeLarge = compute(function()
	return math.round(Theme.FontSize * 1.25)
end)

Theme.FontSizeSmall = compute(function()
	return math.round(Theme.FontSize * 0.75)
end)

Theme.FontSizeSmaller = compute(function()
	return math.round(Theme.FontSize * 0.5)
end)

Theme.FontSizeSmallest = compute(function()
	return math.round(Theme.FontSize * 0.25)
end)

Theme.PaddingHalf = compute(function()
	local padding = Theme.Padding()
	return UDim.new(padding.Scale / 2, padding.Offset / 2)
end)

Theme.PaddingDouble = compute(function()
	local padding = Theme.Padding()
	return UDim.new(padding.Scale * 2, padding.Offset * 2)
end)

Theme.PaddingStroke = compute(function()
	return UDim.new(0, if Theme.StrokeEnabled() then 1 else 0)
end)

Theme.PaddingWithStroke = compute(function()
	return Theme.Padding + Theme.PaddingStroke
end)

Theme.CornerHalf = compute(function()
	local radius = Theme.CornerRadius()
	return UDim.new(radius.Scale / 2, radius.Offset / 2)
end)

Theme.CornerDiameter = compute(function()
	local radius = Theme.CornerRadius()
	return UDim.new(radius.Scale * 2, radius.Offset * 2)
end)

Theme.CornerPadded = compute(function()
	local padding = Theme.Padding()
	local radius = Theme.CornerRadius()
	return UDim.new(radius.Scale, radius.Offset - padding.Offset / 2)
end)

Theme.CornerPadded2 = compute(function()
	local padding = Theme.Padding()
	local radius = Theme.CornerRadius()
	return UDim.new(radius.Scale, radius.Offset - padding.Offset)
end)

Theme.CornerPadded3 = compute(function()
	local padding = Theme.Padding()
	local radius = Theme.CornerRadius()
	return UDim.new(radius.Scale, radius.Offset - padding.Offset * 1.5)
end)

Theme.ScrollMidImage = state("rbxassetid://18370268668")
Theme.ScrollTopImage = compute(function()
	return if Theme.CornerRadius().Offset == 0 then "rbxassetid://18370268668" else "rbxassetid://105141456035522"
end)
Theme.ScrollBottomImage = compute(function()
	return if Theme.CornerRadius().Offset == 0 then "rbxassetid://18370268668" else "rbxassetid://110012579042362"
end)

return Theme :: typeof(Default) & typeof(Theme)


local Players = game:GetService("Players")
local GuiService = game:GetService("GuiService")
local TextService = game:GetService("TextService")
local SoundService = game:GetService("SoundService")
local UserInputService = game:GetService("UserInputService")

local Package = script.Parent.Parent

local Flux = require(Package:WaitForChild("Flux"))
local Type = require(Package.Flux:WaitForChild("Type"))
local state = Flux.state

local Classes = script:WaitForChild("Classes")
local playerGui = Players.LocalPlayer and Players.LocalPlayer:WaitForChild("PlayerGui")

--- @class UI
local UI = { Class = {} }
UI.__index = UI
setmetatable(UI, Flux)

UI.Fonts = require(script.Fonts)
UI.Theme = require(script.Theme)
UI.Themes = UI.Theme.Themes
UI.Sound = {}

local SoundGroup = Flux.new "SoundGroup" {
	Parent = SoundService,
	Name = "_KASounds",
	Volume = function()
		return if UI.Theme.SoundEnabled() then UI.Theme.Volume() else 0
	end,
}

for key, soundIdState in UI.Theme.Sound do
	UI.Sound[key] = Flux.new "Sound" {
		Name = key,
		Parent = SoundGroup,
		SoundId = soundIdState,
		SoundGroup = SoundGroup,
	}
end

UI.Haptic = {
	Click = Flux.new "HapticEffect" {
		Parent = workspace,
		Type = Enum.HapticEffectType.Custom,
		Flux.action(function(self)
			self:SetWaveformKeys({
				FloatCurveKey.new(0, 0.1, Enum.KeyInterpolationMode.Constant),
				FloatCurveKey.new(1, 0, Enum.KeyInterpolationMode.Constant),
			})
		end),
	},
	Hover = Flux.new "HapticEffect" {
		Parent = workspace,
		Type = Enum.HapticEffectType.UIHover,
	},
	Notification = Flux.new "HapticEffect" {
		Parent = workspace,
		Type = Enum.HapticEffectType.UINotification,
	},
}

function UI.getFontName(font: Font | Enum.Font)
	if typeof(font) == "Font" then
		local id = string.match(font.Family, "%d+$")
		if id then
			return UI.Fonts[id]
		else
			return string.match(font.Family, "/([^/]+)%.json$")
		end
	else
		return font.Name
	end
end

--- @within UI
--- @prop GuiService GuiService
UI.GuiService = GuiService

--- @within UI
--- @prop TextService TextService
UI.TextService = TextService

--- @within UI
--- @prop UserInputService UserInputService
UI.UserInputService = UserInputService

--- @within UI
--- @prop LocalPlayer Player
UI.LocalPlayer = Players.LocalPlayer

--- @within UI
--- @prop PlayerGui PlayerGui
UI.PlayerGui = playerGui

--- @within UI
--- @type Platform "Console" | "Mobile" | "PC" | "VR"
export type Platform = "Console" | "Mobile" | "PC" | "VR"

--- @within UI
--- @prop Touch boolean
UI.Touch = UserInputService.TouchEnabled

UI.IsConsoleScreen = GuiService:IsTenFootInterface()

--- @within UI
--- @prop Platform Platform
UI.Platform = if UserInputService.VREnabled
	then "VR"
	elseif UI.IsConsoleScreen then "Console"
	elseif UserInputService.KeyboardEnabled and UserInputService.MouseEnabled then "PC"
	else "Mobile"

--- @within UI
--- A marker for defining UI state hooks in [UI.edit]
--- @prop Hook Flux.Symbol
--- @readonly
UI.Hook = table.freeze({ type = "Symbol" }) :: Type.Symbol

--- @within UI
--- A marker for defining function defaultState in [UI.makeStatefulDefaults]
--- @prop Function Flux.Symbol
--- @readonly
UI.Function = table.freeze({ type = "Symbol" }) :: Type.Symbol

--- @within UI
--- A marker for defining Nil defaultState in [UI.makeStatefulDefaults]
--- @prop Nil Flux.Symbol
--- @readonly
UI.Nil = table.freeze({ type = "Symbol" }) :: Type.Symbol

--- @within UI
--- GuiService.TopbarInset as a Flux state
--- @prop TopbarInset State<Rect>
UI.TopbarInset = state(GuiService, "TopbarInset")
UI.TopbarPadding = Flux.compute(function()
	return UDim.new(0, if UI.IsConsoleScreen then 10 else UI.TopbarInset().Height - 46)
end)

--- @within UI
--- The lowermost ScreenGui for UI components
--- @prop LayerBottom State<ScreenGui>
UI.LayerBottom = Flux.new "ScreenGui" {
	Parent = playerGui,
	Name = "FluxUILayerBottom",
	ResetOnSpawn = false,
	IgnoreGuiInset = true,
	ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
	ScreenInsets = Enum.ScreenInsets.None,
}

--- @within UI
--- The default ScreenGui for UI components
--- @prop LayerDefault State<ScreenGui>
UI.LayerDefault = Flux.new "ScreenGui" {
	Parent = playerGui,
	Name = "FluxUILayerDefault",
	DisplayOrder = 2,
	ResetOnSpawn = false,
	IgnoreGuiInset = true,
	ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
	ScreenInsets = Enum.ScreenInsets.None,
}

--- @within UI
--- The topmost ScreenGui for UI components
--- @prop LayerTop State<ScreenGui>
UI.LayerTop = Flux.new "ScreenGui" {
	Parent = playerGui,
	Name = "FluxUILayerTop",
	DisplayOrder = 999999999,
	ResetOnSpawn = false,
	IgnoreGuiInset = true,
	ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
	ScreenInsets = Enum.ScreenInsets.None,
}

UI.LayerTopInset = UI.new "Frame" {
	Name = "TopbarInset",
	Parent = UI.LayerTop,
	BackgroundTransparency = 1,
	AnchorPoint = Vector2.new(0, 1),
	Position = UDim2.fromScale(0, 1),
	Size = if UI.IsConsoleScreen
		then UDim2.new(1, 0, 1, -54)
		else function()
			return UDim2.new(1, 0, 1, -UI.TopbarInset().Height)
		end,
}

--- @within UI
--- The Topbar ScreenGui for UI components
--- @prop LayerTopbar State<ScreenGui>
UI.LayerTopbar = Flux.new "ScreenGui" {
	Parent = playerGui,
	Name = "FluxUILayerTopbar",
	DisplayOrder = 999999999,
	ResetOnSpawn = false,
	IgnoreGuiInset = true,
	ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
	ScreenInsets = Enum.ScreenInsets.TopbarSafeInsets,
}

--- @within UI
--- A sorted Topbar Frame for UI components, parented under [UI.LayerTopbar]
--- @prop TopbarFrame State<Frame>
UI.TopbarFrame = UI.new "Frame" {
	Parent = UI.LayerTopbar,
	BackgroundTransparency = 1,
	Position = UDim2.new(0, 0, 0, if UI.IsConsoleScreen then 4 else 0),
	Size = function()
		return UDim2.new(1, 0, 0, UI.TopbarInset().Height + if UI.IsConsoleScreen then 50 else 0)
	end,
	Visible = function()
		return UI.IsConsoleScreen or UI.TopbarInset().Height > 0
	end,

	UI.new "UIListLayout" {
		FillDirection = Enum.FillDirection.Horizontal,
		SortOrder = Enum.SortOrder.LayoutOrder,
		VerticalAlignment = Enum.VerticalAlignment.Bottom,
		Padding = UI.TopbarPadding,
	},
	UI.new "UIPadding" {
		PaddingLeft = if UI.IsConsoleScreen
			then UDim.new(0, 10)
			else function()
				return UDim.new(0, 2 + UI.TopbarPadding().Offset / 2)
			end,
		PaddingRight = UI.TopbarPadding,
		PaddingTop = if UI.IsConsoleScreen then UDim.new(0, 6) else UI.TopbarPadding,
		PaddingBottom = UDim.new(0, if UI.IsConsoleScreen then 0 else 2),
	},
}

--- @within UI
--- @type ActiveStateType "hover" | "floating" | "dialog"
export type ActiveStateType = "hover" | "floating" | "dialog"

UI._activeState = {}

--- A generalized function to handle activation/deactivation of both floating and hover states
function UI.activateState(activeState: any, stateType: ActiveStateType)
	if not Flux.isState(activeState) then
		error("Invalid active state")
	end

	-- Clear the previous state if it's different
	local previousState = UI._activeState[stateType]
	if previousState and previousState ~= activeState then
		previousState(false)
	end

	-- Set the new active state
	UI._activeState[stateType] = activeState

	local state: any = activeState
	state(true)
end

--- A generalized function to deactivate both floating and hover states
function UI.deactivateState(activeState: any, stateType: ActiveStateType)
	if not Flux.isState(activeState) then
		error("Invalid active state")
	end

	-- Clear the previous state if it matches
	local previousState = UI._activeState[stateType]
	if previousState and previousState == activeState then
		UI._activeState[stateType] = nil
	end

	local state: any = activeState
	state(false)
end

--- A generalized function to toggle both floating and hover states
function UI.toggleState(activeState: any, stateType: ActiveStateType)
	if not Flux.isState(activeState) then
		error("Invalid active state")
	end
	if activeState._value then
		UI.deactivateState(activeState, stateType)
	else
		UI.activateState(activeState, stateType)
	end
end

--- Function to clear a specific state type
function UI.clearState(stateType: ActiveStateType)
	local activeState = UI._activeState[stateType]
	if Flux.isState(activeState) then
		activeState(false)
		UI._activeState[stateType] = nil
	end
end

--- Function to clear all active state types
function UI.clearActiveStates()
	for stateType in UI._activeState do
		UI.clearState(stateType)
	end
end

--- Gets the luminance of a [Color3]
function UI.getLuminance(color: Color3): number
	return math.sqrt(0.299 * color.R ^ 2 + 0.587 * color.G ^ 2 + 0.114 * color.B ^ 2)
end

--- Inverts the luminance of a [Color3] by an amount
function UI.invertLuminance(color: Color3, amount: number): Color3
	local rLum = 0.299 * color.R ^ 2
	local gLum = 0.587 * color.G ^ 2
	local bLum = 0.114 * color.B ^ 2

	local originalLuminance = math.sqrt(rLum + gLum + bLum)
	if originalLuminance > 0.5 then -- invert amount if light
		amount = -amount
	end

	local targetRatio = math.clamp(originalLuminance + amount, 0, 1) / originalLuminance
	local newR = math.clamp(math.sqrt(rLum * targetRatio / 0.299), 0, 1)
	local newG = math.clamp(math.sqrt(gLum * targetRatio / 0.587), 0, 1)
	local newB = math.clamp(math.sqrt(bLum * targetRatio / 0.114), 0, 1)

	return Color3.new(newR, newG, newB)
end

--- Checks if x, y is within the absolute bounds of the GuiObject
function UI.pointInGuiObject(x: number, y: number, object: GuiObject): boolean
	local size = object.AbsoluteSize
	local position = object.AbsolutePosition
	return x >= position.X and x <= position.X + size.X and y >= position.Y and y <= position.Y + size.Y
end

function UI.sinkInput(x: number, y: number, object: GuiObject): boolean
	for _, gui in UI.PlayerGui:GetGuiObjectsAtPosition(x, y) do
		if gui == object then
			break
		end
		if gui.Active or gui:IsA("GuiButton") then
			return true
		end
	end
	return false
end

function UI.retryGetTextBoundsAsync(params)
	local attempt = 1
	local ok, bounds = pcall(TextService.GetTextBoundsAsync, TextService, params)
	while not ok do
		task.wait(2 ^ attempt)
		attempt += 1
		ok, bounds = pcall(TextService.GetTextBoundsAsync, TextService, params)
	end
	return bounds
end

--- Helper function for defining Flux state defaults
function UI.makeStatefulDefaults(default: Type.Dict<any>, definition: Type.Dict<any>?)
	for key, value in default do
		if definition then
			value = if definition[key] ~= nil then definition[key] else value
		end
		if typeof(value) == "function" and default[key] ~= UI.Function then
			value = Flux.compute(value)
		end
		if value == UI.Nil or value == UI.Function then
			default[key] = nil
			continue
		end
		if not Flux.isState(value) then
			value = Flux.state(value)
		end
		default[key] = value
	end
	return default
end

function UI.newSinkHoverState(object: GuiObject, hovering: any?)
	if not hovering then
		hovering = UI.state(false)
	end

	UI.edit(object, {
		InputChanged = function(input, processed)
			if processed then
				return
			end
			if
				input.UserInputType == Enum.UserInputType.MouseMovement
				or input.UserInputType == Enum.UserInputType.Touch
			then
				if UI.sinkInput(input.Position.X, input.Position.Y, object) then
					UI.deactivateState(hovering, "hover")
					return
				end
				UI.activateState(hovering, "hover")
			end
		end,
		InputEnded = function(input)
			if
				input.UserInputType == Enum.UserInputType.MouseMovement
				or input.UserInputType == Enum.UserInputType.Touch
			then
				UI.deactivateState(hovering, "hover")
			end
		end,
	})

	return hovering
end

--- Inherits [Flux.edit]
--- * Parents children to the ._content variable of a UI component if one exists
--- * Numeric indicies are defined as children with [GuiObject.LayoutOrder] set to the index
--- * Allows UI.Hook to easily hook to default UI state or [RBXScriptSignal] aliases
function UI.edit(new: Instance | Type.Dict<any>, properties: Type.Dict<any>): Instance | Type.Dict<any>
	properties = properties :: Type.Dict<any> & GuiObject

	local instance = new
	local contentParent = instance
	local cleanup = {}

	if type(new) == "table" then
		instance = new._instance
		contentParent = new._content or new._instance
	end

	-- numeric index child definition
	for index, child in properties do
		if type(index) ~= "number" or index ~= math.floor(index :: number) then
			continue
		end
		if type(child) == "table" and child._instance then
			child = child._instance
		end
		if typeof(child) == "Instance" then
			if child:IsA("GuiObject") then
				child.LayoutOrder = if child.LayoutOrder ~= 0 then child.LayoutOrder else index
			end
			child.Parent = contentParent
			properties[index] = nil
		end
	end

	if type(new) == "table" then
		local hooks = properties[UI.Hook]
		properties[UI.Hook] = nil
		if hooks then
			for key, hook in hooks do
				local classMember = new[key]
				if not UI.isState(classMember) then
					error(`Invalid UI State Hook {key}`)
				end
				if type(hook) == "function" then
					table.insert(cleanup, classMember:Connect(hook))
				end
			end
		end
		for key, value in properties do
			local classMember = new[key]
			if classMember == nil then
				continue
			end
			properties[key] = nil -- strip class members
			-- connect alias signals
			if typeof(classMember) == "RBXScriptSignal" then
				classMember:Connect(value)
			end
		end
	end

	if not properties[Flux.Clean] then
		properties[Flux.Clean] = {}
	end

	for _, v in cleanup do
		table.insert(properties[Flux.Clean], v)
	end

	local parent = properties.Parent
	if parent then
		properties.Parent = if type(parent) == "table" then (parent._content or parent._instance) else parent
	end
	-- apply non class members to the root instance with Flux.edit
	Flux.edit(instance, properties)
	return new
end

local defaultProperties = require(script.DefaultProperties)(UI)

local constructors: { [string]: () -> Instance } = {}
setmetatable(constructors, {
	__index = function(self, className: string)
		local class = UI.Class[className]
		local new

		if class then
			new = function(properties: Type.Dict<any>): Instance | Type.Dict<any>
				return UI.edit(class.new(properties), properties)
			end
		else
			new = function(properties: Type.Dict<any>): Instance | Type.Dict<any>
				local instance = Instance.new(className)

				if defaultProperties[className] then
					UI.edit(instance, defaultProperties[className])
				end

				return UI.edit(instance, properties)
			end
		end

		self[className] = new
		return new
	end,
})

type Constructor =
	Type.Constructor
	& Type.New<"Button", typeof(require(Classes.Button).new())>
	& Type.New<"Checkbox", typeof(require(Classes.Checkbox).new())>
	& Type.New<"CircleProgress", typeof(require(Classes.CircleProgress).new())>
	& Type.New<"Color", typeof(require(Classes.Color).new())>
	& Type.New<"ColorPicker", typeof(require(Classes.ColorPicker).new())>
	& Type.New<"Dialog", typeof(require(Classes.Dialog).new())>
	& Type.New<"Gradient", typeof(require(Classes.Gradient).new())>
	& Type.New<"Link", typeof(require(Classes.Link).new())>
	& Type.New<"List", typeof(require(Classes.List).new())>
	& Type.New<"ListItem", typeof(require(Classes.ListItem).new())>
	& Type.New<"Menu", typeof(require(Classes.Menu).new())>
	& Type.New<"Scroller", typeof(require(Classes.Scroller).new())>
	& Type.New<"ScrollerFast", typeof(require(Classes.ScrollerFast).new())>
	& Type.New<"Select", typeof(require(Classes.Select).new())>
	& Type.New<"Slider", typeof(require(Classes.Slider).new())>
	& Type.New<"Spacer", typeof(require(Classes.Spacer).new())>
	& Type.New<"Stroke", typeof(require(Classes.Stroke).new())>
	& Type.New<"Switch", typeof(require(Classes.Switch).new())>
	& Type.New<"Tabs", typeof(require(Classes.Tabs).new())>
	& Type.New<"Tooltip", typeof(require(Classes.Tooltip).new())>
	& Type.New<"Window", typeof(require(Classes.Window).new())>

--- Creates a new Instance with [UI.edit]
--- @within UI
--- @function new
--- @param className string
--- @return UI.edit
UI.new = (function(className)
	return constructors[className]
end :: any) :: Constructor

--- Registers a new UI component class
function UI.register(className: string, class: Type.Dict<any>)
	if UI.Class[className] then
		error(`UI class "{className}" already exists`)
	end
	UI.Class[className] = class
end

--- Run to register the default classes for the UI module
function UI.registerClasses(): typeof(UI)
	UI.registerClasses = nil :: any
	for _, child in script.Classes:GetChildren() do
		if child:IsA("ModuleScript") then
			UI.register(child.Name, require(child))
		end
	end
	return UI
end

return UI


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

return function(from: number?, name: string?, image: string?, badgeEnabled: boolean?)
	local fromK = from :: any == "_K"
	if type(from) == "string" then
		if name == nil and from ~= "_K" then
			name = from
		end
		from = nil
	end

	local displayName = name or ""

	if badgeEnabled == nil then
		badgeEnabled = true
	end

	local userName, badge
	local color = "#fff"

	if from then
		displayName = "Loading..."
		userName = "Unknown"
		badge = "?"
	end

	local imageLabel
	if image or fromK or tonumber(from) then
		imageLabel = if from
			then UI.new "ImageLabel" {
				Name = "Icon",
				BackgroundTransparency = UI.Theme.TransparencyHeavy,
				BackgroundColor3 = UI.Theme.PrimaryText,
				Size = function()
					return UDim2.fromOffset(UI.Theme.FontSizeDouble(), UI.Theme.FontSizeDouble())
				end,
				Image = if image
					then image
					elseif from then `rbxthumb://type=AvatarHeadShot&id={from}&w=48&h=48`
					else "rbxassetid://10650688076",

				UI.new "UICorner" {
					CornerRadius = UI.Theme.CornerPadded,
				},
				UI.new "Stroke" {},
			}
			else UI.new "Frame" {
				BackgroundTransparency = 1,
				Size = function()
					return UDim2.fromOffset(UI.Theme.FontSizeLarger(), UI.Theme.FontSizeLarger())
				end,
				UI.new "ImageLabel" {
					Name = "Icon",
					AnchorPoint = Vector2.new(0.5, 0.5),
					BackgroundTransparency = 1,
					Position = UDim2.fromScale(0.5, 0.5),
					Size = function()
						return UDim2.fromOffset(UI.Theme.FontSize(), UI.Theme.FontSize())
					end,
					Image = "rbxassetid://71961243872230",
				},
			}
	end

	local displayNameLabel = UI.new "TextLabel" {
		AutoLocalize = false,
		Name = "From",
		BackgroundTransparency = 1,
		TextColor3 = UI.Theme.PrimaryText,
		TextStrokeColor3 = UI.Theme.Primary,
		TextStrokeTransparency = UI.Theme.TextStrokeTransparency,
		Text = displayName,
		TextSize = if from then UI.Theme.FontSizeLarge else UI.Theme.FontSizeLarger,
		TextXAlignment = "Left",
		FontFace = UI.Theme.FontHeavy,
		RichText = true,

		AutomaticSize = Enum.AutomaticSize.XY,
	}

	local userNameLabel = UI.new "TextLabel" {
		AutoLocalize = false,
		Name = "FromBottom",
		BackgroundTransparency = 1,
		TextColor3 = UI.Theme.PrimaryText,
		TextStrokeColor3 = UI.Theme.Primary,
		TextStrokeTransparency = UI.Theme.TextStrokeTransparency,
		Text = `<font transparency="0.5"><b>@{userName}</b></font>`,
		TextSize = UI.Theme.FontSizeSmall,
		TextXAlignment = "Left",
		FontFace = UI.Theme.Font,
		RichText = true,

		AutomaticSize = Enum.AutomaticSize.XY,
		Position = function()
			return UDim2.fromOffset(0, UI.Theme.FontSizeLarge())
		end,
	}

	local displaySize = UI.state(displayNameLabel, "AbsoluteSize")

	local badgeLabel = if badgeEnabled and badge
		then UI.new "TextLabel" {
			AutoLocalize = false,
			Name = "Badge",
			BackgroundColor3 = Color3.fromHex(color),
			TextColor3 = if UI.getLuminance(Color3.fromHex(color)) > 0.5 then Color3.new() else Color3.new(1, 1, 1),
			Text = `<b>{string.upper(badge)}</b>`,
			TextSize = function()
				return UI.Theme.FontSizeLarge() - 8
			end,
			TextXAlignment = "Left",
			FontFace = UI.Theme.FontMono,
			RichText = true,

			AutomaticSize = Enum.AutomaticSize.X,
			Size = function()
				return UDim2.fromOffset(0, UI.Theme.FontSizeLarge())
			end,
			Position = function()
				return UDim2.fromOffset(displaySize().X + 4, 0)
			end,

			UI.new "UICorner" {
				CornerRadius = UI.Theme.CornerPadded,
			},
			UI.new "UIPadding" {
				PaddingLeft = UDim.new(0, 4),
				PaddingRight = UDim.new(0, 4),
				PaddingTop = UDim.new(0, 4),
				PaddingBottom = UDim.new(0, 4),
			},
		}
		else nil

	task.spawn(function()
		if not from then
			return
		end

		-- role badge
		if badgeLabel then
			local _rank, role = _K.Auth.getRank(from)
			badgeLabel.BackgroundColor3 = Color3.fromHex(role.color)
			badgeLabel.Text = `<b>{string.upper(if role then role.name else "Unknown")}</b>`
			badgeLabel.TextColor3 = if UI.getLuminance(Color3.fromHex(role.color or Color3.new(1, 1, 1))) > 0.5
				then Color3.new()
				else Color3.new(1, 1, 1)
		end

		-- name
		local info = _K.Util.getUserInfo(from)
		displayNameLabel.Text, userNameLabel.Text =
			`<b>{name or info.DisplayName}</b>`, `<font transparency="0.5"><b>@{info.Username}</b></font>`
	end)

	return UI.new "Frame" {
		Name = "Header",
		Size = UDim2.new(),
		AutomaticSize = Enum.AutomaticSize.XY,
		BackgroundTransparency = 1,

		imageLabel,
		UI.new "Frame" {
			Name = "From",
			AutomaticSize = Enum.AutomaticSize.XY,
			BackgroundTransparency = 1,
			ClipsDescendants = true,
			Position = function()
				return UDim2.fromOffset(UI.Theme.FontSizeDouble() + 4, 0)
			end,

			UI.new "UIFlexItem" {
				FlexMode = Enum.UIFlexMode.Shrink,
			},
			UI.new "UIPadding" {
				PaddingLeft = UI.Theme.PaddingStroke,
				PaddingRight = UI.Theme.PaddingStroke,
				PaddingTop = UI.Theme.PaddingStroke,
				PaddingBottom = UI.Theme.PaddingStroke,
			},

			displayNameLabel,
			if userName then userNameLabel else nil,
			if badge then badgeLabel else nil,
		},
	}
end


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

_K.Announce = require(script:WaitForChild("Announce"))
_K.Notify = require(script:WaitForChild("Notify"))

_K.LocalPlayer = _K.Service.Players.LocalPlayer

_K.client = {
	Character = require(script:WaitForChild("Character")),
	Explorer = require(script:WaitForChild("Explorer")),
	UserFrame = require(script:WaitForChild("UserFrame")),
	TopbarPlus = nil,

	bans = _K.Flux.state(_K.Data.bans),
	logs = _K.Flux.state(_K.Data.logs),
	members = _K.Flux.state(_K.Data.members),
	servers = _K.Flux.state(_K.Data.servers),
	rank = _K.Flux.state(0),

	ready = false,
	hotkeys = {
		dashboard = { key = UI.state(Enum.KeyCode.Quote), mods = UI.state({}), callback = nil },
	},
	settings = {},

	playerPrefix = ";",
}

task.spawn(function()
	local reference = _K.Service.ReplicatedStorage:WaitForChild("TopbarPlusReference", 10)
	if reference then
		local module = reference.Value or reference.Changed:Wait()
		_K.client.TopbarPlus = require(module)
	end
end)

local function deepMergeTheme(from, to, prefix: string?)
	for key, value in from do
		value = UI.raw(value)
		if UI.isState(to[key]) then
			if prefix then
				key = prefix .. key
				_K.Data.savedSettings[key] = value
				_K.client.settings[key](value)
			else
				to[key](value)
			end
		elseif type(value) == "table" then
			deepMergeTheme(value, to[key])
		end
	end
end

function UI.changeTheme(new)
	deepMergeTheme(new, UI.Theme, "theme")
end

local closeTags = { "font", "stroke", "b", "i", "u", "s", "uc", "sc", "uppercase", "smallcaps", "mark" }
local openTags = {}
for i, tag in closeTags do
	openTags[i] = "<" .. tag
	closeTags[i] = "</" .. tag .. ">?"
end

local chatCommandThrottle
local warnPrefixes = _K.Util.String.escapePattern(";:!$~.")

local function Chatted(message: string)
	if not _K.client.ready or _K.Data.settings.chatCommands == false then
		return
	end
	local prefix = _K.getCommandPrefix()

	local commandFormat =
		`%s[^%s{_K.Process.ignoreFormat}{_K.Util.String.escapePattern(_K.Data.settings.splitKey or " ")}]`
	local commandStart
	for _, value in { prefix, unpack(_K.Data.settings.prefix) } do
		local escapedPrefix = _K.Util.String.escapePattern(value)
		commandStart = string.find(message, string.format(commandFormat, escapedPrefix, escapedPrefix))
		if commandStart then
			break
		end
	end

	if not commandStart then
		-- check for optionalPrefix
		local directCommand = _K.Registry.commands[string.lower(message)]
		if directCommand and directCommand.optionalPrefix then
			commandStart = 1
			message = prefix .. message
		else
			if _K.Data.settings.wrongPrefixWarning ~= false and #warnPrefixes > 0 then
				local test = " " .. string.lower(message)
				for alias in _K.Registry.commands do
					local i = string.find(test, `[{warnPrefixes}]{_K.Util.String.escapePattern(alias)}`)
					if i then
						warnPrefixes =
							_K.Util.String.escapePattern(warnPrefixes:gsub("%%", ""):gsub(test:sub(i, i), ""))
						task.spawn(_K.Notify, {
							From = "_K",
							Text = `<b>Invalid command prefix, try "{prefix}{alias}"</b>`,
							TextColor3 = Color3.new(1, 0, 0),
							Sound = "Call_Leave",
						})
						break
					end
					_K.Util.Defer.wait()
				end
			end
			return
		end
	end
	if not chatCommandThrottle then
		chatCommandThrottle = true

		-- trim hanging richtext tags
		message = string.sub(message, commandStart)
		for i, tag in closeTags do
			local findIndex = string.find(message, tag, 1, true)
			if not findIndex or string.find(message, openTags[i], 1, true) < findIndex then
				continue
			end
			message = string.gsub(message, tag, "")
		end

		_K.Process.runCommands(_K, _K.LocalPlayer.UserId, message)
		task.wait(0.2)
		chatCommandThrottle = nil
	end
end

if _K.Service.TextChat.ChatVersion == Enum.ChatVersion.TextChatService then
	_K.Service.TextChat.SendingMessage:Connect(function(textChatMessage: TextChatMessage)
		Chatted(_K.Util.String.unescapeRichText(textChatMessage.Text))
	end)
end

_K.Remote.Command:Connect(Chatted)

local textBoxFocused
do
	local focusedTextBoxes = {}
	textBoxFocused = function()
		if next(focusedTextBoxes) then
			return true
		end
		local ChatInputBarConfiguration = _K.Service.TextChat:FindFirstChildOfClass("ChatInputBarConfiguration")
		if ChatInputBarConfiguration and ChatInputBarConfiguration.IsFocused then
			return true
		end
		return false
	end

	_K.Service.UserInput.TextBoxFocused:Connect(function(textbox)
		if not (textbox and textbox.TextEditable) then
			return
		end
		focusedTextBoxes[textbox] = true
	end)
	_K.Service.UserInput.TextBoxFocusReleased:Connect(function(textbox)
		if not (textbox and textbox.TextEditable) then
			return
		end
		focusedTextBoxes[textbox] = nil
	end)
end

_K.Service.UserInput.InputBegan:Connect(function(input, gameProcessedEvent)
	if input.UserInputType == Enum.UserInputType.Keyboard and not textBoxFocused() then
		for keyName, data in _K.client.hotkeys do
			if
				data.callback
				and (
					input.KeyCode == data.key._value
					or (type(data.key._value) == "table" and table.find(data.key._value, input.KeyCode))
				)
			then
				local missingMod = false
				for _, modifierKey in Enum.ModifierKey:GetEnumItems() do
					local isDown = input:IsModifierKeyDown(modifierKey)
					if isDown == not data.mods._value[modifierKey.Name] then
						missingMod = true
						break
					end
				end
				if missingMod then
					return
				end
				task.defer(data.callback)
			end
		end
	end
end)

-- initial remote connection

_K.Remote.Init:Once(function(bans, logs, members, roles, reservedServers, servers, savedSettings, defaultSettings)
	-- need to improve networking performance!
	if bans then
		_K.Util.Table.merge(_K.Data.bans, bans)
		_K.client.bans(_K.Data.bans)
	end

	if logs then
		local ok, result = pcall(_K.Z.des, _K.Data.Schema.logsRemote, logs)
		if not ok then
			warn(result)
		end
		if ok and result then
			table.move(result, 1, #result, #_K.Data.logs + 1, _K.Data.logs)
			table.sort(_K.Data.logs, _K.Logger.sortTime)
		end
	end

	if reservedServers then
		_K.Util.Table.merge(_K.Data.reservedServers, reservedServers)
	end

	if servers then
		_K.Util.Table.merge(_K.Data.servers, servers)
		_K.client.servers(_K.Data.servers)
	end

	_K.Util.Table.merge(_K.Data.members, members)
	_K.client.members(_K.Data.members)

	_K.Data.roles = roles
	for role, roleData in roles do
		if not table.find(_K.Data.rolesList, roleData) then
			table.insert(_K.Data.rolesList, roleData)
		end
	end
	table.sort(_K.Data.rolesList, function(a, b)
		return a._rank < b._rank
	end)

	local theme = UI.Themes[defaultSettings.theme]
	if theme then
		for key, state in theme do
			if state._value ~= nil then
				defaultSettings["theme" .. key] = state._value
			end
		end
	end

	local merge = _K.Util.Table.merge
	_K.Data.defaultSettings = merge(table.clone(_K.Data.settings), defaultSettings)
	_K.Data.savedSettings = merge(_K.Util.Table.deepCopy(_K.Data.defaultSettings), savedSettings)
	_K.Data.settings = if _K.Data.savedSettings.useSavedSettings ~= false
		then _K.Data.savedSettings
		else _K.Data.defaultSettings

	_K.client.playerPrefix = _K.Flux.state(_K.Data.settings.prefix[1])

	for key, value in _K.Data.settings do
		if key == "useSavedSettings" then
			value = _K.Data.savedSettings.useSavedSettings
		end
		local state
		if key ~= "theme" and string.find(key :: string, "theme", 1, true) == 1 then
			state = UI.Theme[string.sub(key :: string, 6)]
			if state then
				state(value)
			end
		end
		state = state or _K.Flux.state(value)
		_K.client.settings[key] = state

		-- UI.Theme connection to theme settings
		if key == "theme" then
			state:Connect(function(value)
				local theme = UI.Themes[value]
				if theme then
					UI.changeTheme(theme)
				end
			end)
		elseif string.find(key :: string, "theme", 1, true) == 1 then
			key = string.sub(key :: string, 6)
			if UI.Theme[key] then
				state:Connect(function(value)
					UI.Theme[key](value)
				end)
			end
		elseif key == "useSavedSettings" then
			state:Connect(function(useSaved)
				local settings = if useSaved then _K.Data.savedSettings else _K.Data.defaultSettings
				_K.Data.settings = settings
				for key, value in settings do
					if key ~= "useSavedSettings" and _K.client.settings[key] then
						_K.client.settings[key](value, true)
					end
				end
			end)
		end
	end

	_K.client.CommandBar = require(script:WaitForChild("CommandBar"))
	_K.client.CommandBar:init(_K)
	_K.client.Dashboard = require(script:WaitForChild("Dashboard") :: any) -- relies on command bar
	require(script:WaitForChild("GetKA"))
	require(script:WaitForChild("Network"))

	function _K.client.rolesNotification()
		local member = _K.Data.members[tostring(_K.LocalPlayer.UserId)]
		local roleStrings = {}
		if member and member.roles then
			for _, roleId in member.roles do
				local role = _K.Data.roles[roleId]
				if role then
					table.insert(roleStrings, `<font color="{role.color}">{role.name}</font>`)
				end
			end
		end
		for _, roleId in _K.Data.settings.freeAdmin do
			if member and member.roles and table.find(member.roles, roleId) then
				continue
			end
			local role = _K.Data.roles[roleId]
			if role then
				table.insert(roleStrings, `<font color="{role.color}">{role.name}</font>`)
			end
		end
		table.insert(roleStrings, `<font color="{_K.Data.roles.everyone.color}">{_K.Data.roles.everyone.name}</font>`)
		local roleString = table.concat(roleStrings, ", ")
		local nString = if roleString:sub(1, 1):lower():match("aeiou") then "n" else ""

		_K.Notify({
			From = "_K",
			Text = `<font family="{UI.Theme.FontMono._value.Family}"><b>You're a{nString}</b> <sc>{roleString}</sc></font>\n\n<i>Click to see the commands.</i>`,
			Duration = 15,
			Activated = function()
				_K.client.commandsWindowVisible(true)
			end,
		})
	end

	if
		_K.Data.settings.joinNotificationRank
		and _K.Auth.getRank(_K.Service.Players.LocalPlayer.UserId) >= _K.Data.settings.joinNotificationRank
	then
		_K.client.rolesNotification()
	end

	do
		local vipNotificationVisible = false

		local function vipNotificationClosed()
			vipNotificationVisible = false
		end

		local function showPopup()
			_K.VIP.popup.Visible = true
			vipNotificationVisible = false
		end

		_K.client.VIPNotification = _K.Util.Function.throttle(300, function()
			if
				vipNotificationVisible
				or _K.VIP.popup.Visible
				or not _K.Data.settings.vip
				or not _K.Data.settings.chatCommandsNotification
				or (_K.LocalPlayer:GetAttribute("_KDonationLevel") or 0) > 2
			then
				return
			end

			vipNotificationVisible = true

			local initialJoin = _K.LocalPlayer:GetAttribute("_KInitialJoin")
			local line = if workspace:GetServerTimeNow() - initialJoin > 10 * 60
				then "Unlock exclusive chat commands in 10k+ experiences for 40% OFF!"
				else "Unlock exclusive chat commands in 10k+ experiences!"

			_K.Notify({
				From = "System",
				Text = `{line}\n\n<i>Click for more details.</i>`,
				Duration = 60,
				Sound = "Call_Accept",
				Activated = showPopup,
				Close = vipNotificationClosed,
			})
		end)

		_K.Remote.VIPNotification:Connect(_K.client.VIPNotification)
		_K.Hook.postCommand:Connect(_K.client.VIPNotification)
		_K.client.dashboard.Commands._instance:GetPropertyChangedSignal("Visible"):Once(_K.client.VIPNotification)
		_K.client.dashboard.Window._instance:GetPropertyChangedSignal("Visible"):Once(_K.client.VIPNotification)

		_K.VIP.popup.Parent = UI.LayerTopInset

		local connections
		local function showOnFirstOpen()
			if
				_K.Data.settings.vip
				and not vipNotificationVisible
				and (_K.LocalPlayer:GetAttribute("_KDonationLevel") or 0) < 3
			then
				_K.VIP.popup.Visible = true
				_K.Flux.cleanup(connections)
			end
		end

		connections = {
			_K.client.dashboard.Commands._instance:GetPropertyChangedSignal("Visible"):Once(showOnFirstOpen),
			_K.client.dashboard.Window._instance:GetPropertyChangedSignal("Visible"):Once(showOnFirstOpen),
		}
	end

	if savedSettings.announcement then
		local msg, from = unpack(savedSettings.announcement)
		task.spawn(_K.Announce, {
			From = from,
			Text = msg,
			Duration = 0,
		})
	end

	if _K.Data.settings.announcements then
		for _, announcement in _K.Data.settings.announcements do
			task.spawn(_K.Announce, announcement)
		end
	end

	if _K.Data.settings.notifications then
		for _, notification in _K.Data.settings.notifications do
			task.spawn(_K.Notify, notification)
		end
	end

	if not game:IsLoaded() then
		game.Loaded:Wait()
	end

	-- finally load client and shared addons
	local function loadAddons(list)
		for _, object in list do
			if object:IsA("ModuleScript") then
				task.spawn(function()
					require(object :: ModuleScript)(_K)
				end)
			else
				loadAddons(object:GetChildren())
			end
		end
	end

	loadAddons(_K.script.Shared:WaitForChild("DefaultAddons"):GetChildren())
	loadAddons(_K.script:WaitForChild("Addons"):GetChildren())
	loadAddons(_K.Service.Collection:GetTagged(_K._addonTag))

	_K.client.updateInterfaceAuth()

	_K.client.ready = true
	_K.Hook.init:Fire()

	task.spawn(function()
		local page = if _K.Auth.hasRestrictedRole(_K.LocalPlayer.UserId)
			then _K.client.dashboard.About
			else _K.client.dashboard.Market
		_K.client.dashboard.Tabs._pages:JumpTo(_K.client.dashboard.Tabs._containerCache[page._instance])
	end)
end)

_K.Remote.Init:Fire({ Attributes = { Platform = UI.Platform, SessionStart = workspace:GetServerTimeNow() } })

local lastIdle = time()
local function resetIdle()
	if time() - 30 > lastIdle then
		_K.Remote.Idled:Fire(0)
	end
end

_K.Service.Players.LocalPlayer.Idled:Connect(function(idleTime)
	lastIdle = time()
	_K.Remote.Idled:Fire(idleTime)
	task.delay(35, resetIdle)
end)


--!native
--!optimize 2
--!nolint TableOperations
--# selene: allow(multiple_statements, mixed_table)
--[[
FiOne
Copyright (C) 2021  Rerumu

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
]]
--
local bit = bit32

if not table.create then
	function table.create(_)
		return {}
	end
end

if not table.unpack then
	table.unpack = unpack
end

if not table.pack then
	function table.pack(...)
		return { n = select("#", ...), ... }
	end
end

if not table.move then
	function table.move(src, first, last, offset, dst)
		for i = 0, last - first do
			dst[offset + i] = src[first + i]
		end
	end
end

local lua_bc_to_state
local lua_wrap_state
local stm_lua_func

-- SETLIST config
local FIELDS_PER_FLUSH = 50

-- remap for better lookup
local OPCODE_RM = {
	-- level 1
	[22] = 18, -- JMP
	[31] = 8, -- FORLOOP
	[33] = 28, -- TFORLOOP
	-- level 2
	[0] = 3, -- MOVE
	[1] = 13, -- LOADK
	[2] = 23, -- LOADBOOL
	[26] = 33, -- TEST
	-- level 3
	[12] = 1, -- ADD
	[13] = 6, -- SUB
	[14] = 10, -- MUL
	[15] = 16, -- DIV
	[16] = 20, -- MOD
	[17] = 26, -- POW
	[18] = 30, -- UNM
	[19] = 36, -- NOT
	-- level 4
	[3] = 0, -- LOADNIL
	[4] = 2, -- GETUPVAL
	[5] = 4, -- GETGLOBAL
	[6] = 7, -- GETTABLE
	[7] = 9, -- SETGLOBAL
	[8] = 12, -- SETUPVAL
	[9] = 14, -- SETTABLE
	[10] = 17, -- NEWTABLE
	[20] = 19, -- LEN
	[21] = 22, -- CONCAT
	[23] = 24, -- EQ
	[24] = 27, -- LT
	[25] = 29, -- LE
	[27] = 32, -- TESTSET
	[32] = 34, -- FORPREP
	[34] = 37, -- SETLIST
	-- level 5
	[11] = 5, -- SELF
	[28] = 11, -- CALL
	[29] = 15, -- TAILCALL
	[30] = 21, -- RETURN
	[35] = 25, -- CLOSE
	[36] = 31, -- CLOSURE
	[37] = 35, -- VARARG
}

-- opcode types for getting values
local OPCODE_T = {
	[0] = "ABC",
	"ABx",
	"ABC",
	"ABC",
	"ABC",
	"ABx",
	"ABC",
	"ABx",
	"ABC",
	"ABC",
	"ABC",
	"ABC",
	"ABC",
	"ABC",
	"ABC",
	"ABC",
	"ABC",
	"ABC",
	"ABC",
	"ABC",
	"ABC",
	"ABC",
	"AsBx",
	"ABC",
	"ABC",
	"ABC",
	"ABC",
	"ABC",
	"ABC",
	"ABC",
	"ABC",
	"AsBx",
	"AsBx",
	"ABC",
	"ABC",
	"ABC",
	"ABx",
	"ABC",
}

local OPCODE_M = {
	[0] = { b = "OpArgR", c = "OpArgN" },
	{ b = "OpArgK", c = "OpArgN" },
	{ b = "OpArgU", c = "OpArgU" },
	{ b = "OpArgR", c = "OpArgN" },
	{ b = "OpArgU", c = "OpArgN" },
	{ b = "OpArgK", c = "OpArgN" },
	{ b = "OpArgR", c = "OpArgK" },
	{ b = "OpArgK", c = "OpArgN" },
	{ b = "OpArgU", c = "OpArgN" },
	{ b = "OpArgK", c = "OpArgK" },
	{ b = "OpArgU", c = "OpArgU" },
	{ b = "OpArgR", c = "OpArgK" },
	{ b = "OpArgK", c = "OpArgK" },
	{ b = "OpArgK", c = "OpArgK" },
	{ b = "OpArgK", c = "OpArgK" },
	{ b = "OpArgK", c = "OpArgK" },
	{ b = "OpArgK", c = "OpArgK" },
	{ b = "OpArgK", c = "OpArgK" },
	{ b = "OpArgR", c = "OpArgN" },
	{ b = "OpArgR", c = "OpArgN" },
	{ b = "OpArgR", c = "OpArgN" },
	{ b = "OpArgR", c = "OpArgR" },
	{ b = "OpArgR", c = "OpArgN" },
	{ b = "OpArgK", c = "OpArgK" },
	{ b = "OpArgK", c = "OpArgK" },
	{ b = "OpArgK", c = "OpArgK" },
	{ b = "OpArgR", c = "OpArgU" },
	{ b = "OpArgR", c = "OpArgU" },
	{ b = "OpArgU", c = "OpArgU" },
	{ b = "OpArgU", c = "OpArgU" },
	{ b = "OpArgU", c = "OpArgN" },
	{ b = "OpArgR", c = "OpArgN" },
	{ b = "OpArgR", c = "OpArgN" },
	{ b = "OpArgN", c = "OpArgU" },
	{ b = "OpArgU", c = "OpArgU" },
	{ b = "OpArgN", c = "OpArgN" },
	{ b = "OpArgU", c = "OpArgN" },
	{ b = "OpArgU", c = "OpArgN" },
}

-- int rd_int_basic(string src, int s, int e, int d)
-- @src - Source binary string
-- @s - Start index of a little endian integer
-- @e - End index of the integer
-- @d - Direction of the loop
local function rd_int_basic(src, s, e, d)
	local num = 0

	-- if bb[l] > 127 then -- signed negative
	-- 	num = num - 256 ^ l
	-- 	bb[l] = bb[l] - 128
	-- end

	for i = s, e, d do
		local mul = 256 ^ math.abs(i - s)

		num = num + mul * string.byte(src, i, i)
	end

	return num
end

-- float rd_flt_basic(byte f1..8)
-- @f1..4 - The 4 bytes composing a little endian float
local function rd_flt_basic(f1, f2, f3, f4)
	local sign = (-1) ^ bit.rshift(f4, 7)
	local exp = bit.rshift(f3, 7) + bit.lshift(bit.band(f4, 0x7F), 1)
	local frac = f1 + bit.lshift(f2, 8) + bit.lshift(bit.band(f3, 0x7F), 16)
	local normal = 1

	if exp == 0 then
		if frac == 0 then
			return sign * 0
		else
			normal = 0
			exp = 1
		end
	elseif exp == 0x7F then
		if frac == 0 then
			return sign * (1 / 0)
		else
			return sign * (0 / 0)
		end
	end

	return sign * 2 ^ (exp - 127) * (1 + normal / 2 ^ 23)
end

-- double rd_dbl_basic(byte f1..8)
-- @f1..8 - The 8 bytes composing a little endian double
local function rd_dbl_basic(f1, f2, f3, f4, f5, f6, f7, f8)
	local sign = (-1) ^ bit.rshift(f8, 7)
	local exp = bit.lshift(bit.band(f8, 0x7F), 4) + bit.rshift(f7, 4)
	local frac = bit.band(f7, 0x0F) * 2 ^ 48
	local normal = 1

	frac = frac + (f6 * 2 ^ 40) + (f5 * 2 ^ 32) + (f4 * 2 ^ 24) + (f3 * 2 ^ 16) + (f2 * 2 ^ 8) + f1 -- help

	if exp == 0 then
		if frac == 0 then
			return sign * 0
		else
			normal = 0
			exp = 1
		end
	elseif exp == 0x7FF then
		if frac == 0 then
			return sign * (1 / 0)
		else
			return sign * (0 / 0)
		end
	end

	return sign * 2 ^ (exp - 1023) * (normal + frac / 2 ^ 52)
end

-- int rd_int_le(string src, int s, int e)
-- @src - Source binary string
-- @s - Start index of a little endian integer
-- @e - End index of the integer
local function rd_int_le(src, s, e)
	return rd_int_basic(src, s, e - 1, 1)
end

-- int rd_int_be(string src, int s, int e)
-- @src - Source binary string
-- @s - Start index of a big endian integer
-- @e - End index of the integer
local function rd_int_be(src, s, e)
	return rd_int_basic(src, e - 1, s, -1)
end

-- float rd_flt_le(string src, int s)
-- @src - Source binary string
-- @s - Start index of little endian float
local function rd_flt_le(src, s)
	return rd_flt_basic(string.byte(src, s, s + 3))
end

-- float rd_flt_be(string src, int s)
-- @src - Source binary string
-- @s - Start index of big endian float
local function rd_flt_be(src, s)
	local f1, f2, f3, f4 = string.byte(src, s, s + 3)
	return rd_flt_basic(f4, f3, f2, f1)
end

-- double rd_dbl_le(string src, int s)
-- @src - Source binary string
-- @s - Start index of little endian double
local function rd_dbl_le(src, s)
	return rd_dbl_basic(string.byte(src, s, s + 7))
end

-- double rd_dbl_be(string src, int s)
-- @src - Source binary string
-- @s - Start index of big endian double
local function rd_dbl_be(src, s)
	local f1, f2, f3, f4, f5, f6, f7, f8 = string.byte(src, s, s + 7) -- same
	return rd_dbl_basic(f8, f7, f6, f5, f4, f3, f2, f1)
end

-- to avoid nested ifs in deserializing
local float_types = {
	[4] = { little = rd_flt_le, big = rd_flt_be },
	[8] = { little = rd_dbl_le, big = rd_dbl_be },
}

-- byte stm_byte(Stream S)
-- @S - Stream object to read from
local function stm_byte(S)
	local idx = S.index
	local bt = string.byte(S.source, idx, idx)

	S.index = idx + 1
	return bt
end

-- string stm_string(Stream S, int len)
-- @S - Stream object to read from
-- @len - Length of string being read
local function stm_string(S, len)
	local pos = S.index + len
	local str = string.sub(S.source, S.index, pos - 1)

	S.index = pos
	return str
end

-- string stm_lstring(Stream S)
-- @S - Stream object to read from
local function stm_lstring(S)
	local len = S:s_szt()
	local str

	if len ~= 0 then
		str = string.sub(stm_string(S, len), 1, -2)
	end

	return str
end

-- fn cst_int_rdr(string src, int len, fn func)
-- @len - Length of type for reader
-- @func - Reader callback
local function cst_int_rdr(len, func)
	return function(S)
		local pos = S.index + len
		local int = func(S.source, S.index, pos)
		S.index = pos

		return int
	end
end

-- fn cst_flt_rdr(string src, int len, fn func)
-- @len - Length of type for reader
-- @func - Reader callback
local function cst_flt_rdr(len, func)
	return function(S)
		local flt = func(S.source, S.index)
		S.index = S.index + len

		return flt
	end
end

local function stm_inst_list(S)
	local len = S:s_int()
	local list = table.create(len)

	for i = 1, len do
		local ins = S:s_ins()
		local op = bit.band(ins, 0x3F)
		local args = OPCODE_T[op]
		local mode = OPCODE_M[op]
		local data = { value = ins, op = OPCODE_RM[op], A = bit.band(bit.rshift(ins, 6), 0xFF) }

		if args == "ABC" then
			data.B = bit.band(bit.rshift(ins, 23), 0x1FF)
			data.C = bit.band(bit.rshift(ins, 14), 0x1FF)
			data.is_KB = mode.b == "OpArgK" and data.B > 0xFF -- post process optimization
			data.is_KC = mode.c == "OpArgK" and data.C > 0xFF

			if op == 10 then -- decode NEWTABLE array size, store it as constant value
				local e = bit.band(bit.rshift(data.B, 3), 31)
				if e == 0 then
					data.const = data.B
				else
					data.const = bit.lshift(bit.band(data.B, 7) + 8, e - 1)
				end
			end
		elseif args == "ABx" then
			data.Bx = bit.band(bit.rshift(ins, 14), 0x3FFFF)
			data.is_K = mode.b == "OpArgK"
		elseif args == "AsBx" then
			data.sBx = bit.band(bit.rshift(ins, 14), 0x3FFFF) - 131071
		end

		list[i] = data
	end

	return list
end

local function stm_const_list(S)
	local len = S:s_int()
	local list = table.create(len)

	for i = 1, len do
		local tt = stm_byte(S)
		local k

		if tt == 1 then
			k = stm_byte(S) ~= 0
		elseif tt == 3 then
			k = S:s_num()
		elseif tt == 4 then
			k = stm_lstring(S)
		end

		list[i] = k -- offset +1 during instruction decode
	end

	return list
end

local function stm_sub_list(S, src)
	local len = S:s_int()
	local list = table.create(len)

	for i = 1, len do
		list[i] = stm_lua_func(S, src) -- offset +1 in CLOSURE
	end

	return list
end

local function stm_line_list(S)
	local len = S:s_int()
	local list = table.create(len)

	for i = 1, len do
		list[i] = S:s_int()
	end

	return list
end

local function stm_loc_list(S)
	local len = S:s_int()
	local list = table.create(len)

	for i = 1, len do
		list[i] = { varname = stm_lstring(S), startpc = S:s_int(), endpc = S:s_int() }
	end

	return list
end

local function stm_upval_list(S)
	local len = S:s_int()
	local list = table.create(len)

	for i = 1, len do
		list[i] = stm_lstring(S)
	end

	return list
end

function stm_lua_func(S, psrc)
	local proto = {}
	local src = stm_lstring(S) or psrc -- source is propagated

	proto.source = src -- source name

	S:s_int() -- line defined
	S:s_int() -- last line defined

	proto.num_upval = stm_byte(S) -- num upvalues
	proto.num_param = stm_byte(S) -- num params

	proto.is_vararg = stm_byte(S) -- vararg flag
	proto.max_stack = stm_byte(S) -- max stack size

	proto.code = stm_inst_list(S)
	proto.const = stm_const_list(S)
	proto.subs = stm_sub_list(S, src)
	proto.lines = stm_line_list(S)

	stm_loc_list(S)
	stm_upval_list(S)

	-- post process optimization
	proto.needs_arg = bit.band(proto.is_vararg, 0x5) == 0x5
	for _, v in ipairs(proto.code) do
		if v.is_K then
			v.const = proto.const[v.Bx + 1] -- offset for 1 based index
		else
			if v.is_KB then
				v.const_B = proto.const[v.B - 0xFF]
			end

			if v.is_KC then
				v.const_C = proto.const[v.C - 0xFF]
			end
		end
	end

	return proto
end

function lua_bc_to_state(src)
	-- func reader
	local rdr_func

	-- header flags
	local little
	local size_int
	local size_szt
	local size_ins
	local size_num
	local flag_int

	-- stream object
	local stream = {
		-- data
		index = 1,
		source = src,
	}

	assert(stm_string(stream, 4) == "\27Lua", "invalid Lua signature")
	assert(stm_byte(stream) == 0x51, "invalid Lua version")
	assert(stm_byte(stream) == 0, "invalid Lua format")

	little = stm_byte(stream) ~= 0
	size_int = stm_byte(stream)
	size_szt = stm_byte(stream)
	size_ins = stm_byte(stream)
	size_num = stm_byte(stream)
	flag_int = stm_byte(stream) ~= 0

	rdr_func = little and rd_int_le or rd_int_be
	stream.s_int = cst_int_rdr(size_int, rdr_func)
	stream.s_szt = cst_int_rdr(size_szt, rdr_func)
	stream.s_ins = cst_int_rdr(size_ins, rdr_func)

	if flag_int then
		stream.s_num = cst_int_rdr(size_num, rdr_func)
	elseif float_types[size_num] then
		stream.s_num = cst_flt_rdr(size_num, float_types[size_num][little and "little" or "big"])
	else
		error("unsupported float size")
	end

	return stm_lua_func(stream, "@virtual")
end

local function close_lua_upvalues(list, index)
	for i, uv in pairs(list) do
		if uv.index >= index then
			uv.value = uv.store[uv.index] -- store value
			uv.store = uv
			uv.index = "value" -- self reference
			list[i] = nil
		end
	end
end

local function open_lua_upvalue(list, index, memory)
	local prev = list[index]

	if not prev then
		prev = { index = index, store = memory }
		list[index] = prev
	end

	return prev
end

local function on_lua_error(failed, err)
	local src = failed.source
	local line = failed.lines[failed.pc - 1]

	error(string.format("%s:%i: %s", src, line, err), 0)
end

local function run_lua_func(state, env, upvals)
	local code = state.code
	local subs = state.subs
	local vararg = state.vararg

	local top_index = -1
	local open_list = {}
	local memory = state.memory
	local pc = state.pc

	while true do
		local inst = code[pc]
		local op = inst.op
		pc = pc + 1

		if op < 18 then
			if op < 8 then
				if op < 3 then
					if op < 1 then
						--[[LOADNIL]]
						for i = inst.A, inst.B do
							memory[i] = nil
						end
					elseif op > 1 then
						--[[GETUPVAL]]
						local uv = upvals[inst.B]

						memory[inst.A] = uv.store[uv.index]
					else
						--[[ADD]]
						local lhs, rhs

						if inst.is_KB then
							lhs = inst.const_B
						else
							lhs = memory[inst.B]
						end

						if inst.is_KC then
							rhs = inst.const_C
						else
							rhs = memory[inst.C]
						end

						memory[inst.A] = lhs + rhs
					end
				elseif op > 3 then
					if op < 6 then
						if op > 4 then
							--[[SELF]]
							local A = inst.A
							local B = inst.B
							local index

							if inst.is_KC then
								index = inst.const_C
							else
								index = memory[inst.C]
							end

							memory[A + 1] = memory[B]
							memory[A] = memory[B][index]
						else
							--[[GETGLOBAL]]
							memory[inst.A] = env[inst.const]
						end
					elseif op > 6 then
						--[[GETTABLE]]
						local index

						if inst.is_KC then
							index = inst.const_C
						else
							index = memory[inst.C]
						end

						memory[inst.A] = memory[inst.B][index]
					else
						--[[SUB]]
						local lhs, rhs

						if inst.is_KB then
							lhs = inst.const_B
						else
							lhs = memory[inst.B]
						end

						if inst.is_KC then
							rhs = inst.const_C
						else
							rhs = memory[inst.C]
						end

						memory[inst.A] = lhs - rhs
					end
				else --[[MOVE]]
					memory[inst.A] = memory[inst.B]
				end
			elseif op > 8 then
				if op < 13 then
					if op < 10 then
						--[[SETGLOBAL]]
						env[inst.const] = memory[inst.A]
					elseif op > 10 then
						if op < 12 then
							--[[CALL]]
							local A = inst.A
							local B = inst.B
							local C = inst.C
							local params

							if B == 0 then
								params = top_index - A
							else
								params = B - 1
							end

							local ret_list = table.pack(memory[A](table.unpack(memory, A + 1, A + params)))
							local ret_num = ret_list.n

							if C == 0 then
								top_index = A + ret_num - 1
							else
								ret_num = C - 1
							end

							table.move(ret_list, 1, ret_num, A, memory)
						else
							--[[SETUPVAL]]
							local uv = upvals[inst.B]

							uv.store[uv.index] = memory[inst.A]
						end
					else
						--[[MUL]]
						local lhs, rhs

						if inst.is_KB then
							lhs = inst.const_B
						else
							lhs = memory[inst.B]
						end

						if inst.is_KC then
							rhs = inst.const_C
						else
							rhs = memory[inst.C]
						end

						memory[inst.A] = lhs * rhs
					end
				elseif op > 13 then
					if op < 16 then
						if op > 14 then
							--[[TAILCALL]]
							local A = inst.A
							local B = inst.B
							local params

							if B == 0 then
								params = top_index - A
							else
								params = B - 1
							end

							close_lua_upvalues(open_list, 0)

							return memory[A](table.unpack(memory, A + 1, A + params))
						else
							--[[SETTABLE]]
							local index, value

							if inst.is_KB then
								index = inst.const_B
							else
								index = memory[inst.B]
							end

							if inst.is_KC then
								value = inst.const_C
							else
								value = memory[inst.C]
							end

							memory[inst.A][index] = value
						end
					elseif op > 16 then
						--[[NEWTABLE]]
						memory[inst.A] = table.create(inst.const) -- inst.const contains array size
					else
						--[[DIV]]
						local lhs, rhs

						if inst.is_KB then
							lhs = inst.const_B
						else
							lhs = memory[inst.B]
						end

						if inst.is_KC then
							rhs = inst.const_C
						else
							rhs = memory[inst.C]
						end

						memory[inst.A] = lhs / rhs
					end
				else
					--[[LOADK]]
					memory[inst.A] = inst.const
				end
			else
				--[[FORLOOP]]
				local A = inst.A
				local step = memory[A + 2]
				local index = memory[A] + step
				local limit = memory[A + 1]
				local loops

				if step >= 0 then
					loops = index <= limit
				else
					loops = index >= limit
				end

				if loops then
					memory[A] = index
					memory[A + 3] = index
					pc = pc + inst.sBx
				end
			end
		elseif op > 18 then
			if op < 28 then
				if op < 23 then
					if op < 20 then
						--[[LEN]]
						memory[inst.A] = #memory[inst.B]
					elseif op > 20 then
						if op < 22 then
							--[[RETURN]]
							local A = inst.A
							local B = inst.B
							local len

							if B == 0 then
								len = top_index - A + 1
							else
								len = B - 1
							end

							close_lua_upvalues(open_list, 0)

							return table.unpack(memory, A, A + len - 1)
						else
							--[[CONCAT]]
							local B, C = inst.B, inst.C
							local success, str = pcall(table.concat, memory, "", B, C)

							if not success then
								str = memory[B]

								for i = B + 1, C do
									str = str .. memory[i]
								end
							end

							memory[inst.A] = str
						end
					else
						--[[MOD]]
						local lhs, rhs

						if inst.is_KB then
							lhs = inst.const_B
						else
							lhs = memory[inst.B]
						end

						if inst.is_KC then
							rhs = inst.const_C
						else
							rhs = memory[inst.C]
						end

						memory[inst.A] = lhs % rhs
					end
				elseif op > 23 then
					if op < 26 then
						if op > 24 then
							--[[CLOSE]]
							close_lua_upvalues(open_list, inst.A)
						else
							--[[EQ]]
							local lhs, rhs

							if inst.is_KB then
								lhs = inst.const_B
							else
								lhs = memory[inst.B]
							end

							if inst.is_KC then
								rhs = inst.const_C
							else
								rhs = memory[inst.C]
							end

							if (lhs == rhs) == (inst.A ~= 0) then
								pc = pc + code[pc].sBx
							end

							pc = pc + 1
						end
					elseif op > 26 then
						--[[LT]]
						local lhs, rhs

						if inst.is_KB then
							lhs = inst.const_B
						else
							lhs = memory[inst.B]
						end

						if inst.is_KC then
							rhs = inst.const_C
						else
							rhs = memory[inst.C]
						end

						if (lhs < rhs) == (inst.A ~= 0) then
							pc = pc + code[pc].sBx
						end

						pc = pc + 1
					else
						--[[POW]]
						local lhs, rhs

						if inst.is_KB then
							lhs = inst.const_B
						else
							lhs = memory[inst.B]
						end

						if inst.is_KC then
							rhs = inst.const_C
						else
							rhs = memory[inst.C]
						end

						memory[inst.A] = lhs ^ rhs
					end
				else
					--[[LOADBOOL]]
					memory[inst.A] = inst.B ~= 0

					if inst.C ~= 0 then
						pc = pc + 1
					end
				end
			elseif op > 28 then
				if op < 33 then
					if op < 30 then
						--[[LE]]
						local lhs, rhs

						if inst.is_KB then
							lhs = inst.const_B
						else
							lhs = memory[inst.B]
						end

						if inst.is_KC then
							rhs = inst.const_C
						else
							rhs = memory[inst.C]
						end

						if (lhs <= rhs) == (inst.A ~= 0) then
							pc = pc + code[pc].sBx
						end

						pc = pc + 1
					elseif op > 30 then
						if op < 32 then
							--[[CLOSURE]]
							local sub = subs[inst.Bx + 1] -- offset for 1 based index
							local nups = sub.num_upval
							local uvlist

							if nups ~= 0 then
								uvlist = table.create(nups - 1)

								for i = 1, nups do
									local pseudo = code[pc + i - 1]

									if pseudo.op == OPCODE_RM[0] then -- @MOVE
										uvlist[i - 1] = open_lua_upvalue(open_list, pseudo.B, memory)
									elseif pseudo.op == OPCODE_RM[4] then -- @GETUPVAL
										uvlist[i - 1] = upvals[pseudo.B]
									end
								end

								pc = pc + nups
							end

							memory[inst.A] = lua_wrap_state(sub, env, uvlist)
						else
							--[[TESTSET]]
							local A = inst.A
							local B = inst.B

							if (not memory[B]) ~= (inst.C ~= 0) then
								memory[A] = memory[B]
								pc = pc + code[pc].sBx
							end
							pc = pc + 1
						end
					else
						--[[UNM]]
						memory[inst.A] = -memory[inst.B]
					end
				elseif op > 33 then
					if op < 36 then
						if op > 34 then
							--[[VARARG]]
							local A = inst.A
							local len = inst.B

							if len == 0 then
								len = vararg.len
								top_index = A + len - 1
							end

							table.move(vararg.list, 1, len, A, memory)
						else
							--[[FORPREP]]
							local A = inst.A
							local init, limit, step

							init = assert(tonumber(memory[A]), "`for` initial value must be a number")
							limit = assert(tonumber(memory[A + 1]), "`for` limit must be a number")
							step = assert(tonumber(memory[A + 2]), "`for` step must be a number")

							memory[A] = init - step
							memory[A + 1] = limit
							memory[A + 2] = step

							pc = pc + inst.sBx
						end
					elseif op > 36 then
						--[[SETLIST]]
						local A = inst.A
						local C = inst.C
						local len = inst.B
						local tab = memory[A]
						local offset

						if len == 0 then
							len = top_index - A
						end

						if C == 0 then
							C = inst[pc].value
							pc = pc + 1
						end

						offset = (C - 1) * FIELDS_PER_FLUSH

						table.move(memory, A + 1, A + len, offset + 1, tab)
					else
						--[[NOT]]
						memory[inst.A] = not memory[inst.B]
					end
				else
					--[[TEST]]
					if (not memory[inst.A]) ~= (inst.C ~= 0) then
						pc = pc + code[pc].sBx
					end
					pc = pc + 1
				end
			else
				--[[TFORLOOP]]
				local A = inst.A
				local base = A + 3

				local vals = { memory[A](memory[A + 1], memory[A + 2]) }

				table.move(vals, 1, inst.C, base, memory)

				if memory[base] ~= nil then
					memory[A + 2] = memory[base]
					pc = pc + code[pc].sBx
				end

				pc = pc + 1
			end
		else
			--[[JMP]]
			pc = pc + inst.sBx
		end

		state.pc = pc
	end
end

function lua_wrap_state(proto, env, upval)
	local function wrapped(...)
		local passed = table.pack(...)
		local memory = table.create(proto.max_stack)
		local vararg = { len = 0, list = {} }

		table.move(passed, 1, proto.num_param, 0, memory)

		if proto.num_param < passed.n then
			local start = proto.num_param + 1
			local len = passed.n - proto.num_param

			vararg.len = len
			table.move(passed, start, start + len - 1, 1, vararg.list)
		end

		if proto.needs_arg then
			memory[proto.num_param] = { n = vararg.len, table.unpack(vararg.list, 1, vararg.len) }
		end

		local state = { vararg = vararg, memory = memory, code = proto.code, subs = proto.subs, pc = 1 }

		local result = table.pack(pcall(run_lua_func, state, env, upval))

		if result[1] then
			return table.unpack(result, 2, result.n)
		else
			local failed = { pc = state.pc, source = proto.source, lines = proto.lines }

			on_lua_error(failed, result[2])

			return
		end
	end

	return wrapped
end

return setmetatable({
	bc_to_state = lua_bc_to_state,
	wrap_state = lua_wrap_state,
	OPCODE_RM = OPCODE_RM,
	OPCODE_T = OPCODE_T,
	OPCODE_M = OPCODE_M,
}, {
	__call = function(_, BCode, Env)
		return lua_wrap_state(lua_bc_to_state(BCode), Env or {})
	end,
})


--# selene: allow(incorrect_standard_library_use, multiple_statements, shadowing, unused_variable, empty_if, divide_by_zero, unbalanced_assignments)
--[[--------------------------------------------------------------------

  ldump.lua
  Save precompiled Lua chunks
  This file is part of Yueliang.

  Copyright (c) 2006 Kein-Hong Man <khman@users.sf.net>
  The COPYRIGHT file describes the conditions
  under which this software may be distributed.

  See the ChangeLog for more information.

----------------------------------------------------------------------]]

--[[--------------------------------------------------------------------
-- Notes:
-- * WARNING! byte order (little endian) and data type sizes for header
--   signature values hard-coded; see luaU:header
-- * chunk writer generators are included, see below
-- * one significant difference is that instructions are still in table
--   form (with OP/A/B/C/Bx fields) and luaP:Instruction() is needed to
--   convert them into 4-char strings
--
-- Not implemented:
-- * DumpVar, DumpMem has been removed
-- * DumpVector folded into folded into DumpDebug, DumpCode
--
-- Added:
-- * for convenience, the following two functions have been added:
--   luaU:make_setS: create a chunk writer that writes to a string
--   luaU:make_setF: create a chunk writer that writes to a file
--   (lua.h contains a typedef for lua_Writer/lua_Chunkwriter, and
--    a Lua-based implementation exists, writer() in lstrlib.c)
-- * luaU:ttype(o) (from lobject.h)
-- * for converting number types to its binary equivalent:
--   luaU:from_double(x): encode double value for writing
--   luaU:from_int(x): encode integer value for writing
--     (error checking is limited for these conversion functions)
--     (double conversion does not support denormals or NaNs)
--
-- Changed in 5.1.x:
-- * the dumper was mostly rewritten in Lua 5.1.x, so notes on the
--   differences between 5.0.x and 5.1.x is limited
-- * LUAC_VERSION bumped to 0x51, LUAC_FORMAT added
-- * developer is expected to adjust LUAC_FORMAT in order to identify
--   non-standard binary chunk formats
-- * header signature code is smaller, has been simplified, and is
--   tested as a single unit; its logic is shared with the undumper
-- * no more endian conversion, invalid endianness mean rejection
-- * opcode field sizes are no longer exposed in the header
-- * code moved to front of a prototype, followed by constants
-- * debug information moved to the end of the binary chunk, and the
--   relevant functions folded into a single function
-- * luaU:dump returns a writer status code
-- * chunk writer now implements status code because dumper uses it
-- * luaU:endianness removed
----------------------------------------------------------------------]]

--requires luaP
local luaU = {}
local luaP = require(script.Parent.LuaP)

-- mark for precompiled code ('<esc>Lua') (from lua.h)
luaU.LUA_SIGNATURE = "\27Lua"

-- constants used by dumper (from lua.h)
luaU.LUA_TNUMBER = 3
luaU.LUA_TSTRING = 4
luaU.LUA_TNIL = 0
luaU.LUA_TBOOLEAN = 1
luaU.LUA_TNONE = -1

-- constants for header of binary files (from lundump.h)
luaU.LUAC_VERSION = 0x51 -- this is Lua 5.1
luaU.LUAC_FORMAT = 0 -- this is the official format
luaU.LUAC_HEADERSIZE = 12 -- size of header of binary files

--[[--------------------------------------------------------------------
-- Additional functions to handle chunk writing
-- * to use make_setS and make_setF, see test_ldump.lua elsewhere
----------------------------------------------------------------------]]

------------------------------------------------------------------------
-- create a chunk writer that writes to a string
-- * returns the writer function and a table containing the string
-- * to get the final result, look in buff.data
------------------------------------------------------------------------
function luaU:make_setS()
	local buff = {}
	buff.data = ""
	local writer = function(s, buff) -- chunk writer
		if not s then
			return 0
		end
		buff.data = buff.data .. s
		return 0
	end
	return writer, buff
end

------------------------------------------------------------------------
-- create a chunk writer that writes to a file
-- * returns the writer function and a table containing the file handle
-- * if a nil is passed, then writer should close the open file
------------------------------------------------------------------------

--[[
function luaU:make_setF(filename)
  local buff = {}
        buff.h = io.open(filename, "wb")
  if not buff.h then return nil end
  local writer =
    function(s, buff)  -- chunk writer
      if not buff.h then return 0 end
      if not s then
        if buff.h:close() then return 0 end
      else
        if buff.h:write(s) then return 0 end
      end
      return 1
    end
  return writer, buff
end--]]

------------------------------------------------------------------------
-- works like the lobject.h version except that TObject used in these
-- scripts only has a 'value' field, no 'tt' field (native types used)
------------------------------------------------------------------------
function luaU:ttype(o)
	local tt = type(o.value)
	if tt == "number" then
		return self.LUA_TNUMBER
	elseif tt == "string" then
		return self.LUA_TSTRING
	elseif tt == "nil" then
		return self.LUA_TNIL
	elseif tt == "boolean" then
		return self.LUA_TBOOLEAN
	else
		return self.LUA_TNONE -- the rest should not appear
	end
end

-----------------------------------------------------------------------
-- converts a IEEE754 double number to an 8-byte little-endian string
-- * luaU:from_double() and luaU:from_int() are adapted from ChunkBake
-- * supports +/- Infinity, but not denormals or NaNs
-----------------------------------------------------------------------
function luaU:from_double(x)
	local function grab_byte(v)
		local c = v % 256
		return (v - c) / 256, string.char(c)
	end
	local sign = 0
	if x < 0 then
		sign = 1
		x = -x
	end
	local mantissa, exponent = math.frexp(x)
	if x == 0 then -- zero
		mantissa, exponent = 0, 0
	elseif x == 1 / 0 then
		mantissa, exponent = 0, 2047
	else
		mantissa = (mantissa * 2 - 1) * math.ldexp(0.5, 53)
		exponent = exponent + 1022
	end
	local v, byte = "", nil -- convert to bytes
	x = math.floor(mantissa)
	for i = 1, 6 do
		x, byte = grab_byte(x)
		v = v .. byte -- 47:0
	end
	x, byte = grab_byte(exponent * 16 + x)
	v = v .. byte -- 55:48
	x, byte = grab_byte(sign * 128 + x)
	v = v .. byte -- 63:56
	return v
end

-----------------------------------------------------------------------
-- converts a number to a little-endian 32-bit integer string
-- * input value assumed to not overflow, can be signed/unsigned
-----------------------------------------------------------------------
function luaU:from_int(x)
	local v = ""
	x = math.floor(x)
	if x < 0 then
		x = 4294967296 + x
	end -- ULONG_MAX+1
	for i = 1, 4 do
		local c = x % 256
		v = v .. string.char(c)
		x = math.floor(x / 256)
	end
	return v
end

--[[--------------------------------------------------------------------
-- Functions to make a binary chunk
-- * many functions have the size parameter removed, since output is
--   in the form of a string and some sizes are implicit or hard-coded
----------------------------------------------------------------------]]

--[[--------------------------------------------------------------------
-- struct DumpState:
--   L  -- lua_State (not used in this script)
--   writer  -- lua_Writer (chunk writer function)
--   data  -- void* (chunk writer context or data already written)
--   strip  -- if true, don't write any debug information
--   status  -- if non-zero, an error has occured
----------------------------------------------------------------------]]

------------------------------------------------------------------------
-- dumps a block of bytes
-- * lua_unlock(D.L), lua_lock(D.L) unused
------------------------------------------------------------------------
function luaU:DumpBlock(b, D)
	if D.status == 0 then
		-- lua_unlock(D->L);
		D.status = D.write(b, D.data)
		-- lua_lock(D->L);
	end
end

------------------------------------------------------------------------
-- dumps a char
------------------------------------------------------------------------
function luaU:DumpChar(y, D)
	self:DumpBlock(string.char(y), D)
end

------------------------------------------------------------------------
-- dumps a 32-bit signed or unsigned integer (for int) (hard-coded)
------------------------------------------------------------------------
function luaU:DumpInt(x, D)
	self:DumpBlock(self:from_int(x), D)
end

------------------------------------------------------------------------
-- dumps a lua_Number (hard-coded as a double)
------------------------------------------------------------------------
function luaU:DumpNumber(x, D)
	self:DumpBlock(self:from_double(x), D)
end

------------------------------------------------------------------------
-- dumps a Lua string (size type is hard-coded)
------------------------------------------------------------------------
function luaU:DumpString(s, D)
	if s == nil then
		self:DumpInt(0, D)
	else
		s = s .. "\0" -- include trailing '\0'
		self:DumpInt(#s, D)
		self:DumpBlock(s, D)
	end
end

------------------------------------------------------------------------
-- dumps instruction block from function prototype
------------------------------------------------------------------------
function luaU:DumpCode(f, D)
	local n = f.sizecode
	--was DumpVector
	self:DumpInt(n, D)
	for i = 0, n - 1 do
		self:DumpBlock(luaP:Instruction(f.code[i]), D)
	end
end

------------------------------------------------------------------------
-- dump constant pool from function prototype
-- * bvalue(o), nvalue(o) and rawtsvalue(o) macros removed
------------------------------------------------------------------------
function luaU:DumpConstants(f, D)
	local n = f.sizek
	self:DumpInt(n, D)
	for i = 0, n - 1 do
		local o = f.k[i] -- TValue
		local tt = self:ttype(o)
		self:DumpChar(tt, D)
		if tt == self.LUA_TNIL then
		elseif tt == self.LUA_TBOOLEAN then
			self:DumpChar(o.value and 1 or 0, D)
		elseif tt == self.LUA_TNUMBER then
			self:DumpNumber(o.value, D)
		elseif tt == self.LUA_TSTRING then
			self:DumpString(o.value, D)
		else
			--lua_assert(0)  -- cannot happen
		end
	end
	n = f.sizep
	self:DumpInt(n, D)
	for i = 0, n - 1 do
		self:DumpFunction(f.p[i], f.source, D)
	end
end

------------------------------------------------------------------------
-- dump debug information
------------------------------------------------------------------------
function luaU:DumpDebug(f, D)
	local n
	n = D.strip and 0 or f.sizelineinfo -- dump line information
	--was DumpVector
	self:DumpInt(n, D)
	for i = 0, n - 1 do
		self:DumpInt(f.lineinfo[i], D)
	end
	n = D.strip and 0 or f.sizelocvars -- dump local information
	self:DumpInt(n, D)
	for i = 0, n - 1 do
		self:DumpString(f.locvars[i].varname, D)
		self:DumpInt(f.locvars[i].startpc, D)
		self:DumpInt(f.locvars[i].endpc, D)
	end
	n = D.strip and 0 or f.sizeupvalues -- dump upvalue information
	self:DumpInt(n, D)
	for i = 0, n - 1 do
		self:DumpString(f.upvalues[i], D)
	end
end

------------------------------------------------------------------------
-- dump child function prototypes from function prototype
------------------------------------------------------------------------
function luaU:DumpFunction(f, p, D)
	local source = f.source
	if source == p or D.strip then
		source = nil
	end
	self:DumpString(source, D)
	self:DumpInt(f.lineDefined, D)
	self:DumpInt(f.lastlinedefined, D)
	self:DumpChar(f.nups, D)
	self:DumpChar(f.numparams, D)
	self:DumpChar(f.is_vararg, D)
	self:DumpChar(f.maxstacksize, D)
	self:DumpCode(f, D)
	self:DumpConstants(f, D)
	self:DumpDebug(f, D)
end

------------------------------------------------------------------------
-- dump Lua header section (some sizes hard-coded)
------------------------------------------------------------------------
function luaU:DumpHeader(D)
	local h = self:header()
	assert(#h == self.LUAC_HEADERSIZE) -- fixed buffer now an assert
	self:DumpBlock(h, D)
end

------------------------------------------------------------------------
-- make header (from lundump.c)
-- returns the header string
------------------------------------------------------------------------
function luaU:header()
	local x = 1
	return self.LUA_SIGNATURE
		.. string.char(
			self.LUAC_VERSION,
			self.LUAC_FORMAT,
			x, -- endianness (1=little)
			4, -- sizeof(int)
			4, -- sizeof(size_t)
			4, -- sizeof(Instruction)
			8, -- sizeof(lua_Number)
			0
		) -- is lua_Number integral?
end

------------------------------------------------------------------------
-- dump Lua function as precompiled chunk
-- (lua_State* L, const Proto* f, lua_Writer w, void* data, int strip)
-- * w, data are created from make_setS, make_setF
------------------------------------------------------------------------
function luaU:dump(L, f, w, data, strip)
	local D = {} -- DumpState
	D.L = L
	D.write = w
	D.data = data
	D.strip = strip
	D.status = 0
	self:DumpHeader(D)
	self:DumpFunction(f, nil, D)
	-- added: for a chunk writer writing to a file, this final call with
	-- nil data is to indicate to the writer to close the file
	D.write(nil, D.data)
	return D.status
end

return luaU