/*****************************************************************************
 * ------------------------------------------------------------------------- *
 * Licensed under the Apache License, Version 2.0 (the "License");           *
 * you may not use this file except in compliance with the License.          *
 * You may obtain a copy of the License at                                   *
 *                                                                           *
 * http://www.apache.org/licenses/LICENSE-2.0                                *
 *                                                                           *
 * Unless required by applicable law or agreed to in writing, software       *
 * distributed under the License is distributed on an "AS IS" BASIS,         *
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  *
 * See the License for the specific language governing permissions and       *
 * limitations under the License.                                            *
 *****************************************************************************/
package com.google.mu.util.graph;

import static com.google.mu.util.stream.MoreStreams.toListAndThen;
import static com.google.mu.util.stream.MoreStreams.whileNotNull;
import static java.util.Objects.requireNonNull;

import java.util.ArrayDeque;
import java.util.Collections;
import java.util.Deque;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Queue;
import java.util.Spliterator;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;

/**
 * Walker for graph topology (see {@link Walker#inGraph Walker.inGraph()}).
 *
 * <p>Besides {@link #preOrderFrom pre-order}, {@link #postOrderFrom post-order} and {@link
 * #breadthFirstFrom breadth-first} traversals, also supports {@link #topologicalOrderFrom
 * topologicalOrderFrom()} and {@link #detectCycleFrom detectCycleFrom()}.
 *
 * @param <N> the graph node type
 * @since 4.3
 */
public abstract class GraphWalker<N> extends Walker<N> {
  @Override public final Stream<N> preOrderFrom(Iterable<? extends N> startNodes) {
    return start().preOrder(startNodes);
  }

  @Override public final Stream<N> postOrderFrom(Iterable<? extends N> startNodes) {
    return start().postOrder(startNodes);
  }

  @Override public final Stream<N> breadthFirstFrom(Iterable<? extends N> startNodes) {
    return start().breadthFirst(startNodes);
  }

  /**
   * Walking from {@code startNodes}, detects if the graph has any cycle.
   *
   * <p>In the following cyclic graph, if starting from node {@code a}, the detected cyclic path
   * will be: {@code a -> b -> c -> e -> b}, with {@code b -> c -> e -> b} being the cycle, and
   * {@code a -> b} the prefix path leading to the cycle.
   *
   * <pre>{@code
   * a -> b -> c -> d
   *      ^  /
   *      | /
   *      |/
   *      e
   * }</pre>
   *
   * <p>This method will hang if the given graph is infinite without cycle (the sequence of natural
   * numbers for instance).
   *
   * @param startNodes the entry point nodes to start walking the graph.
   * @return The stream of nodes starting from the first of {@code startNodes} that leads to a
   *         cycle, ending with nodes along a cyclic path. The last node will also be the starting
   *         point of the cycle. That is, if {@code A} and {@code B} form a cycle, the stream ends
   *         with {@code A -> B -> A}. If there is no cycle, {@link Optional#empty} is returned.
   * @since 4.3
   */
  @SafeVarargs public final Optional<Stream<N>> detectCycleFrom(N... startNodes) {
    return detectCycleFrom(nonNullList(startNodes));
  }

  /**
   * Walking from {@code startNodes}, detects if the graph has any cycle.
   *
   * <p>In the following cyclic graph, if starting from node {@code a}, the detected cyclic path
   * will be: {@code a -> b -> c -> e -> b}, with {@code b -> c -> e -> b} being the cycle, and
   * {@code a -> b} the prefix path leading to the cycle.
   *
   * <pre>{@code
   * a -> b -> c -> d
   *      ^  /
   *      | /
   *      |/
   *      e
   * }</pre>
   *
   * <p>This method will hang if the given graph is infinite with no cycles (the sequence of natural
   * numbers for instance).
   *
   * @param startNodes the entry point nodes to start walking the graph.
   * @return The stream of nodes starting from the first of {@code startNodes} that leads to a
   *         cycle, ending with nodes along a cyclic path. The last node will also be the starting
   *         point of the cycle. That is, if {@code A} and {@code B} form a cycle, the stream ends
   *         with {@code A -> B -> A}. If there is no cycle, {@link Optional#empty} is returned.
   * @since 4.3
   */
  public final Optional<Stream<N>> detectCycleFrom(Iterable<? extends N> startNodes) {
    return start().detectCycle(startNodes);
  }

  /**
   * Fully traverses the graph by starting from {@code startNodes}, and returns an immutable list of
   * nodes in topological order.
   *
   * <p>Unlike the other {@code Walker} utilities, this method is not lazy:
   * it has to traverse the entire graph in order to figure out the topological order.
   *
   * @param startNodes the entry point nodes to start traversing the graph.
   * @throws CyclicGraphException if the graph has cycles.
   * @since 4.3
   */
  @SafeVarargs public final List<N> topologicalOrderFrom(N... startNodes) {
    return topologicalOrderFrom(nonNullList(startNodes));
  }

  /**
   * Fully traverses the graph by starting from {@code startNodes}, and returns an immutable list of
   * nodes in topological order.
   *
   * <p>Unlike the other {@code Walker} utilities, this method is not lazy:
   * it has to traverse the entire graph in order to figure out the topological order.
   *
   * @param startNodes the entry point nodes to start traversing the graph.
   * @throws CyclicGraphException if the graph has cycles.
   * @since 4.3
   */
  public final List<N> topologicalOrderFrom(Iterable<? extends N> startNodes) {
    return start().topologicalOrder(startNodes);
  }

  abstract Walk<N> start();

  static final class Walk<N> implements Consumer<N> {
    private final Function<? super N, ? extends Stream<? extends N>> findSuccessors;
    private final Predicate<? super N> tracker;
    private final Deque<Spliterator<? extends N>> horizon = new ArrayDeque<>();
    private N visited;

    Walk(
        Function<? super N, ? extends Stream<? extends N>> findSuccessors,
        Predicate<? super N> tracker) {
      this.findSuccessors = findSuccessors;
      this.tracker = tracker;
    }

    @Override public void accept(N value) {
      this.visited = requireNonNull(value);
    }

    Stream<N> breadthFirst(Iterable<? extends N> startNodes) {
      horizon.add(startNodes.spliterator());
      return topDown(Queue::add);
    }

    Stream<N> preOrder(Iterable<? extends N> startNodes) {
      horizon.push(startNodes.spliterator());
      return topDown(Deque::push);
    }

    Stream<N> postOrder(Iterable<? extends N> startNodes) {
      horizon.push(startNodes.spliterator());
      Deque<N> roots = new ArrayDeque<>();
      return whileNotNull(() -> {
        while (visitNext()) {
          N next = visited;
          Stream<? extends N> successors = findSuccessors.apply(next);
          if (successors == null) return next;
          horizon.push(successors.spliterator());
          roots.push(next);
        }
        return roots.poll();
      });
    }

    private Stream<N> topDown(InsertionOrder order) {
      return whileNotNull(() -> {
        do {
          if (visitNext()) {
            N next = visited;
            Stream<? extends N> successors = findSuccessors.apply(next);
            if (successors != null) order.insertInto(horizon, successors.spliterator());
            return next;
          }
        } while (!horizon.isEmpty());
        return null; // no more element
      });
    }

    private boolean visitNext() {
      Spliterator<? extends N> top = horizon.getFirst();
      while (top.tryAdvance(this)) {
        if (tracker.test(visited)) return true;
      }
      horizon.removeFirst();
      return false;
    }

    List<N> topologicalOrder(Iterable<? extends N> startNodes) {
      CycleTracker cycleDetector = new CycleTracker();
      return cycleDetector.startPostOrder(startNodes, n -> {
        throw new CyclicGraphException(
            cycleDetector.currentPath().collect(toListAndThen(l -> l.add(n))));
      }).collect(toListAndThen(Collections::reverse));
    }

    Optional<Stream<N>> detectCycle(Iterable<? extends N> startNodes) {
      AtomicReference<N> cyclic = new AtomicReference<>();
      CycleTracker detector = new CycleTracker();
      return detector.startPostOrder(startNodes, n -> cyclic.compareAndSet(null, n))
          .filter(n -> cyclic.get() != null)
          .findFirst()
          .map(last ->
              Stream.concat(detector.currentPath(), Stream.of(last, cyclic.getAndSet(null))));
    }

    private final class CycleTracker {
      private final LinkedHashSet<N> currentPath = new LinkedHashSet<>();

      Stream<N> startPostOrder(Iterable<? extends N> startNodes, Consumer<N> foundCycle) {
        Walk<N> walk = new Walk<>(
            findSuccessors,
            node -> {
              boolean newNode = tracker.test(node);
              if (newNode) {
                currentPath.add(node);
              } else if (currentPath.contains(node)) {
                foundCycle.accept(node);
              }
              return newNode;
            });
        return walk.postOrder(startNodes).peek(currentPath::remove);
      }

      Stream<N> currentPath() {
        return currentPath.stream();
      }
    }
  }
}