local Argon = script:FindFirstAncestor("Argon")
local App = Argon.App
local Components = App.Components
local Util = Components.Util

local Fusion = require(Argon.Packages.Fusion)

local GlobalUtil = require(Argon.Util)

local Theme = require(App.Theme)
local Assets = require(App.Assets)
local animate = require(Util.animate)
local stripProps = require(Util.stripProps)
local getTextSize = require(Util.getTextSize)
local getState = require(Util.getState)
local isState = require(Util.isState)

local Container = require(Components.Container)
local Padding = require(Components.Padding)
local Corner = require(Components.Corner)
local Image = require(Components.Image)
local Input = require(Components.Input)
local List = require(Components.List)
local Box = require(Components.Box)

local New = Fusion.New
local Ref = Fusion.Ref
local Value = Fusion.Value
local Spring = Fusion.Spring
local Hydrate = Fusion.Hydrate
local OnEvent = Fusion.OnEvent
local Cleanup = Fusion.Cleanup
local Computed = Fusion.Computed
local Children = Fusion.Children
local Observer = Fusion.Observer
local ForValues = Fusion.ForValues
local cleanup = Fusion.cleanup
local peek = Fusion.peek

local BUTTON_COMPONENT_ONLY_PROPS = {
	"Activated",
	"IsSelected",
	"IsDimmed",
	"IsFirst",
	"IsLast",
}

type ButtonProps = {
	Activated: () -> (),
	IsSelected: Fusion.Computed<boolean>,
	IsDimmed: Fusion.Computed<boolean>,
	IsFirst: Fusion.Value<boolean>?,
	IsLast: boolean,
	[any]: any,
}

local function Button(props: ButtonProps): TextButton
	local isHovered = Value(false)
	local isPressed = Value(false)

	local state = getState({
		Hovered = isHovered,
		Pressed = isPressed,
	})
	local lastState = peek(state)

	local color = animate(
		Computed(function(use)
			return use(props.IsSelected) and use(Theme.Colors.Brand) or use(Theme.Colors.Background)
		end),
		state
	)

	-- Hide very minor hover flicker of the first button when the dropdown is expanded
	local disconnect = Observer(state):onChange(function()
		if props.IsFirst and not peek(props.IsFirst) then
			local state = peek(state)

			if state == Enum.GuiState.Idle and lastState == Enum.GuiState.Press then
				task.defer(function()
					if peek(props.IsFirst) then
						color:setPosition(
							peek(props.IsSelected) and peek(Theme.Colors.Brand) or peek(Theme.Colors.Background)
						)
					end
				end)
			end

			lastState = state
		end
	end)

	return Hydrate(New "TextButton" {
		Size = UDim2.new(1, 0, 0, Theme.CompSizeY.Medium),
		FontFace = Theme.Fonts.Regular,
		AutoButtonColor = false,
		TextSize = Theme.TextSize.Large,
		TextXAlignment = Enum.TextXAlignment.Left,
		BackgroundColor3 = color,
		TextColor3 = Computed(function(use)
			return use(props.IsSelected) and use(Theme.Colors.TextBranded)
				or use(props.IsDimmed) and use(Theme.Colors.TextDimmed)
				or use(Theme.Colors.Text)
		end),
		[OnEvent "InputBegan"] = function(input)
			if input.UserInputType == Enum.UserInputType.MouseMovement then
				isHovered:set(true)
			elseif input.UserInputType == Enum.UserInputType.MouseButton1 then
				isPressed:set(true)
			end
		end,
		[OnEvent "InputEnded"] = function(input)
			if input.UserInputType == Enum.UserInputType.MouseMovement then
				isHovered:set(false)
			elseif input.UserInputType == Enum.UserInputType.MouseButton1 then
				isPressed:set(false)
			end
		end,
		[OnEvent "Activated"] = function()
			if props.IsFirst then
				isHovered:set(false)
				isPressed:set(false)
			end

			props.Activated()
		end,
		[Cleanup] = disconnect,
		[Children] = GlobalUtil.join(
			{
				Padding {
					Padding = 6,
					Vertical = false,
				},
			},
			if props.IsFirst
				then {
					Corner {},
					New "Frame" {
						AnchorPoint = Vector2.new(0.5, 1),
						Position = UDim2.fromScale(0.5, 1),
						Size = UDim2.fromScale(1.2, 0.2),
						BackgroundColor3 = color,
						BorderSizePixel = 0,
						Visible = Computed(function(use)
							return use(props.IsFirst)
						end),
					},
				}
				elseif props.IsLast then {
					Corner {},
					New "Frame" {
						AnchorPoint = Vector2.new(0.5, 0),
						Position = UDim2.fromScale(0.5, 0),
						Size = UDim2.fromScale(1.2, 0.2),
						BackgroundColor3 = color,
						BorderSizePixel = 0,
					},
				}
				else {}
		),
	})(stripProps(props, BUTTON_COMPONENT_ONLY_PROPS))
end

local COMPONENT_ONLY_PROPS = {
	"Selected",
	"Options",
	"Value",
	"Placeholder",
}

type Props = {
	Selected: ((option: string) -> ())?,
	Options: { string },
	Value: Fusion.CanBeState<string>?,
	Placeholder: Fusion.CanBeState<string>?,
	[any]: any,
}

return function(props: Props): TextButton
	local value = isState(props.Value) and props.Value or Value(props.Value or props.Options[1])
	local placeholder = isState(props.Placeholder) and props.Placeholder or Value(props.Placeholder or props.Options[1])

	local isOpen = Value(false)
	local anchor = Value(0)
	local ref = Value(nil)

	local size
	do
		local maxSize = Vector2.zero

		for _, option in props.Options do
			local textSize = getTextSize(option)

			if textSize.X > maxSize.X then
				maxSize = textSize
			end
		end

		size = UDim2.fromOffset(maxSize.X + 40, Theme.CompSizeY.Medium)
	end

	return Hydrate(Container {
		AutomaticSize = Enum.AutomaticSize.None,
		Size = size,
		[Children] = {
			Padding {
				Padding = 6,
				Vertical = false,
			},
			Image {
				AnchorPoint = Vector2.new(1, 0.5),
				Position = UDim2.fromScale(1, 0.5),
				Size = UDim2.fromOffset(16, 16),
				Image = Assets.Icons.Dropdown,
				ImageColor3 = Theme.Colors.Text,
				ZIndex = 2,
				ImageTransparency = Computed(function(use)
					return use(isOpen) and 1 or 0
				end),
			},
			Input {
				Finished = function()
					task.delay(0.1, function()
						isOpen:set(false)
					end)
				end,
				[Ref] = ref,
			},
			Box {
				ClipsDescendants = true,
				AutomaticSize = Enum.AutomaticSize.None,
				Size = Spring(
					Computed(function(use)
						return UDim2.new(1, 12, use(isOpen) and #props.Options or 1, 0)
					end),
					40
				),
				AnchorPoint = Computed(function(use)
					if use(isOpen) then
						local index = table.find(props.Options, peek(value))
						anchor:set((index - 1) / #props.Options)
					end

					return Vector2.new(0.5, peek(anchor))
				end),
				Position = Computed(function(use)
					return UDim2.fromScale(0.5, use(isOpen) and 0 or use(anchor))
				end),
				[Children] = {
					Container {
						Size = UDim2.fromScale(1, #props.Options),
						[Children] = {
							List {
								Spacing = 0,
							},
							ForValues(props.Options, function(_, option)
								local isFirst = option == props.Options[1]

								return Button {
									Text = Computed(function(use)
										if isFirst then
											return use(isOpen) and option or use(value)
										else
											return option
										end
									end),
									IsSelected = Computed(function(use)
										return use(isOpen) and use(value) == option
									end),
									IsDimmed = Computed(function(use)
										return not use(isOpen) and use(value) ~= use(placeholder)
									end),
									Activated = function()
										local open = peek(isOpen)
										isOpen:set(not open)

										if not open then
											peek(ref):CaptureFocus()
										end

										if not open or option == peek(value) then
											return
										end

										value:set(option)

										if props.Selected then
											props.Selected(option)
										end
									end,
									IsFirst = isFirst and isOpen or nil,
									IsLast = option == props.Options[#props.Options],
								}
							end, cleanup),
						},
					},
				},
			},
		},
	})(stripProps(props, COMPONENT_ONLY_PROPS))
end


local Argon = script:FindFirstAncestor("Argon")
local App = Argon.App
local Components = App.Components
local Util = Components.Util

local Fusion = require(Argon.Packages.Fusion)

local Theme = require(App.Theme)
local animate = require(Util.animate)
local stripProps = require(Util.stripProps)
local getState = require(Util.getState)
local default = require(Util.default)

local Border = require(Components.Border)
local Image = require(Components.Image)

local New = Fusion.New
local Value = Fusion.Value
local Hydrate = Fusion.Hydrate
local OnEvent = Fusion.OnEvent
local Computed = Fusion.Computed
local Children = Fusion.Children

local COMPONENT_ONLY_PROPS = {
	"Activated",
	"Solid",
	"Icon",
	"BackgroundActive",
	"Blending",
}

type Props = {
	Activated: (() -> ())?,
	Solid: Fusion.CanBeState<boolean>?,
	Icon: Fusion.CanBeState<string>?,
	BackgroundActive: Fusion.Computed<boolean>?,
	Blending: boolean?,
	[any]: any,
}

return function(props: Props): TextButton
	local isHovered = Value(false)
	local isPressed = Value(false)

	local state = getState({
		Hovered = isHovered,
		Pressed = isPressed,
	})

	local color = animate(
		Computed(function(use)
			return if use(props.Solid)
				then use(Theme.Colors.Brand)
				else use(props.BackgroundActive) and use(Theme.Colors.ActiveBackground) or use(
					Theme.Colors.Background
				)
		end),
		state
	)

	return Hydrate(New "TextButton" {
		Size = UDim2.fromOffset(Theme.CompSizeY.Large, Theme.CompSizeY.Large),
		Text = "",
		AutoButtonColor = false,
		BackgroundColor3 = color,
		[OnEvent "InputBegan"] = function(inputObject)
			if inputObject.UserInputType == Enum.UserInputType.MouseMovement then
				isHovered:set(true)
			elseif inputObject.UserInputType == Enum.UserInputType.MouseButton1 then
				isPressed:set(true)
			end
		end,
		[OnEvent "InputEnded"] = function(inputObject)
			if inputObject.UserInputType == Enum.UserInputType.MouseMovement then
				isHovered:set(false)
			elseif inputObject.UserInputType == Enum.UserInputType.MouseButton1 then
				isPressed:set(false)
			end
		end,
		[OnEvent "Activated"] = function()
			if props.Activated then
				props.Activated()
			end
		end,
		[Children] = {
			Border {},
			Image {
				AnchorPoint = Vector2.new(0.5, 0.5),
				Position = UDim2.fromScale(0.5, 0.5),
				Size = UDim2.fromScale(0.55, 0.55),
				SizeConstraint = Enum.SizeConstraint.RelativeYY,
				ImageTransparency = default(props.Blending, true) and Computed(function(use)
					return use(Theme.IsDark) and 0.1 or 0.2
				end) or 0,
				ImageColor3 = Computed(function(use)
					return use(props.Solid) and use(Theme.Colors.TextBranded) or use(Theme.Colors.Text)
				end),
				Image = props.Icon,
			},
		},
	})(stripProps(props, COMPONENT_ONLY_PROPS))
end


local Argon = script:FindFirstAncestor("Argon")

local Fusion = require(Argon.Packages.Fusion)

local New = Fusion.New
local Hydrate = Fusion.Hydrate

type Props = {
	[any]: any,
}

return function(props: Props): ImageLabel
	return Hydrate(New("ImageLabel") {
		BorderSizePixel = 0,
		BackgroundTransparency = 1,
	})(props)
end


local Argon = script:FindFirstAncestor("Argon")
local App = Argon.App
local Components = App.Components
local Util = Components.Util

local Fusion = require(Argon.Packages.Fusion)

local Theme = require(App.Theme)
local default = require(Util.default)
local stripProps = require(Util.stripProps)

local New = Fusion.New
local Out = Fusion.Out
local Value = Fusion.Value
local Hydrate = Fusion.Hydrate
local OnEvent = Fusion.OnEvent
local OnChange = Fusion.OnChange
local Children = Fusion.Children
local peek = Fusion.peek

local COMPONENT_ONLY_PROPS = {
	"Started",
	"Changed",
	"Finished",
	"Font",
	"Color",
	"PlaceholderColor",
	"Scaled",
}

type Props = {
	Started: (() -> ())?,
	Changed: ((text: string) -> ())?,
	Finished: ((text: string) -> ())?,
	AutomaticSize: Enum.AutomaticSize?,
	Font: Fusion.CanBeState<Font>?,
	Color: Fusion.CanBeState<Color3>?,
	PlaceholderColor: Fusion.CanBeState<Color3>?,
	Scaled: boolean?,
	[any]: any,
}

return function(props: Props): TextBox
	local text = Value("")

	return Hydrate(New("TextBox") {
		AutomaticSize = default(props.AutomaticSize, Enum.AutomaticSize.None),
		FontFace = props.Font or Theme.Fonts.Regular,
		TextColor3 = props.Color or Theme.Colors.Text,
		PlaceholderColor3 = props.PlaceholderColor or Theme.Colors.TextDimmed,
		TextXAlignment = Enum.TextXAlignment.Left,
		TextSize = Theme.TextSize.Large,
		BorderSizePixel = 0,
		BackgroundTransparency = 1,
		PlaceholderText = "...",
		TextScaled = props.Scaled,
		-- If the text is not scaled, enable ClipsDescendants so text scrolling works correctly.
		ClipsDescendants = not props.Scaled,
		[OnChange "Text"] = function(text)
			if props.Changed then
				props.Changed(text)
			end
		end,
		[OnEvent "Focused"] = function()
			if props.Started then
				props.Started()
			end
		end,
		[OnEvent "FocusLost"] = function()
			if props.Finished then
				props.Finished(peek(text))
			end
		end,
		[Out "Text"] = text,
		[Children] = props.Scaled and New("UITextSizeConstraint") {
			MaxTextSize = props.TextSize or Theme.TextSize.Large,
		} or nil,
	})(stripProps(props, COMPONENT_ONLY_PROPS))
end


local Argon = script:FindFirstAncestor("Argon")
local App = Argon.App
local Components = App.Components
local Util = Components.Util

local Fusion = require(Argon.Packages.Fusion)

local Theme = require(App.Theme)
local stripProps = require(Util.stripProps)

local New = Fusion.New
local Hydrate = Fusion.Hydrate

local COMPONENT_ONLY_PROPS = {
	"Spacing",
}

type Props = {
	Spacing: number?,
	[any]: any,
}

return function(props: Props): UIListLayout
	return Hydrate(New "UIListLayout" {
		SortOrder = Enum.SortOrder.LayoutOrder,
		Padding = UDim.new(0, props.Spacing or Theme.ListSpacing),
	})(stripProps(props, COMPONENT_ONLY_PROPS))
end


local Argon = script:FindFirstAncestor("Argon")
local App = Argon.App
local Components = App.Components
local Util = Components.Util

local Fusion = require(Argon.Packages.Fusion)

local Theme = require(App.Theme)
local animate = require(Util.animate)
local stripProps = require(Util.stripProps)
local getState = require(Util.getState)
local isState = require(Util.isState)

local Corner = require(Components.Corner)
local List = require(Components.List)
local Box = require(Components.Box)

local New = Fusion.New
local Value = Fusion.Value
local Hydrate = Fusion.Hydrate
local OnEvent = Fusion.OnEvent
local Computed = Fusion.Computed
local Children = Fusion.Children
local ForValues = Fusion.ForValues
local cleanup = Fusion.cleanup
local peek = Fusion.peek

local BUTTON_COMPONENT_ONLY_PROPS = {
	"Activated",
	"IsSelected",
	"IsFirst",
	"IsLast",
}

type ButtonProps = {
	Activated: () -> (),
	IsSelected: Fusion.CanBeState<boolean>,
	IsFirst: boolean,
	IsLast: boolean,
	[any]: any,
}

local function Button(props: ButtonProps): TextButton
	local isHovered = Value(false)
	local isPressed = Value(false)

	local state = getState({
		Hovered = isHovered,
		Pressed = isPressed,
	})

	local color = animate(
		Computed(function(use)
			return use(props.IsSelected) and use(Theme.Colors.Brand) or use(Theme.Colors.Background)
		end),
		state
	)

	local children = if props.IsFirst
		then {
			Corner {},
			New "Frame" {
				AnchorPoint = Vector2.new(1, 0),
				Position = UDim2.fromScale(1, 0),
				Size = UDim2.fromScale(0.1, 1),
				BackgroundColor3 = color,
				BorderSizePixel = 0,
			},
		}
		elseif props.IsLast then {
			Corner {},
			New "Frame" {
				Size = UDim2.fromScale(0.1, 1),
				BackgroundColor3 = color,
				BorderSizePixel = 0,
			},
		}
		else {}

	if not props.IsFirst then
		table.insert(
			children,
			New "Frame" {
				Size = UDim2.new(0, Theme.BorderThickness, 1, 0),
				BackgroundColor3 = Theme.Colors.Border,
				BorderSizePixel = 0,
			}
		)
	end

	return Hydrate(New "TextButton" {
		Size = UDim2.fromScale(1, 1),
		FontFace = Theme.Fonts.Regular,
		AutoButtonColor = false,
		TextSize = Theme.TextSize.Large,
		BackgroundColor3 = color,
		TextColor3 = Computed(function(use)
			return use(props.IsSelected) and use(Theme.Colors.TextBranded) or use(Theme.Colors.Text)
		end),
		[OnEvent "InputBegan"] = function(input)
			if input.UserInputType == Enum.UserInputType.MouseMovement then
				isHovered:set(true)
			elseif input.UserInputType == Enum.UserInputType.MouseButton1 then
				isPressed:set(true)
			end
		end,
		[OnEvent "InputEnded"] = function(input)
			if input.UserInputType == Enum.UserInputType.MouseMovement then
				isHovered:set(false)
			elseif input.UserInputType == Enum.UserInputType.MouseButton1 then
				isPressed:set(false)
			end
		end,
		[OnEvent "Activated"] = function()
			if props.Activated then
				props.Activated()
			end
		end,
		[Children] = children,
	})(stripProps(props, BUTTON_COMPONENT_ONLY_PROPS))
end

local COMPONENT_ONLY_PROPS = {
	"Selected",
	"Options",
	"Value",
}

type Props = {
	Selected: ((option: string) -> ())?,
	Options: { string },
	Value: Fusion.CanBeState<boolean>?,
	[any]: any,
}

return function(props: Props): Frame
	local value = isState(props.Value) and props.Value or Value(props.Value or props.Options[1])

	return Hydrate(Box {
		Size = UDim2.new(1, 0, 0, Theme.CompSizeY.Small),
		AutomaticSize = Enum.AutomaticSize.None,
		[Children] = {
			List {
				FillDirection = Enum.FillDirection.Horizontal,
				HorizontalFlex = Enum.UIFlexAlignment.Fill,
				Spacing = 0,
			},
			ForValues(props.Options, function(_, option)
				return Button {
					Text = option,
					IsSelected = Computed(function(use)
						return use(value) == option
					end),
					Activated = function()
						if option == peek(value) then
							return
						end

						value:set(option)

						if props.Selected then
							props.Selected(option)
						end
					end,
					IsFirst = option == props.Options[1],
					IsLast = option == props.Options[#props.Options],
				}
			end, cleanup),
		},
	})(stripProps(props, COMPONENT_ONLY_PROPS))
end


local Argon = script:FindFirstAncestor("Argon")
local App = Argon.App
local Components = App.Components
local Util = Components.Util

local Fusion = require(Argon.Packages.Fusion)

local Theme = require(App.Theme)
local default = require(Util.default)
local stripProps = require(Util.stripProps)

local New = Fusion.New
local Hydrate = Fusion.Hydrate

local COMPONENT_ONLY_PROPS = {
	"Padding",
	"Horizontal",
	"Vertical",
	"Left",
	"Right",
	"Top",
	"Bottom",
}

type Props = {
	-- Mode 1
	Padding: number?,
	Horizontal: boolean?,
	Vertical: boolean?,
	-- Mode 2
	Left: number?,
	Right: number?,
	Top: number?,
	Bottom: number?,
	[any]: any,
}

return function(props: Props): UIPadding
	local left, right
	local top, bottom

	if props.Left or props.Right or props.Top or props.Bottom then
		local padding = props.Padding or 0

		left = UDim.new(0, props.Left or padding)
		right = UDim.new(0, props.Right or padding)
		top = UDim.new(0, props.Top or padding)
		bottom = UDim.new(0, props.Bottom or padding)
	else
		local padding = UDim.new(0, props.Padding or Theme.Padding)
		local horizontal = default(props.Horizontal, true)
		local vertical = default(props.Vertical, true)

		left = horizontal and padding or nil
		right = horizontal and padding or nil
		top = vertical and padding or nil
		bottom = vertical and padding or nil
	end

	return Hydrate(New "UIPadding" {
		PaddingLeft = left,
		PaddingRight = right,
		PaddingTop = top,
		PaddingBottom = bottom,
	})(stripProps(props, COMPONENT_ONLY_PROPS))
end


local plugin = script:FindFirstAncestorWhichIsA("Plugin")

local Argon = script:FindFirstAncestor("Argon")
local App = Argon.App
local Components = App.Components
local Util = Components.Util

local Fusion = require(Argon.Packages.Fusion)

local stripProps = require(Util.stripProps)

local Hydrate = Fusion.Hydrate

local COMPONENT_ONLY_PROPS = {
	"Name",
}

type Props = {
	Name: string,
	[any]: any,
}

return function(props: Props): PluginToolbar
	return Hydrate(plugin:CreateToolbar(props.Name))(stripProps(props, COMPONENT_ONLY_PROPS))
end


local plugin = script:FindFirstAncestorWhichIsA("Plugin")

local Argon = script:FindFirstAncestor("Argon")
local App = Argon.App
local Components = App.Components
local Util = Components.Util

local Fusion = require(Argon.Packages.Fusion)

local stripProps = require(Util.stripProps)
local isState = require(Util.isState)

local Hydrate = Fusion.Hydrate
local Observer = Fusion.Observer
local peek = Fusion.peek

local COMPONENT_ONLY_PROPS = {
	"Toolbar",
	"Name",
	"ToolTip",
	"Image",
}

type ToolbarButtonProps = {
	Toolbar: PluginToolbar,
	Name: string,
	ToolTip: string,
	Image: Fusion.CanBeState<string>,
	[any]: any,
}

return function(props: ToolbarButtonProps): PluginToolbarButton
	local button = props.Toolbar:CreateButton(props.Name, props.ToolTip, peek(props.Image))
	button.ClickableWhenViewportHidden = true

	if isState(props.Image) then
		plugin.Unloading:Once(Observer(props.Image):onChange(function()
			button.Icon = peek(props.Image)
		end))
	end

	return Hydrate(button)(stripProps(props, COMPONENT_ONLY_PROPS))
end


local plugin = script:FindFirstAncestorWhichIsA("Plugin")

local Argon = script:FindFirstAncestor("Argon")
local App = Argon.App
local Components = App.Components
local Util = Components.Util

local Fusion = require(Argon.Packages.Fusion)

local stripProps = require(Util.stripProps)
local isState = require(Util.isState)
local default = require(Util.default)

local Hydrate = Fusion.Hydrate
local peek = Fusion.peek

local COMPONENT_ONLY_PROPS = {
	"Id",
	"InitialDockTo",
	"InitialEnabled",
	"OverrideEnabled",
	"FloatingSize",
	"MinimumSize",
	"Closed",
}

type WidgetProps = {
	Id: string?,
	InitialDockTo: Enum.InitialDockState?,
	InitialEnabled: boolean?,
	OverrideEnabled: boolean?,
	FloatingSize: Vector2?,
	MinimumSize: Vector2,
	Closed: (() -> ())?,
	[any]: any,
}

return function(props: WidgetProps): DockWidgetPluginGui
	local floatingSize = props.FloatingSize or props.MinimumSize

	local widget = plugin:CreateDockWidgetPluginGui(
		props.Id or peek(props.Name),
		DockWidgetPluginGuiInfo.new(
			props.InitialDockTo or Enum.InitialDockState.Float,
			default(props.InitialEnabled, true),
			default(props.OverrideEnabled, false),
			floatingSize.X,
			floatingSize.Y,
			props.MinimumSize.X,
			props.MinimumSize.Y
		)
	)

	if isState(props.Enabled) then
		props.Enabled:set(widget.Enabled)
	end

	if props.Closed then
		widget:BindToClose(props.Closed)
	end

	widget.ZIndexBehavior = Enum.ZIndexBehavior.Sibling
	props.Title = props.Name

	return Hydrate(widget)(stripProps(props, COMPONENT_ONLY_PROPS))
end


local Argon = script:FindFirstAncestor("Argon")
local App = Argon.App
local Components = App.Components
local Util = Components.Util

local Fusion = require(Argon.Packages.Fusion)

local Theme = require(App.Theme)
local stripProps = require(Util.stripProps)

local New = Fusion.New
local Hydrate = Fusion.Hydrate

local COMPONENT_ONLY_PROPS = {
	"ScrollBar",
}

type Props = {
	ScrollBar: boolean?,
	[any]: any,
}

return function(props: Props): ScrollingFrame
	return Hydrate(New "ScrollingFrame" {
		Size = UDim2.fromScale(1, 1),
		BackgroundTransparency = 1,
		ScrollBarThickness = props.ScrollBar and 5 or 0,
		ScrollBarImageColor3 = Theme.Colors.TextDimmed,
		AutomaticCanvasSize = Enum.AutomaticSize.Y,
		CanvasSize = UDim2.fromScale(0, 0),
		BorderSizePixel = 0,
	})(stripProps(props, COMPONENT_ONLY_PROPS))
end


local Argon = script:FindFirstAncestor("Argon")
local App = Argon.App
local Components = App.Components
local Util = Components.Util

local Fusion = require(Argon.Packages.Fusion)

local Assets = require(App.Assets)
local Theme = require(App.Theme)
local stripProps = require(Util.stripProps)

local Image = require(Components.Image)

local Tween = Fusion.Tween
local Value = Fusion.Value
local Hydrate = Fusion.Hydrate

local COMPONENT_ONLY_PROPS = {
	"Duration",
}

type Props = {
	Duration: number?,
	[any]: any,
}

return function(props: Props): ImageLabel
	local rotation = Value(0)

	task.defer(function()
		rotation:set(360)
	end)

	return Hydrate(Image {
		Size = UDim2.fromOffset(Theme.CompSizeY.Medium, Theme.CompSizeY.Medium),
		Image = Assets.Icons.Spinner,
		ImageColor3 = Theme.Colors.Text,
		Rotation = Tween(
			rotation,
			TweenInfo.new(props.Duration or 1, Enum.EasingStyle.Linear, Enum.EasingDirection.InOut, -1)
		),
	})(stripProps(props, COMPONENT_ONLY_PROPS))
end


local Argon = script:FindFirstAncestor("Argon")
local App = Argon.App
local Components = App.Components
local Util = Components.Util

local Fusion = require(Argon.Packages.Fusion)

local Theme = require(App.Theme)
local stripProps = require(Util.stripProps)

local New = Fusion.New
local Hydrate = Fusion.Hydrate
local Children = Fusion.Children

local COMPONENT_ONLY_PROPS = {
	"Font",
	"Color",
	"Scaled",
}

type Props = {
	Font: Fusion.CanBeState<Font>?,
	Color: Fusion.CanBeState<Color3>?,
	Scaled: boolean?,
	[any]: any,
}

return function(props: Props): TextLabel
	return Hydrate(New("TextLabel") {
		FontFace = props.Font or Theme.Fonts.Regular,
		TextColor3 = props.Color or Theme.Colors.Text,
		TextSize = Theme.TextSize.Large,
		AutomaticSize = props.Scaled and Enum.AutomaticSize.None or Enum.AutomaticSize.XY,
		TextXAlignment = Enum.TextXAlignment.Left,
		BorderSizePixel = 0,
		BackgroundTransparency = 1,
		TextScaled = props.Scaled,
		[Children] = props.Scaled and New("UITextSizeConstraint") {
			MaxTextSize = props.TextSize or Theme.TextSize.Large,
		} or nil,
	})(stripProps(props, COMPONENT_ONLY_PROPS))
end


local Argon = script:FindFirstAncestor("Argon")
local App = Argon.App
local Components = App.Components
local Util = Components.Util

local Fusion = require(Argon.Packages.Fusion)

local Theme = require(App.Theme)
local animate = require(Util.animate)
local stripProps = require(Util.stripProps)
local getTextSize = require(Util.getTextSize)
local getState = require(Util.getState)

local Border = require(Components.Border)

local New = Fusion.New
local Value = Fusion.Value
local Hydrate = Fusion.Hydrate
local OnEvent = Fusion.OnEvent
local Computed = Fusion.Computed
local Children = Fusion.Children

local COMPONENT_ONLY_PROPS = {
	"Activated",
	"Solid",
}

type Props = {
	Activated: (() -> ())?,
	Solid: Fusion.CanBeState<boolean>?,
	[any]: any,
}

return function(props: Props): TextButton
	local isHovered = Value(false)
	local isPressed = Value(false)

	local state = getState({
		Hovered = isHovered,
		Pressed = isPressed,
	})

	local color = animate(
		Computed(function(use)
			return use(props.Solid) and use(Theme.Colors.Brand) or use(Theme.Colors.Background)
		end),
		state
	)

	local size = props.Size
		or Computed(function(use)
			local text = use(props.Text)
			local size = getTextSize(text)

			return UDim2.fromOffset(size.X + 22, Theme.CompSizeY.Large)
		end)

	return Hydrate(New "TextButton" {
		Text = "Button",
		FontFace = Theme.Fonts.Regular,
		AutoButtonColor = false,
		TextSize = Theme.TextSize.Large,
		Size = size,
		BackgroundColor3 = color,
		TextColor3 = Computed(function(use)
			return use(props.Solid) and use(Theme.Colors.TextBranded) or use(Theme.Colors.Text)
		end),
		[OnEvent "InputBegan"] = function(input)
			if input.UserInputType == Enum.UserInputType.MouseMovement then
				isHovered:set(true)
			elseif input.UserInputType == Enum.UserInputType.MouseButton1 then
				isPressed:set(true)
			end
		end,
		[OnEvent "InputEnded"] = function(input)
			if input.UserInputType == Enum.UserInputType.MouseMovement then
				isHovered:set(false)
			elseif input.UserInputType == Enum.UserInputType.MouseButton1 then
				isPressed:set(false)
			end
		end,
		[OnEvent "Activated"] = function()
			if props.Activated then
				props.Activated()
			end
		end,
		[Children] = {
			Border {},
		},
	})(stripProps(props, COMPONENT_ONLY_PROPS))
end


local Argon = script:FindFirstAncestor("Argon")
local App = Argon.App

local Fusion = require(Argon.Packages.Fusion)

local Theme = require(App.Theme)

local Spring = Fusion.Spring
local Computed = Fusion.Computed

return function(
	goal: Fusion.StateObject<Color3>,
	state: Fusion.Computed<Enum.GuiState>,
	speed: Fusion.CanBeState<number>?,
	damping: Fusion.CanBeState<number>?
): Fusion.Spring<Color3>
	return Spring(
		Computed(function(use)
			local goal = use(goal)
			local state = use(state)

			local isDark = use(Theme.IsDark)
			local h, s, v = goal:ToHSV()

			if state == Enum.GuiState.Hover then
				return Color3.fromHSV(h, s, v * (isDark and 1.3 or 0.9))
			elseif state == Enum.GuiState.Press then
				return Color3.fromHSV(h, s, v * (isDark and 1.5 or 0.8))
			else
				return goal
			end
		end),
		speed or Theme.SpringSpeed,
		damping or Theme.SpringDamping
	)
end


return function<T>(value: T?, default: T): T
	if value == nil then
		return default
	else
		return value
	end
end


return function(host: string): string
	return host:gsub("[^%w%.%-]", "")
end


return function(number: string): string
	return number:gsub("[^%d]", "")
end


return function(port: string): string
	return port:sub(1, 5):gsub("[^%d]", "")
end


local Argon = script:FindFirstAncestor("Argon")

local Fusion = require(Argon.Packages.Fusion)

local Computed = Fusion.Computed

type Input = {
	Pressed: Fusion.StateObject<boolean>?,
	Hovered: Fusion.StateObject<boolean>?,
}

return function(input: Input): Fusion.Computed<Enum.GuiState>
	local isPressed = input.Pressed
	local isHovered = input.Hovered

	return Computed(function(use)
		if use(isPressed) then
			return Enum.GuiState.Press
		elseif use(isHovered) then
			return Enum.GuiState.Hover
		else
			return Enum.GuiState.Idle
		end
	end)
end


local TextService = game:GetService("TextService")

local Argon = script:FindFirstAncestor("Argon")
local App = Argon.App

local Theme = require(App.Theme)

return function(text: string, fontSize: number?, font: Enum.Font?, frameSize: Vector2?): Vector2
	if text:find("<font") then
		text = text:gsub("<[^>]+>", "")
	end

	return TextService:GetTextSize(
		text,
		fontSize or Theme.TextSize.Large,
		font or Theme.Fonts.Enums.Regular,
		frameSize or Vector2.new(math.huge, math.huge)
	)
end


return function(target: any): boolean
	return type(target) == "table" and target.type == "State"
end


return function(props: { [any]: any }, strip: { string }): { [any]: any }
	props = table.clone(props)

	for _, prop in strip do
		props[prop] = nil
	end

	return props
end


local ContentProvider = game:GetService("ContentProvider")

local Argon = script:FindFirstAncestor("Argon")

local Util = require(Argon.Util)
local Log = require(Argon.Log)

local Assets = require(script.Parent.Assets)

local Loader = {}

function Loader.assets(): { string }
	return Util.dictionaryToArray(Assets)
end

function Loader.load()
	local assets = Loader.assets()

	Log.trace(`Loading {#assets} assets..`)

	task.spawn(function()
		ContentProvider:PreloadAsync(assets)
	end)
end

return Loader


local Argon = script:FindFirstAncestor("Argon")
local App = Argon.App
local Components = App.Components
local Widgets = App.Widgets

local Fusion = require(Argon.Packages.Fusion)

local Types = require(Argon.Types)

local Assets = require(App.Assets)
local Theme = require(App.Theme)

local Project = require(Widgets.Project)
local TextButton = require(Components.TextButton)
local IconButton = require(Components.IconButton)
local Container = require(Components.Container)
local Padding = require(Components.Padding)
local Text = require(Components.Text)
local List = require(Components.List)
local Box = require(Components.Box)

local Value = Fusion.Value
local Observer = Fusion.Observer
local Computed = Fusion.Computed
local Children = Fusion.Children
local Cleanup = Fusion.Cleanup
local peek = Fusion.peek

type Props = {
	App: { [string]: any },
	Project: Fusion.Value<Types.Project>,
}

return function(props: Props): { Instance }
	local syncedText = Value("just now")
	local projectWidget = Value(nil)

	local function updateSyncedText()
		return task.spawn(function()
			while true do
				local elapsed = os.time() - peek(props.App.lastSync)
				local interval = 1
				local text

				if elapsed < 1 then
					text = "just now"
				elseif elapsed < 60 then
					text = math.floor(elapsed) .. "s ago"
				elseif elapsed < 3600 then
					interval = 60
					text = elapsed // interval .. "m ago"
				elseif elapsed < 86400 then
					interval = 3600
					text = elapsed // interval .. "h ago"
				else
					interval = 86400
					text = elapsed // interval .. "d ago"
				end

				syncedText:set(text)
				task.wait(interval)
			end
		end)
	end

	local thread = updateSyncedText()
	local disconnect = Observer(props.App.lastSync):onChange(function()
		task.cancel(thread)
		thread = updateSyncedText()
	end)

	return {
		List {},
		Box {
			Size = UDim2.new(1, 0, 0, Theme.CompSizeY.Large * 1.5),
			[Children] = {
				Padding {},
				Text {
					Size = UDim2.fromScale(0.8, 0.6),
					Text = Computed(function(use)
						return use(props.Project).name
					end),
					Font = Theme.Fonts.Bold,
					Scaled = true,
				},
				Text {
					Position = UDim2.fromScale(0, 0.6),
					Size = UDim2.fromScale(0.8, 0.4),
					Text = `{props.App.host}:{props.App.port}`,
					TextSize = Theme.TextSize.Small,
					Font = Theme.Fonts.Mono,
					Scaled = true,
				},
				Text {
					AnchorPoint = Vector2.new(1, 0.5),
					Position = UDim2.new(1, -2, 0.5, 0),
					Text = Computed(function(use)
						local lastSyncKind = use(props.App.lastSyncKind)
						local action = ""

						if lastSyncKind == "Server" then
							action = "↓"
						elseif lastSyncKind == "Client" then
							action = "↑"
						end

						return action .. "<i> Synced\n" .. use(syncedText) .. "</i>"
					end),
					RichText = true,
					Color = Theme.Colors.TextDimmed,
					TextSize = Theme.TextSize.Small,
					TextXAlignment = Enum.TextXAlignment.Right,
					LineHeight = 1.2,
				},
			},
		},
		Container {
			Size = UDim2.fromScale(1, 0),
			[Children] = {
				List {
					FillDirection = Enum.FillDirection.Horizontal,
					HorizontalAlignment = Enum.HorizontalAlignment.Right,
				},
				TextButton {
					Text = "Disconnect",
					LayoutOrder = 2,
					Solid = true,
					Activated = function()
						props.App:disconnect()
						props.App:home()
					end,
				},
				IconButton {
					Icon = Assets.Icons.Settings,
					BackgroundActive = Computed(function(use)
						return use(props.App.settingsWidget) ~= nil
					end),
					Activated = function()
						props.App:settings()
					end,
					LayoutOrder = 1,
				},
				IconButton {
					Icon = Assets.Icons.Info,
					BackgroundActive = Computed(function(use)
						return use(projectWidget) ~= nil
					end),
					Activated = function()
						local widget = peek(projectWidget)

						if widget then
							widget:Destroy()
							widget = nil
						else
							widget = Project {
								App = props.App,
								Project = props.Project,

								Closed = function()
									peek(projectWidget):Destroy()
									projectWidget:set(nil)
								end,
							}
						end

						projectWidget:set(widget)
					end,
					[Cleanup] = function()
						local widget = peek(projectWidget)

						if widget then
							widget:Destroy()
							widget = nil
						end

						disconnect()
						task.cancel(thread)
					end,
				},
			},
		},
	}
end


local Argon = script:FindFirstAncestor("Argon")
local App = Argon.App
local Components = App.Components

local Fusion = require(Argon.Packages.Fusion)

local Theme = require(App.Theme)

local TextButton = require(Components.TextButton)
local Spinner = require(Components.Spinner)
local List = require(Components.List)
local Box = require(Components.Box)

local Children = Fusion.Children

type Props = {
	App: { [string]: any },
}

return function(props: Props): { Instance }
	return {
		List {
			HorizontalAlignment = Enum.HorizontalAlignment.Right,
		},
		Box {
			Size = UDim2.new(1, 0, 0, Theme.CompSizeY.Large * 1.5),
			[Children] = {
				Spinner {
					AnchorPoint = Vector2.new(0.5, 0.5),
					Position = UDim2.fromScale(0.5, 0.5),
				},
			},
		},
		TextButton {
			Text = "Cancel",
			Activated = function()
				props.App:disconnect(true)
				props.App:home()
			end,
		},
	}
end


local Argon = script:FindFirstAncestor("Argon")
local App = Argon.App
local Components = App.Components
local Util = Components.Util

local Fusion = require(Argon.Packages.Fusion)

local Theme = require(App.Theme)
local getTextSize = require(Util.getTextSize)

local TextButton = require(Components.TextButton)
local ScrollingContainer = require(Components.ScrollingContainer)
local Padding = require(Components.Padding)
local Text = require(Components.Text)
local List = require(Components.List)
local Box = require(Components.Box)

local Value = Fusion.Value
local Computed = Fusion.Computed
local OnChange = Fusion.OnChange
local Children = Fusion.Children

type Props = {
	App: { [string]: any },
	Message: string,
}

return function(props: Props): { Instance }
	local absoluteSize = Value(Vector2.zero)

	return {
		List {
			HorizontalAlignment = Enum.HorizontalAlignment.Right,
		},
		Box {
			AutomaticSize = Enum.AutomaticSize.None,
			Size = Computed(function(use)
				local absoluteSize = use(absoluteSize)
				local rootSize = use(props.App.rootSize)

				local size = getTextSize(
					props.Message,
					Theme.TextSize.Medium,
					Theme.Fonts.Enums.Mono,
					Vector2.new(absoluteSize.X - Theme.Padding * 2 - 2, math.huge)
				)
				local height = math.min(size.Y + Theme.Padding * 2, rootSize.Y - 120)

				return UDim2.new(1, 0, 0, height)
			end),
			[OnChange "AbsoluteSize"] = function(size)
				absoluteSize:set(size)
			end,
			[Children] = {
				ScrollingContainer {
					Size = UDim2.new(1, -2, 1, 0),
					ScrollBar = true,
					[Children] = {
						Padding {},
						Text {
							Text = props.Message,
							RichText = true,
							TextWrapped = true,
							Font = Theme.Fonts.Mono,
							TextSize = Theme.TextSize.Medium,
						},
					},
				},
			},
		},
		TextButton {
			Text = "Proceed",
			Activated = function()
				props.App:home()
			end,
		},
	}
end


local Argon = script:FindFirstAncestor("Argon")
local App = Argon.App
local Components = App.Components
local Util = Components.Util

local Fusion = require(Argon.Packages.Fusion)
local Config = require(Argon.Config)

local Assets = require(App.Assets)
local Theme = require(App.Theme)
local filterHost = require(Util.filterHost)
local filterPort = require(Util.filterPort)

local TextButton = require(Components.TextButton)
local IconButton = require(Components.IconButton)
local Container = require(Components.Container)
local Padding = require(Components.Padding)
local Input = require(Components.Input)
local List = require(Components.List)
local Text = require(Components.Text)
local Box = require(Components.Box)

local New = Fusion.New
local Value = Fusion.Value
local Cleanup = Fusion.Cleanup
local Computed = Fusion.Computed
local Children = Fusion.Children
local Spring = Fusion.Spring

type Props = {
	App: { [string]: any },
	Host: string,
	Port: number,
}

local function getValue(setting: Config.Setting, value: any, input: boolean?): string
	if input then
		return value ~= Config:getDefault(setting) and value or ""
	else
		return value ~= "" and value or Config:getDefault(setting)
	end
end

return function(props: Props): { Instance }
	local hostInput = Value(getValue("Host", props.Host, true))
	local portInput = Value(getValue("Port", props.Port, true))

	local hostFocused = Value(false)
	local portFocused = Value(false)

	local connections = {
		Config:onChanged("Host", function(value)
			hostInput:set(getValue("Host", value, true))
			props.App:setHost(getValue("Host", value))
		end),
		Config:onChanged("Port", function(value)
			portInput:set(getValue("Port", value, true))
			props.App:setPort(getValue("Port", value))
		end),
	}

	return {
		List {},
		Box {
			Size = UDim2.new(1, 0, 0, Theme.CompSizeY.Large),
			AutomaticSize = Enum.AutomaticSize.None,
			BorderColor = Spring(
				Computed(function(use)
					if use(hostFocused) or use(portFocused) then
						return use(Theme.Colors.Brand)
					else
						return use(Theme.Colors.Border)
					end
				end),
				Theme.SpringFastSpeed,
				Theme.SpringDamping
			),
			[Cleanup] = connections,
			[Children] = {
				List {
					Spacing = 4,
					FillDirection = Enum.FillDirection.Horizontal,
					VerticalAlignment = Enum.VerticalAlignment.Center,
				},
				Input {
					Size = UDim2.fromScale(0.75, 1),
					Font = Theme.Fonts.Mono,
					PlaceholderText = Config:getDefault("Host"),
					Text = hostInput,
					Started = function()
						hostFocused:set(true)
					end,
					Changed = function(text)
						hostInput:set(filterHost(text))
					end,
					Finished = function(host)
						hostFocused:set(false)
						props.App:setHost(getValue("Host", host))
					end,
					[Children] = {
						-- The cursor will not be visible when it is all the way at the left because of ClipsDescendants,
						-- so this padding makes sure there's always 1 pixel of space from the left.
						Padding {
							Left = 1,
						},
						New "UIFlexItem" {
							FlexMode = Enum.UIFlexMode.Fill,
						},
					},
				},
				Text {
					AutomaticSize = Enum.AutomaticSize.XY,
					AnchorPoint = Vector2.new(1, 0.5),
					Position = UDim2.fromScale(0, 0.5),
					Text = ":",
					Font = Theme.Fonts.Mono,
					Color = Theme.Colors.TextDimmed,
				},
				Input {
					AutomaticSize = Enum.AutomaticSize.X,
					AnchorPoint = Vector2.new(1, 0),
					Position = UDim2.fromScale(1, 0),
					Size = UDim2.fromScale(0, 1),
					Font = Theme.Fonts.Mono,
					PlaceholderText = Config:getDefault("Port"),
					Text = portInput,
					Started = function()
						portFocused:set(true)
					end,
					Changed = function(text)
						portInput:set(filterPort(text))
					end,
					Finished = function(port)
						portFocused:set(false)
						props.App:setPort(getValue("Port", port))
					end,
				},
				Padding {
					Padding = 10,
					Vertical = false,
				},
			},
		},
		Container {
			Size = UDim2.fromScale(1, 0),
			[Children] = {
				List {
					FillDirection = Enum.FillDirection.Horizontal,
					HorizontalAlignment = Enum.HorizontalAlignment.Right,
				},
				TextButton {
					Solid = true,
					LayoutOrder = 2,
					Text = "Connect",
					Activated = function()
						props.App:connect()
					end,
				},
				IconButton {
					Icon = Assets.Icons.Settings,
					BackgroundActive = Computed(function(use)
						return use(props.App.settingsWidget) ~= nil
					end),
					Activated = function()
						props.App:settings()
					end,
					LayoutOrder = 1,
				},
				IconButton {
					Icon = Assets.Icons.Help,
					BackgroundActive = Computed(function(use)
						return use(props.App.helpWidget) ~= nil
					end),
					Activated = function()
						props.App:help()
					end,
				},
			},
		},
	}
end


local Argon = script:FindFirstAncestor("Argon")
local App = Argon.App
local Components = App.Components
local Util = Components.Util

local Fusion = require(Argon.Packages.Fusion)
local Signal = require(Argon.Packages.Signal)

local Theme = require(App.Theme)
local getTextSize = require(Util.getTextSize)

local ScrollingContainer = require(Components.ScrollingContainer)
local TextButton = require(Components.TextButton)
local Container = require(Components.Container)
local Padding = require(Components.Padding)
local Text = require(Components.Text)
local List = require(Components.List)
local Box = require(Components.Box)

local Value = Fusion.Value
local Computed = Fusion.Computed
local ForPairs = Fusion.ForPairs
local OnChange = Fusion.OnChange
local Children = Fusion.Children
local cleanup = Fusion.cleanup

type Props = {
	App: { [string]: any },
	Message: string,
	Options: { string },
	Signal: Signal.Signal,
}

return function(props: Props): { Instance }
	local absoluteSize = Value(Vector2.zero)

	return {
		List {
			HorizontalAlignment = Enum.HorizontalAlignment.Right,
		},
		Box {
			AutomaticSize = Enum.AutomaticSize.None,
			Size = Computed(function(use)
				local absoluteSize = use(absoluteSize)
				local rootSize = use(props.App.rootSize)

				local size = getTextSize(
					props.Message,
					Theme.TextSize.Medium,
					Theme.Fonts.Enums.Mono,
					Vector2.new(absoluteSize.X - Theme.Padding * 2 - 2, math.huge)
				)
				local height = math.min(size.Y + Theme.Padding * 2, rootSize.Y - 120)

				return UDim2.new(1, 0, 0, height)
			end),
			[OnChange "AbsoluteSize"] = function(size)
				absoluteSize:set(size)
			end,
			[Children] = {
				ScrollingContainer {
					Size = UDim2.new(1, -2, 1, 0),
					ScrollBar = true,
					[Children] = {
						Padding {},
						Text {
							Text = props.Message,
							RichText = true,
							TextWrapped = true,
							Font = Theme.Fonts.Mono,
							TextSize = Theme.TextSize.Medium,
						},
					},
				},
			},
		},
		Container {
			Size = UDim2.fromScale(1, 0),
			[Children] = {
				List {
					FillDirection = Enum.FillDirection.Horizontal,
					HorizontalAlignment = Enum.HorizontalAlignment.Right,
				},
				ForPairs(props.Options, function(_, index, option)
					return index,
						TextButton {
							LayoutOrder = #props.Options - index + 1,
							Solid = index == 1,
							Text = option,
							Activated = function()
								props.Signal:Fire(option)
							end,
						}
				end, cleanup),
			},
		},
	}
end


local Argon = script:FindFirstAncestor("Argon")
local App = Argon.App
local Components = App.Components

local Fusion = require(Argon.Packages.Fusion)

local Theme = require(App.Theme)

local Padding = require(Components.Padding)
local Text = require(Components.Text)
local Box = require(Components.Box)

local Children = Fusion.Children

type Props = {
	App: { [string]: any },
	Message: string,
}

return function(): Frame
	return Box {
		Size = UDim2.new(1, 0, 0, Theme.CompSizeY.Large * 2),
		[Children] = {
			Padding {},
			Text {
				Text = "Unavailable during playtest! Return to the edit mode to continue using Argon",
				TextWrapped = true,
				Font = Theme.Fonts.Mono,
				TextSize = Theme.TextSize.Medium,
				Color = Theme.Colors.TextDimmed,
			},
		},
	}
end


local plugin = script:FindFirstAncestorWhichIsA("Plugin")
local Studio = settings().Studio

local Argon = script:FindFirstAncestor("Argon")

local Fusion = require(Argon.Packages.Fusion)

local Value = Fusion.Value

local function getColors(isDark: boolean): { [string]: { Color3 | { [string]: Color3 } } }
	local background = Studio.Theme:GetColor(Enum.StudioStyleGuideColor.MainBackground)
	local border = Studio.Theme:GetColor(Enum.StudioStyleGuideColor.Shadow)

	if border == background then
		border = Studio.Theme:GetColor(Enum.StudioStyleGuideColor.ScrollBarBackground)
	end

	local h, s, v = border:ToHSV()
	border = Color3.fromHSV(h, s, v * (isDark and 1.2 or 0.85))

	return {
		-- It's actually 130, 120, 230
		Brand = Color3.fromRGB(120, 110, 220),

		Background = background,
		ActiveBackground = Studio.Theme:GetColor(Enum.StudioStyleGuideColor.Titlebar),
		Border = border,

		Text = Studio.Theme:GetColor(Enum.StudioStyleGuideColor.BrightText),
		TextDimmed = Studio.Theme:GetColor(Enum.StudioStyleGuideColor.SubText),
		TextBranded = isDark and Studio.Theme:GetColor(Enum.StudioStyleGuideColor.BrightText) or background,

		Diff = {
			Add = Color3.fromRGB(80, 220, 100),
			Update = Color3.fromRGB(100, 200, 230),
			Remove = Color3.fromRGB(230, 100, 100),
		},
	}
end

local function init(): Fusion.Value<Color3>
	return Value(Color3.new())
end

local Theme = {
	-- For autocomplete
	Colors = {
		Brand = init(),

		Background = init(),
		ActiveBackground = init(),
		Border = init(),

		Text = init(),
		TextDimmed = init(),
		TextBranded = init(),

		Diff = {
			Add = init(),
			Update = init(),
			Remove = init(),
		},
	},

	Fonts = {
		Regular = Font.fromName("Ubuntu"),
		Bold = Font.fromName("Ubuntu", Enum.FontWeight.Bold),
		Italic = Font.fromName("Ubuntu", Enum.FontWeight.Regular, Enum.FontStyle.Italic),
		Mono = Font.fromName("RobotoMono"),

		-- required for TextService:GetTextSize()
		Enums = {
			Regular = Enum.Font.Ubuntu,
			Bold = Enum.Font.Ubuntu,
			Italic = Enum.Font.Ubuntu,
			Mono = Enum.Font.RobotoMono,
		},
	},

	BorderThickness = 1,
	CornerRadius = 6,
	ListSpacing = 12,
	WidgetPadding = 16,
	Padding = 8,

	TextSize = {
		Large = 20,
		Medium = 18,
		Small = 16,
	},

	CompSizeY = {
		Large = 36,
		Medium = 30,
		Small = 28,
	},

	SpringSpeed = 30,
	SpringFastSpeed = 60,
	SpringDamping = 1.5,

	IsDark = Value(true),
}

do
	local function updateTheme()
		local _, _, v = Studio.Theme:GetColor(Enum.StudioStyleGuideColor.MainBackground):ToHSV()
		local isDark = v <= 0.6

		Theme.IsDark:set(isDark)

		for key, color in getColors(isDark) do
			if type(color) ~= "table" then
				Theme.Colors[key]:set(color)
			else
				for subKey, subColor in color do
					Theme.Colors[key][subKey] = subColor
				end
			end
		end
	end

	updateTheme()

	local connection = Studio.ThemeChanged:Connect(updateTheme)

	plugin.Unloading:Once(function()
		connection:Disconnect()
		connection = nil
	end)

	-- :P
	local now = DateTime.now():ToLocalTime()
	if now.Month == 4 and now.Day == 1 then
		Theme.Colors.Brand = Color3.fromHex("e13835")
		Theme.CornerRadius = 0
	end
end

return Theme


local Argon = script:FindFirstAncestor("Argon")
local App = Argon.App
local Components = App.Components

local Fusion = require(Argon.Packages.Fusion)

local Theme = require(App.Theme)
local Dom = require(Argon.Dom)
local Types = require(Argon.Types)

local Widget = require(Components.Plugin.Widget)
local ScrollingContainer = require(Components.ScrollingContainer)
local Collapsible = require(Components.Collapsible)
local Container = require(Components.Container)
local Padding = require(Components.Padding)
local List = require(Components.List)
local Text = require(Components.Text)

local New = Fusion.New
local Children = Fusion.Children
local ForValues = Fusion.ForValues
local cleanup = Fusion.cleanup

local MAX_VALUE_LENGTH = 80

local function countDescendants(snapshot: Types.AddedSnapshot): number
	local descendants = 0

	if snapshot.children then
		descendants = #snapshot.children

		for _, child in snapshot.children do
			descendants += countDescendants(child)
		end
	end

	return descendants
end

local function countLines(source: string): number
	return #source:split("\n")
end

local function countSnapshotLines(snapshot: Types.AddedSnapshot | Types.UpdatedSnapshot): number
	local source = snapshot.properties and snapshot.properties.Source

	if not source then
		return 0
	end

	local ok, decoded = Dom.EncodedValue.decode(source)
	return ok and countLines(decoded) or 0
end

local function countInstanceLines(instance: Instance): number
	if not instance:IsA("LuaSourceContainer") then
		return 0
	end

	local ok, source = Dom.readProperty(instance, "Source")
	return ok and countLines(source) or 0
end

local function formatValue(value: any)
	if type(value) == "string" then
		value = `"{value}"`
	elseif typeof(value) == "EnumItem" then
		value = value.Name
	end

	value = tostring(value)

	if value:find(",") then
		value = `({value})`
	end

	if #value > MAX_VALUE_LENGTH then
		return value:sub(1, MAX_VALUE_LENGTH - 3) .. "..."
	end

	return value
end

local function formatSnapshotValue(encoded: any, decoded: any?): string
	local ok, value = Dom.EncodedValue.decode(encoded)

	if ok and typeof(decoded) == "EnumItem" then
		local enum = decoded.EnumType
		value = enum:FromValue(value)
	end

	return ok and formatValue(value) or "?"
end

local function formatInstanceValue(instance: Instance, property: string): (string, any?)
	local ok, value = Dom.readProperty(instance, property)
	return ok and formatValue(value), value or "?", nil
end

type SectionProps = {
	Title: string,
	Color: Color3,
	Elements: { { any } },
}

local function Section(props: SectionProps)
	return Collapsible {
		Title = props.Title,
		Color = props.Color,
		Expanded = true,
		Elements = ForValues(props.Elements, function(_, element)
			return Container {
				Size = UDim2.fromScale(1, 0),
				[Children] = {
					New "Frame" {
						Size = UDim2.new(1, 0, 0, Theme.BorderThickness),
						BackgroundColor3 = Theme.Colors.Border,
						BorderSizePixel = 0,
					},
					Container {
						Size = UDim2.fromScale(1, 1),
						[Children] = {
							List {
								Spacing = 0,
							},
							Padding {},
							element,
						},
					},
				},
			}
		end, cleanup),
	}
end

type TitleProps = {
	Symbol: string,
	Name: string,
	Class: string,
	Color: Color3,
}

local function Title(props: TitleProps)
	return Text {
		Text = `{props.Symbol} [{props.Class}] {props.Name}`,
		Color = props.Color,
		TextSize = Theme.TextSize.Small,
		Font = Theme.Fonts.Mono,
		TextWrapped = true,
	}
end

local function Property(name: string, old: any, new: any?)
	local text

	if new then
		text = `<b>{old}</b> → <b>{new}</b>`
	else
		text = `<b>{old}</b>`
	end

	return Text {
		Text = `    {name}: {text}`,
		Color = Theme.Colors.TextDimmed,
		TextSize = Theme.TextSize.Small,
		Font = Theme.Fonts.Mono,
		TextWrapped = true,
		RichText = true,
	}
end

local function Additions(snapshots: { Types.AddedSnapshot }, tree: any)
	local elements = {}

	for _, snapshot in snapshots do
		local parent = tree:getInstance(snapshot.parent)
		local name = snapshot.name

		if parent then
			name = parent:GetFullName() .. "." .. name
		end

		local element = {
			Title {
				Symbol = "+",
				Name = name,
				Class = snapshot.class,
				Color = Theme.Colors.Diff.Add,
			},
		}

		local descendants = countDescendants(snapshot)
		if descendants > 0 then
			table.insert(element, Property("Descendants", descendants))
		end

		local lines = countSnapshotLines(snapshot)
		if lines > 0 then
			table.insert(element, Property("Lines", lines))
		end

		table.insert(elements, element)
	end

	return Section {
		Title = `Additions ({#snapshots})`,
		Color = Theme.Colors.Diff.Add,
		Elements = elements,
	}
end

local function Updates(snapshots: { Types.UpdatedSnapshot }, tree: any)
	local elements = {}

	for _, snapshot in snapshots do
		local instance = tree:getInstance(snapshot.id)
		local element = {
			Title {
				Symbol = "~",
				Name = instance and instance:GetFullName() or snapshot.name or "?",
				Class = instance and instance.ClassName or snapshot.class or "?",
				Color = Theme.Colors.Diff.Update,
			},
		}

		for property, value in snapshot.properties or {} do
			if property ~= "Source" then
				local formatted, raw = "?", nil

				if instance then
					formatted, raw = formatInstanceValue(instance, property)
				end

				table.insert(element, Property(property, formatted, formatSnapshotValue(value, raw)))

				continue
			end

			local new = countSnapshotLines(snapshot)
			local old = instance and countInstanceLines(instance) or 0
			local delta = new - old
			local sign = if delta > 0 then "+" elseif delta < 0 then "-" else "~"

			delta = math.abs(delta)

			table.insert(element, Property(property, `{sign}{delta} line{delta ~= 1 and "s" or ""}`))
		end

		if snapshot.name and instance and instance.Name ~= snapshot.name then
			table.insert(element, Property("Name", instance.Name, snapshot.name))
		end

		table.insert(elements, element)
	end

	return Section {
		Title = `Updates ({#snapshots})`,
		Color = Theme.Colors.Diff.Update,
		Elements = elements,
	}
end

local function Removals(objects: { Types.Ref | Instance }, tree: any)
	local elements = {}

	for _, object in objects do
		local instance = object

		if typeof(object) == "buffer" then
			instance = tree:getInstance(object)
		end

		local element = {
			Title {
				Symbol = "-",
				Name = instance and instance:GetFullName() or "?",
				Class = instance and instance.ClassName or "?",
				Color = Theme.Colors.Diff.Remove,
			},
		}

		local descendants = instance and #instance:GetDescendants() or 0
		if descendants > 0 then
			table.insert(element, Property("Descendants", descendants))
		end

		local lines = instance and countInstanceLines(instance) or 0
		if lines > 0 then
			table.insert(element, Property("Lines", lines))
		end

		table.insert(elements, element)
	end

	return Section {
		Title = `Removals ({#objects})`,
		Color = Theme.Colors.Diff.Remove,
		Elements = elements,
	}
end

type Props = {
	Changes: Types.Changes,
	Tree: any,
	Closed: (() -> ())?,
}

return function(props: Props): DockWidgetPluginGui
	local changes = props.Changes
	local tree = props.Tree
	local entries = {}

	if #changes.additions > 0 then
		table.insert(entries, Additions(changes.additions, tree))
	end

	if #changes.updates > 0 then
		table.insert(entries, Updates(changes.updates, tree))
	end

	if #changes.removals > 0 then
		table.insert(entries, Removals(changes.removals, tree))
	end

	return Widget {
		Name = "Argon - Diff",
		OverrideEnabled = true,
		MinimumSize = Vector2.new(500, 400),
		Closed = props.Closed,
		[Children] = {
			Padding {
				Right = 4,
			},
			ScrollingContainer {
				ScrollBar = true,
				[Children] = {
					List {},
					Padding {
						Padding = Theme.WidgetPadding,
					},
					entries,
				},
			},
		},
	}
end


local Argon = script:FindFirstAncestor("Argon")
local App = Argon.App
local Components = App.Components

local Fusion = require(Argon.Packages.Fusion)

local Theme = require(App.Theme)

local Widget = require(Components.Plugin.Widget)
local ScrollingContainer = require(Components.ScrollingContainer)
local Container = require(Components.Container)
local Padding = require(Components.Padding)
local Input = require(Components.Input)
local List = require(Components.List)
local Text = require(Components.Text)
local Box = require(Components.Box)

local Children = Fusion.Children

local function Entry(body: { any })
	return Box {
		Size = UDim2.fromScale(1, 0),
		[Children] = {
			Padding {},
			List {
				Spacing = 6,
			},
			body,
		},
	}
end

local function Header(text: string)
	return Text {
		Font = Theme.Fonts.Bold,
		Text = text,
	}
end

local function Paragraph(text: string)
	return Text {
		Text = text,
		RichText = true,
		TextWrapped = true,
		TextSize = Theme.TextSize.Small,
		Color = Theme.Colors.TextDimmed,
	}
end

local function Link(text: string, url: string)
	return Container {
		Size = UDim2.fromScale(1, 0),
		[Children] = {
			List {
				HorizontalAlignment = Enum.HorizontalAlignment.Center,
				Spacing = 8,
			},
			Text {
				Font = Theme.Fonts.Bold,
				Text = text,
			},
			Box {
				AutomaticSize = Enum.AutomaticSize.XY,
				[Children] = {
					Padding {},
					Input {
						AutomaticSize = Enum.AutomaticSize.XY,
						Text = url,
						TextEditable = false,
						TextSize = Theme.TextSize.Large,
						Color = Theme.Colors.Brand,
					},
				},
			},
		},
	}
end

type Props = {
	Closed: (() -> ())?,
}

return function(props: Props): DockWidgetPluginGui
	return Widget {
		Name = "Argon - Help",
		OverrideEnabled = true,
		MinimumSize = Vector2.new(400, 350),
		Closed = props.Closed,
		[Children] = {
			Padding {
				Right = 4,
			},
			ScrollingContainer {
				ScrollBar = true,
				[Children] = {
					List {
						Spacing = 22,
					},
					Padding {
						Padding = Theme.WidgetPadding,
					},
					Link("Visit official website to learn more:", "https://argon.wiki"),
					Entry {
						Header "1. Setting up the project",
						Paragraph "First you need to create new Argon project. If you are using CLI run argon init command.",
						Paragraph "If you are using VS Code extension open command palette (ctrl/cmd + shift + P) and run Argon: Open Menu -> Init.",
					},
					Entry {
						Header "2. Starting the server",
						Paragraph "To start the server run argon serve command in your terminal or use Argon: Open Menu -> Serve command in VS Code.",
					},
					Entry {
						Header "3. Connecting to the server",
						Paragraph "To connect to the server first make sure that host name and port are the same. Then press Connect button and you are done!",
					},
					Entry {
						Header "4. Syncing",
						Paragraph "Now you can finally start syncing your changes. Remember to save your files in order to see the changes in Roblox Studio.",
						Paragraph "You can also enable Two-Way Sync in the settings to sync changes made in studio back to the file system.",
					},
				},
			},
		},
	}
end


local StudioService = game:GetService("StudioService")

local Argon = script:FindFirstAncestor("Argon")
local App = Argon.App
local Components = App.Components

local Fusion = require(Argon.Packages.Fusion)

local Types = require(Argon.Types)

local Theme = require(App.Theme)

local Widget = require(Components.Plugin.Widget)
local ScrollingContainer = require(Components.ScrollingContainer)
local Container = require(Components.Container)
local Padding = require(Components.Padding)
local Image = require(Components.Image)
local Text = require(Components.Text)
local List = require(Components.List)
local Box = require(Components.Box)

local Hydrate = Fusion.Hydrate
local Children = Fusion.Children
local Computed = Fusion.Computed
local ForValues = Fusion.ForValues
local cleanup = Fusion.cleanup
local peek = Fusion.peek

type EntryProps = {
	Name: string,
	Value: string,
}

local function Entry(props: EntryProps): Frame
	return Box {
		Size = UDim2.fromScale(1, 0),
		[Children] = {
			Padding {},
			Text {
				AnchorPoint = Vector2.new(0, 0.5),
				Position = UDim2.fromScale(0, 0.5),
				Size = UDim2.new(0, 160, 1, 0),
				Text = props.Name,
			},
			Text {
				AnchorPoint = Vector2.new(1, 0),
				Position = UDim2.fromScale(1, 0),
				Size = UDim2.new(1, -160, 1, 0),
				AutomaticSize = Enum.AutomaticSize.Y,
				TextColor3 = Theme.Colors.TextDimmed,
				TextSize = Theme.TextSize.Medium,
				TextWrapped = true,
				Text = props.Value,
			},
		},
	}
end

type IconListEntryProps = {
	Name: string,
	Values: {
		{
			Name: string,
			Icon: {
				Image: string,
				ImageRectOffset: Vector2,
				ImageRectSize: Vector2,
			},
		}
	},
}

local function IconListEntry(props: IconListEntryProps): Frame
	return Box {
		Size = UDim2.fromScale(1, 0),
		[Children] = {
			Padding {},
			Text {
				AnchorPoint = Vector2.new(0, 0.5),
				Position = UDim2.fromScale(0, 0.5),
				Size = UDim2.new(0, 160, 1, 0),
				Text = props.Name,
			},
			Container {
				AnchorPoint = Vector2.new(1, 0),
				Position = UDim2.fromScale(1, 0),
				Size = UDim2.new(1, -160, 1, 0),
				AutomaticSize = Enum.AutomaticSize.Y,
				[Children] = {
					List {
						Spacing = 0,
					},
					ForValues(props.Values, function(_, value)
						return Container {
							Size = UDim2.new(1, 0, 0, Theme.CompSizeY.Small),
							[Children] = {
								Hydrate(Image {
									AnchorPoint = Vector2.new(0, 0.5),
									Position = UDim2.fromScale(0, 0.5),
									Size = UDim2.fromOffset(Theme.TextSize.Large, Theme.TextSize.Large),
								})(value.Icon),
								Text {
									Position = UDim2.fromOffset(Theme.TextSize.Large + 4, 0),
									Size = UDim2.new(1, -(Theme.TextSize.Large + 4), 1, 0),
									TextColor3 = Theme.Colors.TextDimmed,
									TextSize = Theme.TextSize.Medium,
									AutomaticSize = Enum.AutomaticSize.Y,
									TextWrapped = true,
									Text = value.Name,
								},
							},
						}
					end, cleanup),
				},
			},
		},
	}
end

type Props = {
	App: { [string]: any },
	Project: Fusion.Value<Types.Project>,
	Closed: (() -> ())?,
}

return function(props: Props): DockWidgetPluginGui
	return Widget {
		Name = "Argon - Project",
		OverrideEnabled = true,
		MinimumSize = Vector2.new(400, 300),
		Closed = props.Closed,
		[Children] = {
			Padding {
				Right = 4,
			},
			ScrollingContainer {
				ScrollBar = true,
				[Children] = {
					List {
						HorizontalAlignment = Enum.HorizontalAlignment.Center,
					},
					Padding {
						Padding = Theme.WidgetPadding,
					},
					Entry {
						Name = "Name",
						Value = Computed(function(use)
							return use(props.Project).name
						end),
					},
					Entry {
						Name = "Game ID",
						Value = Computed(function(use)
							return use(props.Project).gameId or "Any"
						end),
					},
					Entry {
						Name = "Place IDs",
						Value = Computed(function(use)
							local project = use(props.Project)
							return #project.placeIds > 0 and table.concat(project.placeIds, ", ") or "Any"
						end),
					},
					IconListEntry {
						Name = "Root Instances",
						Values = Computed(function(use)
							use(props.Project)

							local values = {}

							for i, instance in props.App.core.rootInstances do
								values[i] = {
									Name = instance.Name,
									Icon = StudioService:GetClassIcon(instance.ClassName),
								}
							end

							return values
						end),
					},
					Entry {
						Name = "Server Version",
						Value = Computed(function(use)
							return use(props.Project).version
						end),
					},
					Entry {
						Name = "Host",
						Value = peek(props.App.host),
					},
					Entry {
						Name = "Port",
						Value = peek(props.App.port),
					},
				},
			},
		},
	}
end


local Argon = script:FindFirstAncestor("Argon")
local App = Argon.App
local Components = App.Components
local Util = Components.Util

local Fusion = require(Argon.Packages.Fusion)

local Config = require(Argon.Config)
local GlobalUtil = require(Argon.Util)

local Theme = require(App.Theme)
local default = require(Util.default)
local filterHost = require(Util.filterHost)
local filterPort = require(Util.filterPort)
local filterNumber = require(Util.filterNumber)
local getTextSize = require(Util.getTextSize)

local Widget = require(Components.Plugin.Widget)
local ScrollingContainer = require(Components.ScrollingContainer)
local OptionSelector = require(Components.OptionSelector)
local Collapsible = require(Components.Collapsible)
local TextButton = require(Components.TextButton)
local Container = require(Components.Container)
local Checkbox = require(Components.Checkbox)
local Dropdown = require(Components.Dropdown)
local Padding = require(Components.Padding)
local Input = require(Components.Input)
local List = require(Components.List)
local Text = require(Components.Text)
local Box = require(Components.Box)

local New = Fusion.New
local Value = Fusion.Value
local Hydrate = Fusion.Hydrate
local Cleanup = Fusion.Cleanup
local Observer = Fusion.Observer
local Computed = Fusion.Computed
local Children = Fusion.Children
local OnChange = Fusion.OnChange
local ForValues = Fusion.ForValues
local ForPairs = Fusion.ForPairs
local Spring = Fusion.Spring
local cleanup = Fusion.cleanup
local peek = Fusion.peek

local SETTINGS_DATA: { { Title: string, Settings: { SettingData } } } = {
	{
		Title = "General",
		Settings = {
			{
				Setting = "InitialSyncPriority",
				Name = "Initial Sync Priority",
				Description = "Which side should be considered as up to date when connecting",
				Options = { "Server", "Client", "None" },
			},
			{
				Setting = "Host",
				Name = "Server Host",
				Description = "The host of the server that you want to connect to",
				Filter = filterHost,
				CustomWidth = 0.31,
			},
			{
				Setting = "Port",
				Name = "Server Port",
				Description = "The port of the server that you want to connect to",
				Filter = filterPort,
				CustomWidth = 70,
			},
		},
	},
	{
		Title = "Automation",
		Settings = {
			{
				Setting = "AutoConnect",
				Name = "Auto Connect",
				Description = "Automatically attempt to connect to the server when you open a new place",
			},
			{
				Setting = "AutoReconnect",
				Name = "Auto Reconnect",
				Description = "If the connection is lost, automatically reconnect to the server after delay",
			},
			{
				Setting = "LiveHydrate",
				Name = "Live Hydrate",
				Description = "If the target instance doesn't exist in Studio, automatically hydrate it with the server",
			},
			{
				Setting = "KeepUnknowns",
				Name = "Keep Unknowns",
				Description = "By default keep instances that are not present in the file system",
			},
		},
	},
	{
		Title = "Syncback",
		Settings = {
			{
				Setting = "TwoWaySync",
				Name = "Two-Way Sync",
				Description = "Sync changes made in Roblox Studio back to the server (local file system)",
			},
			{
				Setting = "SyncbackProperties",
				Name = "Syncback Properties",
				Description = "Whether all properties should be synced back to the server (does not affect scripts)",
			},
			{
				Setting = "OnlyCodeMode",
				Name = "Only Code Mode",
				Description = "Syncback only scripts and instances that have descendant scripts",
			},
			{
				Setting = "OpenInEditor",
				Name = "Open In Editor",
				Description = "Open scripts in your OS default editor instead of the Roblox Studio one",
			},
		},
	},
	{
		Title = "Misc",
		Settings = {
			{
				Setting = "DisplayPrompts",
				Name = "Display Prompts",
				Description = "When to show prompts for actions that require confirmation",
				Options = { "Always", "Initial", "Never" },
			},
			{
				Setting = "ChangesThreshold",
				Name = "Changes Threshold",
				Description = "The maximum number of changes that will be applied before prompting the user",
				Filter = filterNumber,
				CustomWidth = 50,
			},
			{
				Setting = "LogLevel",
				Name = "Log Level",
				Description = "The level of logging you want to see in the output",
				Options = { "Off", "Error", "Warn", "Info", "Debug", "Trace" },
			},
			{
				Setting = "OverridePackages",
				Name = "Override Packages",
				Description = "Whether writing changes to the instances affected by PackageLink should be allowed",
			},
		},
	},
}

local LEVELS = { "Global", "Game", "Place" }

type SettingData = {
	Setting: Config.Setting,
	Name: string,
	Description: string,
	Options: { string }?,
	Filter: ((value: string) -> string)?,
	CustomWidth: number?,
}

type EntryProps = {
	Data: SettingData,
	Level: Fusion.Value<Config.Level>,
	Binding: Fusion.Value<any>,
}

local function Entry(props: EntryProps): Frame
	local valueComponent
	local setting = props.Data.Setting

	local absoluteSize = Value(Vector2.zero)
	local placeholder = Value(Config:get(setting))

	local connections = {
		Config:onChanged(setting, function(value)
			placeholder:set(value)
		end),
		Observer(props.Level):onChange(function()
			placeholder:set(Config:get(setting))
		end),
	}

	if props.Data.Filter then
		local userInput = false

		table.insert(
			connections,
			Observer(props.Binding):onChange(function()
				local value = peek(props.Binding)

				if not userInput and value == Config:getDefault(setting) then
					props.Binding:set("")
				end

				userInput = false
			end)
		)

		if peek(props.Binding) == Config:getDefault(setting) then
			props.Binding:set("")
		end

		local focused = Value(false)

		valueComponent = Box {
			Size = props.Data.CustomWidth
					and math.floor(props.Data.CustomWidth) == props.Data.CustomWidth
					and UDim2.fromOffset(props.Data.CustomWidth, Theme.CompSizeY.Medium)
				or UDim2.new(props.Data.CustomWidth or 0.2, 0, 0, Theme.CompSizeY.Medium),
			BorderColor = Spring(
				Computed(function(use)
					if use(focused) then
						return use(Theme.Colors.Brand)
					else
						return use(Theme.Colors.Border)
					end
				end),
				Theme.SpringFastSpeed,
				Theme.SpringDamping
			),
			[Children] = {
				Input {
					Size = UDim2.fromScale(1, 1),
					Text = props.Binding,
					PlaceholderText = placeholder,
					Started = function()
						focused:set(true)
					end,
					Changed = function(text)
						userInput = true
						props.Binding:set(props.Data.Filter(text))
					end,
					Finished = function(text)
						focused:set(false)
						text = text ~= "" and text or Config:getDefault(setting)

						props.Binding:set(text)
						Config:set(setting, text, peek(props.Level))
					end,
					[Children] = {
						Padding {
							Vertical = false,
						},
					},
				},
			},
		}
	elseif props.Data.Options then
		valueComponent = Dropdown {
			Options = props.Data.Options,
			Value = props.Binding,
			Placeholder = placeholder,
			Selected = function(value)
				Config:set(setting, value, peek(props.Level))
			end,
		}
	else
		valueComponent = Checkbox {
			Value = props.Binding,
			Placeholder = placeholder,
			Changed = function(value)
				Config:set(setting, value, peek(props.Level))
			end,
		}
	end

	return Container {
		ZIndex = props.Data.Options and 2 or 1,
		Size = UDim2.fromScale(1, 0),
		[Children] = {
			New "Frame" {
				Size = UDim2.new(1, 0, 0, Theme.BorderThickness),
				BackgroundColor3 = Theme.Colors.Border,
				BorderSizePixel = 0,
			},
			Container {
				Size = UDim2.fromScale(1, 1),
				[Children] = {
					Padding {},
					Text {
						Text = props.Data.Name,
						Font = Theme.Fonts.Bold,
					},
					Container {
						Size = UDim2.fromScale(1, 0),
						[Children] = {
							List {
								FillDirection = Enum.FillDirection.Horizontal,
								VerticalAlignment = Enum.VerticalAlignment.Center,
								HorizontalFlex = Enum.UIFlexAlignment.Fill,
							},
							Container {
								[Children] = {
									Text {
										RichText = true,
										TextWrapped = true,
										AutomaticSize = Enum.AutomaticSize.None,
										Text = props.Data.Description,
										TextSize = Theme.TextSize.Small,
										Color = Theme.Colors.TextDimmed,
										Position = UDim2.fromOffset(0, 22),
										Size = Computed(function(use)
											local absoluteSize = use(absoluteSize)

											local size = getTextSize(
												props.Data.Description,
												Theme.TextSize.Small,
												Theme.Fonts.Enums.Regular,
												Vector2.new(absoluteSize.X, math.huge)
											)

											return UDim2.new(1, 0, 0, size.Y)
										end),
										[OnChange "AbsoluteSize"] = function(size)
											absoluteSize:set(size)
										end,
									},
								},
							},
							Hydrate(valueComponent) {
								[Cleanup] = connections,
								[Children] = {
									New "UIFlexItem" {},
								},
							},
						},
					},
				},
			},
		},
	}
end

type Props = {
	Closed: (() -> ())?,
}

return function(props: Props): DockWidgetPluginGui
	local level = Value("Global")
	local bindings = {}

	return Widget {
		Name = "Argon - Settings",
		OverrideEnabled = true,
		MinimumSize = Vector2.new(350, 400),
		Closed = props.Closed,
		[Children] = {
			Padding {
				Right = 4,
			},
			ScrollingContainer {
				ScrollBar = true,
				[Children] = {
					List {},
					Padding {
						Padding = Theme.WidgetPadding,
					},
					OptionSelector {
						Options = LEVELS,
						Selected = function(option)
							level:set(option)

							for setting, binding in bindings do
								binding:set(default(Config:get(setting, option, true), Config:getDefault(setting)))
							end
						end,
					},
					ForPairs(SETTINGS_DATA, function(_, index, data)
						return index,
							Collapsible {
								Title = `{index}. {data.Title}`,
								Expanded = index == 1,
								Elements = ForValues(data.Settings, function(_, data)
									local setting = data.Setting
									local binding =
										Value(default(Config:get(setting, peek(level)), Config:getDefault(setting)))

									bindings[setting] = binding

									return Entry {
										Data = data,
										Level = level,
										Binding = binding,
									}
								end, cleanup),
							}
					end, cleanup),
					Container {
						Size = UDim2.fromScale(1, 0),
						LayoutOrder = GlobalUtil.len(SETTINGS_DATA) + 1,
						ZIndex = 0,
						[Children] = {
							TextButton {
								AnchorPoint = Vector2.new(1, 0),
								Position = UDim2.fromScale(1, 0),
								Text = "Restore Defaults",
								Activated = function()
									for setting, binding in bindings do
										binding:set(Config:getDefault(setting))
									end

									Config:restoreDefaults(peek(level))
								end,
							},
						},
					},
				},
			},
		},
	}
end


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

local plugin = script:FindFirstAncestorWhichIsA("Plugin")

local Argon = script:FindFirstAncestor("Argon")
local Packages = Argon.Packages
local Helpers = Argon.Helpers

local Components = script.Components
local Widgets = script.Widgets
local Pages = script.Pages

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

local manifest = require(Argon.manifest)
local Config = require(Argon.Config)
local Types = require(Argon.Types)
local Util = require(Argon.Util)
local Core = require(Argon.Core)
local describeChanges = require(Helpers.describeChanges)

local Assets = require(script.Assets)
local Loader = require(script.Loader)
local Theme = require(script.Theme)

local Toolbar = require(Components.Plugin.Toolbar)
local ToolbarButton = require(Components.Plugin.ToolbarButton)
local Widget = require(Components.Plugin.Widget)
local Container = require(Components.Container)
local Padding = require(Components.Padding)
local Image = require(Components.Image)
local Text = require(Components.Text)
local List = require(Components.List)

local NotConnected = require(Pages.NotConnected)
local Connecting = require(Pages.Connecting)
local Connected = require(Pages.Connected)
local Unavailable = require(Pages.Unavailable)
local Prompt = require(Pages.Prompt)
local Error = require(Pages.Error)

local Settings = require(Widgets.Settings)
local Help = require(Widgets.Help)
local Diff = require(Widgets.Diff)

local New = Fusion.New
local Value = Fusion.Value
local Spring = Fusion.Spring
local OnEvent = Fusion.OnEvent
local OnChange = Fusion.OnChange
local Observer = Fusion.Observer
local Computed = Fusion.Computed
local Children = Fusion.Children
local cleanup = Fusion.cleanup
local peek = Fusion.peek

local RECONNECT_INTERVAL = 5

local App = {}
App.__index = App

function App.new()
	Loader.load()

	local self = setmetatable({}, App)

	self.core = nil
	self.lastUpdate = os.clock()
	self.onCanceled = Signal.new()

	self.host = Config:get("Host")
	self.port = Config:get("Port")

	self.lastSync = Value(os.time())
	self.lastSyncKind = Value("Unknown")

	self.rootSize = Value(Vector2.new(300, 200))
	self.icon = Value(Assets.Argon.Logo)
	self.pages = Value({})

	self.settingsWidget = Value(nil)
	self.helpWidget = Value(nil)
	self.diffWidget = Value(nil)

	local isOpen = Value(false)

	local toolbarButton = ToolbarButton {
		Toolbar = Toolbar {
			Name = "Dervex Tools",
		},
		Name = "Argon",
		ToolTip = "Open Argon UI",
		Image = self.icon,
		[OnEvent "Click"] = function()
			isOpen:set(not peek(isOpen))
		end,
	}

	Widget {
		Name = "Argon",
		MinimumSize = peek(self.rootSize),
		Enabled = isOpen,
		[OnChange "Enabled"] = function(isEnabled)
			isOpen:set(isEnabled)
		end,
		[OnChange "AbsoluteSize"] = function(size)
			self.rootSize:set(size)
		end,
		[Children] = self.pages,
	}

	plugin.Unloading:Connect(Observer(isOpen):onChange(function()
		toolbarButton:SetActive(peek(isOpen))
	end))

	toolbarButton:SetActive(peek(isOpen))

	if RunService:IsEdit() then
		self:home()

		if Config:get("AutoConnect") then
			self:connect()
		end
	else
		self:setPage(Unavailable())
	end

	return self
end

function App:setIcon(icon: ("Ok" | "Warn" | "Error")?)
	if icon == "Ok" then
		self.icon:set(Assets.Argon.LogoOk)
	elseif icon == "Warn" then
		self.icon:set(Assets.Argon.LogoWarn)
	elseif icon == "Error" then
		self.icon:set(Assets.Argon.LogoError)
	else
		self.icon:set(Assets.Argon.Logo)
	end
end

function App:setPage(page)
	local pages = peek(self.pages)

	for id, child in pages do
		if child._finished then
			page[id] = nil
			continue
		end

		task.spawn(function()
			child._destructor(child._value)
			child._finished = true
		end)
	end

	local size = Value(UDim2.fromScale(1, 1))

	-- Extremely hacky way to smoothly transition between pages
	page = Computed(function()
		return New "Frame" {
			AnchorPoint = Vector2.new(0.5, 0.5),
			Position = UDim2.fromScale(0.5, 0.5),
			Size = Spring(size, 10),
			BackgroundColor3 = Theme.Colors.Background,
			BorderSizePixel = 0,
			[Children] = {
				List {
					VerticalAlignment = Enum.VerticalAlignment.Center,
					HorizontalAlignment = Enum.HorizontalAlignment.Center,
					Spacing = 8,
				},
				Padding {
					Padding = Theme.WidgetPadding,
				},
				Container {
					Size = UDim2.fromScale(1, 0),
					[Children] = {
						Image {
							Size = UDim2.fromOffset(155, 35),
							Image = Assets.Argon.Banner,
							ImageColor3 = Theme.Colors.Brand,
						},
						Text {
							AnchorPoint = Vector2.new(1, 1),
							Position = UDim2.new(1, -3, 1, 0),
							Text = `v{manifest.package.version}`,
							Color = Theme.Colors.TextDimmed,
							TextSize = Theme.TextSize.Small,
						},
					},
				},
				Container {
					Size = UDim2.fromScale(1, 0),
					[Children] = page,
				},
			},
		}
	end, function(page)
		if not page then
			return
		end

		local TWEEN_INFO = TweenInfo.new(0.2, Enum.EasingStyle.Sine, Enum.EasingDirection.Out)
		TweenService:Create(page, TWEEN_INFO, { BackgroundTransparency = 1 }):Play()

		for _, descendant in page:GetDescendants() do
			local properties = {}

			if descendant:IsA("GuiObject") then
				if descendant:IsA("GuiButton") then
					descendant.Active = false
				end

				properties.BackgroundTransparency = 1

				if descendant:IsA("TextLabel") or descendant:IsA("TextButton") or descendant:IsA("TextBox") then
					properties.TextTransparency = 1
				elseif descendant:IsA("ImageLabel") or descendant:IsA("ImageButton") then
					properties.ImageTransparency = 1
				end
			end

			if descendant:IsA("UIStroke") then
				properties.Transparency = 1
			end

			if next(properties) then
				TweenService:Create(descendant, TWEEN_INFO, properties):Play()
			end
		end

		page.ZIndex = 2

		size:set(UDim2.fromScale(1.2, 1))

		task.wait(0.2)
		cleanup(page)
	end)

	pages[Util.generateGUID()] = page
	self.pages:set(pages)
end

function App:home()
	self.lastUpdate = os.clock()

	self:setPage(NotConnected {
		App = self,
		Host = self.host,
		Port = self.port,
	})

	self:setIcon()
end

function App:settings()
	local widget = peek(self.settingsWidget)

	if widget then
		widget:Destroy()
		widget = nil
	else
		widget = Settings {
			Closed = function()
				peek(self.settingsWidget):Destroy()
				self.settingsWidget:set(nil)
			end,
		}
	end

	self.settingsWidget:set(widget)
end

function App:help()
	local widget = peek(self.helpWidget)

	if widget then
		widget:Destroy()
		widget = nil
	else
		widget = Help {
			Closed = function()
				peek(self.helpWidget):Destroy()
				self.helpWidget:set(nil)
			end,
		}
	end

	self.helpWidget:set(widget)
end

function App:diff(changes: Types.Changes?, tree: any?)
	local widget = peek(self.diffWidget)

	if widget then
		widget:Destroy()
		widget = nil
	elseif changes and tree then
		widget = Diff {
			Changes = changes,
			Tree = tree,
			Closed = function()
				peek(self.diffWidget):Destroy()
				self.diffWidget:set(nil)
			end,
		}
	end

	self.diffWidget:set(widget)
end

function App:connect()
	local project = Value({})
	local canceled = false
	local loading = true

	self.core = Core.new(self.host, self.port)

	task.spawn(function()
		task.wait(0.2)

		if not self:isConnected() and loading then
			self:setPage(Connecting {
				App = self,
			})
		end
	end)

	self.onCanceled:Once(function()
		canceled = true
	end)

	self.core:onPrompt(function(message, changes, tree)
		loading = false

		local signal = Signal.new()
		local result = false

		self:setIcon("Warn")

		-- Prompt user to accept or reject incoming changes
		if changes then
			local options = { "Accept", "Diff", "Cancel" }
			message = describeChanges(changes)

			self:setPage(Prompt {
				App = self,
				Message = message,
				Options = options,
				Signal = signal,
			})

			local choice = signal:Wait()

			while choice == "Diff" do
				self:diff(changes, tree)
				choice = signal:Wait()
			end

			self:diff()

			result = choice == "Accept"
		-- Prompt user whether to continue when server ID differs
		else
			local options = { "Continue", "Cancel" }

			self:setPage(Prompt {
				App = self,
				Message = message,
				Options = options,
				Signal = signal,
			})

			result = signal:Wait() == "Continue"
		end

		if self.core.status == Core.Status.Connected then
			self:setPage(Connected {
				App = self,
				Project = project,
			})

			self:setIcon("Ok")
		end

		return result
	end)

	self.core:onReady(function(projectDetails)
		loading = false
		project:set(projectDetails)

		self.lastSync:set(os.time())

		self.lastSyncKind:set(Config:get("InitialSyncPriority"))

		self:setPage(Connected {
			App = self,
			Project = project,
		})

		self:setIcon("Ok")
	end)

	self.core:onSync(function(kind, data)
		self.lastSync:set(os.time())
		self.lastSyncKind:set(
			(kind == "SyncChanges" or kind == "SyncDetails") and "Server"
				or kind == "SyncbackChanges" and "Client"
				or "None"
		)

		if kind == "SyncDetails" then
			project:set(data)
		end
	end)

	self.core:run():catch(function(err)
		loading = false

		if Core.wasExitGraceful(err) or canceled then
			self:disconnect()
			self:home()
			return
		end

		self:setPage(Error {
			App = self,
			Message = err.message or tostring(err),
		})

		self:setIcon("Error")

		self:disconnect()

		if Config:get("AutoReconnect") then
			task.wait(RECONNECT_INTERVAL)

			if self.lastUpdate + RECONNECT_INTERVAL <= os.clock() then
				self:connect()
			end
		end
	end)
end

function App:disconnect(cancel: boolean?)
	self.lastUpdate = os.clock()

	if self.core then
		if cancel then
			self.onCanceled:Fire()
		end

		self.core:stop()
		self.core = nil
	end
end

function App:isConnected()
	return self.core and self.core.status == Core.Status.Connected
end

function App:setHost(host: string)
	self.host = host
end

function App:setPort(port: number)
	self.port = port
end

return App