Code/InteractiveComputer/Apps/TaskManagerApp.cs
using System;
using System.Collections.Generic;
using System.Linq;
using PaneOS.InteractiveComputer.Core;
using Sandbox.UI;

namespace PaneOS.InteractiveComputer.Apps;

[ComputerApp( "system.taskmanager", "Task Manager", Icon = "TM", SortOrder = 20 )]
public sealed class TaskManagerApp : IComputerApp
{
	public ComputerAppSession Run( ComputerAppContext context )
	{
		return new ComputerAppSession
		{
			Title = "Task Manager",
			Icon = "TM",
			Content = new TaskManagerPanel( context )
		};
	}
}

[StyleSheet( "InteractiveComputerApps.scss" )]
public sealed class TaskManagerPanel : ComputerWarmupPanel
{
	private readonly ComputerAppContext context;
	private readonly Panel contentHost;
	private readonly Button processesButton;
	private readonly Button performanceButton;
	private readonly Button storageButton;
	private TaskManagerTab activeTab = TaskManagerTab.Processes;
	private TaskManagerProcessSortField sortField = TaskManagerProcessSortField.Process;
	private bool sortDescending;
	private int lastVersion = -1;

	public TaskManagerPanel( ComputerAppContext context )
	{
		this.context = context;
		AddClass( "task-manager-app" );

		var tabs = new Panel { Parent = this };
		tabs.AddClass( "task-tabs" );

		processesButton = CreateTabButton( tabs, "Processes", TaskManagerTab.Processes );
		performanceButton = CreateTabButton( tabs, "Performance", TaskManagerTab.Performance );
		storageButton = CreateTabButton( tabs, "Storage", TaskManagerTab.Storage );

		contentHost = new Panel { Parent = this };
		contentHost.AddClass( "task-tab-content" );

		RenderActiveTab( true );
	}

	public override void Tick()
	{
		base.Tick();

		var refreshVersion = GetRefreshVersion();
		if ( lastVersion == refreshVersion )
			return;

		RenderActiveTab( false );
	}

	protected override void WarmupRefresh()
	{
		RenderActiveTab( true );
	}

	private Button CreateTabButton( Panel parent, string label, TaskManagerTab tab )
	{
		var button = new Button( label ) { Parent = parent };
		button.AddClass( "task-tab-button" );
		button.AddEventListener( "onclick", () =>
		{
			activeTab = tab;
			RequestWarmupRefresh();
			RenderActiveTab( true );
		} );
		return button;
	}

	private void RenderActiveTab( bool force )
	{
		var refreshVersion = GetRefreshVersion();
		if ( !force && lastVersion == refreshVersion )
			return;

		lastVersion = refreshVersion;
		processesButton.SetClass( "active", activeTab == TaskManagerTab.Processes );
		performanceButton.SetClass( "active", activeTab == TaskManagerTab.Performance );
		storageButton.SetClass( "active", activeTab == TaskManagerTab.Storage );
		contentHost.DeleteChildren( true );

		switch ( activeTab )
		{
			case TaskManagerTab.Processes:
				RenderProcessesTab();
				break;
			case TaskManagerTab.Performance:
				RenderPerformanceTab();
				break;
			case TaskManagerTab.Storage:
				RenderStorageTab();
				break;
		}
	}

	private void RenderProcessesTab()
	{
		var table = new Panel { Parent = contentHost };
		table.AddClass( "task-table" );
		AddProcessHeader( table );

		var rows = context.Runtime.OpenApps
			.Select( app =>
			{
				var metrics = context.Runtime.GetProcessMetrics( app.State.InstanceId );
				return new
				{
					App = app,
					Metrics = metrics,
					Item = new TaskManagerProcessSortItem
					{
						InstanceId = app.State.InstanceId,
						ProcessName = app.State.Title,
						Status = FormatStatus( context.Runtime.GetEffectiveStatus( app.State.InstanceId ) ),
						CpuPercent = metrics.CpuPercent,
						RamPercent = metrics.RamPercent,
						StartupProcess = app.Descriptor.RunOnStartup
					}
				};
			} )
			.ToArray();

		var sortedRows = TaskManagerProcessSortPolicy.Sort( rows.Select( x => x.Item ), sortField, sortDescending )
			.Join(
				rows,
				sorted => sorted.InstanceId,
				row => row.Item.InstanceId,
				( _, row ) => row,
				StringComparer.OrdinalIgnoreCase );

		foreach ( var entry in sortedRows )
		{
			var app = entry.App;
			var metrics = entry.Metrics;
			var row = new Panel { Parent = table };
			row.AddClass( "task-table-row" );
			row.SetClass( "not-responding", context.Runtime.GetEffectiveStatus( app.State.InstanceId ) == ComputerProcessStatus.NotResponding );

			AddCell( row, app.State.Title );
			AddCell( row, FormatStatus( context.Runtime.GetEffectiveStatus( app.State.InstanceId ) ) );
			AddCell( row, $"{metrics.CpuPercent:0.0}%" );
			AddCell( row, $"{metrics.RamPercent:0.0}%" );

			var cell = new Panel { Parent = row };
			cell.AddClass( "task-cell task-action-cell" );
			var killButton = new Button( "Kill" ) { Parent = cell };
			killButton.AddClass( "task-kill-button" );
			var canKill = CanKillProcess( app );
			killButton.SetClass( "disabled", !canKill );
			if ( canKill )
			{
				killButton.AddEventListener( "onclick", () =>
				{
					context.Runtime.Close( app.State.InstanceId );
					RenderActiveTab( true );
				} );
			}
		}
	}

	private void RenderPerformanceTab()
	{
		var metrics = context.Runtime.Metrics;
		var grid = new Panel { Parent = contentHost };
		grid.AddClass( "perf-grid" );

		AddPerformanceCard( grid, "CPU Usage", $"{metrics.CpuPercent:0.0}%" );
		AddPerformanceCard( grid, "RAM Usage", $"{metrics.RamPercent:0.0}%" );
		AddPerformanceCard( grid, "GPU Core Usage", $"{metrics.GpuCorePercent:0.0}%" );
		AddPerformanceCard( grid, "GPU VRAM Usage", $"{metrics.GpuVramPercent:0.0}%" );
		var historyGrid = new Panel { Parent = contentHost };
		historyGrid.AddClass( "perf-grid" );
		AddSparkline( historyGrid, "CPU History", context.Runtime.MetricHistory.CpuSamples );
		AddSparkline( historyGrid, "RAM History", context.Runtime.MetricHistory.RamSamples );
		AddSparkline( historyGrid, "GPU History", context.Runtime.MetricHistory.GpuSamples );
		AddSparkline( historyGrid, "GPU VRAM History", context.Runtime.MetricHistory.GpuVramSamples );

		var note = new Label( $"Live hardware: {context.Runtime.State.Hardware.CpuCoreCount} cores @ {context.Runtime.State.Hardware.CpuCoreGhz:0.##} GHz, {context.Runtime.State.Hardware.RamGb:0.##} GB RAM" )
		{
			Parent = contentHost
		};
		note.AddClass( "task-storage-note" );
	}

	private void RenderStorageTab()
	{
		var metrics = context.Runtime.Metrics;
		var hardware = context.Runtime.State.Hardware;
		var breakdown = context.Runtime.GetStorageBreakdown();

		var driveCard = new Panel { Parent = contentHost };
		driveCard.AddClass( "storage-drive-card" );
		var driveHeader = new Panel { Parent = driveCard };
		driveHeader.AddClass( "storage-drive-header" );
		new Label( "C:" ) { Parent = driveHeader }.AddClass( "storage-drive-title" );
		new Label( "Primary Drive" ) { Parent = driveHeader }.AddClass( "storage-drive-subtitle" );
		var driveStats = new Panel { Parent = driveCard };
		driveStats.AddClass( "storage-drive-stats" );
		new Label( $"HDD: {hardware.HddStorageGb:0.##} GB total" ) { Parent = driveStats };
		new Label( $"Used: {metrics.UsedStorageGb:0.###} GB" ) { Parent = driveStats };
		new Label( $"Free: {metrics.UnusedStorageGb:0.###} GB" ) { Parent = driveStats };

		var table = new Panel { Parent = contentHost };
		table.AddClass( "task-table" );

		var header = new Panel { Parent = table };
		header.AddClass( "task-table-header" );
		AddHeaderCell( header, "Category" );
		AddHeaderCell( header, "Used (GB)" );

		foreach ( var item in breakdown.OrderByDescending( x => x.SizeGb ).ThenBy( x => x.Name, StringComparer.OrdinalIgnoreCase ) )
		{
			var row = new Panel { Parent = table };
			row.AddClass( "task-table-row" );
			AddCell( row, item.Name );
			AddCell( row, item.SizeGb.ToString( "0.###" ) );
		}

		var unusedRow = new Panel { Parent = table };
		unusedRow.AddClass( "task-table-row" );
		AddCell( unusedRow, "Unused" );
		AddCell( unusedRow, metrics.UnusedStorageGb.ToString( "0.###" ) );

	}

	private void AddProcessHeader( Panel table )
	{
		var header = new Panel { Parent = table };
		header.AddClass( "task-table-header" );
		AddSortableHeaderCell( header, "Process", TaskManagerProcessSortField.Process );
		AddSortableHeaderCell( header, "Status", TaskManagerProcessSortField.Status );
		AddSortableHeaderCell( header, "CPU % Used", TaskManagerProcessSortField.Cpu );
		AddSortableHeaderCell( header, "RAM % Used", TaskManagerProcessSortField.Ram );
		AddHeaderCell( header, "Kill" );
	}

	private static void AddPerformanceCard( Panel parent, string title, string value )
	{
		var card = new Panel { Parent = parent };
		card.AddClass( "perf-card" );
		new Label( title ) { Parent = card }.AddClass( "perf-title" );
		new Label( value ) { Parent = card }.AddClass( "perf-value" );
	}

	private static void AddHeaderCell( Panel row, string text )
	{
		new Label( text ) { Parent = row }.AddClass( "task-cell task-header-cell" );
	}

	private void AddSortableHeaderCell( Panel row, string text, TaskManagerProcessSortField field )
	{
		var button = new Button( text ) { Parent = row };
		button.AddClass( "task-cell task-header-cell task-sort-header" );
		button.AddEventListener( "onclick", () =>
		{
			if ( sortField == field )
				sortDescending = !sortDescending;
			else
			{
				sortField = field;
				sortDescending = field is TaskManagerProcessSortField.Cpu or TaskManagerProcessSortField.Ram;
			}

			RenderActiveTab( true );
		} );
	}

	private static void AddCell( Panel row, string text )
	{
		new Label( text ) { Parent = row }.AddClass( "task-cell" );
	}

	private static void AddSparkline( Panel parent, string title, IReadOnlyList<float> samples )
	{
		var container = new Panel { Parent = parent };
		container.AddClass( "task-history-card" );
		new Label( title ) { Parent = container }.AddClass( "task-history-title" );
		var barRow = new Panel { Parent = container };
		barRow.AddClass( "task-history-bars" );

		foreach ( var sample in samples.DefaultIfEmpty( 0f ) )
		{
			var bar = new Panel { Parent = barRow };
			bar.AddClass( "task-history-bar" );
			bar.Style.Height = Length.Pixels( MathF.Max( 6f, sample * 0.48f ) );
		}
	}

	private static string FormatStatus( ComputerProcessStatus status )
	{
		return status switch
		{
			ComputerProcessStatus.NotResponding => "Not Responding",
			ComputerProcessStatus.Suspended => "Suspended",
			_ => "Running"
		};
	}

	private static bool CanKillProcess( ComputerRunningApp app )
	{
		return !app.Descriptor.ResolvedExecutableName.Equals( "PaneOS32.exe", StringComparison.OrdinalIgnoreCase ) &&
			!app.Descriptor.ResolvedExecutableName.Equals( "PvcHost.exe", StringComparison.OrdinalIgnoreCase );
	}

	private int GetRefreshVersion()
	{
		return TaskManagerRefreshPolicy.GetRefreshVersion(
			activeTab,
			context.Runtime.Version,
			context.Runtime.MetricsVersion,
			context.Runtime.StorageVersion );
	}
}