package prefuse.data.tuple;

import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;

import prefuse.data.Table;
import prefuse.data.Tuple;
import prefuse.data.event.TupleSetListener;
import prefuse.data.expression.Expression;
import prefuse.data.expression.Predicate;
import prefuse.data.expression.parser.ExpressionParser;
import prefuse.util.collections.CompositeIterator;

/**
 * <p>TupleSet implementation for treating a collection of tuple sets
 * as a single, composite tuple set. This composite does not take
 * overlap between contained TupleSets into account.</p>
 * 
 * <p>The {@link TupleSet#addTuple(Tuple)} and {@link #setTuple(Tuple)}
 * methods are not supported by this class, and calling these methods will
 * result in a UnsupportedOperationException. Instead, use the add or set
 * methods on the desired non-composite tuple set.</p>
 * 
 * @author <a href="http://jheer.org">jeffrey heer</a>
 */
public class CompositeTupleSet extends AbstractTupleSet {

    private static final Logger s_logger
        = Logger.getLogger(CompositeTupleSet.class.getName());
    
    private Map m_map;   // map names to tuple sets
    private Set m_sets;  // support quick reverse lookup
    private int m_count; // count of total tuples
    private Listener m_lstnr;
    
    /**
     * Create a new, empty CompositeTupleSet
     */
    public CompositeTupleSet() {
        this(true);
    }
    
    protected CompositeTupleSet(boolean listen) {
        m_map = new LinkedHashMap();
        m_sets = new HashSet();
        m_count = 0;
        m_lstnr = listen ? new Listener() : null;
    }
    
    /**
     * Add a TupleSet to this composite.
     * @param name the name of the TupleSet to add
     * @param set the set to add
     */
    public void addSet(String name, TupleSet set) {
        if ( hasSet(name) ) {
            throw new IllegalArgumentException("Name already in use: "+name);
        }
        m_map.put(name, set);
        m_sets.add(set);
        m_count += set.getTupleCount();
        if ( m_lstnr != null )
            set.addTupleSetListener(m_lstnr);
    }
    
    /**
     * Indicates if this composite contains a TupleSet with the given name.
     * @param name the name to look for
     * @return true if a TupleSet with the given name is found, false otherwise
     */
    public boolean hasSet(String name) {
        return m_map.containsKey(name);
    }
    
    /**
     * Indicates if this composite contains the given TupleSet.
     * @param set the TupleSet to check for containment
     * @return true if the TupleSet is contained in this composite,
     * false otherwise
     */
    public boolean containsSet(TupleSet set) {
        return m_sets.contains(set);
    }
    
    /**
     * Get the TupleSet associated with the given name.
     * @param name the  name of the TupleSet to get
     * @return the associated TupleSet, or null if not found
     */
    public TupleSet getSet(String name) {
        return (TupleSet)m_map.get(name);
    }
    
    /**
     * Get an iterator over the names of all the TupleSets in this composite.
     * @return the iterator over contained set names.
     */
    public Iterator setNames() {
        return m_map.keySet().iterator();
    }

    /**
     * Get an iterator over all the TupleSets in this composite.
     * @return the iterator contained sets.
     */
    public Iterator sets() {
        return m_map.values().iterator();
    }
    
    /**
     * Remove the TupleSet with the given name from this composite.
     * @param name the name of the TupleSet to remove
     * @return the removed TupleSet, or null if not found
     */
    public TupleSet removeSet(String name) {
        TupleSet ts = (TupleSet)m_map.remove(name);
        if ( ts != null ) {
            m_sets.remove(ts);
            if ( m_lstnr != null )
                ts.removeTupleSetListener(m_lstnr);
        }
        return ts;
    }
    
    /**
     * Remove all contained TupleSets from this composite.
     */
    public void removeAllSets() {
        Iterator sets = m_map.entrySet().iterator();
        while ( sets.hasNext() ) {
            Map.Entry entry = (Map.Entry)sets.next();
            TupleSet ts = (TupleSet)entry.getValue();
            sets.remove();
            m_sets.remove(ts);
            if ( m_lstnr != null )
                ts.removeTupleSetListener(m_lstnr);
        }
        m_count = 0;
    }
    
    /**
     * Clear this TupleSet, calling clear on all contained TupleSet
     * instances. All contained TupleSets remain members of this
     * composite, but they have their data cleared.
     * @see prefuse.data.tuple.TupleSet#clear()
     */
    public void clear() {
        Iterator sets = m_map.entrySet().iterator();
        while ( sets.hasNext() ) {
            Map.Entry entry = (Map.Entry)sets.next();
            ((TupleSet)entry.getValue()).clear();
        }
        m_count = 0;
    }
    
    // ------------------------------------------------------------------------
    // TupleSet Interface
    
    /**
     * Not supported.
     * @see prefuse.data.tuple.TupleSet#addTuple(prefuse.data.Tuple)
     */
    public Tuple addTuple(Tuple t) {
        throw new UnsupportedOperationException();
    }

    /**
     * Not supported.
     * @see prefuse.data.tuple.TupleSet#setTuple(prefuse.data.Tuple)
     */
    public Tuple setTuple(Tuple t) {
        throw new UnsupportedOperationException();
    }

    /**
     * Removes the tuple from its source set if that source set is contained
     * within this composite.
     * @see prefuse.data.tuple.TupleSet#removeTuple(prefuse.data.Tuple)
     */
    public boolean removeTuple(Tuple t) {
        Table table = t.getTable();
        if ( m_sets.contains(table) ) {
            return table.removeTuple(t);
        } else {
            return false;
        }
    }
    
    /**
     * @see prefuse.data.tuple.TupleSet#containsTuple(prefuse.data.Tuple)
     */
    public boolean containsTuple(Tuple t) {
        Iterator it = m_map.entrySet().iterator();
        while ( it.hasNext() )  {
            Map.Entry entry = (Map.Entry)it.next();
            TupleSet ts = (TupleSet)entry.getValue();
            if ( ts.containsTuple(t) )
                return true;
        }
        return false;
    }

    /**
     * @see prefuse.data.tuple.TupleSet#getTupleCount()
     */
    public int getTupleCount() {
        if ( m_lstnr != null ) {
            return m_count;
        } else {
            int count = 0;
            Iterator it = m_map.entrySet().iterator();
            for ( int i=0; it.hasNext(); ++i )  {
                Map.Entry entry = (Map.Entry)it.next();
                TupleSet ts = (TupleSet)entry.getValue();
                count += ts.getTupleCount();
            }
            return count;
        }
    }

    /**
     * @see prefuse.data.tuple.TupleSet#tuples()
     */
    public Iterator tuples() {
        CompositeIterator ci = new CompositeIterator(m_map.size());
        Iterator it = m_map.entrySet().iterator();
        for ( int i=0; it.hasNext(); ++i )  {
            Map.Entry entry = (Map.Entry)it.next();
            TupleSet ts = (TupleSet)entry.getValue();
            ci.setIterator(i, ts.tuples());
        }
        return ci;
    }
    
    /**
     * @see prefuse.data.tuple.TupleSet#tuples(prefuse.data.expression.Predicate)
     */
    public Iterator tuples(Predicate filter) {
        CompositeIterator ci = new CompositeIterator(m_map.size());
        Iterator it = m_map.entrySet().iterator();
        for ( int i=0; it.hasNext(); ++i )  {
            Map.Entry entry = (Map.Entry)it.next();
            TupleSet ts = (TupleSet)entry.getValue();
            ci.setIterator(i, ts.tuples(filter));
        }
        return ci;
    }
    
    // -- Data Field Methods --------------------------------------------------
    
    /**
     * Returns true.
     * @see prefuse.data.tuple.TupleSet#isAddColumnSupported()
     */
    public boolean isAddColumnSupported() {
        return true;
    }

    /**
     * Adds the value to all contained TupleSets that return a true value for
     * {@link TupleSet#isAddColumnSupported()}.
     * @see prefuse.data.tuple.TupleSet#addColumn(java.lang.String, java.lang.Class, java.lang.Object)
     */
    public void addColumn(String name, Class type, Object defaultValue) {
        Iterator it = m_map.entrySet().iterator();
        while ( it.hasNext() ) {
            Map.Entry entry = (Map.Entry)it.next();
            TupleSet ts = (TupleSet)entry.getValue();
            if ( ts.isAddColumnSupported() ) {
                try {
                    ts.addColumn(name, type, defaultValue);
                } catch ( IllegalArgumentException iae ) {
                    // already exists
                }
            } else {
                s_logger.fine("Skipped addColumn for "+entry.getKey());
            }
        }
    }

    /**
     * Adds the value to all contained TupleSets that return a true value for
     * {@link TupleSet#isAddColumnSupported()}.
     * @see prefuse.data.tuple.TupleSet#addColumn(java.lang.String, java.lang.Class)
     */
    public void addColumn(String name, Class type) {
        Iterator it = m_map.entrySet().iterator();
        while ( it.hasNext() ) {
            Map.Entry entry = (Map.Entry)it.next();
            TupleSet ts = (TupleSet)entry.getValue();
            if ( ts.isAddColumnSupported() ) {
                try {
                    ts.addColumn(name, type);
                } catch ( IllegalArgumentException iae ) {
                    // already exists
                }
            } else {
                s_logger.fine("Skipped addColumn for "+entry.getKey());
            }
        }
    }

    /**
     * Adds the value to all contained TupleSets that return a true value for
     * {@link TupleSet#isAddColumnSupported()}.
     * @see prefuse.data.tuple.TupleSet#addColumn(java.lang.String, prefuse.data.expression.Expression)
     */
    public void addColumn(String name, Expression expr) {
        Iterator it = m_map.entrySet().iterator();
        while ( it.hasNext() ) {
            Map.Entry entry = (Map.Entry)it.next();
            TupleSet ts = (TupleSet)entry.getValue();
            if ( ts.isAddColumnSupported() ) {
                try {
                    ts.addColumn(name, expr);
                } catch ( IllegalArgumentException iae ) {
                    // already exists
                }
            } else {
                s_logger.fine("Skipped addColumn for "+entry.getKey());
            }
        }
    }

    /**
     * Adds the value to all contained TupleSets that return a true value for
     * {@link TupleSet#isAddColumnSupported()}.
     * @see prefuse.data.tuple.TupleSet#addColumn(java.lang.String, java.lang.String)
     */
    public void addColumn(String name, String expr) {
        Expression ex = ExpressionParser.parse(expr);
        Throwable t = ExpressionParser.getError();
        if ( t != null ) {
            throw new RuntimeException(t);
        } else {
            addColumn(name, ex);
        }
    }    
    
    // ------------------------------------------------------------------------
    // Internal TupleSet Listener
    
    /**
     * Listener that relays tuple set change events as they occur and updates
     * the total tuple count appropriately.
     */
    private class Listener implements TupleSetListener {
        public void tupleSetChanged(TupleSet tset, Tuple[] add, Tuple[] rem) {
            m_count += add.length - rem.length;
            fireTupleEvent(add, rem);
        }
    }

} // end of class CompositeTupleSet
