local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Packages = Rojo.Packages

local Roact = require(Packages.Roact)

local timeUtil = require(Plugin.timeUtil)
local Theme = require(Plugin.App.Theme)
local Assets = require(Plugin.Assets)
local PatchSet = require(Plugin.PatchSet)

local StudioPluginGui = require(Plugin.App.Components.Studio.StudioPluginGui)
local Header = require(Plugin.App.Components.Header)
local IconButton = require(Plugin.App.Components.IconButton)
local TextButton = require(Plugin.App.Components.TextButton)
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
local Tooltip = require(Plugin.App.Components.Tooltip)
local PatchVisualizer = require(Plugin.App.Components.PatchVisualizer)
local StringDiffVisualizer = require(Plugin.App.Components.StringDiffVisualizer)
local TableDiffVisualizer = require(Plugin.App.Components.TableDiffVisualizer)

local e = Roact.createElement

local ChangesViewer = Roact.Component:extend("ChangesViewer")

function ChangesViewer:init()
	-- Hold onto the serve session during the lifecycle of this component
	-- so that it can still render during the fade out after disconnecting
	self.serveSession = self.props.serveSession
end

function ChangesViewer:render()
	if self.props.rendered == false or self.serveSession == nil or self.props.patchData == nil then
		return nil
	end

	local unapplied = PatchSet.countChanges(self.props.patchData.unapplied)
	local applied = PatchSet.countChanges(self.props.patchData.patch) - unapplied

	return Theme.with(function(theme)
		return Roact.createFragment({
			Navbar = e("Frame", {
				Size = UDim2.new(1, 0, 0, 40),
				BackgroundTransparency = 1,
			}, {
				Close = e(IconButton, {
					icon = Assets.Images.Icons.Close,
					iconSize = 24,
					color = theme.Settings.Navbar.BackButtonColor,
					transparency = self.props.transparency,

					position = UDim2.new(0, 0, 0.5, 0),
					anchorPoint = Vector2.new(0, 0.5),

					onClick = self.props.onBack,
				}, {
					Tip = e(Tooltip.Trigger, {
						text = "Close",
					}),
				}),

				Title = e("TextLabel", {
					Text = "Sync",
					FontFace = theme.Font.Main,
					TextSize = theme.TextSize.Large,
					TextXAlignment = Enum.TextXAlignment.Left,
					TextColor3 = theme.TextColor,
					TextTransparency = self.props.transparency,
					Size = UDim2.new(1, -40, 0, theme.TextSize.Large + 2),
					Position = UDim2.new(0, 40, 0, 0),
					BackgroundTransparency = 1,
				}),

				Subtitle = e("TextLabel", {
					Text = DateTime.fromUnixTimestamp(self.props.patchData.timestamp):FormatLocalTime("LTS", "en-us"),
					TextXAlignment = Enum.TextXAlignment.Left,
					FontFace = theme.Font.Thin,
					TextSize = theme.TextSize.Medium,
					TextColor3 = theme.SubTextColor,
					TextTruncate = Enum.TextTruncate.AtEnd,
					TextTransparency = self.props.transparency,
					Size = UDim2.new(1, -40, 0, theme.TextSize.Medium),
					Position = UDim2.new(0, 40, 0, theme.TextSize.Large + 2),
					BackgroundTransparency = 1,
				}),

				Info = e("Frame", {
					BackgroundTransparency = 1,
					Size = UDim2.new(0, 10, 0, 24),
					AutomaticSize = Enum.AutomaticSize.X,
					Position = UDim2.new(1, -5, 0.5, 0),
					AnchorPoint = Vector2.new(1, 0.5),
				}, {
					Tooltip = e(Tooltip.Trigger, {
						text = `{applied} changes applied`
							.. (if unapplied > 0 then `, {unapplied} changes failed` else ""),
					}),
					Content = e("Frame", {
						BackgroundTransparency = 1,
						Size = UDim2.new(0, 0, 1, 0),
						AutomaticSize = Enum.AutomaticSize.X,
					}, {
						Layout = e("UIListLayout", {
							FillDirection = Enum.FillDirection.Horizontal,
							HorizontalAlignment = Enum.HorizontalAlignment.Right,
							VerticalAlignment = Enum.VerticalAlignment.Center,
							SortOrder = Enum.SortOrder.LayoutOrder,
							Padding = UDim.new(0, 4),
						}),

						StatusIcon = e("ImageLabel", {
							BackgroundTransparency = 1,
							Image = if unapplied > 0
								then Assets.Images.Icons.SyncWarning
								else Assets.Images.Icons.SyncSuccess,
							ImageColor3 = if unapplied > 0 then theme.Diff.Warning else theme.TextColor,
							Size = UDim2.new(0, 24, 0, 24),
							LayoutOrder = 10,
						}),
						StatusSpacer = e("Frame", {
							BackgroundTransparency = 1,
							Size = UDim2.new(0, 6, 0, 4),
							LayoutOrder = 9,
						}),
						AppliedIcon = e("ImageLabel", {
							BackgroundTransparency = 1,
							Image = Assets.Images.Icons.Checkmark,
							ImageColor3 = theme.TextColor,
							Size = UDim2.new(0, 16, 0, 16),
							LayoutOrder = 1,
						}),
						AppliedText = e("TextLabel", {
							Text = applied,
							FontFace = theme.Font.Thin,
							TextSize = theme.TextSize.Body,
							TextColor3 = theme.TextColor,
							TextTransparency = self.props.transparency,
							Size = UDim2.new(0, 0, 1, 0),
							AutomaticSize = Enum.AutomaticSize.X,
							BackgroundTransparency = 1,
							LayoutOrder = 2,
						}),
						Warnings = if unapplied > 0
							then Roact.createFragment({
								WarningsSpacer = e("Frame", {
									BackgroundTransparency = 1,
									Size = UDim2.new(0, 4, 0, 4),
									LayoutOrder = 3,
								}),
								UnappliedIcon = e("ImageLabel", {
									BackgroundTransparency = 1,
									Image = Assets.Images.Icons.Exclamation,
									ImageColor3 = theme.Diff.Warning,
									Size = UDim2.new(0, 4, 0, 16),
									LayoutOrder = 4,
								}),
								UnappliedText = e("TextLabel", {
									Text = unapplied,
									FontFace = theme.Font.Thin,
									TextSize = theme.TextSize.Body,
									TextColor3 = theme.Diff.Warning,
									TextTransparency = self.props.transparency,
									Size = UDim2.new(0, 0, 1, 0),
									AutomaticSize = Enum.AutomaticSize.X,
									BackgroundTransparency = 1,
									LayoutOrder = 5,
								}),
							})
							else nil,
					}),
				}),

				Divider = e("Frame", {
					BackgroundColor3 = theme.Settings.DividerColor,
					BackgroundTransparency = self.props.transparency,
					Size = UDim2.new(1, 0, 0, 1),
					Position = UDim2.new(0, 0, 1, 0),
					BorderSizePixel = 0,
				}, {
					Gradient = e("UIGradient", {
						Transparency = NumberSequence.new({
							NumberSequenceKeypoint.new(0, 1),
							NumberSequenceKeypoint.new(0.1, 0),
							NumberSequenceKeypoint.new(0.9, 0),
							NumberSequenceKeypoint.new(1, 1),
						}),
					}),
				}),
			}),

			Patch = e(PatchVisualizer, {
				size = UDim2.new(1, -10, 1, -65),
				position = UDim2.new(0, 5, 1, -5),
				anchorPoint = Vector2.new(0, 1),
				transparency = self.props.transparency,
				layoutOrder = self.props.layoutOrder,

				patchTree = self.props.patchTree,

				showStringDiff = self.props.showStringDiff,
				showTableDiff = self.props.showTableDiff,
			}),
		})
	end)
end

local function ConnectionDetails(props)
	return Theme.with(function(theme)
		return e(BorderedContainer, {
			transparency = props.transparency,
			size = UDim2.new(1, 0, 0, 70),
			layoutOrder = props.layoutOrder,
		}, {
			TextContainer = e("Frame", {
				Size = UDim2.new(1, 0, 1, 0),
				BackgroundTransparency = 1,
			}, {
				ProjectName = e("TextLabel", {
					Text = props.projectName,
					FontFace = theme.Font.Bold,
					TextSize = theme.TextSize.Large,
					TextColor3 = theme.ConnectionDetails.ProjectNameColor,
					TextTransparency = props.transparency,
					TextXAlignment = Enum.TextXAlignment.Left,

					Size = UDim2.new(1, 0, 0, theme.TextSize.Large),

					LayoutOrder = 1,
					BackgroundTransparency = 1,
				}),

				Address = e("TextLabel", {
					Text = props.address,
					FontFace = theme.Font.Code,
					TextSize = theme.TextSize.Medium,
					TextColor3 = theme.ConnectionDetails.AddressColor,
					TextTransparency = props.transparency,
					TextXAlignment = Enum.TextXAlignment.Left,

					Size = UDim2.new(1, 0, 0, theme.TextSize.Medium),

					LayoutOrder = 2,
					BackgroundTransparency = 1,
				}),

				Layout = e("UIListLayout", {
					VerticalAlignment = Enum.VerticalAlignment.Center,
					FillDirection = Enum.FillDirection.Vertical,
					SortOrder = Enum.SortOrder.LayoutOrder,
					Padding = UDim.new(0, 6),
				}),
			}),

			Padding = e("UIPadding", {
				PaddingLeft = UDim.new(0, 17),
				PaddingRight = UDim.new(0, 15),
			}),
		})
	end)
end

local ConnectedPage = Roact.Component:extend("ConnectedPage")

function ConnectedPage:getChangeInfoText()
	local patchData = self.props.patchData
	if patchData == nil then
		return ""
	end
	return timeUtil.elapsedToText(DateTime.now().UnixTimestamp - patchData.timestamp)
end

function ConnectedPage:startChangeInfoTextUpdater()
	-- Cancel any existing updater
	self:stopChangeInfoTextUpdater()

	-- Start a new updater
	self.changeInfoTextUpdater = task.defer(function()
		while true do
			self.setChangeInfoText(self:getChangeInfoText())

			local elapsed = DateTime.now().UnixTimestamp - self.props.patchData.timestamp
			local updateInterval = 1

			-- Update timestamp text as frequently as currently needed
			for _, UnitData in ipairs(timeUtil.AGE_UNITS) do
				local UnitSeconds = UnitData[1]
				if elapsed > UnitSeconds then
					updateInterval = UnitSeconds
					break
				end
			end

			task.wait(updateInterval)
		end
	end)
end

function ConnectedPage:stopChangeInfoTextUpdater()
	if self.changeInfoTextUpdater then
		task.cancel(self.changeInfoTextUpdater)
		self.changeInfoTextUpdater = nil
	end
end

function ConnectedPage:init()
	self:setState({
		renderChanges = false,
		hoveringChangeInfo = false,
		showingStringDiff = false,
		currentString = "",
		incomingString = "",
	})

	self.changeInfoText, self.setChangeInfoText = Roact.createBinding("")

	self:startChangeInfoTextUpdater()
end

function ConnectedPage:willUnmount()
	self:stopChangeInfoTextUpdater()
end

function ConnectedPage:didUpdate(previousProps)
	if self.props.patchData.timestamp ~= previousProps.patchData.timestamp then
		-- New patch recieved
		self:startChangeInfoTextUpdater()
		self:setState({
			showingStringDiff = false,
		})
	end
end

function ConnectedPage:render()
	local syncWarning = self.props.patchData
		and self.props.patchData.unapplied
		and PatchSet.countChanges(self.props.patchData.unapplied) > 0

	return Theme.with(function(theme)
		return Roact.createFragment({
			Padding = e("UIPadding", {
				PaddingLeft = UDim.new(0, 20),
				PaddingRight = UDim.new(0, 20),
			}),

			Layout = e("UIListLayout", {
				VerticalAlignment = Enum.VerticalAlignment.Center,
				FillDirection = Enum.FillDirection.Vertical,
				SortOrder = Enum.SortOrder.LayoutOrder,
				Padding = UDim.new(0, 10),
			}),

			Heading = e("Frame", {
				BackgroundTransparency = 1,
				Size = UDim2.new(1, 0, 0, 32),
			}, {
				Header = e(Header, {
					transparency = self.props.transparency,
				}),

				ChangeInfo = e("TextButton", {
					Text = "",
					Size = UDim2.new(0, 0, 1, 0),
					AutomaticSize = Enum.AutomaticSize.X,
					BackgroundColor3 = theme.BorderedContainer.BorderedColor,
					BackgroundTransparency = if self.state.hoveringChangeInfo then 0.7 else 1,
					BorderSizePixel = 0,
					Position = UDim2.new(1, -5, 0.5, 0),
					AnchorPoint = Vector2.new(1, 0.5),
					[Roact.Event.MouseEnter] = function()
						self:setState({
							hoveringChangeInfo = true,
						})
					end,
					[Roact.Event.MouseLeave] = function()
						self:setState({
							hoveringChangeInfo = false,
						})
					end,
					[Roact.Event.Activated] = function()
						self:setState(function(prevState)
							prevState = prevState or {}
							return {
								renderChanges = not prevState.renderChanges,
							}
						end)
					end,
				}, {
					Corner = e("UICorner", {
						CornerRadius = UDim.new(0, 5),
					}),
					Tooltip = e(Tooltip.Trigger, {
						text = if self.state.renderChanges then "Hide changes" else "View changes",
					}),
					Content = e("Frame", {
						BackgroundTransparency = 1,
						Size = UDim2.new(0, 0, 1, 0),
						AutomaticSize = Enum.AutomaticSize.X,
					}, {
						Layout = e("UIListLayout", {
							FillDirection = Enum.FillDirection.Horizontal,
							HorizontalAlignment = Enum.HorizontalAlignment.Center,
							VerticalAlignment = Enum.VerticalAlignment.Center,
							SortOrder = Enum.SortOrder.LayoutOrder,
							Padding = UDim.new(0, 5),
						}),
						Padding = e("UIPadding", {
							PaddingLeft = UDim.new(0, 5),
							PaddingRight = UDim.new(0, 5),
						}),
						Text = e("TextLabel", {
							BackgroundTransparency = 1,
							Text = self.changeInfoText,
							FontFace = theme.Font.Thin,
							TextSize = theme.TextSize.Body,
							TextColor3 = if syncWarning then theme.Diff.Warning else theme.Header.VersionColor,
							TextTransparency = self.props.transparency,
							TextXAlignment = Enum.TextXAlignment.Right,
							Size = UDim2.new(0, 0, 1, 0),
							AutomaticSize = Enum.AutomaticSize.X,
							LayoutOrder = 1,
						}),
						Icon = e("ImageLabel", {
							BackgroundTransparency = 1,
							Image = if syncWarning
								then Assets.Images.Icons.SyncWarning
								else Assets.Images.Icons.SyncSuccess,
							ImageColor3 = if syncWarning then theme.Diff.Warning else theme.Header.VersionColor,
							ImageTransparency = self.props.transparency,
							Size = UDim2.new(0, 24, 0, 24),
							LayoutOrder = 2,
						}),
					}),
				}),
			}),

			ConnectionDetails = e(ConnectionDetails, {
				projectName = self.state.projectName,
				address = self.state.address,
				transparency = self.props.transparency,
				layoutOrder = 2,

				onDisconnect = self.props.onDisconnect,
			}),

			Buttons = e("Frame", {
				Size = UDim2.new(1, 0, 0, 34),
				LayoutOrder = 3,
				BackgroundTransparency = 1,
				ZIndex = 2,
			}, {
				Settings = e(TextButton, {
					text = "Settings",
					style = "Bordered",
					transparency = self.props.transparency,
					layoutOrder = 1,
					onClick = self.props.onNavigateSettings,
				}, {
					Tip = e(Tooltip.Trigger, {
						text = "View and modify plugin settings",
					}),
				}),

				Disconnect = e(TextButton, {
					text = "Disconnect",
					style = "Solid",
					transparency = self.props.transparency,
					layoutOrder = 2,
					onClick = self.props.onDisconnect,
				}, {
					Tip = e(Tooltip.Trigger, {
						text = "Disconnect from the Rojo sync server",
					}),
				}),

				Layout = e("UIListLayout", {
					HorizontalAlignment = Enum.HorizontalAlignment.Right,
					FillDirection = Enum.FillDirection.Horizontal,
					SortOrder = Enum.SortOrder.LayoutOrder,
					Padding = UDim.new(0, 10),
				}),
			}),

			ChangesViewer = e(StudioPluginGui, {
				id = "Rojo_ChangesViewer",
				title = "View changes",
				active = self.state.renderChanges,
				isEphemeral = true,

				initDockState = Enum.InitialDockState.Float,
				overridePreviousState = true,
				floatingSize = Vector2.new(400, 500),
				minimumSize = Vector2.new(300, 300),

				zIndexBehavior = Enum.ZIndexBehavior.Sibling,

				onClose = function()
					self:setState({
						renderChanges = false,
					})
				end,
			}, {
				TooltipsProvider = e(Tooltip.Provider, nil, {
					Tooltips = e(Tooltip.Container, nil),
					Content = e("Frame", {
						Size = UDim2.fromScale(1, 1),
						BackgroundTransparency = 1,
					}, {
						Changes = e(ChangesViewer, {
							transparency = self.props.transparency,
							rendered = self.state.renderChanges,
							patchData = self.props.patchData,
							patchTree = self.props.patchTree,
							serveSession = self.props.serveSession,
							showStringDiff = function(currentString: string, incomingString: string)
								self:setState({
									showingStringDiff = true,
									currentString = currentString,
									incomingString = incomingString,
								})
							end,
							showTableDiff = function(oldTable: { [any]: any? }, newTable: { [any]: any? })
								self:setState({
									showingTableDiff = true,
									oldTable = oldTable,
									newTable = newTable,
								})
							end,
							onBack = function()
								self:setState({
									renderChanges = false,
								})
							end,
						}),
					}),
				}),
			}),

			StringDiff = e(StudioPluginGui, {
				id = "Rojo_ConnectedStringDiff",
				title = "String diff",
				active = self.state.showingStringDiff,
				isEphemeral = true,

				initDockState = Enum.InitialDockState.Float,
				overridePreviousState = false,
				floatingSize = Vector2.new(500, 350),
				minimumSize = Vector2.new(400, 250),

				zIndexBehavior = Enum.ZIndexBehavior.Sibling,

				onClose = function()
					self:setState({
						showingStringDiff = false,
					})
				end,
			}, {
				TooltipsProvider = e(Tooltip.Provider, nil, {
					Tooltips = e(Tooltip.Container, nil),
					Content = e("Frame", {
						Size = UDim2.fromScale(1, 1),
						BackgroundTransparency = 1,
					}, {
						e(StringDiffVisualizer, {
							size = UDim2.new(1, -10, 1, -10),
							position = UDim2.new(0, 5, 0, 5),
							anchorPoint = Vector2.new(0, 0),
							transparency = self.props.transparency,

							currentString = self.state.currentString,
							incomingString = self.state.incomingString,
						}),
					}),
				}),
			}),

			TableDiff = e(StudioPluginGui, {
				id = "Rojo_ConnectedTableDiff",
				title = "Table diff",
				active = self.state.showingTableDiff,
				isEphemeral = true,

				initDockState = Enum.InitialDockState.Float,
				overridePreviousState = false,
				floatingSize = Vector2.new(500, 350),
				minimumSize = Vector2.new(400, 250),

				zIndexBehavior = Enum.ZIndexBehavior.Sibling,

				onClose = function()
					self:setState({
						showingTableDiff = false,
					})
				end,
			}, {
				TooltipsProvider = e(Tooltip.Provider, nil, {
					Tooltips = e(Tooltip.Container, nil),
					Content = e("Frame", {
						Size = UDim2.fromScale(1, 1),
						BackgroundTransparency = 1,
					}, {
						e(TableDiffVisualizer, {
							size = UDim2.new(1, -10, 1, -10),
							position = UDim2.new(0, 5, 0, 5),
							anchorPoint = Vector2.new(0, 0),
							transparency = self.props.transparency,

							oldTable = self.state.oldTable,
							newTable = self.state.newTable,
						}),
					}),
				}),
			}),
		})
	end)
end

function ConnectedPage.getDerivedStateFromProps(props)
	-- If projectName or address ever get removed from props, make sure we still have
	-- the properties! The component still needs to have its data for it to be properly
	-- animated out without the labels changing.

	return {
		projectName = props.projectName,
		address = props.address,
	}
end

return ConnectedPage
