"""Real-time gaze filters: the existing fixed median+EMA baseline, and two
ML-driven variants (hard state-switch, continuous confidence-weighted) built
on the classifier trained by train_filter_model.py.
"""

from __future__ import annotations

import json
import statistics
from collections import deque
from pathlib import Path

import joblib

from gaze_features import FEATURE_COLUMNS, OnlineFeatureTracker

FIXATION_ALPHA = 0.15
PURSUIT_ALPHA = 0.35
SACCADE_ALPHA = 0.9
SOFT_MIN_ALPHA = 0.15
SOFT_MAX_ALPHA = 0.9
MEDIAN_WINDOW = 5


class BaselineFilter:
    """Fixed median + EMA + dead-zone filter, matching visualize_surface_gaze.py."""

    def __init__(self, smoothing: float = 0.18, median_window: int = 5, dead_zone: float = 3.0) -> None:
        self.smoothing = smoothing
        self.dead_zone = dead_zone
        self.x_history: deque[float] = deque(maxlen=median_window)
        self.y_history: deque[float] = deque(maxlen=median_window)
        self.filtered: tuple[float, float] | None = None

    def reset(self) -> None:
        self.x_history.clear()
        self.y_history.clear()
        self.filtered = None

    def update(self, timestamp: float, x: float, y: float, confidence: float) -> tuple[float, float]:
        self.x_history.append(x)
        self.y_history.append(y)
        median_x = statistics.median(self.x_history)
        median_y = statistics.median(self.y_history)
        if self.filtered is None:
            self.filtered = (median_x, median_y)
        else:
            prev_x, prev_y = self.filtered
            fx = prev_x + self.smoothing * (median_x - prev_x)
            fy = prev_y + self.smoothing * (median_y - prev_y)
            if abs(fx - prev_x) < self.dead_zone:
                fx = prev_x
            if abs(fy - prev_y) < self.dead_zone:
                fy = prev_y
            self.filtered = (fx, fy)
        return self.filtered


class MLAdaptiveFilter:
    """Classifier-driven filter. mode="hard" switches behavior by predicted class;
    mode="soft" blends smoothing strength continuously with P(saccade)."""

    def __init__(
        self,
        model_path: str | Path,
        cm_per_px: float,
        viewing_distance_cm: float,
        mode: str = "soft",
    ) -> None:
        if mode not in ("hard", "soft"):
            raise ValueError("mode must be 'hard' or 'soft'")
        self.mode = mode
        self.model = joblib.load(model_path)
        meta_path = Path(model_path).with_name(Path(model_path).stem + "_meta.json")
        self.meta = json.loads(meta_path.read_text(encoding="utf-8")) if meta_path.exists() else {}
        self.saccade_index = list(self.model.classes_).index("saccade")

        self.tracker = OnlineFeatureTracker(cm_per_px, viewing_distance_cm)
        self.x_history: deque[float] = deque(maxlen=MEDIAN_WINDOW)
        self.y_history: deque[float] = deque(maxlen=MEDIAN_WINDOW)
        self.filtered: tuple[float, float] | None = None
        self.last_label: str | None = None
        self.last_prob_saccade: float | None = None

    def reset(self) -> None:
        self.tracker.reset()
        self.x_history.clear()
        self.y_history.clear()
        self.filtered = None
        self.last_label = None
        self.last_prob_saccade = None

    def update(self, timestamp: float, x: float, y: float, confidence: float) -> tuple[float, float]:
        features = self.tracker.update(timestamp, x, y, confidence)
        if features is None:
            # Not enough history yet for a prediction; pass raw through as median-of-one.
            self.x_history.append(x)
            self.y_history.append(y)
            self.filtered = (x, y)
            return self.filtered

        row = [[features[c] for c in FEATURE_COLUMNS]]
        proba = self.model.predict_proba(row)[0]
        prob_saccade = float(proba[self.saccade_index])
        label = self.model.classes_[proba.argmax()]
        self.last_label = label
        self.last_prob_saccade = prob_saccade

        if self.mode == "hard":
            if label == "saccade":
                self.x_history.clear()
                self.y_history.clear()
                self.x_history.append(x)
                self.y_history.append(y)
                self.filtered = (x, y)
                return self.filtered
            alpha = FIXATION_ALPHA if label == "fixation" else PURSUIT_ALPHA
        else:
            alpha = SOFT_MIN_ALPHA + prob_saccade * (SOFT_MAX_ALPHA - SOFT_MIN_ALPHA)

        self.x_history.append(x)
        self.y_history.append(y)
        median_x = statistics.median(self.x_history)
        median_y = statistics.median(self.y_history)
        if self.filtered is None:
            self.filtered = (median_x, median_y)
        else:
            prev_x, prev_y = self.filtered
            fx = prev_x + alpha * (median_x - prev_x)
            fy = prev_y + alpha * (median_y - prev_y)
            self.filtered = (fx, fy)
        return self.filtered
