Editor/Tileset/TilesetEditor/MainWindow.cs
using Editor;
using Sandbox;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace SpriteTools.TilesetEditor;

[EditorForAssetType( "tileset" )]
[EditorApp( "Tileset Editor", "calendar_view_month", "Edit Tilesets" )]
public partial class MainWindow : DockWindow, IAssetEditor
{
	internal static List<MainWindow> OpenWindows = new();
	public bool CanOpenMultipleAssets => false;

	private readonly UndoStack _undoStack = new();
	public UndoStack UndoStack => _undoStack;
	bool _dirty = true;

	private Asset _asset;
	public TilesetResource Tileset;
	[Property]
	public List<TilesetResource.Tile> SelectedTiles
	{
		get => inspector?.tileList?.Selected?.Select( x => x?.Tile )?.ToList() ?? new();
		set
		{
			inspector.tileList.Selected.Clear();
			foreach ( var tile in value )
			{
				var control = inspector.tileList.Buttons.FirstOrDefault( x => x.Tile == tile );
				if ( control != null )
				{
					inspector.tileList.Selected.Add( control );
				}
			}
		}
	}

	ToolBar toolBar;
	internal Inspector.Inspector inspector;
	internal Preview.Preview preview;

	Option _undoMenuOption;
	Option _redoMenuOption;

	public MainWindow ()
	{
		DeleteOnClose = true;

		Size = new Vector2( 1280, 720 );
		Tileset = new TilesetResource();

		SetWindowIcon( "emoji_emotions" );

		RestoreDefaultDockLayout();
		OpenWindows.Add( this );
	}

	public override void OnDestroyed ()
	{
		base.OnDestroyed();
		OpenWindows.Remove( this );
	}

	public void AssetOpen ( Asset asset )
	{
		Open( "", asset );
		Show();
	}

	public void SelectMember ( string memberName )
	{

	}

	void UpdateWindowTitle ()
	{
		Title = ( $"{_asset?.Name ?? "Untitled Tileset"} - Tileset Editor" ) + ( _dirty ? "*" : "" );
	}

	public void RebuildUI ()
	{
		MenuBar.Clear();

		{
			var file = MenuBar.AddMenu( "File" );
			file.AddOption( "New", "common/new.png", () => New(), "editor.new" ).StatusTip = "New Tileset";
			file.AddOption( "Open", "common/open.png", () => Open(), "editor.open" ).StatusTip = "Open Tileset";
			file.AddOption( "Save", "common/save.png", () => Save(), "editor.save" ).StatusTip = "Save Tileset";
			file.AddOption( "Save As...", "common/save.png", () => Save( true ), "editor.save-as" ).StatusTip = "Save Tileset As...";
			file.AddSeparator();
			file.AddOption( new Option( "Exit" ) { Triggered = Close } );
		}

		{
			var edit = MenuBar.AddMenu( "Edit" );
			_undoMenuOption = edit.AddOption( "Undo", "undo", () => Undo(), "editor.undo" );
			_redoMenuOption = edit.AddOption( "Redo", "redo", () => Redo(), "editor.redo" );

			// edit.AddSeparator();
			// edit.AddOption( "Cut", "common/cut.png", CutSelection, "Ctrl+X" );
			// edit.AddOption( "Copy", "common/copy.png", CopySelection, "Ctrl+C" );
			// edit.AddOption( "Paste", "common/paste.png", PasteSelection, "Ctrl+V" );
			// edit.AddOption( "Select All", "select_all", SelectAll, "Ctrl+A" );
		}

		{
			var view = MenuBar.AddMenu( "View" );

			view.AboutToShow += () => OnViewMenu( view );
		}

		CreateToolBar();

	}

	private void OnViewMenu ( Menu view )
	{
		view.Clear();
		view.AddOption( "Restore To Default", "settings_backup_restore", RestoreDefaultDockLayout );
		view.AddSeparator();

		foreach ( var dock in DockManager.DockTypes )
		{
			var o = view.AddOption( dock.Title, dock.Icon );
			o.Checkable = true;
			o.Checked = DockManager.IsDockOpen( dock.Title );
			o.Toggled += ( b ) => DockManager.SetDockState( dock.Title, b );
		}
	}

	protected override void RestoreDefaultDockLayout ()
	{
		inspector = new Inspector.Inspector( this );
		preview = new Preview.Preview( this );
		// Timeline = new Timeline.Timeline(this);
		// var animationList = new AnimationList.AnimationList(this);

		DockManager.Clear();
		DockManager.RegisterDockType( "Inspector", "edit", () => inspector = new Inspector.Inspector( this ) );
		DockManager.RegisterDockType( "Preview", "emoji_emotions", () => preview = new Preview.Preview( this ) );
		// DockManager.RegisterDockType("Animations", "directions_walk", () => new AnimationList.AnimationList(this));
		// DockManager.RegisterDockType("Timeline", "view_column", () =>
		// {
		//     Timeline = new Timeline.Timeline(this);
		//     return Timeline;
		// });

		DockManager.AddDock( null, inspector, DockArea.Left, DockManager.DockProperty.HideOnClose );
		DockManager.AddDock( null, preview, DockArea.Right, DockManager.DockProperty.HideOnClose, split: 0.8f );

		// DockManager.AddDock(preview, Timeline, DockArea.Bottom, DockManager.DockProperty.HideOnClose, split: 0.2f);
		// DockManager.AddDock(inspector, animationList, DockArea.Bottom, DockManager.DockProperty.HideOnClose, split: 0.45f);

		DockManager.Update();

		RebuildUI();
	}

	void InitInspector ()
	{
		inspector.segmentedControl.SelectedIndex = ( ( Tileset?.Tiles?.Count ?? 0 ) == 0 ) ? 0 : 1;
	}

	void UpdateEverything ()
	{
		UpdateWindowTitle();
		inspector.UpdateControlSheet();
		inspector.UpdateSelectedSheet();
		preview.UpdateTexture( Tileset.FilePath );
	}

	[Shortcut( "editor.new", "CTRL+N", ShortcutType.Window )]
	public void New ()
	{
		PromptSave( () => CreateNew() );
	}

	public void CreateNew ()
	{
		var savePath = GetSavePath( "New 2D Tileset" );

		_asset = null;
		Tileset = AssetSystem.CreateResource( "tileset", savePath ).LoadResource<TilesetResource>();
		_dirty = false;
		_undoStack.Clear();

		InitInspector();
		UpdateEverything();
	}

	[Shortcut( "editor.open", "CTRL+O", ShortcutType.Window )]
	public void Open ()
	{
		var fd = new FileDialog( null )
		{
			Title = "Open 2D Tileset",
			DefaultSuffix = ".tileset"
		};

		fd.SetNameFilter( "2D Tileset (*.tileset)" );

		if ( !fd.Execute() ) return;

		PromptSave( () => Open( fd.SelectedFile ) );
	}

	public void Open ( string path, Asset asset = null )
	{
		if ( !string.IsNullOrEmpty( path ) )
		{
			asset ??= AssetSystem.FindByPath( path );
		}
		if ( asset == null ) return;

		if ( asset == _asset )
		{
			Focus();
			return;
		}

		var tileset = asset.LoadResource<TilesetResource>();
		if ( tileset == null )
		{
			Log.Warning( $"Failed to load tileset from {asset.RelativePath}" );
			return;
		}

		StateCookie = "tileset-editor-window-" + tileset.ResourceId;

		_asset = asset;
		_dirty = false;
		_undoStack.Clear();

		Tileset = tileset;
		var firstTile = Tileset.Tiles?.FirstOrDefault();
		if ( firstTile is not null )
			inspector?.tileList?.Selected?.Add( inspector.tileList.Buttons.FirstOrDefault( x => x.Tile == firstTile ) );

		InitInspector();
		UpdateEverything();
	}

	private void Restore ()
	{
		var path = _asset?.AbsolutePath;
		if ( string.IsNullOrEmpty( path ) )
		{
			_dirty = false;
			return;
		}

		var contents = File.ReadAllText( path );
		ReloadFromString( contents );

		_dirty = false;
	}

	[Shortcut( "editor.save", "CTRL+S", ShortcutType.Window )]
	public bool Save ( bool saveAs = false )
	{
		var savePath = ( _asset == null || saveAs ) ? GetSavePath() : _asset.AbsolutePath;
		if ( string.IsNullOrWhiteSpace( savePath ) ) return false;

		if ( saveAs )
		{
			// If we're saving as, we want to register the new asset
			_asset = null;
		}

		// Register the asset if we haven't already
		_asset ??= AssetSystem.CreateResource( "tileset", savePath );
		_asset?.SaveToDisk( Tileset );
		_dirty = false;
		UpdateWindowTitle();

		if ( _asset == null )
		{
			Log.Warning( $"Failed to register asset at path {savePath}" );
			return false;
		}

		MainAssetBrowser.Instance?.Local?.UpdateAssetList();
		TileAtlas.ClearCache( Tileset );

		return true;
	}

	[Shortcut( "editor.save-as", "CTRL+SHIFT+S", ShortcutType.Window )]
	private void SaveAs ()
	{
		Save( true );
	}

	[EditorEvent.Frame]
	void Frame ()
	{
		_undoOption.Enabled = _undoStack.CanUndo;
		_redoOption.Enabled = _undoStack.CanRedo;
		_undoMenuOption.Enabled = _undoStack.CanUndo;
		_redoMenuOption.Enabled = _undoStack.CanRedo;

		_undoOption.Text = _undoStack.UndoName ?? "Undo";
		_redoOption.Text = _undoStack.RedoName ?? "Redo";
		_undoMenuOption.Text = _undoStack.UndoName ?? "Undo";
		_redoMenuOption.Text = _undoStack.RedoName ?? "Redo";

		_undoOption.StatusTip = _undoStack.UndoName ?? "Undo";
		_redoOption.StatusTip = _undoStack.RedoName ?? "Redo";
		_undoMenuOption.StatusTip = _undoStack.UndoName ?? "Undo";
		_redoMenuOption.StatusTip = _undoStack.RedoName ?? "Redo";
	}

	protected override bool OnClose ()
	{
		if ( _dirty )
		{
			var confirm = new PopupWindow(
				"Save Current Tileset", "The open tileset has unsaved changes. Would you like to save now?", "Cancel",
				new Dictionary<string, System.Action>()
				{
					{ "No", () => { Restore(); Close(); } },
					{ "Yes", () => { Save(); Close(); } }
				}
			);

			confirm.Show();

			return false;
		}

		return true;
	}

	string GetSavePath ( string title = "Save Tileset" )
	{
		var lastDirectory = EditorCookie.GetString( "LastSaveTilesetLocation", "" );
		var fd = new FileDialog( null )
		{
			Title = title,
			Directory = lastDirectory,
			DefaultSuffix = $".tileset"
		};

		fd.SelectFile( "untitled.tileset" );
		fd.SetFindFile();
		fd.SetModeSave();
		fd.SetNameFilter( "2D Tileset (*.tileset)" );
		if ( !fd.Execute() ) return null;

		var selectedFile = fd.SelectedFile;
		EditorCookie.SetString( "LastSaveTilesetLocation", System.IO.Path.GetDirectoryName( selectedFile ) );
		return selectedFile;
	}

	internal void CreateTile ( int x, int y, bool add = false )
	{
		var tileName = $"Tile {x},{y}";

		PushUndo( $"Create Tile \"{tileName}\"" );
		var tile = new TilesetResource.Tile( new Vector2Int( x, y ), 1 );
		Tileset.AddTile( tile );

		if ( Tileset.Tiles.Count == 1 )
		{
			Tileset.CurrentTileSize = Tileset.TileSize;
			Tileset.CurrentTextureSize = (Vector2Int)preview.TextureSize;
			inspector.UpdateControlSheet();
		}
		else
		{
			var control = new TilesetTileControl( inspector.tileList, tile );
			inspector.tileList.content.Add( control );
			inspector.tileList.Buttons.Add( control );
		}

		SelectTile( tile, add );
		PushRedo();
		SetDirty();
	}

	internal void SelectTile ( TilesetResource.Tile tile, bool add = false )
	{
		var btn = inspector.tileList.Buttons.FirstOrDefault( x => x.Tile == tile );
		if ( add )
		{
			if ( inspector.tileList.Selected.Contains( btn ) )
				inspector.tileList.Selected.Remove( btn );
			else
				inspector.tileList.Selected.Add( btn );
		}
		else
		{
			inspector.tileList.Selected.Clear();
			inspector.tileList.Selected.Add( btn );
		}
		inspector.UpdateSelectedSheet();
	}

	internal void DeleteTile ( TilesetResource.Tile tile )
	{
		var tileName = tile.Name;
		if ( string.IsNullOrEmpty( tileName ) ) tileName = $"Tile {tile.Position}";

		PushUndo( $"Delete Tile \"{tileName}\"" );
		bool isSelected = inspector.tileList.Selected.Any( x => x.Tile == tile );
		Tileset.RemoveTile( tile );

		if ( isSelected ) SelectTile( Tileset.Tiles?.FirstOrDefault() ?? null );
		PushRedo();

		if ( Tileset.Tiles.Count == 0 )
		{
			inspector.UpdateControlSheet();
		}
		else
		{
			var btns = inspector.tileList.Buttons.ToList();
			foreach ( var btn in btns )
			{
				if ( btn.Tile == tile )
				{
					inspector.tileList.Buttons.Remove( btn );
					btn.Destroy();
				}
			}
		}
		SetDirty();
	}

	internal void GenerateTiles ()
	{
		if ( Tileset is null ) return;

		PushUndo( "Generate Tiles" );
		foreach ( var tile in Tileset.Tiles.ToList() )
		{
			Tileset.RemoveTile( tile );
		}
		Tileset.CurrentTileSize = Tileset.TileSize;
		Tileset.CurrentTextureSize = (Vector2Int)preview.TextureSize;

		int x = 0;
		int y = 0;
		int framesPerRow = (int)preview.TextureSize.x / Tileset.TileSize.x;
		int framesPerHeight = (int)preview.TextureSize.y / Tileset.TileSize.y;
		var pixels = Texture.LoadFromFileSystem( Tileset.FilePath, Editor.FileSystem.Mounted ).GetPixels();

		while ( y < framesPerHeight )
		{
			while ( x < framesPerRow )
			{
				var hasPixel = false;
				for ( var xx = 0; xx < Tileset.TileSize.x; xx++ )
				{
					for ( var yy = 0; yy < Tileset.TileSize.y; yy++ )
					{
						var tx = x * Tileset.TileSize.x + xx;
						var ty = y * Tileset.TileSize.y + yy;
						int pixelIndex = (int)( ty * preview.TextureSize.x + tx );
						if ( pixels[pixelIndex].a > 0 )
						{
							hasPixel = true;
							break;
						}
					}
					if ( hasPixel ) break;
				}
				if ( hasPixel )
				{
					Tileset.AddTile( new TilesetResource.Tile( new Vector2Int( x, y ), 1 ) );
				}
				x++;
			}
			x = 0;
			y++;
		}
		PushRedo();
		SetDirty();
		UpdateEverything();
	}

	internal void DeleteAllTiles ()
	{
		if ( Tileset is null ) return;

		PushUndo( "Delete All Tiles" );
		Tileset.Tiles ??= new List<TilesetResource.Tile>();
		Tileset.Tiles?.Clear();
		PushRedo();
		SetDirty();
		UpdateEverything();
	}

	void PromptSave ( Action action )
	{
		if ( !_dirty )
		{
			action?.Invoke();
			return;
		}

		var confirm = new PopupWindow(
			"Save Current Tileset", "The open tileset has unsaved changes. Would you like to save before continuing?", "Cancel",
			new Dictionary<string, Action>
			{
				{ "No", () => {
					action?.Invoke();
				} },
				{ "Yes", () => {
					if (Save()) action?.Invoke();
				}}
			} );
		confirm.Show();
	}

	internal void SetDirty ()
	{
		_dirty = true;
		UpdateWindowTitle();
	}

	public void PushUndo ( string name, string buffer = "" )
	{
		if ( string.IsNullOrEmpty( buffer ) ) buffer = Tileset.SerializeString();
		_undoStack.PushUndo( name, buffer );
	}

	public void PushRedo ()
	{
		_undoStack.PushRedo( Tileset.SerializeString() );
		SetDirty();
	}

	[Shortcut( "editor.undo", "CTRL+Z", ShortcutType.Window )]
	public void Undo ()
	{
		if ( _undoStack.Undo() is UndoOp op )
		{
			ReloadFromString( op.undoBuffer );
			Sound.Play( "ui.navigate.back" );
		}
		else
		{
			Sound.Play( "ui.navigate.deny" );
		}
	}

	private void SetUndoLevel ( int level )
	{
		if ( _undoStack.SetUndoLevel( level ) is UndoOp op )
		{
			ReloadFromString( op.undoBuffer );
		}
	}

	[Shortcut( "editor.redo", "CTRL+Y", ShortcutType.Window )]
	public void Redo ()
	{
		if ( _undoStack.Redo() is UndoOp op )
		{
			ReloadFromString( op.redoBuffer );
			Sound.Play( "ui.navigate.forward" );
		}
		else
		{
			Sound.Play( "ui.navigate.deny" );
		}
	}

	internal void ReloadFromString ( string buffer )
	{
		Tileset.DeserializeString( buffer );

		SetDirty();
		UpdateEverything();
	}

	private Option _undoOption;
	private Option _redoOption;

	private void CreateToolBar ()
	{
		toolBar?.Destroy();
		toolBar = new ToolBar( this, "TilesetEditorToolbar" );
		AddToolBar( toolBar, ToolbarPosition.Top );

		toolBar.AddOption( "New", "common/new.png", New ).StatusTip = "New Tileset";
		toolBar.AddOption( "Open", "common/open.png", Open ).StatusTip = "Open Tileset";
		toolBar.AddOption( "Save", "common/save.png", () => Save() ).StatusTip = "Save Tileset";

		toolBar.AddSeparator();

		_undoOption = toolBar.AddOption( "Undo", "undo", Undo );
		_redoOption = toolBar.AddOption( "Redo", "redo", Redo );

		toolBar.AddSeparator();

		toolBar.AddSeparator();

		_undoOption.Enabled = false;
		_redoOption.Enabled = false;
	}
}