Editor/Tilemap/TilemapEditor.cs
using Editor;
using Sandbox;
using System;
using Saandy.Tilemapper;
using Editor.TerrainEditor;

namespace Saandy.Editor.Tilemapper;

[EditorTool]
[Title( "Tilemap Painter" )]
[Icon( "grid_3x3" )]
public class TilemapEditor : EditorTool
{
	private TileMap _tilemap;
	private TileMap _layerListTilemap;

	private Vector3 _hoverWorldPosition;
	private Vector2Int _hoverCell;
	private bool _hasHoverCell;

	private bool _hasLastPaintCell;
	private Vector2Int _lastPaintCell;

	private bool _wasLeftDown;
	private bool _wasRightDown;

	private TilemapTilesetPickerWidget _tilesetPicker;
	private TilemapLayerListWidget _layerList;

	[Property]
	public TilesetResource SelectedTileset { get; set; }

	private TileBrush _brush => SelectedTileset == null ? null : TileBrush.GetBrush( SelectedTileset.BrushType );

	public TilemapEditor()
	{
		RebuildSidebarOnSelectionChange = false;
	}

	public override Widget CreateToolSidebar()
	{
		var sidebar = new ToolSidebarWidget();
		sidebar.AddTitle( "Tilemap Painter", "grid_3x3" );
		sidebar.MinimumWidth = 340;

		// Layers
		{
			var group = sidebar.AddGroup( "Layers" );

			_layerList = new TilemapLayerListWidget( sidebar );
			_layerList.MinimumHeight = 190;
			_layerList.HorizontalSizeMode = SizeMode.Flexible;
			_layerList.VerticalSizeMode = SizeMode.CanGrow;

			_layerListTilemap = null;
			UpdateLayerListTilemap();

			group.Add( _layerList );

			var addLayerButton = new Button( "Add Layer", "add" );
			addLayerButton.Clicked += AddLayer;
			group.Add( addLayerButton );

			var removeLayerButton = new Button( "Remove Selected Layer", "delete" );
			removeLayerButton.Clicked += RemoveSelectedLayer;
			group.Add( removeLayerButton );
		}

		// Tileset selection
		{
			var group = sidebar.AddGroup( "Tileset Resources", SizeMode.Flexible );

			_tilesetPicker = new TilemapTilesetPickerWidget( sidebar );
			_tilesetPicker.MinimumHeight = 420;
			_tilesetPicker.HorizontalSizeMode = SizeMode.Flexible;
			_tilesetPicker.VerticalSizeMode = SizeMode.Flexible;
			_tilesetPicker.SetSelectedTileset( SelectedTileset );
			_tilesetPicker.OnTilesetSelected = OnTilesetSelected;

			group.Add( _tilesetPicker );
		}

		// Actions
		{
			var group = sidebar.AddGroup( "Actions" );

			var refreshButton = new Button( "Refresh Tilesets", "refresh" );
			refreshButton.Clicked += RefreshTilesets;
			refreshButton.ToolTip = "Reload the list of TilesetResource assets from ResourceLibrary.GetAll<TilesetResource>().";
			group.Add( refreshButton );
		}

		SelectDefaultTilesetIfNeeded();
		_tilesetPicker?.SetSelectedTileset( SelectedTileset );

		return sidebar;
	}

	public override void OnEnabled()
	{
		AllowGameObjectSelection = false;

		SetTilemap( TileMap.GetOrCreate( Scene ) );

		SelectDefaultTilesetIfNeeded();
		_tilesetPicker?.SetSelectedTileset( SelectedTileset );
	}

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

		_hasHoverCell = false;
		_hasLastPaintCell = false;
		_wasLeftDown = false;
		_wasRightDown = false;
	}

	public override void OnUpdate()
	{
		if ( _tilemap == null || !_tilemap.IsValid() )
		{
			SetTilemap( TileMap.GetOrCreate( Scene ) );
		}

		if ( _tilemap == null ) { return; }

		UpdateLayerListTilemap();

		//
		// Do NOT call _tilemap.ForceRendererUpdate( Camera ) here.
		//
		// TileMap is ExecuteInEditor and already updates its renderer every frame
		// using Application.Editor.Camera. Calling it again from the tool caused
		// the complete streaming pass to run twice per editor frame.
		//

		UpdateHoverCell();

		bool leftDown = Gizmo.IsLeftMouseDown;
		bool rightDown = Gizmo.IsRightMouseDown;

		if ( rightDown )
		{
			UpdatePaintStroke( eraseMode: true, isNewStroke: !_wasRightDown );
		}
		else if ( leftDown )
		{
			UpdatePaintStroke( eraseMode: false, isNewStroke: !_wasLeftDown );
		}
		else
		{
			_hasLastPaintCell = false;
		}

		DrawHoverSquare();

		_wasLeftDown = leftDown;
		_wasRightDown = rightDown;
	}

	private void SetTilemap( TileMap tilemap )
	{
		if ( _tilemap == tilemap && _tilemap != null && _tilemap.IsValid() )
		{
			UpdateLayerListTilemap();
			return;
		}

		_tilemap = tilemap;
		_layerListTilemap = null;
		UpdateLayerListTilemap();
	}

	private void UpdateLayerListTilemap()
	{
		if ( _layerList == null ) { return; }

		//
		// Avoid asking the layer widget to re-bind/rebuild every editor frame.
		//
		if ( _layerListTilemap == _tilemap ) { return; }

		_layerList.SetTilemap( _tilemap );
		_layerListTilemap = _tilemap;
	}

	private void AddLayer()
	{
		if ( _tilemap == null || !_tilemap.IsValid() )
		{
			SetTilemap( TileMap.GetOrCreate( Scene ) );
		}

		_tilemap?.AddLayer();
		RefreshLayerList();
	}

	private void RemoveSelectedLayer()
	{
		if ( _tilemap == null || !_tilemap.IsValid() ) { return; }

		_tilemap.RemoveLayer( _tilemap.ActiveLayerIndex );
		RefreshLayerList();
	}

	private void RefreshLayerList()
	{
		if ( _layerList == null ) { return; }

		_layerList.SetTilemap( _tilemap );
		_layerListTilemap = _tilemap;
	}

	private void OnTilesetSelected( TilesetResource tileset )
	{
		SelectedTileset = tileset;
		_tilesetPicker?.SetSelectedTileset( SelectedTileset );
	}

	private void RefreshTilesets()
	{
		_tilesetPicker?.RefreshTilesets();
		SelectDefaultTilesetIfNeeded();
		_tilesetPicker?.SetSelectedTileset( SelectedTileset );
	}

	private void SelectDefaultTilesetIfNeeded()
	{
		if ( SelectedTileset != null && SelectedTileset.IsValid() ) { return; }

		SelectedTileset = TilemapTilesetPickerWidget.GetFirstProjectTileset();
	}

	private void UpdateHoverCell()
	{
		var ray = Gizmo.CurrentRay;

		if ( !_tilemap.TryIntersectRayWithPlane( ray, out var hit ) )
		{
			_hasHoverCell = false;
			return;
		}

		_hoverWorldPosition = hit;
		_hasHoverCell = true;

		float tileSize = Math.Max( _tilemap.TileSize, 0.0001f );
		_hoverCell = _tilemap.WorldToCell( hit, tileSize );
	}

	private void UpdatePaintStroke( bool eraseMode, bool isNewStroke )
	{
		if ( !_hasHoverCell ) { return; }

		if ( isNewStroke || !_hasLastPaintCell )
		{
			PaintLine( _hoverCell, _hoverCell, eraseMode );

			_lastPaintCell = _hoverCell;
			_hasLastPaintCell = true;

			return;
		}

		if ( _lastPaintCell == _hoverCell ) { return; }

		PaintLine( _lastPaintCell, _hoverCell, eraseMode );

		_lastPaintCell = _hoverCell;
		_hasLastPaintCell = true;
	}

	private void PaintLine( Vector2Int from, Vector2Int to, bool eraseMode )
	{
		if ( _tilemap == null || !_tilemap.IsValid() ) { return; }
		if ( SelectedTileset == null || !SelectedTileset.IsValid() ) { return; }
		if ( _brush == null ) { return; }

		_brush.Draw( _tilemap, SelectedTileset, from, to, eraseMode );
	}

	private void DrawHoverSquare()
	{
		if ( !_hasHoverCell ) { return; }

		DrawCellOutline( _hoverCell, Color.FromBytes( 255, 220, 120 ) );
	}

	private void DrawCellOutline( Vector2Int cell, Color color )
	{
		if ( _tilemap == null ) { return; }

		var bounds = _tilemap.GetCellBounds( cell, 0.04f );

		var prevColor = Gizmo.Draw.Color;
		var prevThickness = Gizmo.Draw.LineThickness;

		Gizmo.Draw.Color = color;
		Gizmo.Draw.LineThickness = 1.5f;
		Gizmo.Draw.LineBBox( bounds );

		Gizmo.Draw.Color = prevColor;
		Gizmo.Draw.LineThickness = prevThickness;
	}
}