using System.Globalization;
using VRio.Core.Device;

namespace VRio.Core.Input;

/// <summary>
/// The plain-text bindings file: one binding per line, <c>#</c> comments,
/// case-insensitive keywords, whitespace-separated tokens. Grammar:
/// <code>
///   key &lt;name&gt;      button &lt;addr&gt; [toggle]
///   key &lt;name&gt;      axis &lt;axis&gt; deflect &lt;n&gt; | rate &lt;n&gt;
///   pad &lt;button&gt;    button &lt;addr&gt; [toggle]
///   padaxis &lt;src&gt;   axis &lt;axis&gt; [invert] [deadzone &lt;d&gt;] [rate &lt;n&gt;]
/// </code>
/// Bad lines are reported (with their line number) and skipped, so one typo
/// doesn't take the whole profile down.
/// </summary>
public static class BindingProfileFormat
{
    /// <summary>
    /// Parse a bindings file. <paramref name="errors"/> receives one message
    /// per rejected line; every well-formed line still becomes a binding.
    /// </summary>
    public static BindingProfile Parse(string text, out IReadOnlyList<string> errors)
    {
        var keyButtons = new List<KeyButtonBinding>();
        var keyAxes = new List<KeyAxisBinding>();
        var padButtons = new List<PadButtonBinding>();
        var padAxes = new List<PadAxisBinding>();
        var errs = new List<string>();
        errors = errs;

        string[] lines = text.Split('\n');
        for (int i = 0; i < lines.Length; i++)
        {
            string line = lines[i];
            int hash = line.IndexOf('#');
            if (hash >= 0) line = line.Substring(0, hash);
            string[] tok = line.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
            if (tok.Length == 0)
                continue;

            try
            {
                ParseLine(tok, keyButtons, keyAxes, padButtons, padAxes);
            }
            catch (FormatException ex)
            {
                errs.Add($"line {i + 1}: {ex.Message}");
            }
        }

        return new BindingProfile(keyButtons, keyAxes, padButtons, padAxes);
    }

    private static void ParseLine(string[] tok,
        List<KeyButtonBinding> keyButtons, List<KeyAxisBinding> keyAxes,
        List<PadButtonBinding> padButtons, List<PadAxisBinding> padAxes)
    {
        if (tok.Length < 4)
            throw new FormatException("expected '<source> <name> <target> <value> …'");

        string source = tok[0].ToLowerInvariant();
        string target = tok[2].ToLowerInvariant();
        switch (source, target)
        {
            case ("key", "button"):
                keyButtons.Add(new KeyButtonBinding(tok[1], ParseAddress(tok[3]), ParseToggle(tok, 4)));
                break;

            case ("key", "axis"):
            {
                if (tok.Length < 6)
                    throw new FormatException("key axis bindings need 'deflect <n>' or 'rate <n>'");
                KeyAxisMode mode = tok[4].ToLowerInvariant() switch
                {
                    "deflect" => KeyAxisMode.Deflect,
                    "rate" => KeyAxisMode.Rate,
                    var other => throw new FormatException($"unknown key-axis mode '{other}' (deflect/rate)"),
                };
                keyAxes.Add(new KeyAxisBinding(tok[1], ParseRioAxis(tok[3]), mode, ParseFloat(tok[5])));
                break;
            }

            case ("pad", "button"):
                padButtons.Add(new PadButtonBinding(ParsePadButton(tok[1]), ParseAddress(tok[3]), ParseToggle(tok, 4)));
                break;

            case ("padaxis", "axis"):
            {
                bool invert = false;
                float deadzone = 0f, rate = 0f;
                for (int i = 4; i < tok.Length; i++)
                {
                    switch (tok[i].ToLowerInvariant())
                    {
                        case "invert":
                            invert = true;
                            break;
                        case "deadzone":
                            deadzone = ParseFloat(TakeValue(tok, ref i, "deadzone"));
                            break;
                        case "rate":
                            rate = ParseFloat(TakeValue(tok, ref i, "rate"));
                            break;
                        default:
                            throw new FormatException($"unknown padaxis option '{tok[i]}'");
                    }
                }
                padAxes.Add(new PadAxisBinding(ParsePadAxis(tok[1]), ParseRioAxis(tok[3]), invert, deadzone, rate));
                break;
            }

            default:
                throw new FormatException($"unknown binding form '{tok[0]} … {tok[2]}'");
        }
    }

    private static string TakeValue(string[] tok, ref int i, string option)
    {
        if (i + 1 >= tok.Length)
            throw new FormatException($"'{option}' needs a value");
        return tok[++i];
    }

    private static bool ParseToggle(string[] tok, int index)
    {
        if (tok.Length <= index)
            return false;
        if (tok.Length == index + 1 && tok[index].Equals("toggle", StringComparison.OrdinalIgnoreCase))
            return true;
        throw new FormatException($"unexpected '{tok[index]}' (only 'toggle' may follow the address)");
    }

    private static int ParseAddress(string s)
    {
        int addr;
        bool ok = s.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
            ? int.TryParse(s.Substring(2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out addr)
            : int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out addr);
        if (!ok || !(RioAddressSpace.IsButton(addr) || RioAddressSpace.IsKeypad(addr)))
            throw new FormatException($"'{s}' is not a RIO input address (0x00-0x47, 0x50-0x6F)");
        return addr;
    }

    private static RioAxis ParseRioAxis(string s) =>
        Enum.TryParse(s, ignoreCase: true, out RioAxis axis) && Enum.IsDefined(typeof(RioAxis), axis)
            ? axis
            : throw new FormatException($"unknown RIO axis '{s}' (Throttle/LeftPedal/RightPedal/JoystickY/JoystickX)");

    private static PadButtons ParsePadButton(string s) =>
        Enum.TryParse(s, ignoreCase: true, out PadButtons b) && b != PadButtons.None && Enum.IsDefined(typeof(PadButtons), b)
            ? b
            : throw new FormatException($"unknown pad button '{s}'");

    private static PadAxis ParsePadAxis(string s) =>
        Enum.TryParse(s, ignoreCase: true, out PadAxis a) && Enum.IsDefined(typeof(PadAxis), a)
            ? a
            : throw new FormatException($"unknown pad axis '{s}'");

    private static float ParseFloat(string s) =>
        float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out float f)
            ? f
            : throw new FormatException($"'{s}' is not a number");

    /// <summary>
    /// The default profile text, written verbatim (comments and all) as the
    /// user's bindings file on first run.
    /// </summary>
    public const string DefaultText = @"# vRIO input bindings — keyboard and Xbox (XInput) controller.
#
# One binding per line, '#' starts a comment, keywords are case-
# insensitive. Edit freely, then press 'Reload bindings' in vRIO.
#
#   key <name>      button <addr> [toggle]
#   key <name>      axis <axis> deflect <n>
#   key <name>      axis <axis> rate <n-per-second>
#   pad <button>    button <addr> [toggle]
#   padaxis <src>   axis <axis> [invert] [deadzone <d>] [rate <n-per-second>]
#
# <addr>    RIO input address: lamp buttons 0x00-0x47, internal keypad
#           0x50-0x5F, external keypad 0x60-0x6F (hex or decimal).
# <axis>    Throttle | LeftPedal | RightPedal | JoystickY | JoystickX
# <name>    .NET Keys name: A-Z, D0-D9 (digit row), F1-F12,
#           NumPad0-NumPad9, Up, Down, Left, Right, Space, Enter,
#           OemMinus, Oemplus, Oemcomma, OemPeriod, ...
# <button>  A B X Y DPadUp DPadDown DPadLeft DPadRight Start Back
#           LeftShoulder RightShoulder LeftThumb RightThumb
# <src>     LeftStickX LeftStickY RightStickX RightStickY
#           LeftTrigger RightTrigger
#
# Axis values are normalized: 1 = the axis' full realistic travel
# (throttle 0..-800 raw, pedals 0..+500, stick +/-80 — the wire signs
# are applied for you). 'deflect' holds the axis there while the key
# is down and springs back on release; 'rate' walks the axis by <n>
# per second and the position sticks (use it for the throttle).
# NOTE: raw stick X runs NEGATIVE to the right (that is what RIOJoy
# expects), so pad LeftStickX is inverted below.

# ---- Axes: controller only ----------------------------------------
# This profile drives all five axes from the Xbox controller - a pad
# is required. Keyboard axis bindings ('key <name> axis ...' with
# deflect or rate, per the grammar above) are still supported if you
# want them back.

# ---- Upper MFD bank: the number row is the top MFD row and the
# ---- QWERTY row is the row under it, left to right across the
# ---- Left / Middle / Right MFDs.
key D1      button 0x2F
key D2      button 0x2E
key D3      button 0x2D
key D4      button 0x2C
key D5      button 0x27
key D6      button 0x26
key D7      button 0x25
key D8      button 0x24
key D9      button 0x37
key D0      button 0x36
key OemMinus button 0x35
key Oemplus  button 0x34
key Q       button 0x2B
key W       button 0x2A
key E       button 0x29
key R       button 0x28
key T       button 0x23
key Y       button 0x22
key U       button 0x21
key I       button 0x20
key O       button 0x33
key P       button 0x32
key OemOpenBrackets  button 0x31
key OemCloseBrackets button 0x30

# ---- Lower MFD bank: home row and the row below it, two 4-key
# ---- blocks split by an unbound gap key (G / B), mirroring the
# ---- keypad gap between the Lower Left and Lower Right MFDs.
key A       button 0x0F
key S       button 0x0E
key D       button 0x0D
key F       button 0x0C
key H       button 0x07
key J       button 0x06
key K       button 0x05
key L       button 0x04
key Z       button 0x0B
key X       button 0x0A
key C       button 0x09
key V       button 0x08
key N       button 0x03
key M       button 0x02
key Oemcomma  button 0x01
key OemPeriod button 0x00

# ---- Secondary / Screen columns on the function keys, top to
# ---- bottom (0x16/0x17 and 0x1E/0x1F intentionally unmapped).
key F1      button 0x10
key F2      button 0x11
key F3      button 0x12
key F4      button 0x13
key F5      button 0x14
key F6      button 0x15
key F7      button 0x18
key F8      button 0x19
key F9      button 0x1A
key F10     button 0x1B
key F11     button 0x1C
key F12     button 0x1D

# ---- Joystick column: hat on the arrows, Main on Space. Pinky /
# ---- Middle / Upper / Panic are pad-only (X / B / Y / LeftShoulder).
key Space   button 0x40    # Main
key Up      button 0x42    # Hat Up
key Down    button 0x41    # Hat Back
key Left    button 0x44    # Hat Left
key Right   button 0x43    # Hat Right

# ---- Internal keypad on the numpad (hex keys on the operators) ----
key NumPad0 button 0x50
key NumPad1 button 0x51
key NumPad2 button 0x52
key NumPad3 button 0x53
key NumPad4 button 0x54
key NumPad5 button 0x55
key NumPad6 button 0x56
key NumPad7 button 0x57
key NumPad8 button 0x58
key NumPad9 button 0x59
key Divide   button 0x5A    # keypad A  (/)
key Multiply button 0x5B    # keypad B  (*)
key Subtract button 0x5C    # keypad C  (-)
key Add      button 0x5D    # keypad D  (+)
key Decimal  button 0x5E    # keypad E  (.)
key Return   button 0x5F    # keypad F  (Enter - the main Enter key too)

# ---- Xbox controller: axes ----------------------------------------
padaxis LeftStickX   axis JoystickX invert deadzone 0.2
padaxis LeftStickY   axis JoystickY deadzone 0.2
padaxis RightStickY  axis Throttle deadzone 0.2 rate 0.75
padaxis LeftTrigger  axis LeftPedal deadzone 0.12
padaxis RightTrigger axis RightPedal deadzone 0.12

# ---- Xbox controller: buttons -------------------------------------
pad A             button 0x40    # Main
pad B             button 0x46    # Middle
pad X             button 0x45    # Pinky
pad Y             button 0x47    # Upper
pad DPadUp        button 0x42    # Hat Up
pad DPadDown      button 0x41    # Hat Back
pad DPadLeft      button 0x44    # Hat Left
pad DPadRight     button 0x43    # Hat Right
pad LeftShoulder  button 0x3D    # Panic
pad RightShoulder button 0x3F    # Throttle button
";
}
