Editor/Dock/ConnectCard.cs
using System;
using System.Threading.Tasks;
using Editor;
using Sandbox.Services;

namespace TeamCreate;

public sealed class ConnectCard : DockCard
{
    private const string HealthyPlayModeStatus = "Source collaboration";

    private const string PreparingIdentityText = "Preparing identity… waiting for a fresh Steam identity token; the session starts as soon as it arrives.";

    private readonly Button host;

    private readonly Button leave;

    private readonly LineEdit hostCode;

    private readonly Button copy;

    private readonly LineEdit invite;

    private readonly Label joinLabel;

    private readonly Widget joinRow;

    private bool advancedOpen;

    private readonly Label notice;

    private readonly Label tokenNotice;

    private readonly Label runtime;

    private readonly Widget advanced;

    private readonly Button advancedToggle;

    private readonly LineEdit address;

    private readonly LineEdit port;

    private readonly LineEdit key;

    private readonly Checkbox allowUnverified;

    private readonly Checkbox experimentalRuntime;

    private readonly Label playMode;

    private readonly Label lifecycle;

    private bool? lastHostShown;

    private bool? lastCodeShown;

    private bool? lastTokenShown;

    private bool? lastTokenBlocked;

    private bool? lastLifecycleShown;

    private bool? lastRuntimeShown;

    private Task<string> tokenTask;

    private string pendingToken;

    private string tokenProblem;

    private Action pendingStart;

    private Action lastStart;

    private bool retryArmed;

    private bool retrySpent;

    private static string PublishedCode => CollaborationSession.Current?.InviteCode;

    public string Address => address.Text;

    public int PortNumber => ParsePort(port.Text);

    protected override bool FieldsAssigned => host != null && leave != null && joinLabel != null && joinRow != null && hostCode != null && copy != null && invite != null && notice != null && tokenNotice != null && runtime != null && advanced != null && advancedToggle != null && address != null && port != null && key != null && allowUnverified != null && experimentalRuntime != null && playMode != null && lifecycle != null;

    public ConnectCard(Widget parent)
        : base(parent, "Connect")
    {
        host = Layout.Add(new Button.Primary("Host session"));
        host.Clicked = () =>
        {
            DockCard.Run(Host);
        };
        leave = Layout.Add(new Button.Danger("Leave"));
        leave.Clicked = () =>
        {
            DockCard.Run(LeaveSession);
        };
        Widget widget = Layout.Add(new Widget(this));
        widget.Layout = Layout.Row();
        widget.Layout.Spacing = 4f;
        hostCode = widget.Layout.Add(new LineEdit("", this)
        {
            ReadOnly = true,
            ToolTip = "Give this code to the people you want in the session."
        }, 1);
        DockTheme.OutlineField(hostCode);
        copy = widget.Layout.Add(new Button("Copy", this)
        {
            Clicked = () =>
            {
                EditorUtility.Clipboard.Copy(hostCode.Text);
            }
        });
        notice = Layout.Add(new Label("", this)
        {
            WordWrap = true,
            Color = DockTheme.MutedColor
        });
        joinLabel = Layout.Add(new Label("Join with a code", this)
        {
            Color = DockTheme.MutedColor
        });
        Widget widget2 = Layout.Add(new Widget(this));
        joinRow = widget2;
        widget2.Layout = Layout.Row();
        widget2.Layout.Spacing = 4f;
        invite = widget2.Layout.Add(new LineEdit("", this)
        {
            PlaceholderText = "Invite code from the host"
        }, 1);
        DockTheme.OutlineField(invite);
        widget2.Layout.Add(new Button("Join", this)
        {
            Clicked = () =>
            {
                DockCard.Run(JoinSession);
            }
        });
        tokenNotice = Layout.Add(new Label("", this)
        {
            WordWrap = true,
            Color = DockTheme.BlockedColor
        });
        tokenNotice.Visible = false;
        runtime = Layout.Add(new Label("", this)
        {
            WordWrap = true,
            Color = DockTheme.WarningColor
        });
        runtime.Visible = false;
        advancedToggle = Layout.Add(new Button("▸ Advanced", this)
        {
            ToolTip = "Direct-connection details: address, port and session key, plus the admission options. A normal join needs only the invite code."
        });
        advancedToggle.Clicked = ToggleAdvanced;
        advanced = Layout.Add(new Widget(this));
        advanced.Layout = Layout.Column();
        advanced.Layout.Spacing = 4f;
        advanced.Visible = false;
        address = AdvancedField("Host address (used only when the code has no relay or the relay fails)", "127.0.0.1");
        port = AdvancedField("Port", "27845");
        key = AdvancedField("Shared session key (12+ characters)", "");
        allowUnverified = advanced.Layout.Add(new Checkbox("Allow peers whose Steam identity could not be verified", this)
        {
            Value = false,
            ToolTip = "When off (the default), a peer is admitted only if s&box's auth service confirms its Steam account. When on, a peer is also admitted if the service is simply unreachable — its SteamID is then self-asserted. This does not weaken the transport: encryption follows how the session is joined (invite-code and discovery sessions are encrypted, Advanced sessions opened with a typed key are not), and this setting changes neither."
        });
        experimentalRuntime = advanced.Layout.Add(new Checkbox("Enable experimental runtime collaboration", this)
        {
            Value = false,
            ToolTip = "Explicit opt-in for the next play session. Runtime transport, sampling, undo, apply, runtime hotload data flow, and synchronization are not implemented. Hotload lifecycle blocking, recovery archival, and transport disposal are supported."
        });
        experimentalRuntime.Toggled = () =>
        {
            DockCard.Run(() =>
            {
                CollaborationSession.Current.ExperimentalRuntimeOptIn = experimentalRuntime.Value;
            });
        };
        playMode = advanced.Layout.Add(new Label("", this)
        {
            Color = DockTheme.MutedColor
        });
        lifecycle = advanced.Layout.Add(new Label("", this)
        {
            WordWrap = true,
            Color = DockTheme.WarningColor
        });
    }

    private LineEdit AdvancedField(string title, string value)
    {
        advanced.Layout.Add(new Label(title, this)
        {
            Color = DockTheme.MutedColor
        });
        LineEdit lineEdit = advanced.Layout.Add(new LineEdit(value, this));
        DockTheme.OutlineField(lineEdit);
        return lineEdit;
    }

    private void ToggleAdvanced()
    {
        advancedOpen = !advancedOpen;
        advanced.Visible = advancedOpen;
        DockTheme.SetText(advancedToggle, advancedOpen ? "▾ Advanced" : "▸ Advanced");
        AdjustSize();
        Reflow();
    }

    private void Host()
    {
        RequestStart(() =>
        {
            CollaborationSession.Current.StartHosted(ParsePort(port.Text), IdentityName(), IdentitySteamId(), TakeToken(), allowUnverified.Value);
        });
    }

    private void JoinSession()
    {
        string code = invite.Text;
        if (code != null && code.Length > 0)
        {
            RequestStart(() =>
            {
                CollaborationSession.Current.StartFromInvite(code, IdentityName(), IdentitySteamId(), TakeToken(), allowUnverified.Value, address.Text, ParsePort(port.Text));
            });
        }
        else
        {
            RequestStart(() =>
            {
                CollaborationSession.Current.Start(host: false, address.Text, ParsePort(port.Text), IdentityName(), key.Text, IdentitySteamId(), TakeToken(), allowUnverified.Value);
            });
        }
    }

    private static int ParsePort(string text)
    {
        if (!int.TryParse(text, out var result) || result < 1 || result > 65535)
        {
            throw new InvalidOperationException("The port must be a number between 1 and 65535.");
        }
        return result;
    }

    private static string BuildNotice(CollaborationSession session, string typedCode)
    {
        return ConnectCopy.Build(session.Connected, session.IsHost, session.IsHost && session.InviteCode != null, session.Connected && !session.IsHost ? session.RouteDescription : null, typedCode,
            session.RelayStatus ?? session.NoRelayNotice, session.PortMappingFailed ? session.PortMappingStatus : null, session.PlayNotice, session.SoloHarnessNotice, session.DiscoveryStatus);
    }

    public void Refresh()
    {
        CollaborationSession current = CollaborationSession.Current;
        SessionState state = current.State;
        bool flag = (uint)state <= 1u;
        bool flag2 = flag;
        if (lastHostShown != flag2)
        {
            lastHostShown = flag2;
            host.Visible = flag2;
            leave.Visible = !flag2;
            // Joining, and the direct-connection fields, only make sense before a session: once one is live they are out of the way, and come back when it ends.
            joinLabel.Visible = flag2;
            joinRow.Visible = flag2;
            advancedToggle.Visible = flag2;
            advanced.Visible = flag2 && advancedOpen;
            AdjustSize();
            Reflow();
        }
        // The one Leave button is disabled while play mode is changing state, as the Controls card's own copy used to be.
        bool canLeave = !current.RuntimeLifecycle.IsTransitionInProgress;
        if (leave.Enabled != canLeave)
        {
            leave.Enabled = canLeave;
        }
        if (flag2)
        {
            current.StartDiscovery();
        }
        string publishedCode = PublishedCode;
        bool flag3 = current.IsHost && publishedCode != null;
        if (lastCodeShown != flag3)
        {
            lastCodeShown = flag3;
            hostCode.Visible = flag3;
            copy.Visible = flag3;
        }
        if (flag3 && !string.Equals(hostCode.Text, publishedCode, StringComparison.Ordinal))
        {
            hostCode.Text = publishedCode;
        }
        DockCard.SetText(notice, BuildNotice(current, invite.Text));
        if (current.Error != null)
        {
            if (!retryArmed && !retrySpent && lastStart != null && !current.ReconnectRecovery && SteamIdentityAdmission.IsTokenRejection(current.Error))
            {
                ArmTokenRetry(current);
            }
            else if (!retryArmed && pendingStart != null)
            {
                pendingStart = null;
            }
        }
        PollToken();
        ContinuePendingStart();
        bool flag4 = tokenProblem != null;
        string text = tokenProblem ?? ((pendingStart != null) ? "Preparing identity… waiting for a fresh Steam identity token; the session starts as soon as it arrives." : string.Empty);
        DockCard.SetText(tokenNotice, text);
        if (lastTokenBlocked != flag4)
        {
            lastTokenBlocked = flag4;
            tokenNotice.Color = (flag4 ? DockTheme.BlockedColor : DockTheme.MutedColor);
        }
        bool flag5 = text.Length > 0;
        if (lastTokenShown != flag5)
        {
            lastTokenShown = flag5;
            tokenNotice.Visible = flag5;
        }
        if (experimentalRuntime.Value != current.ExperimentalRuntimeOptIn)
        {
            experimentalRuntime.Value = current.ExperimentalRuntimeOptIn;
        }
        string playModeStatus = current.PlayModeStatus;
        bool flag6 = !string.Equals(playModeStatus, "Source collaboration", StringComparison.Ordinal);
        if (flag6)
        {
            string runtimeLifecycleDiagnostic = current.RuntimeLifecycleDiagnostic;
            DockCard.SetText(runtime, (string.IsNullOrWhiteSpace(runtimeLifecycleDiagnostic) || string.Equals(runtimeLifecycleDiagnostic, playModeStatus, StringComparison.Ordinal)) ? playModeStatus : (playModeStatus + "\n" + runtimeLifecycleDiagnostic));
        }
        if (lastRuntimeShown != flag6)
        {
            lastRuntimeShown = flag6;
            runtime.Visible = flag6;
        }
        DockCard.SetText(playMode, playModeStatus);
        DockCard.SetText(lifecycle, current.RuntimeLifecycleDiagnostic ?? string.Empty);
        bool flag7 = lifecycle.Text.Length > 0;
        if (lastLifecycleShown != flag7)
        {
            lastLifecycleShown = flag7;
            lifecycle.Visible = flag7;
        }
    }

    private void BeginTokenRequest()
    {
        if (tokenTask != null || pendingToken != null)
        {
            return;
        }
        try
        {
            tokenTask = Auth.GetToken("sbox-editor-collaboration");
        }
        catch (Exception)
        {
            tokenProblem = "A Steam identity token could not be requested.";
        }
    }

    private void PollToken()
    {
        if (tokenTask == null || !tokenTask.IsCompleted)
        {
            return;
        }
        string value = null;
        try
        {
            value = tokenTask.GetAwaiter().GetResult();
        }
        catch (Exception)
        {
            tokenProblem = "A Steam identity token could not be obtained.";
        }
        finally
        {
            tokenTask = null;
        }
        if (string.IsNullOrWhiteSpace(value))
        {
            pendingToken = null;
            if (tokenProblem == null)
            {
                tokenProblem = "A Steam identity token could not be obtained.";
            }
        }
        else
        {
            pendingToken = value;
        }
        if (pendingToken != null)
        {
            ContinuePendingStart();
        }
        else if (pendingStart != null)
        {
            CancelStarts();
        }
    }

    private string TakeToken()
    {
        string result = pendingToken;
        pendingToken = null;
        return result;
    }

    private void RequestStart(Action start)
    {
        lastStart = start;
        pendingStart = start;
        retryArmed = false;
        retrySpent = false;
        tokenProblem = null;
        ContinuePendingStart();
    }

    public void CancelStarts()
    {
        pendingStart = null;
        lastStart = null;
        retryArmed = false;
        retrySpent = false;
        pendingToken = null;
    }

    private void LeaveSession()
    {
        CancelStarts();
        CollaborationSession.Current.Leave();
    }

    private void ContinuePendingStart()
    {
        if (pendingStart == null || tokenTask != null)
        {
            return;
        }
        if (pendingToken == null)
        {
            BeginTokenRequest();
            if (tokenProblem != null)
            {
                CancelStarts();
            }
            return;
        }
        if (retryArmed)
        {
            CollaborationSession current = CollaborationSession.Current;
            if (current.State == SessionState.Degraded)
            {
                CancelStarts();
                return;
            }
            if (current.Busy)
            {
                return;
            }
        }
        Action action = pendingStart;
        pendingStart = null;
        retryArmed = false;
        DockCard.Run(action);
    }

    private void ArmTokenRetry(CollaborationSession session)
    {
        Action action = lastStart;
        retrySpent = true;
        try
        {
            session.Leave();
        }
        catch (Exception)
        {
            pendingStart = null;
            lastStart = null;
            return;
        }
        retryArmed = true;
        pendingStart = action;
        pendingToken = null;
        tokenProblem = null;
        session.ClearRecordedFailure();
        BeginTokenRequest();
    }

    private static string IdentityName()
    {
        if (LocalSteamIdentity.TryGet(out var _, out var personaName, out var _) && !string.IsNullOrWhiteSpace(personaName))
        {
            return personaName;
        }
        return null;
    }

    private static string IdentitySteamId()
    {
        SteamIdentity identity;
        string personaName;
        string failure;
        return LocalSteamIdentity.TryGet(out identity, out personaName, out failure) ? SteamIdentityRules.Canonical(identity) : null;
    }
}