using System.Runtime.InteropServices;
using System.Text;
using SDL2;

namespace Nerfed.Runtime;

public static class Keyboard
{
    private static readonly List<char> textInput = new List<char>();

    private const int maxPressedKeys = 8;
    private static readonly List<Key> keys = new List<Key>(maxPressedKeys);
    private static readonly List<Key> lastKeys = new List<Key>(maxPressedKeys);

    /// <summary>
    /// True if the key was pressed or continued to be held down this frame.
    /// </summary>
    /// <param name="key"></param>
    /// <returns></returns>
    public static bool IsKeyDown(Key key)
    {
        return HasKey(keys, key);
    }
    
    /// <summary>
    /// True if the key was pressed this frame.
    /// </summary>
    public static bool IsKeyPressed(Key key)
    {
        return HasKey(keys, key) && !HasKey(lastKeys, key);
    }

    /// <summary>
    /// True if the key was released or continued to be released this frame.
    /// </summary>
    public static bool IsKeyUp(Key key)
    {
        return !HasKey(keys, key);
    }

    /// <summary>
    /// True if the key was released this frame.
    /// </summary>
    public static bool IsKeyReleased(Key key)
    {
        return !HasKey(keys, key) && HasKey(lastKeys, key);
    }

    public static ReadOnlySpan<Key> GetPressedKeys()
    {
        return CollectionsMarshal.AsSpan(keys);
    }

    public static ReadOnlySpan<char> GetTextInput()
    {
        return CollectionsMarshal.AsSpan(textInput);
    }

    internal static void Update()
    {
        textInput.Clear();

        lastKeys.Clear();
        if (keys.Count > 0)
        {
            lastKeys.AddRange(keys);
        }
    }

    private static bool HasKey(List<Key> list, Key key)
    {
        for (int i = 0; i < list.Count; i++)
        {
            if (keys[i] == key)
            {
                return true;
            }
        }

        return false;
    }
    
    internal static void ProcessEvent(ref SDL.SDL_Event ev)
    {
        switch (ev.type)
        {
            case SDL.SDL_EventType.SDL_TEXTINPUT:
                ProcessTextInputEvent(ref ev.text);
                break;
            case SDL.SDL_EventType.SDL_KEYDOWN:
                ProcessKeyDownEvent(ref ev.key);
                break;
            case SDL.SDL_EventType.SDL_KEYUP:
                ProcessKeyUpEvent(ref ev.key);
                break;
        }
    }

    private static unsafe void ProcessTextInputEvent(ref SDL.SDL_TextInputEvent evt)
    {
        // Based on the SDL2# LPUtf8StrMarshaler
        int MeasureStringLength(byte* ptr)
        {
            int bytes;
            for (bytes = 0; *ptr != 0; ptr += 1, bytes += 1) ;
            return bytes;
        }

        fixed (byte* textPtr = evt.text)
        {
            int bytes = MeasureStringLength(textPtr);
            if (bytes > 0)
            {
                /* UTF8 will never encode more characters
                 * than bytes in a string, so bytes is a
                 * suitable upper estimate of size needed
                 */
                char* charsBuffer = stackalloc char[bytes];
                int chars = Encoding.UTF8.GetChars(
                    textPtr,
                    bytes,
                    charsBuffer,
                    bytes
                );

                if (chars > 0)
                {
                    textInput.AddRange(new ReadOnlySpan<char>(charsBuffer, chars));
                }
            }
        }
    }

    private static void ProcessKeyDownEvent(ref SDL.SDL_KeyboardEvent ev)
    {
        Key key = ConvertScancode(ev.keysym.scancode);
        SetKeyState(key, KeyState.Down);
    }

    private static void ProcessKeyUpEvent(ref SDL.SDL_KeyboardEvent ev)
    {
        Key key = ConvertScancode(ev.keysym.scancode);
        SetKeyState(key, KeyState.Up);
    }

    public static void SetKeyState(Key key, KeyState state)
    {
        if (keys.Count >= maxPressedKeys)
        {
            return;
        }

        if (state == KeyState.Down)
        {
            if (!keys.Contains(key))
            {
                keys.Add(key);
            }
        }
        else if (state == KeyState.Up)
        {
            keys.Remove(key);
        }
    }

    private static Key ConvertScancode(SDL.SDL_Scancode scancode)
    {
        switch (scancode)
        {
            case SDL.SDL_Scancode.SDL_SCANCODE_A: return Key.A;
            case SDL.SDL_Scancode.SDL_SCANCODE_B: return Key.B;
            case SDL.SDL_Scancode.SDL_SCANCODE_C: return Key.C;
            case SDL.SDL_Scancode.SDL_SCANCODE_D: return Key.D;
            case SDL.SDL_Scancode.SDL_SCANCODE_E: return Key.E;
            case SDL.SDL_Scancode.SDL_SCANCODE_F: return Key.F;
            case SDL.SDL_Scancode.SDL_SCANCODE_G: return Key.G;
            case SDL.SDL_Scancode.SDL_SCANCODE_H: return Key.H;
            case SDL.SDL_Scancode.SDL_SCANCODE_I: return Key.I;
            case SDL.SDL_Scancode.SDL_SCANCODE_J: return Key.J;
            case SDL.SDL_Scancode.SDL_SCANCODE_K: return Key.K;
            case SDL.SDL_Scancode.SDL_SCANCODE_L: return Key.L;
            case SDL.SDL_Scancode.SDL_SCANCODE_M: return Key.M;
            case SDL.SDL_Scancode.SDL_SCANCODE_N: return Key.N;
            case SDL.SDL_Scancode.SDL_SCANCODE_O: return Key.O;
            case SDL.SDL_Scancode.SDL_SCANCODE_P: return Key.P;
            case SDL.SDL_Scancode.SDL_SCANCODE_Q: return Key.Q;
            case SDL.SDL_Scancode.SDL_SCANCODE_R: return Key.R;
            case SDL.SDL_Scancode.SDL_SCANCODE_S: return Key.S;
            case SDL.SDL_Scancode.SDL_SCANCODE_T: return Key.T;
            case SDL.SDL_Scancode.SDL_SCANCODE_U: return Key.U;
            case SDL.SDL_Scancode.SDL_SCANCODE_V: return Key.V;
            case SDL.SDL_Scancode.SDL_SCANCODE_W: return Key.W;
            case SDL.SDL_Scancode.SDL_SCANCODE_X: return Key.X;
            case SDL.SDL_Scancode.SDL_SCANCODE_Y: return Key.Y;
            case SDL.SDL_Scancode.SDL_SCANCODE_Z: return Key.Z;
            case SDL.SDL_Scancode.SDL_SCANCODE_1: return Key.D1;
            case SDL.SDL_Scancode.SDL_SCANCODE_2: return Key.D2;
            case SDL.SDL_Scancode.SDL_SCANCODE_3: return Key.D3;
            case SDL.SDL_Scancode.SDL_SCANCODE_4: return Key.D4;
            case SDL.SDL_Scancode.SDL_SCANCODE_5: return Key.D5;
            case SDL.SDL_Scancode.SDL_SCANCODE_6: return Key.D6;
            case SDL.SDL_Scancode.SDL_SCANCODE_7: return Key.D7;
            case SDL.SDL_Scancode.SDL_SCANCODE_8: return Key.D8;
            case SDL.SDL_Scancode.SDL_SCANCODE_9: return Key.D9;
            case SDL.SDL_Scancode.SDL_SCANCODE_0: return Key.D0;
            case SDL.SDL_Scancode.SDL_SCANCODE_RETURN: return Key.Enter;
            case SDL.SDL_Scancode.SDL_SCANCODE_ESCAPE: return Key.Escape;
            case SDL.SDL_Scancode.SDL_SCANCODE_BACKSPACE: return Key.Backspace;
            case SDL.SDL_Scancode.SDL_SCANCODE_TAB: return Key.Tab;
            case SDL.SDL_Scancode.SDL_SCANCODE_SPACE: return Key.Space;
            case SDL.SDL_Scancode.SDL_SCANCODE_MINUS: return Key.Minus;
            case SDL.SDL_Scancode.SDL_SCANCODE_EQUALS: return Key.Equals;
            case SDL.SDL_Scancode.SDL_SCANCODE_LEFTBRACKET: return Key.LeftBracket;
            case SDL.SDL_Scancode.SDL_SCANCODE_RIGHTBRACKET: return Key.RightBracket;
            case SDL.SDL_Scancode.SDL_SCANCODE_BACKSLASH: return Key.Backslash;
            case SDL.SDL_Scancode.SDL_SCANCODE_SEMICOLON: return Key.Semicolon;
            case SDL.SDL_Scancode.SDL_SCANCODE_APOSTROPHE: return Key.Apostrophe;
            case SDL.SDL_Scancode.SDL_SCANCODE_GRAVE: return Key.Tilde;
            case SDL.SDL_Scancode.SDL_SCANCODE_COMMA: return Key.Comma;
            case SDL.SDL_Scancode.SDL_SCANCODE_PERIOD: return Key.Period;
            case SDL.SDL_Scancode.SDL_SCANCODE_SLASH: return Key.Slash;
            case SDL.SDL_Scancode.SDL_SCANCODE_CAPSLOCK: return Key.CapsLock;
            case SDL.SDL_Scancode.SDL_SCANCODE_F1: return Key.F1;
            case SDL.SDL_Scancode.SDL_SCANCODE_F2: return Key.F2;
            case SDL.SDL_Scancode.SDL_SCANCODE_F3: return Key.F3;
            case SDL.SDL_Scancode.SDL_SCANCODE_F4: return Key.F4;
            case SDL.SDL_Scancode.SDL_SCANCODE_F5: return Key.F5;
            case SDL.SDL_Scancode.SDL_SCANCODE_F6: return Key.F6;
            case SDL.SDL_Scancode.SDL_SCANCODE_F7: return Key.F7;
            case SDL.SDL_Scancode.SDL_SCANCODE_F8: return Key.F8;
            case SDL.SDL_Scancode.SDL_SCANCODE_F9: return Key.F9;
            case SDL.SDL_Scancode.SDL_SCANCODE_F10: return Key.F10;
            case SDL.SDL_Scancode.SDL_SCANCODE_F11: return Key.F11;
            case SDL.SDL_Scancode.SDL_SCANCODE_F12: return Key.F12;
            case SDL.SDL_Scancode.SDL_SCANCODE_PRINTSCREEN: return Key.PrintScreen;
            case SDL.SDL_Scancode.SDL_SCANCODE_SCROLLLOCK: return Key.Scroll;
            case SDL.SDL_Scancode.SDL_SCANCODE_PAUSE: return Key.Pause;
            case SDL.SDL_Scancode.SDL_SCANCODE_INSERT: return Key.Insert;
            case SDL.SDL_Scancode.SDL_SCANCODE_HOME: return Key.Home;
            case SDL.SDL_Scancode.SDL_SCANCODE_PAGEUP: return Key.PageUp;
            case SDL.SDL_Scancode.SDL_SCANCODE_DELETE: return Key.Delete;
            case SDL.SDL_Scancode.SDL_SCANCODE_END: return Key.End;
            case SDL.SDL_Scancode.SDL_SCANCODE_PAGEDOWN: return Key.PageDown;
            case SDL.SDL_Scancode.SDL_SCANCODE_RIGHT: return Key.Right;
            case SDL.SDL_Scancode.SDL_SCANCODE_LEFT: return Key.Left;
            case SDL.SDL_Scancode.SDL_SCANCODE_DOWN: return Key.Down;
            case SDL.SDL_Scancode.SDL_SCANCODE_UP: return Key.Up;
            case SDL.SDL_Scancode.SDL_SCANCODE_NUMLOCKCLEAR: return Key.NumLock;
            case SDL.SDL_Scancode.SDL_SCANCODE_KP_DIVIDE: return Key.Divide;
            case SDL.SDL_Scancode.SDL_SCANCODE_KP_MULTIPLY: return Key.Multiply;
            case SDL.SDL_Scancode.SDL_SCANCODE_KP_MINUS: return Key.Subtract;
            case SDL.SDL_Scancode.SDL_SCANCODE_KP_PLUS: return Key.Add;
            case SDL.SDL_Scancode.SDL_SCANCODE_KP_ENTER: return Key.Enter;
            case SDL.SDL_Scancode.SDL_SCANCODE_KP_1: return Key.NumPad1;
            case SDL.SDL_Scancode.SDL_SCANCODE_KP_2: return Key.NumPad2;
            case SDL.SDL_Scancode.SDL_SCANCODE_KP_3: return Key.NumPad3;
            case SDL.SDL_Scancode.SDL_SCANCODE_KP_4: return Key.NumPad4;
            case SDL.SDL_Scancode.SDL_SCANCODE_KP_5: return Key.NumPad5;
            case SDL.SDL_Scancode.SDL_SCANCODE_KP_6: return Key.NumPad6;
            case SDL.SDL_Scancode.SDL_SCANCODE_KP_7: return Key.NumPad7;
            case SDL.SDL_Scancode.SDL_SCANCODE_KP_8: return Key.NumPad8;
            case SDL.SDL_Scancode.SDL_SCANCODE_KP_9: return Key.NumPad9;
            case SDL.SDL_Scancode.SDL_SCANCODE_KP_0: return Key.NumPad0;
            case SDL.SDL_Scancode.SDL_SCANCODE_KP_PERIOD: return Key.Period;
            case SDL.SDL_Scancode.SDL_SCANCODE_APPLICATION: return Key.Apps;
            case SDL.SDL_Scancode.SDL_SCANCODE_KP_EQUALS: return Key.Equals;
            case SDL.SDL_Scancode.SDL_SCANCODE_F13: return Key.F13;
            case SDL.SDL_Scancode.SDL_SCANCODE_F14: return Key.F14;
            case SDL.SDL_Scancode.SDL_SCANCODE_F15: return Key.F15;
            case SDL.SDL_Scancode.SDL_SCANCODE_F16: return Key.F16;
            case SDL.SDL_Scancode.SDL_SCANCODE_F17: return Key.F17;
            case SDL.SDL_Scancode.SDL_SCANCODE_F18: return Key.F18;
            case SDL.SDL_Scancode.SDL_SCANCODE_F19: return Key.F19;
            case SDL.SDL_Scancode.SDL_SCANCODE_F20: return Key.F20;
            case SDL.SDL_Scancode.SDL_SCANCODE_F21: return Key.F21;
            case SDL.SDL_Scancode.SDL_SCANCODE_F22: return Key.F22;
            case SDL.SDL_Scancode.SDL_SCANCODE_F23: return Key.F23;
            case SDL.SDL_Scancode.SDL_SCANCODE_F24: return Key.F24;
            case SDL.SDL_Scancode.SDL_SCANCODE_EXECUTE: return Key.Execute;
            case SDL.SDL_Scancode.SDL_SCANCODE_HELP: return Key.Help;
            case SDL.SDL_Scancode.SDL_SCANCODE_MENU: return Key.Apps;
            case SDL.SDL_Scancode.SDL_SCANCODE_SELECT: return Key.Select;
            case SDL.SDL_Scancode.SDL_SCANCODE_KP_COMMA: return Key.Comma;
            case SDL.SDL_Scancode.SDL_SCANCODE_LCTRL: return Key.LeftControl;
            case SDL.SDL_Scancode.SDL_SCANCODE_LSHIFT: return Key.LeftShift;
            case SDL.SDL_Scancode.SDL_SCANCODE_LALT: return Key.LeftAlt;
            case SDL.SDL_Scancode.SDL_SCANCODE_LGUI: return Key.LeftSuper;
            case SDL.SDL_Scancode.SDL_SCANCODE_RCTRL: return Key.RightControl;
            case SDL.SDL_Scancode.SDL_SCANCODE_RSHIFT: return Key.RightShift;
            case SDL.SDL_Scancode.SDL_SCANCODE_RALT: return Key.RightAlt;
            case SDL.SDL_Scancode.SDL_SCANCODE_RGUI: return Key.RightSuper;
            default: return Key.None;
        }
    }
}
