// Copyright (c) 2017, 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 'dart:collection';

final _empty = Future<void>.value();

/// Finds and returns every node in a graph who's nodes and edges are
/// asynchronously resolved.
///
/// Cycles are allowed. If this is an undirected graph the [edges] function
/// may be symmetric. In this case the [roots] may be any node in each connected
/// graph.
///
/// [V] is the type of values in the graph nodes. [K] must be a type suitable
/// for using as a Map or Set key. [edges] should return the next reachable
/// nodes.
///
/// There are no ordering guarantees. This is useful for ensuring some work is
/// performed at every node in an asynchronous graph, but does not give
/// guarantees that the work is done in topological order.
///
/// If either [readNode] or [edges] throws the error will be forwarded
/// through the result stream and no further nodes will be crawled, though some
/// work may have already been started.
///
/// Crawling is eager, so calls to [edges] may overlap with other calls that
/// have not completed. If the [edges] callback needs to be limited or throttled
/// that must be done by wrapping it before calling [crawlAsync].
Stream<V> crawlAsync<K extends Object, V>(
  Iterable<K> roots,
  FutureOr<V> Function(K) readNode,
  FutureOr<Iterable<K>> Function(K, V) edges,
) {
  final crawl = _CrawlAsync(roots, readNode, edges)..run();
  return crawl.result.stream;
}

class _CrawlAsync<K, V> {
  final result = StreamController<V>();

  final FutureOr<V> Function(K) readNode;
  final FutureOr<Iterable<K>> Function(K, V) edges;
  final Iterable<K> roots;

  final _seen = HashSet<K>();

  _CrawlAsync(this.roots, this.readNode, this.edges);

  /// Add all nodes in the graph to [result] and return a Future which fires
  /// after all nodes have been seen.
  Future<void> run() async {
    try {
      await Future.wait(roots.map(_visit), eagerError: true);
      await result.close();
    } catch (e, st) {
      result.addError(e, st);
      await result.close();
    }
  }

  /// Resolve the node at [key] and output it, then start crawling all of it's
  /// edges.
  Future<void> _crawlFrom(K key) async {
    final value = await readNode(key);
    if (result.isClosed) return;
    result.add(value);
    final next = await edges(key, value);
    await Future.wait(next.map(_visit), eagerError: true);
  }

  /// Synchronously record that [key] is being handled then start work on the
  /// node for [key].
  ///
  /// The returned Future will complete only after the work for [key] and all
  /// transitively reachable nodes has either been finished, or will be finished
  /// by some other Future in [_seen].
  Future<void> _visit(K key) {
    if (_seen.contains(key)) return _empty;
    _seen.add(key);
    return _crawlFrom(key);
  }
}
