Park/Buildings/BuildingPlacer.cs
using HC3.Terrain;
using HC3.UI;
using System;
using HC3.Rides;

namespace HC3;

public sealed class BuildingPlacer : Component, IBuilder
{
	public static BuildingPlacer Instance { get; private set; } = null;

	[Property] public GameObject EntrancePrefab { get; set; }
	[Property] public GameObject ExitPrefab { get; set; }
	[Property] public Color RedChannel { get; private set; } = Color.White;
	[Property] public Color GreenChannel { get; private set; } = Color.White;
	[Property] public Color BlueChannel { get; private set; } = Color.White;
	[Property] public Color AlphaChannel { get; private set; } = Color.White;

	public bool IsPlacing => ObjectToPlace.IsValid();

	private bool _isDestroying = false;
	public bool IsDestroying
	{
		get => _isDestroying;
		set
		{
			_isDestroying = value;
			if ( _isDestroying )
			{
				StopPlacing();
				WindowManager.Instance.DeactivateAll();
			}
			else
			{
				ClearHoverGhost();
			}
		}
	}

	public IPlacementObject PlacingResource { get; private set; } = null;
	public IPlacementObject ObjectToPlace { get; private set; } = null;

	public BasicRide RideContext { get; private set; }
	public Vector3 PlacingPosition { get; private set; } = 0;
	public TileEdge PlacingEdge { get; private set; }
	public float DecorationHeightOffset { get; set; } = 0f;

	int _angle = 0;
	int _pitch = 0;
	int _height;
	Color _decorationColor = Color.White;

	public void Activate()
	{
		(this as IBuilder).DeactivateAll();
	}

	public void Deactivate()
	{
		IsDestroying = false;
		StopPlacing();
	}

	GameObject HeightTool { get; set; } = Scene.GetPrefab( "prefabs/gameplay/height_tool.prefab" );
	GameObject _heightTool = null;
	GameObject _lastHoveredObject = null;

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

	private void SetupGhostVisuals( GameObject go )
	{
		foreach ( var renderer in go.Components.GetAll<ModelRenderer>( FindMode.EverythingInSelfAndDescendants ) )
		{
			if ( renderer.SceneObject is null ) continue;
			renderer.SceneObject.Attributes.Set( "Ghost", 1 );
			renderer.SceneObject.Batchable = false;
		}
	}

	private void ClearHoverGhost()
	{
		if ( _lastHoveredObject != null )
		{
			foreach ( var renderer in _lastHoveredObject.Components.GetAll<ModelRenderer>( FindMode.EverythingInSelfAndDescendants ) )
			{
				renderer.SceneObject?.Attributes.Set( "Ghost", 0 );
			}
			_lastHoveredObject = null;
		}
	}

	public void StartPlacing( IPlacementObject placable )
	{
		IsDestroying = false;
		if ( IsPlacing && placable == PlacingResource ) return;

		StopPlacing();

		PlacingResource = placable;
		ObjectToPlace = placable.GameObject.Clone().GetComponent<IPlacementObject>();
		ObjectToPlace.IsPlaced = false;

		SetupGhostVisuals( ObjectToPlace.GameObject );

		WindowManager.Instance.DeactivateAll();
	}

	public void StopPlacing()
	{
		if ( ObjectToPlace.IsValid() )
		{
			ObjectToPlace.GameObject.Destroy();
		}

		ObjectToPlace = null;
		PlacingResource = null;
		RideContext = null;
		_lastAutoRotatePos = default;
	}

	private void ShowDestroyDebug( SceneTraceResult tr )
	{
		var go = tr.GameObject;
		var worldPos = go.Root.WorldPosition;
		var screenPos = Scene.Camera.PointToScreenPixels( worldPos );

		if ( go.Components.TryGet<TerrainScenery>( out var scenery ) )
		{
			DebugOverlay.ScreenText( screenPos, $"DESTROY\n{GameUtils.Currency}{scenery.DestructionCost}", 14, TextFlag.CenterBottom, Color.Red );
		}
	}

	private void TryDestroyObject( SceneTraceResult tr )
	{
		var go = tr.GameObject;

		if ( go.Root.Components.TryGet<Building>( out var building ) )
		{
			if ( Input.Keyboard.Down( "shift" ) )
			{
				ParkManager.Instance?.DestroyObject( building );
			}
			else
			{
				Query.Create( "Delete Building", "Are you sure you want to delete this?", () => ParkManager.Instance?.DestroyObject( building ) );
			}
		}
		else if ( go.Components.TryGet<TerrainScenery>( out var scenery ) && ParkManager.Instance.Money >= scenery.DestructionCost )
		{
			ParkManager.Instance?.DestroyObject( scenery );
		}
		else if ( go.Components.TryGet<PathFurniture>( out var furniture, FindMode.EverythingInSelfAndParent ) )
		{
			ParkManager.Instance?.DestroyObject( furniture );
		}
		else if ( go.Root.Components.TryGet<Decoration>( out var deco ) )
		{
			ParkManager.Instance?.DestroyObject( deco );
		}
		else if ( go.Components.TryGet<Path>( out var path ) )
		{
			PathBuilder.Instance.DeletePath( path );
		}
	}

	protected override void OnUpdate()
	{
		if ( !ObjectToPlace.IsValid() && !IsDestroying )
			return;

		if ( Input.EscapePressed )
		{
			Input.EscapePressed = false;
			IsDestroying = false;
			StopPlacing();
			return;
		}

		if ( Input.Keyboard.Down( "MOUSE2" ) || Input.Keyboard.Down( "MOUSE3" ) )
		{
			if ( ObjectToPlace.IsValid() )
			{
				ClearHoverGhost();
				ObjectToPlace.GameObject.Enabled = false;
			}
			return;
		}
		else
		{
			if ( ObjectToPlace.IsValid() )
			{
				ObjectToPlace.GameObject.Enabled = true;
				SetupGhostVisuals( ObjectToPlace.GameObject );

			}
		}

		var trace = Scene.Trace.Ray( Scene.Camera.ScreenPixelToRay( Mouse.Position ), 4000f )
			.UsePhysicsWorld()
			.HitTriggers()
			.WithAnyTags( IsDestroying ? ["building", "decoration", "furniture", "path", "area"] : ["path", "ground"] )
			.Run();

		if ( IsDestroying )
		{
			if ( trace.Hit )
			{
				var hovered = trace.GameObject;
				if ( hovered != _lastHoveredObject )
				{
					ClearHoverGhost();
					foreach ( var renderer in hovered.Components.GetAll<ModelRenderer>( FindMode.EverythingInSelfAndDescendants ) )
					{
						renderer.SceneObject.Batchable = false;
						renderer.SceneObject?.Attributes.Set( "Ghost", 2 );
					}
					_lastHoveredObject = hovered;
				}

				ShowDestroyDebug( trace );

				if ( Input.Pressed( "Attack1" ) )
					TryDestroyObject( trace );
			}
			else
			{
				ClearHoverGhost();
			}

			return;
		}

		TryAutoRotate();

		if ( CanRotate )
		{
			if ( Input.Pressed( "Menu" ) )
			{
				if ( ObjectToPlace is Decoration && Input.Down( "run" ) )
				{
					_pitch += 90;
					if ( _pitch >= 270 ) _pitch = 0;
				}
				else
				{
					_angle += 90;
					if ( _angle > 270 ) _angle = 0;
				}
			}
			if ( Input.Pressed( "Use" ) )
			{
				if ( ObjectToPlace is Decoration && Input.Down( "run" ) )
				{
					_pitch -= 90;
					if ( _pitch < 0 ) _pitch = 0;
				}
				else
				{
					_angle -= 90;
					if ( _angle < 0 ) _angle = 270;
				}
			}
		}

		if ( trace.Hit )
		{
			switch ( ObjectToPlace )
			{
				case Animal animal:
					PlacingPosition = trace.HitPosition;
					animal.WorldPosition = PlacingPosition;
					break;
				case Building building:
					var tileRange = building.GetTileRange( _angle );
					var heightRange = GridManager.Instance.Terrain.GetHeightRange( tileRange );
					_height = heightRange.Max;

					var center = new Vector3( (tileRange.Size.x / 2f) * GridManager.GridSize, (tileRange.Size.y / 2f) * GridManager.GridSize, 0 );
					var gridOffset = center - GridManager.AlignWorldToGrid( center );

					PlacingPosition = building.SnapToGrid ? GridManager.AlignWorldToGrid( trace.HitPosition ) + gridOffset : trace.HitPosition;
					PlacingPosition = PlacingPosition.WithZ( _height * GridManager.HeightStep );

					building.WorldPosition = PlacingPosition;
					building.WorldRotation = new Angles( 0, _angle, 0 );

					foreach ( var renderer in building.Components.GetAll<ModelRenderer>( FindMode.EverythingInSelfAndDescendants ) )
						renderer.SceneObject?.Attributes.Set( "Ghost", CanPlace() ? 1 : 2 );
					break;

				case PathFurniture furniture:
					var tileCenter = GridManager.AlignWorldToGrid( trace.HitPosition ) + GridManager.CentreOffset;
					var offset = (trace.HitPosition - tileCenter).WithZ( 0 );
					if ( MathF.Abs( offset.x ) > MathF.Abs( offset.y ) )
					{
						PlacingEdge = offset.x > 0 ? TileEdge.Right : TileEdge.Left;
					}
					else
					{
						PlacingEdge = offset.y > 0 ? TileEdge.Up : TileEdge.Down;
					}

					var direction = PlacingEdge.GetDirection();
					PlacingPosition = tileCenter + new Vector3( direction * GridManager.CentreOffset * PathFurniture.GetPathOffset( furniture.FurnitureType ) );
					_angle = (MathF.Atan2( direction.y, direction.x ).RadianToDegree()).RoundToInt();

					furniture.WorldPosition = PlacingPosition;
					furniture.WorldRotation = new Angles( 0, _angle, 0 );

					foreach ( var renderer in furniture.Components.GetAll<ModelRenderer>( FindMode.EverythingInSelfAndDescendants ) )
						renderer.SceneObject?.Attributes.Set( "Ghost", CanPlace() ? 1 : 2 );
					break;

				case Decoration decoration:
					PlacingPosition = trace.HitPosition;

					if ( Input.Down( "run" ) )
					{
						DecorationHeightOffset += Input.MouseWheel.y * 8.0f;

						if ( !_heightTool.IsValid() )
							_heightTool = HeightTool.Clone();
						else
						{
							var tool = _heightTool.GetComponent<HeightTool>();
							tool.UpdateStartPos( PlacingPosition );
							tool.UpdateEndPos( PlacingPosition + Vector3.Up * DecorationHeightOffset );
							tool.UpdateSprite( DecorationHeightOffset );
						}
					}
					else if ( _heightTool.IsValid() )
					{
						_heightTool.Destroy();
						_heightTool = null;
					}

					DecorationHeightOffset = DecorationHeightOffset.SnapToGrid( 8f );
					PlacingPosition = PlacingPosition.WithZ( PlacingPosition.z + DecorationHeightOffset );

					// Inverse grid snapping logic
					bool shouldSnap = decoration.SnapToGrid != Input.Down( "duck" );

					if ( shouldSnap )
					{
						PlacingPosition = trace.HitPosition.SnapToGrid( GridManager.GridSize ) - GridManager.GridSize * 0.5f + Vector3.Up * GridManager.HeightStep;
						decoration.WorldPosition = PlacingPosition.WithZ( PlacingPosition.z + DecorationHeightOffset );
					}
					else
					{
						decoration.WorldPosition = PlacingPosition;
					}

					decoration.WorldRotation = new Angles( _pitch, _angle, 0 );

					foreach ( var renderer in decoration.Components.GetAll<ModelRenderer>( FindMode.EverythingInSelfAndDescendants ) )
						renderer.SceneObject?.Attributes.Set( "Ghost", 1 );
					break;
			}
		}

		if ( Input.Pressed( "Attack1" ) )
		{
			if ( !BuildingZone.Instance.IsOwned( PlacingPosition ) )
			{
				Sound.Play( "creature_error_01" );
				DebugOverlay.ScreenText( Screen.Size / 2, "You don't own this land!", 14, TextFlag.CenterBottom, Color.Red, duration: 1 );
				return;
			}

			switch ( ObjectToPlace )
			{
				case Animal:
					TryPlaceAnimal();
					break;
				case Building:
					TryPlace();
					break;
				case PathFurniture:
					TryPlaceFurniture();
					break;
				case Decoration:
					TryPlaceDecoration();
					break;
			}
		}
	}

	private bool CanRotate => PlacingResource is not RideEntranceExit;

	void TryPlaceAnimal()
	{
		if ( !CanPlace() ) return;

		var root = PlacingResource.GameObject.Root ?? PlacingResource.GameObject;
		var path = root.GetPrefabResourcePath();

		ParkManager.Instance?.PlacePlaceable( path, ObjectToPlace.WorldPosition, _angle, _height );
	}

	void TryPlace()
	{
		if ( !CanPlace() ) return;

		var root = PlacingResource.GameObject.Root ?? PlacingResource.GameObject;
		var path = root.GetPrefabResourcePath();

		ParkManager.Instance?.PlaceBuilding( path, ObjectToPlace.WorldPosition, _angle, _height );
	}

	private Vector3Int _lastAutoRotatePos;

	void TryAutoRotate()
	{
		if ( !ObjectToPlace.IsValid() ) return;

		var gridPos = CurrentGridPosition;

		if ( gridPos == _lastAutoRotatePos ) return;

		_lastAutoRotatePos = gridPos;

		if ( ObjectToPlace is RideEntranceExit )
		{
			if ( !RideContext.IsValid() ) return;

			var match = RideContext.ValidEntranceExitPositions
				.FirstOrDefault( x => x.GridPos == gridPos );

			if ( match.GridPos == gridPos )
			{
				var dir = match.Edge.GetDirection();
				_angle = (int)MathF.Round( Rotation.LookAt( -new Vector3( dir.x, dir.y ) ).Yaw() );
			}

			return;
		}

		// Kinda sucky
		if ( ObjectToPlace is Building building && building is { BuildingType: BuildingType.Shop, PathMask: var mask } )
		{
			if ( mask == 0 ) return;

			var grid = GridManager.Instance;

			PathMask pathMask = 0;

			foreach ( var edge in GridManager.AllEdges )
			{
				var dir = edge.GetDirection();
				var neighborPos = new Vector2Int( gridPos.x, gridPos.y ) + dir;

				var hasPath = grid.GetCell( neighborPos )?
					.GetComponents<Path>()
					.Any( x => x is not Queue ) ?? false;

				if ( hasPath )
				{
					pathMask |= edge.RotateDegrees( _angle ).ToPathMask();
				}
			}

			if ( pathMask == 0 ) return;

			var attempts = 0;

			while ( (pathMask & mask) == 0 && attempts++ < 4 )
			{
				_angle += 90;
				pathMask = pathMask.Rotate90DegreesClockwise();
			}

			return;
		}
	}

	void TryPlaceFurniture()
	{
		if ( !ObjectToPlace.IsValid() ) return;
		if ( ObjectToPlace is not PathFurniture ) return;

		if ( !CanPlace() ) return;

		var gridPos = GridManager.WorldToGridPosition3D( PlacingPosition );
		ParkManager.Instance?.PlaceFurniture( ObjectToPlace.PrefabSource, gridPos, PlacingEdge );
	}

	void TryPlaceDecoration()
	{
		if ( !ObjectToPlace.IsValid() ) return;

		ParkManager.Instance?.PlaceDecoration(
			ObjectToPlace.PrefabSource,
			ObjectToPlace.WorldPosition,
			_angle, _pitch, _height,
			_decorationColor,
			RedChannel, GreenChannel, BlueChannel, AlphaChannel
		);
	}

	public bool CanPlace()
	{
		if ( !IsPlacing ) return false;
		if ( (ParkManager.Instance?.Money ?? 0) < PlacingResource.Cost ) return false;

		var gridManager = GridManager.Instance;
		if ( gridManager is null ) return false;

		if ( ObjectToPlace is Building building )
		{
			var range = building.GetTileRange( _angle );

			if ( GridManager.Instance.IsConstructionBlocked( range.Position, range.Position + range.Size - 1 ) )
				return false;

			if ( building is RideEntranceExit )
			{
				return IsValidRideEntranceExitPlacement();
			}
		}
		else if ( ObjectToPlace is PathFurniture furniture )
		{
			var gridPos = GridManager.WorldToGridPosition3D( ObjectToPlace.WorldPosition );
			var cell = gridManager.GetCell( new Vector2Int( gridPos ) );

			return cell?.GetComponents<Path>( gridPos.z ).Any( path => furniture.IsAllowed( path, PlacingEdge ) ) ?? false;
		}

		return true;
	}

	private Vector3Int CurrentGridPosition
	{
		get
		{
			if ( !ObjectToPlace.IsValid() ) return default;

			var building = ObjectToPlace as Building;

			if ( !building.IsValid() ) return default;

			var gridPos2d = building.GetTileRange( _angle ).Position;
			var height = GridManager.RoundWorldHeightToGrid( ObjectToPlace.WorldPosition.z );

			return new Vector3Int( gridPos2d.x, gridPos2d.y, height );
		}
	}

	private bool IsValidRideEntranceExitPlacement()
	{
		if ( !RideContext.IsValid() ) return false;

		var gridPos = CurrentGridPosition;

		var match = RideContext.ValidEntranceExitPositions.FirstOrDefault( x => x.GridPos == gridPos );
		if ( match.GridPos != gridPos ) return false;

		var dir = match.Edge.GetDirection();
		var expectedAngle = (int)MathF.Round( Rotation.LookAt( -new Vector3( dir.x, dir.y ) ).Yaw() );

		return _angle == expectedAngle;
	}

	public void StartPlacingEntrance( BasicRide ride )
	{
		StartPlacing( EntrancePrefab.GetComponent<Building>() );
		RideContext = ride;
	}

	public void StartPlacingExit( BasicRide ride )
	{
		StartPlacing( ExitPrefab.GetComponent<Building>() );
		RideContext = ride;
	}

	public void SetDecorationColor( Color color, TintChannel channel )
	{
		if ( ObjectToPlace.IsValid() )
		{
			var tintComponent = ObjectToPlace.GetTintComponent();
			if ( !tintComponent.IsValid() )
				return;

			switch ( channel )
			{
				case TintChannel.Primary:
					RedChannel = color;
					break;
				case TintChannel.Secondary:
					GreenChannel = color;
					break;
				case TintChannel.Accent:
					BlueChannel = color;
					break;
				case TintChannel.Detail:
					AlphaChannel = color;
					break;
			}
		}
	}

	/// <summary>
	/// A building was placed by someone.
	/// </summary>
	public void OnBuildingPlaced( Building building )
	{
		Stats.Increment( $"building.placed" );
		Stats.Increment( $"building.placed.{building.Title.ToIdentifier()}" );

		switch ( building )
		{
			case TrackRide trackRide:
				StopPlacing();
				TrackBuilder.Instance?.StartBuilding( trackRide );
				break;

			case BasicRide ride:
				StopPlacing();
				SelectionSystem.Instance.Select( building.GameObject );

				StartPlacingEntrance( ride );
				return;
			case RideEntranceExit { IsExit: false }:
				if ( RideContext.IsValid() )
				{
					if ( RideContext.Entrance.IsValid() )
					{
						RideContext.Entrance.DestroyGameObject();
					}

					if ( !RideContext.HasExit )
					{
						StartPlacingExit( RideContext );
						return;
					}
				}
				break;
			case RideEntranceExit { IsExit: true }:
				if ( RideContext.IsValid() && RideContext.Exit.IsValid() )
				{
					RideContext.Exit.DestroyGameObject();
				}
				break;
		}

		StopPlacing();
	}
}