Editor/Core/PlaySpectate.cs
using System;
using System.Collections.Generic;

namespace TeamCreate;

public enum PlaySpectateDecision
{
    /// <summary>Synchronize as usual.</summary>
    Normal,

    /// <summary>
    /// The local editor is playing a copy of the scene. Source edits stop being sampled and incoming scene changes are
    /// held, but the connection, the roster and presence carry on, so the people still in the editor can watch.
    /// </summary>
    PausedForPlay,

    /// <summary>No supported saved scene is in front of the user. Nothing to sample or apply; the connection stays up.</summary>
    NoScene,

    /// <summary>Something that needs a person: a hotload during play, or a state no rule anticipated.</summary>
    FailStop
}

public static class PlaySpectateRules
{
    /// <summary>
    /// Decides what a frame does with source synchronization. Before this rule, pressing Play ended the session
    /// with "Stop play mode..." and even closing the last scene tab did too; a player could not keep a collaborator
    /// in the loop, and a collaborator could not watch. The one state that still stops everything is a hotload that
    /// interrupted play, because that path archives recovery data and must be ended with Leave.
    /// </summary>
    public static PlaySpectateDecision Decide(bool localPlaying, PlayModeLifecycleState lifecycle, bool sourcePacketsAdmissible, bool hasSupportedScene)
    {
        if (lifecycle == PlayModeLifecycleState.RuntimeBlocked)
        {
            return PlaySpectateDecision.FailStop;
        }
        if (localPlaying)
        {
            // An approved experimental runtime run keeps its existing, stricter path.
            return sourcePacketsAdmissible ? PlaySpectateDecision.Normal : PlaySpectateDecision.PausedForPlay;
        }
        if (lifecycle == PlayModeLifecycleState.RuntimeStarting)
        {
            return PlaySpectateDecision.PausedForPlay;
        }
        if (!hasSupportedScene)
        {
            return PlaySpectateDecision.NoScene;
        }
        return sourcePacketsAdmissible ? PlaySpectateDecision.Normal : PlaySpectateDecision.FailStop;
    }

    /// <summary>The state word other editors see next to a collaborator's name.</summary>
    public static string PresenceState(bool saving, bool localPlaying, bool editing)
    {
        if (saving)
        {
            return "saving";
        }
        if (localPlaying)
        {
            return "playing";
        }
        return editing ? "editing" : "idle";
    }

    public static string StatusText(string state)
    {
        switch (state)
        {
            case "playing":
                return "Playing the game";
            case "editing":
                return "Editing";
            case "saving":
                return "Saving";
            default:
                return "Idle";
        }
    }
}

/// <summary>
/// Scene packets received while the local editor is playing. The host does not know a peer is paused, so packets
/// keep arriving; leaving them in the wire's queue would trip its 256-packet cap and drop the connection.
/// They are moved here, in order, and applied when play stops.
/// </summary>
public sealed class HeldPacketBuffer
{
    public const int Capacity = 4096;

    private readonly Queue<Packet> held = new Queue<Packet>();

    public int Count => held.Count;

    public bool IsEmpty => held.Count == 0;

    /// <summary>False when the buffer is full. The caller must then stop synchronizing, because dropping a packet would desynchronize silently.</summary>
    public bool TryHold(Packet packet)
    {
        if (packet == null)
        {
            return true;
        }
        if (held.Count >= Capacity)
        {
            return false;
        }
        held.Enqueue(packet);
        return true;
    }

    public bool TryTake(out Packet packet)
    {
        return held.TryDequeue(out packet);
    }

    public void Clear()
    {
        held.Clear();
    }

    /// <summary>Whether a packet kind changes scene state and therefore has to wait for play to end.</summary>
    public static bool IsSceneChange(string kind)
    {
        return kind == "snapshot" || kind == "accepted";
    }
}