Editor/QuickAssetMenu.cs
using Editor;
using Sandbox;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace QuickAsset;

public class QuickAssetMenu : Menu
{
	public static QuickAssetMenu Current { get; set; }
	public string Filter { get; set; }

	private LineEdit _searchInput;
	private ListView _list;

	private bool UseCloud
	{
		get => EditorCookie.Get( "qam.UseCloud", false );
		set => EditorCookie.Set( "qam.UseCloud", value );
	}

	private IReadOnlyList<string> RecentPackages
	{
		get => EditorCookie.Get<List<string>>( "qam.RecentPackages", [] );
		set => EditorCookie.Set( "qam.RecentPackages", value );
	}

	private IReadOnlyList<string> RecentAssets
	{
		get => EditorCookie.Get<List<string>>( "qam.RecentAssets", [] );
		set => EditorCookie.Set( "qam.RecentAssets", value );
	}

	public QuickAssetMenu() : base( null )
	{
		IsWindow = false;
		Current = this;

		Layout = Layout.Column();
		FixedWidth = 300;
		FixedHeight = 410;

		Layout.Margin = 8;
		Layout.Spacing = 8;

		var row = Layout.AddRow();

		_searchInput = row.Add( new LineEdit(), 1 );
		_searchInput.TextChanged += v =>
		{
			UpdateList();
		};
		_searchInput.PlaceholderText = "Search for assets...";
		_searchInput.FocusMode = FocusMode.Click;
		_searchInput.Focus();

		var cloudToggle = row.Add( new IconButton( "cloud_off" ) );

		void SetCloudToggle()
		{
			cloudToggle.Icon = UseCloud ? "cloud" : "cloud_off";
			cloudToggle.ToolTip = UseCloud ? "Cloud Enabled" : "Cloud Disabled";
		}

		SetCloudToggle();

		cloudToggle.OnClick = () =>
		{
			UseCloud = !UseCloud;
			SetCloudToggle();
			UpdateList();
		};

		Bind( nameof( Filter ) ).ReadOnly().From( _searchInput, x => x.Text );

		Layout.Add( BuildAssets(), 1 );
	}

	// fucking shite x2
	private bool IsSpawnable( AssetType type )
	{
		if ( type == null )
			return false;

		if ( type.IsSimpleAsset )
			return false;

		if ( type.FileExtension == "vmdl" )
			return true;

		if ( type.FileExtension == "prefab" )
			return true;

		if ( type.FileExtension == "sound" )
			return true;

		if ( type.FileExtension == "vpcf" )
			return true;

		return false;
	}

	private void UpdateList()
	{
		if ( UseCloud )
			_ = SetListCloud();
		else
			SetListAssets();
	}

	private void SetListAssets()
	{
		List<object> options = new();

		if ( string.IsNullOrEmpty( Filter ) )
		{
			foreach ( var recent in RecentAssets )
			{
				var asset = AssetSystem.FindByPath( recent );

				if ( asset == null )
					continue;

				if ( !IsSpawnable( asset.AssetType ) )
					continue;

				options.Add( asset );
			}
		}
		else
		{
			var assets = AssetSystem.All
				.Where( x => x.Path.Contains( Filter, StringComparison.OrdinalIgnoreCase ) )
				.Where( x => IsSpawnable( x.AssetType ) )
				.OrderByDescending( x => x.LastOpened )
				.OrderByDescending( x => RecentAssets.Contains( x.Path ) );

			options.AddRange( assets );
		}

		_list.SetItems( options );
	}

	private async Task SetListCloud()
	{
		List<object> options = new();

		if ( string.IsNullOrEmpty( Filter ) )
		{
			foreach ( var ident in RecentPackages )
			{
				var package = await Package.Fetch( ident, false );

				if ( package == null )
					continue;

				if ( !IsSpawnable( GetAssetType( package.TypeName ) ) )
					continue;

				options.Add( package );
			}
		}
		else
		{
			var tokenSource = new CancellationTokenSource();
			var token = tokenSource.Token;
			var found = await Package.FindAsync( Filter, 200, 0, token );

			if ( found.Packages.Length == 0 )
				return;

			//
			// Add them all to the list
			//
			var packages = found.Packages.Where( x => IsSpawnable( GetAssetType( x.TypeName ) ) )
				.OrderByDescending( x => x.Updated )
				.OrderByDescending( x => RecentPackages.Contains( x.FullIdent ) );

			options.AddRange( packages );
		}

		_list.SetItems( options );
	}

	private Widget BuildAssets()
	{
		var canvas = new Widget( null );
		canvas.Layout = Layout.Row();

		_list = new ListView( canvas );
		UpdateList();
		_list.Margin = 0;
		_list.ItemSize = new Vector2( 0, 40 );
		_list.ItemPaint = PaintListMode;
		_list.ItemSpacing = 0;
		_list.ItemClicked = o =>
		{
			if ( o is Asset a )
				_ = OnSelected( a );

			if ( o is Package p )
				_ = OnSelected( p );
		};
		_list.OnPaintOverride = PaintList;

		canvas.Layout.Add( _list );

		return canvas;
	}

	public static AssetType GetAssetType( string typeName ) => typeName switch
	{
		"map" => AssetType.MapFile,
		"model" => AssetType.Model,
		"material" => AssetType.Material,
		"sound" => AssetType.SoundFile,
		"shader" => AssetType.Shader,

		_ => null
	};

	private void PaintListMode( VirtualWidget item )
	{
		string title = "";
		string subtitle = "";
		Color color = Color.White;
		bool isRecent = false;

		{
			if ( item.Object is Asset asset )
			{
				title = asset.Name;
				subtitle = asset.AssetType.FriendlyName;
				color = asset.AssetType.Color;
				isRecent = RecentAssets.Contains( asset.Path );
			}
			else if ( item.Object is Package package )
			{
				var type = GetAssetType( package.TypeName );

				title = package.Title;
				subtitle = package.Org.Title;
				color = type.Color;
				isRecent = RecentPackages.Contains( package.FullIdent );
			}
			else
			{
				Log.Warning( "Can't paint type" );
				return;
			}
		}

		if ( Paint.HasSelected || Paint.HasPressed )
		{
			Paint.ClearPen();
			Paint.SetBrush( Paint.HasPressed ? Theme.Primary.WithAlpha( 0.4f ) : Theme.Primary.WithAlpha( 0.2f ) );
			Paint.DrawRect( item.Rect.Shrink( 0 ), 3 );

			Paint.SetPen( Theme.White );
		}
		else if ( Paint.HasMouseOver )
		{
			Paint.ClearPen();
			Paint.SetBrush( Theme.Blue.Darken( 0.7f ).Desaturate( 0.3f ).WithAlpha( 0.5f ) );
			Paint.DrawRect( item.Rect );
			Paint.SetPen( Theme.White );
		}

		var itemRect = item.Rect.Shrink( 1 );
		itemRect = itemRect.Shrink( 0, 0, 8, 0 );

		var rect = itemRect;
		var textRect = rect.Shrink( 40 + 8, 4, 0, 4 );

		{
			Paint.SetPen( Theme.ControlText );
			Paint.SetDefaultFont();
			Paint.DrawText( textRect, title, TextFlag.LeftTop | TextFlag.SingleLine );
		}

		{
			Paint.SetDefaultFont();
			Paint.SetPen( Theme.ControlText.WithAlpha( 0.5f ) );
			Paint.DrawText( textRect, subtitle, TextFlag.LeftBottom | TextFlag.SingleLine );
		}

		if ( isRecent )
		{
			Paint.SetDefaultFont();
			Paint.SetPen( Theme.ControlText.WithAlpha( 0.5f ) );
			Paint.DrawIcon( textRect, "history", 13.0f, TextFlag.RightCenter );
		}

		Paint.ClearPen();

		var ir = itemRect;
		ir.Size = new Vector2( itemRect.Height, itemRect.Height );

		{
			var aPos = rect.TopLeft;
			var bPos = rect.BottomLeft;

			var aColor = color.WithAlpha( 0 );
			var bColor = color.WithAlpha( 0.5f );

			Paint.SetBrushLinear( aPos, bPos, aColor, bColor );
			Paint.DrawRect( ir );

			if ( item.Object is Asset asset )
			{
				Paint.Draw( ir, asset.GetAssetThumb() ?? asset.AssetType.Icon64 );
			}
			else if ( item.Object is Package package )
			{
				Paint.Draw( ir, package.Thumb );
			}
		}

		ir.Top = ir.Bottom - 4;
		Paint.ClearPen();
		Paint.SetBrush( color );
		Paint.DrawRect( ir );
	}

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

		WindowOpacity = 0;
	}

	protected override void OnKeyPress( KeyEvent e )
	{
		if ( e.KeyboardModifiers.HasFlag( KeyboardModifiers.Ctrl ) && e.Key is KeyCode.K )
			_searchInput.Focus();
	}

	// fucking shite
	private Ray GetRay( Vector2 cursorPosition, Vector2 screenSize )
	{
		var state = SceneViewportWidget.LastSelected.State;
		var fov = 80.0f;

		var aspect = screenSize.x / screenSize.y;
		var posNormalized = new Vector2( (2.0f * cursorPosition.x / screenSize.x) - 1, (2.0f * cursorPosition.y / screenSize.y) - 1 ) * -1.0f;

		float halfWidth = MathF.Tan( fov * MathF.PI / 360.0f );
		float halfHeight = halfWidth / aspect;

		var ray = new Vector3( 1.0f, posNormalized.x / (1.0f / halfWidth), posNormalized.y / (1.0f / halfHeight) ) * state.CameraRotation;
		return new Ray( state.CameraPosition, ray.Normal );
	}

	private async Task CreateGameObject( string path )
	{
		using var a = SceneEditorSession.Scope();
		using var b = SceneEditorSession.Active.UndoScope( "Quick Add Asset" ).Push();

		var drop = await BaseDropObject.CreateDropFor( path );
		var rect = SceneViewportWidget.LastSelected.ScreenRect;
		var cursorPos = CursorPosition - rect.TopLeft;

		var state = SceneViewportWidget.LastSelected.State;

		var ray = GetRay( cursorPos, rect.Size );

		var tr = SceneEditorSession.Active.Scene.Trace
						.WithoutTags( "trigger" )
						.Ray( ray, 1000f )
						.Run();

		if ( drop is not null )
		{
			await drop.StartInitialize( path );
			drop.UpdateDrag( tr, EditorScene.GizmoSettings );

			using ( var sc = SceneEditorSession.Active.Scene.Push() )
			{
				await drop.OnDrop();
				var go = drop.GameObject;

				if ( go.IsValid() )
				{
					EditorScene.Selection.Add( go );
				}
			}

			drop.Delete();
		}
	}

	private async Task OnSelected( Package package )
	{
		Close();

		// Remove existing, append to back
		RecentPackages = [package.FullIdent, .. RecentPackages.Where( x => x != package.FullIdent ).Take( 8 )];

		await CreateGameObject( package.Url );
	}

	private async Task OnSelected( Asset asset )
	{
		Close();

		// Remove existing, append to back
		RecentAssets = [asset.Path, .. RecentAssets.Where( x => x != asset.Path ).Take( 8 )];

		await CreateGameObject( asset.Path );
	}

	private bool PaintList()
	{
		if ( _list.Items.Any() )
			return false;

		Paint.ClearBrush();
		Paint.ClearPen();

		Paint.SetPen( Theme.ControlText.WithAlpha( 0.5f ) );
		Paint.SetDefaultFont();


		if ( string.IsNullOrEmpty( Filter ) )
		{
			Paint.DrawText( _list.ContentRect, "Type to start searching for assets" );
		}
		else
		{
			Paint.DrawText( _list.ContentRect, "Nothing found :(" );
		}

		return true;
	}

	private Vector2 CursorPosition;

	[Shortcut( "quick-asset.toggle", "SHIFT+A", ShortcutType.Widget )]
	public static void ShowQuickAsset()
	{
		var rect = SceneViewWidget.Current.ScreenRect;
		var cursorPos = Editor.Application.CursorPosition;

		if ( !rect.IsInside( cursorPos ) )
			return;

		Current?.Destroy();

		Current = new QuickAssetMenu();
		Current.Show();

		Current.CursorPosition = cursorPos;
		Current.Position = cursorPos + 16;
	}
}