WorldBuilder/WorldManager.cs
using System;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Clover.Data;
using Sandbox.Diagnostics;

namespace Clover;

[Category( "Clover/World" )]
public class WorldManager : Component, Component.INetworkSpawn
{
	public static WorldManager Instance { get; private set; }

	public static World Island => Instance.GetWorld( "island" );

	// [Property] public List<World> Worlds { get; set; } = new();
	[Property, Sync, Change, ReadOnly] public NetDictionary<int, World> Worlds { get; set; } = new();

	[Property, JsonIgnore, ReadOnly] public int ActiveWorldIndex { get; set; }
	[Property, JsonIgnore, ReadOnly] public World ActiveWorld => !Scene.IsEditor ? GetWorld( ActiveWorldIndex ) : null;

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

	public delegate void WorldUnloadEventHandler( World world );

	public delegate void WorldLoadedEventHandler( World world );

	public delegate void ActiveWorldChangedEventHandler( World world );


	[Property] public WorldLoadedEventHandler WorldLoaded { get; set; }
	[Property] public WorldUnloadEventHandler WorldUnload { get; set; }
	[Property] public ActiveWorldChangedEventHandler ActiveWorldChanged { get; set; }


	public string CurrentWorldDataPath { get; set; }

	public bool IsLoading;

	public const float WorldOffset = 1024;

	// public Array LoadingProgress { get; set; } = new Array();

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

	public void OnNetworkSpawn( Connection owner )
	{
		Instance = this;
	}

	public void OnWorldsChanged()
	{
		Log.Info( "Worlds changed." );
		RebuildVisibility();
	}

	protected override void OnDestroy()
	{
		base.OnDestroy();
		Instance = null;
	}

	public World GetWorld( string id )
	{
		var val = Worlds.Values.FirstOrDefault( w => w.Data.ResourceName == id );
		if ( !val.IsValid() )
		{
			Log.Warning( $"World not found: {id}, searching scene..." );
			val = Scene.GetAllComponents<World>().FirstOrDefault( w => w.Data.ResourceName == id );
		}

		return val;
	}

	public World GetWorld( int index )
	{
		if ( index < 0 )
		{
			return null;
		}


		var val = Worlds.Values.FirstOrDefault( w => w.Layer == index );
		if ( !val.IsValid() )
		{
			Log.Warning( $"World not found at index: {index}, searching scene..." );
			val = Scene.GetAllComponents<World>().FirstOrDefault( w => w.Layer == index );
		}

		return val;
	}

	public void SetActiveWorld( int index )
	{
		Log.Info( $"Setting active world to index: {index}" );
		ActiveWorldIndex = index;

		if ( !ActiveWorld.IsValid() )
		{
			Log.Warning( $"Active world is not valid: {index}" );
		}

		RebuildVisibility();
		ActiveWorldChanged?.Invoke( ActiveWorld );
		Scene.RunEvent<IWorldEvent>( x => x.OnWorldChanged( ActiveWorld ) );
	}

	private void RebuildVisibility()
	{
		if ( Worlds.Count == 0 )
		{
			Log.Warning( "No worlds to rebuild visibility for." );
			return;
		}

		Log.Info( $"Rebuilding world visibility for {Worlds.Count} worlds..." );

		// rebuild world visibility
		for ( var i = 0; i < Worlds.Count; i++ )
		{
			var isVisible = i == ActiveWorldIndex;
			var world = Worlds.TryGetValue( i, out var w ) ? w : null;
			if ( world == null )
			{
				Log.Warning( $"World not found at index: {i}" );
				continue;
			}

			world.Tags.Remove( "worldlayer_invisible" );
			world.Tags.Remove( "worldlayer_visible" );

			if ( isVisible )
			{
				world.Tags.Add( "worldlayer_visible" );
			}
			else
			{
				world.Tags.Add( "worldlayer_invisible" );
			}
		}

		// rebuild object visibility
		foreach ( var layerObject in Scene.GetAllComponents<WorldLayerObject>() )
		{
			layerObject.RebuildVisibility( layerObject.Layer );
		}
	}

	public void SetActiveWorld( World world )
	{
		ActiveWorldIndex = world.Layer;
		RebuildVisibility();
	}

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

	public bool HasWorld( string id )
	{
		return Worlds.Values.Any( w => w.Data.ResourceName == id );
	}

	public bool HasWorld( WorldData data )
	{
		return Worlds.Values.Any( w => w.Data == data );
	}

	public async Task<World> LoadWorld( WorldData data )
	{
		Log.Info( $"Loading world: {data.ResourceName}" );

		// use the first available index
		var index = 0;
		while ( Worlds.ContainsKey( index ) )
		{
			index++;
		}

		if ( !data.Prefab.IsValid() )
		{
			Log.Error( $"Invalid prefab for world: {data.ResourceName}" );
			return null;
		}

		var gameObject = data.Prefab.Clone();

		// gameObject.BreakFromPrefab();

		var world = gameObject.GetComponent<World>();
		world.Data = data; // already set
		world.Layer = index;

		gameObject.WorldPosition = new Vector3( new Vector3( 0, 0, index * WorldOffset ) );
		gameObject.Transform.ClearInterpolation();
		gameObject.SetParent( GameObject );

		gameObject.Tags.Add( "dworld" );
		gameObject.Tags.Add( $"dworldlayer_{index}" );

		gameObject.NetworkMode = NetworkMode.Object;
		gameObject.NetworkSpawn();

		Worlds[index] = world;

		world.Setup();

		await world.Load();

		Log.Info( $"Loaded world: {data.ResourceName}, now has {Worlds.Count} worlds." );

		RebuildVisibility();

		// ActiveWorldChanged?.Invoke( world );

		OnWorldLoadedRpc( data.ResourceName );

		// return dummy task to shut up the compiler

		await Task.Frame();

		return world;
	}

	public async Task<World> GetWorldOrLoad( WorldData data )
	{
		Assert.NotNull( data, "World data is null." );
		var world = GetWorld( data.ResourceName );
		return world.IsValid() ? world : await LoadWorld( data );
	}

	[Rpc.Owner]
	public void RequestLoadWorld( string id )
	{
		var worldData = ResourceLibrary.GetAll<WorldData>().FirstOrDefault( w => w.ResourceName == id );
		if ( worldData != null )
		{
			_ = LoadWorld( worldData );
		}
		else
		{
			Log.Warning( $"Could not find world with id: {id}" );
		}
	}

	[Rpc.Broadcast( NetFlags.HostOnly )]
	public void OnWorldLoadedRpc( string id )
	{
		Log.Info( $"World loaded: {id}" );
		var world = GetWorld( id );

		if ( !world.IsValid() )
		{
			Log.Error( $"World not found: {id}" );
			return;
		}

		WorldLoaded?.Invoke( world );
		Scene.RunEvent<IWorldEvent>( x => x.OnWorldLoaded( world ) );
		world.OnWorldLoaded();

		foreach ( var world2 in Worlds )
		{
			Log.Info( $"World #{world2.Key}: {world2.Value.Data.ResourceName}" );
		}
	}


	public void UnloadWorld( string id )
	{
		var world = GetWorld( id );
		if ( world.IsValid() )
		{
			UnloadWorld( world );
		}
	}

	public void UnloadWorld( World world )
	{
		Log.Info( $"Unloading world: {world.Data.ResourceName}" );
		world.OnWorldUnloaded();
		world.DestroyGameObject();
		Worlds.Remove( world.Layer );
		RebuildVisibility();
		WorldUnload?.Invoke( world );
		Scene.RunEvent<IWorldEvent>( x => x.OnWorldUnloaded( world ) );
	}

	public void UnloadWorld( int index )
	{
		var world = GetWorld( index );
		if ( world.IsValid() )
		{
			UnloadWorld( world );
		}
	}


	[ConCmd( "clover_world_load" )]
	public static void LoadWorldCmd( string id )
	{
		var worldManager = NodeManager.WorldManager;
		var worldData = ResourceLibrary.GetAll<WorldData>().FirstOrDefault( w => w.ResourceName == id );
		if ( worldData != null )
		{
			_ = worldManager.LoadWorld( worldData );
		}
		else
		{
			Log.Warning( $"Could not find world with id: {id}" );
		}
	}

	[ConCmd( "clover_world_set_active" )]
	public static void SetActiveWorldCmd( int index )
	{
		NodeManager.WorldManager.SetActiveWorld( index );
	}

	[ConCmd( "clover_world_move_to_entrance" )]
	public static void MoveToEntranceCmd( int worldIndex, string entranceId )
	{
		var world = Instance.GetWorld( worldIndex );
		if ( !world.IsValid() ) throw new Exception( $"Invalid world index: {worldIndex}" );

		var entrance = world.GetEntrance( entranceId );
		if ( entrance == null ) throw new Exception( $"Invalid entrance id: {entranceId}" );

		Instance.SetActiveWorld( worldIndex );

		var player = NodeManager.Player;

		player.WorldLayerObject.SetLayer( worldIndex, true );

		player.WorldPosition = entrance.WorldPosition;
		player.GetComponent<CameraController>().SnapCamera();
	}

	[ConCmd( "clover_world_save_all" )]
	public static void SaveAllCmd()
	{
		foreach ( var world in Instance.Worlds.Values )
		{
			world.Save();
		}
	}

	/*public WorldNodeLink GetWorldNodeLink( GameObject gameObject )
	{
		foreach ( var world in Worlds.Values )
		{
			var link = world.GetNodeLink( gameObject );
			if ( link != null )
			{
				return link;
			}
		}

		return null;
	}*/
}

public interface IWorldEvent
{
	void OnWorldLoaded( World world ) { }
	void OnWorldUnloaded( World world ) { }
	void OnWorldChanged( World world ) { }
}