"""
Synthetic sources -- stand-ins for real hardware so the whole pipeline runs
with no devices attached. Each one is exactly the shape a real adapter takes:
same StreamSpec, same read() generator. To go live you swap the body of read()
for device calls (BrainFlow.get_board_data(), tobii subscribe, cv2 frame -> py-feat)
and delete nothing else.

They deliberately run at DIFFERENT sample rates (60 / 32 / 250 Hz + irregular
events) to prove the sync core aligns heterogeneous streams -- the one thing
that actually matters.
"""

from __future__ import annotations

import math
import time

import neurokit2 as nk
import numpy as np

from ..core.source import Source, StreamSpec


class SyntheticGaze(Source):
    """60 Hz screen gaze: x, y in normalized coords + pupil diameter (mm)."""

    def __init__(self):
        super().__init__(StreamSpec(
            name="EyeTracker", stype="Gaze",
            channels=["gaze_x", "gaze_y", "pupil_mm"],
            nominal_srate=60.0, source_id="synthetic-eye-0",
        ))

    def read(self):
        srate = self.spec.nominal_srate
        t0 = time.time()
        n = 0
        while not self.stopping:
            t = time.time() - t0
            # slow smooth scan path + a little jitter, pupil dilates over time
            x = 0.5 + 0.3 * math.sin(t * 0.7) + np.random.normal(0, 0.01)
            y = 0.5 + 0.2 * math.cos(t * 0.5) + np.random.normal(0, 0.01)
            pupil = 3.5 + 0.4 * math.sin(t * 0.2) + np.random.normal(0, 0.02)
            yield [float(x), float(y), float(pupil)], None
            n += 1
            # pace to the nominal rate
            time.sleep(max(0.0, (n / srate) - (time.time() - t0)))


class SyntheticEDA(Source):
    """32 Hz electrodermal activity (microsiemens): tonic drift + phasic SCRs."""

    def __init__(self):
        super().__init__(StreamSpec(
            name="GSR", stype="GSR",
            channels=["eda_uS"],
            nominal_srate=32.0, source_id="synthetic-eda-0",
        ))

    def read(self):
        srate = self.spec.nominal_srate
        t0 = time.time()
        n = 0
        tonic = 2.0
        while not self.stopping:
            t = time.time() - t0
            tonic += np.random.normal(0, 0.0006)          # slow drift
            phasic = 0.0
            # occasional skin-conductance responses
            if np.random.rand() < 0.004:
                phasic = np.random.uniform(0.3, 0.9)
            val = tonic + phasic + np.random.normal(0, 0.01)
            yield [float(val)], None
            n += 1
            time.sleep(max(0.0, (n / srate) - (time.time() - t0)))


class SyntheticECG(Source):
    """250 Hz single-lead ECG, generated by NeuroKit2 (realistic morphology)."""

    def __init__(self, hr=72):
        super().__init__(StreamSpec(
            name="ECG", stype="ECG",
            channels=["ecg_mV"],
            nominal_srate=250.0, source_id="synthetic-ecg-0",
        ))
        self.hr = hr

    def read(self):
        srate = int(self.spec.nominal_srate)
        # precompute a 60 s buffer and loop it
        buf = nk.ecg_simulate(duration=60, sampling_rate=srate,
                              heart_rate=self.hr, method="ecgsyn")
        t0 = time.time()
        n = 0
        while not self.stopping:
            yield [float(buf[n % len(buf)])], None
            n += 1
            time.sleep(max(0.0, (n / srate) - (time.time() - t0)))


class SyntheticEEG(Source):
    """Multichannel EEG (default 8 ch @ 250 Hz): alpha rhythm + pink-ish noise.

    Stand-in for a BrainFlow board or an actiCHamp/Enobio LSL outlet, so the
    MNE analysis path (filter -> epoch -> ERP) can run with no hardware.
    """

    def __init__(self, n_channels=8, srate=250.0, alpha_hz=10.0):
        super().__init__(StreamSpec(
            name="EEG", stype="EEG",
            channels=[f"ch{i+1}" for i in range(n_channels)],
            nominal_srate=srate, source_id="synthetic-eeg-0",
        ))
        self.n = n_channels
        self.alpha_hz = alpha_hz

    def read(self):
        srate = self.spec.nominal_srate
        t0 = time.time()
        k = 0
        while not self.stopping:
            t = time.time() - t0
            alpha = 20.0 * math.sin(2 * math.pi * self.alpha_hz * t)  # microvolts
            sample = [float(alpha + np.random.normal(0, 8.0)) for _ in range(self.n)]
            yield sample, None
            k += 1
            time.sleep(max(0.0, (k / srate) - (time.time() - t0)))


class StimulusMarkers(Source):
    """
    Irregular event stream -- the PsychoPy/Study-Builder equivalent.
    Pushes stim_on / stim_off pairs at random intervals. These markers are how
    every analysis becomes event-locked (the basis for AOI dwell, ERP epochs,
    stimulus-by-stimulus aggregation).
    """

    def __init__(self, stimuli=("face_A", "face_B", "scene_1", "ad_clip")):
        super().__init__(StreamSpec(
            name="Markers", stype="Markers",
            channels=["marker"],
            nominal_srate=0.0, channel_format="string",
            source_id="synthetic-markers-0",
        ))
        self.stimuli = stimuli

    def read(self):
        i = 0
        while not self.stopping:
            time.sleep(np.random.uniform(1.5, 3.0))
            stim = self.stimuli[i % len(self.stimuli)]
            yield [f"stim_on:{stim}"], None
            dur = np.random.uniform(1.0, 2.0)
            time.sleep(dur)
            yield [f"stim_off:{stim}"], None
            i += 1
