WorldBuilder/World.cs
using System;
using System.Diagnostics;
using System.Text.Json.Serialization;
using Clover.Data;
using Clover.Items;
using Clover.Player;

namespace Clover;

/// <summary>
///  Represents a game world where players can interact with items and objects.
///  A world component is added to a world prefab root, which is spawned into the scene by the WorldManager.
///  Any saved items/objects in the world are children of the world prefab root.
/// </summary>
[Category( "Clover/World" )]
[Icon( "world" )]
public sealed partial class World : Component
{
	public enum ItemPlacementType
	{
		Placed = 1,
		Dropped = 2,
	}

	public enum ItemRotation
	{
		North = 1,
		East = 2,
		South = 3,
		West = 4
	}

	public enum Direction
	{
		North = 1,
		South = 2,
		West = 3,
		East = 4,
		NorthWest = 5,
		NorthEast = 6,
		SouthWest = 7,
		SouthEast = 8
	}

	public static float GridSize = 32f;
	public static float GridSizeCenter = GridSize / 2f;

	[Property] public WorldData Data { get; set; }

	// TODO: player should be able to set this depending on world
	public string Title => Data.Title;


	public delegate void OnItemAddedEvent( WorldItem worldItem );

	public event OnItemAddedEvent OnItemAdded;

	public delegate void OnItemRemovedEvent( WorldItem worldItem );

	public event OnItemRemovedEvent OnItemRemoved;

	// public Dictionary<Vector2Int, Dictionary<ItemPlacement, WorldNodeLink>> Items { get; set; } = new();

	// TODO: should this be synced?
	private HashSet<Vector2Int> BlockedTiles { get; set; } = new();

	[Sync] private Dictionary<Vector2Int, float> TileHeights { get; set; } = new();

	// TODO: some kind of interface for both WorldItem and WorldObject?
	public HashSet<WorldItem> WorldItems { get; set; } = new();
	public HashSet<WorldObject> WorldObjects { get; set; } = new();


	[Sync] public int Layer { get; set; }

	public string WorldId => Data.ResourceName;

	[JsonIgnore]
	public IEnumerable<PlayerCharacter> PlayersInWorld =>
		Scene.GetAllComponents<PlayerCharacter>().Where( p => p.WorldLayerObject.Layer == Layer );

	public bool ShouldUnloadOnExit
	{
		get => !PlayersInWorld.Any();
	}

	public bool IsBlockedGridPosition( Vector2Int position )
	{
		if ( BlockedTiles.Contains( position ) ) return true;

		CheckTerrainAt( position );

		return BlockedTiles.Contains( position );
	}

	public float GetHeightAt( Vector2Int position )
	{
		if ( TileHeights.TryGetValue( position, out var height ) )
		{
			return height;
		}

		CheckTerrainAt( position );

		if ( TileHeights.TryGetValue( position, out height ) )
		{
			return height;
		}

		return 0;
	}

	public void CheckTerrainAt( Vector2Int position )
	{
		var check = CheckGridPositionEligibility( position, out var worldPos );

		if ( worldPos.z != 0 )
		{
			TileHeights[position] = worldPos.z;
		}

		if ( !check )
		{
			BlockedTiles.Add( position );
		}
	}

	/// <summary>
	///  Checks if a player is in the way of an item placement.
	/// </summary>
	/// <param name="positions"></param>
	/// <returns></returns>
	public bool CheckPlayerObstruction( List<Vector2Int> positions )
	{
		foreach ( var player in PlayersInWorld )
		{
			if ( positions.Any( x => ItemGridToWorld( x ).Distance( player.WorldPosition ) < 25 ) )
			{
				Log.Warning( $"Player {player.PlayerName} is in the way" );
				return true;
			}
		}

		return false;
	}

	public bool IsOutsideGrid( Vector2Int position )
	{
		return position.x < 0 || position.y < 0 || position.x >= Data.Width || position.y >= Data.Height;
	}

	public bool CheckGridPositionEligibility( Vector2Int position, out Vector3 worldPosition )
	{
		if ( IsOutsideGrid( position ) )
		{
			worldPosition = Vector3.Zero;
			return false;
		}

		if ( BlockedTiles.Contains( position ) )
		{
			worldPosition = Vector3.Zero;
			return false;
		}

		// trace a ray from the sky straight down in each corner, if height is the same on all corners then it's a valid position

		var basePosition = ItemGridToWorld( position, true );

		var margin = GridSizeCenter * 0.8f;
		var heightTolerance = 0.1f;

		var traceDistance = 2000f;
		var baseHeight = basePosition.z + 1000;

		var topLeft = new Vector3( basePosition.x - margin, basePosition.y - margin, baseHeight );
		var topRight = new Vector3( basePosition.x + margin, basePosition.y - margin, baseHeight );
		var bottomLeft = new Vector3( basePosition.x - margin, basePosition.y + margin, baseHeight );
		var bottomRight = new Vector3( basePosition.x + margin, basePosition.y + margin, baseHeight );

		var traceTopLeft = Scene.Trace.Ray( topLeft, topLeft + (Vector3.Down * traceDistance) )
			.WithAnyTags( "terrain", "floor" )
			.Run();
		var traceTopRight = Scene.Trace.Ray( topRight, topRight + (Vector3.Down * traceDistance) )
			.WithAnyTags( "terrain", "floor" )
			.Run();
		var traceBottomLeft = Scene.Trace.Ray( bottomLeft, bottomLeft + (Vector3.Down * traceDistance) )
			.WithAnyTags( "terrain", "floor" )
			.Run();
		var traceBottomRight = Scene.Trace.Ray( bottomRight, bottomRight + (Vector3.Down * traceDistance) )
			.WithAnyTags( "terrain", "floor" )
			.Run();

		if ( !traceTopLeft.Hit || !traceTopRight.Hit || !traceBottomLeft.Hit || !traceBottomRight.Hit )
		{
			Log.Warning( $"Ray trace failed at {position} ({basePosition}), start height is {baseHeight}" );
			worldPosition = Vector3.Zero;
			return false;
		}

		var heightTopLeft = traceTopLeft.HitPosition.z - WorldPosition.z;
		var heightTopRight = traceTopRight.HitPosition.z - WorldPosition.z;
		var heightBottomLeft = traceBottomLeft.HitPosition.z - WorldPosition.z;
		var heightBottomRight = traceBottomRight.HitPosition.z - WorldPosition.z;

		if ( heightTopLeft <= -50 )
		{
			Log.Warning( $"Height at {position} is below -50" );
		}

		if ( Math.Abs( heightTopLeft - heightTopRight ) > heightTolerance ||
		     Math.Abs( heightTopLeft - heightBottomLeft ) > heightTolerance ||
		     Math.Abs( heightTopLeft - heightBottomRight ) > heightTolerance )
		{
			Log.Trace(
				$"Height difference at {position} is too high ({heightTopLeft}, {heightTopRight}, {heightBottomLeft}, {heightBottomRight})" );
			worldPosition = Vector3.Zero;
			return false;
		}

		if ( heightTopLeft.AlmostEqual( 0f ) ) heightTopLeft = 0f;

		if ( Data.MaxItemAltitude > 0 && heightTopLeft > Data.MaxItemAltitude )
		{
			Log.Warning( $"Height at {position} is too high: {heightTopLeft}" );
			worldPosition = Vector3.Zero;
			return false;
		}

		worldPosition = new Vector3( basePosition.x, basePosition.y, heightTopLeft );

		return true;
	}

	public void Setup()
	{
		var layerObjects = GetComponentsInChildren<WorldLayerObject>( true );
		foreach ( var layerObject in layerObjects )
		{
			layerObject.SetLayer( Layer );
		}

		Scene.NavMesh.Generate( Scene.PhysicsWorld );
	}

	public WorldEntrance GetEntrance( string entranceId )
	{
		var entrances = GetComponentsInChildren<WorldEntrance>( true );

		foreach ( var entrance in entrances )
		{
			if ( entrance.EntranceId == entranceId )
			{
				return entrance;
			}
		}

		return null;
	}

	public void OnWorldLoaded()
	{
		if ( IsProxy ) return;
		Log.Info( $"World {WorldId} loaded" );
	}

	public void OnWorldUnloaded()
	{
		if ( IsProxy ) return;
		Log.Info( $"World {WorldId} unloaded" );
		Save();
	}

	protected override void OnUpdate()
	{
		base.OnUpdate();

		if ( ShowGrid )
		{
			Gizmo.Draw.Grid( Gizmo.GridAxis.XY, 32f );
		}

		if ( !ShowGridInfo ) return;

		if ( !Game.IsEditor || Gizmo.Camera == null || Layer != WorldManager.Instance.ActiveWorldIndex ) return;

		Gizmo.Transform = new Transform( WorldPosition );
		// Log.Info( WorldId + ": " + WorldPosition  );

		foreach ( var item in WorldItems )
		{
			Gizmo.Draw.Text( $"{item.GridPosition} ({item.GetName()})",
				new Transform( ItemGridToWorld( item.GridPosition ) ) );

			Gizmo.Draw.LineSphere( item.WorldPosition, 8f );
		}

		/*foreach ( var pos in _blockedTiles )
		{
			Gizmo.Draw.Text( pos.ToString(), new Transform( ItemGridToWorld( pos ) ) );
		}

		*/

		/*var i = 0;
		foreach ( var item in _nodeLinkGridMap )
		{
			// var pos = item.Key.Split( ':' )[0].Split( ',' ).Select( int.Parse ).ToArray();
			var offset = item.Key.Placement == ItemPlacement.OnTop ? Vector3.Up * 32f : Vector3.Zero;
			Gizmo.Draw.Text( $"{item.Key.Position} {item.Key.Placement} | {item.Value.GetName()}\n{item.Value.GridRotation}",
				new Transform( ItemGridToWorld( item.Key.Position ) + offset ) );

			Gizmo.Draw.ScreenText( $"[{item.Key.Position}:{item.Key.Placement}] {item.Value.GetName()}",
				new Vector2( 20f, 20f + ((i++) * 20f) ) );
		}*/
	}

	[ConVar( "clover_show_grid_info" )] public static bool ShowGridInfo { get; set; }

	[ConVar( "clover_show_grid" )] public static bool ShowGrid { get; set; }

	public Vector3 GetRelativePosition( Vector3 worldPosition )
	{
		return worldPosition - WorldPosition;
	}

	public Rotation GetRelativeRotation( Rotation worldRotation )
	{
		return Rotation.Difference( worldRotation, WorldRotation );
	}
}