# SPDX-License-Identifier: Apache-2.0
"""The Radxa ZERO 3E under Radxa's Debian, through python-periphery.

Why periphery: one pure-Python package covers GPIO (character device, with edges and pull bias), spidev and
i2c-dev, with no C extension to build. `pip install python-periphery` (Debian also packages it as
python3-periphery). It is imported only when a Linux board is opened, so tests never need it.

Nothing here has run on the board yet: the hardware isn't built. Every pin, line and device path is marked
VERIFY. Check each against Radxa's ZERO 3E pinout page and `gpioinfo` before first use.
"""
from __future__ import annotations

import time

from .hw import Board, Edge

# header pin → (gpiochip, line). Radxa names pins GPIO<bank>_<letter><n>: chip = bank, line = letter × 8 + n.
PINS = {
    "eye": ("/dev/gpiochip3", 9),        # pin 16 = GPIO3_B1 (VERIFY)
    "motor": ("/dev/gpiochip3", 10),     # pin 18 = GPIO3_B2 (VERIFY)
    # B-44, proposed and not wired in v01: free header pins that aren't claimed by SPI3, I2C3 or I2S3
    "slide": ("/dev/gpiochip3", 17),     # pin 22 = GPIO3_C1 (VERIFY, proposal)
    "keep_alive": ("/dev/gpiochip3", 8), # pin 15 = GPIO3_B0 (VERIFY, proposal)
}
SPI_DEV = "/dev/spidev3.0"   # pin 19 = SPI3_MOSI_M1; needs Radxa's spi3-m1 overlay (VERIFY)
I2C_DEV = "/dev/i2c-3"       # pins 3/5 = I2C3_M0; needs the i2c3-m0 overlay (VERIFY)
SPIDEV_BUFSIZ = 4096         # spidev's default bufsiz module parameter: one ring frame must fit in one transfer


class LinuxClock:
    def now(self) -> float:
        return time.monotonic()

    def wall(self) -> float:
        return time.time()

    def sleep(self, s: float) -> None:
        time.sleep(max(0.0, s))


class LinuxInput:
    """A GPIO line with both edges and the internal pull-up (the eye switch shorts it to GND)."""

    def __init__(self, chip: str, line: int, bias: str = "pull_up"):
        from periphery import GPIO
        self.gpio = GPIO(chip, line, "in", edge="both", bias=bias)

    def read(self) -> bool:
        return bool(self.gpio.read())

    def edges(self) -> list[Edge]:
        out = []
        while self.gpio.poll(0):
            ev = self.gpio.read_event()
            # Since kernel 5.7 the character device stamps events on CLOCK_MONOTONIC, the clock LinuxClock reads.
            # Older kernels used CLOCK_REALTIME: fall back to "now" if the stamp is plainly on another clock (VERIFY).
            t = ev.timestamp / 1e9
            if abs(t - time.monotonic()) > 5.0:
                t = time.monotonic()
            out.append(Edge(t, ev.edge == "rising"))
        return out


class LinuxOutput:
    def __init__(self, chip: str, line: int):
        from periphery import GPIO
        self.gpio = GPIO(chip, line, "out")

    def write(self, on: bool) -> None:
        self.gpio.write(bool(on))


class LinuxSpi:
    def __init__(self, dev: str = SPI_DEV, hz: int = 6_400_000):
        from periphery import SPI
        self.spi = SPI(dev, 0, hz)

    def write(self, data: bytes) -> None:
        # One transfer only: between transfers MOSI may idle high, which a NeoPixel reads as data (VERIFY on a scope).
        if len(data) > SPIDEV_BUFSIZ:
            raise ValueError(f"{len(data)} bytes > spidev bufsiz {SPIDEV_BUFSIZ}: raise spidev.bufsiz")
        self.spi.transfer(bytes(data))


class LinuxI2c:
    def __init__(self, dev: str = I2C_DEV):
        from periphery import I2C
        self._I2C = I2C
        self.bus = I2C(dev)

    def transfer(self, addr: int, write: bytes, read: int = 0) -> bytes:
        msgs = [self._I2C.Message(bytes(write))]
        if read:
            msgs.append(self._I2C.Message(bytearray(read), read=True))
        self.bus.transfer(addr, msgs)
        return bytes(msgs[-1].data) if read else b""


def linux_board(pins: dict = PINS, b44: bool = False) -> Board:
    """Open the board. `b44=True` also opens the power-slide sense and keep-alive lines, once they are wired."""
    b = Board(clock=LinuxClock(), eye=LinuxInput(*pins["eye"]), motor=LinuxOutput(*pins["motor"]),
              spi=LinuxSpi(), i2c=LinuxI2c())
    b.motor.write(False)
    if b44:
        b.slide = LinuxInput(*pins["slide"])
        b.keep_alive = LinuxOutput(*pins["keep_alive"])
    return b
