"""Numeric codes for Roboteq serial command constants (the `_XXX` names
used as the first argument to GetValue/SetCommand/GetConfig/SetConfig).

Two tiers of confidence here:

1. **Bytecode-confirmed** (the highest bar): decoded directly from real
   RoborunPlus-compiled `.hex` output.

       _VAR      = 6   (batch 4, GetValue and SetCommand)
       _ALIM     = 42 (0x2A)  (batch 4, GetConfig/SetConfig)
       _MS       = 16 (0x10)  (batch 6, SetCommand)
       _GO       = 0          (batch 6, SetCommand)
       _ESTOP    = 14 (0x0E)  (batch 6, SetCommand)
       _AI       = 16 (0x10)  (batch 6, GetValue)
       _BATAMPS  = 12 (0x0C)  (batch 6, GetValue)
       _MOTAMPS  = 0           (batch 6, GetValue)
       _MOTPWR   = 2           (batch 6, GetValue)
       _V        = 13 (0x0D)  (batch 6, GetValue)
       _D0       = 11 (0x0B)  (onoff.mbs, 2026-07-29, SetCommand -- corrects
                                the manual-sourced guess of 9 below, which
                                was apparently confused with _DOUT's own
                                code. _D1 = 10 (0x0A) was independently
                                confirmed correct by the same diff.)

2. **Manual-sourced** (everything else below): read directly from the
   "HexCode:" field documented for each command/query/config parameter in
   *Roboteq Controllers User Manual*, V2.1. All 10 bytecode-confirmed values
   above also appear in the manual and match exactly -- strong evidence the
   manual's HexCode field is what actually goes on the wire, so the other
   ~300 values it documents are trusted here even though they haven't each
   been individually diffed against a compiled `.hex` file.

Extraction notes (see `docs/reverse_engineering.md` for the full writeup):

- A handful of aliases were dropped because the manual itself has
  copy-paste errors: some later sections reuse an earlier section's
  `Syntax Scripting` example without updating the `_XXX` name to match that
  section's own declared alias (e.g. the `CU` command's example says
  `setcommand(_CS, ...)` -- copied from the preceding `CS` section and never
  fixed). Where a section's own alias doesn't match the `_XXX` name in its
  own example, that example was discarded rather than trusted.
- The DS402 (CANopen motion-profile) command/query sections were excluded
  entirely. That's a separate operating profile from plain serial
  MicroBasic scripting and several of its aliases collide with unrelated
  classic-mode codes for the same short name (e.g. a DS402-only `MOTVEL`
  variant using a different HexCode than the classic `S` command's
  `_MOTVEL`) -- since a MicroBasic script on this hardware isn't running in
  DS402 mode, the classic definition is what applies, and the DS402
  duplicate would just be wrong here.
- `_BRUN` and `_CSS` (and only these two, once the above two rules are
  applied) turned out to genuinely mean different things depending on the
  verb: `_BRUN` is HexCode `0x0C` as a `SetCommand` (start/stop/restart the
  running script) but `0x48` as a `SetConfig` (enable auto-start on power
  up) -- two unrelated parameters that happen to share an alias. `_CSS` is
  `0x6C` to `SetCommand` (load an SSI counter) and `0x6E` to `GetValue`
  (read it back) -- related but still separately numbered. These are
  handled by `VERB_SPECIFIC_CODES` below and checked before the flat table.

Command names are matched case-insensitively, without the leading
underscore (the parser strips it before lookup).
"""

from __future__ import annotations

COMMAND_CODES: dict[str, int] = {
    "a": 0,
    "abcntr": 4,
    "abspeed": 3,
    "ac": 7,
    "accel": 7,
    "acs": 11,
    "actr": 22,
    "adb": 23,
    "ai": 16,
    "aic": 35,
    "aina": 25,
    "alim": 42,
    "alin": 24,
    "amax": 21,
    "amaxa": 27,
    "amin": 20,
    "amina": 26,
    "amod": 19,
    "ams": 12,
    "anain": 16,
    "anainc": 35,
    "ang": 66,
    "apol": 28,
    "asi": 51,
    "atga": 44,
    "atgd": 45,
    "atrig": 43,
    "ax": 20,
    "b": 22,
    "ba": 12,
    "badj": 96,
    "badv": 97,
    "batamps": 12,
    "bdt": 232,
    "becc": 189,
    "bee": 100,
    "bfbk": 99,
    "bhl": 62,
    "bhla": 64,
    "bhome": 60,
    "bhv": 231,
    "bind": 28,
    "bkd": 1,
    "blcntr": 5,
    "blfb": 59,
    "bll": 61,
    "blla": 63,
    "blrspeed": 11,
    "blspeed": 10,
    "blstd": 58,
    "bmc": 76,
    "bmf": 77,
    "bmod": 95,
    "bnd": 28,
    "bool": 22,
    "bpol": 57,
    "bpr": 243,
    "brv": 230,
    "bs": 10,
    "bsc": 80,
    "bsr": 11,
    "bzpw": 98,
    "c": 4,
    "calive": 38,
    "can": 39,
    "cango": 25,
    "cansend": 24,
    "cas": 90,
    "cb": 5,
    "cbr": 88,
    "cd": 142,
    "cen": 86,
    "cf": 40,
    "cg": 25,
    "chb": 89,
    "chla": 239,
    "chs": 148,
    "cia": 26,
    "cip": 27,
    "cis": 25,
    "cl": 38,
    "clerd": 56,
    "clin": 13,
    "clsn": 91,
    "cmdana": 26,
    "cmdpls": 27,
    "cmdser": 25,
    "cnod": 87,
    "cpri": 7,
    "cr": 8,
    "cs": 24,
    "csr": 109,
    "csrt": 92,
    "ctps": 93,
    "ctt": 112,
    "d": 14,
    "d0": 11,
    "d1": 10,
    "dc": 8,
    "ddt": 143,
    "decel": 8,
    "dfc": 14,
    "dhcp": 111,
    "di": 15,
    "digin": 14,
    "digout": 23,
    "din": 15,
    "dina": 15,
    "dinl": 16,
    "dmod": 161,
    "do": 23,
    "doa": 17,
    "dol": 18,
    "dout": 9,
    "dpa": 110,
    "dr": 34,
    "dreached": 34,
    "dres": 9,
    "ds": 9,
    "dset": 10,
    "dx": 21,
    "e": 24,
    "echof": 9,
    "edec": 233,
    "ee": 0,
    "ees": 27,
    "eesav": 27,
    "ehl": 76,
    "ehla": 78,
    "ehome": 79,
    "ell": 75,
    "ella": 77,
    "emod": 73,
    "eppr": 74,
    "estop": 14,
    "ex": 14,
    "f": 19,
    "fc": 71,
    "feedbk": 19,
    "ff": 21,
    "fid": 30,
    "fin": 63,
    "fltflag": 21,
    "flw": 123,
    "fm": 48,
    "fs": 20,
    "fsa": 204,
    "g": 0,
    "gid": 123,
    "giq": 122,
    "go": 0,
    "gwa": 105,
    "h": 13,
    "home": 13,
    "hpo": 165,
    "hs": 49,
    "hsat": 194,
    "hsense": 49,
    "hsm": 163,
    "icap": 50,
    "icl": 70,
    "illr": 154,
    "ilm": 155,
    "ipa": 104,
    "ipp": 107,
    "irr": 153,
    "ism": 233,
    "k": 33,
    "kdg": 242,
    "kif": 142,
    "kig": 241,
    "kpf": 141,
    "kpg": 240,
    "ld": 236,
    "lk": 29,
    "locked": 29,
    "lperr": 24,
    "lq": 237,
    "m": 1,
    "ma": 37,
    "mac": 51,
    "mdal": 202,
    "mdec": 52,
    "mdir": 119,
    "mems": 37,
    "mg": 15,
    "mgd": 41,
    "mgdet": 41,
    "mgm": 43,
    "mgmrkr": 43,
    "mgo": 15,
    "mgs": 44,
    "mgstatus": 44,
    "mgt": 42,
    "mgtrack": 42,
    "mgy": 45,
    "mgyro": 45,
    "mlx": 213,
    "mmod": 39,
    "mnod": 162,
    "mnrpm": 203,
    "motamps": 0,
    "motcmd": 1,
    "motflag": 48,
    "motpos": 2,
    "motpwr": 2,
    "motvel": 3,
    "mposrel": 17,
    "mpw": 151,
    "ms": 16,
    "mstop": 16,
    "mvel": 53,
    "mxmd": 5,
    "mxpf": 40,
    "mxpr": 41,
    "mxrpm": 54,
    "mxs": 150,
    "mxtrn": 55,
    "nxtacc": 20,
    "nxtdec": 21,
    "nxtpos": 18,
    "nxtposr": 19,
    "nxtvel": 23,
    "otl": 209,
    "ovh": 66,
    "ovl": 2,
    "p": 2,
    "pctr": 32,
    "pdb": 33,
    "pdns": 109,
    "pha": 73,
    "pi": 17,
    "pic": 36,
    "pina": 35,
    "plin": 34,
    "plsin": 17,
    "plsinc": 36,
    "pmax": 31,
    "pmaxa": 37,
    "pmin": 30,
    "pmina": 36,
    "pmod": 29,
    "pms": 244,
    "ppol": 38,
    "pr": 17,
    "prx": 19,
    "psa": 225,
    "px": 18,
    "r": 12,
    "rc": 26,
    "rcout": 26,
    "relcntr": 8,
    "relspeed": 7,
    "rfc": 152,
    "rs": 235,
    "rs485": 208,
    "rsbr": 10,
    "rwd": 8,
    "s": 3,
    "sblcntr": 5,
    "sbm": 106,
    "scc": 69,
    "scpr": 206,
    "sdns": 110,
    "sec": 141,
    "sed": 228,
    "sencntr": 4,
    "shl": 216,
    "shla": 218,
    "shome": 219,
    "skctr": 83,
    "skdb": 84,
    "sklin": 85,
    "skmax": 82,
    "skmin": 81,
    "skuse": 80,
    "sl": 72,
    "sll": 215,
    "slla": 217,
    "smod": 214,
    "sna": 121,
    "spektrum": 33,
    "spol": 65,
    "sr": 7,
    "sro": 94,
    "ss": 106,
    "ssf": 192,
    "ssr": 107,
    "stflag": 20,
    "sto": 207,
    "stt": 112,
    "svt": 190,
    "swd": 164,
    "sx": 23,
    "t": 18,
    "temp": 18,
    "tid": 143,
    "time": 28,
    "tm": 28,
    "tnm": 223,
    "tr": 32,
    "track": 32,
    "trn": 31,
    "uid": 50,
    "uvl": 3,
    "v": 13,
    "var": 6,
    "vk": 238,
    "volts": 13,
    "vph": 149,
    "wmod": 103,
    "zsma": 229,
    "zsmc": 70,
}

# A small number of aliases genuinely mean different things depending on
# which verb they're used with -- see the module docstring. Checked before
# COMMAND_CODES; (name, verb) not found here falls through to the flat
# table.
VERB_SPECIFIC_CODES: dict[tuple[str, str], int] = {
    ("brun", "setcommand"): 0x0C,
    ("brun", "setconfig"): 0x48,
    ("css", "setcommand"): 0x6C,
    ("css", "getvalue"): 0x6E,
}


class UnknownCommandError(Exception):
    def __init__(self, name: str):
        super().__init__(
            f"Unknown Roboteq command constant '_{name}'. See commands.py "
            f"({len(COMMAND_CODES)} known constants) for how to add more."
        )
        self.name = name


def resolve_command_code(name: str, verb: str | None = None) -> int:
    """name includes the leading underscore, e.g. '_ALIM'.

    verb, when given, is the lowercase device-I/O function name the
    constant is being passed to ('getvalue', 'setcommand', 'getconfig',
    'setconfig') -- only relevant for the handful of aliases in
    VERB_SPECIFIC_CODES that mean different things per verb.
    """
    key = name.lstrip("_").lower()
    if verb is not None and (key, verb) in VERB_SPECIFIC_CODES:
        return VERB_SPECIFIC_CODES[(key, verb)]
    if key not in COMMAND_CODES:
        raise UnknownCommandError(key)
    return COMMAND_CODES[key]
