Code/InteractiveComputer/ComputerRuntime.cs
using System;
using System.Collections.Generic;
using System.Linq;
using PaneOS.InteractiveComputer.Core;

namespace PaneOS.InteractiveComputer;

public sealed class ComputerRuntime
{
	private sealed class ProcessSimulationState
	{
		public float LeakMultiplier { get; set; } = 1f;
		public bool LeakTriggered { get; set; }
		public ComputerProcessMetrics Metrics { get; } = new();
	}

	private readonly Dictionary<string, ComputerRunningApp> runningApps = new();
	private readonly Dictionary<string, ProcessSimulationState> simulationByInstance = new();
	private readonly InteractiveComputerComponent computer;
	private readonly Queue<float> cpuHistory = new();
	private readonly Queue<float> ramHistory = new();
	private readonly Queue<float> gpuHistory = new();
	private readonly Queue<float> gpuVramHistory = new();
	private readonly List<ComputerNotification> notifications = new();
	private static readonly string[] RestartLogTemplates =
	{
		"[init] mounting system32 package registry",
		"[init] probing virtual storage controller",
		"[kern] syncing desktop shell state",
		"[kern] loading process scheduler tables",
		"[svc] starting networking.exe",
		"[svc] starting pvchost.exe",
		"[svc] starting paneos32.exe",
		"[ui ] loading explorer shell resources",
		"[drv] warming gpu compositor",
		"[sec] validating user profile archive",
		"[fs ] replaying recycle bin journal",
		"[net] binding local loopback bridge"
	};
	private readonly Random random = new();
	private int nextZIndex = 10;
	private float systemTickAccumulator;
	private float inputDelayRemaining;
	private float restartLogAccumulator;
	private bool lowMemoryNotificationActive;
	private ComputerActiveMessageBox? activeMessageBox;
	private ComputerActiveFileDialog? activeFileDialog;

	public ComputerRuntime( InteractiveComputerComponent computer, ComputerState state )
	{
		this.computer = computer;
		State = state;
		EnsureRequiredBackgroundAppsInstalled();
		Apps = ResolveInstalledApps();
		RehydrateOpenApps();
		EnsureStartupProcessesRunning();
		RefreshStorageMetrics();
	}

	public ComputerState State { get; }
	public IReadOnlyList<ComputerAppDescriptor> Apps { get; private set; }
	public int Version { get; private set; }
	public int DesktopVersion { get; private set; }
	public int MetricsVersion { get; private set; }
	public int StorageVersion { get; private set; }
	public ComputerSystemMetrics Metrics { get; } = new();
	public ComputerActiveMessageBox? ActiveMessageBox => activeMessageBox;
	public ComputerActiveFileDialog? ActiveFileDialog => activeFileDialog;
	public bool IsInputDelayed => inputDelayRemaining > 0f;
	public IReadOnlyList<ComputerNotification> Notifications => notifications.ToArray();
	public ComputerMetricHistory MetricHistory => new()
	{
		CpuSamples = cpuHistory.ToArray(),
		RamSamples = ramHistory.ToArray(),
		GpuSamples = gpuHistory.ToArray(),
		GpuVramSamples = gpuVramHistory.ToArray()
	};

	public IReadOnlyList<ComputerRunningApp> OpenApps => runningApps.Values
		.OrderBy( x => x.State.ZIndex )
		.ThenBy( x => x.State.Title, StringComparer.OrdinalIgnoreCase )
		.ToArray();

	public ComputerRunningApp? FocusedApp => string.IsNullOrWhiteSpace( State.FocusedInstanceId )
		? null
		: runningApps.GetValueOrDefault( State.FocusedInstanceId );

	public event Action? Changed;

	public bool IsScreenSaverActive => State.ScreenSaver.Enabled && State.ScreenSaver.IsActive;
	public bool IsRestarting => State.RestartLogSecondsRemaining > 0f || State.BootSplashSecondsRemaining > 0f;

	public void TickScreenSaver( float deltaSeconds, bool isPlayerInteracting )
	{
		var wasActive = State.ScreenSaver.IsActive;
		var changed = ScreenSaverSimulator.Tick( State, deltaSeconds, isPlayerInteracting );
		if ( !changed )
			return;

		if ( isPlayerInteracting || (!wasActive && State.ScreenSaver.IsActive) )
		{
			MarkChanged();
			return;
		}

		MarkDesktopChanged();
	}

	public void TickSystem( float deltaSeconds )
	{
		var desktopShouldRefresh = false;
		if ( inputDelayRemaining > 0f )
		{
			var wasDelayed = IsInputDelayed;
			inputDelayRemaining = MathF.Max( 0f, inputDelayRemaining - MathF.Max( 0f, deltaSeconds ) );
			if ( wasDelayed != IsInputDelayed )
				desktopShouldRefresh = true;
		}

		if ( State.RestartLogSecondsRemaining > 0f )
		{
			State.RestartLogSecondsRemaining = MathF.Max( 0f, State.RestartLogSecondsRemaining - MathF.Max( 0f, deltaSeconds ) );
			TickRestartLogs( deltaSeconds );
			desktopShouldRefresh = true;

			if ( State.RestartLogSecondsRemaining <= 0f )
			{
				State.RestartLogLines.Clear();
				State.BootSplashSecondsRemaining = 1.5f;
				restartLogAccumulator = 0f;
			}
		}

		if ( State.BootSplashSecondsRemaining > 0f )
		{
			State.BootSplashSecondsRemaining = MathF.Max( 0f, State.BootSplashSecondsRemaining - MathF.Max( 0f, deltaSeconds ) );
			desktopShouldRefresh = true;
		}

		var notificationsChanged = TickNotifications( deltaSeconds );
		desktopShouldRefresh |= notificationsChanged;

		systemTickAccumulator += MathF.Max( 0f, deltaSeconds );
		if ( systemTickAccumulator < 0.35f )
		{
			if ( desktopShouldRefresh )
				MarkDesktopChanged();

			return;
		}

		var sampledSeconds = systemTickAccumulator;
		systemTickAccumulator = 0f;
		var stateChanged = UpdateResourceSimulation( sampledSeconds );
		MetricsVersion++;
		if ( stateChanged )
		{
			MarkChanged();
			return;
		}

		if ( desktopShouldRefresh )
			MarkDesktopChanged();
		else
			Changed?.Invoke();
	}

	public void NotifyUserActivity()
	{
		if ( ScreenSaverSimulator.NotifyUserActivity( State ) )
			MarkChanged();
	}

	public void ResetScreenSaverIdle()
	{
		NotifyUserActivity();
	}

	public void DisableScreenSaverWhileInteracting()
	{
		NotifyUserActivity();
	}

	public void InstallApp( string appId )
	{
		if ( State.InstalledApps.Any( x => x.AppId == appId ) )
			return;

		State.InstalledApps.Add( new ComputerInstalledAppState { AppId = appId } );
		Apps = ResolveInstalledApps();
		EnsureStartupProcessesRunning();
		RefreshStorageMetrics();
		PushNotification( "Installed", $"{Apps.FirstOrDefault( x => x.Id == appId )?.Title ?? appId} is now available.", "+" );
		MarkChanged();
	}

	public void UninstallApp( string appId, bool closeRunningInstances = true )
	{
		if ( closeRunningInstances )
		{
			foreach ( var app in runningApps.Values.Where( x => x.State.AppId == appId ).ToArray() )
			{
				Close( app.State.InstanceId );
			}
		}

		State.InstalledApps.RemoveAll( x => x.AppId == appId );
		State.OpenApps.RemoveAll( x => x.AppId == appId );
		Apps = ResolveInstalledApps();
		RefreshStorageMetrics();
		PushNotification( "Removed", $"{appId} was uninstalled.", "-" );
		MarkChanged();
	}

	public ComputerRunningApp? OpenApp( string appId )
	{
		NotifyUserActivity();
		State.IsSleeping = false;
		State.StartMenuOpen = false;
		return OpenAppInternal( appId, true, true, null );
	}

	public ComputerRunningApp? OpenApp( string appId, IReadOnlyDictionary<string, string> initialData )
	{
		NotifyUserActivity();
		State.IsSleeping = false;
		State.StartMenuOpen = false;
		return OpenAppInternal( appId, true, true, initialData );
	}

	public void Focus( string instanceId )
	{
		NotifyUserActivity();
		if ( !runningApps.TryGetValue( instanceId, out var app ) || !app.Descriptor.HasWindow )
			return;

		if ( app.State.IsMinimized && !app.Session.Content.IsValid )
			RefreshRunningAppSession( app );

		app.State.IsMinimized = false;
		app.State.ZIndex = ++nextZIndex;
		State.FocusedInstanceId = instanceId;
		app.Session.OnFocused?.Invoke();
		MarkChanged();
	}

	public void Minimize( string instanceId )
	{
		NotifyUserActivity();
		if ( !runningApps.TryGetValue( instanceId, out var app ) || !app.Descriptor.HasWindow )
			return;

		app.State.IsMinimized = true;
		app.Session.OnMinimized?.Invoke();

		if ( State.FocusedInstanceId == instanceId )
			State.FocusedInstanceId = OpenApps.LastOrDefault( x => x.Descriptor.HasWindow && !x.State.IsMinimized && x.State.InstanceId != instanceId )?.State.InstanceId;

		MarkChanged();
	}

	public void ToggleTaskbarApp( string instanceId )
	{
		if ( !runningApps.TryGetValue( instanceId, out var app ) || !app.Descriptor.HasWindow )
			return;

		if ( State.FocusedInstanceId == instanceId && !app.State.IsMinimized )
			Minimize( instanceId );
		else
			Focus( instanceId );
	}

	public void Close( string instanceId )
	{
		NotifyUserActivity();
		if ( !runningApps.TryGetValue( instanceId, out var app ) )
			return;

		app.Session.OnClosed?.Invoke();
		app.Session.Content.Delete();
		runningApps.Remove( instanceId );
		simulationByInstance.Remove( instanceId );
		State.OpenApps.RemoveAll( x => x.InstanceId == instanceId );

		if ( State.FocusedInstanceId == instanceId )
			State.FocusedInstanceId = OpenApps.LastOrDefault( x => x.Descriptor.HasWindow && !x.State.IsMinimized )?.State.InstanceId;

		RefreshStorageMetrics();
		UpdateResourceSimulation( 0.35f );
		MarkChanged();
	}

	public void MoveWindow( string instanceId, int x, int y )
	{
		if ( !runningApps.TryGetValue( instanceId, out var app ) || !app.Descriptor.HasWindow )
			return;

		app.State.X = Math.Clamp( x, 0, Math.Max( 0, State.ResolutionX - 80 ) );
		app.State.Y = Math.Clamp( y, 0, Math.Max( 0, State.ResolutionY - 80 ) );
		MarkChanged();
	}

	public void ResizeWindow( string instanceId, int width, int height )
	{
		if ( !runningApps.TryGetValue( instanceId, out var app ) || !app.Descriptor.HasWindow )
			return;

		app.State.Width = Math.Clamp( width, 260, State.ResolutionX );
		app.State.Height = Math.Clamp( height, 180, State.ResolutionY );
		MarkChanged();
	}

	public void ToggleStartMenu()
	{
		NotifyUserActivity();
		State.IsSleeping = false;
		State.StartMenuOpen = !State.StartMenuOpen;
		MarkChanged();
	}

	public void Sleep()
	{
		State.StartMenuOpen = false;
		State.IsSleeping = true;
		MarkChanged();
	}

	public void Wake()
	{
		NotifyUserActivity();
		if ( !State.IsSleeping )
			return;

		State.IsSleeping = false;
		RefreshWindowAppSessions();
		MarkChanged();
	}

	public void Lock()
	{
		State.StartMenuOpen = false;
		State.IsLocked = true;
		MarkChanged();
	}

	public void Unlock()
	{
		if ( !State.IsLocked )
			return;

		NotifyUserActivity();
		State.IsLocked = false;
		RefreshWindowAppSessions();
		MarkChanged();
	}

	public void RefreshWindowAppSessions()
	{
		foreach ( var app in OpenApps.Where( x => x.Descriptor.HasWindow ).ToArray() )
		{
			if ( app.State.AppId.Equals( "system.ridge", StringComparison.OrdinalIgnoreCase ) && app.Session.Content.IsValid )
				continue;

			RefreshRunningAppSession( app );
		}
	}

	public void Restart()
	{
		NotifyUserActivity();
		foreach ( var app in runningApps.Values )
		{
			app.Session.OnClosed?.Invoke();
			app.Session.Content.Delete();
		}

		runningApps.Clear();
		simulationByInstance.Clear();
		State.OpenApps.Clear();
		State.FocusedInstanceId = null;
		State.StartMenuOpen = false;
		State.IsSleeping = false;
		State.IsLocked = false;
		State.RestartLogSecondsRemaining = 5f;
		State.BootSplashSecondsRemaining = 0f;
		State.RestartLogLines.Clear();
		activeMessageBox = null;
		activeFileDialog = null;
		inputDelayRemaining = 0f;
		systemTickAccumulator = 0f;
		restartLogAccumulator = 0f;
		cpuHistory.Clear();
		ramHistory.Clear();
		gpuHistory.Clear();
		gpuVramHistory.Clear();
		notifications.Clear();
		lowMemoryNotificationActive = false;
		ComputerAppRegistry.Refresh();
		EnsureRequiredBackgroundAppsInstalled();
		Apps = ResolveInstalledApps();
		EnsureStartupProcessesRunning();
		RefreshStorageMetrics();
		MarkChanged();
	}

	public void SetStatus( string instanceId, ComputerProcessStatus status )
	{
		if ( !runningApps.TryGetValue( instanceId, out var app ) )
			return;

		if ( app.State.Status == status )
			return;

		app.State.Status = status;
		MarkChanged();
	}

	public ComputerProcessStatus GetEffectiveStatus( string instanceId )
	{
		return runningApps.TryGetValue( instanceId, out var app )
			? GetEffectiveStatus( app )
			: ComputerProcessStatus.Running;
	}

	public ComputerProcessMetrics GetProcessMetrics( string instanceId )
	{
		if ( !simulationByInstance.TryGetValue( instanceId, out var simulation ) )
			return new ComputerProcessMetrics();

		return new ComputerProcessMetrics
		{
			CpuPercent = simulation.Metrics.CpuPercent,
			RamMb = simulation.Metrics.RamMb,
			RamPercent = simulation.Metrics.RamPercent,
			GpuCorePercent = simulation.Metrics.GpuCorePercent,
			GpuVramPercent = simulation.Metrics.GpuVramPercent,
			StorageGb = simulation.Metrics.StorageGb
		};
	}

	public IReadOnlyList<ComputerStorageBreakdownItem> GetStorageBreakdown()
	{
		EnsureArchiveExists();
		var items = PaneArchiveFileSystem.BuildStorageBreakdown( GetArchivePath(), Apps );
		Metrics.UsedStorageGb = items.Sum( x => x.SizeGb );
		Metrics.UnusedStorageGb = MathF.Max( 0f, State.Hardware.HddStorageGb - Metrics.UsedStorageGb );
		return items;
	}

	public string GetDefaultDocumentsPath()
	{
		return $"/C:/Users/{ResolvePlayerFolderName()}/My Documents";
	}

	public string GetArchivePath()
	{
		return computer.ResolveArchivePath();
	}

	public string? LoadAppSetting( string appId, string key )
	{
		var installedApp = State.InstalledApps.FirstOrDefault( x => x.AppId == appId );
		return installedApp is not null && installedApp.Settings.TryGetValue( key, out var value ) ? value : null;
	}

	public void SaveAppSetting( string appId, string key, string value )
	{
		var installedApp = GetOrCreateInstalledAppState( appId );
		installedApp.Settings[key] = value;
		MarkChanged();
	}

	public bool IsAppInstalled( string appId )
	{
		return State.InstalledApps.Any( x => x.AppId == appId );
	}

	public bool OpenVirtualPath( string virtualPath )
	{
		var path = ParseVirtualPath( virtualPath );
		if ( path.Count == 0 )
			return false;

		EnsureArchiveExists();
		if ( PaneArchiveFileSystem.IsDirectory( GetArchivePath(), path ) )
			return false;

		var fileName = path.LastOrDefault() ?? "";
		var fileContent = PaneArchiveFileSystem.ReadTextFile( GetArchivePath(), path );
		var openResult = ComputerFileAssociationPolicy.ResolveOpenResult( virtualPath, fileName, fileContent, Apps );
		if ( !openResult.CanOpen || openResult.LaunchTarget is null )
		{
			ShowMessageBox( new ComputerMessageBoxOptions
			{
				Title = openResult.FailureTitle,
				Message = string.IsNullOrWhiteSpace( openResult.FailureMessage )
					? $"PaneOS does not know how to open {fileName}."
					: openResult.FailureMessage,
				Icon = "!",
				Buttons = new[] { "OK" }
			} );
			return false;
		}

		OpenApp( openResult.LaunchTarget.AppId, openResult.LaunchTarget.InitialData );
		return true;
	}

	public string DeleteVirtualPath( string virtualPath )
	{
		var path = ParseVirtualPath( virtualPath );
		if ( path.Count == 0 )
			return "";

		EnsureArchiveExists();
		if ( IsRecycleBinPath( path ) )
		{
			PaneArchiveFileSystem.Delete( GetArchivePath(), path );
			PushNotification( "Deleted", $"{path.Last()} was removed permanently.", "X" );
			RefreshTransientUi();
			return "";
		}

		var recycledPath = PaneArchiveFileSystem.MoveToRecycleBin( GetArchivePath(), path );
		PushNotification( "Moved To Recycle Bin", $"{path.Last()} can be restored later.", "RB" );
		RefreshTransientUi();
		return recycledPath;
	}

	public string RestoreVirtualPath( string virtualPath )
	{
		var path = ParseVirtualPath( virtualPath );
		if ( path.Count == 0 || !IsRecycleBinPath( path ) )
			return "";

		EnsureArchiveExists();
		var restoredPath = PaneArchiveFileSystem.RestoreFromRecycleBin( GetArchivePath(), path );
		if ( !string.IsNullOrWhiteSpace( restoredPath ) )
		{
			PushNotification( "Restored", $"{path.Last()} returned from the Recycle Bin.", "RB" );
			RefreshTransientUi();
		}

		return restoredPath;
	}

	public string RunSystemUpdateScan()
	{
		EnsureArchiveExists();
		var record = ComputerMaintenancePolicy.BuildUpdateScanRecord( State, Apps, DateTime.UtcNow );
		var reportPath = WriteMaintenanceRecord( record );
		PushNotification( record.NotificationTitle, record.NotificationMessage, "UP" );
		ShowMessageBox(
			new ComputerMessageBoxOptions
			{
				Title = record.Title,
				Message = record.Summary,
				Icon = "UP",
				Buttons = new[] { "Open Report", "Close" }
			},
			result =>
			{
				if ( result.ButtonPressed.Equals( "Open Report", StringComparison.OrdinalIgnoreCase ) )
					OpenVirtualPath( reportPath );
			} );
		return reportPath;
	}

	public string RunPackageInstall( string packageName )
	{
		EnsureArchiveExists();
		var record = ComputerMaintenancePolicy.BuildPackageInstallRecord( packageName, DateTime.UtcNow );
		var logPath = WriteMaintenanceRecord( record );
		PushNotification( record.NotificationTitle, record.NotificationMessage, "+" );
		ShowMessageBox(
			new ComputerMessageBoxOptions
			{
				Title = record.Title,
				Message = record.Summary,
				Icon = "+",
				Buttons = new[] { "Open Log", "Close" }
			},
			result =>
			{
				if ( result.ButtonPressed.Equals( "Open Log", StringComparison.OrdinalIgnoreCase ) )
					OpenVirtualPath( logPath );
			} );
		return logPath;
	}

	public bool ShouldBlockInput( string instanceId )
	{
		if ( activeMessageBox is not null )
			return true;

		if ( activeFileDialog is not null )
			return true;

		return ComputerInputDelayPolicy.ShouldSuppressFocusedAppInput(
			State.Hardware.SimulateCpuInputDelayWhenMaxed,
			IsInputDelayed,
			State.FocusedInstanceId == instanceId );
	}

	public ComputerActiveMessageBox ShowMessageBox( ComputerMessageBoxOptions options, Action<ComputerMessageBoxResult>? onClosed = null )
	{
		activeFileDialog = null;
		activeMessageBox = new ComputerActiveMessageBox
		{
			Options = options,
			CurrentText = options.TextInputValue,
			OnClosed = onClosed
		};
		MarkDesktopChanged();
		return activeMessageBox;
	}

	public ComputerActiveFileDialog ShowFileDialog( ComputerFileDialogOptions options, Action<ComputerFileDialogResult>? onClosed = null )
	{
		activeMessageBox = null;
		activeFileDialog = new ComputerActiveFileDialog
		{
			Options = options,
			CurrentPathSegments = ParseVirtualPath( string.IsNullOrWhiteSpace( options.InitialPath ) ? GetDefaultDocumentsPath() : options.InitialPath ).ToList(),
			CurrentFileName = options.DefaultFileName,
			OnClosed = onClosed
		};
		RefreshActiveFileDialogItems();
		MarkDesktopChanged();
		return activeFileDialog;
	}

	public void NavigateFileDialogTo( string virtualPath )
	{
		if ( activeFileDialog is null )
			return;

		activeFileDialog.CurrentPathSegments = ParseVirtualPath( virtualPath ).ToList();
		activeFileDialog.SelectedVirtualPath = "";
		RefreshActiveFileDialogItems();
		MarkDesktopChanged();
	}

	public void MoveFileDialogUp()
	{
		if ( activeFileDialog is null || activeFileDialog.CurrentPathSegments.Count == 0 )
			return;

		activeFileDialog.CurrentPathSegments = activeFileDialog.CurrentPathSegments.Take( activeFileDialog.CurrentPathSegments.Count - 1 ).ToList();
		activeFileDialog.SelectedVirtualPath = "";
		RefreshActiveFileDialogItems();
		MarkDesktopChanged();
	}

	public void SelectFileDialogItem( string virtualPath )
	{
		if ( activeFileDialog is null )
			return;

		activeFileDialog.SelectedVirtualPath = virtualPath;
		var fileName = ParseVirtualPath( virtualPath ).LastOrDefault() ?? "";
		if ( activeFileDialog.Options.Mode == ComputerFileDialogMode.Save )
			activeFileDialog.CurrentFileName = fileName;

		MarkDesktopChanged();
	}

	public void UpdateFileDialogFileName( string fileName )
	{
		if ( activeFileDialog is null )
			return;

		activeFileDialog.CurrentFileName = fileName ?? "";
		MarkDesktopChanged();
	}

	public void ActivateFileDialogItem( string virtualPath )
	{
		if ( activeFileDialog is null )
			return;

		var path = ParseVirtualPath( virtualPath );
		if ( PaneArchiveFileSystem.IsDirectory( GetArchivePath(), path ) )
		{
			NavigateFileDialogTo( virtualPath );
			return;
		}

		SelectFileDialogItem( virtualPath );
		if ( activeFileDialog.Options.Mode == ComputerFileDialogMode.Open )
			ConfirmFileDialog();
	}

	public void ConfirmFileDialog()
	{
		if ( activeFileDialog is null )
			return;

		var dialog = activeFileDialog;
		activeFileDialog = null;
		var resolvedPath = ResolveFileDialogPath( dialog );
		dialog.OnClosed?.Invoke( new ComputerFileDialogResult
		{
			Confirmed = !string.IsNullOrWhiteSpace( resolvedPath ),
			VirtualPath = resolvedPath,
			FileName = ParseVirtualPath( resolvedPath ).LastOrDefault() ?? ""
		} );
		MarkDesktopChanged();
	}

	public void CancelFileDialog()
	{
		if ( activeFileDialog is null )
			return;

		var dialog = activeFileDialog;
		activeFileDialog = null;
		dialog.OnClosed?.Invoke( new ComputerFileDialogResult() );
		MarkDesktopChanged();
	}

	public void UpdateMessageBoxText( string value )
	{
		if ( activeMessageBox is null )
			return;

		activeMessageBox.CurrentText = value;
		MarkDesktopChanged();
	}

	public void CloseMessageBox( string button )
	{
		if ( activeMessageBox is null )
			return;

		var messageBox = activeMessageBox;
		activeMessageBox = null;
		messageBox.OnClosed?.Invoke( new ComputerMessageBoxResult
		{
			ButtonPressed = button,
			TextValue = messageBox.CurrentText
		} );
		MarkDesktopChanged();
	}

	public void RefreshTransientUi()
	{
		RefreshStorageMetrics();
		StorageVersion++;
		Changed?.Invoke();
	}

	public void PushNotification( string title, string message, string icon = "i", float lifetimeSeconds = 4f )
	{
		notifications.Add( new ComputerNotification
		{
			Title = title,
			Message = message,
			Icon = icon,
			RemainingSeconds = MathF.Max( 1f, lifetimeSeconds )
		} );
		MarkDesktopChanged();
	}

	public void MarkChanged()
	{
		Version++;
		DesktopVersion++;
		StorageVersion++;
		Changed?.Invoke();
		computer.StoreState();
	}

	private void MarkDesktopChanged()
	{
		DesktopVersion++;
		Changed?.Invoke();
	}

	private void RehydrateOpenApps()
	{
		var duplicateSingleInstances = new HashSet<string>( StringComparer.OrdinalIgnoreCase );

		foreach ( var appState in State.OpenApps.ToArray() )
		{
			var descriptor = Apps.FirstOrDefault( x => x.Id == appState.AppId );
			if ( descriptor is null )
			{
				State.OpenApps.Remove( appState );
				continue;
			}

			if ( descriptor.SingleInstance && !duplicateSingleInstances.Add( descriptor.Id ) )
			{
				State.OpenApps.Remove( appState );
				continue;
			}

			var running = CreateRunningApp( descriptor, appState );
			runningApps[appState.InstanceId] = running;
			simulationByInstance[appState.InstanceId] = new ProcessSimulationState();
			nextZIndex = Math.Max( nextZIndex, appState.ZIndex );
		}
	}

	private ComputerRunningApp? OpenAppInternal( string appId, bool focusWindow, bool markChanged, IReadOnlyDictionary<string, string>? initialData )
	{
		var descriptor = Apps.FirstOrDefault( x => x.Id == appId );
		if ( descriptor is null )
			return null;

		if ( descriptor.SingleInstance )
		{
			var existing = OpenApps.FirstOrDefault( x => x.State.AppId == appId );
			if ( existing is not null )
			{
				if ( initialData is not null )
				{
					foreach ( var item in initialData )
						existing.State.Data[item.Key] = item.Value;

					RefreshRunningAppSession( existing );
				}

				if ( focusWindow && descriptor.HasWindow )
					Focus( existing.State.InstanceId );

				return existing;
			}
		}

		var visibleWindowCount = State.OpenApps.Count( x => GetDescriptor( x.AppId )?.HasWindow == true );
		var initialBounds = ComputerWindowLayoutPolicy.ResolveInitialBounds(
			descriptor,
			State.ResolutionX,
			State.ResolutionY,
			visibleWindowCount,
			LoadAppSettingInt( descriptor.Id, "window_width", descriptor.DefaultWindowWidth ),
			LoadAppSettingInt( descriptor.Id, "window_height", descriptor.DefaultWindowHeight ) );
		var appState = new ComputerAppState
		{
			InstanceId = Guid.NewGuid().ToString( "N" ),
			AppId = descriptor.Id,
			Title = descriptor.Title,
			Icon = descriptor.Icon,
			X = initialBounds.X,
			Y = initialBounds.Y,
			Width = initialBounds.Width,
			Height = initialBounds.Height,
			ZIndex = ++nextZIndex,
			Status = ComputerProcessStatus.Running,
			Data = initialData is null ? new Dictionary<string, string>() : new Dictionary<string, string>( initialData )
		};

		var running = CreateRunningApp( descriptor, appState );
		State.OpenApps.Add( appState );
		runningApps[appState.InstanceId] = running;
		simulationByInstance[appState.InstanceId] = new ProcessSimulationState();

		if ( focusWindow && descriptor.HasWindow )
		{
			appState.IsMinimized = false;
			appState.ZIndex = ++nextZIndex;
			State.FocusedInstanceId = appState.InstanceId;
			running.Session.OnFocused?.Invoke();
		}

		RefreshStorageMetrics();
		if ( markChanged )
			MarkChanged();
		else
			MarkDesktopChanged();

		return running;
	}

	private ComputerRunningApp CreateRunningApp( ComputerAppDescriptor descriptor, ComputerAppState appState )
	{
		var installedApp = GetOrCreateInstalledAppState( descriptor.Id );
		var context = new ComputerAppContext( computer, this, installedApp, appState );
		var session = descriptor.Create().Run( context );

		if ( !string.IsNullOrWhiteSpace( session.Title ) )
			appState.Title = session.Title!;

		if ( !string.IsNullOrWhiteSpace( session.Icon ) )
			appState.Icon = session.Icon!;

		return new ComputerRunningApp( this, descriptor, appState, session );
	}

	public ComputerAppSession CreateMirrorSession( ComputerRunningApp app )
	{
		var installedApp = GetOrCreateInstalledAppState( app.Descriptor.Id );
		var context = new ComputerAppContext( computer, this, installedApp, app.State );
		return app.Descriptor.Create().Run( context );
	}

	private void RefreshRunningAppSession( ComputerRunningApp app )
	{
		try
		{
			app.Session.Content.Delete();
		}
		catch ( Exception )
		{
		}

		var installedApp = GetOrCreateInstalledAppState( app.Descriptor.Id );
		var context = new ComputerAppContext( computer, this, installedApp, app.State );
		var session = app.Descriptor.Create().Run( context );
		if ( !string.IsNullOrWhiteSpace( session.Title ) )
			app.State.Title = session.Title!;
		if ( !string.IsNullOrWhiteSpace( session.Icon ) )
			app.State.Icon = session.Icon!;
		app.Session = session;
	}

	private ComputerInstalledAppState GetOrCreateInstalledAppState( string appId )
	{
		var installedApp = State.InstalledApps.FirstOrDefault( x => x.AppId == appId );
		if ( installedApp is not null )
			return installedApp;

		installedApp = new ComputerInstalledAppState { AppId = appId };
		State.InstalledApps.Add( installedApp );
		Apps = ResolveInstalledApps();
		return installedApp;
	}

	private int? LoadAppSettingInt( string appId, string key, int? fallback )
	{
		var value = LoadAppSetting( appId, key );
		if ( int.TryParse( value, out var parsed ) )
			return parsed;

		return fallback;
	}

	private IReadOnlyList<ComputerAppDescriptor> ResolveInstalledApps()
	{
		var registry = ComputerAppRegistry.Apps.ToDictionary( x => x.Id );
		return State.InstalledApps
			.Select( x => registry.TryGetValue( x.AppId, out var descriptor ) ? descriptor : null )
			.Where( x => x is not null )
			.Select( x => x! )
			.OrderBy( x => x.SortOrder )
			.ThenBy( x => x.Title, StringComparer.OrdinalIgnoreCase )
			.ToArray();
	}

	private void EnsureRequiredBackgroundAppsInstalled()
	{
		foreach ( var app in ComputerAppRegistry.Apps.Where( x => x.RunOnStartup || x.IsBackgroundProcess ) )
		{
			if ( State.InstalledApps.Any( x => x.AppId == app.Id ) )
				continue;

			State.InstalledApps.Add( new ComputerInstalledAppState { AppId = app.Id } );
		}
	}

	private void EnsureStartupProcessesRunning()
	{
		foreach ( var app in Apps.Where( x => x.RunOnStartup ) )
		{
			if ( OpenApps.Any( x => x.State.AppId == app.Id ) )
				continue;

			OpenAppInternal( app.Id, false, false, null );
		}
	}

	private bool UpdateResourceSimulation( float deltaSeconds )
	{
		foreach ( var instanceId in simulationByInstance.Keys.Except( runningApps.Keys ).ToArray() )
		{
			simulationByInstance.Remove( instanceId );
		}

		var ramTotalMb = MathF.Max( 256f, State.Hardware.RamGb * 1024f );
		var gpuVramTotalMb = MathF.Max( 256f, State.Hardware.GpuVramGb * 1024f );
		var rawCpuTotal = 0f;
		var ramUsedMb = 0f;
		var rawGpuTotal = 0f;
		var gpuVramUsedMb = 0f;
		var persistentStateChanged = false;

		foreach ( var app in OpenApps )
		{
			var simulation = GetOrCreateSimulation( app.State.InstanceId );
			var descriptor = app.Descriptor;
			var metrics = simulation.Metrics;

			if ( app.State.Status != ComputerProcessStatus.NotResponding && descriptor.ChanceToStopRespondingPerMinute > 0f )
			{
				if ( random.NextSingle() < ToPerSampleChance( descriptor.ChanceToStopRespondingPerMinute, deltaSeconds ) )
				{
					app.State.Status = ComputerProcessStatus.NotResponding;
					PushNotification( "Application Error", $"{app.State.Title} has stopped responding.", "!" );
					ShowStopRespondingDialog( app );
					persistentStateChanged = true;
				}
			}

			if ( !simulation.LeakTriggered && descriptor.ChanceOfMemoryLeakPerMinute > 0f )
			{
				if ( random.NextSingle() < ToPerSampleChance( descriptor.ChanceOfMemoryLeakPerMinute, deltaSeconds ) )
				{
					simulation.LeakTriggered = true;
					simulation.LeakMultiplier = 1.18f;
					PushNotification( "Memory Warning", $"{app.State.Title} is using more memory than expected.", "!" );
					ShowMemoryLeakDialog( app );
				}
			}
			else if ( simulation.LeakTriggered )
			{
				simulation.LeakMultiplier += random.NextSingle() * 0.12f;
			}

			var cpuPercent = GetBaseCpuUsage( descriptor ) * RandomRange( 0.86f, 1.18f );
			var ramMb = GetBaseRamUsageMb( descriptor, ramTotalMb ) * simulation.LeakMultiplier * RandomRange( 0.95f, 1.08f );
			var gpuPercent = descriptor.ExpectedAvgGpuCoreUsagePercent * RandomRange( 0.85f, 1.2f );
			var gpuVramMb = (gpuVramTotalMb * (descriptor.ExpectedAvgGpuVramUsagePercent / 100f)) * RandomRange( 0.9f, 1.1f );

			if ( app.State.IsMinimized && descriptor.HasWindow )
			{
				cpuPercent *= 0.3f;
				gpuPercent *= 0.25f;
			}

			if ( GetEffectiveStatus( app ) == ComputerProcessStatus.NotResponding )
			{
				cpuPercent *= 0.22f;
				gpuPercent *= 0.2f;
			}

			metrics.CpuPercent = MathF.Max( 0f, cpuPercent );
			metrics.RamMb = MathF.Max( 1f, ramMb );
			metrics.RamPercent = MathF.Max( 0f, metrics.RamMb / ramTotalMb * 100f );
			metrics.GpuCorePercent = MathF.Max( 0f, gpuPercent );
			metrics.GpuVramPercent = MathF.Max( 0f, gpuVramMb / gpuVramTotalMb * 100f );
			metrics.StorageGb = descriptor.StorageSpaceUsedGb;

			rawCpuTotal += metrics.CpuPercent;
			ramUsedMb += metrics.RamMb;
			rawGpuTotal += metrics.GpuCorePercent;
			gpuVramUsedMb += gpuVramMb;
		}

		var hadResourceStarvedApps = OpenApps.Any( x => x.State.IsResourceStarved );
		if ( ramUsedMb > ramTotalMb && FocusedApp is not null )
		{
			foreach ( var app in OpenApps )
			{
				var shouldStarve = app.State.InstanceId == FocusedApp.State.InstanceId;
				if ( app.State.IsResourceStarved == shouldStarve )
					continue;

				app.State.IsResourceStarved = shouldStarve;
				persistentStateChanged = true;
			}
		}
		else if ( hadResourceStarvedApps )
		{
			foreach ( var app in OpenApps.Where( x => x.State.IsResourceStarved ) )
			{
				app.State.IsResourceStarved = false;
				persistentStateChanged = true;
			}
		}

		Metrics.CpuPercent = MathF.Min( 100f, rawCpuTotal );
		Metrics.RamTotalMb = ramTotalMb;
		Metrics.RamUsedMb = ramUsedMb;
		Metrics.RamPercent = MathF.Min( 100f, ramUsedMb / ramTotalMb * 100f );
		Metrics.GpuCorePercent = MathF.Min( 100f, rawGpuTotal );
		Metrics.GpuVramPercent = MathF.Min( 100f, gpuVramUsedMb / gpuVramTotalMb * 100f );
		RecordMetricSample( cpuHistory, Metrics.CpuPercent );
		RecordMetricSample( ramHistory, Metrics.RamPercent );
		RecordMetricSample( gpuHistory, Metrics.GpuCorePercent );
		RecordMetricSample( gpuVramHistory, Metrics.GpuVramPercent );
		MaybeNotifyResourcePressure();

		if ( rawCpuTotal >= 100f && State.Hardware.SimulateCpuInputDelayWhenMaxed )
			inputDelayRemaining = MathF.Max( inputDelayRemaining, RandomRange( 0.18f, 0.75f ) );

		return persistentStateChanged;
	}

	private ProcessSimulationState GetOrCreateSimulation( string instanceId )
	{
		if ( simulationByInstance.TryGetValue( instanceId, out var simulation ) )
			return simulation;

		simulation = new ProcessSimulationState();
		simulationByInstance[instanceId] = simulation;
		return simulation;
	}

	private float GetBaseCpuUsage( ComputerAppDescriptor descriptor )
	{
		var cpuCores = Math.Max( 1, State.Hardware.CpuCoreCount );
		return MathF.Max( 0f, descriptor.ExpectedCoreCountUsageAvg * descriptor.ExpectedAvgCpuCoreUsagePercent / cpuCores );
	}

	private static float GetBaseRamUsageMb( ComputerAppDescriptor descriptor, float totalRamMb )
	{
		if ( descriptor.ExpectedAvgRamUsagePercentOverride.HasValue )
			return totalRamMb * descriptor.ExpectedAvgRamUsagePercentOverride.Value / 100f;

		return descriptor.ExpectedAvgRamUsageMb;
	}

	private ComputerProcessStatus GetEffectiveStatus( ComputerRunningApp app )
	{
		if ( app.State.Status == ComputerProcessStatus.NotResponding || app.State.IsResourceStarved )
			return ComputerProcessStatus.NotResponding;

		if ( app.State.Status == ComputerProcessStatus.Suspended )
			return ComputerProcessStatus.Suspended;

		return app.State.IsMinimized && app.Descriptor.HasWindow
			? ComputerProcessStatus.Suspended
			: ComputerProcessStatus.Running;
	}

	private ComputerAppDescriptor? GetDescriptor( string appId )
	{
		return Apps.FirstOrDefault( x => x.Id == appId );
	}

	private void RefreshStorageMetrics()
	{
		var usedStorage = Apps.Sum( x => x.StorageSpaceUsedGb );
		Metrics.UsedStorageGb = usedStorage;
		Metrics.UnusedStorageGb = MathF.Max( 0f, State.Hardware.HddStorageGb - usedStorage );
	}

	private string WriteMaintenanceRecord( ComputerMaintenanceRecord record )
	{
		var virtualPath = $"{GetDefaultDocumentsPath().TrimEnd( '/')}/{record.FileName}";
		PaneArchiveFileSystem.WriteTextFile( GetArchivePath(), ParseVirtualPath( virtualPath ), record.FileContent );
		RefreshTransientUi();
		return virtualPath;
	}

	private void ShowStopRespondingDialog( ComputerRunningApp app )
	{
		if ( activeMessageBox is not null || !app.Descriptor.HasWindow )
			return;

		ShowMessageBox(
			new ComputerMessageBoxOptions
			{
				Title = "Application Error",
				Message = $"{app.State.Title} has stopped responding. You can wait for it or close it now.",
				Icon = "!",
				Buttons = new[] { "Wait", "Close App" }
			},
			result =>
			{
				if ( result.ButtonPressed.Equals( "Close App", StringComparison.OrdinalIgnoreCase ) )
					Close( app.State.InstanceId );
			} );
	}

	private void ShowMemoryLeakDialog( ComputerRunningApp app )
	{
		if ( activeMessageBox is not null || !app.Descriptor.HasWindow )
			return;

		ShowMessageBox(
			new ComputerMessageBoxOptions
			{
				Title = "Memory Warning",
				Message = $"{app.State.Title} may have a memory leak. Restarting it can recover RAM.",
				Icon = "!",
				Buttons = new[] { "Ignore", "Restart App" }
			},
			result =>
			{
				if ( result.ButtonPressed.Equals( "Restart App", StringComparison.OrdinalIgnoreCase ) )
					RestartAppInstance( app.State.InstanceId );
			} );
	}

	private void RestartAppInstance( string instanceId )
	{
		if ( !runningApps.TryGetValue( instanceId, out var app ) )
			return;

		var appId = app.State.AppId;
		var data = new Dictionary<string, string>( app.State.Data, StringComparer.OrdinalIgnoreCase );
		var focusWindow = app.Descriptor.HasWindow;
		Close( instanceId );
		OpenAppInternal( appId, focusWindow, true, data );
	}

	private void EnsureArchiveExists()
	{
		PaneArchiveFileSystem.EnsureArchive( GetArchivePath(), ResolvePlayerFolderName(), Apps );
	}

	private string ResolvePlayerFolderName()
	{
		return PaneArchiveFileSystem.NormalizeDisplayName( computer.ResolvePersistentArchiveUserName( State ) );
	}

	private static bool IsRecycleBinPath( IReadOnlyList<string> path )
	{
		return path.Count >= 2 &&
			path[0].Equals( "C:", StringComparison.OrdinalIgnoreCase ) &&
			path[1].Equals( "Recycle Bin", StringComparison.OrdinalIgnoreCase );
	}

	private void RefreshActiveFileDialogItems()
	{
		if ( activeFileDialog is null )
			return;

		EnsureArchiveExists();
		activeFileDialog.VisibleItems = PaneArchiveFileSystem.GetItems( GetArchivePath(), activeFileDialog.CurrentPathSegments )
			.Where( x => x.IsDirectory || ComputerFileDialogPolicy.AllowsExtension( activeFileDialog.Options, x.Extension ) )
			.ToArray();
	}

	private static IReadOnlyList<string> ParseVirtualPath( string virtualPath )
	{
		if ( string.IsNullOrWhiteSpace( virtualPath ) )
			return Array.Empty<string>();

		return virtualPath
			.Trim()
			.TrimStart( '/' )
			.Split( '/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries );
	}

	private static string ResolveFileDialogPath( ComputerActiveFileDialog dialog )
	{
		return ComputerFileDialogPolicy.ResolvePath(
			dialog.Options,
			dialog.CurrentPathSegments,
			dialog.SelectedVirtualPath,
			dialog.CurrentFileName );
	}

	private float RandomRange( float min, float max )
	{
		return min + (max - min) * random.NextSingle();
	}

	private static float ToPerSampleChance( float perMinuteChance, float deltaSeconds )
	{
		var normalizedChance = Math.Clamp( perMinuteChance, 0f, 1f );
		return 1f - MathF.Pow( 1f - normalizedChance, MathF.Max( 0f, deltaSeconds ) / 60f );
	}

	private static void RecordMetricSample( Queue<float> history, float value )
	{
		history.Enqueue( value );
		while ( history.Count > 24 )
			history.Dequeue();
	}

	private bool TickNotifications( float deltaSeconds )
	{
		var changed = false;
		for ( var index = notifications.Count - 1; index >= 0; index-- )
		{
			notifications[index].RemainingSeconds -= MathF.Max( 0f, deltaSeconds );
			if ( notifications[index].RemainingSeconds > 0f )
				continue;

			notifications.RemoveAt( index );
			changed = true;
		}

		return changed;
	}

	private void MaybeNotifyResourcePressure()
	{
		if ( Metrics.RamPercent >= 90f && !lowMemoryNotificationActive )
		{
			lowMemoryNotificationActive = true;
			PushNotification( "Low Memory", "PaneOS is running low on RAM. Some apps may stop responding.", "!" );
		}
		else if ( Metrics.RamPercent <= 75f )
		{
			lowMemoryNotificationActive = false;
		}
	}

	private void TickRestartLogs( float deltaSeconds )
	{
		restartLogAccumulator -= MathF.Max( 0f, deltaSeconds );
		if ( restartLogAccumulator > 0f )
			return;

		restartLogAccumulator = RandomRange( 0.18f, 0.55f );
		var nextLine = RestartLogTemplates[random.Next( RestartLogTemplates.Length )];
		State.RestartLogLines.Add( $"{DateTime.UtcNow:HH:mm:ss.fff} {nextLine}" );
		while ( State.RestartLogLines.Count > 12 )
			State.RestartLogLines.RemoveAt( 0 );
	}
}

public sealed class ComputerRunningApp
{
	internal ComputerRunningApp( ComputerRuntime runtime, ComputerAppDescriptor descriptor, ComputerAppState state, ComputerAppSession session )
	{
		Runtime = runtime;
		Descriptor = descriptor;
		State = state;
		Session = session;
	}

	public ComputerRuntime Runtime { get; }
	public ComputerAppDescriptor Descriptor { get; }
	public ComputerAppState State { get; }
	public ComputerAppSession Session { get; internal set; }
}