"""
BrainFlow ingestion — Tier 2 of the device matrix (see HARDWARE.md).

BrainFlow boards (OpenBCI Cyton/Ganglion, Muse, Unicorn, Mentalab, EmotiBit, ...)
do NOT emit LSL on their own — you pull from the board and push into the pipeline.
That bridge is this one adapter: pick a `board_id`, biosync publishes the board's
data as an LSL stream the recorder captures like any other.

Going live from the synthetic board is a one-line change:
    BrainFlowSource(board_id=BoardIds.SYNTHETIC_BOARD)        # no hardware
    BrainFlowSource(board_id=BoardIds.CYTON_BOARD,            # real OpenBCI
                    serial_port="/dev/ttyUSB0")
Everything downstream is identical — that's the invariant (HANDOFF.md §4).
"""

from __future__ import annotations

import time
from typing import Sequence

from brainflow.board_shim import BoardShim, BrainFlowInputParams, BoardIds

from ..core.source import Source, StreamSpec


class BrainFlowSource(Source):
    """
    One adapter for every BrainFlow-supported board.

    Parameters
    ----------
    board_id      : BoardIds (or int). e.g. BoardIds.SYNTHETIC_BOARD, CYTON_BOARD.
    name / stype  : LSL stream identity (default "BrainFlow"/"EEG").
    channel_kind  : which board rows to stream — "eeg" (default), "emg", "ecg",
                    "eda", "ppg", "accel", or "all" data channels.
    serial_port / mac_address / ip_address / other_info : passed to
                    BrainFlowInputParams for real boards (ignored by synthetic).
    poll_interval : seconds between buffer drains.
    """

    _KIND_GETTERS = {
        "eeg": "get_eeg_channels",
        "emg": "get_emg_channels",
        "ecg": "get_ecg_channels",
        "eda": "get_eda_channels",
        "ppg": "get_ppg_channels",
        "accel": "get_accel_channels",
    }

    def __init__(
        self,
        board_id=BoardIds.SYNTHETIC_BOARD,
        *,
        name: str = "BrainFlow",
        stype: str = "EEG",
        channel_kind: str = "eeg",
        channels: Sequence[int] | None = None,
        labels: Sequence[str] | None = None,
        serial_port: str = "",
        mac_address: str = "",
        ip_address: str = "",
        other_info: str = "",
        poll_interval: float = 0.1,
    ):
        self.board_id = int(board_id)
        self.poll_interval = poll_interval

        params = BrainFlowInputParams()
        params.serial_port = serial_port
        params.mac_address = mac_address
        params.ip_address = ip_address
        params.other_info = other_info
        self._params = params

        self.srate = BoardShim.get_sampling_rate(self.board_id)
        self._ts_chan = BoardShim.get_timestamp_channel(self.board_id)

        # Which rows of the board's data matrix become our channels.
        if channels is not None:
            self._data_chans = list(channels)
        elif channel_kind == "all":
            self._data_chans = list(BoardShim.get_eeg_channels(self.board_id))
        else:
            getter = self._KIND_GETTERS.get(channel_kind)
            if getter is None:
                raise ValueError(f"unknown channel_kind {channel_kind!r}")
            self._data_chans = list(getattr(BoardShim, getter)(self.board_id))

        chan_labels = (list(labels) if labels
                       else [f"{channel_kind}{i}" for i in range(len(self._data_chans))])

        super().__init__(StreamSpec(
            name=name, stype=stype, channels=chan_labels,
            nominal_srate=float(self.srate),
            source_id=f"brainflow-{self.board_id}-{channel_kind}",
        ))

    def read(self):
        BoardShim.disable_board_logger()
        board = BoardShim(self.board_id, self._params)
        board.prepare_session()
        board.start_stream()
        # Map BrainFlow's unix timestamps onto the LSL clock once.
        from pylsl import local_clock
        clock_offset = local_clock() - time.time()
        try:
            while not self.stopping:
                time.sleep(self.poll_interval)
                data = board.get_board_data()        # [rows x n_samples], drains buffer
                if data.size == 0:
                    continue
                n = data.shape[1]
                for j in range(n):
                    sample = [float(data[c, j]) for c in self._data_chans]
                    ts_unix = float(data[self._ts_chan, j]) if self._ts_chan >= 0 else None
                    ts = (ts_unix + clock_offset) if ts_unix else None
                    yield sample, ts
        finally:
            try:
                board.stop_stream()
            finally:
                board.release_session()
