/* Glazed Lists                                                 (c) 2003-2006 */
/* http://publicobject.com/glazedlists/                      publicobject.com,*/
/*                                                     O'Dell Engineering Ltd.*/
package ca.odell.glazedlists.jfreechart;

import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.event.ListEvent;
import ca.odell.glazedlists.event.ListEventListener;
import org.jfree.data.UnknownKeyException;
import org.jfree.data.category.CategoryDataset;
import org.jfree.data.general.AbstractDataset;
import org.jfree.data.general.DatasetChangeEvent;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * This class helps adapt an {@link EventList} to the {@link CategoryDataset}
 * interface which is the necessary model for JFreeChart views such as
 *
 * <ul>
 *   <li> Bar Charts
 *   <li> Stacked Bar Charts
 *   <li> Area Charts
 *   <li> Stacked Area Charts
 *   <li> Line Charts
 *   <li> Waterfall Charts
 * </ul>
 *
 * <p><table border="1" width="100%" cellpadding="3" cellspacing="0">
 * <tr class="TableHeadingColor"><td colspan="2"><font size="+2"><b>Extension: JFreeChart</b></font></td></tr>
 * <tr><td  colspan="2">This Glazed Lists <i>extension</i> requires the third party library <b>JFreeChart</b>.</td></tr>
 * <tr><td class="TableSubHeadingColor"><b>Tested Version:</b></td><td>1.0.0</td></tr>
 * <tr><td class="TableSubHeadingColor"><b>Home page:</b></td><td><a href="http://www.jfree.org/jfreechart/">http://www.jfree.org/jfreechart/</a></td></tr>
 * <tr><td class="TableSubHeadingColor"><b>License:</b></td><td><a href="http://www.jfree.org/lgpl.php">LGPL</a></td></tr>
 * </td></tr>
 * </table>
 *
 * <p>If it is possible to create a pipeline of list transformations such that
 * your source is an {@link EventList} of {@link ValueSegment} objects then
 * this class can aid you in showing and maintaining that data within a
 * JFreeChart. It handles the maintenance of efficient data structures for
 * implementing {@link #getValue(int, int)} and leaves subclasses the task
 * of defining the logic for maintaining the lists of rowkeys and columnkeys.
 *
 * <p> If the rowkeys and columnkeys are statically determined, they can be set
 * like so:
 *
 * <pre>
 *   EventList myValueSegments = ...
 *   CategoryDataset dataset = new EventListCategoryDataset(myValueSegments);
 *   dataset.getColumnKeys().add(...);
 *   dataset.getRowKeys().add(...);
 * </pre>
 *
 * If the rowkeys and/or columnkeys are dynamically maintained as
 * {@link ValueSegment}s are added and removed from the {@link EventList}, then
 * two hooks have been given for the benefit of subclasses:
 *
 * <ul>
 *   <li> {@link #postInsert} is a hook to process the insertion of new source.
 *        This is commonly where rows or columns may be created by adding their
 *        keys to the rowKey or columnKey lists.
 *
 *   <li> {@link #postDelete} is a hook to process the deletion of existing
 *        source. This is commonly where rows or columns may be removed by
 *        removing their keys from the rowKey or columnKey lists.
 * </ul>
 *
 * <p><strong><font color="#FF0000">Note:</font></strong> If this
 * {@link EventListCategoryDataset} is being shown in a Swing User Interface,
 * and thus Dataset Changes should be broadcast on the Swing Event Dispatch
 * Thread, it is the responsibility of the <strong>caller</strong> to ensure
 * that {@link ListEvent}s arrive on the Swing EDT.
 *
 * @see ca.odell.glazedlists.swing.GlazedListsSwing#swingThreadProxyList
 *
 * @author James Lemieux
 */
public abstract class EventListCategoryDataset<R extends Comparable, C extends Comparable> extends AbstractDataset implements CategoryDataset, ListEventListener<ValueSegment<C,R>> {

    /** The single immutable DatasetChangeEvent we fire each time this Dataset is changed. */
    private final DatasetChangeEvent immutableChangeEvent = new DatasetChangeEvent(this, this);

    /** Keep a private copy of the contents of the source list in order to access deleted elements. */
    private final List<ValueSegment<C,R>> sourceCopy;

    /** The source of the data to be charted. */
    private final EventList<ValueSegment<C,R>> source;

    /** An ordered list of keys identifying the chart's rows. */
    protected List<? extends Comparable> rowKeys;

    /** An ordered list of keys identifying the chart's columns. */
    protected List<? extends Comparable> columnKeys;

    /**
     * This is the main data structure which organizes the data to make
     * calculating counts of items within a continuum a fast operation. It maps
     * each unique value to a {@link TreePair}. Each tree within the
     * {@link TreePair} orders the value according to the starting value of its
     * {@link ValueSegment} or the ending value of its {@link ValueSegment}.
     * The count of all such values within a range can thus be calculated.
     */
    private Map<R,TreePair<C>> valueToTreePairs = new HashMap<R,TreePair<C>>();

    /**
     * Constructs an implementation of {@link CategoryDataset} which presents
     * the data contained in the given <code>source</code>.
     *
     * @param source the {@link EventList} of data to be charted
     */
    public EventListCategoryDataset(EventList<ValueSegment<C,R>> source) {
        this.source = source;

        // make a copy of the source list's content
        this.sourceCopy = new ArrayList<ValueSegment<C,R>>(source.size());
        this.sourceCopy.addAll(source);

        this.rebuildRowAndColumnKeyList();

        // begin listening to changes in the source list
        this.source.addListEventListener(this);
    }

    /**
     * A convenience method to rebuild the lists of rowKeys and columnKeys.
     */
    private void rebuildRowAndColumnKeyList() {
        this.columnKeys = createColumnKeyList();
        this.rowKeys = createRowKeyList();
    }

    /**
     * A local factory method for creating the list containing the row keys.
     */
    protected List<? extends Comparable> createRowKeyList() {
        return new ArrayList<Comparable>();
    }

    /**
     * A local factory method for creating the list containing the column keys.
     */
    protected List<? extends Comparable> createColumnKeyList() {
        return new ArrayList<Comparable>();
    }

    /**
     * Returns the row key for a given index.
     *
     * @param row the row index (zero-based)
     * @return the row key
     *
     * @throws IndexOutOfBoundsException if <code>row</code> is out of bounds
     */
    public Comparable getRowKey(int row) {
        return rowKeys.get(row);
    }

    /**
     * Returns the row index for a given key.
     *
     * @param key the row key
     * @return the row index, or <code>-1</code> if the key is unrecognized
     */
    public int getRowIndex(Comparable key) {
        return rowKeys.indexOf(key);
    }

    /**
     * Returns the row keys.
     */
    public List getRowKeys() {
        return rowKeys;
    }

    /**
     * Returns the number of rows in the table.
     */
    public int getRowCount() {
        return rowKeys.size();
    }

    /**
     * Returns the column key for a given index.
     *
     * @param column the column index (zero-based)
     * @return the column key
     *
     * @throws IndexOutOfBoundsException if <code>column</code> is out of bounds
     */
    public Comparable getColumnKey(int column) {
        return columnKeys.get(column);
    }

    /**
     * Returns the column index for a given key.
     *
     * @param key the column key
     * @return the column index, or <code>-1</code> if the key is unrecognized
     */
    public int getColumnIndex(Comparable key) {
        return columnKeys.indexOf(key);
    }

    /**
     * Returns the column keys.
     */
    public List getColumnKeys() {
        return columnKeys;
    }

    /**
     * Returns the number of columns in the table.
     */
    public int getColumnCount() {
        return columnKeys.size();
    }

    /**
     * Returns a value from the table.
     *
     * @param row the row index (zero-based)
     * @param column the column index (zero-based)
     * @return the value (possibly <code>null</code>)
     *
     * @throws IndexOutOfBoundsException if the <code>row</code>
     *      or <code>column</code> is out of bounds
     */
    public Number getValue(int row, int column) {
        return getValue(getRowKey(row), getColumnKey(column));
    }

    /**
     * Returns the value associated with the specified keys.
     *
     * @param rowKey the row key (<code>null</code> not permitted)
     * @param columnKey the column key (<code>null</code> not permitted)
     * @return the value
     *
     * @throws UnknownKeyException if either key is not recognized
     */
    public abstract Number getValue(Comparable rowKey, Comparable columnKey);

    /**
     * Releases the resources consumed by this {@link EventListCategoryDataset}
     * so that it may eventually be garbage collected.
     *
     * <p>An {@link EventListCategoryDataset} will be garbage collected without
     * a call to {@link #dispose()}, but not before its source {@link EventList}
     * is garbage collected. By calling {@link #dispose()}, you allow the
     * {@link EventListCategoryDataset} to be garbage collected before its
     * source {@link EventList}. This is necessary for situations where a
     * {@link EventListCategoryDataset} is short-lived but its source
     * {@link EventList} is long-lived.
     *
     * <p><strong><font color="#FF0000">Warning:</font></strong> It is an error
     * to call any method on an {@link EventListCategoryDataset} after it has
     * been disposed.
     */
    public void dispose() {
        this.source.removeListEventListener(this);
    }

    /**
     * This convenience method clears all value, rowkey, and columnkey data
     * structures.
     */
    private void clear() {
        sourceCopy.clear();
        valueToTreePairs.clear();
        rowKeys.clear();
        rebuildRowAndColumnKeyList();
    }

    /**
     * Returns the {@link TreePair} associated with the given
     * <code>rowKey</code>.
     */
    private TreePair<C> getTreePair(R rowKey) {
        return valueToTreePairs.get(rowKey);
    }

    /**
     * Returns the number of values associated with the given <code>rowKey</code>.
     */
    public int getCount(R rowKey) {
        final TreePair treePair = getTreePair(rowKey);
        return treePair == null ? 0 : treePair.size();
    }

    /**
     * Returns the number of values associated with the given <code>rowKey</code>
     * between the given <code>start</code> and <code>end</code> values.
     */
    public int getCount(R rowKey, C start, C end) {
        // fetch the relevant pair of trees
        final TreePair<C> treePair = getTreePair(rowKey);

        // ensure we found something
        if (treePair == null)
            throw new UnknownKeyException("unrecognized rowKey: " + rowKey);

        // return the number of values between start and end
        return treePair.getCount(start, end);
    }

    /**
     * This no-op method is left as a hook for subclasses. It is called after
     * an element has been inserted into the value data structure in this
     * dataset. Subclasses typically override this method to provide extra
     * logic for maintaining the lists of rowkeys and columnkeys dynamically.
     *
     * @param valueSegment the data element inserted into this data set
     */
    protected void postInsert(ValueSegment<C,R> valueSegment) { }

    /**
     * This no-op method is left as a hook for subclasses. It is called after
     * an element has been removed from the value data structure in this
     * dataset. Subclasses typically override this method to provide extra
     * logic for maintaining the lists of rowkeys and columnkeys dynamically.
     *
     * @param valueSegment the data element removed from this data set
     */
    protected void postDelete(ValueSegment<C,R> valueSegment) { }

    /**
     * We override this method for speed reasons, since the super needlessly
     * constructs a new DatasetChangeEvent each time this method is called.
     */
    protected void fireDatasetChanged() {
        notifyListeners(immutableChangeEvent);
    }

    /**
     * This listener maintains a fast set of TreePairs for each unique value
     * found in the ValueSegments of the source list. The TreePairs, in turn,
     * are efficient at answering questions about the number of values which
     * fall in the certain segment of the value's continuum.
     *
     * This listener also rebroadcasts ListEvents as DatasetChangeEvents.
     */
    public void listChanged(ListEvent<ValueSegment<C,R>> listChanges) {
        // speed up the case when listChanges describes a total clearing of
        // the data structures, (which occurs when switching between projects)
        if (listChanges.getSourceList().isEmpty()) {
            clear();

        } else {
            while (listChanges.next()) {
                final int type = listChanges.getType();
                final int index = listChanges.getIndex();

                if (type == ListEvent.INSERT) {
                    // fetch the segment that was inserted
                    final ValueSegment<C,R> segment = listChanges.getSourceList().get(index);
                    // fetch the TreePair for the segment's value
                    TreePair<C> treePair = getTreePair(segment.getValue());

                    // if a TreePair has not been created for the segment's value, create one now
                    if (treePair == null) {
                        treePair = new TreePair<C>();
                        valueToTreePairs.put(segment.getValue(), treePair);
                    }

                    // insert the segment into the TreePair
                    treePair.insert(segment);
                    // maintain our copy of the source contents
                    sourceCopy.add(index, segment);
                    // call into a hook for custom handling of this insert by subclasses
                    postInsert(segment);

                } else if (type == ListEvent.UPDATE) {
                    // fetch the segments involved
                    final ValueSegment<C,R> newSegment = listChanges.getSourceList().get(index);
                    final ValueSegment<C,R> oldSegment = sourceCopy.set(index, newSegment);

                    // fetch the TreePairs involved
                    final TreePair<C> oldTreePair = getTreePair(oldSegment.getValue());
                    final TreePair<C> newTreePair = getTreePair(newSegment.getValue());

                    // delete the segment from the oldTreePair
                    oldTreePair.delete(oldSegment);
                    postDelete(oldSegment);

                    // add the segment to the newTreePair
                    newTreePair.insert(newSegment);
                    postInsert(newSegment);

                } else if (type == ListEvent.DELETE) {
                    // fetch the segment that was removed
                    final ValueSegment<C,R> segment = sourceCopy.remove(index);
                    // fetch the TreePair for the segment's value
                    final TreePair<C> treePair = getTreePair(segment.getValue());

                    // delete the segment from the TreePair
                    treePair.delete(segment);
                    // call into a hook for custom handling of this delete by subclasses
                    postDelete(segment);
                }
            }
        }

        // indicate the dataset contents have changed
        fireDatasetChanged();
    }
}