"""
Core acquisition abstractions.

Every real device adapter (Tobii, BrainFlow board, Pupil Neon, webcam FEA, mic)
subclasses `Source` and implements `read()` to yield samples. The base class
handles the LSL plumbing identically for all of them, so adding a new measure
is "write one read() method" -- nothing else in the stack changes.
"""

from __future__ import annotations

import threading
import time
from dataclasses import dataclass, field
from typing import Iterator, Sequence

from pylsl import StreamInfo, StreamOutlet, local_clock


@dataclass
class StreamSpec:
    """Describes a single sensor stream. Mirrors what iMotions stores per device."""
    name: str                      # e.g. "EyeTracker", "ECG", "Markers"
    stype: str                     # LSL content-type: "Gaze", "ECG", "GSR", "Markers"...
    channels: Sequence[str]        # channel labels, e.g. ["gaze_x", "gaze_y", "pupil"]
    nominal_srate: float           # Hz; 0 for irregular/event streams
    channel_format: str = "float32"  # "float32" or "string" (markers)
    source_id: str = ""            # stable per-device id so re-connects re-bind

    def to_lsl(self) -> StreamInfo:
        info = StreamInfo(
            name=self.name,
            type=self.stype,
            channel_count=len(self.channels),
            nominal_srate=self.nominal_srate,
            channel_format=self.channel_format,
            source_id=self.source_id or self.name,
        )
        # Embed channel metadata in the stream's XML description so the recorder
        # and any downstream tool (incl. real XDF/LabRecorder) carries labels.
        chns = info.desc().append_child("channels")
        for label in self.channels:
            c = chns.append_child("channel")
            c.append_child_value("label", label)
        return info


class Source(threading.Thread):
    """
    Base class for all acquisition adapters.

    Subclass and implement `read()` as a generator yielding (sample, timestamp).
    `timestamp` may be None to stamp at push time on the LSL clock, or a float
    on the LSL `local_clock()` scale if the device gives you its own timing.
    """

    def __init__(self, spec: StreamSpec):
        super().__init__(daemon=True, name=f"src-{spec.name}")
        self.spec = spec
        self._outlet: StreamOutlet | None = None
        self._stop = threading.Event()

    # --- to be overridden by real adapters -------------------------------
    def read(self) -> Iterator[tuple]:
        raise NotImplementedError

    # --- lifecycle -------------------------------------------------------
    def run(self) -> None:
        self._outlet = StreamOutlet(self.spec.to_lsl())
        for sample, ts in self.read():
            if self._stop.is_set():
                break
            if ts is None:
                self._outlet.push_sample(sample)
            else:
                self._outlet.push_sample(sample, ts)

    def stop(self) -> None:
        self._stop.set()

    @property
    def stopping(self) -> bool:
        return self._stop.is_set()
