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

local Roact = require(Packages.Roact)
local Flipper = require(Packages.Flipper)

local bindingUtil = require(Plugin.App.bindingUtil)
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 e = Roact.createElement

local AGE_UNITS = {
	{ 31556909, "year" },
	{ 2629743, "month" },
	{ 604800, "week" },
	{ 86400, "day" },
	{ 3600, "hour" },
	{
		60,
		"minute",
	},
}
function timeSinceText(elapsed: number): string
	if elapsed < 3 then
		return "just now"
	end

	local ageText = string.format("%d seconds ago", elapsed)

	for _, UnitData in ipairs(AGE_UNITS) do
		local UnitSeconds, UnitName = UnitData[1], UnitData[2]
		if elapsed > UnitSeconds then
			local c = math.floor(elapsed / UnitSeconds)
			ageText = string.format("%d %s%s ago", c, UnitName, c > 1 and "s" or "")
			break
		end
	end

	return ageText
end

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

function ChangesDrawer: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 ChangesDrawer:render()
	if self.props.rendered == false or self.serveSession == nil then
		return nil
	end

	return Theme.with(function(theme)
		return e(BorderedContainer, {
			transparency = self.props.transparency,
			size = self.props.height:map(function(y)
				return UDim2.new(1, 0, y, -220 * y)
			end),
			position = UDim2.new(0, 0, 1, 0),
			anchorPoint = Vector2.new(0, 1),
			layoutOrder = self.props.layoutOrder,
		}, {
			Close = e(IconButton, {
				icon = Assets.Images.Icons.Close,
				iconSize = 24,
				color = theme.ConnectionDetails.DisconnectColor,
				transparency = self.props.transparency,

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

				onClick = self.props.onClose,
			}, {
				Tip = e(Tooltip.Trigger, {
					text = "Close the patch visualizer",
				}),
			}),

			PatchVisualizer = e(PatchVisualizer, {
				size = UDim2.new(1, 0, 1, 0),
				transparency = self.props.transparency,
				layoutOrder = 3,

				patchTree = self.props.patchTree,

				showSourceDiff = self.props.showSourceDiff,
			}),
		})
	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,
					Font = Enum.Font.GothamBold,
					TextSize = 20,
					TextColor3 = theme.ConnectionDetails.ProjectNameColor,
					TextTransparency = props.transparency,
					TextXAlignment = Enum.TextXAlignment.Left,

					Size = UDim2.new(1, 0, 0, 20),

					LayoutOrder = 1,
					BackgroundTransparency = 1,
				}),

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

					Size = UDim2.new(1, 0, 0, 15),

					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

	local elapsed = os.time() - patchData.timestamp
	local unapplied = PatchSet.countChanges(patchData.unapplied)

	return "<i>Synced "
		.. timeSinceText(elapsed)
		.. (if unapplied > 0
			then string.format(
				', <font color="#FF8E3C">but %d change%s failed to apply</font>',
				unapplied,
				unapplied == 1 and "" or "s"
			)
			else "")
		.. "</i>"
end

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

	-- Start a new updater
	self.changeInfoTextUpdater = task.defer(function()
		while true do
			if self.state.hoveringChangeInfo then
				self.setChangeInfoText("<u>" .. self:getChangeInfoText() .. "</u>")
			else
				self.setChangeInfoText(self:getChangeInfoText())
			end

			local elapsed = os.time() - self.props.patchData.timestamp
			local updateInterval = 1

			-- Update timestamp text as frequently as currently needed
			for _, UnitData in ipairs(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.changeDrawerMotor = Flipper.SingleMotor.new(0)
	self.changeDrawerHeight = bindingUtil.fromMotor(self.changeDrawerMotor)

	self.changeDrawerMotor:onStep(function(value)
		local renderChanges = value > 0.05

		self:setState(function(state)
			if state.renderChanges == renderChanges then
				return nil
			end

			return {
				renderChanges = renderChanges,
			}
		end)
	end)

	self:setState({
		renderChanges = false,
		hoveringChangeInfo = false,
		showingSourceDiff = false,
		oldSource = "",
		newSource = "",
	})

	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({
			showingSourceDiff = false,
		})
	end
end

function ConnectedPage:render()
	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),
			}),

			Header = e(Header, {
				transparency = self.props.transparency,
				layoutOrder = 1,
			}),

			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),
				}),
			}),

			ChangeInfo = e("TextButton", {
				Text = self.changeInfoText,
				Font = Enum.Font.Gotham,
				TextSize = 14,
				TextWrapped = true,
				RichText = true,
				TextColor3 = theme.Header.VersionColor,
				TextXAlignment = Enum.TextXAlignment.Left,
				TextYAlignment = Enum.TextYAlignment.Top,
				TextTransparency = self.props.transparency,

				Size = UDim2.new(1, 0, 0, 28),

				LayoutOrder = 4,
				BackgroundTransparency = 1,

				[Roact.Event.MouseEnter] = function()
					self:setState({
						hoveringChangeInfo = true,
					})
					self.setChangeInfoText("<u>" .. self:getChangeInfoText() .. "</u>")
				end,

				[Roact.Event.MouseLeave] = function()
					self:setState({
						hoveringChangeInfo = false,
					})
					self.setChangeInfoText(self:getChangeInfoText())
				end,

				[Roact.Event.Activated] = function()
					if self.state.renderChanges then
						self.changeDrawerMotor:setGoal(Flipper.Spring.new(0, {
							frequency = 4,
							dampingRatio = 1,
						}))
					else
						self.changeDrawerMotor:setGoal(Flipper.Spring.new(1, {
							frequency = 3,
							dampingRatio = 1,
						}))
					end
				end,
			}, {
				Tooltip = e(Tooltip.Trigger, {
					text = if self.state.renderChanges then "Hide the changes" else "View the changes",
				}),
			}),

			ChangesDrawer = e(ChangesDrawer, {
				rendered = self.state.renderChanges,
				transparency = self.props.transparency,
				patchTree = self.props.patchTree,
				serveSession = self.props.serveSession,
				height = self.changeDrawerHeight,
				layoutOrder = 5,

				showSourceDiff = function(oldSource: string, newSource: string)
					self:setState({
						showingSourceDiff = true,
						oldSource = oldSource,
						newSource = newSource,
					})
				end,

				onClose = function()
					self.changeDrawerMotor:setGoal(Flipper.Spring.new(0, {
						frequency = 4,
						dampingRatio = 1,
					}))
				end,
			}),

			SourceDiff = e(StudioPluginGui, {
				id = "Rojo_ConnectedSourceDiff",
				title = "Source diff",
				active = self.state.showingSourceDiff,
				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({
						showingSourceDiff = 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,

							oldText = self.state.oldSource,
							newText = self.state.newSource,
						}),
					}),
				}),
			}),
		})
	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
