Park/Areas/AreaTile.cs
using System;
using HC3.Persistence;
using HC3.Terrain;
using System.Collections.Immutable;

namespace HC3;

public partial class AreaTile : Component, Component.ExecuteInEditor, IGridObjectEvent, IPathConnector
{
	public sealed record SaveData( Vector3Int Position, AreaType Type, PathMask Mask );

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

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

	public bool GhostPreview { get; set; }

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

	public virtual int Hash => HashCode.Combine( AreaType, 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 ) );
		}
	}

	protected virtual void UpdateConnections()
	{

	}

	protected override void OnStart()
	{
		// Add a collider to the tile so it can be clicked
		var collider = GetOrAddComponent<BoxCollider>();
		collider.Center = new Vector3( 0f, 0f, GridManager.HeightStep / 2f );
		collider.Scale = new Vector3( GridManager.GridSize, GridManager.GridSize, GridManager.HeightStep );
		collider.IsTrigger = true;
	}

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

	protected override void OnEnabled()
	{
		UpdateConnections();
		UpdateTile();
	}

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

		Mask = 0;

		// Only check connections for the same AreaType
		foreach ( var edge in GridManager.AllEdges )
		{
			var neighborPos = pos2d + edge.GetDirection();
			var cell = GridManager.Instance?.GetCell( neighborPos );

			if ( cell == null )
				continue;

			// Check if there's a matching AreaTile at this position
			var neighborArea = cell.GetComponents<AreaTile>( pos.z )
				.FirstOrDefault( x => !x.GhostPreview && x.AreaType == this.AreaType );

			if ( neighborArea != null )
			{
				Mask |= edge.ToPathMask();
			}
		}
	}

	protected void UpdateTile()
	{
		var tilemap = AreaBuilder.Instance.EnclosureTileMap;

		UpdateMask();

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

		Tile?.DestroyImmediate();

		var (tilePrefab, direction) = tilemap.GetTile( Mask, Vector2Int.Zero, false );

		if ( tilePrefab is null ) return;

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

		if ( GhostPreview )
		{
			var pos = TilePosition;
			var pos2d = new Vector2Int( pos.x, pos.y );

			foreach ( var renderer in Tile.GetComponentsInChildren<ModelRenderer>() )
			{
				renderer.SceneObject.Attributes.Set( "Ghost", AreaBuilder.Instance.CanPlace( pos2d ) ? 1 : 2 );
				renderer.SceneObject.Batchable = false;
			}
		}

		OnTileUpdated();
	}

	protected virtual void OnTileUpdated() { }

	public virtual bool CanConnectTile( Vector3Int gridPos, TileEdge edge )
	{
		if ( GhostPreview )
			return false;

		var targetPos = gridPos + edge.GetDirection();
		var cell = GridManager.Instance?.GetCell( new Vector2Int( targetPos.x, targetPos.y ) );
		if ( cell == null )
			return false;

		// Only connect to same AreaType at same height
		return cell.GetComponents<AreaTile>( targetPos.z )
			.Any( x => !x.GhostPreview && x.AreaType == this.AreaType );
	}

	protected override void OnUpdate()
	{
		if ( DebugMode.Enabled )
		{
			DebugOverlay.Text( WorldPosition + Vector3.Up * 8f, Mask.ToString(), 10f );
		}
	}
}

file sealed class AreaTileSaveData : ISaveDataProperty<ImmutableArray<AreaTile.SaveData>>
{
	string ISaveDataProperty.PropertyName => "Areas";

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

	void ISaveDataProperty<ImmutableArray<AreaTile.SaveData>>.ReadValue( Scene scene, ImmutableArray<AreaTile.SaveData> model )
	{
		var builder = AreaBuilder.Instance;
		builder.Clear();

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

		foreach ( var data in model )
		{
			var path = builder.AddArea( new( data.Position.x, data.Position.y ), data.Type );
			if ( path is null ) continue;

			paths.Add( path, data );
		}
	}
}