Editor/Sprite/SpriteEditor/MainWindow.cs
using Editor;
using Sandbox;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;

namespace SpriteTools.SpriteEditor;

[EditorForAssetType( "spr" )]
[EditorApp( "Sprite Editor", "emoji_emotions", "Edit 2D Sprites" )]
public partial class MainWindow : DockWindow, IAssetEditor
{
	public Action OnAssetLoaded;
	public Action OnTextureUpdate;
	public Action OnAnimationChanges;
	public Action OnAnimationSelected;
	public Action OnPlayPause;

	public bool CanOpenMultipleAssets => false;

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

	private Asset _asset;
	public SpriteResource Sprite;
	public SpriteAnimation SelectedAnimation
	{
		get => _selectedAnimation;
		set
		{
			_selectedAnimation = value;
			CurrentFrameIndex = 0;
		}
	}
	private SpriteAnimation _selectedAnimation;
	public int CurrentFrameIndex
	{
		get => _currentFrameIndex;
		set
		{
			_currentFrameIndex = value;
			OnTextureUpdate?.Invoke();
		}
	}

	private int _currentFrameIndex = 0;
	RealTimeSince frameTimer = 0;
	float FrameTime => ( ( SelectedAnimation?.FrameRate ?? 0 ) == 0 ) ? 0 : ( 1f / ( SelectedAnimation?.FrameRate ?? 30 ) );

	public bool Playing = true;
	public Timeline.Timeline Timeline;
	ToolBar toolBar;

	Option _undoMenuOption;
	Option _redoMenuOption;
	bool _isPingPonging = false;

	public MainWindow ()
	{
		DeleteOnClose = true;

		Size = new Vector2( 1280, 720 );
		Sprite = new SpriteResource();
		Sprite.Animations.Clear();

		SetWindowIcon( "emoji_emotions" );

		RestoreDefaultDockLayout();
	}

	protected override void OnKeyPress ( KeyEvent e )
	{
		base.OnKeyPress( e );

		if ( e.Key == KeyCode.Space )
		{
			PlayPause();
		}
	}

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

	public void SelectMember ( string memberName )
	{

	}

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

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

		{
			var file = MenuBar.AddMenu( "File" );
			file.AddOption( "New", "common/new.png", () => New(), "editor.new" ).StatusTip = "New Sprite";
			file.AddOption( "Open", "common/open.png", () => Open(), "editor.open" ).StatusTip = "Open Sprite";
			file.AddOption( "Save", "common/save.png", () => Save(), "editor.save" ).StatusTip = "Save Sprite";
			file.AddOption( "Save As...", "common/save.png", () => Save( true ), "editor.save-as" ).StatusTip = "Save Sprite 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 ()
	{
		var inspector = new Inspector.Inspector( this );
		var preview = new Preview.Preview( this );
		Timeline = new Timeline.Timeline( this );
		var animationList = new AnimationList.AnimationList( this );
		// var errorList = new ErrorList( null, this );

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

		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.AddDock( inspector, errorList, DockArea.Bottom, DockManager.DockProperty.HideOnClose, split: 0.75f );

		DockManager.Update();

		RebuildUI();
	}

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

	public void CreateNew ( string savePath = null )
	{
		if ( string.IsNullOrEmpty( savePath ) ) savePath = GetSavePath( "New Sprite" );

		_asset = null;
		Sprite = AssetSystem.CreateResource( "spr", savePath ).LoadResource<SpriteResource>();
		_dirty = false;
		_undoStack.Clear();

		if ( Sprite.Animations.Count > 0 )
		{
			SelectedAnimation = Sprite.Animations[0];
			OnAnimationSelected?.Invoke();
		}

		UpdateWindowTitle();
		OnAssetLoaded?.Invoke();
		OnTextureUpdate?.Invoke();
	}

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

		fd.SetNameFilter( "2D Sprite (*.sprite)" );

		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 sprite = asset.LoadResource<SpriteResource>();
		if ( sprite == null )
		{
			Log.Warning( $"Failed to load sprite from {asset.RelativePath}" );
			return;
		}

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

		_asset = asset;
		_dirty = false;
		_undoStack.Clear();
		Sprite = sprite;
		UpdateWindowTitle();

		OnAssetLoaded?.Invoke();
		OnTextureUpdate?.Invoke();

		if ( ( Sprite.Animations?.Count ?? 0 ) > 0 )
		{
			SelectedAnimation = Sprite.Animations[0];
			OnAnimationSelected?.Invoke();
		}
	}

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

		var contents = File.ReadAllText( path );
		var json = Json.ParseToJsonObject( contents );
		var animations = json["Animations"];
		ReloadFromString( animations.ToJsonString() );

		_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( "spr", savePath );
		_asset.SaveToDisk( Sprite );
		_dirty = false;
		UpdateWindowTitle();

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

		MainAssetBrowser.Instance?.Local?.UpdateAssetList();

		return true;
	}

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

	[EditorEvent.Frame]
	void Frame ()
	{
		if ( SelectedAnimation is null ) return;
		if ( SelectedAnimation?.Frames?.Count == 0 ) return;
		if ( FrameTime == 0 ) return;

		if ( Playing )
		{
			if ( SelectedAnimation.LoopMode != SpriteResource.LoopMode.PingPong )
			{
				_isPingPonging = false;
			}
			while ( frameTimer >= FrameTime )
			{
				AdvanceFrame();
			}
		}
		else
		{
			frameTimer = 0f;
		}

		_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";
	}

	void AdvanceFrame ()
	{
		var playbackSpeed = _isPingPonging ? -1 : 1;
		var nextFrame = CurrentFrameIndex + playbackSpeed;
		var loopStart = SelectedAnimation.GetLoopStart();
		var loopEnd = SelectedAnimation.GetLoopEnd();
		if ( nextFrame > loopEnd && playbackSpeed > 0 )
		{
			if ( SelectedAnimation.LoopMode == SpriteResource.LoopMode.Forward )
			{
				nextFrame = loopStart;
			}
			else if ( SelectedAnimation.LoopMode == SpriteResource.LoopMode.PingPong )
			{
				_isPingPonging = true;
				nextFrame = Math.Max( loopEnd - 1, loopStart );
			}
			else if ( nextFrame >= SelectedAnimation.Frames.Count )
			{
				nextFrame = SelectedAnimation.Frames.Count - 1;
				PlayPause();
			}
		}
		else if ( nextFrame < loopStart && playbackSpeed < 0 )
		{
			if ( SelectedAnimation.LoopMode == SpriteResource.LoopMode.Forward )
			{
				nextFrame = loopEnd;
			}
			else if ( SelectedAnimation.LoopMode == SpriteResource.LoopMode.PingPong )
			{
				_isPingPonging = false;
				nextFrame = Math.Min( loopStart + 1, loopEnd );
			}
			else
			{
				nextFrame = 0;
				PlayPause();
			}
		}
		CurrentFrameIndex = nextFrame;
		frameTimer -= FrameTime;
		if ( CurrentFrameIndex == 0 && SelectedAnimation.LoopMode == SpriteResource.LoopMode.None )
		{
			Playing = false;
			CurrentFrameIndex = SelectedAnimation.Frames.Count - 1;
			frameTimer = 0;
		}
	}

	protected override bool OnClose ()
	{
		if ( _dirty )
		{
			var confirm = new PopupWindow(
				"Save Current Sprite", "The open sprite 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;
	}

	static string GetSavePath ( string title = "Save Sprite" )
	{
		var fd = new FileDialog( null )
		{
			Title = title,
			DefaultSuffix = $".sprite"
		};

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

		return fd.SelectedFile;
	}

	internal void PromptImportSpritesheet ()
	{
		if ( SelectedAnimation is null )
		{
			var popup = new PopupWindow( "No Animation Selected", "Please select an animation to import a spritesheet into.", "OK", null );
			popup.Show();
			return;
		}
		var picker = AssetPicker.Create( this, AssetType.ImageFile, new() { EnableMultiselect = false, EnableCloud = false } );
		picker.Window.StateCookie = "SpriteEditor.Import";
		picker.Window.RestoreFromStateCookie();
		picker.Window.Title = $"Import Spritesheet for {SelectedAnimation.Name}";
		picker.OnAssetPicked = x =>
		{
			var path = x.FirstOrDefault()?.GetSourceFile();
			if ( string.IsNullOrEmpty( path ) ) return;
			var importer = new SpritesheetImporter.SpritesheetImporter( this, path );
			importer.OnImport += OnSpritesheetImport;
			importer.Window.Show();
		};
		picker.Window.Show();
	}

	void OnSpritesheetImport ( string path, List<Rect> frames )
	{
		PushUndo( $"Import Spritesheet with {frames.Count} frames" );
		if ( SelectedAnimation is not null )
		{
			SelectedAnimation.Frames.Clear();
			foreach ( var frame in frames )
			{
				SelectedAnimation.Frames.Add( new SpriteAnimationFrame( path ) { SpriteSheetRect = frame } );
			}
		}

		Timeline.UpdateFrameList();
		PushRedo();
	}

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

		var confirm = new PopupWindow(
			"Save Current Sprite", "The open sprite 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();
	}

	public void PlayPause ()
	{
		_isPingPonging = false;
		Playing = !Playing;
		if ( Playing && SelectedAnimation.LoopMode == SpriteResource.LoopMode.None && CurrentFrameIndex >= SelectedAnimation.Frames.Count - 1 )
		{
			CurrentFrameIndex = 0;
		}

		OnPlayPause?.Invoke();
	}

	public void FrameNext ()
	{
		var frame = CurrentFrameIndex + 1; ;
		if ( frame >= SelectedAnimation.Frames.Count )
		{
			frame = 0;
		}
		CurrentFrameIndex = frame;
	}

	public void FramePrevious ()
	{
		var frame = CurrentFrameIndex - 1;
		if ( frame < 0 )
		{
			frame = SelectedAnimation.Frames.Count - 1;
		}
		CurrentFrameIndex = frame;
	}

	public void FrameFirst ()
	{
		CurrentFrameIndex = 0;
	}

	public void FrameLast ()
	{
		CurrentFrameIndex = SelectedAnimation.Frames.Count - 1;
	}

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

	public void PushUndo ( string name, string buffer = "" )
	{
		if ( string.IsNullOrEmpty( buffer ) ) buffer = JsonSerializer.Serialize( Sprite.Animations );
		_undoStack.PushUndo( name, buffer );
	}

	public void PushRedo ()
	{
		_undoStack.PushRedo( JsonSerializer.Serialize( Sprite.Animations ) );
		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 )
	{
		var selectedName = SelectedAnimation?.Name;
		Sprite.Animations = JsonSerializer.Deserialize<List<SpriteAnimation>>( buffer );

		if ( Sprite.Animations.Any( x => x.Name == selectedName ) )
		{
			SelectedAnimation = Sprite.Animations.FirstOrDefault( x => x.Name == selectedName );
		}
		else
		{
			SelectedAnimation = Sprite.Animations.FirstOrDefault();
		}

		OnAssetLoaded?.Invoke();
		OnAnimationSelected?.Invoke();
		OnAnimationChanges?.Invoke();
		SetDirty();
	}

	private Option _undoOption;
	private Option _redoOption;

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

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

		toolBar.AddSeparator();

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

		toolBar.AddSeparator();

		toolBar.AddSeparator();

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