/* Class to draw the revision graph.
 *
 * Changes Copyright (C) 2010, 2011 Darrell Karbott (see below).
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public
 * License as published by the Free Software Foundation; either
 * version 2.0 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 *
 * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks (changes)
 *
 *  This file was developed as component of
 * "fniki" (a wiki implementation running over Freenet).
 *
 * This file is a derived work based on the graphlog.py
 * from rev:643b8212813e in the mercurial hg-stable repo.
 *
 * sha1sum graphlog.py
 * a0e599d4bd0b483025f0b5f182e737d8682a88ba  graphlog.py
 *
 * Original header:
 * # ASCII graph log extension for Mercurial
 * #
 * # Copyright 2007 Joel Rosdahl <joel@rosdahl.net>
 * #
 * # This software may be used and distributed according to the terms of the
 * # GNU General Public License version 2 or any later version.
 *
 */

package fniki.wiki;

import java.io.FileReader;
import java.io.IOException;
import java.io.LineNumberReader;
import java.io.OutputStreamWriter;
import java.io.Writer;

import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class GraphLog {

    ////////////////////////////////////////////////////////////
    // A quick and dirty port of the first ~= 200 lines of
    // graphlog.py from the mercurial codebase.
    ////////////////////////////////////////////////////////////

    public static ColumnData asciiedges(List<Integer> seen, int rev, List<Integer> parents) {
        // """adds edge info to changelog DAG walk suitable for ascii()"""
        if (!seen.contains(rev)) {
            seen.add(rev);
        }

        int nodeidx = seen.indexOf(rev); // i.e. just looking up the index of the rev

        // seen maps revs -> ordinals

        List<Integer> known_parents = new ArrayList<Integer>();
        List<Integer> new_parents = new ArrayList<Integer>();

        for (int parent : parents) {
            if (seen.contains(parent)) {
                known_parents.add(parent);
            } else {
                new_parents.add(parent);
            }
        }

        int ncols = seen.size();
        seen.remove(nodeidx); // Correct? trying to get python slice assignement semantics
        seen.addAll(nodeidx, new_parents);

        List<AsciiEdge> edges = new ArrayList<AsciiEdge>();
        for (int parent : known_parents) {
            edges.add(new AsciiEdge(nodeidx, seen.indexOf(parent)));
        }

        if (new_parents.size() > 0) {
            edges.add(new AsciiEdge(nodeidx, nodeidx));
        }

        if (new_parents.size() > 1) {
            edges.add(new AsciiEdge(nodeidx, nodeidx + 1));
        }

        int nmorecols = seen.size() - ncols;
        String edgeDump = "";
        for (AsciiEdge edge : edges) {
            edgeDump += String.format("(%d, %d), ", edge.mStart, edge.mEnd);
        }
        return new ColumnData(nodeidx, edges, ncols, nmorecols);
    }

    static void fix_long_right_edges(List<AsciiEdge> edges) {
        int index = 0;
        for (AsciiEdge edge : edges) {
            if (edge.mEnd > edge.mStart) {
                edges.set(index,  new AsciiEdge(edge.mStart, edge.mEnd + 1));
            }
            index++;
        }
    }

    static List<String> get_nodeline_edges_tail(int node_index,
                                             int p_node_index,
                                             int n_columns,
                                             int n_columns_diff,
                                             int p_diff,
                                             boolean fix_tail) {
        if (fix_tail && n_columns_diff == p_diff && n_columns_diff != 0) {
            //# Still going in the same non-vertical direction.
            if (n_columns_diff == -1) {
                int start = Math.max(node_index + 1, p_node_index);
                List<String> tail = repeat(BAR_SPACE,  (start - node_index - 1));
                tail.addAll(repeat(SLASH_SPACE, (n_columns - start)));
                return tail;
            } else {
                return repeat(BACKSLASH_SPACE, (n_columns - node_index - 1));
            }
        } else {
            return repeat(BAR_SPACE, (n_columns - node_index - 1));
        }
    }

    static void draw_edges(List<AsciiEdge> edges, List<String> nodeline, List<String> interline) {
        int index = 0;
        for (AsciiEdge edge : edges) {
            if (edge.mStart == edge.mEnd + 1) {
                interline.set(2 * edge.mEnd + 1, "/");
            } else if (edge.mStart == edge.mEnd - 1) {
                interline.set(2 * edge.mStart + 1, "\\");
            } else if (edge.mStart == edge.mEnd) {
                interline.set(2 * edge.mStart, "|");
            } else {
                nodeline.set(2 * edge.mEnd, "+");
                if (edge.mStart > edge.mEnd) {
                    edges.set(index, new AsciiEdge(edge.mEnd, edge.mStart));
                }

                for (int j =  2 * edge.mStart + 1; j < 2 * edge.mEnd; j++) {
                    if (!nodeline.get(j).equals("+")) {
                        nodeline.set(j, "-");
                    }
                }
            }

            index++;
        }
    }

    static List<String> get_padding_line(int ni, int n_columns, List<AsciiEdge> edges) {
        List<String> line = new ArrayList<String>();
        line.addAll(repeat(BAR_SPACE, ni));

        String c = null;
        //if (ni, ni - 1) in edges or (ni, ni) in edges:
        if (edges.contains(new AsciiEdge(ni, ni - 1)) ||
            edges.contains(new AsciiEdge(ni, ni))) {
            //# (ni, ni - 1)      (ni, ni)
            //# | | | |           | | | |
            //# +---o |           | o---+
            //# | | c |           | c | |
            //# | |/ /            | |/ /
            //# | | |             | | |
            c = "|";
        } else {
            c = " ";
        }
        line.add(c);
        line.add(" ");

        line.addAll(repeat(BAR_SPACE, (n_columns - ni - 1)));
        return line;
    }

    public static AsciiState asciistate() {
        //"""returns the initial value for the "state" argument to ascii()"""
        return new AsciiState(0, 0);
    }

    static boolean  should_add_padding_line(List<String> text, int coldiff, List<AsciiEdge> edges) {
        //add_padding_line = (len(text) > 2 and coldiff == -1 and
        //                [x for (x, y) in edges if x + 1 < y])
        if (!(text.size() > 2 && coldiff == -1)) {
            return false;
        }
        for (AsciiEdge edge : edges) {
            if (edge.mStart + 1 < edge.mEnd) {
                return true;
            }
        }
        return false;
    }

    // # Called once for each entry in the dag list
    public static void ascii(Writer ui, AsciiState state, /* type, */
                      String nodeChar, List<String> text,
                      ColumnData coldata) throws IOException {
        //"""prints an ASCII graph of the DAG
        //
        //takes the following arguments (one call per node in the graph):

        //- ui to write to
        //- Somewhere to keep the needed state in (init to asciistate())
        //- Column of the current node in the set of ongoing edges.
        //- Type indicator of node data == ASCIIDATA.
        //- Payload: (char, lines):
        //- Character to use as node's symbol.
        //- List of lines to display as the node's text.
        //- Edges; a list of (col, next_col) indicating the edges between
        //  the current node and its parents.
        //- Number of columns (ongoing edges) in the current revision.
        //- The difference between the number of columns (ongoing edges)
        //  in the next revision and the number of columns (ongoing edges)
        //  in the current revision. That is: -1 means one column removed;
        //  0 means no columns added or removed; 1 means one column added.
        // """

        int idx = coldata.mNodeIdx;
        List<AsciiEdge> edges = coldata.mEdges;
        int ncols = coldata.mNCols;
        int coldiff = coldata.mMoreCols;

        assert_(coldiff > -2  && coldiff < 2);

        if (coldiff == -1) {
            //# Transform
            //#
            //#     | | |        | | |
            //#     o | |  into  o---+
            //#     |X /         |/ /
            //#     | |          | |
            fix_long_right_edges(edges);
        }

        //# add_padding_line says whether to rewrite
        //#
        //#     | | | |        | | | |
        //#     | o---+  into  | o---+
        //#     |  / /         |   | |  # <--- padding line
        //#     o | |          |  / /
        //#                    o | |
        //add_padding_line = (len(text) > 2 and coldiff == -1 and
        //                [x for (x, y) in edges if x + 1 < y])

        boolean add_padding_line = should_add_padding_line(text, coldiff, edges);

        //# fix_nodeline_tail says whether to rewrite
        //#
        //#     | | o | |        | | o | |
        //#     | | |/ /         | | |/ /
        //#     | o | |    into  | o / /   # <--- fixed nodeline tail
        //#     | |/ /           | |/ /
        //#     o | |            o | |
        boolean fix_nodeline_tail = (text.size() <= 2) && (!add_padding_line);

        // # nodeline is the line containing the node character (typically o)
        List<String> nodeline = repeat(BAR_SPACE, idx);
        nodeline.add(nodeChar);
        nodeline.add(" ");

        nodeline.addAll(get_nodeline_edges_tail(idx, state.mIdx, ncols, coldiff,
                                                state.mColDiff, fix_nodeline_tail));

        //# shift_interline is the line containing the non-vertical
        //# edges between this entry and the next
        List<String> shift_interline = repeat(BAR_SPACE, idx);
        String edge_ch = null;
        int n_spaces = -1;
        if (coldiff == -1) {
            n_spaces = 1;
            edge_ch = "/";
        } else if (coldiff == 0) {
            n_spaces = 2;
            edge_ch = "|";
        } else {
            n_spaces = 3;
            edge_ch = "\\";
        }

        shift_interline.addAll(repeat(ONE_SPACE, n_spaces));
        List<String> valueToRepeat = new ArrayList<String>();
        valueToRepeat.add(edge_ch);
        valueToRepeat.add(" ");
        shift_interline.addAll(repeat(valueToRepeat, (ncols - idx - 1)));

        //# draw edges from the current node to its parents
        draw_edges(edges, nodeline, shift_interline);

        //# lines is the list of all graph lines to print
        List<String> lines = new ArrayList<String>(Arrays.asList(list_to_string(nodeline)));
        if (add_padding_line) {
            lines.add(list_to_string(get_padding_line(idx, ncols, edges)));
        }
        lines.add(list_to_string(shift_interline));

        //# make sure that there are as many graph lines as there are
        //# log strings
        while (text.size() < lines.size()) {
            text.add("");
        }

        if (lines.size() < text.size()) {
            String extra_interline = list_to_string(repeat(BAR_SPACE, (ncols + coldiff)));
            while (lines.size() < text.size()) {
                lines.add(extra_interline);
            }
        }
        //# print lines
        int indentation_level = Math.max(ncols, ncols + coldiff);
        assert_(lines.size() == text.size());
        int index = 0;
        for (String line : lines) {
            String logstr = text.get(index);
            index++;
            // "%-*s => means "left justify string with width specified in the tuple"
            // ln = "%-*s %s" % (2 * indentation_level, "".join(line), logstr)
            String fmt = "%-" + ("" + (2 * indentation_level)).trim() + "s %s";
            ui.write(rstrip(String.format(fmt, line, logstr)));
            ui.write("\n");
        }

        state.mColDiff = coldiff;
        state.mIdx = idx;
    }

    ////////////////////////////////////////////////////////////
    // Helper data structures and functions used to port
    // to Java.
    ////////////////////////////////////////////////////////////
    static final class AsciiEdge {
        public final int mStart;
        public final int mEnd;
        AsciiEdge(int start, int end) { mStart = start; mEnd = end; }
    }

    public static final class ColumnData {
        public final int mNodeIdx;
        public final int mNCols;
        public final List<AsciiEdge> mEdges;
        public final int mMoreCols;

        public ColumnData(int nodeidx, List<AsciiEdge> edges, int ncols, int nmorecols) {
            mNodeIdx = nodeidx;
            mEdges = edges;
            mNCols = ncols;
            mMoreCols = nmorecols;
        }
    }

    // NOT immutable. Hrmmm..
    public static class AsciiState {
        public int mColDiff;
        public int mIdx;
        public AsciiState(int coldiff, int idx) { mColDiff = coldiff; mIdx = idx; }
    }

    // A directed graph node.
    public static class DAGNode {
        public final String mTag;
        public final int mId;
        public final List<Integer> mParentIds;
        DAGNode(String tag, int id, List<Integer> parentIds) {
            mTag = tag;
            mId = id;
            mParentIds = parentIds;
        }

        public String asString() {
            return "{" + mTag + ", " + mId + ", " + GraphLog.prettyString_(mParentIds) + "}";
        }
    }

    final static List<String> BAR_SPACE =
        Collections.unmodifiableList(Arrays.asList("|", " "));

    final static List<String> SLASH_SPACE =
        Collections.unmodifiableList(Arrays.asList("/", " "));

    final static List<String> BACKSLASH_SPACE =
        Collections.unmodifiableList(Arrays.asList("\\", " "));

    final static List<String> ONE_SPACE =
        Collections.unmodifiableList(Arrays.asList(" "));

    static List<String> repeat(List<String> value, int count) {
        List<String> ret = new ArrayList<String>();
        while (count > 0) {
            ret.addAll(value);
            count--;
        }
        return ret;
    }

    static void assert_(boolean condition) {
        if (condition) { return; }
        throw new RuntimeException("Assertion Failure!");
    }

    // PARANOID: Can get rid of the explicit  iteration later if this is too slow.
    static String list_to_string(List<String> values) {
        StringBuilder sb = new StringBuilder();
        for (String singleChar : values) {
            assert_(singleChar.length() == 1);
            sb.append(singleChar);
        }
        return sb.toString();
    }

    static String rstrip(String text) {
        if (text.length() == 0) {
            return "";
        }
        int pos = text.length() - 1;
        while (pos >= 0 && text.charAt(pos) == ' ') {
            pos--;
        }
        if (pos == text.length() - 1) {
            return text;
        }
        return text.substring(0, pos + 1);
    }

    ////////////////////////////////////////////////////////////
    // New code written on top of the ported mercurial stuff.
    ////////////////////////////////////////////////////////////

    public static class GraphEdge {
        public final String mFrom;
        public final String mTo;
        public GraphEdge(String from, String to) { mFrom = from; mTo = to; }
        public boolean equals(Object obj) {
            if (obj == null || (!(obj instanceof GraphEdge))) {
                return false;
            }
            GraphEdge other = (GraphEdge)obj;
            return mFrom.equals(other.mFrom) && mTo.equals(other.mTo);
        }
        public int hashCode() { return (mFrom + mTo).hashCode(); } // Hmmm... fast enough?
    }

    static class ParentInfo {
        public final Set<String> mAllParents;
        public final Set<String> mUnseenParents;
        public final Set<String> mRootIds;
        public ParentInfo(Set<String> allParents,
                          Set<String> unseenParents,
                          Set<String> rootIds) {
            mAllParents = allParents;
            mUnseenParents = unseenParents;
            mRootIds = rootIds;
        }
    }

    static class OrdinalMapper {
        private final Map<String, Integer> mLut = new HashMap<String, Integer>();
        private int mCount;

        public OrdinalMapper() {}

        public int ordinal(String for_value) {
            if (!mLut.keySet().contains(for_value)) {
                mLut.put(for_value, mCount);
                mCount++;
            }
            return mLut.get(for_value);
        }
        public void dump() {
            List<String> keys = new ArrayList<String>(mLut.keySet());
            Collections.sort(keys);
            for (String key : keys) {
                System.err.println(String.format("%s -> %d", key, mLut.get(key)));
            }
        }
    }

    static Set<String> allDescendants(Map<String, Set<String>> graph, String leaf_id) {
        Set<String> seen = new HashSet<String>();
        List<String> stack = new ArrayList<String>();
        stack.add(leaf_id);

        while (stack.size() > 0) {
            String edge_id = stack.remove(stack.size() - 1);
            if (seen.contains(edge_id)) {
                continue;
            }

            seen.add(edge_id);
            if (!graph.keySet().contains(edge_id)) {
                continue;
            }

            for (String parent : graph.get(edge_id)) {
                stack.add(parent);
            }
        }
        return seen; // # Hmmmm... includes leaf_id
    }

    static List<Map<String, Set<String>>> find_connected_subgraphs(Map<String, Set<String>> graph,
                                                                   Set<String> root_ids,
                                                                   Set<String> leaf_node_ids) {
        List<Set<String>> subgraph_id_sets = new ArrayList<Set<String>>();
        for (String leaf_id : leaf_node_ids) {
            subgraph_id_sets.add(allDescendants(graph, leaf_id));
        }

        //# Coalesce subgraphs which have common root nodes
        for(String key : root_ids) {
            Set<String> coalesced = new HashSet<String>();

            // # Deep copy the list so I can mutate while iterating
            List<Set<String>> copyForMutatingIteration = new ArrayList<Set<String>>();
            copyForMutatingIteration.addAll(subgraph_id_sets);
            for (Set<String> subgraph_id_set : copyForMutatingIteration) {
                if (!subgraph_id_set.contains(key)) {
                    continue;
                }

                coalesced.addAll(subgraph_id_set);
                subgraph_id_sets.remove(subgraph_id_set);
            }

            if (coalesced.size() > 0) {
                subgraph_id_sets.add(coalesced);
            }

            if (subgraph_id_sets.size() < 2) {
                break; //# Bail out if there is only one graph
            }
        }

        List<Map<String, Set<String>>> subgraphs = new ArrayList<Map<String, Set<String>>>();
        for (Set<String> subgraph_id_set : subgraph_id_sets) {
            Map<String, Set<String>>copy = new HashMap<String, Set<String>>();
            for (String id_value : subgraph_id_set) {
                if (graph.keySet().contains(id_value)) { // # handle unseen parents
                    Set<String> deepCopy = new HashSet<String>();
                    deepCopy.addAll(graph.get(id_value));
                    copy.put(id_value, deepCopy);
                }
            }
            //dumpGraph("subgraph", copy);
            subgraphs.add(copy);
        }
        return subgraphs;
    }

    // graph is a map of node identifiers -> set of parent nodes.
    public static Map<String, Set<String>> buildRawGraph(List<GraphEdge> edges) {
        Map<String, Set<String>> graph = new HashMap<String, Set<String>>();
        for (GraphEdge edge : edges) {
            Set<String> parents = graph.get(edge.mTo);
            if (parents == null) {
                parents = new HashSet<String> ();
                graph.put(edge.mTo, parents);
            }
            parents.add(edge.mFrom);
        }
        return graph;
    }

    // IMPORTANT: The "reversed" graph is what you need to traverse to build the DAG.
    // Take a graph which maps node ids -> set of parents and create a graph
    // which maps node ids -> set of children from it.
    static Map<String, Set<String>> get_reversed_graph(Map<String, Set<String>> graph,
                                                       Set<String> root_ids,
                                                       OrdinalMapper ordinals) {
        Map<String, Set<String>> reversed_graph = new HashMap<String, Set<String>>();

        for(Map.Entry<String, Set<String>> entry : graph.entrySet()) {
            String key = entry.getKey();
            Set<String> parents = entry.getValue();

            for (String parent : parents) {
                if (!graph.keySet().contains(parent)) {
                    continue;
                }

                if (!reversed_graph.keySet().contains(parent)) {
                    reversed_graph.put(parent, new HashSet<String>());
                }

                //# i.e. is a child of the parent
                reversed_graph.get(parent).add(key);
            }

            //# For leaf nodes.
            if (!reversed_graph.keySet().contains(key)) {
                reversed_graph.put(key, new HashSet<String>());
            }
        }
        return reversed_graph;
    }

    public static ParentInfo getParentInfo(Map<String, Set<String>> graph) {
        Set<String> allParents = new HashSet<String>();
        for (Set<String> parents : graph.values()) {
            allParents.addAll(parents);
        }

        Set<String> unseenParents = new HashSet<String>();
        unseenParents.addAll(allParents);
        unseenParents.removeAll(graph.keySet());

        Set<String> rootIds = new HashSet<String>();

        for(Map.Entry<String, Set<String>> entry : graph.entrySet()) {
            Set<String> parents = entry.getValue();
            if (parents == null || parents.size() == 0) {
                rootIds.add(entry.getKey());
                continue;
            }

            assert_(parents.size() > 0);

            Set<String> unknownParents =  new HashSet<String>();

            unknownParents.addAll(parents);
            unknownParents.removeAll(unseenParents);

            // BUG: ??? Revisit. what if only some parents are unknown?
            //# All parents are unknown
            if (unknownParents.size() == 0) {
                rootIds.add(entry.getKey());
            }
        }

        return new ParentInfo(allParents, unseenParents, rootIds);
    }

    // LATER: Grok harder. Possible to get rid of this?
    //# Breadth first traversal of the graph to force creation of ordinals
    //# in an order that won't freak out the drawing code.
    static void traverse_in_plausible_commit_order(Map<String, Set<String>> reversed_graph,
                                                   Set<String> root_ids,
                                                   OrdinalMapper ordinals) {

        LinkedList<String> queue = new LinkedList<String>();
        queue.addAll(root_ids);
        Collections.sort(queue);

        //# Force creation of ordinals for root nodes
        for (String id_value : queue) {
            ordinals.ordinal(id_value);
        }

        Set<String> seen = new HashSet<String>();
        while (queue.size() > 0) {
            String id_value = queue.removeFirst();
            if (seen.contains(id_value)) {
                continue;
            }
            seen.add(id_value);

            // Tricky: Sort to force creation of new ordinals in "natural order"
            //         of child ids.
            List<String> child_ids = new ArrayList<String>();
            child_ids.addAll(reversed_graph.get(id_value));
            Collections.sort(child_ids);

            for (String child_id : child_ids) {
                ordinals.ordinal(child_id);
                queue.add(child_id);
            }
        }
    }

    // Create a list of graph nodes in the correct order so that
    // they can be used to draw the graph with the ascii() function.
    static List<DAGNode> build_dag(Map<String, Set<String>> graph, String null_rev_name) {
        ParentInfo parentInfo = getParentInfo(graph);
        Set<String> all_parents = parentInfo.mAllParents;
        Set<String> unseen_parents = parentInfo.mUnseenParents;
        Set<String> root_ids = parentInfo.mRootIds;

        OrdinalMapper ordinals = new OrdinalMapper();

        Map<String, Set<String>> reversed_graph = get_reversed_graph(graph, root_ids, ordinals);

        //# Important: This sets the graph ordinals correctly.
        traverse_in_plausible_commit_order(reversed_graph, root_ids, ordinals);

        int null_rev_ordinal = ordinals.ordinal(null_rev_name);

        LinkedList<String> queue = new LinkedList<String>();
        queue.addAll(root_ids);
        Collections.sort(queue);

        List<DAGNode> dag = new ArrayList<DAGNode>();
        Set<String> seen = new HashSet<String>();
        while (queue.size() > 0) {
            String id_value = queue.removeFirst();
            if (seen.contains(id_value)) {
                continue;
            }
            seen.add(id_value);

            List<Integer> parents = new ArrayList<Integer>();
            for (String parent_id : graph.get(id_value)) {
                // This accesses ordinals in parent order, but the DAG requires them
                // to be in child order.  That's why we need the call to
                // traverse_in_plausible_commit_order above.
                parents.add(ordinals.ordinal(parent_id));
            }
            Collections.sort(parents);

            if (parents.size() == 1 && parents.get(0) == null_rev_ordinal) {
                // Causes ascii() not to draw a line down from the 'o'.
                parents = new ArrayList<Integer>();
            }

            dag.add(new DAGNode(id_value, ordinals.ordinal(id_value), parents));

            if (!reversed_graph.keySet().contains(id_value)) {
                continue;
            }

            // Tricky: Must traverse in order or the dag nodes won't be right.
            List<String> child_ids = new ArrayList<String>();
            child_ids.addAll(reversed_graph.get(id_value));
            Collections.sort(child_ids);

            for (String child_id : child_ids) {
                queue.add(child_id);
            }
        }

        Collections.reverse(dag);
        return dag;
    }

    // BUG: detect cylces?
    // REQUIRES: edges form an acyclic directed graph.
    public static List<List<DAGNode>> build_dags(List<GraphEdge> edges, String null_rev_name) {
        //""" Creates a list of DAG lists that can be printed with the ascii()
        //    method,  from an unordered list of edges. """

        Map<String, Set<String>> graph = buildRawGraph(edges);
        ParentInfo parentInfo = getParentInfo(graph);
        Set<String> all_parents = parentInfo.mAllParents;
        Set<String> root_ids = parentInfo.mRootIds;

        Set<String> leaf_node_ids = new HashSet<String>();
        leaf_node_ids.addAll(graph.keySet());
        leaf_node_ids.removeAll(all_parents);

        List<Map<String, Set<String>>> subgraphs = find_connected_subgraphs(graph,
                                                                            root_ids,
                                                                            leaf_node_ids);
        List<List<DAGNode>> dags = new ArrayList<List<DAGNode>>();
        for (Map<String, Set<String>> subgraph : subgraphs) {
            dags.add(build_dag(subgraph, null_rev_name));
        }
        return dags;
    }

    private final static String prettyString(Set<String> set) {
        List<String> list = new ArrayList<String>();
        list.addAll(set);
        Collections.sort(list);
        return prettyString(list);
    }

    private final static String prettyString(List<String> list) {
        String ret = "{";
        for (String value : list) {
            ret += value + ", ";
        }
        ret = ret.trim() + "}";
        return ret;
    }

    // GRRRR... Colliding type erasure. Java is a poor man's C++ :-(
    static String prettyString_(List<Integer> values) {
        List<String> list = new ArrayList<String>();
        for (Integer value : values) {
            list.add(value.toString());
        }
        return prettyString(list);
    }

    private final static void dumpGraph(String msg, Map<String, Set<String>> graph) {
        System.err.println("--- graph: " + msg);
        List<String> keys = new ArrayList<String>();
        keys.addAll(graph.keySet());
        Collections.sort(keys);

        for (String key : keys) {
            System.err.println(String.format("   [%s] -> [%s]", key, prettyString(graph.get(key))));
        }
    }
    // File format:
    // <child_rev> <parent_rev>
    // one entry per line, lines starting with # and // are ignored.
    private final static List<GraphEdge> readEdgesFromFile(String fileName) throws IOException {
        List<GraphEdge> list = new ArrayList<GraphEdge>();

        LineNumberReader reader = new LineNumberReader(new FileReader(fileName));
        String line = reader.readLine();

        while(line != null) {
            if (line.trim().equals("") || line.startsWith("#") || line.startsWith("//")) {
                line = reader.readLine();
                continue;
            }
            String[] fields = line.trim().split(" ");
            list.add(new GraphEdge(fields[1], fields[0]));
            line = reader.readLine();
        }
        return list;
    }

    public static void test_dumping_graph(List<DAGNode> dag) throws IOException {
        List<Integer> seen = new ArrayList<Integer>();
        AsciiState state = asciistate();
        Writer out = new OutputStreamWriter(System.out);
        for (DAGNode value : dag) {
            List<String> lines = new ArrayList<String>();
            lines.add(String.format("id: %s (%d)", value.mTag, value.mId));
            ascii(out, state, "o", lines, asciiedges(seen, value.mId, value.mParentIds));
        }
        out.flush();
    }

    public final static void main(String[] args) throws Exception {
        System.out.println(String.format("Reading edges from: %s", args[0]));
        List<GraphEdge> edges = readEdgesFromFile(args[0]);
        List<List<DAGNode>> dags = build_dags(edges, "null_rev");

        for (List<DAGNode> dag : dags) {
            System.out.println("--- dag ---");
            test_dumping_graph(dag);
        }
    }
}