import 'dart:async';

typedef _Debounceable<S, T> = Future<S?> Function(T parameter);

class Debouncer {
  Debouncer(this._duration, this._callback);

  final Duration _duration;
  final dynamic _callback;
  late final _Debounceable<dynamic, String> _debouncedSearch = _debounce<dynamic, String>(_callback);

  /// Returns a new function that is a debounced version of the given function.
  ///
  /// This means that the original function will be called only after no calls
  /// have been made for the given Duration.
  _Debounceable<S, T> _debounce<S, T>(_Debounceable<S?, T> function) {
    _DebounceTimer? debounceTimer;

    return (T parameter) async {
      if (debounceTimer != null && !debounceTimer!.isCompleted) {
        debounceTimer!.cancel();
      }
      debounceTimer = _DebounceTimer(_duration);
      try {
        await debounceTimer!.future;
      } on _CancelException {
        return null;
      }
      return function(parameter);
    };
  }

  process(data) {
    return _debouncedSearch(data);
  }
}

// A wrapper around Timer used for debouncing.
class _DebounceTimer {
  final Duration debounceDuration;

  _DebounceTimer(
    this.debounceDuration
  ) {
    _timer = Timer(debounceDuration, _onComplete);
  }

  late final Timer _timer;
  final Completer<void> _completer = Completer<void>();

  void _onComplete() {
    _completer.complete();
  }

  Future<void> get future => _completer.future;

  bool get isCompleted => _completer.isCompleted;

  void cancel() {
    _timer.cancel();
    _completer.completeError(const _CancelException());
  }
}

// An exception indicating that the timer was canceled.
class _CancelException implements Exception {
  const _CancelException();
}