Editor/Connect/LanDiscovery.cs
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace TeamCreate;

public sealed record DiscoveredHost(string EndPoint, int Port, string HostName, string SecretHex, string Manifest8, DateTime LastSeenUtc);

public sealed class LanDiscovery : IDisposable
{
    public static readonly TimeSpan QueryInterval = TimeSpan.FromMilliseconds(1500L);

    public static readonly TimeSpan HostExpiry = TimeSpan.FromSeconds(5L);

    public const int MaxDatagramsPerPoll = 32;

    public const int MaxTrackedHosts = 16;

    private readonly string projectIdHash8;

    private readonly Dictionary<string, DiscoveredHost> hosts = new Dictionary<string, DiscoveredHost>(StringComparer.Ordinal);

    private UdpClient socket;

    private bool boundToRendezvousPort;

    private bool hosting;

    private bool disposed;

    private int sessionPort;

    private string hostSecretHex;

    private string hostManifest8;

    private DateTime nextQueryUtc;

    private string failure;

    public IReadOnlyList<DiscoveredHost> Hosts
    {
        get
        {
            Prune(hosts, DateTime.UtcNow, HostExpiry);
            return new List<DiscoveredHost>(hosts.Values);
        }
    }

    public string Failure => failure;

    public bool CanHost => socket != null && boundToRendezvousPort && !disposed;

    public LanDiscovery(string projectIdHash8)
    {
        DiscoveryProtocol.BuildQuery(projectIdHash8);
        this.projectIdHash8 = projectIdHash8;
        Open();
    }

    public void Poll()
    {
        if (socket == null || disposed)
        {
            return;
        }
        try
        {
            DateTime now = DateTime.UtcNow;
            Drain(32, () => socket.Client.Poll(0, SelectMode.SelectRead) && socket.Available > 0, () =>
            {
                IPEndPoint remoteEP = null;
                byte[] item = socket.Receive(ref remoteEP);
                return (Bytes: item, From: remoteEP);
            }, ((byte[] Bytes, IPEndPoint From) datagram) =>
            {
                Handle(datagram.Bytes, datagram.From, now);
            });
            Prune(hosts, now, HostExpiry);
        }
        catch (Exception error)
        {
            failure = CollaborationDiagnostics.Bounded(Describe(error));
        }
    }

    public void SendQuery()
    {
        if (socket == null || disposed || hosting)
        {
            return;
        }
        DateTime utcNow = DateTime.UtcNow;
        if (utcNow < nextQueryUtc)
        {
            return;
        }
        nextQueryUtc = utcNow + QueryInterval;
        try
        {
            byte[] bytes = Encoding.ASCII.GetBytes(DiscoveryProtocol.BuildQuery(projectIdHash8));
            socket.Send(bytes, bytes.Length, new IPEndPoint(IPAddress.Broadcast, 27846));
        }
        catch (Exception error)
        {
            failure = CollaborationDiagnostics.Bounded(Describe(error));
        }
    }

    public void StartHost(int sessionPort, string secretHex, string manifest8)
    {
        DiscoveryProtocol.BuildReply(projectIdHash8, sessionPort, "host", secretHex, manifest8);
        if (socket == null || disposed)
        {
            failure = CollaborationDiagnostics.Bounded("LAN discovery is unavailable: the rendezvous socket could not be opened on this machine.");
        }
        else if (!boundToRendezvousPort)
        {
            failure = CollaborationDiagnostics.Bounded($"LAN discovery cannot answer on this machine: UDP port {27846} is already in use.");
        }
        else
        {
            this.sessionPort = sessionPort;
            hostSecretHex = secretHex;
            hostManifest8 = manifest8;
            hosts.Clear();
            hosting = true;
        }
    }

    public void StopHost()
    {
        hosting = false;
    }

    private void Open()
    {
        try
        {
            UdpClient udpClient;
            try
            {
                udpClient = new UdpClient(new IPEndPoint(IPAddress.Any, 27846));
            }
            catch (SocketException)
            {
                udpClient = new UdpClient(new IPEndPoint(IPAddress.Any, 0));
            }
            udpClient.EnableBroadcast = true;
            socket = udpClient;
            boundToRendezvousPort = ((IPEndPoint)udpClient.Client.LocalEndPoint).Port == 27846;
        }
        catch (Exception error)
        {
            socket = null;
            failure = CollaborationDiagnostics.Bounded(Describe(error));
        }
    }

    private void Handle(byte[] bytes, IPEndPoint from, DateTime now)
    {
        if (bytes == null || from == null || bytes.Length > 512)
        {
            return;
        }
        string text = Encoding.ASCII.GetString(bytes);
        DiscoveryReply reply;
        if (DiscoveryProtocol.TryParseQuery(text, out var text2))
        {
            if (hosting && text2 == projectIdHash8)
            {
                Reply(from);
            }
        }
        else if (DiscoveryProtocol.TryParseReply(text, out reply) && !(reply.ProjectIdHash8 != projectIdHash8) && !hosting)
        {
            string text3 = from.Address.ToString();
            if (hosts.Count >= 16 && !hosts.ContainsKey(text3))
            {
                DropOldest();
            }
            hosts[text3] = new DiscoveredHost(text3, reply.SessionPort, reply.HostName, reply.SecretHex, reply.Manifest8, now);
        }
    }

    private void Reply(IPEndPoint to)
    {
        byte[] bytes = Encoding.ASCII.GetBytes(DiscoveryProtocol.BuildReply(projectIdHash8, sessionPort, Environment.MachineName, hostSecretHex, hostManifest8));
        socket.Send(bytes, bytes.Length, to);
    }

    private void DropOldest()
    {
        DiscoveredHost discoveredHost = null;
        foreach (KeyValuePair<string, DiscoveredHost> host in hosts)
        {
            if ((object)discoveredHost == null || host.Value.LastSeenUtc < discoveredHost.LastSeenUtc)
            {
                discoveredHost = host.Value;
            }
        }
        if ((object)discoveredHost != null)
        {
            hosts.Remove(discoveredHost.EndPoint);
        }
    }

    internal static int Drain<T>(int cap, Func<bool> available, Func<T> receive, Action<T> handle)
    {
        int i;
        for (i = 0; i < cap; i++)
        {
            if (!available())
            {
                break;
            }
            handle(receive());
        }
        return i;
    }

    internal static int Prune(Dictionary<string, DiscoveredHost> hosts, DateTime nowUtc, TimeSpan maxAge)
    {
        if (hosts == null || hosts.Count == 0)
        {
            return 0;
        }
        List<string> list = null;
        foreach (KeyValuePair<string, DiscoveredHost> host in hosts)
        {
            if (nowUtc - host.Value.LastSeenUtc > maxAge)
            {
                (list ?? (list = new List<string>())).Add(host.Key);
            }
        }
        if (list == null)
        {
            return 0;
        }
        foreach (string item in list)
        {
            hosts.Remove(item);
        }
        return list.Count;
    }

    private static string Describe(Exception error)
    {
        return CollaborationDiagnostics.DescribeExternalFailure("LAN discovery: ", error);
    }

    public void Dispose()
    {
        if (!disposed)
        {
            disposed = true;
            hosting = false;
            try
            {
                socket?.Close();
            }
            catch
            {
            }
            socket = null;
            hosts.Clear();
        }
    }
}