package org.limewire.ui.swing.filter;

import java.awt.Color;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.Comparator;

import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.ListSelectionModel;
import javax.swing.SwingConstants;
import javax.swing.border.Border;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;

import net.miginfocom.swing.MigLayout;

import org.jdesktop.swingx.JXList;
import org.jdesktop.swingx.decorator.ColorHighlighter;
import org.jdesktop.swingx.decorator.HighlightPredicate;
import org.limewire.collection.glazedlists.GlazedListsFactory;
import org.limewire.core.api.FilePropertyKey;
import org.limewire.ui.swing.components.HyperlinkButton;
import org.limewire.ui.swing.components.RolloverCursorListener;
import org.limewire.ui.swing.util.I18n;
import org.limewire.ui.swing.util.IconManager;
import org.limewire.util.Objects;

import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.FilterList;
import ca.odell.glazedlists.UniqueList;
import ca.odell.glazedlists.FunctionList.Function;
import ca.odell.glazedlists.event.ListEvent;
import ca.odell.glazedlists.event.ListEventListener;
import ca.odell.glazedlists.matchers.Matcher;
import ca.odell.glazedlists.swing.DefaultEventListModel;
import ca.odell.glazedlists.swing.DefaultEventSelectionModel;

import com.google.inject.Provider;

/**
 * Filter component to select items according to a collection of property 
 * values.
 */
class PropertyFilter<E extends FilterableItem> extends AbstractFilter<E> {

    private final FilterType filterType;
    private final FilePropertyKey propertyKey;
    private final Provider<IconManager> iconManager;
    
    private final JPanel panel = new JPanel();
    private final JLabel propertyLabel = new JLabel();
    private final JXList list = new JXList();
    private final HyperlinkButton moreButton = new HyperlinkButton();
    
    private EventList<Object> propertyList;
    private FilterList<Object> nonNullList;
    private UniqueListFactory<Object> uniqueListFactory;
    private UniqueList<Object> uniqueList;
    private DefaultEventSelectionModel<Object> selectionModel;
    private DefaultEventSelectionModel<Object> popupSelectionModel;
    private FilterPopupPanel morePopupPanel;
    
    /**
     * Constructs a PropertyFilter using the specified results list,
     * filter type, property key, and icon manager.
     */
    public PropertyFilter(EventList<E> resultsList,
            FilterType filterType, FilePropertyKey propertyKey, 
            Provider<IconManager> iconManager) {
        
        if ((filterType == FilterType.PROPERTY) && (propertyKey == null)) {
            throw new IllegalArgumentException("Property filter cannot use null key");
        }
        
        this.filterType = filterType;
        this.propertyKey = propertyKey;
        this.iconManager = iconManager;
        
        FilterResources resources = getResources();
        
        panel.setLayout(new MigLayout("insets 0 0 0 0, gap 0!, hidemode 3", 
                "[left,grow]", ""));
        panel.setOpaque(false);
        
        propertyLabel.setFont(resources.getHeaderFont());
        propertyLabel.setForeground(resources.getHeaderColor());
        propertyLabel.setText(getPropertyText());
        
        list.setCellRenderer(new PropertyCellRenderer(resources.getBackground(),
                BorderFactory.createEmptyBorder(1, 7, 0, 7)));
        list.setFont(resources.getRowFont());
        list.setForeground(resources.getRowColor());
        list.setOpaque(false);
        list.setRolloverEnabled(true);
        list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        
        // Add highlighter for rollover.
        list.setHighlighters(new ColorHighlighter(HighlightPredicate.ROLLOVER_ROW,
                resources.getHighlightBackground(), resources.getHighlightForeground()));
        
        // Add listener to show cursor on mouse over.
        new RolloverCursorListener().install(list);
        
        moreButton.setAction(new MoreAction());
        moreButton.setBorder(BorderFactory.createEmptyBorder(0, 1, 1, 1));
        moreButton.setContentAreaFilled(false);
        moreButton.setFocusPainted(false);
        moreButton.setFont(resources.getRowFont());
        moreButton.setHorizontalTextPosition(SwingConstants.LEADING);
        
        // Add listener to set popup trigger indicator.  This activates logic
        // so that pressing "more" a second time closes an open popup.
        moreButton.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseEntered(MouseEvent e) {
                if (morePopupPanel != null) {
                    morePopupPanel.setPopupTriggered(true);
                }
            }
            @Override
            public void mouseExited(MouseEvent e) {
                if (morePopupPanel != null) {
                    morePopupPanel.setPopupTriggered(false);
                }
            }
        });
        
        // Apply results list to filter.
        initialize(resultsList);
        
        // Calculate max list height.
        list.setPrototypeCellValue("Type");
        int listHeight = 3 * list.getFixedCellHeight();
        
        panel.add(propertyLabel, "gap 6 6, wrap");
        panel.add(list         , "hmax " + listHeight + ", grow, wrap");
        panel.add(moreButton   , "gap 6 6");
    }
    
    /**
     * Initializes the filter using the specified list of items.
     */
    private void initialize(EventList<E> resultsList) {
        // Create list of unique property values.
        propertyList = createPropertyList(resultsList);
        nonNullList = createNonNullList(propertyList);
        uniqueListFactory = new UniqueListFactory<Object>(nonNullList, new PropertyComparator(filterType, propertyKey));
        uniqueListFactory.setName(getPropertyText());
        uniqueList = uniqueListFactory.getUniqueList();
        
        // Initialize label and button visibility.
        propertyLabel.setVisible(uniqueList.size() > 0);
        moreButton.setVisible(uniqueList.size() > 3);

        // Add listener to display label and button when needed.
        uniqueList.addListEventListener(new ListEventListener<Object>() {
            @Override
            public void listChanged(ListEvent listChanges) {
                // Update label visibility.
                if (!propertyLabel.isVisible() && (uniqueList.size() > 0)) {
                    propertyLabel.setVisible(true);
                } else if (propertyLabel.isVisible() && (uniqueList.size() < 1)) {
                    propertyLabel.setVisible(false);
                }
                // Update "more" button visibility.
                if (!moreButton.isVisible() && (uniqueList.size() > 3)) {
                    moreButton.setVisible(true);
                } else if (moreButton.isVisible() && (uniqueList.size() < 4)) {
                    moreButton.setVisible(false);
                }
            }
        });
        
        // Create sorted list to display most popular values.
        EventList<Object> sortedList = GlazedListsFactory.sortedList(uniqueList, new PropertyCountComparator());
        
        // Create list and selection models.
        DefaultEventListModel<Object> listModel = new DefaultEventListModel<Object>(sortedList);
        selectionModel = new DefaultEventSelectionModel<Object>(sortedList);
        list.setModel(listModel);
        list.setSelectionModel(selectionModel);
        
        // Add selection listener to update filter.
        selectionModel.addListSelectionListener(new SelectionListener(selectionModel));
    }
    
    @Override
    public JComponent getComponent() {
        return panel;
    }

    @Override
    public void reset() {
        // Clear selections.
        if (selectionModel != null) {
            selectionModel.clearSelection();
        }
        if (popupSelectionModel != null) {
            popupSelectionModel.clearSelection();
        }
        // Deactivate filter.
        deactivate();
    }
    
    @Override
    public void dispose() {
        // Dispose of property list.
        uniqueListFactory.dispose();
        propertyList.dispose();
    }
    
    /**
     * Activates the filter using the specified text description and matcher.
     * This method also hides the filter component.
     */
    @Override
    protected void activate(String activeText, Matcher<E> matcher) {
        super.activate(activeText, matcher);
        getComponent().setVisible(false);
    }
    
    /**
     * Deactivates the filter by clearing the text description and matcher.
     * This method also displays the filter component.
     */
    @Override
    protected void deactivate() {
        super.deactivate();
        getComponent().setVisible(true);
    }
    
    /**
     * Returns a text description of the filter state.
     */
    @Override
    public String toString() {
        StringBuilder buf = new StringBuilder();
        
        buf.append(getClass().getSimpleName()).append("[");
        buf.append("type=").append(filterType);
        buf.append(", property=").append(propertyKey);
        buf.append(", uniqueItems=").append(uniqueList.size());
        buf.append(", active=").append(isActive());
        EventList<Object> selectedList = selectionModel.getSelected();
        buf.append(", selection=").append((selectedList.size() > 0) ? selectedList.get(0) : "null");
        buf.append("]");
        
        return buf.toString();
    }
    
    /**
     * Returns the text string describing the property for the current filter 
     * type and property key. 
     */
    private String getPropertyText() {
        switch (filterType) {
        case EXTENSION:
            return I18n.tr("Extensions");
            
        case PROPERTY:
            switch (propertyKey) {
            case AUTHOR:
                return I18n.tr("Artists");
            case ALBUM:
                return I18n.tr("Albums");
            case GENRE:
                return I18n.tr("Genres");
            default:
                return propertyKey.toString();
            }
            
        case FILE_TYPE:
            return I18n.tr("Types");
            
        default:
            throw new IllegalStateException("Unknown filter type " + filterType);
        }
    }
    
    /**
     * Returns a list of property values in the specified list of items.
     */
    private EventList<Object> createPropertyList(EventList<E> resultsList) {
        switch (filterType) {
        case EXTENSION:
        case PROPERTY:
        case FILE_TYPE:
            // Create list of property values.
            return GlazedListsFactory.simpleFunctionList(resultsList, 
                    new PropertyFunction(filterType, propertyKey));
            
        default:
            throw new IllegalArgumentException("Invalid filter type " + filterType);
        }
    }

    /**
     * Returns a list of non-null values in the specified list of property 
     * values.
     */
    private FilterList<Object> createNonNullList(EventList<Object> propertyList) {
        // Create list of non-null values.
        return GlazedListsFactory.filterList(propertyList, 
            new Matcher<Object>() {
                @Override
                public boolean matches(Object item) {
                    return (item != null);
                }
            }
        );
    }
    
    /**
     * Creates a popup to display the complete list of property values.
     */
    private FilterPopupPanel createMorePopup() {
        FilterPopupPanel popupPanel = new FilterPopupPanel(getResources(), getPropertyText());
        
        // Set list cell renderer.
        popupPanel.setListCellRenderer(new PropertyCellRenderer(popupPanel.getBackground(),
                BorderFactory.createEmptyBorder(1, 4, 0, 1)));
        
        // Set list and selection models.  We use the unique list directly
        // to display values alphabetically.
        DefaultEventListModel<Object> listModel = new DefaultEventListModel<Object>(uniqueList);
        popupSelectionModel = new DefaultEventSelectionModel<Object>(uniqueList);
        popupPanel.setListModel(listModel);
        popupPanel.setListSelectionModel(popupSelectionModel);
        
        // Add selection listener to update filter.
        popupSelectionModel.addListSelectionListener(new SelectionListener(popupSelectionModel));
        
        return popupPanel;
    }
    
    /**
     * Displays the "more" popup that lists all property values.
     */
    private void showMorePopup() {
        if (morePopupPanel == null) {
            morePopupPanel = createMorePopup();
        }
        morePopupPanel.showPopup(moreButton, list.getWidth() - 12, propertyLabel.getY() - moreButton.getY());
    }
    
    /**
     * Hides the "more" popup that lists all property values.
     */
    private void hideMorePopup() {
        if (morePopupPanel != null) {
            morePopupPanel.hidePopup();
        }
    }
    
    /**
     * Action to display list of all property values in popup window.
     */
    private class MoreAction extends AbstractAction {

        public MoreAction() {
            super(I18n.tr("more"));
        }
        
        @Override
        public void actionPerformed(ActionEvent e) {
            if (morePopupPanel == null) {
                showMorePopup();
            } else if (morePopupPanel.isPopupReady()) {
                showMorePopup();
            } else {
                morePopupPanel.setPopupReady(true);
            }
        }
    }
    
    /**
     * Listener to handle selection changes to update the matcher editor.  
     */
    private class SelectionListener implements ListSelectionListener {
        private final DefaultEventSelectionModel<Object> selectionModel;

        public SelectionListener(DefaultEventSelectionModel<Object> selectionModel) {
            this.selectionModel = selectionModel;
        }
        
        @Override
        public void valueChanged(ListSelectionEvent e) {
            // Skip selection change if filter is active.
            if (isActive()) {
                return;
            }
            
            // Get list of selected values.
            EventList<Object> selectedList = selectionModel.getSelected();
            if (selectedList.size() > 0) {
                // Create new matcher and activate.
                Matcher<E> newMatcher = 
                    new PropertyMatcher<E>(filterType, propertyKey, iconManager, selectedList);
                activate(selectedList.get(0).toString(), newMatcher);
                
            } else {
                // Deactivate to clear matcher.
                deactivate();
            }
            
            // Hide popup if showing.
            hideMorePopup();
            
            // Notify filter listeners.
            fireFilterChanged(PropertyFilter.this);
        }
    }
    
    /**
     * Cell renderer for property values.
     */
    private class PropertyCellRenderer extends DefaultListCellRenderer {
        private final Color background;
        private final Border border;
        
        public PropertyCellRenderer(Color background, Border border) {
            this.background = background;
            this.border = border;
        }
        
        @Override
        public Component getListCellRendererComponent(JList list, Object value, 
                int index, boolean isSelected, boolean cellHasFocus) {
            
            Component renderer = super.getListCellRendererComponent(list, value,
                    index, isSelected, cellHasFocus);
            
            if ((renderer instanceof JLabel) && (value != null)) {
                // Get count for property value.
                int count = uniqueList.getCount(value);
                
                // Display count in cell.
                StringBuilder buf = new StringBuilder();
                buf.append(value.toString()).append(" (").append(count).append(")");
                ((JLabel) renderer).setText(buf.toString());
                
                // Set appearance.
                ((JLabel) renderer).setBackground(background);
                ((JLabel) renderer).setBorder(border);
            }
            
            return renderer;
        }
    }
    
    /**
     * Comparator to sort property values by their result count.
     */
    private class PropertyCountComparator implements Comparator<Object> {

        @Override
        public int compare(Object o1, Object o2) {
            int count1 = uniqueList.getCount(o1);
            int count2 = uniqueList.getCount(o2);
            // Return inverse value to sort in descending order.
            return (count1 < count2) ? 1 : ((count1 > count2) ? -1 : 0);
        }
    }
    
    /**
     * A Comparator for values in a specific property.  This is used to create
     * a list of unique property values.
     */
    private static class PropertyComparator implements Comparator<Object> {
        private final FilterType filterType;
        private final FilePropertyKey propertyKey;

        public PropertyComparator(FilterType filterType, FilePropertyKey propertyKey) {
            this.filterType = filterType;
            this.propertyKey = propertyKey;
        }

        @Override
        public int compare(Object o1, Object o2) {
            if ((filterType == FilterType.PROPERTY) && FilePropertyKey.isLong(propertyKey)) {
                Long long1 = (Long) o1;
                Long long2 = (Long) o2;
                return Objects.compareToNull(long1, long2, false);
                
            } else {
                String s1 = (String) o1;
                String s2 = (String) o2;
                return Objects.compareToNullIgnoreCase(s1, s2, false);
            }
        }
    }
    
    /**
     * A function to transform a list of filterable items into a list of
     * specific property values.
     */
    private class PropertyFunction implements Function<E, Object> {
        private final FilterType filterType;
        private final FilePropertyKey propertyKey;

        public PropertyFunction(FilterType filterType, FilePropertyKey propertyKey) {
            this.filterType = filterType;
            this.propertyKey = propertyKey;
        }
        
        @Override
        public Object evaluate(E item) {
            switch (filterType) {
            case EXTENSION:
                return item.getFileExtension().toLowerCase();
            case PROPERTY:
                return item.getProperty(propertyKey);
            case FILE_TYPE:
                return iconManager.get().getMIMEDescription(item.getFileExtension());
            default:
                return null;
            }
        }
    }
}
