--// Written by depso
--// MIT License
--// Copyright (c) 2024 Depso

local ImGui = {
	Animations = {
		Buttons = {
			MouseEnter = {
				BackgroundTransparency = 0.5,
			},
			MouseLeave = {
				BackgroundTransparency = 0.7,
			}
		},
		Tabs = {
			MouseEnter = {
				BackgroundTransparency = 0.5,
			},
			MouseLeave = {
				BackgroundTransparency = 1,
			}
		},
		Inputs = {
			MouseEnter = {
				BackgroundTransparency = 0,
			},
			MouseLeave = {
				BackgroundTransparency = 0.5,
			}
		},
		WindowBorder = {
			Selected = {
				Transparency = 0,
				Thickness = 1
			},
			Deselected = {
				Transparency = 0.7,
				Thickness = 1
			}
		},
	},

	Windows = {},
	Animation = TweenInfo.new(0.1),
	UIAssetId = "rbxassetid://76246418997296"
}

--// Universal functions
local NullFunction = function() end
local CloneRef = cloneref or function(_)return _ end
local function GetService(...): ServiceProvider
	return CloneRef(game:GetService(...))
end

function ImGui:Warn(...)
	if self.NoWarnings then return end
	return warn("[IMGUI]", ...)
end

--// Services
local TweenService: TweenService = GetService("TweenService")
local UserInputService: UserInputService = GetService("UserInputService")
local Players: Players = GetService("Players")
local CoreGui = GetService("CoreGui")
local RunService: RunService = GetService("RunService")

--// LocalPlayer
local LocalPlayer = Players.LocalPlayer
local PlayerGui = LocalPlayer.PlayerGui
local Mouse = LocalPlayer:GetMouse()

--// ImGui Config
local IsStudio = RunService:IsStudio()
ImGui.NoWarnings = not IsStudio

--// Prefabs
function ImGui:FetchUI()
	--// Cache check
	local CacheName = "DepsoImGui"
	if _G[CacheName] then
		self:Warn("Prefabs loaded from Cache")
		return _G[CacheName]
	end

	local UI = nil

	--// Universal
	if not IsStudio then
		local UIAssetId = ImGui.UIAssetId
		UI = game:GetObjects(UIAssetId)[1]
	else --// Studio
		local UIName = "DepsoImGui"
		UI = PlayerGui:FindFirstChild(UIName) or script.DepsoImGui
	end

	_G[CacheName] = UI
	return UI
end

local UI = ImGui:FetchUI()
local Prefabs = UI.Prefabs
ImGui.Prefabs = Prefabs
Prefabs.Visible = false

--// Styles
local AddionalStyles = {
	[{
		Name="Border"
	}] = function(GuiObject: GuiObject, Value, Class)
		local Outline = GuiObject:FindFirstChildOfClass("UIStroke")
		if not Outline then return end

		local BorderThickness = Class.BorderThickness
		if BorderThickness then
			Outline.Thickness = BorderThickness
		end

		Outline.Enabled = Value
	end,

	[{
		Name="Ratio"
	}] = function(GuiObject: GuiObject, Value, Class)
		local RatioAxis = Class.RatioAxis or "Height"
		local AspectRatio = Class.Ratio or 4/3
		local AspectType = Class.AspectType or Enum.AspectType.ScaleWithParentSize

		local Ratio = GuiObject:FindFirstChildOfClass("UIAspectRatioConstraint")
		if not Ratio then
			Ratio = ImGui:CreateInstance("UIAspectRatioConstraint", GuiObject)
		end

		Ratio.DominantAxis = Enum.DominantAxis[RatioAxis]
		Ratio.AspectType = AspectType
		Ratio.AspectRatio = AspectRatio
	end,

	[{
		Name="CornerRadius",
		Recursive=true
	}] = function(GuiObject: GuiObject, Value, Class)
		local UICorner = GuiObject:FindFirstChildOfClass("UICorner")
		if not UICorner then
			UICorner = ImGui:CreateInstance("UICorner", GuiObject)
		end

		UICorner.CornerRadius = Class.CornerRadius
	end,

	[{
		Name="Label"
	}] = function(GuiObject: GuiObject, Value, Class)
		local Label = GuiObject:FindFirstChild("Label")
		if not Label then return end

		Label.Text = Class.Label
		function Class:SetLabel(Text)
			Label.Text = Text
			return Class
		end
	end,

	[{
		Name="NoGradient",
		Aliases = {"NoGradientAll"},
		Recursive=true
	}] = function(GuiObject: GuiObject, Value, Class)
		local UIGradient = GuiObject:FindFirstChildOfClass("UIGradient")
		if not UIGradient then return end
		UIGradient.Enabled = not Value
	end,

	--// Addional functions for classes
	[{
		Name="Callback"
	}] = function(GuiObject: GuiObject, Value, Class)
		function Class:SetCallback(NewCallback)
			Class.Callback = NewCallback
			return Class
		end
		function Class:FireCallback(NewCallback)
			return Class.Callback(GuiObject)
		end
	end,

	[{
		Name="Value"
	}] = function(GuiObject: GuiObject, Value, Class)
		function Class:GetValue()
			return Class.Value
		end
	end,
}

function ImGui:GetName(Name: string)
	local Format = "%s_"
	return Format:format(Name)
end

function ImGui:CreateInstance(Class, Parent, Properties)
	local Instance = Instance.new(Class, Parent)
	for Key, Value in next, Properties or {} do
		Instance[Key] = Value
	end
	return Instance
end

function ImGui:ApplyColors(ColorOverwrites, GuiObject: GuiObject, ElementType: string)
	for Info, Value in next, ColorOverwrites do
		local Key = Info
		local Recursive = false

		if typeof(Info) == "table" then
			Key = Info.Name or ""
			Recursive = Info.Recursive or false
		end

		--// Child object
		if typeof(Value) == "table" then
			local Element = GuiObject:FindFirstChild(Key, Recursive)

			if not Element then
				if ElementType == "Window" then
					Element = GuiObject.Content:FindFirstChild(Key, Recursive)
					if not Element then continue end
				else
					warn(Key, "was not found in", GuiObject)
					warn("Table:", Value)

					continue
				end
			end

			ImGui:ApplyColors(Value, Element)
			continue
		end

		--// Set property
		GuiObject[Key] = Value
	end
end

function ImGui:CheckStyles(GuiObject: GuiObject, Class, Colors)
	--// Addional styles
	for Info, Callback in next, AddionalStyles do
		local Value = Class[Info.Name]
		local Aliases = Info.Aliases

		if Aliases and not Value then
			for _, Alias in Info.Aliases do
				Value = Class[Alias]
				if Value then break end
			end
		end
		if Value == nil then continue end

		--// Stylise children
		Callback(GuiObject, Value, Class)
		if Info.Recursive then
			for _, Child in next, GuiObject:GetChildren() do
				Callback(Child, Value, Class)
			end
		end
	end

	--// Label functions/Styliser
	local ElementType = GuiObject.Name
	GuiObject.Name = self:GetName(ElementType)

	--// Apply Colors
	local Colors = Colors or {}
	local ColorOverwrites = Colors[ElementType]

	if ColorOverwrites then
		ImGui:ApplyColors(ColorOverwrites, GuiObject, ElementType)
	end

	--// Set properties
	for Key, Value in next, Class do
		pcall(function() --// If the property does not exist
			GuiObject[Key] = Value
		end)
	end
end

function ImGui:MergeMetatables(Class, Instance: GuiObject)
	local Metadata = {}
	Metadata.__index = function(self, Key)
		local suc, Value = pcall(function()
			local Value = Instance[Key]
			if typeof(Value) == "function" then
				return function(...)
					return Value(Instance, ...)
				end
			end
			return Value
		end)
		return suc and Value or Class[Key]
	end

	Metadata.__newindex = function(self, Key, Value)
		local Key2 = Class[Key]
		if Key2 ~= nil or typeof(Value) == "function" then
			Class[Key] = Value
		else
			Instance[Key] = Value
		end
	end

	return setmetatable({}, Metadata)
end

function ImGui:Concat(Table, Separator: " ")
	local Concatenated = ""
	for Index, String in next, Table do
		Concatenated ..= tostring(String) .. (Index ~= #Table and Separator or "")
	end
	return Concatenated
end

function ImGui:ContainerClass(Frame: Frame, Class, Window)
	local ContainerClass = Class or {}
	local WindowConfig = ImGui.Windows[Window]

	function ContainerClass:NewInstance(Instance: Frame, Class, Parent)
		--// Config
		Class = Class or {}

		--// Set Parent
		Instance.Parent = Parent or Frame
		Instance.Visible = true

		--// TODO
		if WindowConfig.NoGradientAll then
			Class.NoGradient = true
		end

		local Colors = WindowConfig.Colors
		ImGui:CheckStyles(Instance, Class, Colors)

		--// External callback check
		if Class.NewInstanceCallback then
			Class.NewInstanceCallback(Instance)
		end

		--// Merge the class with the properties of the instance
		return ImGui:MergeMetatables(Class, Instance)
	end

	function ContainerClass:Button(Config)
		Config = Config or {}
		local Button = Prefabs.Button:Clone()
		local ObjectClass = self:NewInstance(Button, Config)

		local function Callback(...)
			local func = Config.Callback or NullFunction
			return func(ObjectClass, ...)
		end
		Button.Activated:Connect(Callback)

		--// Apply animations
		ImGui:ApplyAnimations(Button, "Buttons")
		return ObjectClass
	end

	function ContainerClass:Image(Config)
		Config = Config or {}
		local Image = Prefabs.Image:Clone()

		--// Check for rbxassetid
		if tonumber(Config.Image) then
			Config.Image = `rbxassetid://{Config.Image}`
		end

		local ObjectClass = self:NewInstance(Image, Config)
		local function Callback(...)
			local func = Config.Callback or NullFunction
			return func(ObjectClass, ...)
		end
		Image.Activated:Connect(Callback)

		--// Apply animations
		ImGui:ApplyAnimations(Image, "Buttons")
		return ObjectClass
	end

	function ContainerClass:ScrollingBox(Config)
		Config = Config or {}
		local Box = Prefabs.ScrollBox:Clone()
		local ContainClass = ImGui:ContainerClass(Box, Config, Window)
		return self:NewInstance(Box, ContainClass)
	end

	function ContainerClass:Label(Config)
		Config = Config or {}
		local Label = Prefabs.Label:Clone()
		return self:NewInstance(Label, Config)
	end

	function ContainerClass:Checkbox(Config)
		Config = Config or {}
		local IsRadio = Config.IsRadio

		local CheckBox = Prefabs.CheckBox:Clone()
		local Tickbox: ImageButton = CheckBox.Tickbox
		local Tick: ImageLabel = Tickbox.Tick
		local Label = CheckBox.Label
		local ObjectClass = self:NewInstance(CheckBox, Config)

		--// Stylise to correct type
		if IsRadio then
			Tick.ImageTransparency = 1
			Tick.BackgroundTransparency = 0
		else
			Tickbox:FindFirstChildOfClass("UIPadding"):Remove()
			Tickbox:FindFirstChildOfClass("UICorner"):Remove()
		end

		--// Apply animations
		ImGui:ApplyAnimations(CheckBox, "Buttons", Tickbox)

		local Value = Config.Value or false

		--// Callback
		local function Callback(...)
			local func = Config.Callback or NullFunction
			return func(ObjectClass, ...)
		end

		function Config:SetTicked(NewValue: boolean, NoAnimation: false)
			Value = NewValue
			Config.Value = Value

			--// Animations
			local Size = Value and UDim2.fromScale(1,1) or UDim2.fromScale(0,0)
			ImGui:Tween(Tick, {
				Size = Size
			}, nil, NoAnimation)
			ImGui:Tween(Label, {
				TextTransparency = Value and 0 or 0.3
			}, nil, NoAnimation)

			--// Fire callback
			Callback(Value)

			return Config
		end
		Config:SetTicked(Value, true)

		function Config:Toggle()
			Config:SetTicked(not Value)
			return Config
		end

		--// Connect functions
		local function Clicked()
			Value = not Value
			Config:SetTicked(Value)
		end
		CheckBox.Activated:Connect(Clicked)
		Tickbox.Activated:Connect(Clicked)

		return ObjectClass
	end

	function ContainerClass:RadioButton(Config)
		Config = Config or {}
		Config.IsRadio = true
		return self:Checkbox(Config)
	end

	function ContainerClass:Viewport(Config)
		Config = Config or {}
		local Model = Config.Model

		local Holder = Prefabs.Viewport:Clone()
		local Viewport: ViewportFrame = Holder.Viewport
		local WorldModel: WorldModel = Viewport.WorldModel
		Config.WorldModel = WorldModel
		Config.Viewport = Viewport

		function Config:SetCamera(Camera)
			Viewport.CurrentCamera = Camera
			Config.Camera = Camera
			Camera.CFrame = CFrame.new(0,0,0)
			return Config
		end

		local Camera = Config.Camera or ImGui:CreateInstance("Camera", Viewport)
		Config:SetCamera(Camera)

		function Config:SetModel(Model: Model, PivotTo: CFrame)
			WorldModel:ClearAllChildren()

			--// Set new model
			if Config.Clone then
				Model = Model:Clone()
			end
			if PivotTo then
				Model:PivotTo(PivotTo)
			end

			Model.Parent = WorldModel
			Config.Model = Model
			return Model
		end

		--// Set model
		if Model then
			Config:SetModel(Model)
		end

		local ContainClass = ImGui:ContainerClass(Holder, Config, Window)
		return self:NewInstance(Holder, ContainClass)
	end

	function ContainerClass:InputText(Config)
		Config = Config or {}
		local TextInput = Prefabs.TextInput:Clone()
		local TextBox: TextBox = TextInput.Input
		local ObjectClass = self:NewInstance(TextInput, Config)

		TextBox.Text = Config.Value or ""
		TextBox.PlaceholderText = Config.PlaceHolder
		TextBox.MultiLine = Config.MultiLine == true

		--// Apply animations
		ImGui:ApplyAnimations(TextInput, "Inputs")

		local function Callback(...)
			local func = Config.Callback or NullFunction
			return func(ObjectClass, ...)
		end
		TextBox:GetPropertyChangedSignal("Text"):Connect(function()
			local Value = TextBox.Text
			Config.Value = Value
			return Callback(Value)
		end)

		function Config:SetValue(Text)
			TextBox.Text = tostring(Text)
			Config.Value = Text
			return Config
		end

		function Config:Clear()
			TextBox.Text = ""
			return Config
		end

		return ObjectClass
	end

	function ContainerClass:InputTextMultiline(Config)
		Config = Config or {}
		Config.Label = ""
		Config.Size = UDim2.new(1, 0, 0, 38)
		Config.MultiLine = true
		return ContainerClass:InputText(Config)
	end

	function ContainerClass:GetRemainingHeight()
		local Padding = Frame:FindFirstChildOfClass("UIPadding")
		local UIListLayout = Frame:FindFirstChildOfClass("UIListLayout")

		local LayoutPaddding = UIListLayout.Padding
		local PaddingTop = Padding.PaddingTop
		local PaddingBottom = Padding.PaddingBottom

		local PaddingSizeY = PaddingTop+PaddingBottom+LayoutPaddding
		local OccupiedY = Frame.AbsoluteSize.Y+PaddingSizeY.Offset+3

		return UDim2.new(1, 0, 1, -OccupiedY)
	end

	function ContainerClass:Console(Config)
		Config = Config or {}
		local Console: ScrollingFrame = Prefabs.Console:Clone()
		local Source: TextBox = Console.Source
		local Lines = Console.Lines

		if Config.Fill then
			Console.Size = ContainerClass:GetRemainingHeight()
		end

		--// Set values from config
		Source.TextEditable = Config.ReadOnly ~= true
		Source.Text = Config.Text or ""
		Source.TextWrapped = Config.TextWrapped == true
		Source.RichText = Config.RichText == true
		Lines.Visible = Config.LineNumbers == true

		function Config:UpdateLineNumbers()
			if not Config.LineNumbers then return end

			local LinesCount = #Source.Text:split("\n")
			local Format = Config.LinesFormat or "%s"

			--// Update lines text
			Lines.Text = ""
			for i = 1, LinesCount do
				Lines.Text ..= `{Format:format(i)}{i ~= LinesCount and '\n' or ''}`
			end

			Source.Size = UDim2.new(1, -Lines.AbsoluteSize.X, 0, 0)
			return Config
		end

		function Config:UpdateScroll()
			local CanvasSizeY = Console.AbsoluteCanvasSize.Y
			Console.CanvasPosition = Vector2.new(0, CanvasSizeY)
			return Config
		end

		function Config:SetText(Text)
			if not Config.Enabled then return end
			Source.Text = Text
			Config:UpdateLineNumbers()
			return Config
		end

		function Config:GetValue()
			return Source.Text
		end

		function Config:Clear(Text)
			Source.Text = ""
			Config:UpdateLineNumbers()
			return Config
		end

		function Config:AppendText(...)
			if not Config.Enabled then return end

			local MaxLines = Config.MaxLines or 100
			local NewString = "\n" .. ImGui:Concat({...}, " ")

			Source.Text ..= NewString
			Config:UpdateLineNumbers()

			if Config.AutoScroll then
				Config:UpdateScroll()
			end

			local Lines = Source.Text:split("\n")
			if #Lines > MaxLines then
				Source.Text = Source.Text:sub(#Lines[1]+2)
			end
			return Config
		end

		--// Connect events
		Source.Changed:Connect(Config.UpdateLineNumbers)

		return self:NewInstance(Console, Config)
	end

	function ContainerClass:Table(Config)
		Config = Config or {}
		local Table: Frame = Prefabs.Table:Clone()
		local TableChildCount = #Table:GetChildren() --// Performance

		--// Configure Table style
		if Config.Fill then
			Table.Size = ContainerClass:GetRemainingHeight()
		end
		local RowName = "Row"

		local RowsCount = 0
		function Config:CreateRow()
			local RowClass = {}

			local Row: Frame = Table.RowTemp:Clone()
			local UIListLayout = Row:FindFirstChildOfClass("UIListLayout")
			UIListLayout.VerticalAlignment = Enum.VerticalAlignment[Config.Align or "Center"]

			local RowChildCount = #Row:GetChildren() --// Performance
			Row.Name = RowName
			Row.Visible = true

			--// Background colors
			if Config.RowBackground then
				Row.BackgroundTransparency = RowsCount % 2 == 1 and 0.92 or 1
			end

			function RowClass:CreateColumn(CConfig)
				CConfig = CConfig or {}
				local Column: Frame = Row.ColumnTemp:Clone()
				Column.Visible = true
				Column.Name = "Column"

				local Stroke = Column:FindFirstChildOfClass("UIStroke")
				Stroke.Enabled = Config.Border ~= false

				local ContainClass = ImGui:ContainerClass(Column, CConfig, Window)
				return ContainerClass:NewInstance(Column, ContainClass, Row)
			end

			function RowClass:UpdateColumns()
				if not Row or not Table then return end
				local Columns = Row:GetChildren()
				local RowsCount = #Columns - RowChildCount

				for _, Column: Frame in next, Columns do
					if not Column:IsA("Frame") then continue end
					Column.Size = UDim2.new(1/RowsCount, 0, 0, 0)
				end
				return RowClass
			end
			Row.ChildAdded:Connect(RowClass.UpdateColumns)
			Row.ChildRemoved:Connect(RowClass.UpdateColumns)

			RowsCount += 1
			return ContainerClass:NewInstance(Row, RowClass, Table)
		end

		function Config:UpdateRows()
			local Rows = Table:GetChildren()
			local PaddingY = Table.UIListLayout.Padding.Offset + 2
			local RowsCount = #Rows - TableChildCount

			for _, Row: Frame in next, Rows do
				if not Row:IsA("Frame") then continue end
				Row.Size = UDim2.new(1, 0, 1/RowsCount, -PaddingY)
			end
			return Config
		end

		if Config.RowsFill then
			Table.AutomaticSize = Enum.AutomaticSize.None
			Table.ChildAdded:Connect(Config.UpdateRows)
			Table.ChildRemoved:Connect(Config.UpdateRows)
		end

		function Config:ClearRows()
			RowsCount = 0
			local PostRowName = ImGui:GetName(RowName)
			for _, Row: Frame in next, Table:GetChildren() do
				if not Row:IsA("Frame") then continue end

				if Row.Name == PostRowName then
					Row:Remove()
				end
			end
			return Config
		end

		return self:NewInstance(Table, Config)
	end

	function ContainerClass:Grid(Config)
		Config = Config or {}
		Config.Grid = true

		return self:Table(Config)
	end

	function ContainerClass:CollapsingHeader(Config)
		Config = Config or {}
		local Title = Config.Title or ""
		Config.Name = Title

		local Header = Prefabs.CollapsingHeader:Clone()
		local Titlebar: TextButton = Header.TitleBar
		local Container: Frame = Header.ChildContainer
		Titlebar.Title.Text = Title

		--// Apply animations
		if Config.IsTree then
			ImGui:ApplyAnimations(Titlebar, "Tabs")
		else
			ImGui:ApplyAnimations(Titlebar, "Buttons")
		end

		--// Open Animations
		function Config:SetOpen(Open)
			local Animate = Config.NoAnimation ~= true
			Config.Open = Open
			ImGui:HeaderAnimate(Header, Animate, Open, Titlebar)
			return self
		end

		--// Toggle
		local ToggleButton = Titlebar.Toggle.ToggleButton
		local function Toggle()
			Config:SetOpen(not Config.Open)
		end
		Titlebar.Activated:Connect(Toggle)
		ToggleButton.Activated:Connect(Toggle)

		--// Custom toggle image
		if Config.Image then
			ToggleButton.Image = Config.Image
		end

		--// Open
		Config:SetOpen(Config.Open or false)

		local ContainClass = ImGui:ContainerClass(Container, Config, Window)
		return self:NewInstance(Header, ContainClass)
	end

	function ContainerClass:TreeNode(Config)
		Config = Config or {}
		Config.IsTree = true
		return self:CollapsingHeader(Config)
	end

	function ContainerClass:Separator(Config)
		Config = Config or {}
		local Separator = Prefabs.SeparatorText:Clone()
		local HeaderLabel = Separator.TextLabel
		HeaderLabel.Text = Config.Text or ""

		if not Config.Text then
			HeaderLabel.Visible = false
		end

		return self:NewInstance(Separator, Config)
	end

	function ContainerClass:Row(Config)
		Config = Config or {}
		local Row: Frame = Prefabs.Row:Clone()
		local UIListLayout = Row:FindFirstChildOfClass("UIListLayout")
		local UIPadding = Row:FindFirstChildOfClass("UIPadding")

		if Config.Spacing then
			UIListLayout.Padding = UDim.new(0, Config.Spacing)
		end

		function Config:Fill()
			local Children = Row:GetChildren()
			local Rows = #Children - 2 --// -UIListLayout + UIPadding

			--// Change layout
			local Padding = UIListLayout.Padding.Offset * 2
			UIListLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center

			--// Apply correct margins
			UIPadding.PaddingLeft = UIListLayout.Padding
			UIPadding.PaddingRight = UIListLayout.Padding

			for _, Child: Instance in next, Children do
				local YScale = 0
				if Child:IsA("ImageButton") then
					YScale = 1
				end
				pcall(function()
					Child.Size = UDim2.new(1/Rows, -Padding, YScale, 0)
				end)
			end
			return Config
		end

		local ContainClass = ImGui:ContainerClass(Row, Config, Window)
		return self:NewInstance(Row, ContainClass)
	end

	--TODO
	-- Vertical
	-- :SetPercentage
	-- This will use UIDragdetectors in the upcoming release, please do not report this!
	function ContainerClass:Slider(Config)
		Config = Config or {}

		--// Unpack config
		local Value = Config.Value or 0
		local ValueFormat = Config.Format or "%.d"
		local IsProgress = Config.Progress
		Config.Name = Config.Label or ""

		--// Slider element
		local Slider: TextButton = Prefabs.Slider:Clone()
		local UIPadding = Slider:FindFirstChildOfClass("UIPadding")
		local Grab: Frame = Slider.Grab
		local ValueText = Slider.ValueText
		local Label = Slider.Label

		--// Input data
		local Dragging = false
		local MouseMoveConnection = nil
		local InputType = Enum.UserInputType.MouseButton1

		local ObjectClass = self:NewInstance(Slider, Config)

		local function Callback(...)
			local func = Config.Callback or NullFunction
			return func(ObjectClass, ...)
		end

		--// Apply Progress styles
		if IsProgress then
			local UIGradient = Grab:FindFirstChildOfClass("UIGradient")

			local PaddingSides = UDim.new(0,2)
			local Diff = UIPadding.PaddingLeft - PaddingSides

			Grab.AnchorPoint = Vector2.new(0, 0.5)
			UIGradient.Enabled = true

			UIPadding.PaddingLeft = PaddingSides
			UIPadding.PaddingRight = PaddingSides

			Label.Position = UDim2.new(1, 15-Diff.Offset, 0, 0)
		end

		function Config:SetValue(Value: number, Slider: false)
			local MinValue = Config.MinValue
			local MaxValue = Config.MaxValue
			local Difference = MaxValue - MinValue
			local Percentage = (Value - MinValue) / Difference

			if not Slider then
				Value = tonumber(Value)
			else
				Percentage = Value
				Value = MinValue + (Difference * Percentage)
			end

			--// Animate grab
			local Props = {
				Position = UDim2.fromScale(Percentage, 0.5)
			}

			if IsProgress then
				Props = {
					Size = UDim2.fromScale(Percentage, 1)
				}
			end

			--// Animate
			ImGui:Tween(Grab, Props)

			--// Update UI
			Config.Value = Value
			ValueText.Text = ValueFormat:format(Value, MaxValue)

			--// Fire callback
			Callback(Value)

			return Config
		end

		------// Move events
		local function MouseMove()
			if Config.ReadOnly then return end
			if not Dragging then return end

			local MouseX = UserInputService:GetMouseLocation().X
			local LeftPos = Slider.AbsolutePosition.X

			local Percentage = (MouseX-LeftPos)/Slider.AbsoluteSize.X
			Percentage = math.clamp(Percentage, 0, 1)
			Config:SetValue(Percentage, true)
		end

		local function InputEnded(inputObject)
			if not Dragging then return end
			if inputObject.UserInputType ~= InputType then return end

			Dragging = false
			MouseMoveConnection:Disconnect()
		end

		--// Connect mouse events
		ImGui:ConnectHover({
			Parent = Slider,
			OnInput = function(MouseHovering, Input)
				if not MouseHovering then return end
				if Input.UserInputType ~= InputType then return end

				Dragging = true
				MouseMoveConnection = Mouse.Move:Connect(MouseMove) --// Save heavy performance
			end
		})

		--// Connect events
		Slider.Activated:Connect(MouseMove)
		UserInputService.InputEnded:Connect(InputEnded)

		--// Update UI
		Config:SetValue(Value)

		return ObjectClass
	end

	function ContainerClass:ProgressSlider(Config)
		Config = Config or {}
		Config.Progress = true
		return self:Slider(Config)
	end

	function ContainerClass:ProgressBar(Config)
		Config = Config or {}
		Config.Progress = true
		Config.ReadOnly = true
		Config.MinValue = 0
		Config.MaxValue = 100
		Config.Format = "% i%%"
		Config = self:Slider(Config)

		function Config:SetPercentage(Value: number)
			Config:SetValue(Value)
		end

		return Config
	end

	function ContainerClass:Keybind(Config)
		Config = Config or {}

		local Key = Config.Value
		local TobeNullKey = Config.NullKey or Enum.KeyCode.Backspace

		local Keybind: TextButton = Prefabs.Keybind:Clone()
		local ValueText: TextButton = Keybind.ValueText

		local ObjectClass = nil
		local function Callback(...)
			local func = Config.Callback or NullFunction
			return func(ObjectClass, ...)
		end

		function Config:SetValue(NewKey: Enum.KeyCode)
			if not NewKey then return end

			if NewKey == TobeNullKey then
				ValueText.Text = "Not set"
				Config.Value = nil
			else
				ValueText.Text = NewKey.Name
				Config.Value = NewKey
			end
		end

		Keybind.Activated:Connect(function()
			ValueText.Text = "..."

			local NewKey = UserInputService.InputBegan:Wait()
			if not UserInputService.WindowFocused then return end

			--// Reset back to previous if unknown
			local Previous = Config.Value
			if NewKey.KeyCode.Name == "Unknown" then
				return Config:SetValue(Previous)
			end

			wait(.1) --// 👍
			Config:SetValue(NewKey.KeyCode)
		end)

		Config.Connection = UserInputService.InputBegan:Connect(function(Input, GameProcessed)
			if not Config.IgnoreGameProcessed and GameProcessed then return end
			local KeyCode = Input.KeyCode
			local Match = Config.Value

			if KeyCode == TobeNullKey then return end
			if KeyCode ~= Match then return end

			return Callback(Input.KeyCode)
		end)

		--// Update UI
		Config:SetValue(Key)

		ObjectClass = self:NewInstance(Keybind, Config)
		return ObjectClass
	end

	function ContainerClass:Combo(Config)
		Config = Config or {}
		Config.Open = false
		Config.Value = ""

		local Combo: TextButton = Prefabs.Combo:Clone()
		local Toggle: ImageButton = Combo.Toggle.ToggleButton
		local ValueText = Combo.ValueText
		ValueText.Text = Config.Placeholder or ""

		local Dropdown = nil
		local ObjectClass = self:NewInstance(Combo, Config)

		local ComboHovering = ImGui:ConnectHover({
			Parent = Combo
		})

		local function Callback(Value, ...)
			local func = Config.Callback or NullFunction
			Config:SetOpen(false)
			return func(ObjectClass, Value, ...)
		end

		function Config:SetValue(Value, ...)
			local Items = Config.Items or {}
			local DictValue = Items[Value]
			ValueText.Text = tostring(Value)
			Config.Value = Value

			return Callback(DictValue or Value)
		end

		function Config:SetOpen(Open: true)
			local Animate = Config.NoAnimation ~= true
			ImGui:HeaderAnimate(Combo, Animate, Open, Combo, Toggle)
			Config.Open = Open

			if Open then
				Dropdown = ImGui:Dropdown({
					Parent = Combo,
					Items = Config.Items or {},
					SetValue = Config.SetValue,
					Closed = function()
						if not ComboHovering.Hovering then
							Config:SetOpen(false)
						end
					end,
				})
			end

			return self
		end

		local function ToggleOpen()
			if Dropdown then
				Dropdown:Close()
			end
			Config:SetOpen(not Config.Open)
		end

		--// Connect events
		Combo.Activated:Connect(ToggleOpen)
		Toggle.Activated:Connect(ToggleOpen)
		ImGui:ApplyAnimations(Combo, "Buttons")

		if Config.Selected then
			Config:SetValue(Config.Selected)
		end

		return ObjectClass
	end

	return ContainerClass
end

function ImGui:Dropdown(Config)
	local Parent: GuiObject = Config.Parent
	if not Parent then return end

	local Selection: ScrollingFrame = Prefabs.Selection:Clone()
	local UIStroke = Selection:FindFirstChildOfClass("UIStroke")

	local Padding = UIStroke.Thickness*2
	local Position = Parent.AbsolutePosition
	local Size = Parent.AbsoluteSize

	Selection.Parent = self.ScreenGui
	Selection.Position = UDim2.fromOffset(Position.X+Padding, Position.Y+Size.Y)

	local Hover = self:ConnectHover({
		Parent = Selection,
		OnInput = function(MouseHovering, Input)
			if not Input.UserInputType.Name:find("Mouse") then return end

			if not MouseHovering then
				Config:Close()
			end
		end,
	})

	function Config:Close()
		local CloseCallback = Config.Closed
		if CloseCallback then
			CloseCallback()
		end

		Hover:Disconnect()
		Selection:Remove()
	end

	local function SetValue(Value)
		Config:Close()
		Config:SetValue(Value)
	end

	--// Append items
	local ItemTemplate: TextButton = Selection.Template
	ItemTemplate.Visible = false

	for Index, Index2 in next, Config.Items do
		local Value = typeof(Index) ~= "number" and Index or Index2

		local NewItem: TextButton = ItemTemplate:Clone()
		NewItem.Text = tostring(Value)
		NewItem.Parent = Selection
		NewItem.Visible = true
		NewItem.Activated:Connect(function()
			return SetValue(Value)
		end)

		self:ApplyAnimations(NewItem, "Tabs")
	end

	--// Configure size of the frame
	-- Roblox does not support UISizeConstraint on a scrolling frame grr

	local MaxSizeY = Config.MaxSizeY or 200
	local YSize = math.clamp(Selection.AbsoluteCanvasSize.Y, Size.Y, MaxSizeY)
	Selection.Size = UDim2.fromOffset(Size.X-Padding, YSize)

	return Config
end

function ImGui:GetAnimation(Animation: boolean?)
	return Animation and self.Animation or TweenInfo.new(0)
end

function ImGui:Tween(Instance: GuiObject, Props: SharedTable, tweenInfo, NoAnimation: false)
	local tweenInfo = tweenInfo or ImGui:GetAnimation(not NoAnimation)
	local Tween = TweenService:Create(Instance,
		tweenInfo,
		Props
	)
	Tween:Play()
	return Tween
end

function ImGui:ApplyAnimations(Instance: GuiObject, Class: string, Target: GuiObject?)
	local Animatons = ImGui.Animations
	local ColorProps = Animatons[Class]

	if not ColorProps then
		return warn("No colors for", Class)
	end

	--// Apply tweens for connections
	local Connections = {}
	for Connection, Props in next, ColorProps do
		if typeof(Props) ~= "table" then continue end
		local Target = Target or Instance
		local Callback = function()
			ImGui:Tween(Target, Props)
		end

		--// Connections
		Connections[Connection] = Callback
		Instance[Connection]:Connect(Callback)
	end

	--// Reset colors
	if Connections["MouseLeave"] then
		Connections["MouseLeave"]()
	end

	return Connections
end

function ImGui:HeaderAnimate(Header: Instance, Animation, Open, TitleBar: Instance, Toggle)
	local ToggleButtion = Toggle or TitleBar.Toggle.ToggleButton

	--// Togle animation
	ImGui:Tween(ToggleButtion, {
		Rotation = Open and 90 or 0,
	}):Play()

	--// Container animation
	local Container: Frame = Header:FindFirstChild("ChildContainer")
	if not Container then return end

	local UIListLayout: UIListLayout = Container.UIListLayout
	local UIPadding: UIPadding = Container:FindFirstChildOfClass("UIPadding")
	local ContentSize = UIListLayout.AbsoluteContentSize

	if UIPadding then
		local Top = UIPadding.PaddingTop.Offset
		local Bottom = UIPadding.PaddingBottom.Offset
		ContentSize = Vector2.new(ContentSize.X, ContentSize.Y+Top+Bottom)
	end

	Container.AutomaticSize = Enum.AutomaticSize.None
	if not Open then
		Container.Size = UDim2.new(1, -10, 0, ContentSize.Y)
	end

	--// Animate
	local Tween = ImGui:Tween(Container, {
		Size = UDim2.new(1, -10, 0, Open and ContentSize.Y or 0),
		Visible = Open
	})
	Tween.Completed:Connect(function()
		if not Open then return end
		Container.AutomaticSize = Enum.AutomaticSize.Y
		Container.Size = UDim2.new(1, -10, 0, 0)
	end)
end

function ImGui:ApplyDraggable(Frame: Frame, Header: Frame)
	local tweenInfo = ImGui:GetAnimation(true)
	local Header = Header or Frame

	local Dragging = false
	local KeyBeganPos = nil
	local BeganPos = Frame.Position

	--// Whitelist
	local UserInputTypes = {
		Enum.UserInputType.MouseButton1,
		Enum.UserInputType.Touch
	}

	local function UserInputTypeAllowed(InputType: Enum.UserInputType)
		return table.find(UserInputTypes, InputType)
	end

	--// Debounce
	Header.InputBegan:Connect(function(Key)
		if UserInputTypeAllowed(Key.UserInputType) then
			Dragging = true
			KeyBeganPos = Key.Position
			BeganPos = Frame.Position
		end
	end)

	UserInputService.InputEnded:Connect(function(Key)
		if UserInputTypeAllowed(Key.UserInputType) then
			Dragging = false
		end
	end)

	--// Dragging
	local function Movement(Input)
		if not Dragging then return end

		local Delta = Input.Position - KeyBeganPos
		local Position = UDim2.new(
			BeganPos.X.Scale,
			BeganPos.X.Offset + Delta.X,
			BeganPos.Y.Scale,
			BeganPos.Y.Offset + Delta.Y
		)

		ImGui:Tween(Frame, {
			Position = Position
		}):Play()
	end

	--// Connect movement events
	UserInputService.TouchMoved:Connect(Movement)
	UserInputService.InputChanged:Connect(function(Input)
		if Input.UserInputType == Enum.UserInputType.MouseMovement then
			return Movement(Input)
		end
	end)
end

function ImGui:ApplyResizable(MinSize, Frame: Frame, Dragger: TextButton, Config)
	local DragStart
	local OrignialSize

	MinSize = MinSize or Vector2.new(160, 90)

	Dragger.MouseButton1Down:Connect(function()
		if DragStart then return end
		OrignialSize = Frame.AbsoluteSize
		DragStart = Vector2.new(Mouse.X, Mouse.Y)
	end)

	UserInputService.InputChanged:Connect(function(Input)
		if not DragStart or Input.UserInputType ~= Enum.UserInputType.MouseMovement then
			return
		end

		local MousePos = Vector2.new(Mouse.X, Mouse.Y)
		local mouseMoved = MousePos - DragStart

		local NewSize = OrignialSize + mouseMoved
		NewSize = UDim2.fromOffset(
			math.max(MinSize.X, NewSize.X),
			math.max(MinSize.Y, NewSize.Y)
		)

		Frame.Size = NewSize
		if Config then
			Config.Size = NewSize
		end
	end)

	UserInputService.InputEnded:Connect(function(Input)
		if Input.UserInputType == Enum.UserInputType.MouseButton1 then
			DragStart = nil
		end
	end)
end

function ImGui:ConnectHover(Config)
	local Parent = Config.Parent
	local Connections = {}
	Config.Hovering = false

	--// Connect Events
	table.insert(Connections, Parent.MouseEnter:Connect(function()
		Config.Hovering = true
	end))
	table.insert(Connections, Parent.MouseLeave:Connect(function()
		Config.Hovering = false
	end))

	if Config.OnInput then
		table.insert(Connections, UserInputService.InputBegan:Connect(function(Input)
			return Config.OnInput(Config.Hovering, Input)
		end))
	end

	function Config:Disconnect()
		for _, Connection in next, Connections do
			Connection:Disconnect()
		end
	end

	return Config
end

function ImGui:ApplyWindowSelectEffect(Window: GuiObject, TitleBar)
	local UIStroke = Window:FindFirstChildOfClass("UIStroke")

	local Colors = {
		Selected = {
			BackgroundColor3 = TitleBar.BackgroundColor3
		},
		Deselected = {
			BackgroundColor3 = Color3.fromRGB(0, 0, 0)
		}
	}

	local function SetSelected(Selected)
		local Animations = ImGui.Animations
		local Type = Selected and "Selected" or "Deselected"
		local TweenInfo = ImGui:GetAnimation(true)

		ImGui:Tween(TitleBar, Colors[Type])
		ImGui:Tween(UIStroke, Animations.WindowBorder[Type])
	end

	self:ConnectHover({
		Parent = Window,
		OnInput = function(MouseHovering, Input)
			if Input.UserInputType.Name:find("Mouse") then
				SetSelected(MouseHovering)
			end
		end,
	})
end

function ImGui:SetWindowProps(Properties, IgnoreWindows)
	local Module = {
		OldProperties = {}
	}

	--// Collect windows & set properties
	for Window in next, ImGui.Windows do
		if table.find(IgnoreWindows, Window) then continue end

		local OldValues = {}
		Module.OldProperties[Window] = OldValues

		for Key, Value in next, Properties do
			OldValues[Key] = Window[Key]
			Window[Key] = Value
		end
	end

	--// Revert to previous values
	function Module:Revert()
		for Window in next, ImGui.Windows do
			local OldValues = Module.OldProperties[Window]
			if not OldValues then continue end

			for Key, Value in next, OldValues do
				Window[Key] = Value
			end
		end
	end

	return Module
end

function ImGui:CreateWindow(WindowConfig)
	--// Create Window frame
	local Window: Frame = Prefabs.Window:Clone()
	Window.Parent = ImGui.ScreenGui
	Window.Visible = true
	WindowConfig.Window = Window

	local Content = Window.Content
	local Body = Content.Body

	--// Window Resize
	local Resize = Window.ResizeGrab
	Resize.Visible = WindowConfig.NoResize ~= true

	local MinSize = WindowConfig.MinSize or Vector2.new(160, 90)
	ImGui:ApplyResizable(
		MinSize,
		Window,
		Resize,
		WindowConfig
	)

	--// Title Bar
	local TitleBar: Frame = Content.TitleBar
	TitleBar.Visible = WindowConfig.NoTitleBar ~= true

	local Toggle = TitleBar.Left.Toggle
	Toggle.Visible = WindowConfig.NoCollapse ~= true
	ImGui:ApplyAnimations(Toggle.ToggleButton, "Tabs")

	local ToolBar = Content.ToolBar
	ToolBar.Visible = WindowConfig.TabsBar ~= false

	if not WindowConfig.NoDrag then
		ImGui:ApplyDraggable(Window)
	end

	--// Close Window
	local CloseButton: TextButton = TitleBar.Close
	CloseButton.Visible = WindowConfig.NoClose ~= true

	function WindowConfig:Close()
		local Callback = WindowConfig.CloseCallback
		WindowConfig:SetVisible(false)
		if Callback then
			Callback(WindowConfig)
		end
		return WindowConfig
	end
	CloseButton.Activated:Connect(WindowConfig.Close)

	function WindowConfig:GetHeaderSizeY(): number
		local ToolbarY = ToolBar.Visible and ToolBar.AbsoluteSize.Y or 0
		local TitlebarY = TitleBar.Visible and TitleBar.AbsoluteSize.Y or 0
		return ToolbarY + TitlebarY
	end

	function WindowConfig:UpdateBody()
		local HeaderSizeY = self:GetHeaderSizeY()
		Body.Size = UDim2.new(1, 0, 1, -HeaderSizeY)
	end
	WindowConfig:UpdateBody()

	--// Open/Close
	WindowConfig.Open = true
	function WindowConfig:SetOpen(Open: true, NoAnimation: false)
		local WindowAbSize = Window.AbsoluteSize
		local TitleBarSize = TitleBar.AbsoluteSize

		self.Open = Open

		--// Call animations
		ImGui:HeaderAnimate(TitleBar, true, Open, TitleBar, Toggle.ToggleButton)
		ImGui:Tween(Resize, {
			TextTransparency = Open and 0.6 or 1,
			Interactable = Open
		}, nil, NoAnimation)
		ImGui:Tween(Window, {
			Size = Open and self.Size or UDim2.fromOffset(WindowAbSize.X, TitleBarSize.Y)
		}, nil, NoAnimation)
		ImGui:Tween(Body, {
			Visible = Open
		}, nil, NoAnimation)
		return self
	end

	function WindowConfig:SetVisible(Visible: boolean)
		Window.Visible = Visible
		return self
	end

	function WindowConfig:SetTitle(Text)
		TitleBar.Left.Title.Text = tostring(Text)
		return self
	end
	function WindowConfig:Remove()
		Window:Remove()
		return self
	end

	Toggle.ToggleButton.Activated:Connect(function()
		local Open = not WindowConfig.Open
		WindowConfig.Open = Open
		return WindowConfig:SetOpen(Open)
	end)

	function WindowConfig:CreateTab(Config)
		local Name = Config.Name or ""
		local TabButton = ToolBar.TabButton:Clone()
		TabButton.Name = Name
		TabButton.Text = Name
		TabButton.Visible = true
		TabButton.Parent = ToolBar
		Config.Button = TabButton

		local AutoSizeAxis = WindowConfig.AutoSize or "Y"
		local Content: Frame = Body.Template:Clone()
		Content.AutomaticSize = Enum.AutomaticSize[AutoSizeAxis]
		Content.Visible = Config.Visible or false
		Content.Name = Name
		Content.Parent = Body
		Config.Content = Content

		if AutoSizeAxis == "Y" then
			Content.Size = UDim2.fromScale(1, 0)
		elseif AutoSizeAxis == "X" then
			Content.Size = UDim2.fromScale(0, 1)
		end

		TabButton.Activated:Connect(function()
			WindowConfig:ShowTab(Config)
		end)

		function Config:GetContentSize()
			return Content.AbsoluteSize
		end

		--// Apply animations
		Config = ImGui:ContainerClass(Content, Config, Window)
		ImGui:ApplyAnimations(TabButton, "Tabs")

		--// Automatic sizes
		self:UpdateBody()
		if WindowConfig.AutoSize then
			Content:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
				local Size = Config:GetContentSize()
				self:SetSize(Size)
			end)
		end

		return Config
	end

	function WindowConfig:SetPosition(Position)
		Window.Position = Position
		return self
	end

	function WindowConfig:SetSize(Size)
		local HeaderSizeY = self:GetHeaderSizeY()

		if typeof(Size) == "Vector2" then
			Size = UDim2.fromOffset(Size.X, Size.Y)
		end

		--// Apply new size
		local NewSize = UDim2.new(
			Size.X.Scale,
			Size.X.Offset,
			Size.Y.Scale,
			Size.Y.Offset + HeaderSizeY
		)
		self.Size = NewSize
		Window.Size = NewSize

		return self
	end

	--// Tab change system
	function WindowConfig:ShowTab(TabClass: SharedTable)
		local TargetPage: Frame = TabClass.Content

		--// Page animation
		if not TargetPage.Visible and not TabClass.NoAnimation then
			TargetPage.Position = UDim2.fromOffset(0, 5)
		end

		--// Hide other tabs
		for _, Page in next, Body:GetChildren() do
			Page.Visible = Page == TargetPage
		end

		--// Page animation
		ImGui:Tween(TargetPage, {
			Position = UDim2.fromOffset(0, 0)
		})
		return self
	end

	function WindowConfig:Center() --// Without an Anchor point
		local Size = Window.AbsoluteSize
		local Position = UDim2.new(0.5,-Size.X/2,0.5,-Size.Y/2)
		self:SetPosition(Position)
		return self
	end

	--// Load Style Configs
	WindowConfig:SetTitle(WindowConfig.Title or "Depso UI")

	if not WindowConfig.Open then
		WindowConfig:SetOpen(WindowConfig.Open or true, true)
	end

	ImGui.Windows[Window] = WindowConfig
	ImGui:CheckStyles(Window, WindowConfig, WindowConfig.Colors)

	--// Window section events
	if not WindowConfig.NoSelectEffect then
		ImGui:ApplyWindowSelectEffect(Window, TitleBar)
	end

	return ImGui:MergeMetatables(WindowConfig, Window)
end

function ImGui:CreateModal(Config)
	local ModalEffect = Prefabs.ModalEffect:Clone()
	ModalEffect.BackgroundTransparency = 1
	ModalEffect.Parent = ImGui.FullScreenGui
	ModalEffect.Visible = true

	ImGui:Tween(ModalEffect, {
		BackgroundTransparency = 0.6
	})

	--// Config
	Config = Config or {}
	Config.TabsBar = Config.TabsBar ~= nil and Config.TabsBar or false
	Config.NoCollapse = true
	Config.NoResize = true
	Config.NoClose = true
	Config.NoSelectEffect = true
	Config.Parent = ModalEffect

	--// Center
	Config.AnchorPoint = Vector2.new(0.5, 0.5)
	Config.Position = UDim2.fromScale(0.5, 0.5)

	--// Create Window
	local Window = self:CreateWindow(Config)
	Config = Window:CreateTab({
		Visible = true
	})

	--// Disable other windows
	local WindowManger = ImGui:SetWindowProps({
		Interactable = false
	}, {Window.Window})

	--// Close functions
	local WindowClose = Window.Close
	function Config:Close()
		local Tween = ImGui:Tween(ModalEffect, {
			BackgroundTransparency = 1
		})
		Tween.Completed:Connect(function()
			ModalEffect:Remove()
		end)

		WindowManger:Revert()
		WindowClose()
	end

	return Config
end

local GuiParent = IsStudio and PlayerGui or CoreGui
ImGui.ScreenGui = ImGui:CreateInstance("ScreenGui", GuiParent, {
	DisplayOrder = 9999,
	ResetOnSpawn = false
})
ImGui.FullScreenGui = ImGui:CreateInstance("ScreenGui", GuiParent, {
	DisplayOrder = 99999,
	ResetOnSpawn = false,
	ScreenInsets = Enum.ScreenInsets.None
})

return ImGui


--!optimize 2

--[[
This file is for use by Benchmarker (https://boatbomber.itch.io/benchmarker)

|WARNING| THIS RUNS IN YOUR REAL ENVIRONMENT. |WARNING|
--]]

local BinaryOctreeModule = require("path to binary octree")
local SleitnickModule = require("path to sleitnicks module")

local Dot = Vector3.zero.Dot
local OctreeSize = 516

local Objects = {}

for i = 1,1000 do
	table.insert(Objects,{Position = Vector3.new(math.random(-OctreeSize,OctreeSize),math.random(-OctreeSize,OctreeSize),math.random(-OctreeSize,OctreeSize))})
end

local StartPosition = Vector3.new(50,1,50)

local Radius = 30

local function LinearSearch()
	local GottenObjects = {}

	for _,Object in Objects do
		local SubtractedPosition = Object.Position - StartPosition
		if SubtractedPosition.Magnitude <= Radius then
			table.insert(GottenObjects,Object)
		end
	end

	return GottenObjects
end

local QueryBinaryOctree = BinaryOctreeModule.new(OctreeSize,5,Vector3.zero)
BinaryOctreeModule.InsertObjects(QueryBinaryOctree,Objects)
local SleitnickOctree = SleitnickModule.new(OctreeSize)

local SleitNodes = {}

for _,Object in Objects do
	table.insert(SleitNodes,SleitnickOctree:CreateNode(Object.Position,Object))
end

return {
	ParameterGenerator = function()
		return
	end,

	Functions = {
		["Linear Search Sphere"] = function(Profiler)
			LinearSearch()
		end,
		["Sleitnick query sphere"] = function(Profiler)
			SleitnickOctree:SearchRadius(StartPosition,Radius)
		end,
		["BinaryOctree Query Sphere"] = function(Profiler)
			BinaryOctreeModule.QuerySphere(QueryBinaryOctree,StartPosition,Radius)
		end,
	},
}


--!optimize 2
--!native

type Object = {
	Position: Vector3
}

export type BinaryOctree = {
	MaxDepth: number?,
	Size: number,
	OffsetPosition : Vector3?,
	Nodes: {{Object}},
}

type Module = {
	new: (Size: number, MaxDepth: number?,OffsetPosition : Vector3?) -> BinaryOctree,
	InsertObjects: (BinaryOctree: BinaryOctree, Objects: { Object }) -> (),
	QueryBox: (BinaryOctree: BinaryOctree, Position: Vector3, Size: Vector3) -> {Object},
	QuerySphere: (BinaryOctree : BinaryOctree,Position : Vector3,Radius : number) -> {Object},
	RemoveObject: (BinaryOctree : BinaryOctree,Object : Object) -> (),
	InsertObject: (BinaryOctree : BinaryOctree,Object : Object) -> (),
	VisualizeOctree: (BinaryOctree : BinaryOctree) -> ()
}

local SuffixToOrder = {
	Vector3.new(-1,-1,-1),--0
	Vector3.new(-1,-1,1), --1
	Vector3.new(-1,1,-1), --2
	Vector3.new(-1,1,1),  --3
	Vector3.new(1,-1,-1), --4
	Vector3.new(1,-1,1),  --5
	Vector3.new(1,1,-1),  --6
	Vector3.new(1,1,1),   --7
}

local Dot = Vector3.new().Dot
local SubdivideThreshold = 30

local BinaryOctreeModule: Module = {} :: Module

local function IsPositionInBox(Position: Vector3, MinBounds : Vector3, MaxBounds : Vector3) : boolean
	return Position.X >= MinBounds.X and Position.X <= MaxBounds.X
		and Position.Y >= MinBounds.Y and Position.Y <= MaxBounds.Y
		and Position.Z >= MinBounds.Z and Position.Z <= MaxBounds.Z
end

local function DetectBoxOverlap(MinBounds1 : Vector3,MaxBounds1 : Vector3,MinBounds2 : Vector3,MaxBounds2 : Vector3) : boolean
	return MinBounds1.X < MaxBounds2.X and MaxBounds1.X > MinBounds2.X and
		MinBounds1.Y < MaxBounds2.Y and MaxBounds1.Y > MinBounds2.Y and
		MinBounds1.Z < MaxBounds2.Z and MaxBounds1.Z > MinBounds2.Z
end

local function MagnitudeSquared(Position : Vector3) : number
	return (Position.X * Position.X) + (Position.Y * Position.Y)  + (Position.Z * Position.Z)
end

local function DetectBoxAndSphereOverlap(MinBounds : Vector3,MaxBounds : Vector3,SphereCenter : Vector3,RadiusSquared : number) : boolean
	local ClosestPoint = SphereCenter:Max(MinBounds):Min(MaxBounds)
	local Distance = MagnitudeSquared(SphereCenter - ClosestPoint)

	return Distance <= RadiusSquared
end

local function Subdivide(BinaryOctree : BinaryOctree,Node : number)
	local ShiftedNode = Node * 8

	for i = 0,7 do
		BinaryOctree.Nodes[ShiftedNode + i] = {}
	end
end

local function GetNodePositionAndSize(BinaryOctree : BinaryOctree,Node : number) : (Vector3,Vector3)
	local NumberLength = math.max(32-bit32.countlz(Node), 0) - 1
	local Position = Vector3.zero
	local HalfSize = BinaryOctree.Size / 4
	HalfSize = Vector3.new(HalfSize,HalfSize,HalfSize)

	for Index = 1,NumberLength,3 do
		local Suffix = bit32.extract(Node,NumberLength - Index - 2,3)

		Position = Position + (HalfSize * SuffixToOrder[Suffix + 1])

		Index += 3
		HalfSize = HalfSize / 2
	end

	return BinaryOctree.OffsetPosition + Position,HalfSize * 2
end

local function ReassignObjects(BinaryOctree : BinaryOctree,Node : number,NodePosition : Vector3)
	Subdivide(BinaryOctree,Node)

	local NodeTable = BinaryOctree.Nodes[Node]
	local ShiftedNode = Node * 8

	for i = 1,#NodeTable do
		local Object = NodeTable[i]
		local Position = Object.Position
		local Suffix = 0

		if Position.X > NodePosition.X then
			Suffix += 4
		end

		if Position.Y > NodePosition.Y then
			Suffix += 2
		end

		if Position.Z > NodePosition.Z then
			Suffix += 1
		end

		table.insert(BinaryOctree.Nodes[ShiftedNode + Suffix],Object)
	end

	table.clear(NodeTable)
end

function BinaryOctreeModule.new(Size : number,MaxDepth : number?,OffsetPosition : Vector3?) : BinaryOctree
	local newBinaryOctree = {
		MaxDepth = if MaxDepth then math.clamp(MaxDepth,1,10) else 5,
		Size = Size,
		OffsetPosition = if OffsetPosition then OffsetPosition else Vector3.zero,
		Nodes = {{}},
	}

	return newBinaryOctree
end

function BinaryOctreeModule.InsertObjects(BinaryOctree : BinaryOctree,Objects : {Object})
	local QuarterSize = BinaryOctree.Size / 4
	local Size = QuarterSize
	local Depth = 0
	local NodePosition = BinaryOctree.OffsetPosition
	local ChosenNode = 1
	local MaxDepth = BinaryOctree.MaxDepth

	for _,Object in Objects do
		local Position = Object.Position

		while true do
			local Suffix = 0

			if Position.X > NodePosition.X then
				Suffix += 4
			end

			if Position.Y > NodePosition.Y then
				Suffix += 2
			end

			if Position.Z > NodePosition.Z then
				Suffix += 1
			end

			local NextNode = ChosenNode * 8 + Suffix

			if BinaryOctree.Nodes[NextNode] == nil then
				local ChosenNodeTable = BinaryOctree.Nodes[ChosenNode]

				table.insert(ChosenNodeTable,Object)

				if #ChosenNodeTable > SubdivideThreshold and Depth < MaxDepth then
					ReassignObjects(BinaryOctree,ChosenNode,NodePosition)
				end

				break
			end

			NodePosition = NodePosition + (Size * SuffixToOrder[Suffix + 1])

			Size = Size / 2
			ChosenNode = NextNode
			Depth = Depth + 1
		end

		Size = QuarterSize
		Depth = 0
		NodePosition = BinaryOctree.OffsetPosition
		ChosenNode = 1
	end
end

function BinaryOctreeModule.InsertObject(BinaryOctree : BinaryOctree,Object : Object)
	local Size = BinaryOctree.Size / 4
	local Depth = 0
	local NodePosition = BinaryOctree.OffsetPosition
	local ChosenNode = 1
	local MaxDepth = BinaryOctree.MaxDepth
	local Position = Object.Position

	while true do
		local Suffix = 0

		if Position.X > NodePosition.X then
			Suffix += 4
		end

		if Position.Y > NodePosition.Y then
			Suffix += 2
		end

		if Position.Z > NodePosition.Z then
			Suffix += 1
		end

		local NextNode = ChosenNode * 8 + Suffix

		if BinaryOctree.Nodes[NextNode] == nil then
			local ChosenNodeTable = BinaryOctree.Nodes[ChosenNode]

			table.insert(ChosenNodeTable,Object)

			if #ChosenNodeTable > SubdivideThreshold and Depth < MaxDepth then
				ReassignObjects(BinaryOctree,ChosenNode,NodePosition)
			end

			break
		end

		NodePosition = NodePosition + (Size * SuffixToOrder[Suffix + 1])

		Size = Size / 2
		ChosenNode = NextNode
		Depth = Depth + 1
	end
end

function BinaryOctreeModule.RemoveObject(BinaryOctree : BinaryOctree,Object : Object)
	local Size = BinaryOctree.Size / 4
	local ChosenNode = 1
	local NodePosition = BinaryOctree.OffsetPosition
	local Position = Object.Position

	while true do
		local Suffix = 0

		if Position.X > NodePosition.X then
			Suffix += 4
		end

		if Position.Y > NodePosition.Y then
			Suffix += 2
		end

		if Position.Z > NodePosition.Z then
			Suffix += 1
		end

		local NextNode = ChosenNode * 8 + Suffix

		if BinaryOctree.Nodes[NextNode] == nil then
			local LeafNode = BinaryOctree.Nodes[ChosenNode]
			table.remove(LeafNode,table.find(LeafNode,Object))

			break
		end

		NodePosition = NodePosition + (Size * SuffixToOrder[Suffix + 1])
		Size = Size / 2
		ChosenNode = NextNode
	end
end

function BinaryOctreeModule.QueryBox(BinaryOctree : BinaryOctree,Position : Vector3,Size : Vector3) : {Object}
	local MinBound = Position - (Size / 2)
	local MaxBound = Position + (Size / 2)
	local ChosenNodes = {1}
	local Nodes = BinaryOctree.Nodes
	local GottenObjects = {}

	while #ChosenNodes > 0 do
		local Node = table.remove(ChosenNodes)
		local NodeTable = Nodes[Node]
		local NodePosition,NodeSize = GetNodePositionAndSize(BinaryOctree,Node)
		if not DetectBoxOverlap(MinBound,MaxBound,NodePosition - NodeSize,NodePosition + NodeSize) then continue end

		local ShifterNumber = Node * 8

		for HashSuffix = 0,7 do
			local NextNode = ShifterNumber + HashSuffix

			if Nodes[NextNode] == nil then
				if #NodeTable > 0 then
					for _,Object in NodeTable do
						if not IsPositionInBox(Object.Position,MinBound,MaxBound) then continue end

						table.insert(GottenObjects,Object)
					end
				end

				break
			end

			table.insert(ChosenNodes,NextNode)
		end
	end

	return GottenObjects
end

function BinaryOctreeModule.QuerySphere(BinaryOctree : BinaryOctree,Position : Vector3,Radius : number) : {Object}
	local ChosenNodes = {1}
	local Nodes = BinaryOctree.Nodes
	local GottenObjects = {}
	Radius *= Radius

	while #ChosenNodes > 0 do
		local Node = table.remove(ChosenNodes)
		local NodeTable = Nodes[Node]
		local NodePosition,NodeSize = GetNodePositionAndSize(BinaryOctree,Node)

		if not DetectBoxAndSphereOverlap(NodePosition - NodeSize,NodePosition + NodeSize,Position,Radius) then continue end

		local ShifterNumber = Node * 8

		for HashSuffix = 0,7 do
			local NextNode = ShifterNumber + HashSuffix

			if Nodes[NextNode] == nil then
				if #NodeTable > 0 then
					for _,Object in NodeTable do
						local SubtractedPositions = Position - Object.Position

						if Dot(SubtractedPositions,SubtractedPositions,SubtractedPositions) <= Radius then
							table.insert(GottenObjects,Object)
						end
					end
				end

				break
			end

			table.insert(ChosenNodes,NextNode)
		end
	end

	return GottenObjects
end

local function MakeVisualisePart(Size, MaxBound, Suffix)
	local MiddlePosition = MaxBound

	local VisualizePart = Instance.new("Part")
	VisualizePart.Parent = workspace
	VisualizePart.Position = MiddlePosition
	VisualizePart.Size = Vector3.new(Size,Size,Size)
	VisualizePart.Anchored = true
	VisualizePart.Transparency = 1
	VisualizePart.Name = Suffix

	local SelectionBox = Instance.new("SelectionBox")
	SelectionBox.Parent = VisualizePart
	SelectionBox.Adornee = VisualizePart
end

function BinaryOctreeModule.VisualizeOctree(BinaryOctree : BinaryOctree)
	local UnvisualisedNodes = {{1,Size = BinaryOctree.Size}}

	while #UnvisualisedNodes > 0 do
		local Node = table.remove(UnvisualisedNodes)
		local NodePosition = GetNodePositionAndSize(BinaryOctree,Node[1])

		MakeVisualisePart(Node.Size,NodePosition,Node[1])

		for i = 0,7 do
			local HashIndex = Node[1] * 8 + i

			if BinaryOctree.Nodes[HashIndex] == nil then continue end

			table.insert(UnvisualisedNodes,{HashIndex, Size = Node.Size / 2})
		end
	end
end

return BinaryOctreeModule


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

local INIT_PATH = "src/init.luau"

local BUNDLE_PATH = "bin/PNGuin.luau"
local RBXM_PATH = "bin/PNGuin.rbxm"

local function buildPesde()
	local result = process.exec("darklua", {"process", INIT_PATH, BUNDLE_PATH})
	assert(result.ok, result.stderr)
end

local function buildRBXM()
	local result = process.exec("rojo", {"build", "--output", RBXM_PATH})
	assert(result.ok, result.stderr)
end

local clock = os.clock()

buildPesde()
buildRBXM()

print(string.format("Build completed in %.2fs", os.clock() - clock))


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

local PESDE_CONFIG_PATH = "pesde.toml"

local function writeVersion(version: string)
	local data = fs.readFile(PESDE_CONFIG_PATH)
	local config: {version: string} = serde.decode("toml", data)
	config.version = version

	fs.writeFile(PESDE_CONFIG_PATH, serde.encode("toml", config, true))
end

if #process.args == 0 then
	error("No version provided")
end

local version = process.args[1]
writeVersion(version)

print(string.format("Version updated to %s", version))

-- 1axen my goat


--!strict
--!native
--!optimize 2

local BitStream = require("../Parsing/BitStream")

type BitStream = BitStream.BitStream

type HuffmanNode = {
	[number]: HuffmanNode
} & {sym: number?}

export type HuffmanTree = {
	root: HuffmanNode,

	Read: (self: HuffmanTree, stream: BitStream) -> number
}

local function Read(self: HuffmanTree, stream: BitStream): number
	local cur = self.root

	local pos = 0
	local chunk = stream:Bits(16)

	while true do
		local sym = cur.sym
		if sym then
			local shift = stream.offset - (16 - pos)
			stream.pos += shift // 8
			stream.offset = shift % 8
			return sym
		end

		local bit = chunk % 2^1
		chunk //= 2^1

		cur = cur[bit]
		if not cur then
			error("Invalid huffman code")
		end

		pos += 1
	end
end

local HuffmanTree = {}

function HuffmanTree.new(info: {number}): HuffmanTree
	local maxLen = 0
	local lenCount: {number} = {}

	for _, len in info do
		if len > 0 then
			lenCount[len] = (lenCount[len] or 0) + 1
			if len > maxLen then maxLen = len end
		end
	end

	local nextCode = table.create(maxLen, 0)

	local code = 0
	for len = 1, maxLen do
		code = (code + (lenCount[len - 1] or 0)) * 2^1
		nextCode[len] = code
	end

	local root: HuffmanNode = {}

	for sym, len in info do
		if len > 0 then
			local cur = nextCode[len]
			nextCode[len] += 1

			local node = root
			for i = len - 1, 0, -1 do
				local bit = bit32.extract(cur, i)

				if not node[bit] then node[bit] = {} end
				node = node[bit]
			end

			node.sym = sym - 1
		end
	end

	return {
		root = root,

		Read = Read
	}
end

function HuffmanTree.newRepeat(info: {number}): HuffmanTree
	local newInfo: {number} = {}

	for i = 1, #info - 2, 2 do
		local idxStart = info[i]
		local len = info[i + 1]
		local idxEnd = info[i + 2]

		for _ = idxStart, idxEnd - 1 do
			table.insert(newInfo, len)
		end
	end

	return HuffmanTree.new(newInfo)
end

return HuffmanTree


--!strict
--!native
--!optimize 2

local BitStream = require("../Parsing/BitStream")

type BitStream = BitStream.BitStream

export type Parser = {
	_output: buffer,
	_outputPos: number,
	_outputChunks: {buffer},

	window: buffer,
	windowPos: number,
	windowSize: number,

	stream: BitStream,

	ToOutput: (self: Parser, val: number) -> (),
	ToOutputBulk: (self: Parser, val: string) -> (),
	ToWindow: (self: Parser, val: number) -> (),
	ToWindowBulk: (self: Parser, val: string) -> (),
	ReadWindow: (self: Parser, pos: number) -> number,
	ReadWindowBulk: (self: Parser, pos: number, len: number) -> string,

	Finalize: (self: Parser) -> buffer
}

local function ToOutput(self: Parser, val: number): ()
	local output = self._output
	local outputPos = self._outputPos
	local windowSize = self.windowSize

	buffer.writeu8(output, outputPos, val)

	if outputPos + 1 == windowSize then
		table.insert(self._outputChunks, output)

		local outputNew = buffer.create(windowSize)
		self._output = outputNew
		self._outputPos = 0
	else
		self._outputPos += 1
	end
end

local function ToOutputBulk(self: Parser, val: string): ()
	local output = self._output
	local outputPos = self._outputPos
	local windowSize = self.windowSize

	local len = #val

	if outputPos + len >= windowSize then
		local split = windowSize - outputPos
		buffer.writestring(output, outputPos, val, split)
		table.insert(self._outputChunks, output)

		local outputNew = buffer.create(windowSize)
		self._output = outputNew

		buffer.writestring(outputNew, 0, val:sub(split + 1), len - split)
		self._outputPos = len - split
	else
		buffer.writestring(output, outputPos, val, len)
		self._outputPos += len
	end
end

local function ToWindow(self: Parser, val: number): ()
	local window = self.window
	local windowPos = self.windowPos
	local windowSize = self.windowSize

	buffer.writeu8(window, windowPos, val)

	if windowPos + 1 == windowSize then
		self.windowPos = 0
	else
		self.windowPos += 1
	end
end

local function ToWindowBulk(self: Parser, val: string): ()
	local window = self.window
	local windowPos = self.windowPos
	local windowSize = self.windowSize

	local len = #val

	if windowPos + len >= windowSize then
		local split = windowSize - windowPos

		buffer.writestring(window, windowPos, val, split)
		buffer.writestring(window, 0, val:sub(split + 1), len - split)
		self.windowPos = len - split
	else
		buffer.writestring(window, windowPos, val, len)
		self.windowPos += len
	end
end

local function ReadWindow(self: Parser, pos: number): number
	return buffer.readu8(self.window, pos)
end

local function ReadWindowBulk(self: Parser, pos: number, len: number): string
	local window = self.window
	local windowSize = self.windowSize

	if pos + len > windowSize then
		local split = windowSize - pos
		return buffer.readstring(window, pos, split) .. buffer.readstring(window, 0, len - split)
	else
		return buffer.readstring(self.window, pos, len)
	end
end

local function Finalize(self: Parser): buffer
	local outputPos = self._outputPos
	local outputChunks = self._outputChunks
	local windowSize = self.windowSize

	local result = buffer.create(#outputChunks * windowSize + outputPos)

	for i, chunk in outputChunks do
		buffer.copy(result, (i - 1) * windowSize, chunk, 0, windowSize)
	end

	if outputPos > 0 then
		buffer.copy(result, #outputChunks * windowSize, self._output, 0, outputPos)
	end

	return result
end

local Parser = {}

function Parser.new(stream: BitStream, windowSize: number): Parser
	return {
		_output = buffer.create(windowSize),
		_outputPos = 0,
		_outputChunks = {},

		window = buffer.create(windowSize),
		windowPos = 0,
		windowSize = windowSize,

		stream = stream,

		ToOutput = ToOutput,
		ToOutputBulk = ToOutputBulk,
		ToWindow = ToWindow,
		ToWindowBulk = ToWindowBulk,
		ReadWindow = ReadWindow,
		ReadWindowBulk = ReadWindowBulk,

		Finalize = Finalize
	}
end

return Parser


--!strict
--!native
--!optimize 2

local HuffmanTree = require("../Decoding/HuffmanTree")
local Parser = require("../Decoding/Parser")

local BitStream = require("../Parsing/BitStream")

type BitStream = BitStream.BitStream
type HuffmanTree = HuffmanTree.HuffmanTree
type Parser = Parser.Parser

local lenBase = {
	3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31,
	35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258
}
local lenExtra = {
	0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2,
	3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0
}
local distBase = {
	1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193,
	257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577
}
local distExtra = {
	0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6,
	7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13
}
local clenOrder = {16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15}

local function buildDynamicTrees(stream: BitStream): (HuffmanTree, HuffmanTree)
	local hlit = stream:Bits(5) + 257
	local hdist = stream:Bits(5) + 1
	local hclen = stream:Bits(4) + 4

	local codeLens: {number} = table.create(19, 0)
	for i = 1, hclen do
		codeLens[clenOrder[i] + 1] = stream:Bits(3)
	end

	local clenTree = HuffmanTree.new(codeLens)

	local totalCodes = hlit + hdist
	local symLens = table.create(totalCodes, 0)

	local i = 1
	while i <= totalCodes do
		local sym = clenTree:Read(stream)
		local val = 0
		local rep = 1

		if sym <= 15 then
			val = sym
		elseif sym == 16 then
			rep = stream:Bits(2) + 3
			val = symLens[i - 1]
		elseif sym == 17 then
			rep = stream:Bits(3) + 3
			val = 0
		elseif sym == 18 then
			rep = stream:Bits(7) + 11
			val = 0
		end

		for _ = 1, rep do
			symLens[i] = val
			i += 1
		end
	end

	local litTree = HuffmanTree.new(
		table.move(symLens, 1, hlit, 1, table.create(hlit, 0))
	)
	local distTree = HuffmanTree.new(
		table.move(symLens, hlit + 1, totalCodes, 1, table.create(hdist, 0))
	)

	return litTree, distTree
end

local function parseCompressed(self: Parser, litTree: HuffmanTree, distTree: HuffmanTree): ()
	local windowSize = self.windowSize
	local stream = self.stream

	while true do
		local sym = litTree:Read(stream)

		if sym == 256 then
			break
		elseif sym < 256 then
			self:ToOutput(sym)
			self:ToWindow(sym)
		else
			local idxLen = sym - 256
			local len = lenBase[idxLen] + stream:Bits(lenExtra[idxLen])

			local idxDist = distTree:Read(stream) + 1
			local dist = distBase[idxDist] + stream:Bits(distExtra[idxDist])

			local pos = (self.windowPos - dist) % windowSize

			local chunk = math.min(dist, len)
			if chunk > 0 then
				local val = self:ReadWindowBulk(pos, chunk)
				self:ToOutputBulk(val)
				self:ToWindowBulk(val)
			end

			if chunk >= len then continue end

			for _ = chunk + 1, len do
				pos = (self.windowPos - dist) % windowSize
				local val = self:ReadWindow(pos)

				self:ToOutput(val)
				self:ToWindow(val)
			end
		end
	end
end

local function parseBlocks(self: Parser): buffer
	local litFixed = HuffmanTree.newRepeat({0, 8, 144, 9, 256, 7, 280, 8, 288})
	local distFixed = HuffmanTree.newRepeat({0, 5, 32})

	local stream = self.stream

	while true do
		local read = stream:Bits(1 + 2)

		local final = read % 2^1
		local blockType = read // 2^1

		if blockType > 2 then
			error(string.format(
				"Invalid block type (%d)",
				blockType
			))
		elseif blockType == 0 then
			stream:Align()
			local len = stream:UInt(2 + 2) % 2^16

			local block = stream:String(len)
			self:ToOutputBulk(block)
			self:ToWindowBulk(block)
		else
			local litTree, distTree = litFixed, distFixed
			if blockType == 2 then
				litTree, distTree = buildDynamicTrees(stream)
			end

			parseCompressed(self, litTree, distTree)
		end

		if final == 1 then break end
	end

	return self:Finalize()
end

local Zlib = {}

function Zlib.Decompress(data: buffer): buffer
	local stream = BitStream.new(data)

	local cmf = stream:UInt(1)

	local compMethod = cmf % 2^4
	if compMethod ~= 8 then
		error(string.format(
			"Invalid Zlib compression method (%d): Only DEFLATE (8) is supported",
			compMethod
		))
	end

	local compInfo = (cmf // 2^4) % 2^4
	if compInfo > 7 then
		error(string.format(
			"Invalid Zlib window size: Expected <32KB, got %dKB",
			2^(compInfo + 8) // 1000
		))
	end

	local flg = stream:UInt(1)

	if (cmf * 2^8 + flg) % 31 ~= 0 then
		error("Invalid Zlib header: CMF+FLG checksum mismatch")
	end

	if (flg // 2^5) % 2^1 ~= 0 then
		error("Invalid Zlib header: preset dictionary is not supported")
	end

	return parseBlocks(
		Parser.new(stream, 2^(compInfo + 8))
	)
end

return Zlib


--!strict
--!native
--!optimize 2

export type BitStream = {
	data: buffer,
	len: number,
	pos: number,
	offset: number,

	Bits: (self: BitStream, width: number) -> number,
	UInt: (self: BitStream, width: number) -> number,
	String: (self: BitStream, length: number) -> string,
	Align: (self: BitStream) -> ()
}

local readMap = {
	[1] = buffer.readu8,
	[2] = buffer.readu16,
	[4] = buffer.readu32
}

local function Bits(self: BitStream, width: number): number
	local pos = self.pos
	local offset = self.offset

	local dist = (self.len - pos) * 8 - offset
	width = if width > dist then dist else width
	local shift = offset + width

	-- previously bit32.extract(buffer.readu32(self.data, self.pos), offset, width)
	local num = buffer.readbits(self.data, pos * 8 + offset, width)
	self.pos += shift // 8
	self.offset = shift % 8
	return num
end

local function UInt(self: BitStream, width: number): number
	local num = readMap[width](self.data, self.pos)
	self.pos += width
	return num
end

local function String(self: BitStream, length: number): string
	local str = buffer.readstring(self.data, self.pos, length)
	self.pos += length
	return str
end

local function Align(self: BitStream): ()
	if self.offset > 0 then
		self.pos += 1
		self.offset = 0
	end
end

local BitStream = {}

function BitStream.new(data: buffer): BitStream
	return {
		data = data,
		len = buffer.len(data),
		pos = 0,
		offset = 0,

		Bits = Bits,
		UInt = UInt,
		String = String,
		Align = Align,
	}
end

return BitStream


--!strict
--!native
--!optimize 2

export type ByteStream = {
	data: buffer,
	len: number,
	pos: number,

	UInt: (self: ByteStream, width: number, big: boolean?) -> number,
	String: (self: ByteStream, length: number) -> string,
}

local readMap = {
	[1] = buffer.readu8,
	[2] = buffer.readu16,
	[4] = buffer.readu32
}

local function UInt(self: ByteStream, width: number, big: boolean?): number
	local num = readMap[width](self.data, self.pos)
	self.pos += width

	return if big
		then bit32.byteswap(bit32.lshift(num, (4 - width) * 8))
		else num
end

local function String(self: ByteStream, length: number): string
	local str = buffer.readstring(self.data, self.pos, length)
	self.pos += length
	return str
end

local ByteStream = {}

function ByteStream.new(data: buffer): ByteStream
	return {
		data = data,
		len = buffer.len(data),
		pos = 0,

		UInt = UInt,
		String = String
	}
end

return ByteStream


--!strict
--!native
--!optimize 2

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

type PNGInfo = Types.PNGInfo
type ColorType = Types.ColorType
type ByteStream = ByteStream.ByteStream

export type ChunkStream = {
	info: PNGInfo,
	stream: ByteStream,

	Read: (self: ChunkStream, chunkType: string, length: number) -> ()
}

local bitDepthMap = {
	[0] = {1, 2, 4, 8, 16},
	[2] = {8, 16},
	[3] = {1, 2, 4, 8},
	[4] = {8, 16},
	[6] = {8, 16},
}

local chunks = {
	IHDR = function(self: ChunkStream, chunkLen: number): ()
		if chunkLen ~= 13 then
			error(string.format(
				"Invalid IHDR chunk length: Expected 13 bytes, got %d",
				chunkLen
			))
		end

		local info = self.info
		local stream = self.stream

		local width = stream:UInt(4, true)
		local height = stream:UInt(4, true)
		if width == 0 or height == 0 then
			error("Invalid image dimensions")
		end

		local bitDepth = stream:UInt(1)
		local colorSpace = stream:UInt(1)
		if not bitDepthMap[colorSpace] then
			error("Invalid color space")
		elseif not table.find(bitDepthMap[colorSpace], bitDepth) then
			error("Invalid bit depth")
		end

		local colorType: ColorType =
			if colorSpace % 4 == 0 then "Grayscale"
			elseif colorSpace == 3 then "Indexed"
			else "Truecolor"

		local hasAlpha = colorSpace > 3

		local channels = 1
			+ (colorType == "Truecolor" and 2 or 0)
			+ (hasAlpha and 1 or 0)

		local compMethod = stream:UInt(1)
		if compMethod > 0 then
			error(string.format(
				"Invalid compression method (%d): Only DEFLATE (0) is supported",
				compMethod
			))
		end

		local filterMethod = stream:UInt(1)
		if filterMethod > 0 then
			error(string.format(
				"Invalid filter method (%d): Only adaptive filtering (0) is supported",
				filterMethod
			))
		end

		local interlacing = stream:UInt(1)
		if interlacing > 0 then
			error("Interlacing is not supported")
		end

		info.width = width
		info.height = height

		info.bitDepth = bitDepth
		info.colorType = colorType
		info.hasAlpha = hasAlpha
		info.channels = channels
	end,

	PLTE = function(self: ChunkStream, chunkLen: number): ()
		if chunkLen % 3 ~= 0 then
			error(string.format(
				"Invalid PLTE chunk length: %d is not divisible by 3",
				chunkLen
			))
		end

		local info = self.info
		local stream = self.stream

		info.PLTE = buffer.fromstring(stream:String(chunkLen))
	end,

	tRNS = function(self: ChunkStream, chunkLen: number): ()
		local info = self.info
		local stream = self.stream

		info.tRNS = buffer.fromstring(stream:String(chunkLen))
	end,

	IDAT = function(self: ChunkStream, chunkLen: number): ()
		local info = self.info
		local stream = self.stream

		local IDAT: {string} = {stream:String(chunkLen)}

		while true do
			stream.pos += 4

			local length = stream:UInt(4, true)
			if stream:String(4) ~= "IDAT" then
				stream.pos -= 12
			break end

			table.insert(IDAT, stream:String(length))
		end

		info.IDAT = buffer.fromstring(table.concat(IDAT))
	end,
}

local function Read(self: ChunkStream, chunkType: string, chunkLen: number): ()
	local func = chunks[chunkType]
	if not func then
		self.stream.pos += chunkLen
	else
		func(self, chunkLen)
	end
end

local ChunkStream = {}

function ChunkStream.new(info: PNGInfo, stream: ByteStream): ChunkStream
	return {
		info = info,
		stream = stream,

		Read = Read
	}
end

return ChunkStream


--!strict
--!optimize 2

export type PNGInfo = {
	IDAT: buffer,
	PLTE: buffer,
	tRNS: buffer?,

	width: number,
	height: number,

	bitDepth: number,
	colorType: ColorType,
	hasAlpha: boolean,
	channels: number,

	--[=[
		Decodes IDAT chunks into (RGBA) pixel data

		@within PNGInfo

		@return buffer -- Pixel data
		@return number -- Time taken to decode in seconds
	]=]
	DecodeRGBA: (self: PNGInfo) -> (buffer, number)
}

export type ColorType = "Truecolor" | "Grayscale" | "Indexed"

return {}


--!strict
--!native
--!optimize 2

local Types = require("@self/Types")
local ChunkStream = require("@self/Parsing/ChunkStream")
local ByteStream = require("@self/Parsing/ByteStream")
local Zlib = require("@self/Decoding/Zlib")
local Filter = require("@self/Decoding/Filter")

type PNGInfo = Types.PNGInfo

local PNGuin = {}

local function DecodeRGBA(self: PNGInfo): (buffer, number)
	local clock = os.clock()

	local filtered = Zlib.Decompress(self.IDAT)
	local pixelData = Filter.Unfilter(self, filtered)

	return pixelData, os.clock() - clock
end

--[=[
	Parses PNG data into `PNGInfo`

	### Example Usage:
	```lua
	local png -- Your PNG binary data

	local pngInfo = PNGuin.Extract(png)
	local pixelData, elapsed = pngInfo:DecodeRGBA()
	```

	@within PNGuin

	@param input -- PNG binary data as a `buffer` or `string`

	@return PNGInfo -- Extracted information
]=]
function PNGuin.Extract(input: buffer | string): PNGInfo
	local info: PNGInfo = {
		IDAT = buffer.create(0),
		PLTE = buffer.create(0),
		tRNS = nil,

		width = 0,
		height = 0,

		bitDepth = 8,
		colorType = "Truecolor",
		hasAlpha = true,
		channels = 6,

		DecodeRGBA = DecodeRGBA
	}

	local data = if typeof(input) == "buffer" then input
		else buffer.fromstring(input)

	local stream = ByteStream.new(data)
	local chunkStream = ChunkStream.new(info, stream)

	local signature = stream:String(8)
	if signature ~= "\137PNG\r\n\26\n" then
		error("Invalid PNG signature")
	end

	while true do
		if stream.pos + 8 > stream.len then
			error("IEND chunk not found")
		end

		local chunkLen = stream:UInt(4, true)
		local chunkType = stream:String(4)

		if chunkType == "IEND" then break end
		chunkStream:Read(chunkType, chunkLen)
		stream.pos += 4
	end

	return info
end

return PNGuin


local Package = script.Parent
local Packages = Package.Parent

local React = require(Packages.React)
local Signal = require(Packages.Signal)

--[=[
	A React component.

	@type Component ReactComponent<unknown>
	@within ReactMatter
]=]
export type Component = React.ComponentType<unknown>

--[=[
	A mapping of component keys to components.

	@type Components { [string]: Component }
	@within ReactMatter
]=]
export type Components = { [string]: Component }

local components: Components = {}
local componentsChanged = Signal.new()

local function updateComponents()
	componentsChanged:Fire(components)
end

local function insertComponent(key: string, component: Component)
	assert(not components[key], `Duplicate system component key '{key}'. Keys must be unique.`)
	components[key] = component
	updateComponents()
end

local function removeComponent(key: string)
	assert(
		components[key],
		`No system component with key '{key}'. This indicates a duplicate key or a missing component.`
	)
	components[key] = nil
	updateComponents()
end

return {
	Components = components,
	ComponentsChanged = componentsChanged.Event,

	insertComponent = insertComponent,
	removeComponent = removeComponent,
}


local Package = script.Parent.Parent
local Packages = Package.Parent
local Contexts = Package.contexts

local Params = require(Contexts.Params)
local React = require(Packages.React)
local Return = require(Contexts.Return)
local Signal = require(Packages.Signal)

local e = React.createElement
local useEffect = React.useEffect
local useState = React.useState

--[=[
	A complete state for a set of system components.

	Indicates that the components have completed rendering and are current. The
	`value` field contains the return value of the system components.

	```lua
	local ready = useReact({
		Component = MyComponent,
	}, {})

	if ready.completed then
		print("The return value is", ready.value)
	end
	```

	.completed true -- The components are rendered
	.value T -- The return value

	@interface Complete<T>
	@within ReactMatter
]=]
export type Complete<T> = {
	completed: true,
	value: T,
}

--[=[
	A previously complete state for a set of system components.

	Indicates that the components have completed rendering at some point, however
	the set of components or parameters have changed and a newly rendered value is
	not yet available. The `value` field contains the return value of the system
	components.

	```lua
	local ready = useReact({
		Component = MyComponent,
	}, {})

	if not ready.completed then
		print("The components are not yet ready.")

		if ready.value then
			print("The most recent return value is", ready.value)
		end
	end
	```

	.completed false -- The components are not yet rendered
	.value T -- The most recent return value

	@interface PreviouslyComplete<T>
	@within ReactMatter
]=]
export type PreviouslyComplete<T> = {
	completed: false,
	value: T,
}

--[=[
	An incomplete state for a set of system components.

	Indicates that the components have not yet completed rendering. No `value` is
	yet available.

	```lua
	local ready = useReact({
		Component = MyComponent,
	}, {})

	if not ready.completed then
		print("The components are not yet ready.")
	end
	```

	.completed false -- The components are not yet rendered

	@interface Incomplete
	@within ReactMatter
]=]
export type Incomplete = {
	completed: false,
}

--[=[
	A ready state for a set of system components.

	Informs the system of the result of rendering components. It is always one of
	three states: [[Complete]], [[PreviouslyComplete]], or [[Incomplete]].

	The `completed` field always exists and indicates whether the components have
	completed rendering since the components or parameters changed.

	The `value` field is only present when the components have successfully
	rendered at some point. It contains the return value of the components.

	```lua
	local ready = useReact({
		Component = MyComponent,
	}, {})

	if ready.completed then
		print("The return value is", ready.value)
	end
	```

	@type Ready<T> Complete<T> | PreviouslyComplete<T> | Incomplete
	@within ReactMatter
]=]
export type Ready<T> = Complete<T> | PreviouslyComplete<T> | Incomplete

export type Passed<Params, Return> = {
	params: Params,
	ready: Ready<Return>,
}

local function SystemComponent<Props, Params, Return>(
	component: React.ComponentType<Props>,
	passed: Passed<Params, Return>,
	signal: typeof(Signal.new())
): React.ComponentType<Props>
	return function(props: Props): React.ReactNode
		local passed, updatePassed = useState(passed)

		local function updateReturn(value: Return | (value: Return?) -> Return)
			if type(value) == "function" then
				updatePassed(function(oldPassed: Passed<Params, Return>): Passed<Params, Return>
					local newPassed = {
						params = oldPassed.params,
						ready = {
							completed = true,
							value = value(
								if oldPassed.ready.completed then oldPassed.ready.value else nil
							),
						},
					}

					signal:Fire(newPassed)
					return newPassed
				end)
			else
				local newPassed = {
					params = passed.params,
					ready = {
						completed = true,
						value = value,
					},
				}

				signal:Fire(newPassed)
			end
		end

		useEffect(function()
			local connection = signal:Connect(function(value: Passed<Params, Return>)
				updatePassed(value)
			end)

			return function()
				connection.disconnect()
			end
		end, {})

		return e(Params.Provider, {
			value = {
				params = passed.params,
			},
		}, {
			e(Return.Provider, {
				value = {
					updateReturn = updateReturn,
				},
			}, {
				SystemComponent = e(component, props),
			}),
		})
	end
end

return SystemComponent


local Package = script.Parent.Parent
local Packages = Package.Parent

local React = require(Packages.React)

export type ParamType<T> = T & {}

export type Params<T> = {
	params: ParamType<T>?,
}

local defaultParams: Params<unknown> = {}

local Params = React.createContext(defaultParams)

return Params


local Package = script.Parent.Parent
local Packages = Package.Parent

local React = require(Packages.React)

--[=[
	Updates the return value of a system component.

	@type UpdateReturn<T> (value: T | (value: T?) -> T) -> ()
	@within ReactMatter
]=]
export type UpdateReturn<T> = (value: T | (value: T?) -> T) -> ()

export type Return<T> = {
	updateReturn: UpdateReturn<T>?,
}

local defaultReturn: Return<unknown> = {}

local Return = React.createContext(defaultReturn)

return Return


local function diffTables(left: { [unknown]: unknown }?, right: { [unknown]: unknown }?): boolean
	if left and right then
		if left == right then
			return false
		end

		local size = 0

		for key, value in left do
			if value ~= right[key] then
				return true
			end
			size += 1
		end

		for _ in right do
			size -= 1
		end

		if size ~= 0 then
			return true
		end

		return false
	end

	return true
end

return diffTables


local Package = script.Parent
local Packages = Package.Parent
local Hooks = Package.hooks

local React = require(Packages.React)
local useParams = require(Hooks.useParams)
local useReact = require(Package.useReact)
local useReturn = require(Hooks.useReturn)

local e = React.createElement
local Fragment = React.Fragment
local useEffect = React.useEffect
local useState = React.useState

local function merge(left, right)
	local result = table.clone(left)

	for key, value in right do
		result[key] = value
	end

	return result
end

local function filter(left, right)
	local result = table.clone(left)

	for key in right do
		result[key] = nil
	end

	return result
end

local function getComponentsINeedButtonsFor(world)
	return world
end

local function ComponentButton() end

local function RenderButtons()
	local componentsWithButtons = useParams()
	local updateReturn = useReturn()
	local buttonsPressed, updateState = useState({})

	useEffect(function()
		updateReturn(buttonsPressed)
	end, { buttonsPressed })

	local buttons = {}
	for _, component in componentsWithButtons do
		table.insert(
			buttons,
			e(ComponentButton, {
				text = component,

				onPress = function(pressed)
					if pressed then
						updateState(function(state)
							merge(state, { [component] = true })
						end)
					else
						updateState(function()
							filter(buttonsPressed, { [component] = true })
						end)
					end
				end,
			})
		)
	end

	return e(Fragment, buttons)
end

local function mySystem(world)
	local componentsWithButtons = getComponentsINeedButtonsFor(world)

	local pressed = useReact({
		Buttons = RenderButtons,
	}, { componentsWithButtons })

	if pressed.completed then
		for component in pressed.value do
			print(component, "was pressed.")
		end
	end
end

return mySystem


local Package = script.Parent.Parent
local Packages = Package.Parent
local Contexts = Package.contexts

local Params = require(Contexts.Params)
local React = require(Packages.React)

local useContext = React.useContext

--[=[
	Retrieves the parameters passed to a system component.

	This is a hook that can only be called within a `useReact` context, typically
	within a system component. It returns the parameters passed to the system
	component.

	```lua
	local function MySystemComponent()
		local x, y, z = useParams()

		-- ...
	end
	```

	Notably these parameters are distinct from the props passed to a system
	component. Parameters are passed to a system component via the `useReact`
	matter hook, while props are passed to a system component via the
	[[RenderSystems]] component from within a React render tree. Both can be used
	simultaneously to pass data to a system component, but originate from
	different sources.

	```lua
	local function MySystemComponent(props)
		local param = useParams() -- Comes from a Matter system
		local prop = props.value -- Comes from a React component

		-- ...
	end
	```

	@within ReactMatter
	@tag ReactHook
]=]
local function useParams<T>(): ...T
	local paramsContext: Params.Params<T> = useContext(Params)

	if not paramsContext.params then
		error("'useParams' was called outside of a 'useReact' context.")
	end

	return table.unpack(paramsContext.params)
end

return useParams


local Package = script.Parent.Parent
local Packages = Package.Parent
local Contexts = Package.contexts

local React = require(Packages.React)
local Return = require(Contexts.Return)

local useContext = React.useContext

export type UpdateReturn<T> = Return.UpdateReturn<T>

--[=[
	Retrieves the update function for the return value of a system component.

	This is a hook that can only be called within a `useReact` context, typically
	within a system component. It returns the function that can be used to update
	the return value of the system component.

	```lua
	local function MySystemComponent()
		local updateReturn = useReturn()

		useEffect(function()
			updateReturn(10)

			-- or

			updateReturn(function(value)
				return value + 1
			end)
		end)

		-- ...
	end
	```

	@within ReactMatter
	@tag ReactHook
]=]
local function useReturn<T>(): UpdateReturn<T>
	local returnContext: Return.Return<T> = useContext(Return)

	if not returnContext.updateReturn then
		error("'useReturn' was called outside of a 'useReact' context.")
	end

	return returnContext.updateReturn
end

return useReturn


local RenderSystems = require(script.components.RenderSystems)
local useParams = require(script.hooks.useParams)
local useReact = require(script.useReact)
local useReturn = require(script.hooks.useReturn)

export type Component = useReact.Component
export type Components = useReact.Components
export type Complete<T> = useReact.Complete<T>
export type PreviouslyComplete<T> = useReact.PreviouslyComplete<T>
export type Incomplete = useReact.Incomplete
export type Ready<T> = useReact.Ready<T>
export type UpdateReturn<T> = useReturn.UpdateReturn<T>
export type RenderProps
 = RenderSystems.RenderProps

export type RenderSystemsProps
 = RenderSystems.RenderSystemsProps

--[=[
	@class ReactMatter
]=]

return {
	useReact = useReact,

	useParams = useParams,
	useReturn = useReturn,

	RenderSystems = RenderSystems,
}


local Package = script.Parent
local Packages = Package.Parent
local Components = Package.components

local Matter = require(Packages.Matter)
local RegisteredComponents = require(Package.RegisteredComponents)
local Signal = require(Packages.Signal)
local SystemComponent = require(Components.SystemComponent)
local diffTables = require(Package.diffTables)

export type Component = RegisteredComponents.Component
export type Components = RegisteredComponents.Components
export type Complete<T> = SystemComponent.Complete<T>
export type PreviouslyComplete<T> = SystemComponent.PreviouslyComplete<T>
export type Incomplete = SystemComponent.Incomplete
export type Ready<T> = SystemComponent.Ready<T>
export type Passed<Params, Return> = SystemComponent.Passed<Params, Return>

type Storage<Params, Return> = {
	components: Components?,
	passed: Passed<Params, Return>?,
	signal: typeof(Signal.new())?,
}

local function clearComponents<Params, Return>(storage: Storage<Params, Return>)
	if storage.components then
		for key in storage.components do
			RegisteredComponents.removeComponent(key)
		end
	end
end

local function cleanup<Params, Return>(storage: Storage<Params, Return>)
	clearComponents(storage)

	if storage.signal then
		storage.signal:DisconnectAll()
	end
end

--[=[
	Renders a set of components from within a Matter system.

	This is a Matter hook that can be used to render a set of components from
	within a Matter system. It's able to pass parameters to the components and
	receive a return value from the components.

	```lua
	local function mySystem()
		useReact({
			MyComponent = MyComponent,
		}, {})
	end
	```

	Parameters can be passed to the components by providing a table of parameters
	to the `useReact` hook. The parameters can be updated by providing a new set
	of parameters to the `useReact` hook at any time. Treat these parameters as
	if they were dependencies of some other hook. When the parameters change, the
	components will be re-rendered with the new parameters, and the parameters
	should be immutable.

	```lua
	local function mySystem()
		useReact({
			MyComponent = MyComponent,
		}, { param1, param2, param3 })
	end
	```

	When the components are rendered, they may return a value. This value can be
	read by the system by calling the `useReact` hook. The return value is not
	immediately available as React may not have rendered the components yet. The
	returned [[Ready]] value indicates whether the components have completed
	rendering and provides the return value of the components.

	```lua
	local function mySystem()
		local ready = useReact({
			MyComponent = MyComponent,
		}, {})

		if ready.completed then
			print("The component returned", ready.value)
		end
	end
	```
]=]
local function useReact<Params, Return>(
	components: Components,
	dependencies: Params & {},
	discriminator: unknown?
): Ready<Return>
	local storage = Matter.useHookState(discriminator, cleanup) :: Storage<Params, Return>

	if diffTables(components, storage.components) then
		clearComponents(storage)
		storage.components = components

		if not storage.passed then
			storage.passed = {
				params = {},
				ready = {
					completed = false,
				},
			}

			storage.signal = Signal.new()

			storage.signal:Connect(function(value: Passed<Params, Return>)
				storage.passed = value
			end)
		end

		for key, component in components do
			local systemComponent = SystemComponent(component, storage.passed, storage.signal)
			RegisteredComponents.insertComponent(key, systemComponent)
		end
	end

	if diffTables(dependencies, storage.passed.params) then
		storage.passed = {
			params = dependencies,
			ready = {
				completed = false,
				value = if storage.ready and storage.ready.completed
					then storage.ready.value
					else nil,
			},
		}

		storage.signal:Fire(storage.passed)
	end

	return storage.passed.ready
end

return useReact


local rootTree = require(script.Parent.rootTree)

local function getRoots(node, parent)
	local roots = {}

	for key, subNode in node do
		local subRoots =
			getRoots(subNode, parent:FindFirstChild(key) or error("Could not find child" .. key))
		table.move(subRoots, 1, #subRoots + 1, #roots + 1, roots)
	end

	if #roots == 0 then
		table.insert(roots, parent)
	end

	return roots
end

local function getAllRoots(node)
	local roots = {}

	for key, subNode in node do
		local subRoots = getRoots(subNode, game:GetService(key))
		table.move(subRoots, 1, #subRoots + 1, #roots + 1, roots)
	end

	return roots
end

return getAllRoots(rootTree)


local roots = require(script.Parent.roots)
local test = require(script.Parent.tests)

local completed, result = test(roots)

if completed then
	if not result then
		error("Tests have failed.", 0)
	end
else
	error(result, 0)
end


local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TestEZ = require(ReplicatedStorage.DevPackages.TestEZ)

local function test(roots)
	print()
	local completed, result = xpcall(function()
		local results = TestEZ.TestBootstrap.run(roots)
		return results.failureCount == 0
	end, debug.traceback)
	print()
	return completed, result
end

return test