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

class TiltDetector {
  static final TiltDetector _instance = TiltDetector._internal();
  factory TiltDetector() => _instance;
  TiltDetector._internal();

  StreamSubscription<AccelerometerEvent>? _accelerometerSubscription;
  Function(double)? onTiltChanged;
  Function()? onShakeDetected;
  
  bool _isListening = false;
  double _currentTilt = 0.0;
  
  // Filtering parameters to smooth out noise
  static const double _alpha = 0.8; // Low-pass filter constant
  double _filteredX = 0.0;
  
  // Shake detection parameters
  static const double _shakeThreshold = 18.0; // Acceleration threshold for shake
  static const int _shakeTimeWindow = 500; // ms window for shake detection
  static const int _shakeDebounceTime = 1000; // ms debounce between shake events
  final List<double> _accelerationHistory = [];
  final List<int> _accelerationTimestamps = [];
  int _lastShakeTime = 0;
  
  bool get isListening => _isListening;
  double get currentTilt => _currentTilt;

  /// Start listening to accelerometer data
  /// [onTiltChanged] callback receives tilt angle in degrees
  /// [onShakeDetected] callback is called when shake is detected
  /// Positive values = device tilted right, negative = tilted left
  void startListening({
    Function(double)? onTiltChanged,
    Function()? onShakeDetected,
  }) {
    if (_isListening) return;
    
    this.onTiltChanged = onTiltChanged;
    this.onShakeDetected = onShakeDetected;
    _isListening = true;
    
    _accelerometerSubscription = accelerometerEventStream(
      samplingPeriod: const Duration(milliseconds: 100), // Increased frequency for shake detection
    ).listen(_onAccelerometerEvent);
  }
  
  /// Stop listening to accelerometer data
  void stopListening() {
    if (!_isListening) return;
    
    _accelerometerSubscription?.cancel();
    _accelerometerSubscription = null;
    _isListening = false;
    onTiltChanged = null;
    onShakeDetected = null;
    _currentTilt = 0.0;
    _filteredX = 0.0;
    _accelerationHistory.clear();
    _accelerationTimestamps.clear();
  }
  
  void _onAccelerometerEvent(AccelerometerEvent event) {
    final currentTime = DateTime.now().millisecondsSinceEpoch;
    
    // Calculate total acceleration magnitude for shake detection
    final acceleration = math.sqrt(
      event.x * event.x + event.y * event.y + event.z * event.z
    );
    
    // Add to history for shake detection
    _accelerationHistory.add(acceleration);
    _accelerationTimestamps.add(currentTime);
    
    // Remove old entries outside the time window
    while (_accelerationTimestamps.isNotEmpty &&
           currentTime - _accelerationTimestamps.first > _shakeTimeWindow) {
      _accelerationHistory.removeAt(0);
      _accelerationTimestamps.removeAt(0);
    }
    
    // Check for shake pattern
    _checkForShake();
    
    // Apply low-pass filter to smooth out noise for tilt detection
    _filteredX = _alpha * _filteredX + (1 - _alpha) * event.x;
    
    // Calculate tilt angle in degrees
    // The accelerometer X axis represents left-right tilt
    // Clamp to reasonable tilt range (-45 to +45 degrees)
    final tiltRadians = math.atan2(_filteredX, 9.8);
    _currentTilt = (tiltRadians * 180 / math.pi).clamp(-45.0, 45.0);
    
    // Only trigger callback if tilt is significant (> 5 degrees)
    if (_currentTilt.abs() > 5.0) {
      onTiltChanged?.call(_currentTilt);
    }
  }
  
  void _checkForShake() {
    if (_accelerationHistory.length < 3) return;
    
    // Look for rapid changes in acceleration that indicate shaking
    double maxAcceleration = _accelerationHistory.reduce(math.max);
    double minAcceleration = _accelerationHistory.reduce(math.min);
    
    // Check if we have significant acceleration variation (shake pattern)
    if (maxAcceleration - minAcceleration > _shakeThreshold) {
      // Check for multiple peaks to confirm shake
      int peaks = 0;
      for (int i = 1; i < _accelerationHistory.length - 1; i++) {
        if (_accelerationHistory[i] > _accelerationHistory[i - 1] &&
            _accelerationHistory[i] > _accelerationHistory[i + 1] &&
            _accelerationHistory[i] > 12.0) { // Peak threshold
          peaks++;
        }
      }
      
      // If we have at least 2 peaks, it's likely a shake
      if (peaks >= 2) {
        _handleShakeDetected();
      }
    }
  }
  
  /// Get normalized tilt strength from -1.0 to 1.0
  /// -1.0 = maximum left tilt, +1.0 = maximum right tilt
  double get normalizedTilt {
    return (_currentTilt / 45.0).clamp(-1.0, 1.0);
  }
  
  /// Check if device is significantly tilted
  bool get isTilted => _currentTilt.abs() > 5.0;
  
  /// Check if device is tilted left
  bool get isTiltedLeft => _currentTilt < -5.0;
  
  /// Check if device is tilted right
  bool get isTiltedRight => _currentTilt > 5.0;
  
  /// Handle shake detection with debouncing
  void _handleShakeDetected() {
    final currentTime = DateTime.now().millisecondsSinceEpoch;
    
    // Only trigger shake if enough time has passed since last shake
    if (currentTime - _lastShakeTime >= _shakeDebounceTime) {
      _lastShakeTime = currentTime;
      onShakeDetected?.call();
      
      // Clear history to prevent multiple rapid shake detections
      _accelerationHistory.clear();
      _accelerationTimestamps.clear();
    }
  }
  
  /// Get time since last shake in milliseconds
  int get timeSinceLastShake {
    return DateTime.now().millisecondsSinceEpoch - _lastShakeTime;
  }
  
  /// Check if shake is currently debounced
  bool get isShakeDebounced {
    return timeSinceLastShake < _shakeDebounceTime;
  }
}
