Park/Paths/Path.cs
using System;
using HC3.Persistence;
using HC3.Terrain;
using System.Collections.Immutable;

namespace HC3;

/// <summary>
/// A path connection interface.
/// </summary>
public interface IPathConnector
{
	int GetHeightOffset( TileEdge edge ) { return 0; }

	/// <summary>
	/// Can this path connect at a specific edge?
	/// </summary>
	/// <param name="gridPos"></param>
	/// <param name="edge"></param>
	/// <returns></returns>
	bool CanConnectTile( Vector3Int gridPos, TileEdge edge );
}

public class Path : Component, Component.ExecuteInEditor, IGridObjectEvent, IPathConnector
{
	public const int PATH_HEIGHT = 2;
	public const int PATH_COST = 10;

	public sealed record SaveData( Vector3Int Position, PathType Type, PathMask Mask, Vector2Int StairDirection );

	[ConVar( "hc3.debug.paths" )]
	public static bool DebugDraw { get; set; }

	public SaveData GetSaveData()
	{
		return new( TilePosition, PathType, Mask, StairDirection );
	}

	[RequireComponent]
	public GridObject GridObject { get; private set; }

	public bool GhostPreview { get; set; }

	[Property, Hide] public Vector2Int StairDirection { get; set; }
	[Property, Hide] public PathMask Mask { get; private set; }
	[Property, Hide] public GameObject Tile { get; private set; }
	[Property] public PathType PathType { get; set; }

	public bool IsElevated { get; set; }
	public bool IsStairs => StairDirection != 0;

	public virtual int Hash => HashCode.Combine( PathType, Mask );
	private int prevHash;

	public Vector3Int TilePosition
	{
		get
		{
			var pos2d = GridManager.WorldToGridPosition( WorldPosition );
			return new Vector3Int( pos2d.x, pos2d.y, (int)MathF.Round( WorldPosition.z / GridManager.HeightStep ) );
		}
	}

	public int RegionId => GridObject.RegionId;

	protected override void OnUpdate()
	{
		if ( !DebugDraw ) return;

		using ( Gizmo.Scope( $"path_{GameObject.Id}" ) )
		{
			var pos = WorldPosition;
			var center = pos + Vector3.Up * 8;

			foreach ( var edge in GridManager.AllEdges )
			{
				var rawMask = edge.ToPathMask();
				var isWalkable = IsWalkable( edge );

				var cell = GridManager.Instance?.GetCell( new Vector2Int( TilePosition.x, TilePosition.y ) );
				var decorationBlocking = false;

				if ( cell != null )
				{
					var decorations = cell.GetComponents<Decoration>( TilePosition.z );
					foreach ( var decoration in decorations )
					{
						var mask = edge.ToPathMask();
						var yaw = (int)MathF.Round( decoration.WorldRotation.Yaw() / 90f );
						while ( yaw < 0 ) yaw += 4;
						yaw %= 4;

						// Rotate mask to match decoration
						for ( int i = 0; i < yaw; i++ )
							mask = mask.Rotate90DegreesClockwise();

						if ( (decoration.BlockedDirections & mask) != 0 )
						{
							decorationBlocking = true;
							break;
						}
					}
				}

				var dir = edge.GetDirection();
				var end = center + new Vector3( dir.x, dir.y, 0 ) * 32;

				var color = isWalkable ? Color.Green
					: decorationBlocking ? Color.Red
					: Color.Yellow;

				Gizmo.Draw.Color = color.WithAlpha( 0.5f );
				Gizmo.Draw.Line( center, end );

				var arrowDir = (end - center).Normal;
				var arrowEnd = end - arrowDir * 8;
				var right = Vector3.Cross( Vector3.Up, arrowDir ).Normal * 4;

				Gizmo.Draw.Line( end, arrowEnd + right );
				Gizmo.Draw.Line( end, arrowEnd - right );

				if ( !isWalkable )
				{
					var textPos = Vector3.Lerp( center, end, 0.5f ) + Vector3.Up * 4;
					Gizmo.Draw.Color = color;
					var info = decorationBlocking ? "Blocked by\nDecoration" : "Blocked";
					Gizmo.Draw.Text( info, new Transform( textPos ) );
				}
			}
		}
	}

	protected virtual void UpdateConnections()
	{
	}

	void IGridObjectEvent.NeighborsChanged( GridCell cell )
	{
		UpdateConnections();
		UpdateTile();
	}

	protected override void OnEnabled()
	{
		// hmmmm
		GridObject = GetOrAddComponent<GridObject>();

		UpdateConnections();
		UpdateTile();
	}

	public bool IsWalkable( TileEdge edge, GridCell.ConnectionFlags flags = GridCell.ConnectionFlags.Default )
	{
		var pos = TilePosition + edge.GetDirection();
		var pos2d = new Vector2Int( pos.x, pos.y );

		if ( !GridManager.Instance?.Terrain.Bounds.IsInside( pos2d ) ?? false )
			return true;

		// Check cached blocked directions in source cell
		var cell = GridManager.Instance?.GetCell( new Vector2Int( TilePosition.x, TilePosition.y ) );
		if ( cell != null )
		{
			var mask = edge.ToPathMask();
			if ( (cell.GetBlockedDirections( TilePosition.z ) & mask) != 0 )
				return false;
		}

		// Check cached blocked directions in target cell (opposite direction)
		var targetCell = GridManager.Instance?.GetCell( pos2d );
		if ( targetCell != null )
		{
			var oppositeMask = edge.GetOpposite().ToPathMask();
			if ( (targetCell.GetBlockedDirections( pos.z ) & oppositeMask) != 0 )
				return false;
		}

		if ( !CanConnectTile( TilePosition, edge ) )
			return false;

		return targetCell?.GetConnection( pos, edge.GetOpposite(), flags ) is not null;
	}

	public void AlignToTerrain()
	{
		var terrain = GridManager.Instance?.Terrain;
		Vector2Int pos2d = new Vector2Int( TilePosition );

		if ( terrain?[pos2d] is { } tile && PathBuilder.IsPathableSlope( tile.Slope ) )
		{
			StairDirection = tile.Slope.GetGradients();
		}

		UpdateTile();
	}

	public void SetStair( TileEdge edge, PathElevation elevation )
	{
		if ( elevation is PathElevation.Flat )
		{
			StairDirection = Vector2Int.Zero;
		}
		else if ( elevation is PathElevation.StairUp )
		{
			StairDirection = edge.GetDirection();
		}
		else if ( elevation is PathElevation.StairDown )
		{
			StairDirection = edge.GetOpposite().GetDirection();

			var pos = WorldPosition;
			pos.z -= GridManager.HeightStep;
			WorldPosition = pos;
		}

		UpdateTile();
	}

	private void UpdateMask()
	{
		var terrain = GridManager.Instance?.Terrain;
		var pos = TilePosition;
		var pos2d = new Vector2Int( pos.x, pos.y );

		if ( StairDirection.Length != 0 )
		{
			Mask = StairDirection.x != 0
				? PathMask.Left | PathMask.Right
				: PathMask.Up | PathMask.Down;
		}
		else
		{
			Mask = 0;

			if ( IsWalkable( TileEdge.Left ) )
				Mask |= PathMask.Left;
			if ( IsWalkable( TileEdge.Right ) )
				Mask |= PathMask.Right;
			if ( IsWalkable( TileEdge.Up ) )
				Mask |= PathMask.Up;
			if ( IsWalkable( TileEdge.Down ) )
				Mask |= PathMask.Down;
		}

		IsElevated = StairDirection.Length != 0 || TilePosition.z > terrain[pos2d].MinHeight;
	}

	protected void UpdateTile()
	{
		var tilemap = PathBuilder.Instance.PathTileMap;
		if ( PathType == PathType.Queue )
			tilemap = PathBuilder.Instance.QueueTileMap;

		UpdateMask();

		if ( !GhostPreview && prevHash == Hash )
			return;
		prevHash = Hash;

		Tile?.DestroyImmediate();

		var (tilePrefab, direction) = tilemap.GetTile( Mask, StairDirection, IsElevated );

		if ( tilePrefab is null ) return;

		Tile = tilePrefab.Clone( new CloneConfig { Parent = GameObject, StartEnabled = true, Name = "tile" } );
		Tile.LocalRotation = Rotation.LookAt( direction );
		Tile.Flags |= GameObjectFlags.NotSaved;

		if ( GhostPreview )
		{
			var renderer = Tile.GetComponentInChildren<ModelRenderer>();

			if ( renderer.IsValid() )
			{
				renderer.SceneObject.Attributes.Set( "Ghost", PathBuilder.Instance.CanPlace( TilePosition ) ? 1 : 2 );
				renderer.SceneObject.Batchable = false;
			}
		}

		OnTileUpdated();
	}

	protected virtual void OnTileUpdated() { }

	public virtual bool CanConnectTile( Vector3Int gridPos, TileEdge edge )
	{
		if ( StairDirection == 0 )
		{
			// Easy case for non-stairs
			return gridPos == TilePosition;
		}

		float dot = Vector2.Dot( edge.GetDirection(), StairDirection );
		if ( dot == 0 ) return false; // Can't connect to the side of the stair

		return gridPos == TilePosition + Vector3Int.Up * GetHeightOffset( edge );
	}

	public int GetHeightOffset( TileEdge edge )
	{
		float dot = Vector2.Dot( edge.GetDirection(), StairDirection );
		return dot > 0 ? 1 : 0;
	}

	public float GetHeightAt( Vector3 worldPos )
	{
		Vector2 delta2D = Transform.World.PointToLocal( worldPos );
		delta2D += StairDirection * (GridManager.GridSize / 2.0f);

		float frac = Vector2.Dot( delta2D / GridManager.GridSize, StairDirection );

		return WorldPosition.z + (frac * GridManager.HeightStep);
	}
}

file sealed class PathSaveData : ISaveDataProperty<ImmutableArray<Path.SaveData>>
{
	string ISaveDataProperty.PropertyName => "Paths";

	ImmutableArray<Path.SaveData> ISaveDataProperty<ImmutableArray<Path.SaveData>>.WriteValue( Scene scene ) =>
		scene.GetAllComponents<Path>()
			.Select( x => x.GetSaveData() )
			.ToImmutableArray();

	void ISaveDataProperty<ImmutableArray<Path.SaveData>>.ReadValue( Scene scene, ImmutableArray<Path.SaveData> model )
	{
		var pathBuilder = PathBuilder.Instance;
		pathBuilder.Clear();

		Dictionary<Path, Path.SaveData> paths = new();

		foreach ( var data in model )
		{
			var path = pathBuilder.AddPath( data.Position, data.Type );
			if ( path is null ) continue;

			path.StairDirection = data.StairDirection;

			paths.Add( path, data );
		}

		foreach ( var queue in paths.Keys.OfType<Queue>() )
		{
			queue.OnPostLoad( paths[queue] );
		}
	}
}