Terrain/Terraforming/Terraformer.cs
using System;
using System.Collections.Immutable;
using HC3.Terrain;
using HC3.UI;

namespace HC3.Terraforming;

#nullable enable

/// <summary>
/// Construction tool for editing the terrain.
/// </summary>
public sealed class Terraformer : Component, IBuilder
{
	public static Terraformer? Instance { get; private set; }

	private TerraformMode? _currentMode;

	public TerraformMode? CurrentMode
	{
		get => _currentMode;
		set
		{
			if ( _currentMode == value ) return;

			if ( _currentMode is not null ) _currentMode.IsActive = false;

			_currentMode = value;

			if ( _currentMode is not null )
			{
				_currentMode.IsActive = true;
				Activate();
			}
		}
	}

	public void Activate()
	{
		Enabled = true;

		WindowManager.Instance.DeactivateAll();
		(this as IBuilder).DeactivateAll();

		_currentMode ??= AllModes.FirstOrDefault();
		if ( _currentMode is not null ) _currentMode.IsActive = true;
	}

	public void Deactivate()
	{
		if ( _currentMode is not null ) _currentMode.IsActive = false;
		RemoveCursors();
		Enabled = false;
	}

	public bool IsTerraforming => Active && CurrentMode is not null;

	/// <summary>
	/// Cost to raise one corner of one tile one unit.
	/// </summary>
	[Property]
	public int UnitCost { get; set; } = 5;

	/// <summary>
	/// If not null, paint this material when teraaforming.
	/// </summary>
	[Property]
	public int? MaterialIndex { get; set; }

	[Property]
	public GameObject? CursorPrefab { get; set; }

	private readonly List<GameObject> _cursors = new();

	private RectInt _tileRange;
	private RectInt _cursorRange;

	private TileArraySlice _originalTiles;
	private TileArraySlice _modifiedTiles;
	private ParkTerrain? _terrain;

	private Vector2 _dragStartPos;
	private int? _lastDragDelta;

	private ITerraformBrush? _lastBrush;

	[SkipHotload]
	private ImmutableArray<TerraformMode>? _allModes;

	public IReadOnlyList<TerraformMode> AllModes =>
		_allModes ??= TerraformMode.GetAll().ToImmutableArray();

	public Vector2 CursorGridPos { get; private set; }

	protected override void OnAwake()
	{
		Instance = this;
		Enabled = false;
	}

	protected override void OnDestroy()
	{
		if ( Instance == this ) Instance = null;
	}

	protected override void OnEnabled()
	{
		Activate();
	}

	protected override void OnDisabled()
	{
		Deactivate();
	}

	protected override void OnUpdate()
	{
		if ( Input.EscapePressed )
		{
			Input.EscapePressed = false;
			Deactivate();
		}

		if ( !Input.Down( "Attack1" ) )
		{
			UpdateCursorPos();
		}

		_currentMode?.Update( CursorGridPos );

		UpdateBrush();

		if ( !Input.Down( "Attack1" ) )
		{
			UpdateCursorTiles();
		}
		else
		{
			UpdateDrag();
		}
	}

	private void UpdateCursorPos()
	{
		if ( Scene.Camera is not { } camera ) return;

		var ray = camera.ScreenPixelToRay( Mouse.Position );

		var result = Scene.Trace
			.Ray( ray, 65536f )
			.UsePhysicsWorld()
			.WithTag( "ground" )
			.Run();

		if ( !result.Hit || result.Component is not TerrainMesh { Terrain: var terrain } ) return;

		CursorGridPos = terrain.WorldToGrid( result.HitPosition );
		_terrain = terrain;
	}

	private void UpdateCursorTiles()
	{
		if ( CurrentMode is not { Brush: { } brush, Margin: var margin } ) return;
		if ( _terrain is null ) return;
		if ( !Mouse.Active ) return;

		_dragStartPos = Mouse.Position;
		_lastDragDelta = null;

		var lastRange = _tileRange;
		var min = _terrain.GetTileIndex( CursorGridPos - (brush.Size - 1) / 2f );

		_cursorRange = new RectInt( min, brush.Size );
		_tileRange = _cursorRange.Grow( margin );

		if ( _tileRange != lastRange || brush != _lastBrush )
		{
			_lastBrush = brush;
			UpdateCursorPosition();
		}
	}

	private void RemoveCursors()
	{
		foreach ( var cursor in _cursors )
		{
			cursor.Destroy();
		}

		_cursors.Clear();
	}

	/// <summary>
	/// Make sure we have the right number of cursors for the current brush.
	/// </summary>
	private void UpdateBrush()
	{
		if ( CurrentMode is not { Brush: { } brush, Margin: var margin } )
		{
			RemoveCursors();
			return;
		}

		if ( !CursorPrefab.IsValid() ) return;

		var size = brush.Size + 1;
		var count = size.x * size.y;

		while ( _cursors.Count > count )
		{
			foreach ( var cursor in _cursors.Skip( count ) )
			{
				cursor.Destroy();
			}

			_cursors.RemoveRange( count, _cursors.Count - count );
		}

		while ( _cursors.Count < count )
		{
			var cursor = CursorPrefab.Clone( global::Transform.Zero, GameObject );
			_cursors.Add( cursor );
		}
	}

	private void UpdateDrag()
	{
		if ( _terrain is not { } terrain ) return;
		if ( CurrentMode is not { } mode ) return;

		var delta = (int)MathF.Round( (_dragStartPos.y - Mouse.Position.y) / 32f );

		if ( delta == _lastDragDelta ) return;

		_lastDragDelta = delta;

		if ( _modifiedTiles.Size != _originalTiles.Size )
		{
			_modifiedTiles = new TileArraySlice( _originalTiles.Size );
		}

		mode.Apply( _originalTiles, _modifiedTiles, new TerraformContext( terrain, _tileRange, CursorGridPos, delta ) );

		var gridManager = GridManager.Instance;
		var buildingZone = BuildingZone.Instance;

		foreach ( var (index, tile) in _originalTiles )
		{
			var gridPos = index + _tileRange.Position;

			if ( !buildingZone.IsOwned( gridPos ) )
			{
				_modifiedTiles[index] = tile;
			}
			else if ( gridManager.IsConstructionBlocked( gridPos ) )
			{
				_modifiedTiles[index] = tile with { Paint = _modifiedTiles[index].Paint };
			}
		}

		ParkManager.Instance?.Terraform( _tileRange, _modifiedTiles.ToArray() );

		UpdateCursorPositions( _modifiedTiles );
	}

	private void UpdateCursorPosition()
	{
		if ( CurrentMode?.Brush is not { } brush ) return;
		if ( _terrain is not { } terrain ) return;

		if ( _originalTiles.Size != _tileRange.Size )
		{
			_originalTiles = new TileArraySlice( _tileRange.Size );
		}

		terrain.CopyToClamped( _tileRange, _originalTiles );

		UpdateCursorPositions( _originalTiles );
	}

	private void UpdateCursorPositions( TileArraySlice tiles )
	{
		if ( CurrentMode is not { Brush: { } brush, Margin: var margin } ) return;
		if ( _terrain is not { } terrain ) return;

		tiles = tiles.Slice( new RectInt( margin, brush.Size ) );

		var size = tiles.Size + 1;
		var count = size.x * size.y;

		var gridManager = GridManager.Instance;
		var buildingZone = BuildingZone.Instance;

		Span<int> cursorHeights = stackalloc int[count];

		foreach ( var (tileIndex, tile) in tiles )
		{
			var gridPos = tileIndex + _cursorRange.Position;
			var index = tileIndex.x + tileIndex.y * size.x;
			var canBuild = !gridManager.IsConstructionBlocked( gridPos ) && buildingZone.IsOwned( gridPos );

			cursorHeights[index] = Math.Max( cursorHeights[index], canBuild ? tile.GetCornerHeight( TileCorner.XMinYMin ) : -1 );
			cursorHeights[index + 1] = Math.Max( cursorHeights[index + 1], canBuild ? tile.GetCornerHeight( TileCorner.XMaxYMin ) : -1 );
			cursorHeights[index + size.x] = Math.Max( cursorHeights[index + size.x], canBuild ? tile.GetCornerHeight( TileCorner.XMinYMax ) : -1 );
			cursorHeights[index + size.x + 1] = Math.Max( cursorHeights[index + size.x + 1], canBuild ? tile.GetCornerHeight( TileCorner.XMaxYMax ) : -1 );
		}

		for ( var i = 0; i < count; ++i )
		{
			var cursor = _cursors[i];
			var x = i % size.x;
			var y = i / size.x;

			var height = cursorHeights[i];
			var gridPos = new Vector2( x, y ) + _cursorRange.Position;
			var weight = brush.GetWeight( new Vector2Int( x, y ) );

			cursor.Enabled = weight > 0f;

			cursor.WorldPosition = terrain.GridToWorld( new Vector3( gridPos, height ) );
			cursor.WorldScale = 0.25f + weight * 0.75f;
		}
	}
}