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

import 'package:collection/collection.dart' hide stronglyConnectedComponents;

import 'cycle_exception.dart';

/// Returns a topological sort of the nodes of the directed edges of a graph
/// provided by [nodes] and [edges].
///
/// Each element of the returned iterable is guaranteed to appear after all
/// nodes that have edges leading to that node. The result is not guaranteed to
/// be unique, nor is it guaranteed to be stable across releases of this
/// package; however, it will be stable for a given input within a given package
/// version.
///
/// If [equals] is provided, it is used to compare nodes in the graph. If
/// [equals] is omitted, the node's own [Object.==] is used instead.
///
/// Similarly, if [hashCode] is provided, it is used to produce a hash value
/// for nodes to efficiently calculate the return value. If it is omitted, the
/// key's own [Object.hashCode] is used.
///
/// If you supply one of [equals] or [hashCode], you should generally also to
/// supply the other.
///
/// If you supply [secondarySort], the resulting list will be sorted by that
/// comparison function as much as possible without violating the topological
/// ordering. Note that even with a secondary sort, the result is _still_ not
/// guaranteed to be unique or stable across releases of this package.
///
/// Note: this requires that [nodes] and each iterable returned by [edges]
/// contain no duplicate entries.
///
/// Throws a [CycleException<T>] if the graph is cyclical.
List<T> topologicalSort<T>(
  Iterable<T> nodes,
  Iterable<T> Function(T) edges, {
  bool Function(T, T)? equals,
  int Function(T)? hashCode,
  Comparator<T>? secondarySort,
}) {
  if (secondarySort != null) {
    return _topologicalSortWithSecondary(
      [...nodes],
      edges,
      secondarySort,
      equals,
      hashCode,
    );
  }

  // https://en.wikipedia.org/wiki/Topological_sorting#Depth-first_search
  final result = QueueList<T>();
  final permanentMark = HashSet<T>(equals: equals, hashCode: hashCode);
  final temporaryMark = LinkedHashSet<T>(equals: equals, hashCode: hashCode);
  final stack = [...nodes];
  while (stack.isNotEmpty) {
    final node = stack.removeLast();
    if (permanentMark.contains(node)) continue;

    // If we're visiting this node while it's already marked and not through a
    // dependency, that must mean we've traversed all its dependencies and it's
    // safe to add it to the result.
    if (temporaryMark.contains(node)) {
      temporaryMark.remove(node);
      permanentMark.add(node);
      result.addFirst(node);
    } else {
      temporaryMark.add(node);

      // Revisit this node once we've visited all its children.
      stack.add(node);
      for (var child in edges(node)) {
        if (temporaryMark.contains(child)) throw CycleException(temporaryMark);
        stack.add(child);
      }
    }
  }

  return result;
}

/// An implementation of [topologicalSort] with a secondary comparison function.
List<T> _topologicalSortWithSecondary<T>(
  List<T> nodes,
  Iterable<T> Function(T) edges,
  Comparator<T> comparator,
  bool Function(T, T)? equals,
  int Function(T)? hashCode,
) {
  // https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm,
  // modified to sort the nodes to traverse. Also documented in
  // https://www.algotree.org/algorithms/tree_graph_traversal/lexical_topological_sort_c++/

  // For each node, the number of incoming edges it has that we haven't yet
  // traversed.
  final incomingEdges = HashMap<T, int>(equals: equals, hashCode: hashCode);
  for (var node in nodes) {
    for (var child in edges(node)) {
      incomingEdges[child] = (incomingEdges[child] ?? 0) + 1;
    }
  }

  // A priority queue of nodes that have no remaining incoming edges.
  final nodesToTraverse = PriorityQueue<T>(comparator);
  for (var node in nodes) {
    if (!incomingEdges.containsKey(node)) nodesToTraverse.add(node);
  }

  final result = <T>[];
  while (nodesToTraverse.isNotEmpty) {
    final node = nodesToTraverse.removeFirst();
    result.add(node);
    for (var child in edges(node)) {
      var remainingEdges = incomingEdges[child]!;
      remainingEdges--;
      incomingEdges[child] = remainingEdges;
      if (remainingEdges == 0) nodesToTraverse.add(child);
    }
  }

  if (result.length < nodes.length) {
    // This algorithm doesn't automatically produce a cycle list as a side
    // effect of sorting, so to throw the appropriate [CycleException] we just
    // call the normal [topologicalSort] with a view of this graph that only
    // includes nodes that still have edges.
    bool nodeIsInCycle(T node) {
      final edges = incomingEdges[node];
      return edges != null && edges > 0;
    }

    topologicalSort<T>(
      nodes.where(nodeIsInCycle),
      edges,
      equals: equals,
      hashCode: hashCode,
    );
    assert(false, 'topologicalSort() should throw if the graph has a cycle');
  }

  return result;
}
