// Copyright 2008-2010 by Mary McGlohon
// Carnegie Mellon University
// mmcgloho@cs.cmu.edu

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;

// A near-tree representing a conversation of e.g. blog links, Twitter, etc.
// Uses a modified version of tree isomorphisms to compute whether
// two cascades are equal.
public class Cascade{
    private boolean useAuthors;
    private ArrayList<CascadeNode> nodes;
    private ArrayList<CascadeNode> roots;
    
    public Cascade() {
        this.nodes = new ArrayList<CascadeNode>();
        this.roots = new ArrayList<CascadeNode>();
        this.useAuthors = false;
    }

    // Make a cascade with in format :
    // rootid:rootauthor:roottime -1 src:srcauthor:srctime dest ...
    // Note that root does not have to be first.
    // If author and time are absent they are assigned to be 0
    // If there is any problem in formatting null cascade is returned.
    // Note: we dont check for cycles, because that would take forever.
    public Cascade(String line, boolean useAuthors) {
        this.nodes = new ArrayList<CascadeNode>();
        this.useAuthors = useAuthors;
        this.roots = new ArrayList<CascadeNode>();
	String[] parts = line.trim().split(" ");
	if (parts.length>1) {
            if (parts.length % 2 == 1) {
                return;
            }
	    for (int i = 0; i < parts.length; i=i+2) {
		String fromnode = parts[i];
		String tonode = parts[i+1];
		addEdge(fromnode, tonode);
	    }
	    setRoots();
	}
    }

    public boolean useAuthors() {
        return this.useAuthors;
    }

    public void setUseAuthors(boolean use) {
        this.useAuthors = use;
    }

    // It makes calcs much easier if they can access nodes and roots
    ArrayList<CascadeNode> getNodes() {
        return this.nodes;
    }

    ArrayList<CascadeNode> getRoots() {
        return this.roots;
    }

    CascadeNode getNode(long id) {
	for (int i = 0; i < this.nodes.size(); i++) {
	    CascadeNode curr = this.nodes.get(i);
	    if (curr.id==id) {
		return curr;
	    }
	}
	return null;
    }


    // Some simple calcs that are handy to have.    
    public int size() {
	return this.nodes.size();
    }

    public int depth() {
        int depth = 0;
        HashSet<CascadeNode> alreadySeen = new HashSet<CascadeNode>();
        if (this.nodes.size() > 0) {
            ArrayList<CascadeNode> currlevel = new ArrayList<CascadeNode>();
            currlevel.addAll(this.roots);
            while (currlevel.size() > 0) {
                ++depth;
                ArrayList<CascadeNode> nextlevel = new ArrayList<CascadeNode>();
                for (int i = 0; i < currlevel.size(); ++i) {
                    nextlevel.addAll(currlevel.get(i).children);
                }
                currlevel= new ArrayList<CascadeNode>
                    (new HashSet<CascadeNode>(nextlevel));  
                currlevel.removeAll(alreadySeen);
            }
        }
        return depth;
    }

    public int authorCount() {
        HashSet<Integer> uniqueAuthors = new HashSet<Integer>();
        for (int i = 0; i < nodes.size(); ++i) {
            if (nodes.get(i).author > 0)
                uniqueAuthors.add(nodes.get(i).author);
        }
        return uniqueAuthors.size();
    }


    // Get the String that would form a cascade like this.
    public String edgeString() {
	String mystring = "";
	if (nodes.size()==1) {
	    CascadeNode cn = nodes.get(0);
            mystring = mystring + cn.id+ ":"+cn.author+":"+cn.time+" -1 ";
        }
	else {
	    for (int i = 0; i < nodes.size(); i++) {
		CascadeNode cn = nodes.get(i);
		if (!cn.isOrphan()) {
		    for (int p = 0; p < cn.parents.size(); p++) {
			long parentid = cn.parents.get(p).id;
                        mystring = mystring + cn.id + ":" + cn.author + ":" +
                            cn.time + " " + parentid + " ";
		    }
		}
                else
                    mystring = mystring + cn.id + ":" + cn.author + ":" +
                        cn.time + " -1 ";
	    }
	}
	return mystring;
    }

    public Cascade copy() {
        return new Cascade(this.edgeString(), this.useAuthors);
    }


    // Remaps according to the ID order. (1 to n)
    public void remapByIds() {
        ArrayList<Long> sortedIds = new ArrayList<Long>();
        for (int i = 0; i < nodes.size(); ++i) {
            sortedIds.add(nodes.get(i).id);
        }
        Collections.sort(sortedIds);
        for (int i = 0; i < sortedIds.size(); ++i) {
            getNode(sortedIds.get(i)).id = i+1;
        }
    }

    public void remapAuthors() {
        if (this.useAuthors) {
            HashMap<Integer,Integer> aidmap = new HashMap<Integer,Integer>();
            int currid = 0;
            for (int i = 0; i < nodes.size(); ++i) {
                if (!aidmap.containsKey(nodes.get(i).author)) {
                    aidmap.put(nodes.get(i).author,++currid);
                }
            }
            for (int i = 0; i < nodes.size(); ++i) {
                int newauthid = aidmap.get(nodes.get(i).author);
                nodes.get(i).author = newauthid;
            }
        }
    }
                                                
    //TODO:marymc  Make it so it works.
    // Uses BFS to put it in a nicer format.
    public void remapByBFS() {
	HashMap<Long,Integer> idmap = new HashMap<Long,Integer>();
	int currid = 0;	
	ArrayList<CascadeNode> nodequeue = new ArrayList<CascadeNode>();
	nodequeue.addAll(this.roots);
	while(nodequeue.size() > 0) {
	    CascadeNode currnode = nodequeue.get(0);
	    if (!idmap.containsKey(currnode.id)) {
		idmap.put(currnode.id,++currid);
		ArrayList<CascadeNode> currchildren = currnode.children;
		for (int i = 0; i < currchildren.size(); ++i) {
		    nodequeue.add(currchildren.get(i));
		}
	    }
	    nodequeue.remove(0);
	}
	for (int i = 0; i < nodes.size(); i++) {
		int newid = idmap.get(nodes.get(i).id);
		nodes.get(i).id=newid;
	}
        remapAuthors();
  
    }

    // *************** BUILDING METHODS *************//

    // Makes a node according to the id:auth:time format
    // where auth and time are optional.
    private CascadeNode makeNode(String nodestring) {
	nodestring = nodestring.trim();
        // split for possible author and timestamp
        String[] parts = nodestring.split(":");
        long id = new Long(parts[0]);

        // See if node is already in cascade
	CascadeNode mynode = getNode(id);
	if (mynode == null) {
            mynode = new CascadeNode(id);
        }

        // Note that this will reset the node's author to the given one,
        // in case it had already been assigned 0.
        if (parts.length > 1 && this.useAuthors) {
            int author = new Integer(parts[1]);
            mynode.setAuthor(author);
        }
        if (parts.length > 2) {
            long time = new Long(parts[2]);
            mynode.setTime(time);
        }
        return  mynode;
    }
    
    
    private void addEdge(String fromnodestring, String tonodestring) {
	CascadeNode fromnode;
	CascadeNode tonode;
	CascadeNode tempfrom = makeNode(fromnodestring);
	CascadeNode tempto = makeNode(tonodestring);
	
	long fromid = tempfrom.id;
	long toid = tempto.id;
	fromnode = getNode(fromid);
	if (fromnode==null) {
	    nodes.add(tempfrom);
	    fromnode=tempfrom;
	}
        if (tempto.id != -1) {
            tonode = getNode(toid);
            if (tonode==null) {
                nodes.add(tempto);
                tonode=tempto;
            }
            if (fromid != toid && tonode.id != -1) {
                fromnode.setParent(tonode);
                tonode.addChild(fromnode);
            }
        }
    }

    private void setRoots() {
	int rootindex = -1;
	int roots = 0;
	for (int i = 0; i < this.nodes.size(); i++) {
	    if (nodes.get(i).isOrphan()) {
                this.roots.add(nodes.get(i));
	    }
	}
    }


    //******** STATIC CASCADE ISOMORPHISM METHODS ******************//

    /* say whether two cascades (rooted trees) are isomorphic
       modified to allow for nodes to have multiple parents 
        (possibly in different levels)
       Modifications include:
         - Label nodes by level: l1v0, for instance
         - Make sure no repeats of nodes inside level and between levels-- we want each one at lowest possible level.
    */
    // TODO(mmcgloho): Refactor this to make Cascade class smaller.

    public static boolean isomorphic(Cascade c1, Cascade c2) {
        // Things will break if we don't first check to see if the references
        // are the same.  Note equals is isomorphic. :-D
        if (c1 == c2)
            return true;
        
        //obviously if diff number of nodes, not isomorphic
	if (c1.nodes.size() != c2.nodes.size())
	    return false;
	
        // Condition: same number of nodes.
        // If there are zero nodes there's nothing to check.
        if (c1.nodes.size() < 2)
            return true;
        
	// First we're going to want to clear out all pre-made labels
	for (int i = 0; i < c1.nodes.size(); i++) {
	    c1.nodes.get(i).label=" ";
	    c2.nodes.get(i).label=" ";
	}
        
	// Now get all levels and check if the level counts match.
	ArrayList<ArrayList<CascadeNode>> allLevels1 = 
	    putNodesIntoLevels(c1);
	ArrayList<ArrayList<CascadeNode>> allLevels2 = 
	    putNodesIntoLevels(c2);

	if (!equalLevels(allLevels1, allLevels2)) {
	    return false;
	}

	// Condition: allLevels 1 and 2 contain each level of nodes.
	// Each node appears only on one level.    
        // Starting with lowest level, relabel the nodes.
        int level = 0;
	while (allLevels1.size() > 0 && allLevels2.size() > 0) {
	    ArrayList<CascadeNode> currLevel1 = 
                allLevels1.remove(allLevels1.size()-1);
	    ArrayList<CascadeNode> currLevel2 = 
                allLevels2.remove(allLevels2.size()-1);

            // Label nodes in each level by their children.
	    longLabelLevel(currLevel1);
	    longLabelLevel(currLevel2);
	    
	    // Now check if each node in the level has match in other cascade
            // Also relabel the levels, if so.
	    boolean labelCheck = relabelLevel(currLevel1, currLevel2, level);
	    if (!labelCheck)
		return false;
            ++level;
	}
	
        // Now check on authors, if specified.
        if (c1.useAuthors || c2.useAuthors) {
            boolean authorCheck = checkAuthorLabels(c1, c2);
            if (!authorCheck)
                return false;
        }
	// Now we should be done.  If we're here we are isomorphic. 
	return true;
    }


    private static ArrayList<ArrayList<CascadeNode>> 
        putNodesIntoLevels (Cascade c) {
	ArrayList<ArrayList<CascadeNode>> levels = new ArrayList<ArrayList<CascadeNode>>();
       
	ArrayList<CascadeNode> currLevel = new ArrayList<CascadeNode>();
	ArrayList<CascadeNode> children = new ArrayList<CascadeNode>();
	children.addAll(c.roots);
	
	// Add all children, BFS.  Will end up with repeats.
	while (children.size() > 0) {
	    levels.add(children);
	    currLevel = children;
	    children = new ArrayList<CascadeNode>();
	    for (int i = 0; i < currLevel.size(); ++i) {
		for (int j = 0; j < currLevel.get(i).children.size(); ++j) {
		    children.add(currLevel.get(i).children.get(j));
		}
	    }	 
	}
	// Now remove repeats by going bottom-up.
        // Note that putting nodes on the lowest possible level is necessary
	HashSet<CascadeNode> placed = new HashSet<CascadeNode>();

	for (int l = levels.size()-1; l>=0; l--) {
	    currLevel = levels.get(l);
	    ArrayList<CascadeNode> newLevel = new ArrayList<CascadeNode>();

	    // Go through all nodes and add those not already placed
	    for (int i = 0; i < currLevel.size(); i++) {
		CascadeNode cn = currLevel.get(i);
		if (!placed.contains(cn)) {
		    newLevel.add(cn);
		    placed.add(cn);
		}
	    }
	    
	    // See if we need to replace level with new one
	    if (currLevel.size()!=newLevel.size()) {
		levels.remove(l);
		levels.add(l,newLevel);
	    }
            
	    //just to be safe, count nodes.
	    //int oldsize=c.size();
	    //int newcount = 0;
	    //for (int lev = 0; lev < levels.size(); lev++) {
            //	newcount+=levels.get(lev).size();
	    //}
	    //assert(oldsize==newcount);
	} 
	return levels;
    }

    // See if the number of nodes per level matches.
    private static boolean equalLevels
        (ArrayList<ArrayList<CascadeNode>> levels1,
         ArrayList<ArrayList<CascadeNode>> levels2) {
	// Check number of levels
	if (levels1.size() != levels2.size())
	    return false;
	// Check count of nodes in each level
	for (int l = 0; l < levels1.size(); ++l) {
	    if (levels1.get(l).size() != levels2.get(l).size()) {
		return false;
	    }
	}
	// All OK
	return true;
    }

    // Label a level with its children
    private static void longLabelLevel(ArrayList<CascadeNode> currLevel) {
	for (int i = 0; i < currLevel.size(); i++) {
            // For each node, get the labels of its children and sort them.
            // The label for a node is its children.
	    CascadeNode curr = currLevel.get(i);
	    String[] labels = new String[curr.children.size()];
	    for (int j = 0; j < curr.children.size(); j++)
		labels[j]= curr.children.get(j).label;
	    java.util.Arrays.sort(labels);
	    for (int j = 0; j < labels.length; j++)
		curr.label=curr.label+labels[j];   
	}
    }

    // Checking along the way to make sure each node in a level has a match,
    // relabel for brevity.
    private static boolean relabelLevel
        (ArrayList<CascadeNode> currLevel1, 
         ArrayList<CascadeNode> currLevel2,
         int level) {
	String[] labels1 = new String[currLevel1.size()];
	for (int i = 0; i < labels1.length; i++) {
	    labels1[i] = currLevel1.get(i).label;
	}
	String[] labels2 = new String[currLevel2.size()];
	for (int i = 0; i < labels2.length; i++) {
	    labels2[i] = currLevel2.get(i).label;
	}
	
        // Sorting them is the easiest way to check item-by-item,
        // and will give us consistent labels.
	java.util.Arrays.sort(labels1);
	java.util.Arrays.sort(labels2);
	

	// Re-label nodes, checking to see if they've got the same ones.
	int nindex = 0;
	String prevlabel=" ";
	HashMap<String,Integer> labelIndex = new HashMap<String,Integer>();
	labelIndex.put(prevlabel,nindex);
	for (int i = 0; i < labels1.length; ++i) {
	    String l1 = labels1[i];
	    String l2 = labels2[i];
	    if (l1.compareTo(l2)!=0)
		return false;
            // Two nodes may be isomorphic in the tree.
	    if (l1.compareTo(prevlabel)!=0) {
		labelIndex.put(l1,++nindex);
		prevlabel=l1;
	    }
	}
        // Condition: levels are isomorphic.
        // Label each node with level and index in level.
	for (int i = 0; i < currLevel1.size(); i++) {
	    int myindex = labelIndex.get(currLevel1.get(i).label);
	    currLevel1.get(i).label="l"+level+"v"+myindex;
	}
	for (int i = 0; i < currLevel2.size(); i++) {
	    int myindex = labelIndex.get(currLevel2.get(i).label);
	    currLevel2.get(i).label="l"+level+"v"+myindex;
	}
	return true;
    }

    // Will check if the authors are isomorphic.
    private static boolean checkAuthorLabels(Cascade c1, Cascade c2) {
        // If isomorphic cascades are also isomorphic by author,
        // then we should be able to make a table of authors with their nodes
        // and each author entry should have a match in the other cascade.

        // Assign each author a list of nodes.
	HashMap<Integer,ArrayList<String>> authorlabels1 = 
            new HashMap<Integer,ArrayList<String>>();
	HashMap<Integer,ArrayList<String>> authorlabels2 = 
            new HashMap<Integer,ArrayList<String>>();
	for (int i = 0; i < c1.nodes.size(); i++) {
	    CascadeNode cn1 = c1.nodes.get(i);
	    if (!authorlabels1.containsKey(cn1.author)) {
		authorlabels1.put(cn1.author,new ArrayList<String>());
	    }
	    authorlabels1.get(cn1.author).add(cn1.label);
	    
	    CascadeNode cn2 = c2.nodes.get(i);
	    if (!authorlabels2.containsKey(cn2.author)) {
		authorlabels2.put(cn2.author,new ArrayList<String>());
	    }
	    authorlabels2.get(cn2.author).add(cn2.label);
	}

	// First off, we should have the same number of authors.
	if (authorlabels1.size()!=authorlabels2.size())
	    return false;

        // Again, for convenience, sort the labels.
	Iterator<Integer> authorkeys1 = authorlabels1.keySet().iterator();
	while (authorkeys1.hasNext()) {
	    int key = authorkeys1.next();
            Collections.sort(authorlabels1.get(key));
	}
	Iterator<Integer> authorkeys2 = authorlabels2.keySet().iterator();
	while (authorkeys2.hasNext()) {
	    int key = authorkeys2.next();
	    Collections.sort(authorlabels2.get(key));
        }


	//now we should have a mapping between authorlabels1 and authorlabels2
        // TODO(mmcgloho): More efficient?
	authorkeys1 = authorlabels1.keySet().iterator();
	while (authorkeys1.hasNext()) {
	    int key = authorkeys1.next();
	    ArrayList<String> nodelabels1 = authorlabels1.get(key);
	    boolean found = false;
	    int foundindex = -1;
	    //iterate through authorlabels2, and if found remove
	    authorkeys2 = authorlabels2.keySet().iterator();
	    search:
	    while (authorkeys2.hasNext()) {
		int key2 = authorkeys2.next();
		ArrayList<String> nodelabels2 = authorlabels2.get(key2);
		if (nodelabels1.size() == nodelabels2.size()) {
		    // check if each label matches.
                    boolean match = true;
                    for (int i = 0; i < nodelabels1.size(); i++) {
			if (!nodelabels1.get(i).equals(nodelabels2.get(i))) {
			    match=false;
			    break;
			}
		    }
		    if (match) {
			// found!
			found=true;
			foundindex = key2;
			break search;
		    }
		    // not found, go on to next one.
		}
		//if diff length go on to next one
	    }
	    if (found) {
		//remove from authorlabels2 and go to next search
		assert(foundindex>-1);
		authorlabels2.remove(foundindex);
	    }
	    else
		return false;
	}
	//assert(authorlabels2.size()==0);
	return true;
    }


}


// ************ CascadeNode class ******x

// Was inner class, but accessing the nodes directly is useful for calculations.
// TODO(mmcgloho): work on accessors to keep someone from screwing up a cascade
class CascadeNode {
    long id;    
    ArrayList<CascadeNode> parents; //allows mutiple parents
    ArrayList<CascadeNode> children;
    int author = 0;
    long time = 0;
    
    // Bookkeeping for isomorphisms
    int level = 0;
    int leafnum = 0;
    String label = " ";
    
    CascadeNode(long id) {
	this.id = id;
	this.parents = new ArrayList<CascadeNode>();
	this.children = new ArrayList<CascadeNode>();
    }	
    long getID() {
	return id;
    }
    void setAuthor(int author) {
        this.author = author;
    }
    void setTime(long time) {
        this.time=time;
    }
    void setParent(CascadeNode parent) {
	this.parents.add(parent);
    }
    ArrayList<CascadeNode> getParents() {
        // if we're only working with one
        return this.parents;
    }
    CascadeNode getParent() {
        // if we're only working with one
        return this.parents.get(0);
    }
     void addChild(CascadeNode child) {
         this.children.add(child);
     }
    boolean isOrphan() {
	return parents.size()==0;
    }
    boolean isLeaf() {
	return children.size()==0;
    }
    int degree() {
        return children.size();
    }
}