"""
NMEA 0183 parser — populates NavData from sentences.
Handles: HDG, HDT, HDM, RMC, VTG, ROT, XDR, MWV, GGA, ZDA
"""
from dataclasses import dataclass
from typing import Optional


def _checksum_ok(sentence: str) -> bool:
    if '*' not in sentence:
        return True
    try:
        data, cs = sentence.rsplit('*', 1)
        val = 0
        for c in data.lstrip('$!'):
            val ^= ord(c)
        return val == int(cs.strip()[:2], 16)
    except Exception:
        return False


def _f(s: str) -> Optional[float]:
    try:
        return float(s) if s.strip() else None
    except ValueError:
        return None


@dataclass
class NavData:
    # ── Heading ──────────────────────────────────────────────────────────────
    hdg_mag:   Optional[float] = None   # Magnetic heading °
    hdg_true:  Optional[float] = None   # True heading °
    variation: Optional[float] = None   # Mag variation (+ = East)
    deviation: Optional[float] = None

    # ── Motion ───────────────────────────────────────────────────────────────
    rot:       Optional[float] = None   # Rate of turn °/min (+ = stbd)

    # ── Attitude (BNO085) ─────────────────────────────────────────────────────
    pitch:     Optional[float] = None   # ° bow-up positive
    roll:      Optional[float] = None   # ° stbd positive
    heel:      Optional[float] = None   # alias for roll on some talkers

    # ── Acceleration (BNO085 via XDR) ────────────────────────────────────────
    accel_x:   Optional[float] = None   # m/s² — surge (fore-aft)
    accel_y:   Optional[float] = None   # m/s² — sway (port-stbd)
    accel_z:   Optional[float] = None   # m/s² — heave (up-down)

    # ── Angular velocity / gyro (BNO085 via XDR) ─────────────────────────────
    gyro_x:    Optional[float] = None   # °/s — roll rate
    gyro_y:    Optional[float] = None   # °/s — pitch rate
    gyro_z:    Optional[float] = None   # °/s — yaw rate

    # ── Quaternion (BNO085 via XDR or proprietary) ────────────────────────────
    quat_w:    Optional[float] = None
    quat_x:    Optional[float] = None
    quat_y:    Optional[float] = None
    quat_z:    Optional[float] = None

    @property
    def hdg_true_calc(self) -> Optional[float]:
        if self.hdg_mag is not None and self.variation is not None:
            return (self.hdg_mag + self.variation) % 360
        return self.hdg_true


def parse(sentence: str, data: NavData) -> bool:
    sentence = sentence.strip()
    if not (sentence.startswith('$') or sentence.startswith('!')):
        return False
    if not _checksum_ok(sentence):
        return False

    body = sentence[1:sentence.rfind('*')] if '*' in sentence else sentence[1:]
    p = body.split(',')
    if not p:
        return False

    stype = p[0][-3:].upper()
    try:
        return _PARSERS.get(stype, lambda *_: False)(p, data)
    except Exception:
        return False


# ── Individual sentence parsers ────────────────────────────────────────────

def _hdg(p, d):
    # $HCHDG,x.x,x.x,a,x.x,a
    d.hdg_mag = _f(p[1])
    dev = _f(p[2])
    if dev is not None:
        d.deviation = dev if (len(p) > 3 and p[3].upper() != 'W') else -dev
    var = _f(p[4]) if len(p) > 4 else None
    if var is not None:
        d.variation = var if (len(p) > 5 and p[5].upper() != 'W') else -var
    return True

def _hdt(p, d):
    if len(p) > 2 and p[2].upper() == 'T':
        d.hdg_true = _f(p[1])
    return True

def _hdm(p, d):
    if len(p) > 2 and p[2].upper() == 'M':
        d.hdg_mag = _f(p[1])
    return True

def _rmc(p, d):
    # Only used to extract magnetic variation as fallback
    if len(p) > 11 and p[10]:
        var = _f(p[10])
        if var is not None:
            d.variation = var if p[11].upper() != 'W' else -var
    return True

def _rot(p, d):
    if len(p) > 2 and p[2].upper() == 'A':
        d.rot = _f(p[1])
    return True

def _xdr(p, d):
    """
    XDR carries all BNO085 outputs.
    Expected names (configurable in ESP32 firmware):
      PITCH, ROLL, HEEL
      ACCELX, ACCELY, ACCELZ   (or SURGE, SWAY, HEAVE)
      GYROX,  GYROY,  GYROZ
      QUATW,  QUATX,  QUATY,  QUATZ
    """
    i = 1
    while i + 3 <= len(p) - 1:
        name  = p[i + 3].upper()
        value = _f(p[i + 1])
        if value is not None:
            if   'PITCH'  in name:              d.pitch   = value
            elif 'ROLL'   in name:              d.roll    = value
            elif 'HEEL'   in name:              d.heel    = value
            elif name in ('ACCELX', 'SURGE'):   d.accel_x = value
            elif name in ('ACCELY', 'SWAY'):    d.accel_y = value
            elif name in ('ACCELZ', 'HEAVE'):   d.accel_z = value
            elif name in ('GYROX',  'ROLLR'):   d.gyro_x  = value
            elif name in ('GYROY',  'PITCHR'):  d.gyro_y  = value
            elif name in ('GYROZ',  'YAWR'):    d.gyro_z  = value
            elif 'QUATW'  in name:              d.quat_w  = value
            elif 'QUATX'  in name:              d.quat_x  = value
            elif 'QUATY'  in name:              d.quat_y  = value
            elif 'QUATZ'  in name:              d.quat_z  = value
        i += 4
    return True


_PARSERS = {
    'HDG': _hdg,   # Heading + variation
    'HDT': _hdt,   # True heading
    'HDM': _hdm,   # Magnetic heading
    'ROT': _rot,   # Rate of turn
    'XDR': _xdr,   # Pitch / Roll transducer
    'RMC': _rmc,   # Variation fallback (only var field used)
}
