using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.LowLevel;

namespace OkonaPad
{
    /// <summary>
    /// Drives Okona controllers into Unity's Input System. Self-bootstrapping — just
    /// having the SDK in the project is enough; no GameObject or setup required.
    ///
    /// Each frame it pulls the shared input snapshot from the web layer (straight out
    /// of WebAssembly memory — no SendMessage / Base64 / JSON / allocation) and queues
    /// one <see cref="GamepadState"/> per connected player onto an <see cref="OkonaPadDevice"/>.
    /// Devices are added/removed as players connect/disconnect.
    /// </summary>
    public static class OkonaInputBridge
    {
        static readonly OkonaPadDevice[] s_Devices = new OkonaPadDevice[OkonaPadState.MaxPads];
        static byte[] s_Buf;
        static bool s_Registered;
        static bool s_Hooked;

        /// <summary>Connected device for a player slot (0-based), or null.</summary>
        public static OkonaPadDevice GetDevice(int slot)
            => (slot >= 0 && slot < OkonaPadState.MaxPads) ? s_Devices[slot] : null;

#if UNITY_EDITOR
        /// <summary>Editor/test only: feed a snapshot as if it came from the web layer, then call <see cref="Pump"/>.</summary>
        public static void SimulateSnapshot(byte[] snapshot) { OkonaInterop.EditorInject = snapshot; }
#endif

        [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
        static void Init()
        {
            EnsureRegistered();
            if (!s_Hooked)
            {
                s_Hooked = true;
                InputSystem.onBeforeUpdate += Pump;
#if UNITY_WEBGL && !UNITY_EDITOR
                // Don't let Unity's own browser-gamepad backend also create devices —
                // OkonaPad is the single source of truth, so a pad is never seen twice.
                foreach (var d in InputSystem.devices) SuppressNative(d);
                InputSystem.onDeviceChange += (d, change) =>
                {
                    if (change == InputDeviceChange.Added) SuppressNative(d);
                };
#endif
            }
        }

        static void EnsureRegistered()
        {
            if (!s_Registered)
            {
                s_Registered = true;
                InputSystem.RegisterLayout<OkonaPadDevice>("OkonaPad");
            }
            if (s_Buf == null) s_Buf = new byte[OkonaPadState.TotalSize];
        }

        static void SuppressNative(InputDevice d)
        {
            if (d is Gamepad && !(d is OkonaPadDevice) && d.enabled)
                InputSystem.DisableDevice(d);
        }

        /// <summary>
        /// Pull the current snapshot and push it to the Input System. Called every frame
        /// via onBeforeUpdate at runtime; also callable directly for deterministic tests.
        /// </summary>
        public static void Pump()
        {
            EnsureRegistered();

            int n = OkonaInterop.ReadState(s_Buf);
            if (!OkonaPadState.HeaderValid(s_Buf, n)) return;

            for (int slot = 0; slot < OkonaPadState.MaxPads; slot++)
            {
                var f = OkonaPadState.DecodePad(s_Buf, slot);
                var dev = s_Devices[slot];

                if (f.connected)
                {
                    if (dev == null)
                    {
                        dev = InputSystem.AddDevice<OkonaPadDevice>("Okona Pad " + (slot + 1));
                        dev.okonaSlot = slot;
                        s_Devices[slot] = dev;
                    }
                    InputSystem.QueueStateEvent(dev, ToGamepadState(f));
                }
                else if (dev != null)
                {
                    s_Devices[slot] = null;
                    InputSystem.RemoveDevice(dev);
                }
            }
        }

        static GamepadState ToGamepadState(in OkonaPadState.Frame f)
        {
            var gs = new GamepadState()
                .WithButton(GamepadButton.South, f.IsPressed(OkonaPadState.Button.A))
                .WithButton(GamepadButton.East, f.IsPressed(OkonaPadState.Button.B))
                .WithButton(GamepadButton.West, f.IsPressed(OkonaPadState.Button.X))
                .WithButton(GamepadButton.North, f.IsPressed(OkonaPadState.Button.Y))
                .WithButton(GamepadButton.LeftShoulder, f.IsPressed(OkonaPadState.Button.LeftShoulder))
                .WithButton(GamepadButton.RightShoulder, f.IsPressed(OkonaPadState.Button.RightShoulder))
                .WithButton(GamepadButton.Select, f.IsPressed(OkonaPadState.Button.Select))
                .WithButton(GamepadButton.Start, f.IsPressed(OkonaPadState.Button.Start))
                .WithButton(GamepadButton.LeftStick, f.IsPressed(OkonaPadState.Button.LeftStickPress))
                .WithButton(GamepadButton.RightStick, f.IsPressed(OkonaPadState.Button.RightStickPress))
                .WithButton(GamepadButton.DpadUp, f.IsPressed(OkonaPadState.Button.DpadUp))
                .WithButton(GamepadButton.DpadDown, f.IsPressed(OkonaPadState.Button.DpadDown))
                .WithButton(GamepadButton.DpadLeft, f.IsPressed(OkonaPadState.Button.DpadLeft))
                .WithButton(GamepadButton.DpadRight, f.IsPressed(OkonaPadState.Button.DpadRight));

            gs.leftStick = new Vector2(f.leftX, f.leftY);
            gs.rightStick = new Vector2(f.rightX, f.rightY);
            gs.leftTrigger = f.leftTrigger;
            gs.rightTrigger = f.rightTrigger;
            return gs;
        }
    }
}
