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.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;

import javax.swing.AbstractAction;
import javax.swing.Action;
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.SwingUtilities;
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.friend.api.Friend;
import org.limewire.ui.swing.components.HyperlinkButton;
import org.limewire.ui.swing.components.RolloverCursorListener;
import org.limewire.ui.swing.util.I18n;
import org.limewire.util.Objects;

import ca.odell.glazedlists.CollectionList;
import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.SortedList;
import ca.odell.glazedlists.TransformedList;
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;

/**
 * Filter component to select items according to their sources.
 */
class SourceFilter<E extends FilterableItem> extends AbstractFilter<E> {

    private final JPanel panel = new JPanel();
    private final JLabel label = new JLabel();
    private final JXList list = new JXList();
    private final HyperlinkButton moreButton = new HyperlinkButton();
    private final List<FriendListener> friendListenerList = new ArrayList<FriendListener>();
    
    private EventList<SourceItem> sourceList;
    private UniqueListFactory<SourceItem> uniqueSourceListFactory;
    private UniqueList<SourceItem> uniqueSourceList;
    
    private EventList<SourceItem> friendList;
    private UniqueListFactory<SourceItem> uniqueFriendListFactory;
    private UniqueList<SourceItem> uniqueFriendList;
    
    private UniqueList<SourceItem> currentUniqueList;
    private SortedList<SourceItem> sortedList;
    private DefaultEventListModel<SourceItem> listModel;
    private DefaultEventSelectionModel<SourceItem> selectionModel;
    private DefaultEventSelectionModel<SourceItem> popupSelectionModel;
    private SelectionListener selectionListener;
    private FilterPopupPanel filterPopupPanel;

    private boolean anyFriendFound;
    
    /**
     * Constructs a SourceFilter using the specified results list.
     */
    public SourceFilter(EventList<E> resultsList) {
        FilterResources resources = getResources();
        
        // Set up visual components.
        panel.setLayout(new MigLayout("insets 0 0 0 0, gap 0!", 
                "[left,grow]", ""));
        panel.setOpaque(false);
        
        label.setFont(resources.getHeaderFont());
        label.setForeground(resources.getHeaderColor());
        label.setText(I18n.tr("From"));
        
        list.setCellRenderer(new SourceCellRenderer(resources.getBackground(), 
                BorderFactory.createEmptyBorder(1, 7, 0, 7), false));
        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 (filterPopupPanel != null) {
                    filterPopupPanel.setPopupTriggered(true);
                }
            }
            @Override
            public void mouseExited(MouseEvent e) {
                if (filterPopupPanel != null) {
                    filterPopupPanel.setPopupTriggered(false);
                }
            }
        });
        
        // Apply results list to filter.
        initialize(resultsList);
        
        // Calculate max list height.
        list.setPrototypeCellValue("Type");
        int listHeight = 3 * list.getFixedCellHeight();
        
        panel.add(label     , "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 search results.
     */
    private void initialize(EventList<E> resultsList) {
        // Create list of unique source values.
        sourceList = createSourceList(resultsList);
        uniqueSourceListFactory = new UniqueListFactory<SourceItem>(sourceList, new SourceItemComparator());
        uniqueSourceListFactory.setName(I18n.tr("Sources"));
        uniqueSourceList = uniqueSourceListFactory.getUniqueList();
        
        // Create list of unique friend values.
        friendList = createFriendList(resultsList);
        uniqueFriendListFactory = new UniqueListFactory<SourceItem>(friendList, new SourceItemComparator());
        uniqueFriendListFactory.setName(I18n.tr("Friends"));
        uniqueFriendList = uniqueFriendListFactory.getUniqueList();
        
        // Add listener to update unique list in use.
        uniqueSourceList.addListEventListener(new ListEventListener<SourceItem>() {
            @Override
            public void listChanged(ListEvent<SourceItem> listChanges) {
                updateAnonymousFound();
                updateAnyFriendFound();
                updateMoreVisibility();
            }
        });
        
        // Add listener to display "more" button when needed.
        uniqueFriendList.addListEventListener(new ListEventListener<SourceItem>() {
            @Override
            public void listChanged(ListEvent<SourceItem> listChanges) {
                updateMoreVisibility();
            }
        });
        
        // Set unique list for filter.
        currentUniqueList = uniqueFriendList;
        updateMoreVisibility();
        updateFilterList();
    }
    
    /**
     * Updates the filter list using the current unique list.
     */
    private void updateFilterList() {
        // Dispose of old models and lists.
        if (selectionListener != null) list.removeListSelectionListener(selectionListener);
        if (listModel != null) listModel.dispose();
        if (selectionModel != null) selectionModel.dispose();
        if (sortedList != null) sortedList.dispose();
        
        // Create sorted list using current unique list.
        sortedList = GlazedListsFactory.sortedList(currentUniqueList, new SourceItemCountComparator());
        
        // Create list and selection models.
        listModel = new DefaultEventListModel<SourceItem>(sortedList);
        selectionModel = new DefaultEventSelectionModel<SourceItem>(sortedList);
        list.setSelectionModel(selectionModel);
        list.setModel(listModel);
        
        // Add selection listener to update filter.
        selectionListener = new SelectionListener(selectionModel);
        list.addListSelectionListener(selectionListener);
    }
    
    /**
     * Adds the specified listener to the list that is notified when the
     * <code>anyFriendFound</code> state changes.
     */
    public void addFriendListener(FriendListener listener) {
        friendListenerList.add(listener);
    }
    
    /**
     * Removes the specified listener from the list that is notified when the
     * <code>anyFriendFound</code> state changes.
     */
    public void removeFriendListener(FriendListener listener) {
        friendListenerList.remove(listener);
    }
    
    @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 source and friend lists.
        uniqueSourceListFactory.dispose();
        uniqueFriendListFactory.dispose();
        ((TransformedList) sourceList).dispose();
        ((TransformedList) friendList).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(anyFriendFound);
    }
    
    /**
     * Returns a text description of the filter state.
     */
    @Override
    public String toString() {
        StringBuilder buf = new StringBuilder();
        
        buf.append(getClass().getSimpleName()).append("[");
        buf.append("uniqueItems=").append(currentUniqueList.size());
        buf.append(", active=").append(isActive());
        EventList<SourceItem> selectedList = selectionModel.getSelected();
        buf.append(", selection=").append((selectedList.size() > 0) ? selectedList.get(0) : "null");
        buf.append("]");
        
        return buf.toString();
    }
    
    /**
     * Returns a list of SourceItem objects that represent the sources for the 
     * elements in the specified results list.
     */
    private EventList<SourceItem> createSourceList(EventList<E> resultsList) {
        // Create collection list model.
        CollectionList.Model<E, SourceItem> model = new CollectionList.Model<E, SourceItem>() {
            @Override
            public List<SourceItem> getChildren(E parent) {
                List<SourceItem> list = new ArrayList<SourceItem>();
                if (parent.isAnonymous()) {
                    list.add(SourceItem.ANONYMOUS_SOURCE);
                }
                if (parent.getFriends().size() > 0) {
                    list.add(SourceItem.ANY_FRIEND_SOURCE);
                }
                return list;
            }
        };
        
        // Create collection list.
        return GlazedListsFactory.collectionList(resultsList, model);
    }
    
    /**
     * Returns a list of SourceItem objects that represent the friends for the 
     * elements in the specified results list.
     */
    private EventList<SourceItem> createFriendList(EventList<E> resultsList) {
        // Create collection list model for friends.
        CollectionList.Model<E, Friend> model = new CollectionList.Model<E, Friend>() {
            @Override
            public List<Friend> getChildren(E parent) {
                Collection<Friend> friends = parent.getFriends();
                return new ArrayList<Friend>(friends);
            }
        };
        
        // Create collection list for friends.
        CollectionList<E, Friend> collectionList = GlazedListsFactory.collectionList(resultsList, model);
        
        // Create function list.
        return GlazedListsFactory.simpleFunctionList(collectionList, new SourceItemFriendFunction());
    }
    
    /**
     * Determines whether anonymous sources are found, and updates the current
     * unique list being displayed.  When anonymous sources are found, the
     * P2P Network/Any Friends list is used; otherwise, the friends-only list
     * is used.
     */
    private void updateAnonymousFound() {
        // Determine if anonymous sources are found.
        boolean found = uniqueSourceList.contains(SourceItem.ANONYMOUS_SOURCE);
        
        // Update current unique list only if changed.
        UniqueList<SourceItem> newList = found ? uniqueSourceList : uniqueFriendList;
        if (currentUniqueList == newList) return;
        currentUniqueList = newList;
        
        // Update button label.
        moreButton.getAction().putValue(Action.NAME, found ? I18n.tr("friends") : I18n.tr("more"));
        
        // Post Runnable on event queue to update list.  Because this method is
        // handling a listChanged event, we need to post a new event to modify
        // the list.
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                updateFilterList();
            }
        });
    }
    
    /**
     * Determines whether any friend sources are found, and notifies listeners
     * when the state changes.
     */
    private void updateAnyFriendFound() {
        // Determine if any friend sources are found.
        boolean found = uniqueSourceList.contains(SourceItem.ANY_FRIEND_SOURCE);
        
        // Update indicator if necessary.
        if (anyFriendFound == found) return;
        anyFriendFound = found;
        
        // Notify listeners about state change.
        for (int i = 0, size = friendListenerList.size(); i < size; i++) {
            friendListenerList.get(i).friendFound(found);
        }
    }
    
    /**
     * Updates the visibility of the more button.  For the P2P Network/Any 
     * Friends list, the button is always displayed; for the Friends-only list,
     * the button is displayed when there are more than three friends.  
     */
    private void updateMoreVisibility() {
        boolean visible = (currentUniqueList == uniqueSourceList) || (uniqueFriendList.size() > 3);
        
        if (!moreButton.isVisible() && visible) {
            moreButton.setVisible(true);
        } else if (moreButton.isVisible() && !visible) {
            moreButton.setVisible(false);
        }
    }
    
    /**
     * Creates a popup to display the complete list of friends.
     */
    private FilterPopupPanel createFriendPopup() {
        FilterPopupPanel popupPanel = new FilterPopupPanel(getResources(), I18n.tr("Friends"));
        
        // Set list cell renderer.
        popupPanel.setListCellRenderer(new SourceCellRenderer(popupPanel.getBackground(), 
                BorderFactory.createEmptyBorder(1, 4, 0, 1), true));
        
        // Set list and selection models.  We use the unique list directly
        // to display values alphabetically.
        DefaultEventListModel<SourceItem> listModel = new DefaultEventListModel<SourceItem>(uniqueFriendList);
        popupSelectionModel = new DefaultEventSelectionModel<SourceItem>(uniqueFriendList);
        popupPanel.setListModel(listModel);
        popupPanel.setListSelectionModel(popupSelectionModel);
        
        // Add selection listener to update filter.
        popupSelectionModel.addListSelectionListener(new SelectionListener(popupSelectionModel));
        
        return popupPanel;
    }
    
    /**
     * Displays the "friend" popup that lists all friends.
     */
    private void showFriendPopup() {
        if (filterPopupPanel == null) {
            filterPopupPanel = createFriendPopup();
        }
        filterPopupPanel.showPopup(moreButton, list.getWidth() - 12, label.getY() - moreButton.getY());
    }
    
    /**
     * Hides the "friend" popup that lists all friends.
     */
    private void hideFriendPopup() {
        if (filterPopupPanel != null) {
            filterPopupPanel.hidePopup();
        }
    }
    
    /**
     * Action to display list of all friends in popup window.
     */
    private class MoreAction extends AbstractAction {

        public MoreAction() {
            super(I18n.tr("more"));
        }
        
        @Override
        public void actionPerformed(ActionEvent e) {
            if (filterPopupPanel == null) {
                showFriendPopup();
            } else if (filterPopupPanel.isPopupReady()) {
                showFriendPopup();
            } else {
                filterPopupPanel.setPopupReady(true);
            }
        }
    }
    
    /**
     * Listener to handle selection changes to update the matcher editor.  
     */
    private class SelectionListener implements ListSelectionListener {
        private final DefaultEventSelectionModel<SourceItem> selectionModel;
        
        public SelectionListener(DefaultEventSelectionModel<SourceItem> 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<SourceItem> selectedList = selectionModel.getSelected();
            if (selectedList.size() > 0) {
                SourceItem value = selectedList.get(0);
                // Create new matcher and activate.
                Matcher<E> newMatcher = new SourceMatcher<E>(value);
                activate(value.toString(), newMatcher);
                
            } else {
                // Deactivate to clear matcher.
                deactivate();
            }
            
            // Hide popup if showing.
            hideFriendPopup();
            
            // Notify filter listeners.
            fireFilterChanged(SourceFilter.this);
        }
    }
    
    /**
     * Cell renderer for SourceItem values.  Note that the filter list uses the
     * current unique list, while the popup list always uses the unique friend
     * list.
     */
    private class SourceCellRenderer extends DefaultListCellRenderer {
        private final Color background;
        private final Border border;
        private final boolean useFriend;
        
        public SourceCellRenderer(Color background, Border border, boolean useFriend) {
            this.background = background;
            this.border = border;
            this.useFriend = useFriend;
        }
        
        @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 instanceof SourceItem)) {
                // Get count.
                int count = useFriend ? uniqueFriendList.getCount((SourceItem) value) :
                    currentUniqueList.getCount((SourceItem) value);
                
                // Set text.
                StringBuilder buf = new StringBuilder();
                buf.append(((SourceItem) value).getName());
                buf.append(" (").append(count).append(")");
                ((JLabel) renderer).setText(buf.toString());
                
                // Set appearance.
                ((JLabel) renderer).setBackground(background);
                ((JLabel) renderer).setBorder(border);
            }

            return renderer;
        }
    }
    
    /**
     * A Comparator for SourceItem values.
     */
    private static class SourceItemComparator implements Comparator<SourceItem> {

        @Override
        public int compare(SourceItem item1, SourceItem item2) {
            if (item1.getType() == item2.getType()) {
                String name1 = item1.getName();
                String name2 = item2.getName();
                return Objects.compareToNullIgnoreCase(name1, name2, false);
                
            } else if (item1.getType() == SourceItem.Type.ANONYMOUS) {
                return 1;
            } else if (item2.getType() == SourceItem.Type.ANONYMOUS) {
                return -1;
            } else if (item1.getType() == SourceItem.Type.ANY_FRIEND) {
                return 1;
            } else {
                return -1;
            }
        }
    }
    
    /**
     * A Comparator to sort SourceItem values by their result count.
     */
    private class SourceItemCountComparator implements Comparator<SourceItem> {

        @Override
        public int compare(SourceItem item1, SourceItem item2) {
            int count1 = currentUniqueList.getCount(item1);
            int count2 = currentUniqueList.getCount(item2);
            // Return inverse value to sort in descending order.
            return (count1 < count2) ? 1 : ((count1 > count2) ? -1 : 0);
        }
    }
    
    /**
     * A function to transform a list of friends into a list of source 
     * items.
     */
    private static class SourceItemFriendFunction implements Function<Friend, SourceItem> {

        @Override
        public SourceItem evaluate(Friend sourceValue) {
            return new SourceItem(SourceItem.Type.FRIEND, sourceValue.getRenderName());
        }
    }
    
    /**
     * Defines a listener for friend found events. 
     */
    public static interface FriendListener {
        
        void friendFound(boolean found);
        
    }
}
