DeskNet.cs
namespace PixelPusher;

/// <summary>
/// Watch + fork. Host Desk snapshots to the lobby. Guests watch live or edit a local copy.
/// No co-paint on the same pixel.
/// </summary>
public sealed class DeskNet : Component
{
	public static DeskNet Instance { get; private set; }

	public static bool Watching { get; private set; }
	public static int Version { get; private set; }
	public static string Presence { get; private set; } = "";

	static ProjectSave _watch;
	static ProjectSave _stash;
	static bool _preferWatch = true;
	static int _sentHash;
	static float _sendIn;
	static float _presenceIn;

	public static bool Live
	{
		get
		{
			try { return Networking.IsActive; }
			catch { return false; }
		}
	}

	public static bool Host
	{
		get
		{
			try { return !Networking.IsActive || Networking.IsHost; }
			catch { return true; }
		}
	}

	public static bool GuestLocked => Live && !Host && Watching;

	protected override void OnEnabled() => Instance = this;
	protected override void OnDisabled()
	{
		if ( Instance == this ) Instance = null;
	}

	protected override void OnDestroy()
	{
		if ( Instance == this ) Instance = null;
	}

	public static void Tick()
	{
		EnsureHostRelay();
		if ( !Live )
		{
			if ( Watching )
				StopWatch();
			return;
		}

		_presenceIn -= Time.Delta;
		if ( _presenceIn <= 0f )
		{
			_presenceIn = 1f;
			Presence = BuildPresence();
			Version++;
			GameFlow.Bump();
		}

		if ( !Host )
			return;

		Watching = false;
		_sendIn -= Time.Delta;
		if ( _sendIn > 0f )
			return;
		_sendIn = 0.35f;

		var hash = HashCode.Combine(
			Desk.Sprite?.Version ?? 0,
			Desk.Tileset?.Version ?? 0,
			Desk.Stage?.Version ?? 0,
			Desk.Chip?.Version ?? 0,
			Desk.Name );
		if ( hash == _sentHash )
			return;
		_sentHash = hash;
		var net = Instance;
		if ( net.IsValid() )
			net.PushSnapshot();
	}

	public static void Watch()
	{
		if ( Host || !Live )
			return;
		_preferWatch = true;
		if ( !Watching && Desk.Ready )
			_stash = Desk.ToSave();
		Watching = true;
		if ( _watch is not null )
			Desk.Apply( _watch );
		GameFlow.ShowToast( "watching the host desk" );
		Version++;
		GameFlow.Bump();
	}

	public static void MyDesk()
	{
		if ( Host )
			return;
		_preferWatch = false;
		Watching = false;
		if ( _stash is not null )
			Desk.Apply( _stash );
		GameFlow.ShowToast( "your desk" );
		Version++;
		GameFlow.Bump();
	}

	public static void Fork()
	{
		if ( Host || _watch is null )
			return;
		_preferWatch = false;
		Watching = false;
		_stash = null;
		Desk.Apply( _watch );
		Desk.Touch();
		GameFlow.ShowToast( "forked to your desk" );
		Version++;
		GameFlow.Bump();
	}

	public static void DropOnDesk()
	{
		if ( Host || !Live )
			return;
		var net = Instance;
		if ( !net.IsValid() )
		{
			GameFlow.ShowToast( "no live desk" );
			return;
		}
		var save = Watching && _stash is not null ? _stash : Desk.ToSave();
		net.RequestDrop( Json.Serialize( save ) );
		GameFlow.ShowToast( "dropped on host desk" );
	}

	static void StopWatch()
	{
		Watching = false;
		_watch = null;
	}

	public static void EnsureHostRelay()
	{
		try
		{
			if ( !Networking.IsActive )
				return;
			if ( Instance.IsValid() )
				return;
			if ( !Networking.IsHost )
				return;

			var go = new GameObject( true, "DeskNet" );
			go.Components.Create<DeskNet>();
			go.NetworkSpawn();
			Log.Info( "[PIXEL PUSHER] desk net spawned" );
		}
		catch ( Exception e )
		{
			Log.Warning( $"[PIXEL PUSHER] desk net: {e.Message}" );
		}
	}

	void PushSnapshot()
	{
		if ( !this.IsValid() )
			return;
		try
		{
			if ( GameObject is null || !GameObject.Network.Active )
				return;
			ReceiveSnapshot( Json.Serialize( Desk.ToSave() ) );
		}
		catch ( Exception e )
		{
			Log.Warning( $"[PIXEL PUSHER] snapshot: {e.Message}" );
		}
	}

	[Rpc.Broadcast]
	void ReceiveSnapshot( string json )
	{
		if ( Host )
			return;
		if ( string.IsNullOrWhiteSpace( json ) )
			return;
		try
		{
			_watch = Json.Deserialize<ProjectSave>( json );
			if ( _watch is null )
				return;
			if ( _preferWatch )
			{
				if ( Watching )
					Desk.Apply( _watch );
				else
					Watch();
			}
			Version++;
			GameFlow.Bump();
		}
		catch ( Exception e )
		{
			Log.Warning( $"[PIXEL PUSHER] watch apply: {e.Message}" );
		}
	}

	[Rpc.Host]
	void RequestDrop( string json )
	{
		if ( string.IsNullOrWhiteSpace( json ) )
			return;
		try
		{
			var save = Json.Deserialize<ProjectSave>( json );
			if ( save is null )
				return;
			var who = "guest";
			try { who = Rpc.Caller?.DisplayName ?? who; } catch { }
			Desk.Apply( save );
			Desk.Touch();
			_sentHash = 0;
			_sendIn = 0;
			GameFlow.ShowToast( $"{who} dropped their desk" );
			GameFlow.Bump();
		}
		catch ( Exception e )
		{
			Log.Warning( $"[PIXEL PUSHER] drop: {e.Message}" );
		}
	}

	static string BuildPresence()
	{
		try
		{
			var n = 0;
			foreach ( var _ in Connection.All )
				n++;
			if ( n <= 0 )
				n = 1;
			return n == 1 ? "1 at the CRT" : $"{n} at the CRT";
		}
		catch
		{
			return "live";
		}
	}
}