package org.limewire.ui.swing.util;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.jdesktop.swingx.JXTable;
import org.jdesktop.swingx.decorator.SortController;
import org.jdesktop.swingx.decorator.SortKey;
import org.jdesktop.swingx.decorator.SortOrder;
import org.jdesktop.swingx.table.TableColumnExt;
import org.limewire.ui.swing.table.GlazedJXTable;

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 ca.odell.glazedlists.swing.DefaultEventTableModel;

/**
 * This is essentially a copy of glazedLists.EventListJXTableSorting class. The 
 * main difference is this class enforces the column comparators within 
 * AdvancedTableFormat if the table is constructed with an AdvancedTableFormat.
 * Otherwise is reverts to the default GlazedListComparator.
 * 
 * <p>This class supports multiple column sorts with stable results so that 
 * equal elements are not reordered by sorts on successive columns.  In 
 * addition, any column may have secondary sort columns associated with it - 
 * a single sort request on such a column will result in a multiple-column
 * sort.  Secondary sort columns are specified by providing a non-null instance
 * of EventListTableSortFormat in the <code>install()</code> method.</p> 
 */
public class EventListJXTableSorting {
    
    /** Maximum number of sorted columns. */
    private static final int MAX_SORT_COLUMNS = 5;

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

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

    /** the original filter pipeline, used in {@link #uninstall} only */
    private final SortController originalSortController;
    
    /** Format for table sorts on event list. */
    private final EventListTableSortFormat tableSortFormat;

    /**
     * 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(GlazedJXTable table, SortedList sortedList,
            EventListTableSortFormat tableSortFormat) {
        this.table = table;
        this.sortedList = sortedList;
        this.tableSortFormat = tableSortFormat;
        this.originalSortController = table.getSortController();

        this.sortController = new EventListSortController();
        table.setSortController(sortController);
        
        // Apply default sort keys.
        if (tableSortFormat != null) {
            List<SortKey> sortKeys = tableSortFormat.getDefaultSortKeys();
            this.sortController.setSortKeys(sortKeys);
        }
    }

    /**
     * Install this {@link EventListJXTableSorting} to provide the sorting
     * behaviour for the specified {@link JXTable}.
     */
    public static EventListJXTableSorting install(GlazedJXTable table, SortedList sortedList) {
        return new EventListJXTableSorting(table, sortedList, null);
    }

    /**
     * Install this {@link EventListJXTableSorting} to provide the sorting
     * behaviour for the specified {@link JXTable}.  A non-null value for
     * <code>tableSortFormat</code> provides support for secondary sort columns.
     */
    public static EventListJXTableSorting install(GlazedJXTable table, SortedList sortedList, 
            EventListTableSortFormat tableSortFormat) {
        return new EventListJXTableSorting(table, sortedList, tableSortFormat);
    }

    /**
     * Remove this {@link EventListJXTableSorting} from the {@link JXTable}.
     */
    public void uninstall() {
        table.setSortController(originalSortController);
    }

    /**
     * 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>(MAX_SORT_COLUMNS);
        private final List<SortKey> sortKeysReadOnly = Collections.unmodifiableList(sortKeys);
        
        /** active sort columns that do not contain any preSort columns */
        private final List<SortKey> nonHiddenSortKeys = new ArrayList<SortKey>(MAX_SORT_COLUMNS);
        private final List<SortKey> nonHiddenKeysReadOnly = Collections.unmodifiableList(nonHiddenSortKeys);

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

        /** {@inheritDoc} */
        @Override
		@SuppressWarnings("cast")
        public void toggleSortOrder(int columnIndex, Comparator comparator) {
            List<? extends SortKey> sortKeys = getSortKeys();

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

            // create the new sort key
            if(columnSortKey == null) {
                columnSortKey = new SortKey(SortOrder.ASCENDING, columnIndex);
            } else {
                SortOrder sortOrder = columnSortKey.getSortOrder() == SortOrder.ASCENDING ? SortOrder.DESCENDING : SortOrder.ASCENDING;
                columnSortKey = new SortKey(sortOrder, columnIndex);
            }
            
            // Create new list of sort keys based on existing list.
            List<? extends SortKey> newSortKeys = sortKeys;
            
            // Apply secondary sort columns if available.
            if (tableSortFormat != null) {
                List<Integer> columnList = tableSortFormat.getSecondarySortColumns(columnIndex);
                // Apply secondary sort columns in reverse order, from least 
                // significant to most significant.  If possible, we use the
                // existing sort key for the column.
                for (int i = columnList.size(); i > 0; i--) {
                    Integer secondaryColumn = columnList.get(i - 1);
                    SortKey sortKey = findSortKey(secondaryColumn, sortKeys);
                    newSortKeys = buildSortKeys(sortKey, newSortKeys);
                }
            }

            // Apply primary sort column, and set sort keys to perform sort.
            setSortKeys(buildSortKeys(columnSortKey, newSortKeys));
        }
        
        /**
         * Returns the SortKey corresponding to the specified column from the 
         * specified list.  If a meaningful SortKey is not found, then a 
         * default SortKey with ascending order is returned. 
         */
        private SortKey findSortKey(int column, List<? extends SortKey> sortKeys) {
            // Search for sort key with matching column.
            for (SortKey sortKey : sortKeys) {
                if ((sortKey.getSortOrder() != SortOrder.UNSORTED) && 
                    (sortKey.getColumn() == column)) {
                    return new SortKey(sortKey.getSortOrder(), column);
                }
            }
            
            // Not found so return default sort key.
            return new SortKey(SortOrder.ASCENDING, column);
        }

        /**
         * Returns a list of sort keys containing the specified first key, 
         * followed by the specified list of additional keys.  This method 
         * provides support for sorting over multiple columns.
         */
        private List<? extends SortKey> buildSortKeys(SortKey firstKey, 
                List<? extends SortKey> sortKeys) {
            
            // Create SortKey list and unique column set. 
            List<SortKey> newSortKeys = new ArrayList<SortKey>();
            Set<Integer> columnSet = new HashSet<Integer>();
            
            // Add first sort key.
            newSortKeys.add(firstKey);
            columnSet.add(firstKey.getColumn());
            
            // Add remaining sort keys.  Only keys with a meaningful sort order
            // are added, and duplicate columns are omitted.
            for (SortKey sortKey : sortKeys) {
                if (newSortKeys.size() >= MAX_SORT_COLUMNS) {
                    break;
                }
                if ((sortKey.getSortOrder() != SortOrder.UNSORTED) &&
                        !columnSet.contains(sortKey.getColumn())) {
                    newSortKeys.add(sortKey);
                    columnSet.add(sortKey.getColumn());
                }
            }

            // Return new list.
            return newSortKeys;
        }

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

            this.sortKeys.clear();
            this.nonHiddenSortKeys.clear();
            if(tableSortFormat != null && tableSortFormat.getPreSortColumns() != null 
            	&& tableSortFormat.getPreSortColumns().size() > 0) {
                this.sortKeys.addAll(tableSortFormat.getPreSortColumns());
            }
            this.nonHiddenSortKeys.addAll(sortKeys);
            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 = null;
            } 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.
         */
        @SuppressWarnings("unchecked")
        private Comparator getComparator(int modelIndex) {
            DefaultEventTableModel tableModel = (DefaultEventTableModel)table.getModel();
            TableFormat tableFormat = tableModel.getTableFormat();
            // TODO: local changes compared to GlazedLists implementation
            // If the tableFormat is an AdvancedTableFormat, use the table column comparators
            // found in the AdvancedTableFormat for sorting.
            if(tableFormat instanceof AdvancedTableFormat) {
                return new EventListColumnComparator(tableFormat, modelIndex, ((AdvancedTableFormat)tableFormat).getColumnComparator(modelIndex));
            } else 
                return new TableColumnComparator(tableFormat, modelIndex);
        }

        /** {@inheritDoc} */
        @Override
		public List<? extends SortKey> getSortKeys() {
            // return the sort keys which do not contain preSort columns. This ensures
            // the correct table header is used when displaying sort to the user.
            return nonHiddenKeysReadOnly;
        }

        /** {@inheritDoc} */
        @Override
		public SortOrder getSortOrder(int columnIndex) {
            for(SortKey s : sortKeys) {
                if(s.getColumn() == columnIndex) return s.getSortOrder();
            }
            return SortOrder.UNSORTED;
        }
    }
}
