WorldBuilder/World.Spawn.cs
using System;
using Clover.Data;
using Clover.Items;
using Clover.Persistence;
using Sandbox.Diagnostics;

namespace Clover;

public sealed partial class World
{
	public WorldItem SpawnPlacedItem( ItemData itemData, Vector2Int position, ItemRotation rotation )
	{
		return SpawnNode( itemData, ItemPlacementType.Placed, position, rotation );
	}

	public WorldItem SpawnPlacedItem( PersistentItem persistentItem, Vector2Int position, ItemRotation rotation )
	{
		var itemData = persistentItem.ItemData;
		var worldItem = SpawnPlacedItem( itemData, position, rotation );
		worldItem.LoadPersistence( persistentItem );
		return worldItem;
	}

	public WorldItem SpawnPlacedItem( ItemData itemData, Vector3 position, Rotation rotation )
	{
		return SpawnNode( itemData, ItemPlacementType.Placed, position, rotation );
	}

	public WorldItem SpawnPlacedItem( PersistentItem persistentItem, Vector3 position, Rotation rotation )
	{
		var itemData = persistentItem.ItemData;
		var worldItem = SpawnPlacedItem( itemData, position, rotation );
		worldItem.LoadPersistence( persistentItem );
		return worldItem;
	}

	public WorldItem SpawnDroppedNode( ItemData itemData, Vector2Int position, ItemRotation rotation )
	{
		return SpawnNode( itemData, ItemPlacementType.Dropped, position, rotation );
	}

	public WorldItem SpawnDroppedNode( PersistentItem persistentItem, Vector2Int position, ItemRotation rotation )
	{
		var itemData = persistentItem.ItemData;
		var worldItem = SpawnDroppedNode( itemData, position, rotation );
		worldItem.LoadPersistence( persistentItem );
		return worldItem;
	}

	public WorldItem SpawnCustomItem( ItemData itemData, GameObject prefab, Vector2Int position,
		ItemRotation rotation )
	{
		if ( IsOutsideGrid( position ) )
		{
			throw new Exception( $"Position {position} is outside the grid" );
		}

		/*if ( !itemData.Placements.HasFlag( placement ) )
		{
			throw new Exception( $"Item {itemData.Name} does not support placement {placement}" );
		}*/

		var positions = itemData.GetGridPositions( rotation, position );

		/*if ( !CanPlaceItem( positions, placement ) )
		{
			throw new Exception( $"Cannot place item {itemData.Name} at {position} with placement {placement}" );
		}*/

		if ( prefab == null )
		{
			throw new Exception( $"Item {itemData.Name} has no scene" );
		}

		var gameObject = prefab.Clone();
		if ( !gameObject.IsValid() )
		{
			throw new Exception( $"Failed to clone scene for {itemData.Name}" );
		}

		gameObject.WorldPosition = ItemGridToWorld( position );
		gameObject.WorldRotation = GetRotation( rotation );

		var nodeLink = AddItem( position, rotation, gameObject );

		nodeLink.SetItemData( itemData );
		nodeLink.ItemPlacement = ItemPlacementType.Placed;

		nodeLink.CalculateSize();

		// UpdateTransform( nodeLink );

		// nodeLink.OnNodeAdded();

		gameObject.NetworkSpawn();

		var worldItem = nodeLink.GetComponent<WorldItem>();

		return worldItem;
	}

	private WorldItem SpawnNode( ItemData itemData, ItemPlacementType placementType, Vector2Int position,
		ItemRotation rotation )
	{
		Assert.NotNull( itemData, "Item data is null" );

		if ( IsOutsideGrid( position ) )
		{
			throw new Exception( $"Position {position} is outside the grid" );
		}

		/*if ( !itemData.Placements.HasFlag( placement ) )
		{
			throw new Exception( $"Item {itemData.Name} does not support placement {placement}" );
		}*/


		var defaultDropScene = GameObject.GetPrefab( "items/misc/dropped_item/dropped_item.prefab" );

		var spawnPrefab = placementType switch
		{
			ItemPlacementType.Placed => itemData.PlaceScene,
			ItemPlacementType.Dropped => itemData.DropScene ?? defaultDropScene,
			_ => throw new ArgumentOutOfRangeException( nameof(placementType), placementType, null )
		};

		if ( !spawnPrefab.IsValid() )
		{
			throw new Exception( $"Item {(itemData.Name ?? itemData.ResourceName)} has no {placementType} scene" );
		}

		// dropped items are always 1x1
		var positions = placementType == ItemPlacementType.Dropped
			? new List<Vector2Int> { position }
			: itemData.GetGridPositions( rotation, position );

		/*if ( !CanPlaceItem( positions, placement ) )
		{
			throw new Exception( $"Cannot place item {itemData.Name} at {position} with placement {placement}" );
		}*/

		var gameObject = spawnPrefab.Clone();

		gameObject.WorldPosition = ItemGridToWorld( position );
		gameObject.WorldRotation = GetRotation( rotation );

		var nodeLink = AddItem( position, rotation, gameObject );

		nodeLink.SetItemData( itemData );
		nodeLink.ItemPlacement = ItemPlacementType.Placed;

		// replace itemdata with the one from the item, mainly for dropped items
		/*if ( nodeLink.Components.TryGet<WorldItem>( out var worldItem ) )
		{
			worldItem.ItemData = itemData;
		}*/

		// nodeLink.CalculateSize();

		// UpdateTransform( nodeLink );

		gameObject.Name = nodeLink.GetName();

		// AddNodeLinkToGridMap( nodeLink );

		WorldItems.Add( nodeLink );

		// nodeLink.OnNodeAdded();

		gameObject.NetworkSpawn();

		return nodeLink;
	}

	private WorldItem SpawnNode( ItemData itemData, ItemPlacementType placementType, Vector3 position,
		Rotation rotation )
	{
		Assert.NotNull( itemData, "Item data is null" );

		if ( IsOutsideGrid( WorldToItemGrid( position ) ) )
		{
			throw new Exception( $"Position {position} is outside the grid" );
		}

		/*if ( !itemData.Placements.HasFlag( placement ) )
		{
			throw new Exception( $"Item {itemData.Name} does not support placement {placement}" );
		}
		*/

		var defaultDropScene =
			GameObject.GetPrefab( "items/misc/dropped_item/dropped_item.prefab" );

		var spawnPrefab = placementType switch
		{
			ItemPlacementType.Placed => itemData.PlaceScene,
			ItemPlacementType.Dropped => itemData.DropScene ?? defaultDropScene,
			_ => throw new ArgumentOutOfRangeException( nameof(placementType), placementType, null )
		};

		if ( spawnPrefab == null )
		{
			throw new Exception( $"Item {(itemData.Name ?? itemData.ResourceName)} has no {placementType} scene" );
		}

		// dropped items are always 1x1
		/*var positions = placementType == ItemPlacementType.Dropped
			? new List<Vector2Int> { position }
			: itemData.GetGridPositions( rotation, position );

		if ( !CanPlaceItem( positions, placement ) )
		{
			throw new Exception( $"Cannot place item {itemData.Name} at {position} with placement {placement}" );
		}*/

		var gameObject = spawnPrefab.Clone();

		gameObject.WorldPosition = position;
		gameObject.WorldRotation = rotation;

		var nodeLink = AddItem( gameObject );

		nodeLink.SetItemData( itemData );
		nodeLink.ItemPlacement = ItemPlacementType.Placed;

		// replace itemdata with the one from the item, mainly for dropped items
		/*if ( nodeLink.Components.TryGet<WorldItem>( out var worldItem ) )
		{
			worldItem.ItemData = itemData;
		}*/

		// nodeLink.CalculateSize();

		// UpdateTransform( nodeLink );

		gameObject.Name = nodeLink.GetName();

		// AddNodeLinkToGridMap( nodeLink );

		// nodeLink.OnNodeAdded();

		gameObject.NetworkSpawn();

		return nodeLink;
	}

	/// <summary>
	///  Adds an item to the world at the specified position and placement. It does not check if the item can be placed at the specified position.
	/// </summary>
	/// <param name="position"></param>
	/// <param name="rotation"></param>
	/// <param name="placement"></param>
	/// <param name="item"></param>
	/// <returns></returns>
	/// <exception cref="Exception"></exception>
	public WorldItem AddItem( Vector2Int position, ItemRotation rotation, GameObject item )
	{
		if ( IsOutsideGrid( position ) )
		{
			throw new Exception( $"Position {position} is outside the grid" );
		}

		// var nodeLink = new WorldNodeLink( this, item );

		// nodeLink.GridPosition = position;
		// nodeLink.GridRotation = rotation;

		// nodeLink.GridPlacement = placement;
		// nodeLink.PrefabPath = nodeLink.GetPrefabPath();

		// NODE LINK IS NOT ADDED TO WORLD YET, CAN'T DO IT HERE BECAUSE WE NEED TO CALCULATE SIZE FIRST
		// AddNodeLinkToGridMap( nodeLink );

		item.SetParent( GameObject ); // TODO: should items be parented to the world?

		// OnItemAdded?.Invoke( nodeLink );

		// nodeLink.OnNodeAdded();

		// UpdateTransform( nodeLink );

		var worldItem = item.GetComponent<WorldItem>();
		WorldItems.Add( worldItem );

		Log.Info( $"Added item {item.Name} at {position} ({item.Id})" );

		// return nodeLink;

		return worldItem;
	}

	public WorldItem AddItem( GameObject item )
	{
		/*if ( IsOutsideGrid( position ) )
		{
			throw new Exception( $"Position {position} is outside the grid" );
		}*/

		// var nodeLink = new WorldNodeLink( this, item );

		// nodeLink.GridPlacement = placement;
		// nodeLink.PrefabPath = nodeLink.GetPrefabPath();

		// NODE LINK IS NOT ADDED TO WORLD YET, CAN'T DO IT HERE BECAUSE WE NEED TO CALCULATE SIZE FIRST
		// AddNodeLinkToGridMap( nodeLink );

		item.SetParent( GameObject ); // TODO: should items be parented to the world?

		// WorldItems.Add( nodeLink );
		var nodeLink = item.GetComponent<WorldItem>();
		WorldItems.Add( nodeLink );

		// OnItemAdded?.Invoke( nodeLink );

		// nodeLink.OnNodeAdded();

		// UpdateTransform( nodeLink );

		Log.Info( $"Added item {item.Name} at {item.WorldPosition} ({item.Id})" );

		return nodeLink;
	}
}