using System;

namespace OkonaPad
{
    /// <summary>
    /// Wire format for the shared input snapshot that the Okona web layer writes
    /// into WebAssembly memory and the SDK reads each frame — zero allocation, no
    /// SendMessage / Base64 / JSON. Layout is fixed and little-endian.
    ///
    /// MUST stay byte-for-byte in lockstep with <c>bridge/okona-padstate.js</c>.
    ///
    /// Header (4 bytes): [magic 'O'=0x4F][version][maxPads][reserved]
    /// Per pad (17 bytes), one per slot 0..maxPads-1:
    ///   [0]      flags         bit0 = connected
    ///   [1..4]   buttons       uint32 LE — bit i set = standard gamepad button i
    ///                          pressed (i = 0..16, see <see cref="Button"/>)
    ///   [5..6]   leftX         int16  LE  (-32767..32767  =>  -1..1)
    ///   [7..8]   leftY         int16  LE
    ///   [9..10]  rightX        int16  LE
    ///   [11..12] rightY        int16  LE
    ///   [13..14] leftTrigger   uint16 LE  (0..65535  =>  0..1)
    ///   [15..16] rightTrigger  uint16 LE
    /// Total = 4 + maxPads*17 bytes.
    ///
    /// Stick axes follow the Web Gamepad convention: Y is +down. Triggers are also
    /// exposed as button bits (6/7) for digital use.
    /// </summary>
    public static class OkonaPadState
    {
        public const byte Magic = 0x4F; // 'O'
        public const byte Version = 1;
        public const int MaxPads = 6;

        public const int HeaderSize = 4;
        public const int PadSize = 17;
        public const int TotalSize = HeaderSize + MaxPads * PadSize;

        /// <summary>Standard Web Gamepad button indices — also the bit positions in <c>buttons</c>.</summary>
        public enum Button
        {
            A = 0, B = 1, X = 2, Y = 3,
            LeftShoulder = 4, RightShoulder = 5,
            LeftTrigger = 6, RightTrigger = 7,
            Select = 8, Start = 9,
            LeftStickPress = 10, RightStickPress = 11,
            DpadUp = 12, DpadDown = 13, DpadLeft = 14, DpadRight = 15,
            Guide = 16
        }

        /// <summary>Decoded snapshot of one player slot.</summary>
        public struct Frame
        {
            public bool connected;
            public uint buttons;
            public float leftX, leftY, rightX, rightY;   // -1..1
            public float leftTrigger, rightTrigger;       // 0..1

            public bool IsPressed(Button b) => (buttons & (1u << (int)b)) != 0;
        }

        // ----- decode (runtime: read the buffer the web layer wrote) -----

        /// <summary>True if the buffer has a valid header for this format/version.</summary>
        public static bool HeaderValid(byte[] buf, int len)
            => buf != null && len >= TotalSize && buf[0] == Magic && buf[1] == Version;

        public static Frame DecodePad(byte[] buf, int pad)
        {
            int o = HeaderSize + pad * PadSize;
            Frame f = default;
            f.connected = (buf[o] & 0x1) != 0;
            f.buttons = (uint)(buf[o + 1] | (buf[o + 2] << 8) | (buf[o + 3] << 16) | (buf[o + 4] << 24));
            f.leftX = I16(buf, o + 5) / 32767f;
            f.leftY = I16(buf, o + 7) / 32767f;
            f.rightX = I16(buf, o + 9) / 32767f;
            f.rightY = I16(buf, o + 11) / 32767f;
            f.leftTrigger = U16(buf, o + 13) / 65535f;
            f.rightTrigger = U16(buf, o + 15) / 65535f;
            return f;
        }

        static short I16(byte[] b, int o) => (short)(b[o] | (b[o + 1] << 8));
        static ushort U16(byte[] b, int o) => (ushort)(b[o] | (b[o + 1] << 8));

        // ----- encode (tests + an in-editor simulator; the JS side does this at runtime) -----

        public static byte[] NewBuffer()
        {
            var b = new byte[TotalSize];
            b[0] = Magic; b[1] = Version; b[2] = (byte)MaxPads; b[3] = 0;
            return b;
        }

        public static void EncodePad(byte[] buf, int pad, in Frame f)
        {
            int o = HeaderSize + pad * PadSize;
            buf[o] = (byte)(f.connected ? 1 : 0);
            uint bm = f.buttons;
            buf[o + 1] = (byte)(bm & 0xFF);
            buf[o + 2] = (byte)((bm >> 8) & 0xFF);
            buf[o + 3] = (byte)((bm >> 16) & 0xFF);
            buf[o + 4] = (byte)((bm >> 24) & 0xFF);
            PutI16(buf, o + 5, f.leftX);
            PutI16(buf, o + 7, f.leftY);
            PutI16(buf, o + 9, f.rightX);
            PutI16(buf, o + 11, f.rightY);
            PutU16(buf, o + 13, f.leftTrigger);
            PutU16(buf, o + 15, f.rightTrigger);
        }

        // floor(x + 0.5) matches JavaScript Math.round exactly, so JS and C#
        // produce identical bytes for every input (no midpoint divergence).
        static void PutI16(byte[] b, int o, float v)
        {
            int i = (int)Math.Floor(Clamp(v, -1f, 1f) * 32767f + 0.5f);
            b[o] = (byte)(i & 0xFF);
            b[o + 1] = (byte)((i >> 8) & 0xFF);
        }
        static void PutU16(byte[] b, int o, float v)
        {
            int u = (int)Math.Floor(Clamp(v, 0f, 1f) * 65535f + 0.5f);
            b[o] = (byte)(u & 0xFF);
            b[o + 1] = (byte)((u >> 8) & 0xFF);
        }
        static float Clamp(float v, float lo, float hi) => v < lo ? lo : (v > hi ? hi : v);
    }
}
