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

import ca.odell.glazedlists.FilterList;
import ca.odell.glazedlists.GlazedLists;
import ca.odell.glazedlists.SortedList;
import ca.odell.glazedlists.gui.AdvancedTableFormat;
import ca.odell.glazedlists.gui.TableFormat;
import ca.odell.glazedlists.impl.sort.TableColumnComparator;

import java.util.*;

import org.jdesktop.swingx.JXTable;
import org.jdesktop.swingx.decorator.FilterPipeline;
import org.jdesktop.swingx.decorator.SortController;
import org.jdesktop.swingx.decorator.SortKey;
import org.jdesktop.swingx.decorator.SortOrder;
import org.jdesktop.swingx.table.TableColumnExt;

/**
 * Sort a {@link JXTable} using {@link ca.odell.glazedlists.SortedList}.
 *
 * <p><table border="1" width="100%" cellpadding="3" cellspacing="0">
 * <tr class="TableHeadingColor"><td colspan="2"><font size="+2"><b>Extension: SwingX</b></font></td></tr>
 * <tr><td  colspan="2">This Glazed Lists <i>extension</i> requires the third party library <b>SwingX</b>.</td></tr>
 * <tr><td class="TableSubHeadingColor"><b>Tested Version:</b></td><td>CVS Head, September 18, 2006</td></tr>
 * <tr><td class="TableSubHeadingColor"><b>Home page:</b></td><td><a href="https://swingx.dev.java.net/">https://swingx.dev.java.net/</a></td></tr>
 * <tr><td class="TableSubHeadingColor"><b>License:</b></td><td><a href="http://www.gnu.org/copyleft/lesser.html">LGPL</a></td></tr>
 * </td></tr>
 * </table>
 *
 * <p>To prepare a {@link JXTable} to be sorted using a {@link SortedList}:
 * <li>Create a {@link SortedList} and {@link AdvancedTableModel} that depends
 * on that {@link SortedList}.
 * <li>Create a {@link JXTable} using the {@link AdvancedTableModel} as its model.
 * <li>Run the {@link EventListJXTableSorting#install} method to bind the
 * {@link JXTable}'s headers to the {@link SortedList}'s {@link Comparator}.
 *
 * <p>Note that your {@link FilterPipeline} will be changed as a consequence of
 * using {@link EventListJXTableSorting}. This is due to API limitations in
 * SwingX which will hopefully be eventually resolved. For this reason it is
 * recommended that you not use {@link FilterPipeline} with this class. Instead,
 * consider using {@link FilterList} for filtering your {@link JXTable}.
 *
 * <p>Since this class works with the <a href="https://swingx.dev.java.net/">SwingX</a>
 * project, it is likely to change as SwingX continues to evolve and mature.
 * We will attempt to maintain source and binary compatibility as SwingX changes,
 * but it is highly recommended that you test your table sorting each time you
 * update your SwingX jar file.
 *
 * <p>The behaviour of multiple column sorting in JXTable is not particularly
 * flexible. For different configuration options, consider using a
 * {@link TableComparatorChooser} instead of {@link EventListJXTableSorting}.
 * To do this, you'll first need to disable the JXTable's built-in sorting
 * and restore the standard header renderer:
 * <pre><code>JXTable table = ...
 * table.setSortable(false);
 * table.getTableHeader().setDefaultRenderer(new JTableHeader().getDefaultRenderer());</code></pre>
 *
 * @author <a href="mailto:jesse@swank.ca">Jesse Wilson</a>
 */
public class EventListJXTableSorting {

    /** the sorted list behind the table being sorted */
    private final SortedList sortedList;
    private final JXTable table;

    /** adapters between SortedList and JXTable */
    private final SortController sortController;
    private final EventListFilterPipeline filterPipeline;

    /** the original filter pipeline, used in {@link #uninstall} only */
    private final FilterPipeline originalFilterPipeline;

    /** whether to sort multiple columns at a time */
    private boolean multipleColumnSort = false;

    /** comparator to use, if there are no sort keys.*/
    private final Comparator defaultComparator;

    /**
     * Usually, constructors shouldn't supposed to have side-effects, but this one
     * changes the table's filter pipeline. Therefore we use this private
     * constructor and call through it from the {@link #install} method.
     */
    private EventListJXTableSorting(JXTable table, SortedList sortedList, Comparator defaultComparator) {
        this.table = table;
        this.sortedList = sortedList;
        this.defaultComparator = defaultComparator;
        this.originalFilterPipeline = table.getFilters();

        this.sortController = new EventListSortController();
        this.filterPipeline = new EventListFilterPipeline();
        table.setFilters(filterPipeline);
    }

    /**
     * Install this {@link EventListJXTableSorting} to provide the sorting behaviour for the
     * specified {@link JXTable}. No comparator is used, when there are no sort keys.
     */
    public static EventListJXTableSorting install(JXTable table, SortedList sortedList) {
        return install(table, sortedList, null);
    }

    /**
     * Install this {@link EventListJXTableSorting} to provide the sorting behaviour for the
     * specified {@link JXTable}. Optionally, a default comparator can be specified, which is
     * used, when there are no sort keys.
     */
    public static EventListJXTableSorting install(JXTable table, SortedList sortedList, Comparator defaultComparator) {
        return new EventListJXTableSorting(table, sortedList, defaultComparator);
    }

    /**
     * Toggle whether clicked columns will be sorted after other sorted columns
     * (ie. break ties), or whether they will be come the exclusive sorted
     * column.
     *
     * <p>The default behaviour of JXTable multiple column sorting is that each
     * click appends a column to the sorting columns, reversing that column
     * if it's already sorting. Clearing the sorting columns is done by holding
     * <code>SHIFT</code> and clicking on a column header.
     *
     * @see org.jdesktop.swingx.JXTableHeader.SortGestureRecognizer to change
     * the clear/click behaviour
     * @see TableComparatorChooser for a more flexible approach to table sorting
     */
    public void setMultipleColumnSort(boolean multipleColumnSort) {
        this.multipleColumnSort = multipleColumnSort;
    }

    /**
     * Remove this {@link EventListJXTableSorting} from the {@link JXTable} and restores the
     * original filter pipeline.
     */
    public void uninstall() {
        table.setFilters(originalFilterPipeline);
    }

    /**
     * Unfortunately, the only way to provide a {@link SortController} for a
     * {@link JXTable} is to extend {@link FilterPipeline}. This is a significant
     * weakness in the SwingX API, and unfortunately they do not have the
     * resources to improve it for us. Perhaps sometime in the future we can
     * provide a patch to SwingX that makes installing a {@link SortController}
     * more elegant.
     */
    private class EventListFilterPipeline extends FilterPipeline {
        @Override
        public SortController getSortController() {
            return sortController;
        }
    }

    /**
     * Implement {@link SortController} to provide sorting for {@link JXTable}.
     */
    private class EventListSortController implements SortController {

        /** the active sort columns */
        private final List<SortKey> sortKeys = new ArrayList<SortKey>(1);
        private final List<SortKey> sortKeysReadOnly = Collections.unmodifiableList(sortKeys);

        /** {@inheritDoc} */
        public void toggleSortOrder(int columnIndex) {
            toggleSortOrder(columnIndex, GlazedLists.comparableComparator());
        }

        /** {@inheritDoc} */
        public void toggleSortOrder(int columnIndex, Comparator comparator) {
            List<SortKey> newSortKeys = new ArrayList<SortKey>(getSortKeys());

            // see if we're already sorting with this column
            SortOrder columnSortOrder = SortOrder.ASCENDING;
            for(Iterator<? extends SortKey> s = newSortKeys.iterator(); s.hasNext(); ) {
                SortKey sortKey = s.next();
                if(sortKey.getSortOrder() == SortOrder.UNSORTED) continue;
                if(sortKey.getColumn() == columnIndex) {
                    columnSortOrder = (sortKey.getSortOrder() == SortOrder.ASCENDING) ? SortOrder.DESCENDING : SortOrder.ASCENDING;
                    s.remove();
                    break;
                }
            }

            // prepare the new sort order
            if(!multipleColumnSort) newSortKeys.clear();
            newSortKeys.add(new SortKey(columnSortOrder, columnIndex));

            // kick off the sort
            setSortKeys(newSortKeys);
        }

        /** {@inheritDoc} */
        public void setSortKeys(List<? extends SortKey> sortKeys) {
            if(sortKeys == sortKeysReadOnly) return;
            if(sortKeys == null) sortKeys = Collections.emptyList();

            this.sortKeys.clear();
            this.sortKeys.addAll(sortKeys);

            // rebuild the SortedList's comparator
            List<Comparator> comparators = new ArrayList<Comparator>(this.sortKeys.size());
            for(int k = 0; k < this.sortKeys.size(); k++) {
                SortKey sortKey = this.sortKeys.get(k);
                if(sortKey.getSortOrder() == SortOrder.UNSORTED) continue;

                Comparator comparator = getComparator(sortKey.getColumn());
                if(sortKey.getSortOrder() == SortOrder.DESCENDING) comparator = GlazedLists.reverseComparator(comparator);

                comparators.add(comparator);
            }

            // figure out the final comparator
            final Comparator comparator;
            if(comparators.isEmpty()) {
                comparator = defaultComparator;
            } else if(comparators.size() == 1) {
                comparator = comparators.get(0);
            } else {
                comparator = GlazedLists.chainComparators((List)comparators);
            }

            // apply this comparator to the sortedlist
            sortedList.getReadWriteLock().writeLock().lock();
            try {
                sortedList.setComparator(comparator);
            } finally {
                sortedList.getReadWriteLock().writeLock().unlock();
            }
        }

        /**
         * We need to fix this implementation so it looks into the {@link JXTable}'s
         * {@link TableColumnExt} to find the appropriate column comparator. Failing
         * that, it could look for an {@link AdvancedTableFormat} and the column's
         * Comparator.
         */
        private Comparator getComparator(int modelIndex) {
            AdvancedTableModel tableModel = (AdvancedTableModel) table.getModel();
            TableFormat tableFormat = tableModel.getTableFormat();
            return new TableColumnComparator(tableFormat, modelIndex);
        }

        /** {@inheritDoc} */
        public List<? extends SortKey> getSortKeys() {
            return sortKeysReadOnly;
        }

        /** {@inheritDoc} */
        public SortOrder getSortOrder(int columnIndex) {
            for(int k = 0; k < this.sortKeys.size(); k++) {
                SortKey s = this.sortKeys.get(k);
                if(s.getColumn() == columnIndex) return s.getSortOrder();
            }
            return SortOrder.UNSORTED;
        }
    }
}