package org.testng.internal;

import org.testng.ITestResult;
import org.testng.TestNGException;
import org.testng.collections.Lists;
import org.testng.collections.Maps;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
 * Simple graph class to implement topological sort (used to sort methods based on what groups
 * they depend on).
 *
 * @author Cedric Beust, Aug 19, 2004
 */
public class Graph<T extends Object> {
  private static boolean m_verbose = false;
  private Map<T, Node<T>> m_nodes = Maps.newHashMap();
  private List<T> m_strictlySortedNodes = null;
  
  //  A map of nodes that are not the predecessors of any node
  // (not needed for the algorithm but convenient to calculate
  // the parallel/sequential lists in TestNG).
  private Map<T, Node<T>> m_independentNodes = null;

  public void addNode(T tm) {
    ppp("ADDING NODE " + tm + " " + tm.hashCode());
    m_nodes.put(tm, new Node<T>(tm));
    // Initially, all the nodes are put in the independent list as well
  }
  
  public Set<T> getPredecessors(T node) {
    return findNode(node).getPredecessors().keySet();
  }
  
  /*private boolean hasBeenSorted() {
    return null != m_strictlySortedNodes;
  }*/
  
  public boolean isIndependent(T object) {
    return m_independentNodes.containsKey(object);
  }
  
  private Node<T> findNode(T object) {
    return m_nodes.get(object);
  }

  public void addPredecessor(T tm, T predecessor) {
    Node<T> node = findNode(tm);
    if (null == node) {
      throw new TestNGException("Non-existing node: " + tm);
    }
    else {
      node.addPredecessor(predecessor);
      addNeighbor(tm, predecessor);
      // Remove these two nodes from the independent list
      if (null == m_independentNodes) {
        m_independentNodes = Maps.newHashMap();
        for (T k : m_nodes.keySet()) {
          m_independentNodes.put(k, m_nodes.get(k));
        }
      }
      m_independentNodes.remove(predecessor);
      m_independentNodes.remove(tm);
      ppp("  REMOVED " + predecessor + " FROM INDEPENDENT OBJECTS");
    }
  }

  private void addNeighbor(T tm, T predecessor) {
    findNode(tm).addNeighbor(findNode(predecessor));
  }
  
  public Set<T> getNeighbors(T t) {
    Set<T> result = new HashSet<T>();
    for (Node<T> n : findNode(t).getNeighbors()) {
      result.add(n.getObject());
    }
    
    return result;
  }

  private Collection<Node<T>> getNodes() {
    return m_nodes.values();
  }

  public Collection<T> getNodeValues() {
    return m_nodes.keySet();
  }
  
  /**
   * @return All the nodes that don't have any order with each other.
   */
  public Set<T> getIndependentNodes() {
    return m_independentNodes.keySet();
  }
  
  /**
   * @return All the nodes that have an order with each other, sorted
   * in one of the valid sorts.
   */
  public List<T> getStrictlySortedNodes() {
    return m_strictlySortedNodes;
  }
  
  public void topologicalSort() {
    ppp("================ SORTING");
    m_strictlySortedNodes = Lists.newArrayList();
    if (null == m_independentNodes) {
      m_independentNodes = Maps.newHashMap();
    }
    
    //
    // Clone the list of nodes but only keep those that are
    // not independent.
    //
    List<Node<T>> nodes2 = Lists.newArrayList();
    for (Node<T> n : getNodes()) {
      if (! isIndependent((T) n.getObject())) {
        ppp("ADDING FOR SORT: " + n.getObject());
        nodes2.add(n.clone());
      }
      else {
        ppp("SKIPPING INDEPENDENT NODE " + n);
      }
    }
    
    //
    // Sort the nodes alphabetically to make sure that methods of the same class
    // get run close to each other as much as possible
    //
    Collections.sort(nodes2);
    
    //
    // Sort
    //
    while (! nodes2.isEmpty()) {
      
      //
      // Find all the nodes that don't have any predecessors, add
      // them to the result and mark them for removal
      //
      Node<T> node = findNodeWithNoPredecessors(nodes2);
      if (null == node) {
        List<T> cycle = new Tarjan<T>(this, nodes2.get(0).getObject()).getCycle();
        StringBuffer sb = new StringBuffer();
        sb.append("The following methods have cyclic dependencies:\n");
        for (T m : cycle) {
          sb.append(m).append("\n");
        }
        throw new TestNGException(sb.toString());
      }
      else {
        m_strictlySortedNodes.add((T) node.getObject());
        removeFromNodes(nodes2, node);
      }
    }

    ppp("=============== DONE SORTING");
    if (m_verbose) {
      dumpSortedNodes();
    }
  }
  
  private void dumpSortedNodes() {
    System.out.println("====== SORTED NODES");
    for (T n : m_strictlySortedNodes) {
      System.out.println("              " + n);
    }
    System.out.println("====== END SORTED NODES");
  }

  private void dumpGraph() {
    System.out.println("====== GRAPH");
    for (Node<T> n : m_nodes.values()) {
      System.out.println("  " + n);
    }
  }
  
  /**
   * Remove a node from a list of nodes and update the list of predecessors
   * for all the remaining nodes.
   */
  private void removeFromNodes(List<Node<T>> nodes, Node<T> node) {
    nodes.remove(node);
    for (Node<T> n : nodes) {
      n.removePredecessor(node.getObject());
    }
  }
  
  private static void ppp(String s) {
    if (m_verbose) {
      System.out.println("[Graph] " + s);
    }
  }
  
  private Node<T> findNodeWithNoPredecessors(List<Node<T>> nodes) {
    for (Node<T> n : nodes) {
      if (! n.hasPredecessors()) {
        return n;
      }
    }
    
    return null;
  }
  
  /**
   * @param o
   * @return A list of all the predecessors for o
   */
  public List<T> findPredecessors(T o) {
    // Locate the node
    Node<T> node = findNode(o);
    if (null == node) {
      // This can happen if an interceptor returned new methods
      return Lists.newArrayList();
    }

    // If we found the node, use breadth first search to find all
    // all of the predecessors of o.  "result" is the growing list
    // of all predecessors.  "visited" is the set of items we've
    // already encountered.  "queue" is the queue of items whose
    // predecessors we haven't yet explored.

    LinkedList<T> result = new LinkedList<T>();
    Set<T> visited = new HashSet<T>();
    LinkedList<T> queue = new LinkedList<T>();
    visited.add(o);
    queue.addLast(o);

    while (! queue.isEmpty()) {
      for (T obj : getPredecessors(queue.removeFirst())) {
        if (! visited.contains(obj)) {
          visited.add(obj);
          queue.addLast(obj);
          result.addFirst(obj);
        }
      }
    }

    return result;
  }
  
  @Override
  public String toString() {
    StringBuffer result = new StringBuffer("[Graph ");
    for (T node : m_nodes.keySet()) {
      result.append(findNode(node)).append(" ");
    }
    result.append("]");
    
    return result.toString();
  }
  
  
  /////
  // class Node
  //
  public static class Node<T> implements Comparable<Node<T>> {
    private T m_object = null;
    private Map<T, T> m_predecessors = Maps.newHashMap();
    
    public Node(T tm) {
      m_object = tm;
    }
    
    private Set<Node<T>> m_neighbors = new HashSet<Node<T>>();
    public void addNeighbor(Node<T> neighbor) {
      m_neighbors.add(neighbor);
    }
    
    public Set<Node<T>> getNeighbors() {
      return m_neighbors;
    }
        
    @Override
    public Node<T> clone() {
      Node<T> result = new Node<T>(m_object);
      for (T pred : m_predecessors.values()) {
        result.addPredecessor(pred);
      }
      return result;
    }
    
    public T getObject() {
      return m_object;
    }
    
    public Map<T, T> getPredecessors() {
      return m_predecessors;
    }
    
    /**
     * 
     * @return true if this predecessor was found and removed
     */
    public boolean removePredecessor(T o) {
      boolean result = false;
      
//      dump();
      T pred = m_predecessors.get(o);
      if (null != pred) {
        result = null != m_predecessors.remove(o);
        if (result) {
          ppp("  REMOVED PRED " + o + " FROM NODE " + m_object);
        }
        else {
          ppp("  FAILED TO REMOVE PRED " + o + " FROM NODE " + m_object);        
        }
      }
      
      return result;
    }
    
    private void dump() {
      ppp(toString());
    }
    
    @Override
    public String toString() {
      StringBuffer sb = new StringBuffer("[Node:" + m_object);
      sb.append("  pred:");
      for (T o : m_predecessors.values()) {
        sb.append(" " + o.toString());
      }
      sb.append("]");
      String result = sb.toString();
      return result;
    }
    
    public void addPredecessor(T tm) {
      ppp("  ADDING PREDECESSOR FOR " + m_object + " ==> " + tm);
      m_predecessors.put(tm, tm);
    }
    
    public boolean hasPredecessors() {
      return m_predecessors.size() > 0;
    }
    
    public boolean hasPredecessor(T m) {
      return m_predecessors.containsKey(m);
    }

    public int compareTo(Node<T> o) {
      return getObject().toString().compareTo(o.getObject().toString());
    }
  } 
  
  //
  // class Node
  /////
  
  public static void main(String[] argv) {
    Graph<String> g = new Graph<String>();
    g.addNode("3");
    g.addNode("1");
    g.addNode("2.2");
    g.addNode("independent");
    g.addNode("2.1");
    g.addNode("2.3");
    
    // 1 ->  2.1, 2.2, 2.3 --> 3
    g.addPredecessor("3", "2.1");
    g.addPredecessor("3", "2.1");
    g.addPredecessor("3", "2.3");
    g.addPredecessor("2.1", "1");
    g.addPredecessor("2.2", "1");
    g.addPredecessor("2.3", "1");
    
    g.topologicalSort();
    
    List<String> l = g.getStrictlySortedNodes();
    for (String s : l) {
      System.out.println("  " + s);
    }
    int i = 0;
    assert "1".equals(l.get(i));
    i++;
    assert "2.1".equals(l.get(i)) || "2.1".equals(l.get(i)) || "2.2".equals(l.get(i));
    i++;
    assert "2.1".equals(l.get(i)) || "2.1".equals(l.get(i)) || "2.2".equals(l.get(i));
    i++;
    assert "2.1".equals(l.get(i)) || "2.1".equals(l.get(i)) || "2.2".equals(l.get(i));
    i++;
    assert "3".equals(l.get(i));
    
    assert 1 == g.getIndependentNodes().size();
    
    //
    // Test findPredecessors
    //
    ppp("GRAPH:" + g);
    {
      List<String> predecessors = g.findPredecessors("2.1");
      assert predecessors.size() == 1;
      assert predecessors.get(0).equals("1");
    }
    
    {
      List<String> predecessors = g.findPredecessors("3");
      assert predecessors.size() == 4;
      assert predecessors.get(0).equals("1");
      assert predecessors.get(1).equals("2.1") ||
        predecessors.get(2).equals("2.2") ||
        predecessors.get(2).equals("2");
      assert predecessors.get(2).equals("2.1") ||
      predecessors.get(2).equals("2.2") ||
      predecessors.get(2).equals("2");
      assert predecessors.get(3).equals("2.1") ||
      predecessors.get(2).equals("2.2") ||
      predecessors.get(2).equals("2");
    }

    ppp("TESTS PASSED");
  }
  
}
