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

namespace PaneOS.InteractiveComputer.Apps;

[ComputerApp( "system.ridge", "Ridge", Icon = "RG", SortOrder = 15 )]
public sealed class RidgeBrowserApp : IComputerApp
{
	public ComputerAppSession Run( ComputerAppContext context )
	{
		return new ComputerAppSession
		{
			Title = "Ridge",
			Icon = "RG",
			Content = new RidgeBrowserPanel( context )
		};
	}
}

[StyleSheet( "InteractiveComputerApps.scss" )]
public sealed class RidgeBrowserPanel : ComputerWarmupPanel
{
	private readonly ComputerAppContext context;
	private ComputerInputAwareTextEntry addressBar = null!;
	private Panel contentHost = null!;
	private Label statusLabel = null!;
	private string currentUrl;
	private RidgePolicyResult pageState = new();

	public RidgeBrowserPanel( ComputerAppContext context )
	{
		this.context = context;
		AddClass( "ridge-app" );

		currentUrl = context.LoadValue( "url" ) ?? context.LoadSetting( "home_url" ) ?? "paneos://default";
		BuildUi();
	}

	protected override void WarmupRefresh()
	{
		MarkRenderDirty();
	}

	private void BuildUi()
	{
		DeleteChildren( true );

		var toolbar = new Panel { Parent = this };
		toolbar.AddClass( "ridge-toolbar" );

		var homeButton = new Button( "Home" ) { Parent = toolbar };
		homeButton.AddClass( "ridge-button" );
		homeButton.AddEventListener( "onclick", GoHome );

		addressBar = new ComputerInputAwareTextEntry( ShouldSuppressInput )
		{
			Parent = toolbar,
			Text = currentUrl
		};
		addressBar.AddClass( "ridge-address" );

		var goButton = new Button( "Go" ) { Parent = toolbar };
		goButton.AddClass( "ridge-button ridge-go" );
		goButton.AddEventListener( "onclick", NavigateFromAddressBar );

		statusLabel = new Label { Parent = this };
		statusLabel.AddClass( "ridge-status" );

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

		Navigate( currentUrl );
	}

	private void GoHome()
	{
		Navigate( context.LoadSetting( "home_url" ) ?? "paneos://default" );
	}

	private void NavigateFromAddressBar()
	{
		Navigate( addressBar.Text );
	}

	private void Navigate( string rawUrl )
	{
		pageState = ResolvePageState( rawUrl );
		currentUrl = pageState.NormalizedUrl;
		addressBar.Text = currentUrl;
		context.SaveValue( "url", currentUrl );
		context.SaveValue( "last_visited_at", DateTime.UtcNow.ToString( "O" ) );
		RenderPage();
	}

	private void RenderPage()
	{
		contentHost.DeleteChildren( true );
		statusLabel.Text = pageState.Status;

		if ( IsLocalDefaultPage( currentUrl ) )
		{
			var localPage = new PoodleSearchPanel( context, OnPoodleSearch )
			{
				Parent = contentHost
			};
			localPage.AddClass( "ridge-local-page" );
			statusLabel.Text = "Poodle search ready";
			return;
		}

		if ( !IsNetworkingAvailable() )
		{
			RenderNetworkMissingPage();
			return;
		}

		if ( pageState.CanRenderWebPanel )
		{
			var webPanel = new WebPanel
			{
				Parent = contentHost,
				Url = currentUrl
			};
			webPanel.AddClass( "ridge-webpanel" );
			return;
		}

		var message = new Panel { Parent = contentHost };
		message.AddClass( $"ridge-message {pageState.MessageClass}" );

		new Label( pageState.Title ) { Parent = message }.AddClass( "ridge-message-title" );
		new Label( pageState.Body ) { Parent = message }.AddClass( "ridge-message-body" );

		if ( pageState.AllowedHosts.Count > 0 )
		{
			var list = new Panel { Parent = message };
			list.AddClass( "ridge-allow-list" );
			new Label( "Allowed hosts" ) { Parent = list }.AddClass( "ridge-allow-title" );

			foreach ( var host in pageState.AllowedHosts )
			{
				new Label( host ) { Parent = list }.AddClass( "ridge-allow-host" );
			}
		}
	}

	private void OnPoodleSearch( string query )
	{
		context.SaveValue( "poodle_query", query );
		statusLabel.Text = string.IsNullOrWhiteSpace( query )
			? "Poodle search ready"
			: $"Poodle sniffed out results for \"{query}\"";
	}

	private void RenderNetworkMissingPage()
	{
		statusLabel.Text = "404 Not Found";
		var message = new Panel { Parent = contentHost };
		message.AddClass( "ridge-message blocked ridge-404-page" );
		new Label( "404 Not Found" ) { Parent = message }.AddClass( "ridge-message-title" );
		new Label( "Networking.exe is not running, so Ridge cannot reach the outside kennel." ) { Parent = message }.AddClass( "ridge-message-body" );
		new Label( "TODO: tiny pixel mouse platformer goes here." ) { Parent = message }.AddClass( "ridge-message-body" );
	}

	private bool IsNetworkingAvailable()
	{
		var networking = context.Runtime.OpenApps.FirstOrDefault( x => x.State.AppId.Equals( "system.networking", StringComparison.OrdinalIgnoreCase ) );
		return networking is not null &&
			context.Runtime.GetEffectiveStatus( networking.State.InstanceId ) == ComputerProcessStatus.Running;
	}

	private bool ShouldSuppressInput()
	{
		return context.Runtime.ShouldBlockInput( context.State.InstanceId ) ||
			context.State.IsMinimized ||
			context.Runtime.FocusedApp?.State.InstanceId != context.State.InstanceId;
	}

	private RidgePolicyResult ResolvePageState( string url )
	{
		var normalized = RidgeBrowserPolicy.NormalizeUrl( string.IsNullOrWhiteSpace( url ) ? "paneos://default" : url );
		if ( IsLocalDefaultPage( normalized ) )
		{
			return new RidgePolicyResult
			{
				NormalizedUrl = "paneos://default",
				Status = "Poodle search ready",
				Title = "Poodle",
				Body = "A local search page"
			};
		}

		return RidgeBrowserPolicy.Evaluate(
			normalized,
			context.LoadSetting( "web_rendering_enabled" ),
			context.LoadSetting( "allowed_hosts" ) );
	}

	private static bool IsLocalDefaultPage( string url )
	{
		return url.Equals( "paneos://default", StringComparison.OrdinalIgnoreCase )
			|| url.Equals( "paneos://home", StringComparison.OrdinalIgnoreCase )
			|| url.Equals( "paneos://poodle", StringComparison.OrdinalIgnoreCase );
	}
}

public sealed class PoodleSearchPanel : ComputerWarmupPanel
{
	private readonly ComputerAppContext context;
	private readonly Action<string> onSearch;
	private ComputerInputAwareTextEntry searchEntry = null!;
	private Panel resultsHost = null!;

	public PoodleSearchPanel( ComputerAppContext context, Action<string> onSearch )
	{
		this.context = context;
		this.onSearch = onSearch;
		AddClass( "poodle-page" );
		BuildUi();
	}

	protected override void WarmupRefresh()
	{
		BuildUi();
	}

	private void BuildUi()
	{
		var currentQuery = searchEntry?.Text ?? context.LoadValue( "poodle_query" ) ?? "";
		DeleteChildren( true );

		var logo = new Label( "Poodle" ) { Parent = this };
		logo.AddClass( "poodle-logo" );

		var tagline = new Label( "Search the local kennel." ) { Parent = this };
		tagline.AddClass( "poodle-tagline" );

		var searchRow = new Panel { Parent = this };
		searchRow.AddClass( "poodle-search-row" );

		searchEntry = new ComputerInputAwareTextEntry( ShouldSuppressInput )
		{
			Parent = searchRow,
			Text = currentQuery,
			Placeholder = "Search PaneOS"
		};
		searchEntry.AddClass( "poodle-input" );

		var searchButton = new Button( "Let loose the dogs!" ) { Parent = searchRow };
		searchButton.AddClass( "poodle-button" );
		searchButton.AddEventListener( "onclick", Search );

		resultsHost = new Panel { Parent = this };
		resultsHost.AddClass( "poodle-results" );

		RenderResults();
	}

	private void Search()
	{
		context.SaveValue( "poodle_query", searchEntry.Text );
		onSearch( searchEntry.Text );
		RenderResults();
	}

	private bool ShouldSuppressInput()
	{
		return context.Runtime.ShouldBlockInput( context.State.InstanceId ) ||
			context.State.IsMinimized ||
			context.Runtime.FocusedApp?.State.InstanceId != context.State.InstanceId;
	}

	private void RenderResults()
	{
		resultsHost.DeleteChildren( true );
		var query = context.LoadValue( "poodle_query" ) ?? "";

		if ( string.IsNullOrWhiteSpace( query ) )
		{
			new Label( "Poodle is waiting for a scent." ) { Parent = resultsHost }.AddClass( "poodle-empty" );
			return;
		}

		foreach ( var result in BuildResults( query ) )
		{
			var row = new Panel { Parent = resultsHost };
			row.AddClass( "poodle-result" );
			new Label( result.Title ) { Parent = row }.AddClass( "poodle-result-title" );
			new Label( result.Url ) { Parent = row }.AddClass( "poodle-result-url" );
			new Label( result.Body ) { Parent = row }.AddClass( "poodle-result-body" );
		}
	}

	private static IReadOnlyList<(string Title, string Url, string Body)> BuildResults( string query )
	{
		return new[]
		{
			($"Poodle result for {query}", $"paneos://search/{query.Replace( ' ', '-' ).ToLowerInvariant() }", $"The local dogs found the strongest scent trail for {query}."),
			($"Best of {query}", "paneos://apps", $"Try checking your apps, notes, and documents for {query}."),
			($"PaneOS knowledge: {query}", "paneos://help", $"No internet needed. Poodle keeps things local and playful.")
		};
	}
}