using System.Globalization;
using System.Text;
using System.Threading;

namespace Editor;

/// <summary>
/// A list of cloud assets.
/// </summary>
public partial class CloudAssetBrowser : Widget, IBrowser
{
	/// <summary>
	/// Only show these asset types.
	/// </summary>
	public List<AssetType> FilterAssetTypes { get; protected set; }

	/// <summary>
	/// Internal asset list panel.
	/// </summary>
	protected AssetList AssetList;

	/// <inheritdoc cref="BaseItemWidget.MultiSelect"/>
	public bool MultiSelect
	{
		get => AssetList.MultiSelect;
		set => AssetList.MultiSelect = value;
	}

	public List<T> GetSelected<T>() => AssetList.Selection.OfType<T>().ToList();

	public string BaseQuery { get; private set; }
	public SearchWidget Search;

	ToolButton ViewMode;
	ToolButton OrderMode;
	ListHeader ListHeader;
	string lastSortColumn = "";
	bool lastSortAscending = true;

	Layout FacetLayout;

	/// <summary>
	/// Package has been clicked
	/// </summary>
	public Action<Package> OnPackageHighlight;

	/// <summary>
	/// Package has been selected (double click, or enter)
	/// </summary>
	public Action<Package> OnPackageSelected;

	/// <summary>
	/// Internal left side panel
	/// </summary>
	protected CloudLocations CloudLocations;

	public Action OnFolderOpened;

	/// <summary>
	/// Show files generated by asset compilation and other usually unuseful files.
	/// </summary>
	public bool ShowJunkFiles { get; set; }
	public bool HideNonAssets { get; set; }

	bool _showRecursive = false;

	/// <summary>
	/// If true, show all files in a flat view, if false, show files and folders like they would appear in Windows Explorer.
	/// </summary>
	public bool ShowRecursiveFiles
	{
		get => _showRecursive;
		set
		{
			if ( _showRecursive == value ) return;
			_showRecursive = value;

			UpdateAssetList();
			SaveSettings();
		}
	}

	/// <summary>
	/// View mode, i.e. thumbs, list or whatever else.
	/// </summary>
	public AssetListViewMode ViewModeType
	{
		get;
		set
		{
			field = value;

			AssetList.ViewMode = field;
			SaveSettings();

			if ( value == AssetListViewMode.List ) ViewMode.Icon = "view_headline";
			if ( value == AssetListViewMode.SmallIcons ) ViewMode.Icon = "apps";
			if ( value == AssetListViewMode.MediumIcons ) ViewMode.Icon = "grid_on";
			if ( value == AssetListViewMode.LargeIcons ) ViewMode.Icon = "grid_view";
		}
	}

	public static string DefaultView
	{
		get => EditorCookie.GetString( "CloudBrowser.DefaultView", "" );
		set => EditorCookie.SetString( "CloudBrowser.DefaultView", value );
	}

	string CurrentOrderMode = "popular";

	Task ResultsTask;

	public CloudAssetBrowser( Widget parent ) : this( parent, null )
	{

	}

	public CloudAssetBrowser( Widget parent, List<AssetType> assetTypeFilters ) : base( parent )
	{
		MinimumSize = new( 100, 100 );

		Layout = Layout.Row();
		Layout.Spacing = 4;

		FilterAssetTypes = assetTypeFilters;

		Search = new SearchWidget( this, false );
		Search.MinimumHeight = Theme.RowHeight;
		Search.ValueChanged = () =>
		{
			UpdateAssetList();
		};

		ListHeader = new ListHeader( this, ["Name", "Author", "Date", "Type"] );
		ListHeader.OnColumnSelect += SortAssetList;
		ListHeader.OnColumnResize += () => AssetList?.Update();

		AssetList = new AssetList( this );
		AssetList.Browser = this;
		AssetList.OnViewModeChanged += () => SaveSettings();
		AssetList.OnHighlight = ( entries ) =>
		{
			var packages = entries.OfType<PackageEntry>().ToList();
			if ( packages.Count == entries.Count() )
			{
				OnPackageHighlight?.Invoke( packages.First().Package );
			}
		};

		var body = new Widget( this );
		body.Layout = Layout.Column();

		var toolbar = body.Layout.AddRow();
		body.Layout.AddSpacingCell( 4 );
		body.Layout.Add( ListHeader );
		body.Layout.Add( AssetList, 1 );

		CloudLocations = new CloudLocations( this );
		CloudLocations.OnFilterSelected = ( filter ) =>
		{
			BaseQuery = filter;
			ClearFacets();
			UpdateAssetList();
		};

		var splitter = new Splitter( this );
		splitter.IsHorizontal = true;
		splitter.AddWidget( CloudLocations );
		splitter.SetStretch( 0, 1 );
		splitter.AddWidget( body );
		splitter.SetStretch( 1, 5 );

		Layout.Add( splitter );

		BuildToolbar( toolbar );

		RefreshCookies();
	}

	protected override void OnVisibilityChanged( bool visible )
	{
		base.OnVisibilityChanged( visible );

		if ( !visible )
			return;

		// Calling this here so that when the window is made visible the cookie
		// key gets set up correctly - when we add a new instance of the asset
		// browser, it requires its own unique key for settings.
		RefreshCookies();
	}

	private void RefreshCookies()
	{
		SettingsCookie = GetCookieKey();
		LoadSettings();

		UpdateAssetList();
	}

	private string GetCookieKey()
	{
		StringBuilder stringBuilder = new( "CloudBrowser" );

		if ( FilterAssetTypes != null )
			stringBuilder.Append( $".{string.Join( ",", FilterAssetTypes )}" );

		if ( Name != null )
			stringBuilder.Append( $".{Name.Replace( " ", "" )}" );

		return stringBuilder.ToString();
	}

	/// <summary>
	/// Directory, contents of which are currently being displayed.
	/// </summary>
	public LocalAssetBrowser.Location CurrentLocation { get; set; }

	CancellationTokenSource tokenSource;

	/// <summary>
	/// Update the list of displayed assets.
	/// </summary>
	public virtual void UpdateAssetList()
	{
		string query = $"{BaseQuery} {Search.Query}";
		if ( query.Contains( "@referenced" ) )
		{
			var searchValue = query.Replace( "@referenced", "" ).Trim();

			UpdateFromReferenced( searchValue );
		}
		else
		{
			ResultsTask = UpdateFromCloud( query );
		}
	}

	private void SortAssetList( string sortBy, bool ascending )
	{
		List<object> items;
		switch ( sortBy )
		{
			case "Name":
				items = AssetList.Items.OrderBy( x =>
				{
					if ( x is PackageEntry pe ) return pe.Package.Title;
					return "";
				} ).ToList();
				break;
			case "Author":
				items = AssetList.Items.OrderBy( x =>
				{
					if ( x is PackageEntry pe ) return pe.Package.Org.Title;
					return "";
				} ).ToList();
				break;
			case "Date":
				items = AssetList.Items.OrderBy( x =>
				{
					if ( x is PackageEntry pe ) return pe.Package.Updated.UtcTicks;
					return 0L;
				} ).ToList();
				break;
			case "Type":
				items = AssetList.Items.OrderBy( x =>
				{
					if ( x is PackageEntry pe ) return pe.Package.TypeName;
					return "";
				} ).ToList();
				break;
			default:
				items = AssetList.Items.ToList();
				break;
		}

		if ( !ascending )
		{
			items.Reverse();
		}
		AssetList.SetItems( items );

		lastSortColumn = sortBy;
		lastSortAscending = ascending;
	}

	/// <summary>
	/// Check if <paramref name="str"/> contains <paramref name="value"/>, ignoring case and white space.
	/// </summary>
	private static bool FuzzyContains( string str, string value )
	{
		return CultureInfo.InvariantCulture.CompareInfo.IndexOf( str, value, CompareOptions.IgnoreCase | CompareOptions.IgnoreSymbols ) >= 0;
	}

	void UpdateFromReferenced( string query )
	{
		var packages = AssetSystem.GetReferencedPackages();

		AssetList.Clear();
		FacetLayout.Clear( true );

		foreach ( var package in packages.Where( x => FuzzyContains( x.Title, query ) ) )
		{
			if ( package.TypeName == "game" ) continue;
			string assetPath = package.GetMeta<string>( "PrimaryAsset" );
			var a = AssetSystem.FindByPath( assetPath );

			if ( a is null )
				continue;

			AssetList.AddItem( new PackageEntry( package ) );
		}
	}

	async Task UpdateFromCloud( string q )
	{
		AssetList.Clear();

		if ( FilterAssetTypes?.Count > 0 )
		{
			q += $" type:{GetAssetType( FilterAssetTypes.First().FileExtension )}";
		}

		foreach ( var dropdown in FacetDropDowns )
		{
			if ( dropdown.Selected != null && !q.Contains( $"{dropdown.Facet.Name}:" ) )
				q += $" {dropdown.GetFilter()}";
		}

		q += $" sort:{CurrentOrderMode}";
		q = q.Trim();

		await FetchPackages( q, 400, 0 );

		if ( !IsValid )
			return;
	}

	List<FacetDropdown> FacetDropDowns = new();

	void ClearFacets()
	{
		foreach ( var dropdown in FacetDropDowns )
		{
			dropdown.Selected = null;
		}
	}

	string GetAssetType( string extension )
	{
		switch ( extension )
		{
			case "vmap":
				return "map";
			case "vmdl":
				return "model";
			case "vmat":
				return "material";
			default:
				return extension;
		}
	}

	string _lastQuery = "";
	int _lastTake = 0;
	int _lastSkip = 0;
	bool _lastQueryReachedEnd = false;
	async Task FetchPackages( string q, int take, int skip )
	{
		tokenSource?.Cancel();
		tokenSource = new CancellationTokenSource();

		if ( !IsValid )
			return;

		var token = tokenSource.Token;
		var found = await Package.FindAsync( q, take, skip, token );

		_lastQuery = q;
		_lastTake = take;
		_lastSkip = skip;
		_lastQueryReachedEnd = found.Packages.Length < take;

		if ( !IsValid || token.IsCancellationRequested || found == null )
			return;

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


		// build the sidebar facets
		FacetLayout.Clear( true );

		var facets = found.Facets;

		{
			// Add a type facet for the Browse tab
			Dictionary<string, int> typeDict = new();
			foreach ( var package in found.Packages )
			{
				if ( !typeDict.ContainsKey( package.TypeName ) )
					typeDict[package.TypeName] = 0;
				typeDict[package.TypeName]++;
			}
			var entries = new List<Package.Facet.Entry>();
			foreach ( var facet in typeDict )
			{
				var entry = new Package.Facet.Entry( facet.Key, facet.Key, "category", facet.Value, new() );
				entries.Add( entry );
			}
			var realFacet = new Package.Facet( "type", "Type", entries.ToArray() );
			var newFacets = new List<Package.Facet> { realFacet };
			newFacets.AddRange( facets );
			facets = newFacets.ToArray();
		}

		if ( facets.Length > 0 )
		{
			var parts = q.Split( ' ' ).Select( x => x.Split( ':' ) );
			Dictionary<string, string> facetTags = parts.Where( x => x.Length > 1 )
				.GroupBy( x => x[0] ) // avoid duplicates
				.ToDictionary( g => g.Key, g => g.First()[1] );

			var newDropdowns = new List<FacetDropdown>();

			foreach ( var facet in facets )
			{
				if ( BaseQuery?.Contains( $"{facet.Name}:" ) ?? false )
					continue;

				string selection = null;
				facetTags.TryGetValue( facet.Name, out selection );

				var dd = FacetLayout.Add( new FacetDropdown( facet, selection, this ) );
				dd.OnChanged += UpdateAssetList;

				if ( facet.Name == "type" && FilterAssetTypes is not null )
					dd.Enabled = false;

				newDropdowns.Add( dd );
			}

			FacetDropDowns = newDropdowns;
		}

		//
		// Add them all to the list
		//
		foreach ( var item in found.Packages )
		{
			if ( item.TypeName == "game" || item.TypeName == "library" ) continue;
			AssetList.AddItem( new PackageEntry( item ) );
		}

		if ( !string.IsNullOrEmpty( lastSortColumn ) )
		{
			SortAssetList( lastSortColumn, lastSortAscending );
		}

		//
		// Get order modes
		//
		OrderMode.MouseLeftPress = () =>
		{
			var menu = new ContextMenu( this );

			foreach ( var orderMode in found.Orders )
			{
				var option = menu.AddOption( orderMode.Title, orderMode.Icon, () =>
				{
					CurrentOrderMode = orderMode.Name;
					OrderMode.Icon = orderMode.Icon;

					UpdateAssetList();
				} );

				option.Checkable = true;
				option.Checked = orderMode.Name == CurrentOrderMode;
			}

			menu.OpenAt( OrderMode.ScreenRect.BottomLeft, false );
		};
	}

	public void LoadMore()
	{
		if ( _lastQueryReachedEnd || BaseQuery.StartsWith( "@referenced" ) )
			return;
		var skip = _lastSkip + _lastTake;
		_ = FetchPackages( _lastQuery, _lastTake, skip );
	}

	protected override void OnKeyPress( KeyEvent e )
	{
		if ( e.Key == KeyCode.F5 )
		{
			UpdateAssetList();
			return;
		}

		base.OnKeyPress( e );
	}

	string SettingsCookie { get; set; }
	bool SuppressSaveSettings { get; set; }

	/// <summary>
	/// Load settings saved by <see cref="SaveSettings"/>.
	/// </summary>
	public virtual void LoadSettings()
	{
		if ( string.IsNullOrEmpty( SettingsCookie ) )
			return;

		SuppressSaveSettings = true;

		ViewModeType = (AssetListViewMode)ProjectCookie.Get<int>( $"{SettingsCookie}.ViewMode", (int)AssetList.ViewMode );
		ShowJunkFiles = ProjectCookie.Get<bool>( $"{SettingsCookie}.ShowJunkFiles", ShowJunkFiles );
		HideNonAssets = ProjectCookie.Get<bool>( $"{SettingsCookie}.HideNonAssets", HideNonAssets );
		ShowRecursiveFiles = ProjectCookie.Get<bool>( $"{SettingsCookie}.ShowRecursiveFiles", ShowRecursiveFiles );

		SuppressSaveSettings = false;
	}

	/// <summary>
	/// Save asset browser user settings, such as view mode, <see cref="ShowJunkFiles"/> and <see cref="ShowRecursiveFiles"/>.
	/// The save slot is separate for each asset browser filter combination set in the constructor.
	/// </summary>
	public virtual void SaveSettings()
	{
		if ( string.IsNullOrEmpty( SettingsCookie ) )
			return;

		if ( SuppressSaveSettings )
			return;

		ProjectCookie.Set<int>( $"{SettingsCookie}.ViewMode", (int)AssetList.ViewMode );
		ProjectCookie.Set<bool>( $"{SettingsCookie}.ShowJunkFiles", ShowJunkFiles );
		ProjectCookie.Set<bool>( $"{SettingsCookie}.HideNonAssets", HideNonAssets );
		ProjectCookie.Set<bool>( $"{SettingsCookie}.ShowRecursiveFiles", ShowRecursiveFiles );
	}

	/// <summary>
	/// Focus on this package, make it selected, when results are recieved
	/// </summary>
	public async void FocusOnPackage( Package package, bool skipEvents = false )
	{
		if ( package is null ) return;

		await ResultsTask;
		await Task.Yield();

		var entry = AssetList.Items.OfType<PackageEntry>().Where( x => x.Package.Ident == package.Ident ).FirstOrDefault();
		AssetList.SelectItem( entry, skipEvents: skipEvents );
		AssetList.ScrollTo( entry );
	}

	public void AddPin( string filter )
	{
		// TODO
		throw new NotImplementedException();
	}
}

public class FacetDropdown : Widget
{
	public Package.Facet Facet;
	public bool IncludeChildren = false;
	public bool ShowCount = true;

	public Package.Facet.Entry Selected = null;
	public Action OnChanged;

	public FacetDropdown( Package.Facet facet, string selection, Widget parent = null ) : base( parent )
	{
		Facet = facet;
		Cursor = CursorShape.Finger;

		Layout = Layout.Row();
		Layout.Spacing = 2;

		Selected = selection is null ? null : Facet.Entries.FirstOrDefault( x => x.Name == selection );
	}

	public string GetFilter()
	{
		if ( Selected == null )
			return "";

		var stringBuilder = new StringBuilder();

		if ( Selected != null )
		{
			stringBuilder.Append( $"{Facet.Name}:{Selected.Name} " );
		}

		return stringBuilder.ToString();
	}

	protected override Vector2 SizeHint()
	{
		return new Vector2( 128, 24 );
	}

	protected override void OnPaint()
	{
		var color = Enabled && Paint.HasMouseOver ? Theme.Blue : Theme.TextControl;

		if ( !Enabled )
			color = color.WithAlpha( 0.5f );

		var rect = LocalRect;

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

		Paint.SetBrush( Theme.ControlBackground );
		Paint.DrawRect( rect, Theme.ControlRadius );

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

		rect = rect.Shrink( 8, 0 );

		Paint.SetPen( color );

		if ( Selected == null )
		{
			Paint.DrawText( rect, Facet.Title, TextFlag.LeftCenter );
		}
		else
		{
			Paint.DrawIcon( rect, Selected.Icon, 16, TextFlag.LeftCenter );

			rect.Left += 20;
			Paint.DrawText( rect, $"{Facet.Title}: {Selected.Title}", TextFlag.LeftCenter );
		}

		Paint.SetPen( color );
		Paint.DrawIcon( rect, "Arrow_Drop_Down", 17, TextFlag.RightCenter );
	}

	protected override void OnMouseReleased( MouseEvent e )
	{
		if ( !Enabled ) return;

		OpenMenu();
	}

	void OpenMenu()
	{
		var menu = new ContextMenu( null )
		{
			Layout = Layout.Column(),
			MinimumWidth = ScreenRect.Width,
			MaximumWidth = ScreenRect.Width
		};

		foreach ( var entry in Facet.Entries )
		{
			if ( entry.Name == "game" || entry.Name == "library" )
				continue;

			AddEntry( menu, entry );
		}

		menu.Position = ScreenRect.BottomLeft;
		menu.Visible = true;
		menu.AdjustSize();
		menu.ConstrainToScreen();
		menu.OnPaintOverride = PaintMenuBackground;
	}

	void AddEntry( ContextMenu menu, Package.Facet.Entry entry, int level = 0 )
	{
		var title = entry.Title;
		if ( ShowCount ) title += $" ({entry.Count})";
		var op = menu.AddOption( title, entry.Icon );
		op.Checkable = true;
		op.Checked = Selected == entry;

		op.Toggled = ( b ) =>
		{
			if ( b )
				Selected = entry;
			else
				Selected = null;

			menu.Update();

			OnChanged?.Invoke();
		};

		if ( IncludeChildren )
		{
			foreach ( var child in entry.Children )
			{
				AddEntry( menu, child, level + 1 );
			}
		}
	}
	bool PaintMenuBackground()
	{
		Paint.SetBrushAndPen( Theme.ControlBackground );
		Paint.DrawRect( Paint.LocalRect, 0 );
		return true;
	}
}
