// Copyright (c) 2016, 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 'package:meta/meta.dart';

/// An instance of [DependencyWalker] contains the core algorithms for
/// walking a dependency graph and evaluating nodes in a safe order.
abstract class DependencyWalker<NodeType extends Node<NodeType>> {
  /// Evaluates a single non-cyclical node, after all that node's dependencies
  /// have been evaluated.
  ///
  /// Should only be called by [walk].
  @visibleForOverriding
  void evaluate(NodeType v);

  /// Evaluates a strongly connected component containing one or more nodes.
  ///
  /// All dependencies of the strongly connected component have been evaluated.
  /// Should only be called by [walk].
  @visibleForOverriding
  void evaluateScc(List<NodeType> scc);

  /// Walk the dependency graph starting at [startingPoint], finding
  /// strongly connected components and evaluating them in a safe order
  /// by calling [evaluate] and [evaluateScc].
  ///
  /// This is an implementation of Tarjan's strongly connected
  /// components algorithm
  /// (https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm).
  void walk(NodeType startingPoint) {
    // TODO(paulberry): consider rewriting in a non-recursive way so
    // that long dependency chains don't cause stack overflow.

    // TODO(paulberry): in the event that an exception occurs during
    // the walk, restore the state of the [Node] data structures so
    // that further evaluation will be safe.

    if (startingPoint.isEvaluated) return;

    // The index which will be assigned to the next node that is
    // freshly visited.
    int index = 1;

    // Stack of nodes which have been seen so far and whose strongly
    // connected component is still being determined.  Nodes are only
    // popped off the stack when they are evaluated, so sometimes the
    // stack contains nodes that were visited after the current node.
    List<NodeType> stack = <NodeType>[];

    void strongConnect(NodeType node) {
      bool hasTrivialCycle = false;

      // Assign the current node an index and add it to the stack.  We
      // haven't seen any of its dependencies yet, so set its lowLink
      // to its index, indicating that so far it is the only node in
      // its strongly connected component.
      node._index = node._lowLink = index++;
      stack.add(node);

      // Consider the node's dependencies one at a time.
      for (NodeType dependency in Node.getDependencies(node)) {
        // If the dependency has already been evaluated, it can't be
        // part of this node's strongly connected component, so we can
        // skip it.
        if (dependency.isEvaluated) {
          continue;
        }
        if (identical(node, dependency)) {
          // If a node includes itself as a dependency, there is no need to
          // explore the dependency further.
          hasTrivialCycle = true;
        } else if (dependency._index == 0) {
          // The dependency hasn't been seen yet, so recurse on it.
          strongConnect(dependency);
          // If the dependency's lowLink refers to a node that was
          // visited before the current node, that means that the
          // current node, the dependency, and the node referred to by
          // the dependency's lowLink are all part of the same
          // strongly connected component, so we need to update the
          // current node's lowLink accordingly.
          if (dependency._lowLink < node._lowLink) {
            node._lowLink = dependency._lowLink;
          }
        } else {
          // The dependency has already been seen, so it is part of
          // the current node's strongly connected component.  If it
          // was visited earlier than the current node's lowLink, then
          // it is a new addition to the current node's strongly
          // connected component, so we need to update the current
          // node's lowLink accordingly.
          if (dependency._index < node._lowLink) {
            node._lowLink = dependency._index;
          }
        }
      }

      // If the current node's lowLink is the same as its index, then
      // we have finished visiting a strongly connected component, so
      // pop the stack and evaluate it before moving on.
      if (node._lowLink == node._index) {
        // The strongly connected component has only one node.  If there is a
        // cycle, it's a trivial one.
        if (identical(stack.last, node)) {
          stack.removeLast();
          if (hasTrivialCycle) {
            evaluateScc(<NodeType>[node]);
          } else {
            evaluate(node);
          }
        } else {
          // There are multiple nodes in the strongly connected
          // component.
          List<NodeType> scc = <NodeType>[];
          while (true) {
            NodeType otherNode = stack.removeLast();
            scc.add(otherNode);
            if (identical(otherNode, node)) {
              break;
            }
          }
          evaluateScc(scc);
        }
      }
    }

    // Kick off the algorithm starting with the starting point.
    strongConnect(startingPoint);
  }
}

/// Instances of [Node] represent nodes in a dependency graph.  The
/// type parameter, [NodeType], is the derived type (this affords some
/// extra type safety by making it difficult to accidentally construct
/// bridges between unrelated dependency graphs).
abstract class Node<NodeType> {
  /// Index used by Tarjan's strongly connected components algorithm.
  /// Zero means the node has not been visited yet; a nonzero value
  /// counts the order in which the node was visited.
  int _index = 0;

  /// Low link used by Tarjan's strongly connected components
  /// algorithm.  This represents the smallest [_index] of all the nodes
  /// in the strongly connected component to which this node belongs.
  int _lowLink = 0;

  List<NodeType>? _dependencies;

  /// Indicates whether this node has been evaluated yet.
  bool get isEvaluated;

  /// Compute the dependencies of this node.
  List<NodeType> computeDependencies();

  /// Gets the dependencies of the given node, computing them if necessary.
  static List<NodeType> getDependencies<NodeType>(Node<NodeType> node) {
    return node._dependencies ??= node.computeDependencies();
  }
}
