using System.IO;
using System.Text;
using System.Threading;

namespace Editor;

/// <summary>
/// A list of assets with filtering options.
/// </summary>
public partial class AssetBrowser : Widget, IBrowser, AssetSystem.IEventListener
{

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

	/// <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 int SelectedCount => AssetList.Selection.Count;
	public List<T> GetSelected<T>() => AssetList.Selection.OfType<T>().ToList();

	public PathWidget Path;
	public SearchWidget Search;

	ToolButton ViewMode;
	ChipsWidget Chips;
	ListHeader ListHeader;
	string lastSortColumn = "";
	bool lastSortAscending = true;

	/// <summary>
	/// Internal left side panel that lists all local projects and other asset locations.
	/// </summary>
	protected AssetLocations AssetLocations;

	/// <summary>
	/// Asset has been clicked
	/// </summary>
	public Action<Asset> OnAssetHighlight;

	/// <summary>
	/// Asset has been clicked
	/// </summary>
	public Action<Asset[]> OnAssetsHighlight;

	/// <inheritdoc cref="AssetList.OnHighlight"/> 
	public Action<IEnumerable<IAssetListEntry>> OnHighlight;

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

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

	/// <summary>
	/// Asset has been selected (double click, or enter)
	/// </summary>
	public Action<string> OnFileSelected;

	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;

	private HistoryStack<string> History = new();

	protected virtual string CookieKey => "AssetBrowser";

	/// <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;

			watcher.IncludeSubdirectories = value;

			UpdateAssetList();
			SaveSettings();
		}
	}

	/// <summary>
	/// View mode, i.e. thumbs, list or whatever else.
	/// </summary>
	public AssetListViewMode ViewModeType
	{
		get;
		set
		{
			field = value;
			AssetList.ViewMode = field;
			UpdateViewModeIcon();
			SaveSettings();
		}
	}

	private FileSystemWatcher watcher;

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

	}

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

		Layout = Layout.Row();

		FilterAssetTypes = assetTypeFilters;

		Path = new PathWidget( this );
		Path.MinimumHeight = Theme.RowHeight;
		Path.OnPathEdited += ( path ) => NavigateTo( path );

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

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

		AssetList = new AssetList( this );
		AssetList.Browser = this;
		AssetList.OnViewModeChanged += () => { UpdateViewModeIcon(); SaveSettings(); };
		AssetList.OnHighlight = ( entries ) =>
		{
			var assets = entries.OfType<AssetEntry>().ToList();
			if ( assets.Count == entries.Count() )
			{
				if ( assets.Count > 1 )
				{
					OnAssetsHighlight?.Invoke( assets.Select( x => x.Asset ).ToArray() );
				}
				else
				{
					OnAssetHighlight?.Invoke( assets.First().Asset );
				}
			}

			OnHighlight?.Invoke( entries );
		};

		Chips = new ChipsWidget();
		Chips.OnActiveChanged = UpdateAssetList;

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

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

		CreateLocations();

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

		Layout.Add( splitter );

		BuildToolbar( toolbar );
		UpdateToolbarOptions();

		if ( FilterAssetTypes is not null )
		{
			Search.AssetTypes.ActiveTags = FilterAssetTypes.SelectMany( x => x.FileExtensions ).ToHashSet();
			Search.AssetTypes.Rebuild();
			Search.AssetTypes.Enabled = false;
		}

		watcher = new FileSystemWatcher();
		watcher.Changed += OnExternalChanges;
		watcher.Created += OnExternalChanges;
		watcher.Deleted += OnExternalChanges;
		watcher.Renamed += OnExternalChanges;

		SetInitialLocation();
		RefreshCookies();

		if ( History.Count == 0 && CurrentLocation != null )
		{
			History.Add( CurrentLocation.Path );
		}
	}

	protected virtual void SetInitialLocation()
	{

	}

	protected virtual void CreateLocations()
	{

	}

	public override void OnDestroyed()
	{
		base.OnDestroyed();
		watcher?.Dispose();
	}

	private void OnExternalChanges( object sender, FileSystemEventArgs e )
	{
		MainThread.Queue( UpdateAssetList );
	}

	private void GoBack()
	{
		if ( !History.CanGoBack() )
			return;

		NavigateTo( History.GoBack(), false );
	}

	private void GoForward()
	{
		if ( !History.CanGoForward() )
			return;

		NavigateTo( History.GoForward(), false );
	}

	protected override void OnMousePress( MouseEvent e )
	{
		base.OnMousePress( e );

		if ( e.Button == MouseButtons.Back && History.CanGoBack() )
		{
			GoBack();
			return;
		}

		if ( e.Button == MouseButtons.Forward && History.CanGoForward() )
		{
			GoForward();
			return;
		}
	}

	private void RefreshCookies()
	{
		SettingsCookie = GetCookieKey();
		History.StateCookie = SettingsCookie;

		GlobalSettingsCookie = "AssetBrowser.Global";

		if ( History.Count > 0 )
		{
			NavigateTo( History.Current, false );
		}

		LoadSettings();

		UpdateAssetList();
	}

	private string GetCookieKey()
	{
		StringBuilder stringBuilder = new( CookieKey );

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

		return stringBuilder.ToString();
	}

	/// <summary>
	/// Go up once in the folder structure.
	/// </summary>
	public void OpenParentFolder()
	{
		if ( CurrentLocation == null ) return;
		if ( !CurrentLocation.CanGoUp() ) return;

		string path = CurrentLocation.Path.NormalizeFilename( false, false );
		string parentPath = path.TrimEnd( '/' ); // remove trailing slash if any
		int i = path.LastIndexOf( '/' );
		parentPath = (i > 0) ? path[..(i + 1)] : null;

		NavigateTo( parentPath );
	}

	public void NavigateTo( string absolutePath, bool addToHistory = true )
	{
		if ( string.IsNullOrEmpty( absolutePath ) )
			return;

		if ( !Location.TryParse( absolutePath.NormalizeFilename( false, false ), out var location ) )
			return;

		NavigateTo( location, addToHistory );
	}

	public void NavigateTo( Location location, bool addToHistory = true )
	{
		if ( !location.IsValid() )
			return;

		CurrentLocation = location;
		UpdateAssetList();

		if ( location is DiskLocation )
		{
			watcher.Path = CurrentLocation.Path;
			watcher.EnableRaisingEvents = true;
		}
		else
		{
			watcher.EnableRaisingEvents = false;
		}

		if ( addToHistory )
		{
			History.Add( CurrentLocation.Path );
		}

		OnFolderOpened?.Invoke();
		AssetLocations.SelectFolder( CurrentLocation.Path );
	}

	void AssetSystem.IEventListener.OnAssetSystemChanges()
	{
		UpdateAssetList();
	}


	[Event( "content.changed" )]
	void OnContentChanged( string file )
	{
		UpdateAssetList();
	}

	CancellationTokenSource refreshToken;
	Task<bool> RefreshTask;

	/// <summary>
	/// Update the list of displayed assets.
	/// </summary>
	public virtual void UpdateAssetList()
	{
		if ( CurrentLocation is null )
			return;

		refreshToken?.Cancel();
		refreshToken = new CancellationTokenSource();

		if ( !CurrentLocation.IsValid() )
		{
			// folder must've been deleted, try go somewhere safe
			var location = new DiskLocation( Sandbox.Project.Current.GetAssetsPath() );
			if ( location.IsValid() )
			{
				NavigateTo( location, false );
			}
			return;
		}

		bool recursive = ShowRecursiveFiles || !Search.IsEmpty;
		AssetList.FullPathMode = recursive;

		Path.Rebuild();
		RefreshTask = UpdateAssetListAsync( recursive, refreshToken.Token );
	}

	private async Task<bool> UpdateAssetListAsync( bool recursive, CancellationToken token )
	{
		List<object> items = new List<object>();
		var tagCounts = new Dictionary<string, int>();

		AssetList.Clear();

		await Task.Run( () =>
		{
			//
			// Collect directories
			//
			if ( !recursive )
			{
				string path = CurrentLocation.Path;
				foreach ( var directory in CurrentLocation.GetDirectories() )
				{
					if ( token.IsCancellationRequested )
						return;

					var di = new DirectoryEntry( directory.Path );

					// Hide hidden directories
					if ( di.DirectoryInfo.Exists && di.DirectoryInfo.Attributes.HasFlag( FileAttributes.Hidden ) )
						continue;

					// Hide obj/ folder in code directories
					if ( EditorUtility.IsCodeFolder( path ) && di.Name.Equals( "obj", StringComparison.OrdinalIgnoreCase ) )
						continue;

					items.Add( di );
				}
			}

			//
			// Collect files
			//

			var files = Search.Filter( CurrentLocation, token, recursive );
			if ( files is null ) return;

			foreach ( var file in files )
			{
				if ( token.IsCancellationRequested )
					return;

				if ( Chips.ShouldFilter( file ) )
					continue;

				if ( !ShowJunkFiles )
				{
					//
					// Filter out unwanted crap
					//
					if ( file.Name.Contains( ".generated", StringComparison.OrdinalIgnoreCase ) ||
						file.Name.EndsWith( ".meta", StringComparison.OrdinalIgnoreCase ) )
					{
						continue;
					}
				}

				if ( file.Exists && file.Attributes.HasFlag( FileAttributes.Hidden ) )
					continue;

				var asset = AssetSystem.FindByPath( file.ToString() );

				//
				// Filter out compiled assets if we have a source asset
				//
				string sourcePath = asset?.GetSourceFile();
				if ( file.Name.EndsWith( "_c" ) && !string.IsNullOrEmpty( sourcePath ) )
				{
					// ( but only if the extensions are similar and would both be shown in this filter, eg so we don't hide .sounds because we have a .wav etc )
					if ( Search.AssetTypes.ActiveTags.Count == 0 || file.Extension.Contains( System.IO.Path.GetExtension( sourcePath ) ) )
						continue;
				}

				if ( asset == null && HideNonAssets )
					continue;

				items.Add( new AssetEntry( file, asset ) );

				if ( asset != null )
				{
					foreach ( var tag in asset.Tags )
					{
						tagCounts[tag] = tagCounts.GetValueOrDefault( tag ) + 1;
					}
				}
			}
		}, token );

		if ( token.IsCancellationRequested )
			return false;

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

		Chips.ClearButKeepActive();

		foreach ( var tagPair in tagCounts )
		{
			if ( token.IsCancellationRequested )
				return false;

			var tag = FindTag( tagPair.Key );
			Chips.AddOption( tag );
		}

		return true;
	}

	private void SortAssetList( string sortBy, bool ascending )
	{
		List<object> items;

		switch ( sortBy )
		{
			case "Name":
				items = AssetList.Items.OrderBy( x =>
				{
					string sort = "";
					if ( x is AssetEntry ae )
					{
						sort = ae.Name.ToLower();
					}
					else if ( x is DirectoryEntry de )
					{
						// Directories always come first
						sort = (ListHeader.SortAscending ? "! " : "z ") + de.Name.ToLower();
					}
					return sort;
				} ).ToList();
				break;
			case "Date":
				items = AssetList.Items.OrderBy( x =>
				{
					var date = DateTimeOffset.Now;
					if ( x is AssetEntry ae )
					{
						date = ae.FileInfo?.LastWriteTime ?? date;
					}
					var sort = date.UtcTicks.ToString();
					if ( x is DirectoryEntry de )
					{
						// Directories always come first
						sort = (ListHeader.SortAscending ? "! " : "z ") + de.Name.ToLower();
					}
					return sort;
				} ).ToList();
				break;
			case "Type":
				items = AssetList.Items.OrderBy( x =>
				{
					var type = (x is AssetEntry ae) ? ae.TypeName : "";
					var sort = type.ToLower();
					if ( x is DirectoryEntry de )
					{
						// Directories always come first
						sort = (ListHeader.SortAscending ? "! " : "z ") + de.Name.ToLower();
					}
					return sort;
				} ).ToList();
				break;
			case "Size":
				items = AssetList.Items.OrderBy( x =>
				{
					long size = (x is AssetEntry ae) ? (ae.FileInfo?.Length ?? 0) : 0;
					if ( x is DirectoryEntry de )
					{
						// Directories always come first/last
						size = -long.MaxValue + de.Name.ToLong();
					}
					return size;
				} ).ToList();
				break;
			default:
				items = AssetList.Items.ToList();
				break;
		}

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

		lastSortColumn = sortBy;
		lastSortAscending = ascending;
	}

	private static AssetTagSystem.TagDefinition FindTag( string tagName )
	{
		foreach ( var tag in AssetTagSystem.All )
		{
			if ( tag.Tag.Equals( tagName ) )
			{
				return tag;
			}
		}

		return default;
	}

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

		base.OnKeyPress( e );
	}

	void OpenSettingsMenu( Rect source )
	{
		var menu = new ContextMenu( this );

		{
			var o = menu.AddOption( new Option( this, "Show Bullshit Files" ) );
			o.StatusTip = "Show All Files, even those that are useless for us to see and will make a big mess of the UI.";
			o.Checkable = true;
			o.Checked = ShowJunkFiles;
			o.Toggled = ( b ) =>
			{
				ShowJunkFiles = b;
				UpdateAssetList();
				SaveSettings();
			};
		}

		{
			var o = menu.AddOption( new Option( this, "Show Assets Only" ) );
			o.StatusTip = "Filters out file types that aren't part of our asset system";
			o.Checkable = true;
			o.Checked = HideNonAssets;
			o.Toggled = ( b ) =>
			{
				HideNonAssets = b;
				UpdateAssetList();
				SaveSettings();
			};
		}

		{
			var o = new Option( "Include Path in Search" );
			o.Checkable = true;
			o.Checked = AssetLocations.IncludePathNames;
			o.Toggled = ( b ) =>
			{
				AssetLocations.SetIncludePathNames( b );
				UpdateAssetList();
			};
			menu.AddOption( o );
		}

		menu.AddSeparator();

		{
			var o = menu.AddOption( new Option( this, "Split Left", "first_page" ) );
			o.Triggered = () =>
			{
				var ab = EditorWindow.DockManager.Create<MainAssetBrowser>();
				ab.Local.NavigateTo( CurrentLocation );
				ab.Local.ViewModeType = ViewModeType;
				EditorWindow.DockManager.AddDock( this, ab, DockArea.Left );
			};
		}

		{
			var o = menu.AddOption( new Option( this, "Split Right", "last_page" ) );
			o.Triggered = () =>
			{
				var ab = EditorWindow.DockManager.Create<MainAssetBrowser>();
				ab.Local.NavigateTo( CurrentLocation );
				ab.Local.ViewModeType = ViewModeType;
				EditorWindow.DockManager.AddDock( this, ab, DockArea.Right );
			};
		}

		menu.OpenAt( source.BottomLeft, false );
	}

	string SettingsCookie { get; set; }
	string GlobalSettingsCookie { 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( $"{SettingsCookie}.ViewMode", (int)AssetList.ViewMode );
		ShowJunkFiles = ProjectCookie.Get( $"{SettingsCookie}.ShowJunkFiles", ShowJunkFiles );
		HideNonAssets = ProjectCookie.Get( $"{SettingsCookie}.HideNonAssets", HideNonAssets );
		ShowRecursiveFiles = ProjectCookie.Get( $"{SettingsCookie}.ShowRecursiveFiles", ShowRecursiveFiles );

		if ( FilterAssetTypes is null )
		{
			Search.Value = ProjectCookie.Get( $"{SettingsCookie}.Search", Search.Value );

			if ( ProjectCookie.TryGet( $"{SettingsCookie}.ActiveTags", out string activeTags ) )
			{
				Search.AssetTypes.ActiveTags = activeTags.Split( ';' ).Where( x => x.Length > 0 ).ToHashSet();
			}
			if ( ProjectCookie.TryGet( $"{SettingsCookie}.ExcludedTags", out string excludedTags ) )
			{
				Search.AssetTypes.ExcludedTags = excludedTags.Split( ';' ).Where( x => x.Length > 0 ).ToHashSet();
			}

			Search.AssetTypes.Rebuild();
		}

		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( $"{SettingsCookie}.ViewMode", (int)AssetList.ViewMode );
		ProjectCookie.Set( $"{SettingsCookie}.ShowJunkFiles", ShowJunkFiles );
		ProjectCookie.Set( $"{SettingsCookie}.HideNonAssets", HideNonAssets );
		ProjectCookie.Set( $"{SettingsCookie}.ShowRecursiveFiles", ShowRecursiveFiles );

		if ( FilterAssetTypes is null )
		{
			ProjectCookie.Set( $"{SettingsCookie}.Search", Search.Value );
			ProjectCookie.Set( $"{SettingsCookie}.ActiveTags", string.Join( ';', Search.AssetTypes.ActiveTags ) );
			ProjectCookie.Set( $"{SettingsCookie}.ExcludedTags", string.Join( ';', Search.AssetTypes.ExcludedTags ) );
		}
	}

	/// <summary>
	/// Focus on this asset, make it selected
	/// </summary>
	public async void FocusOnAsset( Asset asset, bool skipEvents = false )
	{
		if ( asset is null ) return;

		var folder = System.IO.Path.GetDirectoryName( asset.AbsolutePath );

		EditorWindow.DockManager.RaiseDock( this );

		NavigateTo( folder );

		// wait for the list to (successfully) populate before selecting the item
		bool success = await RefreshTask;
		if ( !success )
			return;

		AssetEntry entry = AssetList.Items.OfType<AssetEntry>().Where( x => x.Asset?.RelativePath == asset.RelativePath ).FirstOrDefault();
		if ( entry is null )
			return;

		AssetList.SelectItem( entry, skipEvents: skipEvents );
		AssetList.ScrollTo( entry );
	}

	public void OnAssetCreated( Asset asset, string path )
	{
		var entry = AssetList.AddItem( new AssetEntry( new FileInfo( path ), asset ) );
		AssetList.SelectItem( entry, skipEvents: true );

		AssetList.OpenRenameFlyout( entry );
	}

	void AssetSystem.IEventListener.OnAssetTagsChanged()
	{
		UpdateToolbarOptions();
	}

	[Event( "tools.package.loaded" ), Event( "localaddons.changed", Priority = 9999 )]
	void UpdateToolbarOptions()
	{
		if ( Search.AssetTypes == null ) return;

		Search.AssetTypes.Options.Clear();

		TagPicker.Option SelectAssetTypes( AssetType x ) => new( x.FileExtension )
		{
			PixmapIcon = x.Icon16,
			Title = x.FriendlyName,
			Group = (string.IsNullOrEmpty( x.Category ) ? "Other" : x.Category),
			Column = 0,
			Count = () => AssetSystem.All.Where( y => y.AssetType == x ).Count(),
			Color = x.Color
		};

		TagPicker.Option SelectAssetTags( AssetTagSystem.TagDefinition x ) => new( $"tag:{x.Tag}" )
		{
			PixmapIcon = x.IconPixmap,
			Title = x.Title,
			Subtitle = x.Description,
			Group = "Tags",
			Column = 1,
			Count = () => AssetSystem.All.Where( y => y.Tags.Contains( x.Tag ) ).Count()
		};

		Search.AssetTypes.Options.AddRange( AssetType.All.Where( x => !x.HiddenByDefault && !x.IsGameResource ).Select( SelectAssetTypes ).OrderBy( x => x.Title ) );
		Search.AssetTypes.Options.AddRange( AssetType.All.Where( x => !x.HiddenByDefault && x.IsGameResource ).Select( SelectAssetTypes ).OrderBy( x => x.Title ) );
		Search.AssetTypes.Options.AddRange( AssetTagSystem.All.Where( x => x.AutoTag ).Select( SelectAssetTags ).OrderBy( x => x.Title ) );
		Search.AssetTypes.Options.AddRange( AssetTagSystem.All.Where( x => !x.AutoTag ).Select( SelectAssetTags ).OrderBy( x => x.Title ) );
	}

	[Event( "assetsystem.highlight" )]
	public void Highlight( string path )
	{
		var folder = System.IO.Path.GetDirectoryName( path );
		var absolutePath = FileSystem.Mounted.GetFullPath( folder );
		NavigateTo( absolutePath );

		foreach ( var item in AssetList.Items )
		{
			if ( item is AssetEntry entry && entry.FileInfo.FullName == path )
			{
				AssetList.SelectItem( item );
			}
		}
	}

	public virtual void AddPin( string filter )
	{

	}
}
