Park/Areas/AreaBuilder.cs
using HC3.Terrain;
using System;

namespace HC3;

public enum AreaType
{
	Enclosure
}

internal class AreaBuilder : Component, Component.ExecuteInEditor
{
	public AreaType AreaType { get; set; }
	public static AreaBuilder Instance { get; private set; }
	public GameObject Ghost { get; set; }

	[Property]
	public PathTileMap EnclosureTileMap { get; internal set; }

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

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

	protected override void OnDisabled()
	{
		Ghost?.Destroy();
	}

	public void UpdateGhost()
	{
		Ghost?.DestroyImmediate();
		Ghost = new GameObject( false, "Ghost Area" );
		Ghost.Flags |= GameObjectFlags.NotNetworked;

		// For now
		var area = Ghost.AddComponent<AreaTile>();
		var gridObject = Ghost.GetOrAddComponent<GridObject>();

		gridObject.IsWalkable = false;
		gridObject.BlocksConstruction = false;

		area.GhostPreview = true;
		area.AreaType = AreaType;

		Ghost.Enabled = true;
	}

	bool startedDragging = false;
	Vector3 dragStartPos;
	Vector3 dragEndPos;

	List<GameObject> Ghosts = new();

	void UpdateGhosts()
	{
		Ghosts.ForEach( g => g.DestroyImmediate() );
		Ghosts.Clear();

		var startGrid = GridManager.WorldToGridPosition( dragStartPos );
		var endGrid = GridManager.WorldToGridPosition( dragEndPos );

		var min = Vector2Int.Min( startGrid, endGrid );
		var max = Vector2Int.Max( startGrid, endGrid );

		for ( int x = min.x; x <= max.x; x++ )
		{
			for ( int y = min.y; y <= max.y; y++ )
			{
				var worldPos = GridManager.GridToWorldPosition( new Vector2Int( x, y ) );
				var height = GridManager.Instance.Terrain[new Vector2Int( x, y )].MaxHeight;
				worldPos = worldPos.WithZ( height * GridManager.Instance.Terrain.TileHeight ) + GridManager.CentreOffset;

				var ghost = new GameObject( false, "Ghost Area" );
				ghost.Flags |= GameObjectFlags.NotNetworked;
				ghost.WorldPosition = worldPos;

				var area = ghost.AddComponent<AreaTile>();
				area.GhostPreview = true;
				area.AreaType = AreaType;

				var gridObject = ghost.GetOrAddComponent<GridObject>();
				gridObject.IsWalkable = false;
				gridObject.BlocksConstruction = false;

				ghost.Enabled = true;
				Ghosts.Add( ghost );
			}
		}
	}


	protected override void OnUpdate()
	{
		if ( Scene.Camera is not { } camera ) return;
		if ( !Mouse.Active ) 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;

		var gridPos = GridManager.WorldToGridPosition( result.HitPosition );
		var snappedPos = GridManager.GridToWorldPosition( gridPos );

		var height = terrain[gridPos].MaxHeight;

		// finish current dragging
		if ( !Input.Down( "Attack1" ) && startedDragging )
		{
			startedDragging = false;

			List<Vector3> areasToPlace = new();

			foreach ( var ghost in Ghosts )
			{
				areasToPlace.Add( ghost.WorldPosition );
				ghost.DestroyImmediate();
			}

			Ghosts.Clear();
			PlaceMany( dragStartPos, dragEndPos );
		}

		// Dragging
		if ( Input.Down( "Attack1" ) )
		{
			if ( !startedDragging )
			{
				startedDragging = true;
				dragStartPos = snappedPos.WithZ( terrain.TileHeight * height ) + new Vector3( 32, 32, 0 );
			}

			// keep drag end pos in line with start pos
			var endPos = snappedPos.WithZ( terrain.TileHeight * height ) + new Vector3( 32, 32, 0 );
			var diff = endPos - dragStartPos;

			dragEndPos = endPos;

			Ghost.WorldPosition = dragEndPos;

			// prevent z-fighting with existing areas
			Ghost.WorldPosition += Vector3.Up * 0.1f;

			UpdateGhosts();
		}
		else
		{
			Ghost.WorldPosition = snappedPos.WithZ( terrain.TileHeight * height ) + new Vector3( 32, 32, 0 );
		}

		if ( Input.Pressed( "Attack2" ) )
		{
			Delete( Ghost.WorldPosition );
		}
	}

	public static bool IsareaableSlope( TileSlope slope )
	{
		var gradients = slope.GetGradients();
		// Flat only
		return gradients == Vector2Int.Zero;
	}

	/// <summary>
	/// Place a new area. This is called on the host.
	/// </summary>
	[Rpc.Host]
	public void Place( Vector3 pos, AreaType type )
	{
		var gridPos = GridManager.WorldToGridPosition( pos );
		var cost = 10;

		// Can we afford to place the area?
		if ( !ParkManager.Instance?.TakeMoney( cost, "Areas" ) ?? false )
			return;

		if ( GridManager.Instance?.Terrain[gridPos] is not { } tile )
			return;

		if ( !BuildingZone.Instance.IsOwned( pos ) )
			return;

		if ( !IsareaableSlope( tile.Slope ) )
		{
			GridManager.Instance.Terrain[gridPos] = TerrainTile.LevelGround( tile.MaxHeight );
		}

		if ( AddArea( gridPos, type ) is null )
			return;

		var dust = GameObject.Clone( "prefabs/particles/place_dustcloud.prefab", new Transform( pos + Vector3.Up * 8 ) );

		Sound.Play( "sounds/gameplay/building_placed.sound", pos );

		MoneyEffect.Broadcast( pos + Vector3.Up * 10f, $"-{GameUtils.Currency}{cost}", Color.Red );
	}

	public AreaTile AddArea( Vector2Int tilePosition, AreaType type = AreaType.Enclosure )
	{
		var grid = Scene.GetAllComponents<GridManager>()
			.FirstOrDefault() ?? throw new Exception( "Grid doesn't exist!" );

		var terrain = grid.Terrain;

		var go = new GameObject( true, "area" );

		var level = terrain[tilePosition].MaxHeight;
		int height = 2;

		if ( grid.IsConstructionBlocked( new Vector3Int( tilePosition ).WithZ( level ), height ) ) return null;

		go.WorldPosition = terrain.GridToWorld( new( tilePosition + 0.5f, level ) );
		go.Tags.Add( "area" );

		var area = go.AddComponent<AreaTile>();
		area.AreaType = type;

		var gridObject = go.GetOrAddComponent<GridObject>();

		gridObject.IsWalkable = true;
		gridObject.BlocksConstruction = true;

		if ( tilePosition.x == terrain.Bounds.Left )
		{
			// spawn guests from here!
			gridObject.AddComponent<SpawnPoint>();
		}

		if ( Scene.IsEditor )
		{
			go.Flags |= GameObjectFlags.NotSaved;
		}
		else if ( Networking.IsHost )
		{
			// Network spawn this area object if we're the host
			go.NetworkSpawn();
		}

		return area;
	}

	public async void PlaceMany( Vector3 start, Vector3 end )
	{
		var startGrid = GridManager.WorldToGridPosition( start );
		var endGrid = GridManager.WorldToGridPosition( end );

		var min = Vector2Int.Min( startGrid, endGrid );
		var max = Vector2Int.Max( startGrid, endGrid );

		for ( int x = min.x; x <= max.x; x++ )
		{
			for ( int y = min.y; y <= max.y; y++ )
			{
				var world = GridManager.GridToWorldPosition( new Vector2Int( x, y ) ) + GridManager.CentreOffset;
				Place( world, AreaType );

				await Task.DelayRealtime( 5 );
			}
		}

	}

	[Rpc.Host]
	public void Delete( Vector3 pos )
	{
		if ( !BuildingZone.Instance.IsOwned( pos ) ) return;

		var gridPos = GridManager.WorldToGridPosition( pos );
		var grid = GridManager.Instance;
		var areaCost = 10;

		if ( grid.GetCell( gridPos )?.GetComponents<AreaTile>()?.Where( x => !x.GhostPreview ).FirstOrDefault() is not { } area )
			return;

		area.GameObject.DestroyImmediate();

		MoneyEffect.Broadcast( pos + Vector3.Up * 10f, $"-{GameUtils.Currency}{areaCost}", Color.Green );

		ParkManager.Instance?.GiveMoney( areaCost, "Building Refunds" );
	}

	internal void Clear()
	{
	}

	internal bool CanPlace( Vector2Int tilePosition )
	{
		var level = GridManager.Instance.Terrain[tilePosition].MaxHeight;
		int height = 2;

		if ( GridManager.Instance.IsConstructionBlocked( new Vector3Int( tilePosition ).WithZ( level ), height ) ) return false;

		return true;
	}
}