Park/Paths/PathBuilder.cs
using HC3.Terrain;
using HC3.UI;
using Sandbox.Diagnostics;
using System;

namespace HC3;

internal class PathBuilder : Component, Component.ExecuteInEditor, IBuilder
{
	public PathType PathType { get; set; }
	public static PathBuilder Instance { get; private set; }

	public Vector3Int? CursorPosition;
	public TileEdge Direction;
	public PathElevation Elevation;
	public bool CursorMode;

	[Property] public PathTileMap PathTileMap { get; set; }
	[Property] public PathTileMap QueueTileMap { get; set; }

	public GameObject CursorPath { get; set; }
	public Path GhostPath { get; set; }

	public void Activate()
	{
		(this as IBuilder).DeactivateAll();
		Enabled = true;

		CursorMode = false;
	}

	public void Deactivate()
	{
		PathWindow.Current?.Close();
		WindowManager.Instance.DeactivateAll();
		Enabled = false;
	}

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

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

	public void UpdateGhostPath()
	{
		if ( GhostPath.IsValid() )
		{
			GhostPath?.GameObject.DestroyImmediate();
		}

		var obj = new GameObject( false, "Ghost Path" );
		obj.Flags |= GameObjectFlags.NotNetworked;

		GhostPath = PathType == PathType.Queue ? obj.AddComponent<Queue>() : obj.AddComponent<Path>();

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

		GhostPath.GhostPreview = true;
		GhostPath.PathType = PathType;

		GhostPath.GameObject.Enabled = true;
	}

	protected override void OnDisabled()
	{
		if ( GhostPath.IsValid() )
			GhostPath.DestroyGameObject();
	}

	bool startedDragging = false;
	Vector3 dragStartPos;
	Vector3 dragEndPos;

	List<GameObject> Ghosts = new();

	void UpdateGhosts()
	{
		foreach ( var ghost in Ghosts )
		{
			ghost.DestroyImmediate();
		}

		Ghosts.Clear();

		// DebugOverlay.Line( new Line( dragStartPos + Vector3.Up * 16, dragEndPos + Vector3.Up * 16 ) );

		var normal = (dragEndPos - dragStartPos).Normal;
		var length = (dragEndPos - dragStartPos).Length;
		var blocks = Math.Ceiling( length / 64.0f );
		for ( int i = 0; i < blocks; i++ )
		{
			// DebugOverlay.Sphere( new Sphere( dragStartPos + normal * 64 * i, 16.0f ), Color.Red );

			var ghostPath = new GameObject( false, "Ghost Path" );
			ghostPath.Flags |= GameObjectFlags.NotNetworked;

			var path = PathType == PathType.Queue ? ghostPath.AddComponent<Queue>() : ghostPath.AddComponent<Path>();
			path.GhostPreview = true;
			path.PathType = PathType;

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

			ghostPath.WorldPosition = dragStartPos + normal * 64 * i;

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

			ghostPath.Enabled = true;

			Ghosts.Add( ghostPath );
		}
	}

	Ray CursorRay => Scene.Camera.ScreenPixelToRay( Mouse.Position );

	SceneTrace PathTrace => Scene.Trace
				.Ray( CursorRay, 65536f )
				.WithTag( "path" );

	protected override void OnUpdate()
	{
		if ( Scene.Camera is not { } camera ) return;
		if ( !Mouse.Active ) return;

		if ( Input.EscapePressed )
		{
			Deactivate();
			return;
		}

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

		{
			var result = PathTrace.Run();

			if ( CursorMode && CursorPosition.HasValue )
			{
				// ghost from cursor path when the cursor controls are hovered

				Vector3Int tilePosition = CursorPosition.Value + Direction.GetDirection();
				GhostPath.WorldPosition = GridManager.GridToWorldPosition( tilePosition ) + GridManager.CentreOffset;
				GhostPath.GameObject.Enabled = true;

				var path = GhostPath.GetComponent<Path>();
				path.SetStair( Direction, Elevation );
			}
			else if ( result.Hit && !startedDragging )
			{
				// path selection/hover ghosts

				var hoverPath = result.GameObject.GetComponentInParent<Path>();

				if ( Input.Pressed( "Attack1" ) )
				{
					CursorPosition = GridManager.WorldToGridPosition3D( hoverPath.WorldPosition );
					CursorPosition += Vector3Int.Up * hoverPath.GetHeightOffset( Direction );

					var offset = (result.HitPosition - hoverPath.WorldPosition).WithZ( 0 );
					Direction = offset.GetTileEdge();

					if ( !CursorPath.IsValid() )
					{
						CursorPath = new GameObject();
						CursorPath.AddComponent<CursorPath>();
					}
				}

				// no ghost for stairs
				GhostPath.GameObject.Enabled = !hoverPath.IsStairs;

				if ( GhostPath.GameObject.Enabled )
				{
					GhostPath.WorldPosition = GridManager.AlignWorldToGrid( result.HitPosition ) + GridManager.CentreOffset;

					// prevent z-fighting with existing paths
					GhostPath.WorldPosition += Vector3.Up * 0.1f;
				}
			}
			else
			{
				// terrain-surface paths and dragging
				// TODO: 3d paths drag?

				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 );

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

					List<Vector3> pathsToPlace = new();

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

					Ghosts.Clear();

					pathsToPlace.Add( GhostPath.WorldPosition );

					PlacePaths( pathsToPlace );

					// select last path as the cursor
					CursorPosition = GridManager.WorldToGridPosition3D( result.HitPosition );

					// face the right way if we can work that out
					if ( pathsToPlace.Count == 1 )
					{
						var offset = (result.HitPosition - (GridManager.AlignWorldToGrid( result.HitPosition ) + GridManager.CentreOffset)).WithZ( 0 );
						Direction = offset.GetTileEdge();
					}
					else if ( pathsToPlace.FirstOrDefault() is { } firstGhost )
					{
						Direction = (GhostPath.WorldPosition - firstGhost).GetTileEdge();
					}

					if ( !CursorPath.IsValid() )
					{
						CursorPath = new GameObject();
						CursorPath.AddComponent<CursorPath>();
					}
				}

				var height = terrain[gridPos].MinHeight;
				// 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;

					if ( MathF.Abs( diff.x ) > MathF.Abs( diff.y ) )
					{
						dragEndPos = endPos.WithY( dragStartPos.y );
					}
					else
					{
						dragEndPos = endPos.WithX( dragStartPos.x );
					}

					GhostPath.WorldPosition = dragEndPos;

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

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

				GhostPath.GameObject.Enabled = true;
			}
		}

		if ( Input.Pressed( "Attack2" ) )
		{
			var result = PathTrace.Run();
			if ( result.Hit )
			{
				var hoverPath = result.GameObject.GetComponentInParent<Path>();
				DeletePath( hoverPath.TilePosition );
			}
		}
	}

	public async void PlacePaths( List<Vector3> paths )
	{
		foreach ( var pos in paths )
		{
			PlacePath( pos, PathType );
			await Task.DelayRealtime( 50 );
		}

		if ( PathType == PathType.Path )
			Stats.Increment( "path.placed", paths.Count );
		else if ( PathType == PathType.Queue )
			Stats.Increment( "queue.placed", paths.Count );
	}

	public void Clear()
	{
		// Why do we have no scene
		if ( Scene is null ) return;

		foreach ( var path in Scene.GetAllComponents<Path>().ToArray() )
		{
			path.GameObject.DestroyImmediate();
		}
	}

	public void RotateCursor( int direction )
	{
		Direction = Direction.Rotate( direction );
	}

	public void BuildCursor()
	{
		Assert.NotNull( CursorPosition );

		Vector3Int tilePosition = CursorPosition.Value + Direction.GetDirection();
		PlacePath( tilePosition, PathType, Direction, Elevation );
	}

	public Path AddPath( Vector2Int tilePosition, PathType type = PathType.Path )
	{
		if ( GridManager.Instance is null ) return null;

		var terrain = GridManager.Instance.Terrain;

		if ( terrain is null ) return null;

		var groundHeight = terrain[tilePosition].MinHeight;

		var path = AddPath( new Vector3Int( tilePosition.x, tilePosition.y, groundHeight ), type );
		if ( path.IsValid() )
		{
			path.AlignToTerrain();
		}

		return path;
	}

	public Path AddPath( Vector3Int tilePosition, PathType type = PathType.Path, TileEdge direction = TileEdge.Down, PathElevation elevation = PathElevation.Flat )
	{
		var grid = Scene.GetAllComponents<GridManager>()
			.FirstOrDefault() ?? throw new Exception( "Grid doesn't exist!" );

		if ( grid.IsConstructionBlocked( tilePosition, Path.PATH_HEIGHT ) ) return null;

		var terrain = grid.Terrain;

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

		go.WorldPosition = GridManager.GridToWorldPosition( tilePosition ) + GridManager.CentreOffset;
		go.Tags.Add( "path" );

		var path = type == PathType.Queue ? go.AddComponent<Queue>() : go.AddComponent<Path>();
		path.PathType = type;

		if ( elevation is not PathElevation.Flat )
		{
			path.SetStair( direction, elevation );
		}

		var collider = go.GetOrAddComponent<PlaneCollider>();
		collider.Tags.Add( "path" );
		collider.Scale = GridManager.GridSize;

		var gridObject = go.GetOrAddComponent<GridObject>();
		gridObject.IsWalkable = true;
		gridObject.Height = Path.PATH_HEIGHT;
		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 path object if we're the host
			go.NetworkSpawn();
		}

		return path;
	}

	[Rpc.Host]
	public void DeletePath( Path path )
	{
		var pathCost = 10;
		MoneyEffect.Broadcast( path.WorldPosition + Vector3.Up * 10f, $"-{GameUtils.Currency}{pathCost}", Color.Green );

		path.GameObject.DestroyImmediate();

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

	public void DeletePath( Vector3Int gridPos )
	{
		var gridPos2d = new Vector2Int( gridPos );
		if ( !BuildingZone.Instance.IsOwned( gridPos2d ) ) return;

		var grid = GridManager.Instance;

		if ( grid.GetCell( new Vector2Int( gridPos2d ) )?.GetComponents<Path>( gridPos.z )?.Where( x => !x.GhostPreview ).FirstOrDefault() is not { } path )
			return;

		DeletePath( path );
	}

	public static bool IsPathableSlope( TileSlope slope )
	{
		var gradients = slope.GetGradients();

		if ( gradients.x == 0 && gradients.y == 0 )
			return true;

		return Math.Min( Math.Abs( gradients.x ), Math.Abs( gradients.y ) ) == 0
			&& Math.Max( Math.Abs( gradients.x ), Math.Abs( gradients.y ) ) == 1;
	}

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

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

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

		// Can we afford to place the path?
		if ( !ParkManager.Instance?.TakeMoney( Path.PATH_COST, "Paths" ) ?? false )
			return;

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

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

		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}{Path.PATH_COST}", Color.Red );
	}

	/// <summary>
	/// Place a new path. This is called on the host.
	/// </summary>
	[Rpc.Host]
	public void PlacePath( Vector3Int gridPos, PathType type, TileEdge direction, PathElevation elevation )
	{
		var caller = Rpc.Caller;

		// Can we afford to place the path?
		if ( !ParkManager.Instance?.TakeMoney( Path.PATH_COST, "Paths" ) ?? false )
			return;

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

		if ( AddPath( gridPos, type, direction, elevation ) is not { } path )
			return;

		GameObject.Clone( "prefabs/particles/place_dustcloud.prefab", new Transform( path.WorldPosition + Vector3.Up * 8 ) );
		Sound.Play( "sounds/gameplay/building_placed.sound", path.WorldPosition );
		MoneyEffect.Broadcast( path.WorldPosition + Vector3.Up * 10f, $"-{GameUtils.Currency}{Path.PATH_COST}", Color.Red );

		var nextPosition = gridPos + Vector3Int.Up * path.GetHeightOffset( direction );
		if ( elevation is PathElevation.StairDown )
		{
			nextPosition += Vector3Int.Down;
		}

		using ( Rpc.FilterInclude( caller ) )
		{
			// tell the client where they should go next
			SetCursorPosition( nextPosition );
		}
	}

	[Rpc.Broadcast( NetFlags.HostOnly )]
	public void SetCursorPosition( Vector3Int gridPos )
	{
		CursorPosition = gridPos;
	}

	public bool CanPlace( Vector3Int pos )
	{
		var grid = GridManager.Instance;

		if ( grid.IsConstructionBlocked( pos, Path.PATH_HEIGHT ) ) return false;

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

		var tile = grid.Terrain[new Vector2Int( pos )];

		if ( pos.z < tile.MaxHeight )
			return false; // tunnels??

		if ( pos.z == tile.MaxHeight )
		{
			// Check steepness of hill
			return tile.MaxHeight - tile.MinHeight <= 1;
		}

		return true;
	}
}