import 'dart:async';
import 'dart:math' as math;
import 'package:sensors_plus/sensors_plus.dart';

/// Statut de parallélisme retourné en temps réel.
enum ParallelismStatus {
  /// Pas encore de données capteur disponibles.
  unknown,

  /// L'appareil est bien parallèle à la cible.
  aligned,

  /// L'appareil est trop incliné.
  misaligned,
}

/// Données de parallélisme calculées à chaque frame capteur.
class ParallelismData {
  final ParallelismStatus status;

  /// Inclinaison avant/arrière en degrés (0° = parfaitement vertical).
  final double pitchDegrees;

  /// Inclinaison gauche/droite en degrés (0° = parfaitement droit).
  final double rollDegrees;

  const ParallelismData({
    required this.status,
    required this.pitchDegrees,
    required this.rollDegrees,
  });

  bool get isAligned => status == ParallelismStatus.aligned;

  @override
  String toString() =>
      'ParallelismData(status: $status, pitch: ${pitchDegrees.toStringAsFixed(1)}°, roll: ${rollDegrees.toStringAsFixed(1)}°)';
}

/// Service de détection du parallélisme par accéléromètre.
///
/// Implémente une hystérésis à deux seuils pour éviter le clignotement :
///
///   État actuel = misaligned → passe à aligned si angle < [alignThreshold]
///   État actuel = aligned    → passe à misaligned si angle > [misalignThreshold]
///
/// Cela crée une "zone de confort" entre les deux seuils où le statut
/// ne change pas — le vert reste vert même si la main tremble légèrement.
///
/// Valeurs par défaut recommandées :
///   alignThreshold    = 15°  (seuil d'entrée dans le vert — assez souple)
///   misalignThreshold = 22°  (seuil de sortie du vert — tolérant au tremblement)
class ParallelismService {
  /// Angle max pour passer de misaligned → aligned.
  final double alignThreshold;

  /// Angle à dépasser pour passer de aligned → misaligned.
  /// Doit être > alignThreshold pour créer la zone d'hystérésis.
  final double misalignThreshold;

  StreamSubscription<AccelerometerEvent>? _subscription;
  final StreamController<ParallelismData> _controller =
  StreamController<ParallelismData>.broadcast();

  /// État interne mémorisé entre deux frames (cœur de l'hystérésis).
  ParallelismStatus _currentStatus = ParallelismStatus.unknown;

  ParallelismService({
    this.alignThreshold = 25.0,
    this.misalignThreshold = 32.0,
  }) : assert(
  misalignThreshold > alignThreshold,
  'misalignThreshold doit être supérieur à alignThreshold',
  );

  Stream<ParallelismData> get stream => _controller.stream;

  void start() {
    if (_subscription != null) return;

    _subscription = accelerometerEventStream(
      samplingPeriod: SensorInterval.normalInterval, // ~50 ms
    ).listen(
      _onAccelerometerEvent,
      onError: (_) {
        // Simulateur ou capteur absent — on reste en "unknown" sans bloquer l'UI
        if (!_controller.isClosed) {
          _currentStatus = ParallelismStatus.unknown;
          _controller.add(const ParallelismData(
            status: ParallelismStatus.unknown,
            pitchDegrees: 0,
            rollDegrees: 0,
          ));
        }
      },
    );
  }

  void stop() {
    _subscription?.cancel();
    _subscription = null;
    _currentStatus = ParallelismStatus.unknown;
  }

  void dispose() {
    stop();
    _controller.close();
  }

  void _onAccelerometerEvent(AccelerometerEvent event) {
    if (_controller.isClosed) return;

    final double gx = event.x;
    final double gy = event.y;
    final double gz = event.z;

    final double magnitude = math.sqrt(gx * gx + gy * gy + gz * gz);
    if (magnitude < 1.0) return; // Données aberrantes

    // Normalisation par la magnitude réelle (indépendant de g exact)
    final double nx = gx / magnitude;
    final double ny = gy / magnitude;
    final double nz = gz / magnitude;

    // Pitch et Roll mesurent l'inclinaison autour de chaque axe.
    final double pitchDeg = math.asin(nz.clamp(-1.0, 1.0)) * (180.0 / math.pi);
    final double rollDeg = math.asin(nx.clamp(-1.0, 1.0)) * (180.0 / math.pi);

    // Le critère de couleur = le PIRE des deux angles affichés à l'écran.
    // Ainsi ce que voit l'utilisateur (Pitch / Roll) correspond exactement
    // à la décision vert/orange : à 2° d'écart, on est largement dans le vert.
    final double worstAngle = math.max(pitchDeg.abs(), rollDeg.abs());

    // ── Hystérésis ──────────────────────────────────────────────────────────
    // Premier appel : on décide selon alignThreshold uniquement
    if (_currentStatus == ParallelismStatus.unknown) {
      _currentStatus = worstAngle <= alignThreshold
          ? ParallelismStatus.aligned
          : ParallelismStatus.misaligned;
    }
    // Déjà misaligned → devient aligned seulement si on passe sous alignThreshold
    else if (_currentStatus == ParallelismStatus.misaligned) {
      if (worstAngle <= alignThreshold) {
        _currentStatus = ParallelismStatus.aligned;
      }
    }
    // Déjà aligned → devient misaligned seulement si on dépasse misalignThreshold
    else if (_currentStatus == ParallelismStatus.aligned) {
      if (worstAngle > misalignThreshold) {
        _currentStatus = ParallelismStatus.misaligned;
      }
      // Entre alignThreshold et misalignThreshold → on garde le vert, on ne change rien
    }
    // ────────────────────────────────────────────────────────────────────────

    _controller.add(ParallelismData(
      status: _currentStatus,
      pitchDegrees: pitchDeg,
      rollDegrees: rollDeg,
    ));
  }
}