using System;
using UnityEngine;

namespace OkonaPad
{
    /// <summary>
    /// Phone-as-controller pairing, surfaced to the game so it can show its own
    /// "scan to join" code — in a lobby, on a HUD corner, on a digital sign —
    /// instead of relying on Okona's system join screen.
    ///
    /// <code>
    /// void Start()
    /// {
    ///     Refresh();
    ///     Pairing.OnChanged += _ => Refresh();      // the URL rotates, see below
    ///     SystemUI.HideJoinScreen();                // we're drawing it ourselves
    /// }
    ///
    /// void Refresh()
    /// {
    ///     joinPanel.SetActive(Pairing.IsAvailable);
    ///     joinImage.texture = Pairing.GetQrTexture();
    ///     joinLabel.text    = Pairing.Caption;
    /// }
    /// </code>
    ///
    /// Works identically on the Okona Console (where the code points at the console
    /// itself over your Wi-Fi, or at the relay if the owner turned that on) and in a
    /// browser playing a shared link (where it always goes through the relay).
    ///
    /// IMPORTANT — the URL is not fixed for the session. Pairing tokens rotate: the
    /// console's LAN token cycles, and the relay re-mints the phone token a few
    /// minutes before its hour is up. A game that reads <see cref="Url"/> once at
    /// startup will be displaying a dead code an hour into an unattended run, so
    /// subscribe to <see cref="OnChanged"/>. (<see cref="GetQrTexture"/> reuses one
    /// Texture2D, so if you only ever assign its result to a RawImage you get the
    /// new code for free — but the caption can change with it.)
    ///
    /// <see cref="IsAvailable"/> is false when there is no pairing to show: on the
    /// web for the first moment while the relay room mints, permanently if the
    /// visitor's browser blocked that, and on a console that isn't offering phone
    /// pairing. Always give the player another way in when it's false.
    /// </summary>
    public static class Pairing
    {
        /// <summary>True when there is a pairing code to show right now.</summary>
        public static bool IsAvailable { get; private set; }

        /// <summary>The pairing URL a phone opens, or "" when unavailable.</summary>
        public static string Url { get; private set; } = "";

        /// <summary>
        /// Short human explanation of how the phone reaches this session, ready to
        /// print under the code — "On the same Wi-Fi" or "Uses your phone's
        /// internet". "" when unavailable.
        /// </summary>
        public static string Caption { get; private set; } = "";

        /// <summary>
        /// Raised (on the main thread) whenever the pairing changes — a new code, a
        /// rotated token, or pairing becoming available/unavailable. The argument is
        /// the new <see cref="Url"/> ("" when it went away).
        /// </summary>
        public static event Action<string> OnChanged;

        /// <summary>
        /// The current pairing code as a texture, ready for a RawImage — 512×512,
        /// black on white, quiet zone included. Returns null when no code is
        /// available (check <see cref="IsAvailable"/> first).
        ///
        /// The same Texture2D instance is reused across rotations, so assigning it
        /// once is enough; don't Destroy it. Decoding happens on the first call
        /// after a change, not every frame.
        ///
        /// In the Unity Editor this returns a NON-SCANNABLE placeholder so you can
        /// build and preview your join screen without a WebGL build. Only a real
        /// Okona session produces a working code.
        /// </summary>
        public static Texture2D GetQrTexture()
        {
            if (!IsAvailable) return null;
            if (!s_QrDirty) return s_Qr;
            s_QrDirty = false;

            int n = OkonaInterop.GetPairQr(s_QrBuf);
            if (n < 0)
            {
                s_QrBuf = new byte[-n];
                n = OkonaInterop.GetPairQr(s_QrBuf);
            }

            if (n <= 0)
            {
                // No PNG from the shell. In a real build that means the QR renderer
                // was unavailable — return null rather than something fake, because
                // an unscannable code on a sign is worse than an empty panel. The
                // game still has Url and can encode it itself.
                DropTexture();
                if (OkonaInterop.HasJsLayer) return null;
                s_Qr = MakeEditorPlaceholder();
                return s_Qr;
            }

            EnsureTexture();
            // LoadImage resizes in place, so the instance the game already assigned
            // to its RawImage keeps working across every token rotation.
            if (!s_Qr.LoadImage(SliceExact(s_QrBuf, n)))
            {
                DropTexture();
                return null;
            }
            return s_Qr;
        }

        // ---- internals ----

        static int s_Version;
        static byte[] s_InfoBuf = new byte[512];
        static byte[] s_QrBuf = new byte[4096];
        static Texture2D s_Qr;
        static bool s_QrDirty;

        /// <summary>Called once per frame from OkonaInputBridge.Pump — one int call unless something changed.</summary>
        internal static void Poll()
        {
            int v = OkonaInterop.GetPairVersion();
            if (v == s_Version) return;
            s_Version = v;

            if (v == 0)
            {
                if (!IsAvailable) return;
                IsAvailable = false;
                Url = "";
                Caption = "";
                DropTexture();
                Raise();
                return;
            }

            int n = OkonaInterop.GetPairInfo(s_InfoBuf);
            if (n < 0)
            {
                s_InfoBuf = new byte[-n];
                n = OkonaInterop.GetPairInfo(s_InfoBuf);
            }
            if (n <= 0)
            {
                // Read raced the shell rewriting the pairing. Forget the version
                // we just accepted so the next frame retries — otherwise we'd sit
                // on stale state until the token happened to rotate again.
                s_Version = 0;
                return;
            }

            string s = System.Text.Encoding.UTF8.GetString(s_InfoBuf, 0, n);
            int split = s.IndexOf('\n');
            Url = split >= 0 ? s.Substring(0, split) : s;
            Caption = split >= 0 ? s.Substring(split + 1) : "";
            IsAvailable = Url.Length > 0;
            s_QrDirty = true;
            Raise();
        }

        // A game's handler must never break the input pump it's called from.
        static void Raise()
        {
            var h = OnChanged;
            if (h == null) return;
            try { h(Url); }
            catch (Exception e) { Debug.LogException(e); }
        }

        static void EnsureTexture()
        {
            if (s_Qr == null) s_Qr = NewTexture(2, 2);   // LoadImage resizes it
        }

        static Texture2D NewTexture(int w, int h) =>
            new Texture2D(w, h, TextureFormat.RGBA32, false)
            {
                name = "OkonaPairingQR",
                // Nearest-neighbour: a QR scaled to a 4K sign must keep hard module
                // edges — bilinear smears them and costs scanners their contrast.
                filterMode = FilterMode.Point,
                wrapMode = TextureWrapMode.Clamp,
                hideFlags = HideFlags.HideAndDontSave
            };

        static void DropTexture()
        {
            if (s_Qr == null) return;
            // The Input System pumps in edit mode too, where Destroy throws.
            if (Application.isPlaying) UnityEngine.Object.Destroy(s_Qr);
            else UnityEngine.Object.DestroyImmediate(s_Qr);
            s_Qr = null;
        }

        /// <summary>
        /// Clear cached state at player start. Matters with Domain Reload turned
        /// off, where statics survive between Play Mode sessions — a carried-over
        /// version number would make the first Poll of the next session a no-op.
        /// </summary>
        internal static void Reset()
        {
            s_Version = 0;
            IsAvailable = false;
            Url = "";
            Caption = "";
            s_QrDirty = false;
            DropTexture();
        }

        static byte[] SliceExact(byte[] buf, int n)
        {
            if (buf.Length == n) return buf;
            var exact = new byte[n];
            Array.Copy(buf, exact, n);
            return exact;
        }

        // Editor-only stand-in: QR-shaped, deterministic, and deliberately NOT a
        // valid code — it exists so layouts can be sized and previewed in Play Mode.
        static Texture2D MakeEditorPlaceholder()
        {
            const int Modules = 25, Quiet = 2, Scale = 16;   // 25 * 16 = 400 + quiet zone
            int size = (Modules + Quiet * 2) * Scale;
            var tex = NewTexture(size, size);

            var px = new Color32[size * size];
            var white = new Color32(255, 255, 255, 255);
            var black = new Color32(0, 0, 0, 255);
            for (int i = 0; i < px.Length; i++) px[i] = white;

            uint seed = 0x9E3779B9;
            for (int my = 0; my < Modules; my++)
            {
                for (int mx = 0; mx < Modules; mx++)
                {
                    seed = seed * 1664525u + 1013904223u;
                    bool on = IsFinder(mx, my, Modules) ?? ((seed >> 16 & 1u) == 1u);
                    if (!on) continue;
                    int ox = (mx + Quiet) * Scale, oy = (my + Quiet) * Scale;
                    for (int y = 0; y < Scale; y++)
                        for (int x = 0; x < Scale; x++)
                            px[(oy + y) * size + ox + x] = black;
                }
            }
            tex.SetPixels32(px);
            tex.Apply(false);
            return tex;
        }

        // The three corner finder patterns, so the placeholder reads as a QR.
        static bool? IsFinder(int x, int y, int n)
        {
            if (!(InBox(x, y, 0, 0) || InBox(x, y, n - 7, 0) || InBox(x, y, 0, n - 7))) return null;
            int lx = x < 7 ? x : x - (n - 7);
            int ly = y < 7 ? y : y - (n - 7);
            int ring = Math.Max(Math.Abs(lx - 3), Math.Abs(ly - 3));
            return ring != 2;   // solid 7×7 border, white ring, solid 3×3 core
        }

        static bool InBox(int x, int y, int bx, int by)
            => x >= bx && x < bx + 7 && y >= by && y < by + 7;
    }
}
