package prefuse.data;

import java.util.Iterator;

import prefuse.data.column.Column;
import prefuse.data.event.ColumnListener;
import prefuse.data.event.EventConstants;
import prefuse.data.event.GraphListener;
import prefuse.data.event.TableListener;
import prefuse.data.expression.Predicate;
import prefuse.data.tuple.CompositeTupleSet;
import prefuse.data.tuple.TableEdge;
import prefuse.data.tuple.TableNode;
import prefuse.data.tuple.TupleManager;
import prefuse.data.tuple.TupleSet;
import prefuse.data.util.Index;
import prefuse.data.util.NeighborIterator;
import prefuse.util.PrefuseConfig;
import prefuse.util.TypeLib;
import prefuse.util.collections.CompositeIntIterator;
import prefuse.util.collections.CompositeIterator;
import prefuse.util.collections.CopyOnWriteArrayList;
import prefuse.util.collections.IntArrayIterator;
import prefuse.util.collections.IntIterator;

/**
 * <p>A Graph models a network of nodes connected by a collection of edges.
 * Both nodes and edges can have any number of associated data fields.
 * Additionally, edges are either directed or undirected, indicating a
 * possible directionality of the connection. Each edge has both a source
 * node and a target node, for a directed edge this indicates the
 * directionality, for an undirected edge this is just an artifact
 * of the order in which the nodes were specified when added to the graph.
 * </p>
 * 
 * <p>Both nodes and edges are represented by backing data {@link Table}
 * instances storing the data attributes. For edges, this table must
 * also contain a source node field and a target node field. The default
 * column name for these fields are {@link #DEFAULT_SOURCE_KEY} and
 * {@link #DEFAULT_TARGET_KEY}, but these can be configured by the graph
 * constructor. These columns should be integer valued, and contain
 * either the row number for a corresponding node in the node table,
 * or another unique identifier for a node. In this second case, the
 * unique identifier must be included as a data field in the node
 * table. This name of this column can be configured using the appropriate
 * graph constructor. The default column name for this field is
 * {@link #DEFAULT_NODE_KEY}, which by default evaluates to null,
 * indicating that no special node key should be used, just the direct
 * node table row numbers. Though the source, target, and node key
 * values completely specify the graph linkage structure, to make
 * graph operations more efficient an additional table is maintained
 * internally by the Graph class, storing node indegree and outdegree
 * counts and adjacency lists for the inlinks and outlinks for all nodes.</p>
 * 
 * <p>Graph nodes and edges can be accessed by application code by either
 * using the row numbers of the node and edge tables, which provide unique ids
 * for each, or using the {@link prefuse.data.Node} and
 * {@link prefuse.data.Edge} classes --
 * {@link prefuse.data.Tuple} instances that provide
 * object-oriented access to both node or edge data values and the
 * backing graph structure. Node and Edge tuples are maintained by
 * special TupleManager instances contained within this Graph. By default, they
 * are not accessible from the backing node or edge tables directly. The reason
 * for this is that these tables might be used in multiple graphs
 * simultaneously. For example, a node data table could be used in a number of
 * different graphs, exploring different possible linkages between those node.
 * In short, use this Graph instance to request iterators over Node or
 * Edge tuples, not the backing data tables.</p>
 * 
 * <p>All Graph instances also support an internally generated
 * spanning tree, provided by the {@link #getSpanningTree()} or
 * {@link #getSpanningTree(Node)} methods. This is particularly
 * useful in visualization contexts that use an underlying tree structure
 * to compute a graph layout.</p>
 * 
 * @author <a href="http://jheer.org">jeffrey heer</a>
 */
public class Graph extends CompositeTupleSet {
    
    /** Indicates incoming edges (inlinks) */
    public static final int INEDGES    = 0;
    /** Indicates outgoing edges (outlinks) */
    public static final int OUTEDGES   = 1;
    /** Indicates all edges, regardless of direction */
    public static final int UNDIRECTED = 2;
    
    /** Default data field used to uniquely identify a node */
    public static final String DEFAULT_NODE_KEY 
        = PrefuseConfig.get("data.graph.nodeKey");
    /** Default data field used to denote the source node in an edge table */
    public static final String DEFAULT_SOURCE_KEY 
        = PrefuseConfig.get("data.graph.sourceKey");
    /** Default data field used to denote the target node in an edge table */
    public static final String DEFAULT_TARGET_KEY 
        = PrefuseConfig.get("data.graph.targetKey"); 
    /** Data group name to identify the nodes of this graph */
    public static final String NODES 
        = PrefuseConfig.get("data.graph.nodeGroup");
    /** Data group name to identify the edges of this graph */
    public static final String EDGES 
        = PrefuseConfig.get("data.graph.edgeGroup");
    
    // -- auxiliary data structures -----
    
    /** Table containing the adjacency lists for the graph */
    protected Table m_links;
    /** TupleManager for managing Node tuple instances */
    protected TupleManager m_nodeTuples;
    /** TupleManager for managing Edge tuple instances */
    protected TupleManager m_edgeTuples;
    
    /** Indicates if this graph is directed or undirected */
    protected boolean      m_directed = false;
    /** The spanning tree over this graph */
    protected SpanningTree m_spanning = null;
    
    /** The node key field (for the Node table) */
    protected String m_nkey;
    /** The source node key field (for the Edge table) */
    protected String m_skey;
    /** The target node key field (for the Edge table) */
    protected String m_tkey;
    /** Reference to an index over the node key field */
    protected Index m_nidx;
    /** Indicates if the key values are of type long */
    protected boolean m_longKey = false;
    /** Update listener */
    private Listener m_listener;
    /** Listener list */
    private CopyOnWriteArrayList m_listeners = new CopyOnWriteArrayList();
    
    // ------------------------------------------------------------------------
    // Constructors
    
    /**
     * Creates a new, empty undirected Graph.
     */
    public Graph() {
        this(false);
    }
    
    /**
     * Creates a new, empty Graph.
     * @param directed true for directed edges, false for undirected
     */
    public Graph(boolean directed) {
        this(new Table(), directed);
    }

    /**
     * Create a new Graph using the provided table of node data and
     * an empty set of edges.
     * @param nodes the backing table to use for node data.
     * Node instances of this graph will get their data from this table.
     * @param directed true for directed edges, false for undirected
     */
    public Graph(Table nodes, boolean directed) {
        this(nodes, directed, DEFAULT_NODE_KEY,
                DEFAULT_SOURCE_KEY, DEFAULT_TARGET_KEY);
    }
 
    /**
     * Create a new Graph using the provided table of node data and
     * an empty set of edges.
     * @param nodes the backing table to use for node data.
     * Node instances of this graph will get their data from this table.
     * @param directed true for directed edges, false for undirected
     * @param nodeKey data field used to uniquely identify a node. If this
     * field is null, the node table row numbers will be used
     * @param sourceKey data field used to denote the source node in an edge
     * table
     * @param targetKey data field used to denote the target node in an edge
     * table
     */
    public Graph(Table nodes, boolean directed, String nodeKey,
            String sourceKey, String targetKey) {
        Table edges = new Table();
        edges.addColumn(sourceKey, int.class, new Integer(-1));
        edges.addColumn(targetKey, int.class, new Integer(-1));
        init(nodes, edges, directed, nodeKey, sourceKey, targetKey);
    }
    
    /**
     * Create a new Graph, using node table row numbers to uniquely
     * identify nodes in the edge table's source and target fields.
     * @param nodes the backing table to use for node data.
     * Node instances of this graph will get their data from this table.
     * @param edges the backing table to use for edge data.
     * Edge instances of this graph will get their data from this table.
     * @param directed true for directed edges, false for undirected
     */
    public Graph(Table nodes, Table edges, boolean directed) {
        this(nodes, edges, directed,
            DEFAULT_NODE_KEY, DEFAULT_SOURCE_KEY, DEFAULT_TARGET_KEY);
    }
    
    /**
     * Create a new Graph, using node table row numbers to uniquely
     * identify nodes in the edge table's source and target fields.
     * @param nodes the backing table to use for node data.
     * Node instances of this graph will get their data from this table.
     * @param edges the backing table to use for edge data.
     * Edge instances of this graph will get their data from this table.
     * @param directed true for directed edges, false for undirected
     * @param sourceKey data field used to denote the source node in an edge
     * table
     * @param targetKey data field used to denote the target node in an edge
     * table
     */
    public Graph(Table nodes, Table edges, boolean directed,
            String sourceKey, String targetKey)
    {
        init(nodes, edges, directed, DEFAULT_NODE_KEY, sourceKey, targetKey);
    }
    
    /**
     * Create a new Graph.
     * @param nodes the backing table to use for node data.
     * Node instances of this graph will get their data from this table.
     * @param edges the backing table to use for edge data.
     * Edge instances of this graph will get their data from this table.
     * @param directed true for directed edges, false for undirected
     * @param nodeKey data field used to uniquely identify a node. If this
     * field is null, the node table row numbers will be used
     * @param sourceKey data field used to denote the source node in an edge
     * table
     * @param targetKey data field used to denote the target node in an edge
     * table
     */
    public Graph(Table nodes, Table edges, boolean directed,
            String nodeKey, String sourceKey, String targetKey)
    {
        init(nodes, edges, directed, nodeKey, sourceKey, targetKey);
    }
    
    // ------------------------------------------------------------------------
    // Initialization
    
    /**
     * Initialize this Graph instance.
     * @param nodes the node table
     * @param edges the edge table
     * @param directed the edge directionality
     * @param nodeKey data field used to uniquely identify a node
     * @param sourceKey data field used to denote the source node in an edge
     * table
     * @param targetKey data field used to denote the target node in an edge
     * table
     */
    protected void init(Table nodes, Table edges, boolean directed, 
            String nodeKey, String sourceKey, String targetKey)
    {
        // sanity check
        if ( (nodeKey!=null && 
              !TypeLib.isIntegerType(nodes.getColumnType(nodeKey)))  ||
              !TypeLib.isIntegerType(edges.getColumnType(sourceKey)) ||
              !TypeLib.isIntegerType(edges.getColumnType(targetKey)) )
        {
            throw new IllegalArgumentException(
                "Incompatible column types for graph keys");
        }
        
        removeAllSets();
        super.addSet(EDGES, edges);
        super.addSet(NODES, nodes);
        
        m_directed = directed;
        
        // INVARIANT: these three should all reference the same type
        // currently limited to int
        m_nkey = nodeKey;
        m_skey = sourceKey;
        m_tkey = targetKey;
        
        // set up indices
        if ( nodeKey != null ) {
            if ( nodes.getColumnType(nodeKey) == long.class )
                m_longKey = true;
            nodes.index(nodeKey);
            m_nidx = nodes.getIndex(nodeKey);
        }
        
        // set up tuple manager
        if ( m_nodeTuples == null )
            m_nodeTuples = new TupleManager(nodes, this, TableNode.class);
        m_edgeTuples = new TupleManager(edges, this, TableEdge.class);
        
        // set up node attribute optimization
        initLinkTable();
        
        // set up listening
        if ( m_listener == null )
            m_listener = new Listener();
        nodes.addTableListener(m_listener);
        edges.addTableListener(m_listener);
        m_listener.setEdgeTable(edges);
    }
    
    /**
     * Set the tuple managers used to manage the Node and Edge tuples of this
     * Graph.
     * @param ntm the TupleManager to use for nodes
     * @param etm the TupleManager to use for edges
     */
    public void setTupleManagers(TupleManager ntm, TupleManager etm) {
        if ( !Node.class.isAssignableFrom(ntm.getTupleType()) )
            throw new IllegalArgumentException("The provided node " +
                "TupleManager must generate tuples that implement " +
                "the Node interface.");
        if ( !Edge.class.isAssignableFrom(etm.getTupleType()) )
            throw new IllegalArgumentException("The provided edge " +
                "TupleManager must generate tuples that implement " +
                "the Edge interface.");
        m_nodeTuples = ntm;
        m_edgeTuples = etm;
    }
    
    /**
     * Dispose of this graph. Unregisters this graph as a listener to its
     * included tables.
     */
    public void dispose() {
        getNodeTable().removeTableListener(m_listener);
        getEdgeTable().removeTableListener(m_listener);
    }
    
    /**
     * Updates this graph to use a different edge structure for the same nodes.
     * All other settings will remain the same (e.g., directionality, keys)
     * @param edges the new edge table.
     */
    public void setEdgeTable(Table edges) {
        Table oldEdges = getEdgeTable();
        oldEdges.removeTableListener(m_listener);
        m_edgeTuples.invalidateAll();
        m_links.clear();
        
        init(getNodeTable(), edges, m_directed, m_nkey, m_skey, m_tkey);
    }
    
    // ------------------------------------------------------------------------
    // Data Access Optimization
    
    /**
     * Initialize the link table, which holds adjacency lists for this graph.
     */
    protected void initLinkTable() {
        // set up cache of node data
        m_links = createLinkTable();
                
        IntIterator edges = getEdgeTable().rows();
        while ( edges.hasNext() ) {
            updateDegrees(edges.nextInt(), 1);
        }
    }
    
    /**
     * Instantiate and return the link table.
     * @return the created link table
     */
    protected Table createLinkTable() {
        return LINKS_SCHEMA.instantiate(getNodeTable().getMaximumRow()+1);
    }
    
    /**
     * Internal method for updating the linkage of this graph.
     * @param e the edge id for the updated link
     * @param incr the increment value, 1 for an added link,
     * -1 for a removed link
     */
    protected void updateDegrees(int e, int incr) {
        if ( !getEdgeTable().isValidRow(e) ) return;
        int s = getSourceNode(e);
        int t = getTargetNode(e);
        if ( s < 0 || t < 0 ) return;
        updateDegrees(e, s, t, incr);
        if ( incr < 0 ) {
            m_edgeTuples.invalidate(e);
        }
    }
    
    /**
     * Internal method for updating the linkage of this graph.
     * @param e the edge id for the updated link
     * @param s the source node id for the updated link
     * @param t the target node id for the updated link
     * @param incr the increment value, 1 for an added link,
     * -1 for a removed link
     */
    protected void updateDegrees(int e, int s, int t, int incr) {
        int od = m_links.getInt(s, OUTDEGREE);
        int id = m_links.getInt(t, INDEGREE);
        // update adjacency lists
        if ( incr > 0 ) {
            // add links
            addLink(OUTLINKS, od, s, e);
            addLink(INLINKS, id, t, e);
        } else if ( incr < 0 ) {
            // remove links
            remLink(OUTLINKS, od, s, e);
            remLink(INLINKS, id, t, e);
        }
        // update degree counts
        m_links.setInt(s, OUTDEGREE, od+incr);
        m_links.setInt(t, INDEGREE, id+incr);
        // link structure changed, invalidate spanning tree
        m_spanning = null;
    }
    
    /**
     * Internal method for adding a link to an adjacency list
     * @param field which adjacency list (inlinks or outlinks) to use
     * @param len the length of the adjacency list
     * @param n the node id of the adjacency list to use
     * @param e the edge to add to the list
     */
    protected void addLink(String field, int len, int n, int e) {
        int[] array = (int[])m_links.get(n, field);
        if ( array == null ) {
            array = new int[] {e};
            m_links.set(n, field, array);
            return;
        } else if ( len == array.length ) {
            int[] narray = new int[Math.max(3*array.length/2, len+1)];
            System.arraycopy(array, 0, narray, 0, array.length);
            array = narray;
            m_links.set(n, field, array);
        }
        array[len] = e;
    }
    
    /**
     * Internal method for removing a link from an adjacency list
     * @param field which adjacency list (inlinks or outlinks) to use
     * @param len the length of the adjacency list
     * @param n the node id of the adjacency list to use
     * @param e the edge to remove from the list
     * @return true if the link was removed successfully, false otherwise
     */
    protected boolean remLink(String field, int len, int n, int e) {
        int[] array = (int[])m_links.get(n, field);
        for ( int i=0; i<len; ++i ) {
            if ( array[i] == e ) {
                System.arraycopy(array, i+1, array, i, len-i-1);
                return true;
            }
        }
        return false;
    }
    
    /**
     * Update the link table to accomodate an inserted or deleted node
     * @param r the node id, also the row number into the link table
     * @param added indicates if a node was added or removed
     */
    protected void updateNodeData(int r, boolean added) {
        if ( added ) {
            m_links.addRow();
        } else {
            m_nodeTuples.invalidate(r);
            m_links.removeRow(r);
        }
    }
    
    // ------------------------------------------------------------------------
    // Key Transforms
    
    /**
     * Get the data field used to uniquely identify a node
     * @return the data field used to uniquely identify a node
     */
    public String getNodeKeyField() {
        return m_nkey;
    }
    
    /**
     * Get the data field used to denote the source node in an edge table.
     * @return the data field used to denote the source node in an edge table.
     */
    public String getEdgeSourceField() {
        return m_skey;
    }
    
    /**
     * Get the data field used to denote the target node in an edge table.
     * @return the data field used to denote the target node in an edge table.
     */
    public String getEdgeTargetField() {
        return m_tkey;
    }
    
    /**
     * Given a node id (a row number in the node table), get the value of
     * the node key field.
     * @param node the node id
     * @return the value of the node key field for the given node
     */
    public long getKey(int node) {
        return m_nkey == null ? node : getNodeTable().getLong(node, m_nkey);
    }
    
    /**
     * Given a value of the node key field, get the node id (the row number
     * in the node table).
     * @param key a node key field value
     * @return the node id (the row number in the node table)
     */
    public int getNodeIndex(long key) {
        if ( m_nidx == null ) {
            return (int)key;
        } else {
            int idx = m_longKey ? m_nidx.get(key) : m_nidx.get((int)key);
            return idx<0 ? -1 : idx;
        }
    }
    
    // ------------------------------------------------------------------------
    // Graph Mutators
    
    /**
     * Add row to the node table, thereby adding a node to the graph.
     * @return  the node id (node table row number) of the added node
     */
    public int addNodeRow() {
        return getNodeTable().addRow();
    }
    
    /**
     * Add a new node to the graph.
     * @return the new Node instance
     */
    public Node addNode() {
        int nrow = addNodeRow();
        return (Node)m_nodeTuples.getTuple(nrow);
    }
    
    /**
     * Add an edge to the graph. Both multiple edges between two nodes
     * and edges from a node to itself are allowed.
     * @param s the source node id
     * @param t the target node id
     * @return the edge id (edge table row number) of the added edge
     */
    public int addEdge(int s, int t) {
        // get keys for the nodes
        long key1 = getKey(s);
        long key2 = getKey(t);
        
        // add edge row, set source/target fields
        Table edges = getEdgeTable();
        int r = edges.addRow();
        if ( m_longKey ) {
            edges.setLong(r, m_skey, key1);
            edges.setLong(r, m_tkey, key2);
        } else {
            edges.setInt(r, m_skey, (int)key1);
            edges.setInt(r, m_tkey, (int)key2);
        }
        return r;
    }
    
    /**
     * Add an edge to the graph.
     * @param s the source Node
     * @param t the target Node
     * @return the new Edge instance
     */
    public Edge addEdge(Node s, Node t) {
        nodeCheck(s, true);
        nodeCheck(t, true);
        int e = addEdge(s.getRow(), t.getRow());
        return getEdge(e);
    }
    
    /**
     * Remove a node from the graph, also removing all incident edges.
     * @param node the node id (node table row number) of the node to remove
     * @return true if the node was successfully removed, false if the
     * node id was not found or was not valid
     */
    public boolean removeNode(int node) {
        Table nodeTable = getNodeTable();
        if ( nodeTable.isValidRow(node) ) {
            int id = getInDegree(node);
            if ( id > 0 ) {
                int[] links = (int[])m_links.get(node, INLINKS);
                for ( int i=id; --i>=0; )
                    removeEdge(links[i]);
            }
            int od = getOutDegree(node);
            if ( od > 0 ) {
                int[] links = (int[])m_links.get(node, OUTLINKS);
                for ( int i=od; --i>=0; )
                    removeEdge(links[i]);
            }
        }
        return nodeTable.removeRow(node);
    }
    
    /**
     * Remove a node from the graph, also removing all incident edges.
     * @param n the Node to remove from the graph
     * @return true if the node was successfully removed, false if the
     * node was not found in this graph
     */
    public boolean removeNode(Node n) {
        nodeCheck(n, true);
        return removeNode(n.getRow());
    }
    
    /**
     * Remove an edge from the graph.
     * @param edge the edge id (edge table row number) of the edge to remove
     * @return true if the edge was successfully removed, false if the
     * edge was not found or was not valid
     */
    public boolean removeEdge(int edge) {
        return getEdgeTable().removeRow(edge);
    }
    
    /**
     * Remove an edge from the graph.
     * @param e the Edge to remove from the graph
     * @return true if the edge was successfully removed, false if the
     * edge was not found in this graph
     */
    public boolean removeEdge(Edge e) {
        edgeCheck(e, true);
        return removeEdge(e.getRow());
    }
    
    /**
     * Internal method for clearing the edge table, removing all edges.
     */
    protected void clearEdges() {
        getEdgeTable().clear();
    }
    
    // ------------------------------------------------------------------------
    // Node Accessor Methods
    
    /**
     * Internal method for checking the validity of a node.
     * @param n the Node to check for validity
     * @param throwException true if this method should throw an Exception
     * when an invalid node is encountered
     * @return true is the node is valid, false if invalid
     */
    protected boolean nodeCheck(Node n, boolean throwException) {
        if ( !n.isValid() ) {
            if ( throwException ) {
                throw new IllegalArgumentException(
                    "Node must be valid.");
            }
            return false;
        }
        Graph ng = n.getGraph();
        if ( ng != this && ng.m_spanning != this ) {
            if ( throwException ) {
                throw new IllegalArgumentException(
                    "Node must be part of this Graph.");
            }
            return false;
        }
        return true;
    }
    
    /**
     * Get the collection of nodes as a TupleSet. Returns the same result as
     * {@link CompositeTupleSet#getSet(String)} using
     * {@link #NODES} as the parameter.
     * @return the nodes of this graph as a TupleSet instance
     */
    public TupleSet getNodes() {
        return getSet(NODES);
    }
    
    /**
     * Get the backing node table.
     * @return the table of node values
     */
    public Table getNodeTable() {
        return (Table)getSet(NODES);
    }
    
    /**
     * Get the number of nodes in this graph.
     * @return the number of nodes
     */
    public int getNodeCount() {
        return getNodeTable().getRowCount();
    }
    
    /**
     * Get the Node tuple instance corresponding to a node id.
     * @param n a node id (node table row number)
     * @return the Node instance corresponding to the node id
     */
    public Node getNode(int n) {
        return (Node)m_nodeTuples.getTuple(n);
    }
    
    /**
     * Get the Node tuple corresponding to the input node key field value.
     * The node key field is used to find the node id (node table row number),
     * which is then used to retrieve the Node tuple.
     * @param key a node key field value
     * @return the requested Node instance
     */
    public Node getNodeFromKey(long key) {
        int n = getNodeIndex(key);
        return (n<0 ? null : getNode(n) );
    }
    
    /**
     * Get the in-degree of a node, the number of edges for which the node
     * is the target.
     * @param node the node id (node table row number)
     * @return the in-degree of the node
     */
    public int getInDegree(int node) {
        return m_links.getInt(node, INDEGREE);
    }
    
    /**
     * Get the in-degree of a node, the number of edges for which the node
     * is the target.
     * @param n the Node instance
     * @return the in-degree of the node
     */
    public int getInDegree(Node n) {
        nodeCheck(n, true);
        return getInDegree(n.getRow());
    }
    
    /**
     * Get the out-degree of a node, the number of edges for which the node
     * is the source.
     * @param node the node id (node table row number)
     * @return the out-degree of the node
     */
    public int getOutDegree(int node) {
        return m_links.getInt(node, OUTDEGREE);
    }
    
    /**
     * Get the out-degree of a node, the number of edges for which the node
     * is the source.
     * @param n the Node instance
     * @return the out-degree of the node
     */
    public int getOutDegree(Node n) {
        nodeCheck(n, true);
        return getOutDegree(n.getRow());
    }
    
    /**
     * Get the degree of a node, the number of edges for which a node
     * is either the source or the target.
     * @param node the node id (node table row number)
     * @return the total degree of the node
     */
    public int getDegree(int node) {
        return getInDegree(node) + getOutDegree(node);
    }
    
    /**
     * Get the degree of a node, the number of edges for which a node
     * is either the source or the target.
     * @param n the Node instance
     * @return the total degree of the node
     */
    public int getDegree(Node n) {
        nodeCheck(n, true);
        return getDegree(n.getRow());
    }
    
    // ------------------------------------------------------------------------
    // Edge Accessor Methods
    
    /**
     * Indicates if the edges of this graph are directed or undirected.
     * @return true if directed edges, false if undirected edges
     */
    public boolean isDirected() {
        return m_directed;
    }
    
    /**
     * Internal method for checking the validity of an edge.
     * @param e the Edge to check for validity
     * @param throwException true if this method should throw an Exception
     * when an invalid node is encountered
     * @return true is the edge is valid, false if invalid
     */
    protected boolean edgeCheck(Edge e, boolean throwException) {
        if ( !e.isValid() ) {
            if ( throwException ) {
                throw new IllegalArgumentException(
                    "Edge must be valid.");
            }
            return false;
        }
        if ( e.getGraph() != this ) {
            if ( throwException ) {
                throw new IllegalArgumentException(
                    "Edge must be part of this Graph.");
            }
            return false;
        }
        return true;
    }
    
    /**
     * Get the collection of edges as a TupleSet. Returns the same result as
     * {@link CompositeTupleSet#getSet(String)} using
     * {@link #EDGES} as the parameter.
     * @return the edges of this graph as a TupleSet instance
     */
    public TupleSet getEdges() {
        return getSet(EDGES);
    }

    /**
     * Get the backing edge table.
     * @return the table of edge values
     */
    public Table getEdgeTable() {
        return (Table)getSet(EDGES);
    }
    /**
     * Get the number of edges in this graph.
     * @return the number of edges
     */
    public int getEdgeCount() {
        return getEdgeTable().getRowCount();
    }

    /**
     * Get the Edge tuple instance corresponding to an edge id.
     * @param e an edge id (edge table row number)
     * @return the Node instance corresponding to the node id
     */
    public Edge getEdge(int e) {
        return ( e < 0 ? null : (Edge)m_edgeTuples.getTuple(e) );
    }
    
    /**
     * Returns an edge from the source node to the target node. This
     * method returns the first such edge found; in the case of multiple
     * edges there may be more.
     */
    public int getEdge(int source, int target) {
        int outd = getOutDegree(source); 
        if ( outd > 0 ) {
            int[] edges = (int[])m_links.get(source, OUTLINKS);
            for ( int i=0; i<outd; ++i ) {
                if ( getTargetNode(edges[i]) == target )
                    return edges[i];
            }
        }
        return -1;
    }
    
    /**
     * Get an Edge with given source and target Nodes. There may be times
     * where there are multiple edges between two nodes; in those cases
     * this method returns the first such edge found.
     * @param source the source Node
     * @param target the target Node
     * @return an Edge with given source and target nodes, or null if no
     * such edge is found.
     */
    public Edge getEdge(Node source, Node target) {
        nodeCheck(source, true);
        nodeCheck(target, true);
        return getEdge(getEdge(source.getRow(), target.getRow()));
    }
    
    /**
     * Get the source node id (node table row number) for the given edge
     * id (edge table row number).
     * @param edge an edge id (edge table row number)
     * @return the source node id (node table row number)
     */
    public int getSourceNode(int edge) {
        return getNodeIndex(getEdgeTable().getLong(edge, m_skey));
    }
    
    /**
     * Get the source Node for the given Edge instance.
     * @param e an Edge instance
     * @return the source Node of the edge
     */
    public Node getSourceNode(Edge e) {
        edgeCheck(e, true);
        return getNode(getSourceNode(e.getRow()));
    }
    
    /**
     * Get the target node id (node table row number) for the given edge
     * id (edge table row number).
     * @param edge an edge id (edge table row number)
     * @return the target node id (node table row number)
     */
    public int getTargetNode(int edge) {
        return getNodeIndex(getEdgeTable().getLong(edge, m_tkey));
    }
    
    /**
     * Get the target Node for the given Edge instance.
     * @param e an Edge instance
     * @return the target Node of the edge
     */
    public Node getTargetNode(Edge e) {
        edgeCheck(e, true);
        return getNode(getTargetNode(e.getRow()));
    }
    
    /**
     * Given an edge id and an incident node id, return the node id for
     * the other node connected to the edge.
     * @param edge an edge id (edge table row number)
     * @param node a node id (node table row number). This node id must
     * be connected to the edge
     * @return the adjacent node id
     */
    public int getAdjacentNode(int edge, int node) {
        int s = getSourceNode(edge);
        int d = getTargetNode(edge);
        
        if ( s == node ) {
            return d;
        } else if ( d == node ) {
            return s;
        } else {
            throw new IllegalArgumentException(
                "Edge is not incident on the input node.");
        }
    }
    
    /**
     * Given an Edge and an incident Node, return the other Node
     * connected to the edge.
     * @param e an Edge instance
     * @param n a Node instance. This node must
     * be connected to the edge
     * @return the adjacent Node
     */
    public Node getAdjacentNode(Edge e, Node n) {
        edgeCheck(e, true);
        nodeCheck(n, true);
        return getNode(getAdjacentNode(e.getRow(), n.getRow()));
    }

    // ------------------------------------------------------------------------
    // Iterators
    
    // -- table row iterators ----
    
    /**
     * Get an iterator over all node ids (node table row numbers).
     * @return an iterator over all node ids (node table row numbers)
     */
    public IntIterator nodeRows() {
        return getNodeTable().rows();
    }

    /**
     * Get an iterator over all edge ids (edge table row numbers).
     * @return an iterator over all edge ids (edge table row numbers)
     */
    public IntIterator edgeRows() {
        return getEdgeTable().rows();
    }
    
    /**
     * Get an iterator over all edge ids for edges incident on the given node.
     * @param node a node id (node table row number)
     * @return an iterator over all edge ids for edges incident on the given
     * node
     */
    public IntIterator edgeRows(int node) {
        return edgeRows(node, UNDIRECTED);
    }
    
    /**
     * Get an iterator edge ids for edges incident on the given node.
     * @param node a node id (node table row number)
     * @param direction the directionality of the edges to include. One of
     * {@link #INEDGES} (for in-linking edges),
     * {@link #OUTEDGES} (for out-linking edges), or
     * {@link #UNDIRECTED} (for all edges).
     * @return an iterator over all edge ids for edges incident on the given
     * node
     */
    public IntIterator edgeRows(int node, int direction) {
        if ( direction==OUTEDGES ) {
            int[] outedges = (int[])m_links.get(node, OUTLINKS);
            return new IntArrayIterator(outedges, 0, getOutDegree(node));
        } else if ( direction==INEDGES ) {
            int[] inedges = (int[])m_links.get(node, INLINKS);
            return new IntArrayIterator(inedges, 0, getInDegree(node));
        } else if ( direction==UNDIRECTED ) {
            return new CompositeIntIterator(
                edgeRows(node, OUTEDGES), edgeRows(node, INEDGES));
        } else {
            throw new IllegalArgumentException("Unrecognized edge type: " 
                + direction + ". Type should be one of Graph.OUTEDGES, "
                + "Graoh.INEDGES, or Graph.ALL");
        }
    }
    
    /**
     * Get an iterator over all edges that have the given node as a target.
     * That is, edges that link into the given target node.
     * @param node a node id (node table row number)
     * @return an iterator over all edges that have the given node as a target
     */
    public IntIterator inEdgeRows(int node) {
        return edgeRows(node, INEDGES);
    }

    /**
     * Get an iterator over all edges that have the given node as a source.
     * That is, edges that link out from the given source node.
     * @param node a node id (node table row number)
     * @return an iterator over all edges that have the given node as a source
     */
    public IntIterator outEdgeRows(int node) {
        return edgeRows(node, OUTEDGES);
    }
    
    // -- tuple iterators --
    
    /**
     * Get an iterator over all nodes in the graph.
     * @return an iterator over Node instances
     */
    public Iterator nodes() {
        return m_nodeTuples.iterator(nodeRows());
    }

    /**
     * Get an iterator over all neighbor nodes for the given Node in the graph.
     * @param n a Node in the graph
     * @return an iterator over all Nodes connected to the input node
     */
    public Iterator neighbors(Node n) {
        return new NeighborIterator(n, edges(n));
    }

    /**
     * Get an iterator over all in-linking neighbor nodes for the given Node.
     * @param n a Node in the graph
     * @return an iterator over all Nodes that point to the input target node
     */
    public Iterator inNeighbors(Node n) {
        return new NeighborIterator(n, inEdges(n));
    }

    /**
     * Get an iterator over all out-linking neighbor nodes for the given Node.
     * @param n a Node in the graph
     * @return an iterator over all Nodes pointed to by the input source node
     */
    public Iterator outNeighbors(Node n) {
        return new NeighborIterator(n, outEdges(n));
    }
    
    /**
     * Get an iterator over all edges in the graph.
     * @return an iterator over Edge instances
     */
    public Iterator edges() {
        return m_edgeTuples.iterator(edgeRows());
    }
    
    /**
     * Get an iterator over all Edges connected to the given Node in the graph.
     * @param node a Node in the graph
     * @return an iterator over all Edges connected to the input node
     */
    public Iterator edges(Node node) {
        nodeCheck(node, true);
        return m_edgeTuples.iterator(edgeRows(node.getRow(), UNDIRECTED)); 
    }
    
    /**
     * Get an iterator over all in-linking edges to the given Node.
     * @param node a Node in the graph
     * @return an iterator over all in-linking edges to the input target node
     */
    public Iterator inEdges(Node node) {
        nodeCheck(node, true);
        return m_edgeTuples.iterator(inEdgeRows(node.getRow()));
    }

    /**
     * Get an iterator over all out-linking edges from the given Node.
     * @param node a Node in the graph
     * @return an iterator over all out-linking edges from the input source
     * node
     */
    public Iterator outEdges(Node node) {
        nodeCheck(node, true);
        return m_edgeTuples.iterator(outEdgeRows(node.getRow()));
    }
    
    // ------------------------------------------------------------------------
    // TupleSet Interface

    /**
     * Clear this graph, removing all nodes and edges.
     * @see prefuse.data.tuple.TupleSet#clear()
     */
    public void clear() {
        m_nodeTuples.invalidateAll();
        m_edgeTuples.invalidateAll();
        super.clear();
        m_links.clear();
    }
    
    /**
     * If the given tuple is a Node or Edge in this graph, remove it.
     * @see prefuse.data.tuple.TupleSet#removeTuple(prefuse.data.Tuple)
     */
    public boolean removeTuple(Tuple t) {
        // TODO: check underlying table tuples as well?
        if ( t instanceof Node ) {
            return removeNode((Node)t);
        } else if ( t instanceof Edge ) {
            return removeEdge((Edge)t);
        } else {
            throw new IllegalArgumentException(
                    "Input tuple must be part of this graph");
        }
    }

    /**
     * Get a filtered iterator over the edges and nodes of this graph.
     * @see prefuse.data.tuple.TupleSet#tuples(prefuse.data.expression.Predicate)
     */
    public Iterator tuples(Predicate filter) {
        if ( filter == null ) {
            return tuples();
        } else {
            return new CompositeIterator(
                    m_edgeTuples.iterator(getEdgeTable().rows(filter)),
                    m_nodeTuples.iterator(getNodeTable().rows(filter)));
        }
    }
    
    /**
     * Get an iterator over all the edges and nodes of this graph. The iterator
     * will return all edges first, then all nodes.
     * @see prefuse.data.tuple.TupleSet#tuples()
     */
    public Iterator tuples() {
        return new CompositeIterator(edges(), nodes());
    }    
    
    // ------------------------------------------------------------------------
    // Spanning Tree Methods
    
    /**
     * Return the current spanning tree over this graph. If no spanning tree
     * has been constructed, a SpanningTree rooted at the first valid node
     * found in the node table will be generated.
     * 
     * Spanning trees are generated using an unweighted breadth first search
     * over the graph structure.
     * 
     * @return a spanning tree over this graph
     * @see #getSpanningTree(Node)
     * @see #clearSpanningTree()
     */
    public Tree getSpanningTree() {
        if ( m_spanning == null )
            return getSpanningTree((Node)nodes().next());
        else
            return m_spanning;
    }

    /**
     * Returns a spanning tree rooted at the specified node. If the current
     * spanning tree is alrady rooted at the given node, it is simply
     * returned. Otherwise, the tree is reconstructed at the new root and
     * made the current spanning tree for this Graph instance.
     * 
     * Spanning trees are generated using an unweighted breadth first search
     * over the graph structure.
     * 
     * @param root the node at which to root the spanning tree.
     * @return a spanning tree over this graph, rooted at the given root
     * @see #getSpanningTree()
     * @see #clearSpanningTree()
     */
    public Tree getSpanningTree(Node root) {
        nodeCheck(root, true);
        if ( m_spanning == null ) {
            m_spanning = new SpanningTree(this, root);
        } else if ( m_spanning.getRoot() != root ) {
            m_spanning.buildSpanningTree(root);
        }
        return m_spanning;
    }
    
    /**
     * Clear the internally stored spanning tree. Any new calls to a
     * getSpanningTree() method will generate a new spanning tree 
     * instance as needed.
     * 
     * This method is primarily useful for subclasses.
     * For example, calling this method on a Tree instance will revert the
     * state to the original rooted tree such that a sbusequent call to
     * getSpanningTree() will return the backing Tree itself.
     * @see #getSpanningTree()
     * @see #getSpanningTree(Node)
     * @see Tree#getSpanningTree(Node)
     */
    public void clearSpanningTree() {
        m_spanning = null;
    }
    
    // ------------------------------------------------------------------------
    // Graph Listeners
    
    /**
     * Add a listener to be notified of changes to the graph.
     * @param listnr the listener to add
     */
    public void addGraphModelListener(GraphListener listnr) {
        if ( !m_listeners.contains(listnr) )
            m_listeners.add(listnr);
    }

    /**
     * Remove a listener from this graph.
     * @param listnr the listener to remove
     */
    public void removeGraphModelListener(GraphListener listnr) {
        m_listeners.remove(listnr);
    }
    
    /**
     * Fire a graph change event
     * @param t the backing table where the change occurred (either a
     * node table or an edge table)
     * @param first the first modified table row
     * @param last the last (inclusive) modified table row
     * @param col the number of the column modified, or
     * {@link prefuse.data.event.EventConstants#ALL_COLUMNS} for operations
     * affecting all columns
     * @param type the type of modification, one of
     * {@link prefuse.data.event.EventConstants#INSERT},
     * {@link prefuse.data.event.EventConstants#DELETE}, or
     * {@link prefuse.data.event.EventConstants#UPDATE}.
     */
    protected void fireGraphEvent(Table t, 
            int first, int last, int col, int type)
    {
        String table = (t==getNodeTable() ? NODES : EDGES);
        
        if ( type != EventConstants.UPDATE ) {
            // fire event to all tuple set listeners
            fireTupleEvent(t, first, last, type);
        }
        
        if ( !m_listeners.isEmpty() ) {            
            // fire event to all listeners
            Object[] lstnrs = m_listeners.getArray();
            for ( int i=0; i<lstnrs.length; ++i ) {
                ((GraphListener)lstnrs[i]).graphChanged(
                        this, table, first, last, col, type);
            }
        }
    }

    // ------------------------------------------------------------------------
    // Table and Column Listener
    
    /**
     * Listener class for tracking updates from node and edge tables,
     * and their columns that determine the graph linkage structure.
     */
    protected class Listener implements TableListener, ColumnListener {

        private Table m_edges;
        private Column m_scol, m_tcol;
        private int m_sidx, m_tidx;
        
        public void setEdgeTable(Table edges) {
            // remove any previous listeners
            if ( m_scol != null ) m_scol.removeColumnListener(this);
            if ( m_tcol != null ) m_tcol.removeColumnListener(this);
            m_scol = m_tcol = null;
            m_sidx = m_tidx = -1;
            
            m_edges = edges;
            
            // register listeners
            if ( m_edges != null ) {
                m_sidx = edges.getColumnNumber(m_skey);
                m_tidx = edges.getColumnNumber(m_tkey);
                m_scol = edges.getColumn(m_sidx);
                m_tcol = edges.getColumn(m_tidx);
                m_scol.addColumnListener(this);
                m_tcol.addColumnListener(this);
            }
        }
        
        public void tableChanged(Table t, int start, int end, int col, int type) {
            if ( !containsSet(t) )
                throw new IllegalStateException(
                     "Graph shouldn't be listening to an unrelated table");
            
            if ( type != EventConstants.UPDATE ) {
                if ( t == getNodeTable() ) {
                    // update the linkage structure table
                    if ( col == EventConstants.ALL_COLUMNS ) {
                        boolean added = type==EventConstants.INSERT;
                        for ( int r=start; r<=end; ++r )
                            updateNodeData(r, added);
                    }
                } else {
                    // update the linkage structure table
                    if ( col == EventConstants.ALL_COLUMNS ) {
                        boolean added = type==EventConstants.INSERT;
                        for ( int r=start; r<=end; ++r )
                            updateDegrees(start, added?1:-1);
                    }
                }
                // clear the spanning tree reference
                m_spanning = null;
            }
            fireGraphEvent(t, start, end, col, type);
        }

        public void columnChanged(Column src, int idx, int prev) {
            columnChanged(src, idx, (long)prev);
        }

        public void columnChanged(Column src, int idx, long prev) {
            if ( src==m_scol || src==m_tcol ) {
                boolean isSrc = src==m_scol;
                int e = m_edges.getTableRow(idx, isSrc?m_sidx:m_tidx);
                if ( e == -1 )
                    return; // edge not in this graph
                int s = getSourceNode(e);
                int t = getTargetNode(e);
                int p = getNodeIndex(prev);
                if ( p > -1 && ((isSrc && t > -1) || (!isSrc && s > -1)) )
                    updateDegrees(e, isSrc?p:s, isSrc?t:p, -1);
                if ( s > -1 && t > -1 )
                    updateDegrees(e, s, t, 1);
            } else {
                throw new IllegalStateException();
            }
        }
        
        public void columnChanged(Column src, int type, int start, int end) {
            // should never be called
            throw new IllegalStateException();
        }
        public void columnChanged(Column src, int idx, float prev) {
            // should never be called
            throw new IllegalStateException();
        }
        public void columnChanged(Column src, int idx, double prev) {
            // should never be called
            throw new IllegalStateException();
        }
        public void columnChanged(Column src, int idx, boolean prev) {
            // should never be called
            throw new IllegalStateException();
        }
        public void columnChanged(Column src, int idx, Object prev) {
            // should never be called
            throw new IllegalStateException();
        }   
    } // end of inner class Listener
    
    
    // ------------------------------------------------------------------------
    // Graph Linkage Schema
    
    /** In-degree data field for the links table */
    protected static final String INDEGREE  = "_indegree";
    /** Out-degree data field for the links table */
    protected static final String OUTDEGREE = "_outdegree";
    /** In-links adjacency list data field for the links table */
    protected static final String INLINKS   = "_inlinks";
    /** Out-links adjacency list data field for the links table */
    protected static final String OUTLINKS  = "_outlinks";
    /** Schema used for the internal graph linkage table */
    protected static final Schema LINKS_SCHEMA = new Schema();
    static {
        Integer defaultValue = new Integer(0);
        LINKS_SCHEMA.addColumn(INDEGREE,  int.class, defaultValue);
        LINKS_SCHEMA.addColumn(OUTDEGREE, int.class, defaultValue);
        LINKS_SCHEMA.addColumn(INLINKS,   int[].class);
        LINKS_SCHEMA.addColumn(OUTLINKS,  int[].class);
        LINKS_SCHEMA.lockSchema();
    }
    
} // end of class Graph
