import 'dart:async';

enum RateLimitMode { debounce, throttle }

class ThrottleOptions {
  const ThrottleOptions({this.leading = true, this.trailing = true});

  final bool leading;
  final bool trailing;
}

class DebounceThrottle<T> {
  DebounceThrottle({
    required this.duration,
    required this.mode,
    this.onCallback,
    this.throttleOptions = const ThrottleOptions(),
  });

  final Duration duration;
  final RateLimitMode mode;
  final void Function(T value)? onCallback;
  final ThrottleOptions throttleOptions;

  Timer? _timer;
  T? _lastValue;
  DateTime? _lastExecuteTime;
  bool _waitingTrailing = false;

  void call(T value) {
    _lastValue = value;
    switch (mode) {
      case RateLimitMode.debounce:
        _debounce(value);
      case RateLimitMode.throttle:
        _throttle(value);
    }
  }

  void _debounce(T value) {
    _timer?.cancel();
    _timer = Timer(duration, () {
      onCallback?.call(value);
      _lastExecuteTime = DateTime.now();
    });
  }

  void _throttle(T value) {
    final now = DateTime.now();
    final last = _lastExecuteTime;
    final inWindow = last != null && now.difference(last) < duration;

    if (!inWindow && throttleOptions.leading) {
      onCallback?.call(value);
      _lastExecuteTime = now;
      _waitingTrailing = false;
      _timer?.cancel();
      return;
    }

    if (!throttleOptions.trailing || _waitingTrailing) return;

    _waitingTrailing = true;
    final remaining = last == null ? duration : duration - now.difference(last);
    _timer?.cancel();
    _timer = Timer(remaining, () {
      final value = _lastValue;
      if (value != null) onCallback?.call(value);
      _lastExecuteTime = DateTime.now();
      _waitingTrailing = false;
    });
  }

  void flush() {
    _timer?.cancel();
    final value = _lastValue;
    if (value != null) {
      onCallback?.call(value);
      _lastExecuteTime = DateTime.now();
    }
    _lastValue = null;
    _waitingTrailing = false;
  }

  void cancel() {
    _timer?.cancel();
    _lastValue = null;
    _waitingTrailing = false;
  }

  void dispose() => cancel();
}

class RateLimitHub {
  RateLimitHub({this.removeDebouncerOnDone = true});

  final bool removeDebouncerOnDone;
  final Map<Object, DebounceThrottle<dynamic>> _debouncers = {};
  final Map<Object, DebounceThrottle<dynamic>> _throttlers = {};

  void debounce<T>({
    required Object key,
    required T value,
    Duration duration = const Duration(milliseconds: 300),
    required void Function(T value) onCallback,
  }) {
    _debouncers[key]?.cancel();

    late DebounceThrottle<T> limiter;
    limiter = DebounceThrottle<T>(
      duration: duration,
      mode: RateLimitMode.debounce,
      onCallback: (value) {
        onCallback(value);
        if (removeDebouncerOnDone) _debouncers.remove(key);
      },
    );

    _debouncers[key] = limiter;
    limiter(value);
  }

  void throttle<T>({
    required Object key,
    required T value,
    Duration duration = const Duration(milliseconds: 300),
    ThrottleOptions options = const ThrottleOptions(),
    required void Function(T value) onCallback,
  }) {
    _throttlers.putIfAbsent(
      key,
      () =>
          DebounceThrottle<T>(
                duration: duration,
                mode: RateLimitMode.throttle,
                throttleOptions: options,
                onCallback: onCallback,
              )
              as DebounceThrottle<dynamic>,
    );

    (_throttlers[key] as DebounceThrottle<T>)(value);
  }

  void cancel(Object key) {
    _debouncers.remove(key)?.cancel();
    _throttlers.remove(key)?.cancel();
  }

  void clear() {
    for (final limiter in _debouncers.values) {
      limiter.dispose();
    }
    for (final limiter in _throttlers.values) {
      limiter.dispose();
    }
    _debouncers.clear();
    _throttlers.clear();
  }
}

class RateLimit {
  RateLimit._();

  static final instance = RateLimitHub();
}
