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

public enum ZoneExpansionSide
{
	Top,
	Bottom,
	Left,
	Right
}

/// <summary>
/// The building zone. This defines chunks of tiles that we may or may not own. This lets players buy more land.
/// TODO: I think this code needs to be cleaned big time.
/// </summary>
public sealed class BuildingZone : Component, ITerrainEvent,
	ISaveDataProperty<ImmutableHashSet<Vector2Int>>, Component.INetworkSnapshot
{
	private List<Vector2Int> previewCandidates = new();

	public static BuildingZone Instance { get; private set; }

	[Property] public LineRenderer Line { get; set; }

	[Property] public Vector2Int EntranceGridPos { get; set; }

	[Property] public Vector2Int StartingZonePosition { get; set; }
	[Property] public Vector2Int StartingZoneSize { get; set; } = new Vector2Int( 32, 32 );
	[Property] public ZoneExpansionSide PreviewEdge { get; set; } = ZoneExpansionSide.Top;
	[Property] GameObject ZoneWallObject { get; set; }
	[Property] GameObject ZoneWallPillarObject { get; set; }

	List<GameObject> zoneWalls { get; set; } = new();

	public HashSet<Vector2Int> OwnedChunks { get; private set; } = new();
	public List<ZoneTile> Tiles { get; private set; } = new();
	public bool PurchaseMode { get; set; } = false;

	public HashSet<Vector2Int> OwnedTiles { get; private set; } = new();

	void INetworkSnapshot.ReadSnapshot( ref ByteStream reader )
	{
		var chunkCount = reader.Read<int>();

		for ( var i = 0; i < chunkCount; i++ )
		{
			var chunk = reader.Read<Vector2Int>();
			AddChunkLocal( chunk );
		}
	}

	void INetworkSnapshot.WriteSnapshot( ref ByteStream writer )
	{
		writer.Write( OwnedChunks.Count );

		foreach ( var chunk in OwnedChunks )
		{
			writer.Write( chunk );
		}
	}

	public List<(Vector2Int, Vector3)> GetAllPreview()
	{
		var preview = new List<(Vector2Int, Vector3)>();
		foreach ( var chunk in previewCandidates )
		{
			var worldOrigin = GridManager.GridToWorldPosition( chunk );
			var worldSize = new Vector3(
				GridManager.GridSize * StartingZoneSize.x,
				GridManager.GridSize * StartingZoneSize.y,
				840f
			);
			preview.Add( (chunk, worldOrigin + worldSize / 2) );
		}
		return preview;
	}

	void ITerrainEvent.EntranceMoved( Vector3Int gridPos3d )
	{
		var gridPos = new Vector2Int( gridPos3d.x, gridPos3d.y );
		EntranceGridPos = gridPos;
		StartingZonePosition = gridPos + new Vector2Int( 1, -StartingZoneSize.y / 2 );

		Clear();
		AddChunkAt( StartingZonePosition );
	}

	protected override void OnAwake()
	{
		Instance = this;

		if ( GridManager.Instance.IsValid() )
		{
			foreach ( var tilePos in OwnedTiles )
			{
				GridManager.Instance?.Add( tilePos );
			}
		}
	}

	[Rpc.Broadcast( NetFlags.HostOnly )]
	private void Clear()
	{
		OwnedChunks.Clear();
		Tiles.Clear();
		OwnedTiles.Clear();
	}

	private void AddChunkLocal( Vector2Int chunkOrigin )
	{
		if ( !OwnedChunks.Add( chunkOrigin ) ) return;

		for ( int x = 0; x < StartingZoneSize.x; x++ )
			for ( int y = 0; y < StartingZoneSize.y; y++ )
			{
				var tilePos = chunkOrigin + new Vector2Int( x, y );
				if ( !OwnedTiles.Contains( tilePos ) )
				{
					Tiles.Add( new ZoneTile( tilePos ) );
					OwnedTiles.Add( tilePos );

					GridManager.Instance?.Add( tilePos );
				}
			}

		UpdateZoneOutline();
	}

	[Rpc.Broadcast( NetFlags.HostOnly )]
	private void AddChunkAt( Vector2Int chunkOrigin )
	{
		AddChunkLocal( chunkOrigin );
	}

	[Rpc.Host]
	private void TryPurchase( Vector2Int chunkOrigin )
	{
		var chunkSize = StartingZoneSize;
		var cost = GetChunkCost( chunkOrigin );

		if ( ParkManager.Instance.Money < cost )
			return;

		Log.Info( $"Adding chunk at {chunkOrigin} to {chunkOrigin + chunkSize}" );

		AddChunkAt( chunkOrigin );
		ParkManager.Instance.TakeMoney( cost, "Building Zones" );
	}

	public int GetChunkCost( Vector2Int chunkOrigin )
	{
		var dist = Vector2.Distance( chunkOrigin, StartingZonePosition );
		return 10_000 * (int)Math.Round( dist / StartingZoneSize.Length );
	}

	[Button( "Toggle Purchase Mode" )]
	public void TogglePurchaseMode()
	{
		PurchaseMode = !PurchaseMode;
	}

	protected override void OnUpdate()
	{
		if ( Tiles.Count == 0 ) return;
		if ( !PurchaseMode ) return;

		previewCandidates = new List<Vector2Int>();
		var chunkSize = StartingZoneSize;

		var bounds = GridManager.Instance.Terrain.Bounds;

		bounds.Left = StartingZonePosition.x;

		// TODO: RectInt.IsInside( fullyInside: true ) seems off by 1?

		bounds = bounds.Grow( 1 );

		foreach ( var chunk in OwnedChunks )
		{
			var directions = new[]
			{
				(Vector2Int.Up, ZoneExpansionSide.Top),
				(Vector2Int.Down, ZoneExpansionSide.Bottom),
				(Vector2Int.Left, ZoneExpansionSide.Left),
				(Vector2Int.Right, ZoneExpansionSide.Right),
			};

			foreach ( var (dir, side) in directions )
			{
				var neighborChunk = chunk + dir * chunkSize;
				var chunkBounds = new RectInt( neighborChunk, StartingZoneSize );

				if ( !bounds.IsInside( chunkBounds, true ) )
					continue;

				if ( !OwnedChunks.Contains( neighborChunk ) )
					previewCandidates.Add( neighborChunk );
			}
		}

		var tr = Scene.Trace.Ray( Scene.Camera.ScreenPixelToRay( Mouse.Position ), 40000f )
			.UsePhysicsWorld()
			.WithAnyTags( "ground" )
			.Run();

		foreach ( var chunk in previewCandidates )
		{
			var worldOrigin = GridManager.GridToWorldPosition( chunk );
			var worldSize = new Vector3(
				GridManager.GridSize * chunkSize.x,
				GridManager.GridSize * chunkSize.y,
				840f
			);

			var color = TraceWithinBounds( tr.HitPosition, chunk, chunk + chunkSize ) ? Color.Green : Color.Red;

			DebugOverlay.Box(
				worldOrigin + worldSize / 2,
				worldSize.WithZ( 64 ),
				color,
				0.1f,
				overlay: true
			);

			if ( Input.Pressed( "attack1" ) && TraceWithinBounds( tr.HitPosition, chunk, chunk + chunkSize ) )
			{
				TryPurchase( chunk );
				PurchaseMode = false;
			}
		}
	}
	bool TraceWithinBounds( Vector3 worldPos, Vector2Int min, Vector2Int max )
	{
		var gridPos = GridManager.WorldToGridPosition( worldPos );
		return gridPos.x >= min.x && gridPos.x <= max.x && gridPos.y >= min.y && gridPos.y <= max.y;
	}

	[ConCmd( "hc3.debug.updatezone", ConVarFlags.Cheat )]
	public static void UpdateZone()
	{
		BuildingZone.Instance?.UpdateZoneOutline( debug: true );
	}

	public void UpdateZoneOutline( bool debug = false )
	{
		if ( ZoneWallObject == null || Tiles.Count == 0 ) return;

		var terrain = GridManager.Instance?.Terrain;
		if ( terrain == null ) return;

		foreach ( var wall in zoneWalls )
		{
			wall.Destroy();
		}

		if ( debug )
		{
			Log.Info( $"Clearing {zoneWalls.Count} walls" );
		}

		zoneWalls.Clear();

		var outline = GenerateOutlineFromTiles( Tiles );
		if ( outline.Count == 0 ) return;

		// Update LineRenderer points
		if ( Line.IsValid() )
		{
			var linePoints = new List<Vector3>();
			foreach ( var point in outline )
			{
				// Convert grid position to world position and add some height offset
				var height = terrain[point].MaxHeight;
				var worldPoint = terrain.GridToWorld( new Vector3( point.x, point.y, height ) );
				// Add small offset to keep line visible above ground
				worldPoint.z += 10f;
				linePoints.Add( worldPoint );
			}
			// Close the loop by adding first point again
			if ( linePoints.Count > 0 )
			{
				linePoints.Add( linePoints[0] );
			}
			Line.VectorPoints = linePoints;
		}

		const int pillarSpacing = 5;
		int wallCount = 0;

		for ( int i = 0; i < outline.Count; i++ )
		{
			var startGrid = outline[i];
			var endGrid = outline[(i + 1) % outline.Count];

			// Yucky
			if ( startGrid == new Vector2Int( -47, -1 ) || startGrid == new Vector2Int( -47, -2 ) || startGrid == new Vector2Int( -47, 0 ) ||
				endGrid == new Vector2Int( -47, 2 ) || endGrid == new Vector2Int( -47, 3 ) )
				continue;

			var edgeMidpoint = (startGrid + endGrid) * 0.5f;
			var edgeHeight = terrain[new Vector2Int( (int)edgeMidpoint.x, (int)edgeMidpoint.y )].MaxHeight;

			var worldPos = terrain.GridToWorld( new Vector3( edgeMidpoint.x, edgeMidpoint.y, edgeHeight ) );
			var startWorld = terrain.GridToWorld( new Vector3( startGrid.x, startGrid.y, edgeHeight ) );
			var endWorld = terrain.GridToWorld( new Vector3( endGrid.x, endGrid.y, edgeHeight ) );

			var direction = (endWorld - startWorld).Normal;
			var length = (endWorld - startWorld).Length;

			var wall = ZoneWallObject.Clone( worldPos );
			wall.Name = $"Zone Wall {startGrid} to {endGrid}";
			wall.WorldTransform = new Transform(
				worldPos,
				Rotation.LookAt( direction ),
				new Vector3( 1f, 1f, 1f )
			);
			zoneWalls.Add( wall );

			wallCount++;
			if ( wallCount % pillarSpacing == 0 && ZoneWallPillarObject != null )
			{
				var pillar = ZoneWallPillarObject.Clone( worldPos );
				pillar.Name = $"Pillar {startGrid}";
				pillar.WorldTransform = new Transform(
					worldPos,
					Rotation.LookAt( direction ),
					Vector3.One
				);
				zoneWalls.Add( pillar );
			}
		}

		if ( debug )
		{
			Log.Info( $"Loaded {zoneWalls.Count} zone walls" );
		}
	}

	private List<Vector2Int> GenerateOutlineFromTiles( List<ZoneTile> tiles )
	{
		if ( tiles.Count == 0 ) return new List<Vector2Int>();

		var tileSet = new HashSet<Vector2Int>( tiles.Select( t => t.GridPos ) );

		// Find the bounds of our tile set
		var minX = tileSet.Min( p => p.x );
		var maxX = tileSet.Max( p => p.x );
		var minY = tileSet.Min( p => p.y );
		var maxY = tileSet.Max( p => p.y );

		var outline = new List<Vector2Int>();
		var current = new Vector2Int( minX, minY );
		var direction = Vector2Int.Right;

		// Walk around the perimeter in a clockwise direction
		do
		{
			outline.Add( current );

			// Try to turn right first, then straight, then left
			var turnDirections = new[]
			{
			new { Dir = TurnRight( direction ), Pos = current + TurnRight( direction ) },
			new { Dir = direction, Pos = current + direction },
			new { Dir = TurnLeft( direction ), Pos = current + TurnLeft( direction ) }
		};

			bool found = false;
			foreach ( var turn in turnDirections )
			{
				// Check if the position we're looking at is on the edge of our owned tiles
				if ( IsEdgePosition( turn.Pos, tileSet, minX, maxX, minY, maxY ) )
				{
					current = turn.Pos;
					direction = turn.Dir;
					found = true;
					break;
				}
			}

			if ( !found ) break; // Shouldn't happen in a well-formed outline

		} while ( current != outline[0] && outline.Count < (maxX - minX + maxY - minY) * 2 );

		return outline;
	}

	private bool IsEdgePosition( Vector2Int pos, HashSet<Vector2Int> tileSet, int minX, int maxX, int minY, int maxY )
	{
		// Position must be on the bounding box
		if ( pos.x < minX || pos.x > maxX || pos.y < minY || pos.y > maxY )
			return false;

		// Must have at least one adjacent tile in our set and one adjacent empty space
		bool hasAdjacent = false;
		bool hasEmpty = false;

		var adjacent = new[]
		{
		pos + Vector2Int.Up,
		pos + Vector2Int.Right,
		pos + Vector2Int.Down,
		pos + Vector2Int.Left
	};

		foreach ( var adj in adjacent )
		{
			if ( tileSet.Contains( adj ) )
				hasAdjacent = true;
			else
				hasEmpty = true;

			if ( hasAdjacent && hasEmpty )
				return true;
		}

		return false;
	}

	private Vector2Int TurnRight( Vector2Int dir )
	{
		return new Vector2Int( -dir.y, dir.x );
	}

	private Vector2Int TurnLeft( Vector2Int dir )
	{
		return new Vector2Int( dir.y, -dir.x );
	}

	public bool IsOwned( Vector3 worldPos )
	{
		var gridPos = GridManager.WorldToGridPosition( worldPos );
		return IsOwned( gridPos );
	}

	// otherwise the implicit Vector3 conversion will fuck ME or YOU
	public bool IsOwned( Vector3Int worldPos ) => IsOwned( new Vector2Int( worldPos ) );

	public bool IsOwned( Vector2Int gridPos )
	{
		return OwnedTiles.Contains( gridPos );
	}

	protected override void DrawGizmos()
	{
		Gizmo.Transform = new Transform( 0 );
		Gizmo.Draw.IgnoreDepth = true;
		Gizmo.Draw.Color = Color.Green;

		if ( !Gizmo.IsSelected ) return;

		var startingWorldPosition = GridManager.GridToWorldPosition( StartingZonePosition );

		var bbox = new BBox( startingWorldPosition.WithZ( 420 ), startingWorldPosition + new Vector3( StartingZoneSize.x * GridManager.GridSize, StartingZoneSize.y * GridManager.GridSize, 420f ) );

		Gizmo.Draw.LineBBox( bbox );

		Gizmo.Draw.Color = Color.Green.WithAlpha( 0.5f );
		Gizmo.Draw.SolidBox( bbox );
	}

	ImmutableHashSet<Vector2Int> ISaveDataProperty<ImmutableHashSet<Vector2Int>>.WriteValue( Scene scene ) =>
		OwnedChunks.ToImmutableHashSet();

	void ISaveDataProperty<ImmutableHashSet<Vector2Int>>.ReadValue( Scene scene, ImmutableHashSet<Vector2Int> chunks )
	{
		Clear();

		foreach ( var chunk in chunks )
		{
			AddChunkAt( chunk );
		}
	}
}

public struct ZoneTile
{
	public Vector2Int GridPos;
	public ZoneTile( Vector2Int pos ) => GridPos = pos;
	public Vector3 WorldCenter => GridManager.GridToWorldPosition( GridPos ) + new Vector3( GridManager.GridSize / 2, GridManager.GridSize / 2, 0 );
}