Editor/Launcher/HotCodeLauncher.cs
using Sandbox;
using Editor;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

/// <summary>
/// HotCode's start window:
/// <code>
/// ┌ sidebar ──────┬ main ─────────────────────────────────┐
/// │ HotCode       │ Projects                  [search...] │
/// │ New Project   │ ───────────────────────────────────── │
/// │ Open Project  │ Pinned / Recent / Loaded in s&amp;box     │
/// │ Open Solution │   [card] [card] ...                   │
/// └───────────────┴───────────────────────────────────────┘
/// </code>
/// </summary>
[EditorApp( "HotCode", HotCodeLogo.IconName, "A code editor built right into s&box" )]
public class HotCodeLauncher : Window
{
	const string LastLocationCookie = "hotcode.newproject.location";

	private readonly Widget _main;
	private readonly SidebarButton _newButton;
	private string _search = "";

	public HotCodeLauncher()
	{
		WindowTitle = "HotCode";
		SetWindowIcon( HotCodeLogo.WindowIcon );
		Size = new Vector2( 980, 620 );
		MinimumSize = new Vector2( 700, 420 );

		Canvas = new Widget( this );
		Canvas.Layout = Layout.Row();

		// --- Sidebar ---
		var sidebar = Canvas.Layout.Add( new Widget( Canvas ) );
		sidebar.FixedWidth = 230;
		sidebar.OnPaintOverride = () =>
		{
			Paint.ClearPen();
			Paint.SetBrush( Theme.SidebarBackground );
			Paint.DrawRect( sidebar.LocalRect );
			return true;
		};

		sidebar.Layout = Layout.Column();
		sidebar.Layout.Margin = 12;
		sidebar.Layout.Spacing = 4;

		sidebar.Layout.Add( new LauncherLogo( sidebar ) );
		sidebar.Layout.AddSpacingCell( 12 );
		sidebar.Layout.AddSeparator( true );
		sidebar.Layout.AddSpacingCell( 8 );

		var projects = sidebar.Layout.Add( new SidebarButton( sidebar, "dashboard", "Projects", "Recent and loaded projects" ) { Selected = true } );
		_newButton = sidebar.Layout.Add( new SidebarButton( sidebar, "add_circle_outline", "New Project", "Create a game or library" ) );
		var openProject = sidebar.Layout.Add( new SidebarButton( sidebar, "folder_open", "Open Project...", "Pick an .sbproj file" ) );
		var openSolution = sidebar.Layout.Add( new SidebarButton( sidebar, "account_tree", "Open Solution...", "Pick a .sln or .slnx file" ) );

		projects.Clicked = () => { projects.Selected = true; _newButton.Selected = false; ShowProjects(); };
		_newButton.Clicked = () => { projects.Selected = false; _newButton.Selected = true; ShowNewProject(); };
		openProject.Clicked = HandleOpenProjectDialog;
		openSolution.Clicked = HandleOpenSolutionDialog;

		sidebar.Layout.AddStretchCell();

		var settings = sidebar.Layout.Add( new SidebarButton( sidebar, "settings", "Settings", "Editor, code help, files" ) );
		settings.Clicked = HotCodeSettingsWindow.Open;
		sidebar.Layout.AddSpacingCell( 8 );

		var hint = sidebar.Layout.Add( new Label( "Each project opens in its own HotCode window.", sidebar ) );
		hint.WordWrap = true;
		hint.SetStyles( $"color: {Theme.TextLight.Hex}; font-size: 11px;" );

		Canvas.Layout.AddSeparator( 1, Theme.Border );

		// --- Main area ---
		_main = Canvas.Layout.Add( new Widget( Canvas ), 1 );
		_main.OnPaintOverride = () =>
		{
			Paint.ClearPen();
			Paint.SetBrush( Theme.TabBackground );
			Paint.DrawRect( _main.LocalRect );
			return true;
		};
		_main.Layout = Layout.Column();

		ShowProjects();
		Show();
	}

	[Menu( "Editor", "HotCode/Launcher", HotCodeLogo.IconName )]
	public static void OpenFromEditorMenu() => new HotCodeLauncher();

	// --- PROJECTS ---

	private void ShowProjects()
	{
		_main.Layout.Clear( true );

		// Header: "Projects"        [search]
		var header = _main.Layout.AddRow();
		header.Margin = new Sandbox.UI.Margin( 20, 16, 20, 12 );
		header.Spacing = 8;
		header.Add( new Label.Title( "Projects", _main ) );
		header.AddStretchCell();

		var search = header.Add( new LineEdit( _main ) { PlaceholderText = "Search projects...", Text = _search, FixedWidth = 260 } );

		_main.Layout.AddSeparator( 1, Theme.Border );

		var scroll = _main.Layout.Add( new ScrollArea( _main ), 1 );
		scroll.Canvas = new Widget( scroll );
		scroll.Canvas.Layout = Layout.Column();
		scroll.Canvas.Layout.Margin = new Sandbox.UI.Margin( 20, 12, 20, 20 );
		scroll.Canvas.Layout.Spacing = 6;

		var list = scroll.Canvas;
		search.TextEdited += text =>
		{
			_search = text;
			FillProjectList( list );
		};

		FillProjectList( list );
		search.Focus( false );
	}

	private void FillProjectList( Widget list )
	{
		list.Layout.Clear( true );

		var loadedPaths = LoadedProjectPaths();
		bool IsLoaded( HotCodeEntry e ) => loadedPaths.Contains( RecentEntries.Normalize( e.Path ) );
		bool Matches( HotCodeEntry e ) => _search.Length == 0
			|| e.Title.Contains( _search, StringComparison.OrdinalIgnoreCase )
			|| e.Path.Contains( _search, StringComparison.OrdinalIgnoreCase );

		var recents = RecentEntries.GetAll().ToList();
		var recentPaths = recents.Select( x => RecentEntries.Normalize( x.Path ) ).ToHashSet();

		var pinned = recents.Where( e => e.Pinned && Matches( e ) ).ToList();
		var recent = recents.Where( e => !e.Pinned && Matches( e ) ).ToList();

		// Projects s&box has loaded right now that aren't remembered yet
		var loaded = GetLoadedProjects().Where( e => !recentPaths.Contains( RecentEntries.Normalize( e.Path ) ) && Matches( e ) ).ToList();

		AddSection( list, "Pinned", "star", pinned, IsLoaded );
		AddSection( list, "Recent", "history", recent, IsLoaded );
		AddSection( list, "Loaded in s&box", "bolt", loaded, IsLoaded );

		if ( pinned.Count + recent.Count + loaded.Count == 0 )
		{
			var empty = list.Layout.Add( new Label( _search.Length > 0 ? $"No projects match \"{_search}\"." : "No projects yet. Open or create one from the left.", list ) );
			empty.SetStyles( $"color: {Theme.TextLight.Hex}; padding: 20px 0px;" );
		}

		list.Layout.AddStretchCell();
	}

	private void AddSection( Widget list, string title, string icon, List<HotCodeEntry> entries, Func<HotCodeEntry, bool> isLoaded )
	{
		if ( entries.Count == 0 ) return;

		var header = list.Layout.Add( new SectionHeader( list, title, icon, entries.Count ) );
		header.FixedHeight = 30;

		foreach ( var entry in entries )
		{
			var hasHotCode = HotCodeInstallUi.StateOf( entry ) == HotCodeInstallUi.State.Installed;
			var card = list.Layout.Add( new ProjectCard( list, entry, isLoaded( entry ), hasHotCode ) );
			card.Opened = () => OpenEntry( entry );
			card.PinToggled = pinned =>
			{
				RecentEntries.SetPinned( entry.Path, pinned );
				FillProjectList( list );
			};
			card.ContextMenuRequested = () => ShowCardMenu( list, entry );
		}

		list.Layout.AddSpacingCell( 10 );
	}

	private void ShowCardMenu( Widget list, HotCodeEntry entry )
	{
		var menu = new ContextMenu( this );
		menu.AddOption( "Open", "open_in_new", () => OpenEntry( entry ) );
		menu.AddOption( entry.Pinned ? "Unpin" : "Pin to Top", entry.Pinned ? "star_border" : "star", () =>
		{
			RecentEntries.SetPinned( entry.Path, !entry.Pinned );
			FillProjectList( list );
		} );
		menu.AddSeparator();
		HotCodeInstallUi.AddMenuOptions( menu, entry, () => FillProjectList( list ) );
		menu.AddSeparator();
		menu.AddOption( "Reveal in File Explorer", "folder_open", () => EditorUtility.OpenFileFolder( entry.Path ) );
		menu.AddOption( "Copy Path", "content_copy", () => EditorUtility.Clipboard.Copy( entry.Path ) );

		if ( entry.LastOpened is not null )
		{
			menu.AddSeparator();
			menu.AddOption( "Remove from List", "close", () =>
			{
				RecentEntries.Remove( entry.Path );
				FillProjectList( list );
			} );
		}

		menu.OpenAtCursor();
	}

	private static HashSet<string> LoadedProjectPaths() => EditorUtility.Projects.GetAll()
		.Where( p => p.ConfigFilePath is not null )
		.Select( p => RecentEntries.Normalize( p.ConfigFilePath ) )
		.ToHashSet();

	private static IEnumerable<HotCodeEntry> GetLoadedProjects()
	{
		return EditorUtility.Projects.GetAll()
			.Where( p => !p.IsBuiltIn && p.ConfigFilePath is not null && p.ConfigFilePath.EndsWith( ".sbproj", StringComparison.OrdinalIgnoreCase ) && File.Exists( p.ConfigFilePath ) )
			.OrderByDescending( p => p == Project.Current )
			.ThenBy( p => p.Config.Title )
			.Select( p => HotCodeEntry.FromPath( p.ConfigFilePath ) );
	}

	// --- NEW PROJECT ---

	private void ShowNewProject()
	{
		_main.Layout.Clear( true );

		var header = _main.Layout.AddRow();
		header.Margin = new Sandbox.UI.Margin( 20, 16, 20, 12 );
		header.Add( new Label.Title( "New Project", _main ) );
		header.AddStretchCell();

		_main.Layout.AddSeparator( 1, Theme.Border );

		var form = _main.Layout.AddColumn();
		form.Margin = 20;
		form.Spacing = 6;

		// Kind of project, as tiles
		form.Add( new Label.Subtitle( "Type", _main ) );
		var type = "game";
		var tiles = form.AddRow();
		tiles.Spacing = 10;
		var game = tiles.Add( new ChoiceTile( _main, "sports_esports", "Game", "Something people play", Theme.Green ) { Selected = true }, 1 );
		var library = tiles.Add( new ChoiceTile( _main, "local_library", "Library", "Code and assets for other projects", Theme.Blue ), 1 );
		game.Clicked = () => { type = "game"; game.Selected = true; library.Selected = false; game.Update(); library.Update(); };
		library.Clicked = () => { type = "library"; game.Selected = false; library.Selected = true; game.Update(); library.Update(); };

		form.AddSpacingCell( 12 );
		form.Add( new Label.Subtitle( "Name", _main ) );
		var nameInput = form.Add( new LineEdit( _main ) { Text = "MyProject" } );

		form.AddSpacingCell( 8 );
		form.Add( new Label.Subtitle( "Location", _main ) );
		Label preview = null;
		var locationRow = form.AddRow();
		locationRow.Spacing = 4;
		var locationInput = locationRow.Add( new LineEdit( _main ) { Text = GetDefaultLocation() }, 1 );
		locationRow.Add( new Button( "Browse...", "folder", _main )
		{
			Clicked = () =>
			{
				var folder = PickFolder( "Choose Project Location", locationInput.Text );
				if ( folder is not null )
				{
					locationInput.Text = folder;
					UpdatePreview();
				}
			}
		} );

		preview = form.Add( new Label( "", _main ) );
		preview.SetStyles( $"color: {Theme.TextLight.Hex};" );
		void UpdatePreview() => preview.Text = $"Creates {Path.Combine( locationInput.Text.Trim(), nameInput.Text.Trim() )}";
		nameInput.TextEdited += _ => UpdatePreview();
		locationInput.TextEdited += _ => UpdatePreview();
		UpdatePreview();

		var error = form.Add( new Label( "", _main ) );
		error.SetStyles( $"color: {Theme.Red.Hex};" );
		error.Visible = false;

		form.AddStretchCell();

		_main.Layout.AddSeparator( 1, Theme.Border );

		// Footer
		var footer = _main.Layout.AddRow();
		footer.Margin = new Sandbox.UI.Margin( 20, 10, 20, 12 );
		footer.Spacing = 8;
		footer.AddStretchCell();
		footer.Add( new Button( "Cancel", "close", _main ) { Clicked = () => { _newButton.Selected = false; ShowProjects(); } } );
		footer.Add( new Button.Primary( "Create Project", "done", _main )
		{
			Clicked = () =>
			{
				try
				{
					var sbproj = ProjectCreator.Create( nameInput.Text, locationInput.Text, type );
					EditorCookie.SetString( LastLocationCookie, locationInput.Text.Trim() );
					Log.Info( $"[HotCode] Created project at {sbproj}" );
					_newButton.Selected = false;
					OpenEntry( HotCodeEntry.FromPath( sbproj ) );
				}
				catch ( Exception e ) when ( e is InvalidOperationException or IOException or UnauthorizedAccessException )
				{
					error.Text = e.Message;
					error.Visible = true;
				}
			}
		} );

		nameInput.Focus( false );
		nameInput.SelectAll();
	}

	private static string GetDefaultLocation()
	{
		var last = EditorCookie.GetString( LastLocationCookie, null );
		if ( !string.IsNullOrEmpty( last ) && Directory.Exists( last ) ) return last;

		// Default to the folder the current project lives in
		var root = Project.Current?.GetRootPath();
		return root is null ? "" : Path.GetDirectoryName( root.TrimEnd( '/', '\\' ) ) ?? "";
	}

	// --- DIALOGS ---

	private void HandleOpenProjectDialog()
	{
		var file = PickFile( "Open s&box Project", "s&box Project (*.sbproj)" );
		if ( file is not null ) OpenEntry( HotCodeEntry.FromPath( file ) );
	}

	private void HandleOpenSolutionDialog()
	{
		var file = PickFile( "Open Solution", "Solution (*.sln *.slnx)" );
		if ( file is not null ) OpenEntry( HotCodeEntry.FromPath( file ) );
	}

	private static string PickFile( string title, string filter )
	{
		var fd = new FileDialog( null );
		fd.Title = title;
		fd.Directory = GetDefaultLocation();
		fd.SetFindExistingFile();
		fd.SetModeOpen();
		fd.SetNameFilter( filter );
		return fd.Execute() ? fd.SelectedFile : null;
	}

	private static string PickFolder( string title, string startIn )
	{
		var fd = new FileDialog( null );
		fd.Title = title;
		fd.Directory = Directory.Exists( startIn ) ? startIn : GetDefaultLocation();
		fd.SetFindDirectory();
		return fd.Execute() ? fd.SelectedFile : null;
	}

	// --- OPENING ---

	private void OpenEntry( HotCodeEntry entry )
	{
		RecentEntries.Touch( entry.Path );
		Log.Info( $"[HotCode] Opening {entry.Kind.ToString().ToLowerInvariant()} '{entry.Title}' ({entry.Path})" );

		new HotCodeEditor( entry );

		ShowProjects();
	}

	/// <summary>
	/// The HotCode name and tagline at the top of the sidebar.
	/// </summary>
	private class LauncherLogo : Widget
	{
		public LauncherLogo( Widget parent ) : base( parent )
		{
			FixedHeight = 48;
		}

		protected override void OnPaint()
		{
			HotCodeLogo.PaintBadge( new Rect( 2, 4, 40, 40 ) );

			Paint.SetHeadingFont( 13, 700 );
			Paint.SetPen( Theme.Text );
			Paint.DrawText( new Rect( 52, 4, Width - 52, 24 ), "HotCode", TextFlag.LeftCenter );

			Paint.SetDefaultFont( 7 );
			Paint.SetPen( Theme.TextLight );
			Paint.DrawText( new Rect( 52, 26, Width - 52, 18 ), "Code editor for s&box", TextFlag.LeftCenter );
		}
	}

	/// <summary>
	/// "★ PINNED  2" with a line to the right.
	/// </summary>
	private class SectionHeader : Widget
	{
		private readonly string _title;
		private readonly string _icon;
		private readonly int _count;

		public SectionHeader( Widget parent, string title, string icon, int count ) : base( parent )
		{
			_title = title;
			_icon = icon;
			_count = count;
		}

		protected override void OnPaint()
		{
			Paint.SetPen( Theme.TextLight );
			Paint.DrawIcon( new Rect( 0, 0, 16, Height ), _icon, 14, TextFlag.LeftCenter );

			Paint.SetDefaultFont( 7, 700 );
			var label = $"{_title.ToUpperInvariant()}   {_count}";
			var width = Paint.MeasureText( label ).x;
			Paint.DrawText( new Rect( 22, 0, width + 4, Height ), label, TextFlag.LeftCenter );

			Paint.SetPen( Theme.Border );
			Paint.DrawLine( new Vector2( 34 + width, Height / 2 ), new Vector2( Width, Height / 2 ) );
		}
	}
}