// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';

import 'aggregate_sample.dart';
import 'common_callbacks.dart';
import 'from_handlers.dart';

/// Utilities to rate limit events.
///
/// - [debounce] - emit the the _first_ or _last_ event of a series of closely
///   spaced events.
/// - [debounceBuffer] - emit _all_ events at the _end_ of a series of closely
///   spaced events.
/// - [throttle] - emit the _first_ event at the _beginning_ of the period.
/// - [audit] - emit the _last_ event at the _end_ of the period.
/// - [buffer] - emit _all_ events on a _trigger_.
extension RateLimit<T> on Stream<T> {
  /// Suppresses events with less inter-event spacing than [duration].
  ///
  /// Events which are emitted with less than [duration] elapsed between them
  /// are considered to be part of the same "series". If [leading] is `true`,
  /// the first event of this series is emitted immediately. If [trailing] is
  /// `true` the last event of this series is emitted with a delay of at least
  /// [duration]. By default only trailing events are emitted, both arguments
  /// must be specified with `leading: true, trailing: false` to emit only
  /// leading events.
  ///
  /// If this stream is a broadcast stream, the result will be as well.
  /// Errors are forwarded immediately.
  ///
  /// If there is a trailing event waiting during the debounce period when the
  /// source stream closes the returned stream will wait to emit it following
  /// the debounce period before closing. If there is no pending debounced event
  /// when this stream closes the returned stream will close immediately.
  ///
  /// For example:
  ///
  ///     source.debounce(Duration(seconds: 1));
  ///
  ///     source: 1-2-3---4---5-6-|
  ///     result: ------3---4-----6|
  ///
  ///     source.debounce(Duration(seconds: 1), leading: true, trailing: false);
  ///
  ///     source: 1-2-3---4---5-6-|
  ///     result: 1-------4---5---|
  ///
  ///     source.debounce(Duration(seconds: 1), leading: true);
  ///
  ///     source: 1-2-3---4---5-6-|
  ///     result: 1-----3-4---5---6|
  ///
  /// To collect values emitted during the debounce period see [debounceBuffer].
  Stream<T> debounce(Duration duration,
          {bool leading = false, bool trailing = true}) =>
      _debounceAggregate(duration, _dropPrevious,
          leading: leading, trailing: trailing);

  /// Buffers values until this stream does not emit for [duration] then emits
  /// the collected values.
  ///
  /// Values will always be delayed by at least [duration], and values which
  /// come within this time will be aggregated into the same list.
  ///
  /// If this stream is a broadcast stream, the result will be as well.
  /// Errors are forwarded immediately.
  ///
  /// If there are events waiting during the debounce period when this stream
  /// closes the returned stream will wait to emit them following the debounce
  /// period before closing. If there are no pending debounced events when this
  /// stream closes the returned stream will close immediately.
  ///
  /// To keep only the most recent event during the debounce period see
  /// [debounce].
  Stream<List<T>> debounceBuffer(Duration duration) =>
      _debounceAggregate(duration, _collect, leading: false, trailing: true);

  /// Reduces the rate that events are emitted to at most once per [duration].
  ///
  /// No events will ever be emitted within [duration] of another event on the
  /// result stream.
  /// If this stream is a broadcast stream, the result will be as well.
  /// Errors are forwarded immediately.
  ///
  /// If [trailing] is `false`, source events emitted during the [duration]
  /// period following a result event are discarded.
  /// The result stream will not emit an event until this stream emits an event
  /// following the throttled period.
  /// If this stream is consistently emitting events with less than
  /// [duration] between events, the time between events on the result stream
  /// may still be more than [duration].
  /// The result stream will close immediately when this stream closes.
  ///
  /// If [trailing] is `true`, the latest source event emitted during the
  /// [duration] period following an result event is held and emitted following
  /// the period.
  /// If this stream is consistently emitting events with less than [duration]
  /// between events, the time between events on the result stream will be
  /// [duration].
  /// If this stream closes the result stream will wait to emit a pending event
  /// before closing.
  ///
  /// For example:
  ///
  ///     source.throttle(Duration(seconds: 6));
  ///
  ///     source: 1-2-3---4-5-6---7-8-|
  ///     result: 1-------4-------7---|
  ///
  ///     source.throttle(Duration(seconds: 6), trailing: true);
  ///
  ///     source: 1-2-3---4-5----6--|
  ///     result: 1-----3-----5-----6|
  ///
  ///     source.throttle(Duration(seconds: 6), trailing: true);
  ///
  ///     source: 1-2-----------3|
  ///     result: 1-----2-------3|
  ///
  /// See also:
  /// - [audit], which emits the most recent event at the end of the period.
  /// Compared to `audit`, `throttle` will not introduce delay to forwarded
  /// elements, except for the [trailing] events.
  /// - [debounce], which uses inter-event spacing instead of a fixed period
  /// from the first event in a window. Compared to `debouce`, `throttle` cannot
  /// be starved by having events emitted continuously within [duration].
  Stream<T> throttle(Duration duration, {bool trailing = false}) =>
      trailing ? _throttleTrailing(duration) : _throttle(duration);

  Stream<T> _throttle(Duration duration) {
    Timer? timer;

    return transformByHandlers(onData: (data, sink) {
      if (timer == null) {
        sink.add(data);
        timer = Timer(duration, () {
          timer = null;
        });
      }
    });
  }

  Stream<T> _throttleTrailing(Duration duration) {
    Timer? timer;
    T? pending;
    var hasPending = false;
    var isDone = false;

    return transformByHandlers(onData: (data, sink) {
      void onTimer() {
        if (hasPending) {
          sink.add(pending as T);
          if (isDone) {
            sink.close();
          } else {
            timer = Timer(duration, onTimer);
            hasPending = false;
            pending = null;
          }
        } else {
          timer = null;
        }
      }

      if (timer == null) {
        sink.add(data);
        timer = Timer(duration, onTimer);
      } else {
        hasPending = true;
        pending = data;
      }
    }, onDone: (sink) {
      isDone = true;
      if (hasPending) return; // Will be closed by timer.
      sink.close();
      timer?.cancel();
      timer = null;
    });
  }

  /// Audit a single event from each [duration] length period where there are
  /// events on this stream.
  ///
  /// No events will ever be emitted within [duration] of another event on the
  /// result stream.
  /// If this stream is a broadcast stream, the result will be as well.
  /// Errors are forwarded immediately.
  ///
  /// The first event will begin the audit period. At the end of the audit
  /// period the most recent event is emitted, and the next event restarts the
  /// audit period.
  ///
  /// If the event that started the period is the one that is emitted it will be
  /// delayed by [duration]. If a later event comes in within the period it's
  /// delay will be shorter by the difference in arrival times.
  ///
  /// If there is no pending event when this stream closes the output
  /// stream will close immediately. If there is a pending event the output
  /// stream will wait to emit it before closing.
  ///
  /// For example:
  ///
  ///     source.audit(Duration(seconds: 5));
  ///
  ///     source: a------b--c----d--|
  ///     output: -----a------c--------d|
  ///
  /// See also:
  /// - [throttle], which emits the _first_ event during the window, instead of
  /// the last event in the window. Compared to `throttle`, `audit` will
  /// introduce delay to forwarded events.
  /// - [debounce], which only emits after the stream has not emitted for some
  /// period. Compared to `debouce`, `audit` cannot be starved by having events
  /// emitted continuously within [duration].
  Stream<T> audit(Duration duration) {
    Timer? timer;
    var shouldClose = false;
    T recentData;

    return transformByHandlers(onData: (data, sink) {
      recentData = data;
      timer ??= Timer(duration, () {
        sink.add(recentData);
        timer = null;
        if (shouldClose) {
          sink.close();
        }
      });
    }, onDone: (sink) {
      if (timer != null) {
        shouldClose = true;
      } else {
        sink.close();
      }
    });
  }

  /// Buffers the values emitted on this stream and emits them when [trigger]
  /// emits an event.
  ///
  /// If [longPoll] is `false`, if there are no buffered values when [trigger]
  /// emits an empty list is immediately emitted.
  ///
  /// If [longPoll] is `true`, and there are no buffered values when [trigger]
  /// emits one or more events, then the *next* value from this stream is
  /// immediately emitted on the returned stream as a single element list.
  /// Subsequent events on [trigger] while there have been no events on this
  /// stream are ignored.
  ///
  /// The result stream will close as soon as there is a guarantee it will not
  /// emit any more events. There will not be any more events emitted if:
  /// - [trigger] is closed and there is no waiting long poll.
  /// - Or, this stream is closed and previously buffered events have been
  /// delivered.
  ///
  /// If this stream is a broadcast stream, the result will be as well.
  /// Errors from this stream or the trigger are immediately forwarded to the
  /// output.
  ///
  /// See also:
  /// - [sample] which use a [trigger] stream in the same way, but keeps only
  /// the most recent source event.
  Stream<List<T>> buffer(Stream<void> trigger, {bool longPoll = true}) =>
      aggregateSample(
          trigger: trigger,
          aggregate: _collect,
          longPoll: longPoll,
          onEmpty: _empty);

  /// Emits the most recent new value from this stream when [trigger] emits an
  /// event.
  ///
  /// If [longPoll] is `false`, then an event on [trigger] when there is no
  /// pending source event will be ignored.
  /// If [longPoll] is `true` (the default), then an event on [trigger] when
  /// there is no pending source event will cause the next source event
  /// to immediately flow to the result stream.
  ///
  /// If [longPoll] is `false`, if there is no pending source event when
  /// [trigger] emits, then the trigger event will be ignored.
  ///
  /// If [longPoll] is `true`, and there are no buffered values when [trigger]
  /// emits one or more events, then the *next* value from this stream is
  /// immediately emitted on the returned stream as a single element list.
  /// Subsequent events on [trigger] while there have been no events on this
  /// stream are ignored.
  ///
  /// The result stream will close as soon as there is a guarantee it will not
  /// emit any more events. There will not be any more events emitted if:
  /// - [trigger] is closed and there is no waiting long poll.
  /// - Or, this source stream is closed and any pending source event has been
  /// delivered.
  ///
  /// If this source stream is a broadcast stream, the result will be as well.
  /// Errors from this source stream or the trigger are immediately forwarded to
  /// the output.
  ///
  /// See also:
  /// - [buffer] which use [trigger] stream in the same way, but keeps a list of
  /// pending source events.
  Stream<T> sample(Stream<void> trigger, {bool longPoll = true}) =>
      aggregateSample(
          trigger: trigger,
          aggregate: _dropPrevious,
          longPoll: longPoll,
          onEmpty: ignoreArgument);

  /// Aggregates values until this source stream does not emit for [duration],
  /// then emits the aggregated values.
  Stream<S> _debounceAggregate<S>(
      Duration duration, S Function(T element, S? soFar) collect,
      {required bool leading, required bool trailing}) {
    Timer? timer;
    S? soFar;
    var hasPending = false;
    var shouldClose = false;
    var emittedLatestAsLeading = false;

    return transformByHandlers(onData: (value, sink) {
      void emit() {
        sink.add(soFar as S);
        soFar = null;
        hasPending = false;
      }

      timer?.cancel();
      soFar = collect(value, soFar);
      hasPending = true;
      if (timer == null && leading) {
        emittedLatestAsLeading = true;
        emit();
      } else {
        emittedLatestAsLeading = false;
      }
      timer = Timer(duration, () {
        if (trailing && !emittedLatestAsLeading) emit();
        if (shouldClose) sink.close();
        timer = null;
      });
    }, onDone: (EventSink<S> sink) {
      if (hasPending && trailing) {
        shouldClose = true;
      } else {
        timer?.cancel();
        sink.close();
      }
    });
  }
}

T _dropPrevious<T>(T element, _) => element;
List<T> _collect<T>(T event, List<T>? soFar) => (soFar ?? <T>[])..add(event);
void _empty<T>(Sink<List<T>> sink) => sink.add([]);
