// Copyright (c) 2023, 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.

part of "dart:async";

@Since("3.0")
extension FutureIterable<T> on Iterable<Future<T>> {
  /// Waits for futures in parallel.
  ///
  /// Waits for all the futures in this iterable.
  /// Returns a list of the resulting values,
  /// in the same order as the futures which created them,
  /// if all futures are successful.
  ///
  /// Similar to [Future.wait], but reports errors using a
  /// [ParallelWaitError], which allows the caller to
  /// handle errors and dispose successful results if necessary.
  ///
  /// The returned future is completed when all the futures have completed.
  /// If any of the futures do not complete, nor does the returned future.
  ///
  /// If any future completes with an error,
  /// the returned future completes with a [ParallelWaitError].
  /// The [ParallelWaitError.values] is a list of the values for
  /// successful futures and `null` for futures with errors.
  /// The [ParallelWaitError.errors] is a list of the same length,
  /// with `null` values for the successful futures
  /// and an [AsyncError] with the error for futures
  /// which completed with an error.
  Future<List<T>> get wait {
    var results = [for (var f in this) _FutureResult<T>(f)];
    if (results.isEmpty) return Future<List<T>>.value(<T>[]);

    @pragma('vm:awaiter-link')
    final c = Completer<List<T>>.sync();

    _FutureResult._waitAll(results, (errors) {
      if (errors == 0) {
        c.complete([for (var r in results) r.value]);
      } else {
        var errorList = [for (var r in results) r.errorOrNull];
        c.completeError(
          ParallelWaitError<List<T?>, List<AsyncError?>>(
            [for (var r in results) r.valueOrNull],
            errorList,
            errorCount: errors,
            defaultError: errorList.firstWhere(_notNull),
          ),
        );
      }
    });
    return c.future;
  }
}

bool _notNull(Object? object) => object != null;

/// Parallel operations on a record of futures.
///
/// {@template record-parallel-wait}
/// Waits for futures in parallel.
///
/// Waits for all the futures in this record.
/// Returns a record of the values, if all futures are successful.
///
/// The returned future is completed when all the futures have completed.
/// If any of the futures do not complete, nor does the returned future.
///
/// If some futures complete with an error,
/// the returned future completes with a [ParallelWaitError].
/// The [ParallelWaitError.values] is a record of the values of
/// successful futures, and `null` for futures with errors.
/// The [ParallelWaitError.errors] is a record of the same shape,
/// with `null` values for the successful futures
/// and an [AsyncError] with the error of futures
/// which completed with an error.
/// {@endtemplate}
@Since("3.0")
extension FutureRecord2<T1, T2> on (Future<T1>, Future<T2>) {
  /// {@macro record-parallel-wait}
  Future<(T1, T2)> get wait {
    @pragma('vm:awaiter-link')
    final c = Completer<(T1, T2)>.sync();
    final v1 = _FutureResult<T1>($1);
    final v2 = _FutureResult<T2>($2);

    _FutureResult._waitAll([v1, v2], (int errors) {
      if (errors == 0) {
        c.complete((v1.value, v2.value));
      } else {
        c.completeError(
          ParallelWaitError(
            (v1.valueOrNull, v2.valueOrNull),
            (v1.errorOrNull, v2.errorOrNull),
            errorCount: errors,
            defaultError: v1.errorOrNull ?? v2.errorOrNull,
          ),
        );
      }
    });
    return c.future;
  }
}

/// Parallel operations on a record of futures.
@Since("3.0")
extension FutureRecord3<T1, T2, T3> on (Future<T1>, Future<T2>, Future<T3>) {
  /// {@macro record-parallel-wait}
  Future<(T1, T2, T3)> get wait {
    @pragma('vm:awaiter-link')
    final c = Completer<(T1, T2, T3)>.sync();
    final v1 = _FutureResult<T1>($1);
    final v2 = _FutureResult<T2>($2);
    final v3 = _FutureResult<T3>($3);

    _FutureResult._waitAll([v1, v2, v3], (int errors) {
      if (errors == 0) {
        c.complete((v1.value, v2.value, v3.value));
      } else {
        c.completeError(
          ParallelWaitError(
            (v1.valueOrNull, v2.valueOrNull, v3.valueOrNull),
            (v1.errorOrNull, v2.errorOrNull, v3.errorOrNull),
            errorCount: errors,
            defaultError: v1.errorOrNull ?? v2.errorOrNull ?? v3.errorOrNull,
          ),
        );
      }
    });
    return c.future;
  }
}

/// Parallel operations on a record of futures.
@Since("3.0")
extension FutureRecord4<T1, T2, T3, T4>
    on (Future<T1>, Future<T2>, Future<T3>, Future<T4>) {
  /// {@macro record-parallel-wait}
  Future<(T1, T2, T3, T4)> get wait {
    @pragma('vm:awaiter-link')
    final c = Completer<(T1, T2, T3, T4)>.sync();
    final v1 = _FutureResult<T1>($1);
    final v2 = _FutureResult<T2>($2);
    final v3 = _FutureResult<T3>($3);
    final v4 = _FutureResult<T4>($4);

    _FutureResult._waitAll([v1, v2, v3, v4], (int errors) {
      if (errors == 0) {
        c.complete((v1.value, v2.value, v3.value, v4.value));
      } else {
        c.completeError(
          ParallelWaitError(
            (v1.valueOrNull, v2.valueOrNull, v3.valueOrNull, v4.valueOrNull),
            (v1.errorOrNull, v2.errorOrNull, v3.errorOrNull, v4.errorOrNull),
            errorCount: errors,
            defaultError:
                v1.errorOrNull ??
                v2.errorOrNull ??
                v3.errorOrNull ??
                v4.errorOrNull,
          ),
        );
      }
    });
    return c.future;
  }
}

/// Parallel operations on a record of futures.
@Since("3.0")
extension FutureRecord5<T1, T2, T3, T4, T5>
    on (Future<T1>, Future<T2>, Future<T3>, Future<T4>, Future<T5>) {
  /// {@macro record-parallel-wait}
  Future<(T1, T2, T3, T4, T5)> get wait {
    @pragma('vm:awaiter-link')
    final c = Completer<(T1, T2, T3, T4, T5)>.sync();
    final v1 = _FutureResult<T1>($1);
    final v2 = _FutureResult<T2>($2);
    final v3 = _FutureResult<T3>($3);
    final v4 = _FutureResult<T4>($4);
    final v5 = _FutureResult<T5>($5);

    _FutureResult._waitAll([v1, v2, v3, v4, v5], (int errors) {
      if (errors == 0) {
        c.complete((v1.value, v2.value, v3.value, v4.value, v5.value));
      } else {
        c.completeError(
          ParallelWaitError(
            (
              v1.valueOrNull,
              v2.valueOrNull,
              v3.valueOrNull,
              v4.valueOrNull,
              v5.valueOrNull,
            ),
            (
              v1.errorOrNull,
              v2.errorOrNull,
              v3.errorOrNull,
              v4.errorOrNull,
              v5.errorOrNull,
            ),
            errorCount: errors,
            defaultError:
                v1.errorOrNull ??
                v2.errorOrNull ??
                v3.errorOrNull ??
                v4.errorOrNull ??
                v5.errorOrNull,
          ),
        );
      }
    });
    return c.future;
  }
}

/// Parallel operations on a record of futures.
@Since("3.0")
extension FutureRecord6<T1, T2, T3, T4, T5, T6>
    on
        (
          Future<T1>,
          Future<T2>,
          Future<T3>,
          Future<T4>,
          Future<T5>,
          Future<T6>,
        ) {
  /// {@macro record-parallel-wait}
  Future<(T1, T2, T3, T4, T5, T6)> get wait {
    @pragma('vm:awaiter-link')
    final c = Completer<(T1, T2, T3, T4, T5, T6)>.sync();
    final v1 = _FutureResult<T1>($1);
    final v2 = _FutureResult<T2>($2);
    final v3 = _FutureResult<T3>($3);
    final v4 = _FutureResult<T4>($4);
    final v5 = _FutureResult<T5>($5);
    final v6 = _FutureResult<T6>($6);

    _FutureResult._waitAll([v1, v2, v3, v4, v5, v6], (int errors) {
      if (errors == 0) {
        c.complete((
          v1.value,
          v2.value,
          v3.value,
          v4.value,
          v5.value,
          v6.value,
        ));
      } else {
        c.completeError(
          ParallelWaitError(
            (
              v1.valueOrNull,
              v2.valueOrNull,
              v3.valueOrNull,
              v4.valueOrNull,
              v5.valueOrNull,
              v6.valueOrNull,
            ),
            (
              v1.errorOrNull,
              v2.errorOrNull,
              v3.errorOrNull,
              v4.errorOrNull,
              v5.errorOrNull,
              v6.errorOrNull,
            ),
            errorCount: errors,
            defaultError:
                v1.errorOrNull ??
                v2.errorOrNull ??
                v3.errorOrNull ??
                v4.errorOrNull ??
                v5.errorOrNull ??
                v6.errorOrNull,
          ),
        );
      }
    });
    return c.future;
  }
}

/// Parallel operations on a record of futures.
@Since("3.0")
extension FutureRecord7<T1, T2, T3, T4, T5, T6, T7>
    on
        (
          Future<T1>,
          Future<T2>,
          Future<T3>,
          Future<T4>,
          Future<T5>,
          Future<T6>,
          Future<T7>,
        ) {
  /// {@macro record-parallel-wait}
  Future<(T1, T2, T3, T4, T5, T6, T7)> get wait {
    @pragma('vm:awaiter-link')
    final c = Completer<(T1, T2, T3, T4, T5, T6, T7)>.sync();
    final v1 = _FutureResult<T1>($1);
    final v2 = _FutureResult<T2>($2);
    final v3 = _FutureResult<T3>($3);
    final v4 = _FutureResult<T4>($4);
    final v5 = _FutureResult<T5>($5);
    final v6 = _FutureResult<T6>($6);
    final v7 = _FutureResult<T7>($7);

    _FutureResult._waitAll([v1, v2, v3, v4, v5, v6, v7], (int errors) {
      if (errors == 0) {
        c.complete((
          v1.value,
          v2.value,
          v3.value,
          v4.value,
          v5.value,
          v6.value,
          v7.value,
        ));
      } else {
        c.completeError(
          ParallelWaitError(
            (
              v1.valueOrNull,
              v2.valueOrNull,
              v3.valueOrNull,
              v4.valueOrNull,
              v5.valueOrNull,
              v6.valueOrNull,
              v7.valueOrNull,
            ),
            (
              v1.errorOrNull,
              v2.errorOrNull,
              v3.errorOrNull,
              v4.errorOrNull,
              v5.errorOrNull,
              v6.errorOrNull,
              v7.errorOrNull,
            ),
            errorCount: errors,
            defaultError:
                v1.errorOrNull ??
                v2.errorOrNull ??
                v3.errorOrNull ??
                v4.errorOrNull ??
                v5.errorOrNull ??
                v6.errorOrNull ??
                v7.errorOrNull,
          ),
        );
      }
    });
    return c.future;
  }
}

/// Parallel operations on a record of futures.
@Since("3.0")
extension FutureRecord8<T1, T2, T3, T4, T5, T6, T7, T8>
    on
        (
          Future<T1>,
          Future<T2>,
          Future<T3>,
          Future<T4>,
          Future<T5>,
          Future<T6>,
          Future<T7>,
          Future<T8>,
        ) {
  /// {@macro record-parallel-wait}
  Future<(T1, T2, T3, T4, T5, T6, T7, T8)> get wait {
    @pragma('vm:awaiter-link')
    final c = Completer<(T1, T2, T3, T4, T5, T6, T7, T8)>.sync();
    final v1 = _FutureResult<T1>($1);
    final v2 = _FutureResult<T2>($2);
    final v3 = _FutureResult<T3>($3);
    final v4 = _FutureResult<T4>($4);
    final v5 = _FutureResult<T5>($5);
    final v6 = _FutureResult<T6>($6);
    final v7 = _FutureResult<T7>($7);
    final v8 = _FutureResult<T8>($8);

    _FutureResult._waitAll([v1, v2, v3, v4, v5, v6, v7, v8], (int errors) {
      if (errors == 0) {
        c.complete((
          v1.value,
          v2.value,
          v3.value,
          v4.value,
          v5.value,
          v6.value,
          v7.value,
          v8.value,
        ));
      } else {
        c.completeError(
          ParallelWaitError(
            (
              v1.valueOrNull,
              v2.valueOrNull,
              v3.valueOrNull,
              v4.valueOrNull,
              v5.valueOrNull,
              v6.valueOrNull,
              v7.valueOrNull,
              v8.valueOrNull,
            ),
            (
              v1.errorOrNull,
              v2.errorOrNull,
              v3.errorOrNull,
              v4.errorOrNull,
              v5.errorOrNull,
              v6.errorOrNull,
              v7.errorOrNull,
              v8.errorOrNull,
            ),
            errorCount: errors,
            defaultError:
                v1.errorOrNull ??
                v2.errorOrNull ??
                v3.errorOrNull ??
                v4.errorOrNull ??
                v5.errorOrNull ??
                v6.errorOrNull ??
                v7.errorOrNull ??
                v8.errorOrNull,
          ),
        );
      }
    });
    return c.future;
  }
}

/// Parallel operations on a record of futures.
@Since("3.0")
extension FutureRecord9<T1, T2, T3, T4, T5, T6, T7, T8, T9>
    on
        (
          Future<T1>,
          Future<T2>,
          Future<T3>,
          Future<T4>,
          Future<T5>,
          Future<T6>,
          Future<T7>,
          Future<T8>,
          Future<T9>,
        ) {
  /// {@macro record-parallel-wait}
  Future<(T1, T2, T3, T4, T5, T6, T7, T8, T9)> get wait {
    @pragma('vm:awaiter-link')
    final c = Completer<(T1, T2, T3, T4, T5, T6, T7, T8, T9)>.sync();
    final v1 = _FutureResult<T1>($1);
    final v2 = _FutureResult<T2>($2);
    final v3 = _FutureResult<T3>($3);
    final v4 = _FutureResult<T4>($4);
    final v5 = _FutureResult<T5>($5);
    final v6 = _FutureResult<T6>($6);
    final v7 = _FutureResult<T7>($7);
    final v8 = _FutureResult<T8>($8);
    final v9 = _FutureResult<T9>($9);

    _FutureResult._waitAll([v1, v2, v3, v4, v5, v6, v7, v8, v9], (int errors) {
      if (errors == 0) {
        c.complete((
          v1.value,
          v2.value,
          v3.value,
          v4.value,
          v5.value,
          v6.value,
          v7.value,
          v8.value,
          v9.value,
        ));
      } else {
        c.completeError(
          ParallelWaitError(
            (
              v1.valueOrNull,
              v2.valueOrNull,
              v3.valueOrNull,
              v4.valueOrNull,
              v5.valueOrNull,
              v6.valueOrNull,
              v7.valueOrNull,
              v8.valueOrNull,
              v9.valueOrNull,
            ),
            (
              v1.errorOrNull,
              v2.errorOrNull,
              v3.errorOrNull,
              v4.errorOrNull,
              v5.errorOrNull,
              v6.errorOrNull,
              v7.errorOrNull,
              v8.errorOrNull,
              v9.errorOrNull,
            ),
            errorCount: errors,
            defaultError:
                v1.errorOrNull ??
                v2.errorOrNull ??
                v3.errorOrNull ??
                v4.errorOrNull ??
                v5.errorOrNull ??
                v6.errorOrNull ??
                v7.errorOrNull ??
                v8.errorOrNull ??
                v9.errorOrNull,
          ),
        );
      }
    });
    return c.future;
  }
}

/// Error thrown when waiting for multiple futures, when some have errors.
///
/// The [V] and [E] types will have the same basic shape as the
/// original collection of futures that was waited on.
///
/// For example, if the original awaited futures were a record
/// `(Future<T1>, ..., Future<Tn>)`,
/// the type `V` will be `(T1?, ..., Tn?)` which allows keeping the
/// values of futures that completed with a value,
/// and `E` will be `(AsyncError?, ..., AsyncError?)`, also with *n*
/// fields, which can contain the errors for the futures which completed
/// with an error.
///
/// Waiting for a list or iterable of futures should provide
/// a list of nullable values and errors of the same length.
@Since("3.0")
class ParallelWaitError<V, E> extends Error {
  /// Values of successful futures.
  ///
  /// Has the same shape as the original collection of futures,
  /// with values for each successful future and `null` values
  /// for each failing future.
  final V values;

  /// Errors of failing futures.
  ///
  /// Has the same shape as the original collection of futures,
  /// with errors, typically [AsyncError], for each failing
  /// future and `null` values for each successful future.
  final E errors;

  /// An error which, if present, is included in the [toString] output.
  ///
  /// If the default error has a stack trace, it's also reported by the
  /// [stackTrace] getter, instead of where this [ParallelWaitError] was thrown.
  final AsyncError? _defaultError;

  /// Number of errors, if available.
  final int? _errorCount;

  /// Creates error with the provided [values] and [errors].
  ///
  /// If [defaultError] is provided, its [AsyncError.error] is used in
  /// the [toString] of this parallel error, and its [AsyncError.stackTrace]
  /// is returned by [stackTrace].
  ///
  /// If [errorCount] is provided, and it's greater than one,
  /// the number is reported in the [toString].
  ParallelWaitError(
    this.values,
    this.errors, {
    @Since("3.4") int? errorCount,
    @Since("3.4") AsyncError? defaultError,
  }) : _defaultError = defaultError,
       _errorCount = errorCount;

  String toString() {
    if (_defaultError == null) {
      if (_errorCount == null || _errorCount <= 1) {
        return "ParallelWaitError";
      }
      return "ParallelWaitError($_errorCount errors)";
    }
    return "ParallelWaitError${_errorCount != null && _errorCount > 1 //
                ? "($_errorCount errors)" : ""}: ${_defaultError.error}";
  }

  StackTrace? get stackTrace => _defaultError?.stackTrace ?? super.stackTrace;
}

/// The result of a future, when it has completed.
///
/// Stores a value result in [value] and an error result in [error].
/// Then calls [onReady] with a 0 argument for a value, and 1 for an error.
///
/// The [onReady] callback must be set synchronously,
/// before the future has a chance to complete.
///
/// Abstracted into a class of its own in order to reuse code.
class _FutureResult<T> {
  // Consider integrating directly into `_Future` as a `_FutureListener`
  // to avoid creating as many function tear-offs.

  final Future<T> source;

  /// The value or `null`.
  ///
  /// Set when the future completes with a value.
  T? valueOrNull;

  /// Set when the future completes with an error or value.
  AsyncError? errorOrNull;

  _FutureResult(this.source);

  /// The value.
  ///
  /// Should only be used when the future is known to have completed with
  /// a value.
  T get value => valueOrNull ?? valueOrNull as T;

  void _wait(@pragma('vm:awaiter-link') void Function(int) whenReady) {
    source.then(
      (T value) {
        valueOrNull = value;
        whenReady(0);
      },
      onError: (Object error, StackTrace stack) {
        errorOrNull = AsyncError(error, stack);
        whenReady(1);
      },
    );
  }

  /// Waits for a number of [_FutureResult]s to all have completed.
  ///
  /// List must not be empty.
  static void _waitAll(
    List<_FutureResult> results,
    @pragma('vm:awaiter-link') void Function(int) whenReady,
  ) {
    assert(results.isNotEmpty);
    var ready = 0;
    var errors = 0;
    void onReady(int error) {
      errors += error;
      if (++ready == results.length) {
        whenReady(errors);
      }
    }

    for (var r in results) {
      r._wait(onReady);
    }
  }

  static void _noop(_) {}
}
