"""LED + GPIO EEPROM field accessors (advanced tier, moved into v1).

⚠ BIT-LAYOUT VERIFY: docs/01 give the byte OFFSETS for these regions but not the
datasheet's bit-level packing. The offset placement below is authoritative (from
docs/01 + the task brief); the *bit assignments within each byte* are a
best-effort mapping of the standard LAN7800 LED_CFG / GPIO_CFG registers and are
flagged VERIFY — confirm against datasheet DS00001992G §15 (LED_CFG, GPIO_*) or a
real dump before trusting for production. A raw get/set_region() escape hatch is
provided so an operator can always write exact bytes regardless of this mapping.

Regions (docs/01 + task brief):
  LED:  0x0B-0x0D (config 0/1/2)  + 0x58-0x59 (behavior/blink)
  GPIO: 0x07 wake-enable, 0x0E wake-polarity, 0x48-0x4F per-pin config
"""
from __future__ import annotations

from .eeprom import Eeprom, EepromError, DD_LEN

# ---- LED --------------------------------------------------------------------
LED_MODES = [
    "Link/Act", "Link1000/Act", "Link100/Act", "Link10/Act",
    "Link100+1000/Act", "Link10+1000/Act", "Link10+100/Act", "reserved",
    "Duplex/Collision", "Collision", "Activity", "reserved",
    "AutoNeg Fault", "Serial Mode", "Force Off", "Force On",
]
LED_COUNT = 4
LED_MODE_BASE = 0x0B          # LED i mode nibble: byte 0x0B+(i//2), nibble (i%2)*4  [VERIFY]
LED_ENABLE_OFFSET = 0x0D      # bit i = LED i enabled                                [VERIFY]
LED_BEHAVIOR_OFFSET = 0x58    # u16 LE; blink rate in bits[1:0]                       [VERIFY]
LED_BLINK_RATES = {0: "2.5Hz", 1: "5Hz", 2: "10Hz", 3: "20Hz"}

# ---- GPIO -------------------------------------------------------------------
GPIO_COUNT = 8
GPIO_WAKE_EN_OFFSET = 0x07    # bit i = GPIO i wake enable                            [VERIFY]
GPIO_WAKE_POL_OFFSET = 0x0E   # bit i = GPIO i wake polarity (1=active high)          [VERIFY]
GPIO_CFG_BASE = 0x48          # one byte per pin, 0x48..0x4F                          [VERIFY]
# per-pin config byte bits [VERIFY]:
GPIO_DIR_BIT = 0x01           # 1 = output, 0 = input
GPIO_OPEN_DRAIN_BIT = 0x02    # 1 = open-drain
GPIO_PUSH_PULL_BIT = 0x04     # 1 = push-pull
GPIO_OUT_VALUE_BIT = 0x08     # output level when dir=output


def _check_index(i, count, what):
    if not isinstance(i, int) or i < 0 or i >= count:
        raise EepromError(f"{what} index must be 0..{count - 1}, got {i}")


# Fixed LED/GPIO register byte ranges (inclusive). These are absolute EEPROM
# offsets; USB descriptor blocks are pointer-placed and *should* live elsewhere.
# If a descriptor block lands on one of these, a named LED/GPIO write would
# corrupt the descriptor (and vice-versa) — so we detect and block it.
FIXED_RANGES = [
    ("GPIO wake-enable", GPIO_WAKE_EN_OFFSET, GPIO_WAKE_EN_OFFSET),
    ("LED config", LED_MODE_BASE, LED_ENABLE_OFFSET),
    ("GPIO wake-polarity", GPIO_WAKE_POL_OFFSET, GPIO_WAKE_POL_OFFSET),
    ("GPIO config", GPIO_CFG_BASE, GPIO_CFG_BASE + GPIO_COUNT - 1),
    ("LED behavior", LED_BEHAVIOR_OFFSET, LED_BEHAVIOR_OFFSET + 1),
]


def _descriptor_at(eep: Eeprom, offset: int):
    """Return the descriptor kind whose block covers `offset`, else None."""
    for d in eep.descriptors():
        if d.present and d.byte_offset <= offset < d.byte_offset + DD_LEN:
            return d.kind
    return None


def _guard(eep: Eeprom, offset: int, field: str):
    kind = _descriptor_at(eep, offset)
    if kind:
        raise EepromError(
            f"{field} at 0x{offset:02X} lies inside the {kind.upper()} USB "
            f"descriptor block; a named write here would corrupt the "
            f"descriptor. This image has an overlapping layout. Use --set-raw "
            f"only if you truly intend to write that byte.")


def region_overlaps(eep: Eeprom) -> list[dict]:
    """List every collision between a fixed LED/GPIO range and a descriptor."""
    out = []
    for name, lo, hi in FIXED_RANGES:
        for off in range(lo, hi + 1):
            kind = _descriptor_at(eep, off)
            if kind:
                out.append({"register": name, "offset": off, "descriptor": kind})
    return out


# ===== raw region escape hatch (authoritative) ==============================
def get_region(eep: Eeprom, offset: int, length: int) -> bytes:
    if offset < 0 or offset + length > eep.size:
        raise EepromError(f"region 0x{offset:02X}+{length} out of range")
    return bytes(eep.data[offset:offset + length])


def set_region(eep: Eeprom, offset: int, values: bytes) -> None:
    if offset < 0 or offset + len(values) > eep.size:
        raise EepromError(f"region 0x{offset:02X}+{len(values)} out of range")
    eep.data[offset:offset + len(values)] = values


# ===== LED ===================================================================
def get_led(eep: Eeprom, i: int) -> dict:
    _check_index(i, LED_COUNT, "LED")
    byte = eep.data[LED_MODE_BASE + (i // 2)]
    mode = (byte >> ((i % 2) * 4)) & 0x0F
    enabled = bool(eep.data[LED_ENABLE_OFFSET] & (1 << i))
    return {"index": i, "mode": mode, "mode_name": LED_MODES[mode],
            "enabled": enabled}


def set_led_mode(eep: Eeprom, i: int, mode: int) -> None:
    _check_index(i, LED_COUNT, "LED")
    if not 0 <= mode <= 15:
        raise EepromError("LED mode must be 0..15")
    off = LED_MODE_BASE + (i // 2)
    _guard(eep, off, f"LED{i} mode")
    shift = (i % 2) * 4
    eep.data[off] = (eep.data[off] & ~(0x0F << shift)) | (mode << shift)


def set_led_enable(eep: Eeprom, i: int, on: bool) -> None:
    _check_index(i, LED_COUNT, "LED")
    _guard(eep, LED_ENABLE_OFFSET, f"LED{i} enable")
    bit = 1 << i
    if on:
        eep.data[LED_ENABLE_OFFSET] |= bit
    else:
        eep.data[LED_ENABLE_OFFSET] &= ~bit


def get_led_blink(eep: Eeprom) -> dict:
    val = eep.data[LED_BEHAVIOR_OFFSET] | (eep.data[LED_BEHAVIOR_OFFSET + 1] << 8)
    rate = val & 0x03
    return {"raw": val, "blink_rate": LED_BLINK_RATES.get(rate, "?"), "code": rate}


def set_led_blink(eep: Eeprom, rate_code: int) -> None:
    if rate_code not in LED_BLINK_RATES:
        raise EepromError(f"blink rate code must be one of {list(LED_BLINK_RATES)}")
    _guard(eep, LED_BEHAVIOR_OFFSET, "LED blink")
    val = eep.data[LED_BEHAVIOR_OFFSET] | (eep.data[LED_BEHAVIOR_OFFSET + 1] << 8)
    val = (val & ~0x03) | rate_code
    eep.data[LED_BEHAVIOR_OFFSET] = val & 0xFF
    eep.data[LED_BEHAVIOR_OFFSET + 1] = (val >> 8) & 0xFF


# ===== GPIO ==================================================================
def get_gpio(eep: Eeprom, i: int) -> dict:
    _check_index(i, GPIO_COUNT, "GPIO")
    cfg = eep.data[GPIO_CFG_BASE + i]
    return {
        "index": i,
        "direction": "out" if cfg & GPIO_DIR_BIT else "in",
        "drive": ("open-drain" if cfg & GPIO_OPEN_DRAIN_BIT
                  else "push-pull" if cfg & GPIO_PUSH_PULL_BIT else "default"),
        "output_value": 1 if cfg & GPIO_OUT_VALUE_BIT else 0,
        "wake_enabled": bool(eep.data[GPIO_WAKE_EN_OFFSET] & (1 << i)),
        "wake_active_high": bool(eep.data[GPIO_WAKE_POL_OFFSET] & (1 << i)),
    }


def set_gpio(eep: Eeprom, i: int, direction=None, drive=None, output_value=None,
             wake_enabled=None, wake_active_high=None) -> None:
    _check_index(i, GPIO_COUNT, "GPIO")
    off = GPIO_CFG_BASE + i
    if any(v is not None for v in (direction, drive, output_value)):
        _guard(eep, off, f"GPIO{i} config")
    if wake_enabled is not None:
        _guard(eep, GPIO_WAKE_EN_OFFSET, f"GPIO{i} wake-enable")
    if wake_active_high is not None:
        _guard(eep, GPIO_WAKE_POL_OFFSET, f"GPIO{i} wake-polarity")
    cfg = eep.data[off]
    if direction is not None:
        if direction not in ("in", "out"):
            raise EepromError("direction must be 'in' or 'out'")
        cfg = (cfg | GPIO_DIR_BIT) if direction == "out" else (cfg & ~GPIO_DIR_BIT)
    if drive is not None:
        cfg &= ~(GPIO_OPEN_DRAIN_BIT | GPIO_PUSH_PULL_BIT)
        if drive == "open-drain":
            cfg |= GPIO_OPEN_DRAIN_BIT
        elif drive == "push-pull":
            cfg |= GPIO_PUSH_PULL_BIT
        elif drive != "default":
            raise EepromError("drive must be open-drain/push-pull/default")
    if output_value is not None:
        cfg = (cfg | GPIO_OUT_VALUE_BIT) if int(output_value) else (cfg & ~GPIO_OUT_VALUE_BIT)
    eep.data[off] = cfg & 0xFF
    if wake_enabled is not None:
        bit = 1 << i
        eep.data[GPIO_WAKE_EN_OFFSET] = (eep.data[GPIO_WAKE_EN_OFFSET] | bit) \
            if wake_enabled else (eep.data[GPIO_WAKE_EN_OFFSET] & ~bit)
    if wake_active_high is not None:
        bit = 1 << i
        eep.data[GPIO_WAKE_POL_OFFSET] = (eep.data[GPIO_WAKE_POL_OFFSET] | bit) \
            if wake_active_high else (eep.data[GPIO_WAKE_POL_OFFSET] & ~bit)


def describe_regs(eep: Eeprom) -> dict:
    overlaps = region_overlaps(eep)
    return {
        "leds": [get_led(eep, i) for i in range(LED_COUNT)],
        "led_blink": get_led_blink(eep),
        "gpios": [get_gpio(eep, i) for i in range(GPIO_COUNT)],
        "overlaps": overlaps,
        "overlap_warning": (
            "LED/GPIO register offsets overlap a USB descriptor block in this "
            "image; named LED/GPIO edits are blocked on the colliding bytes to "
            "avoid corrupting the descriptor." if overlaps else None),
        "verify_note": "LED/GPIO bit layout is best-effort; confirm vs datasheet "
                       "sec 15 or a real dump. Offsets are authoritative.",
    }
