package org.limewire.ui.swing.search;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GradientPaint;
import java.awt.Insets;
import java.awt.Rectangle;

import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.ScrollPaneConstants;
import javax.swing.Scrollable;
import javax.swing.SwingUtilities;
import javax.swing.table.JTableHeader;

import net.miginfocom.swing.MigLayout;

import org.jdesktop.application.Resource;
import org.jdesktop.jxlayer.JXLayer;
import org.jdesktop.swingx.JXPanel;
import org.jdesktop.swingx.painter.RectanglePainter;
import org.limewire.core.api.search.SearchCategory;
import org.limewire.core.api.search.browse.BrowseStatus;
import org.limewire.setting.evt.SettingEvent;
import org.limewire.setting.evt.SettingListener;
import org.limewire.ui.swing.components.Disposable;
import org.limewire.ui.swing.components.HeaderBar;
import org.limewire.ui.swing.components.decorators.HeaderBarDecorator;
import org.limewire.ui.swing.filter.AdvancedFilterPanel;
import org.limewire.ui.swing.filter.AdvancedFilterPanelFactory;
import org.limewire.ui.swing.filter.AdvancedFilterPanel.CategoryListener;
import org.limewire.ui.swing.friends.refresh.AllFriendsRefreshManager;
import org.limewire.ui.swing.search.SearchResultsMessagePanel.MessageType;
import org.limewire.ui.swing.search.model.SearchResultsModel;
import org.limewire.ui.swing.search.model.VisualSearchResult;
import org.limewire.ui.swing.search.resultpanel.BaseResultPanel.ListViewTable;
import org.limewire.ui.swing.settings.SwingUiSettings;
import org.limewire.ui.swing.table.TableCellHeaderRenderer;
import org.limewire.ui.swing.util.CategoryIconManager;
import org.limewire.ui.swing.util.GuiUtils;
import org.limewire.ui.swing.util.I18n;

import ca.odell.glazedlists.event.ListEvent;
import ca.odell.glazedlists.event.ListEventListener;

import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;

/**
 * This is the top-level container for the search results display.  
 * SearchResultsPanel contains several UI components, including the category
 * tab items, sort and filter panel, sponsored results panel, and search
 * results tables.
 */
public class SearchResultsPanel extends JXPanel implements Disposable {
    
    /**
     * The type of overlay which should be placed over the search results.

     * NONE indicates that no overlay should be shown.
     * AWAITING_CONNECTIONS indicates that an overlay with a busy icon and a "WireShare is connecting..." message should be shown.
     * NO_FRIENDS_ON_LIMEWIRE indicates that friends' files cannot be shown b/c no friends are logged on
     */
    public enum OverlayType {
        NONE, 
        AWAITING_CONNECTIONS,
        NO_FRIENDS_ON_LIMEWIRE
    }

    /** Decorator used to set the appearance of the header bar. */
    private final HeaderBarDecorator headerBarDecorator;
    
    /** Icon manager for categories. */
    private final CategoryIconManager categoryIconManager;
    
    /** Label that displays the search title. */
    private final JLabel searchTitleLabel = new JLabel();
    
    /** Panel containing filter components. */
    private final AdvancedFilterPanel<VisualSearchResult> filterPanel;
    
    /**
     * This is the subpanel that displays the actual search results.
     */
    private final ResultsContainer resultsContainer;
       
    /**
     * This is the subpanel that appears in the upper-right corner
     * of each search results tab.
     */
    private final SortAndFilterPanel sortAndFilterPanel;
    
    /** The scroll pane embedding the search results & sponsored results. */
    private JScrollPane scrollPane;
    
    /** The ScrollablePanel that the scroll pane is embedding. */
    private ScrollablePanel scrollablePanel;
       
    /** The panel for showing messages and a pointer to the classic search results view button. */
    private final SearchResultsMessagePanel messagePanel;
    
    /** This is the white gap which appears between the message panel and the search results */
    private Component messagePanelsGap;
    
    /** Listener for changes in the view type. */
    private final SettingListener viewTypeListener;
    
    /** Listener for updates to the result count. */
    private final ListEventListener<VisualSearchResult> resultCountListener;
    
    /** Search results data model. */
    private final SearchResultsModel searchResultsModel;
    
    @Resource private Color tabHighlightTopGradientColor;
    @Resource private Color tabHighlightBottomGradientColor;

    private boolean lifeCycleComplete = true;

    private boolean fullyConnected = true;

    private boolean receivedSponsoredResults = false;

    private boolean receivedSearchResults = false;
    
    private BrowseStatus browseStatus = null;
    
    /** Shows status of failed browses and refresh button.
     */
    private BrowseStatusPanel browseStatusPanel;
    
    /** Title when browsing friends; null for search results. */
    private String browseTitle;

    private final BrowseFailedMessagePanel browseFailedPanel;

    /** This class has a JXLayer as its sole component. The JXLayer has the search results components
        as its main panel and sometimes places overlays over these components. */
    private final JXLayer jxlayer;
    
    /** This is the active OverlayType for the JXLayer */
    private OverlayType overlayType = OverlayType.NONE;
    
    private SettingListener messagePanelGapHider;
    
    /**
     * Constructs a SearchResultsPanel with the specified components.
     */
    @Inject
    public SearchResultsPanel(
            @Assisted SearchResultsModel searchResultsModel,
            ResultsContainerFactory containerFactory,
            SortAndFilterPanelFactory sortAndFilterFactory,
            AdvancedFilterPanelFactory<VisualSearchResult> filterPanelFactory,
            HeaderBarDecorator headerBarDecorator,
            CategoryIconManager categoryIconManager, 
            BrowseFailedMessagePanelFactory browseFailedMessagePanelFactory,
            AllFriendsRefreshManager allFriendsRefreshManager) {
        super(new BorderLayout());
        
        GuiUtils.assignResources(this);
        
        this.searchResultsModel = searchResultsModel;
        this.headerBarDecorator = headerBarDecorator; 
        this.categoryIconManager = categoryIconManager;
        
        this.browseFailedPanel = browseFailedMessagePanelFactory.create(searchResultsModel);
        
        // Create sort and filter components.
        sortAndFilterPanel = sortAndFilterFactory.create(searchResultsModel);
        
        filterPanel = filterPanelFactory.create(searchResultsModel, searchResultsModel.getSearchType());
        
        scrollPane = new JScrollPane();
        scrollPane.setBorder(BorderFactory.createEmptyBorder());
        scrollablePanel = new ScrollablePanel();
        configureEnclosingScrollPane();
        
        // Create results container with tables.
        resultsContainer = containerFactory.create(searchResultsModel);
        
        viewTypeListener = new SettingListener() {
            int oldSearchViewTypeId = SwingUiSettings.SEARCH_VIEW_TYPE_ID.getValue();
            @Override
            public void settingChanged(SettingEvent evt) {
               SwingUtilities.invokeLater(new Runnable() {
                   @Override
                   public void run() {
                       int newSearchViewTypeId = SwingUiSettings.SEARCH_VIEW_TYPE_ID.getValue();
                       if(newSearchViewTypeId != oldSearchViewTypeId) {
                           SearchViewType newSearchViewType = SearchViewType.forId(newSearchViewTypeId);
                           resultsContainer.setViewType(newSearchViewType);
                           syncScrollPieces();
                           oldSearchViewTypeId = newSearchViewTypeId;
                       }
                   }               
               });
            } 
        };
        SwingUiSettings.SEARCH_VIEW_TYPE_ID.addSettingListener(viewTypeListener);
        
        // Initialize header label.
        updateTitle();
        
        // Install listener to update header label.
        resultCountListener = new ListEventListener<VisualSearchResult>() {
            @Override
            public void listChanged(ListEvent<VisualSearchResult> listChanges) {
                // these updates are coming in on the AWT thread. so, it's safe to make GUI updates here.
                updateTitle();

                receivedSearchResults = (SearchResultsPanel.this.searchResultsModel.getUnfilteredList().size() > 0);

                updateMessages();
            }
        };
        searchResultsModel.getUnfilteredList().addListEventListener(resultCountListener);
        searchResultsModel.getFilteredList().addListEventListener(resultCountListener);
        
        // Configure sort panel and results container.
        sortAndFilterPanel.setSearchCategory(searchResultsModel.getSearchCategory());
        resultsContainer.showCategory(searchResultsModel.getSearchCategory());
        syncScrollPieces();
        
        // Configure advanced filters.
        filterPanel.setSearchCategory(searchResultsModel.getSearchCategory());
        filterPanel.addCategoryListener(new CategoryListener() {
            @Override
            public void categorySelected(SearchCategory displayCategory) {
                sortAndFilterPanel.setSearchCategory(displayCategory);
                resultsContainer.showCategory(displayCategory);
                syncScrollPieces();
                updateTitle();
            }
        });

        browseStatusPanel = new BrowseStatusPanel(searchResultsModel, allFriendsRefreshManager);
        
        messagePanel = new SearchResultsMessagePanel();
        
        // if the user closes the hint showing where the classic search view is by clicking on the hint's close button,
        // we need to hear about that in this class so that we can make the gap separating the message box from
        // the search results disappear
        if (messagePanel.isShowClassicSearchResultsHint()) {
            messagePanelGapHider = new SettingListener() {
                @Override
                public void settingChanged(SettingEvent evt) {
                    if (!SwingUiSettings.SHOW_CLASSIC_REMINDER.getValue()) {
                        messagePanelsGap.setVisible(false);
                    }
                }
            };
            SwingUiSettings.SHOW_CLASSIC_REMINDER.addSettingListener(messagePanelGapHider);
        }
        
        jxlayer = new JXLayer<JComponent>(createSearchResultsPanel());
        jxlayer.getGlassPane().setLayout(new BorderLayout());
        
        add(jxlayer, BorderLayout.CENTER);
    }
    
    /**
     * This method controls whether an overlay should be shown and if so, which type of overlay.
     * 
     * @param overlayType -- the overlay type. See the type definition for more info.
     */
    void setOverlayType(OverlayType overlayType) {
        // this method can be called multiple times for the same overlay type.
        // so, let's check to make sure that the overlay actually needs to change
        // before installing or uninstalling an overlay panel.
        if (this.overlayType != overlayType) {
            this.overlayType = overlayType;
            switch (overlayType) {
            case AWAITING_CONNECTIONS:
                installOverlay(new AwaitingConnectionsPanel(headerBarDecorator));
                break;
                
            case NO_FRIENDS_ON_LIMEWIRE:
                installOverlay(browseFailedPanel);
                break;
                
            case NONE:
                uninstallOverlay();
                break;
                
            default:
                throw new IllegalStateException("invalid type: " + overlayType); 
            }
        }
    }
          
    private void installOverlay(JComponent component) {
        jxlayer.getGlassPane().setVisible(false);
        jxlayer.getGlassPane().removeAll();
        jxlayer.getGlassPane().add(component);
        jxlayer.getGlassPane().setVisible(true);
    }
    
    private void uninstallOverlay() {
        jxlayer.getGlassPane().setVisible(false);
        jxlayer.getGlassPane().removeAll();
    }
    

    /**
     * Disposes of resources used by the container.  This method is called when 
     * the search is closed.
     */
    @Override
    public void dispose() {
        SwingUiSettings.SEARCH_VIEW_TYPE_ID.removeSettingListener(viewTypeListener);
        if (messagePanelGapHider != null) SwingUiSettings.SHOW_CLASSIC_REMINDER.removeSettingListener(messagePanelGapHider);
        searchResultsModel.getFilteredList().removeListEventListener(resultCountListener);
        searchResultsModel.getUnfilteredList().removeListEventListener(resultCountListener);
        sortAndFilterPanel.dispose();
        filterPanel.dispose();
        messagePanel.dispose();
        browseFailedPanel.dispose();
        searchResultsModel.dispose();
        browseStatusPanel.dispose();
    }
    
    /**
     * @return the SearchResultsModel of the SearchResultsPanel.
     */
    public SearchResultsModel getModel(){
        return searchResultsModel;
    }

    /**
     * Fills in the top right corner if a scrollbar appears with an empty table
     * header.
     */
    protected void configureEnclosingScrollPane() {
        JTableHeader th = new JTableHeader();
        th.setDefaultRenderer(new TableCellHeaderRenderer());
        // Put a dummy header in the upper-right corner.
        final Component renderer = th.getDefaultRenderer().getTableCellRendererComponent(null, "", false, false, -1, -1);
        JPanel cornerComponent = new JPanel(new BorderLayout());
        cornerComponent.add(renderer, BorderLayout.CENTER);
        scrollPane.setCorner(ScrollPaneConstants.UPPER_RIGHT_CORNER, cornerComponent);
    }
    
    /**
     * Sets the browse title in the container.  When not null, the browse title
     * is displayed at the top of the panel.  When null, the container displays 
     * the search title from the data model.
     */
    public void setBrowseTitle(String title) {
        browseTitle = title;
        updateTitle();
    }
    
    /**
     * Updates the title icon and text in the container.  For search results, 
     * the title includes the category name, search title, and result counts. 
     */
    private void updateTitle() {
        // Get result counts.
        int total = searchResultsModel.getUnfilteredList().size();
        int actual = searchResultsModel.getFilteredList().size();
        
        if (browseTitle != null) {
            // Set browse title.
            searchTitleLabel.setText((actual == total) ?
                    // {0}: browse title, {1}: total count
                    I18n.tr("Browse {0} ({1})", browseTitle, total) :
                    // {0}: browse title, {1}: actual count, {2}: total count 
                    I18n.tr("Browse {0} - Showing {1} of {2}", browseTitle, actual, total));
            
        } else {
            // Get search category and title.
            SearchCategory displayCategory = searchResultsModel.getSelectedCategory();
            String title = searchResultsModel.getSearchTitle();
            
            // Set title icon based on category.
            Icon icon = (displayCategory == SearchCategory.ALL) ? null :
                categoryIconManager.getIcon(displayCategory.getCategory());
            searchTitleLabel.setIcon(icon);

            // Set title text.
            switch (displayCategory) {
            case ALL:
                searchTitleLabel.setText((actual == total) ?
                        // {0}: search title, {1}: total count
                        I18n.tr("All results for {0} ({1})", title, total) :
                        // {0}: search title, {1}: actual count, {2}: total count 
                        I18n.tr("All results for {0} - Showing {1} of {2}", title, actual, total));
                break;
            case AUDIO:
                searchTitleLabel.setText((actual == total) ?
                        // {0}: search title, {1}: total count
                        I18n.tr("Audio results for {0} ({1})", title, total) :
                        // {0}: search title, {1}: actual count, {2}: total count 
                        I18n.tr("Audio results for {0} - Showing {1} of {2}", title, actual, total));
                break;
            case VIDEO:
                searchTitleLabel.setText((actual == total) ?
                        // {0}: search title, {1}: total count
                        I18n.tr("Video results for {0} ({1})", title, total) :
                        // {0}: search title, {1}: actual count, {2}: total count 
                        I18n.tr("Video results for {0} - Showing {1} of {2}", title, actual, total));
                break;
            case IMAGE:
                searchTitleLabel.setText((actual == total) ?
                        // {0}: search title, {1}: total count
                        I18n.tr("Image results for {0} ({1})", title, total) :
                        // {0}: search title, {1}: actual count, {2}: total count 
                        I18n.tr("Image results for {0} - Showing {1} of {2}", title, actual, total));
                break;
            case DOCUMENT:
                searchTitleLabel.setText((actual == total) ?
                        // {0}: search title, {1}: total count
                        I18n.tr("Document results for {0} ({1})", title, total) :
                        // {0}: search title, {1}: actual count, {2}: total count 
                        I18n.tr("Document results for {0} - Showing {1} of {2}", title, actual, total));
                break;
            case PROGRAM:
                searchTitleLabel.setText((actual == total) ?
                        // {0}: search title, {1}: total count
                        I18n.tr("Program results for {0} ({1})", title, total) :
                        // {0}: search title, {1}: actual count, {2}: total count 
                        I18n.tr("Program results for {0} - Showing {1} of {2}", title, actual, total));
                break;
            case OTHER:
                searchTitleLabel.setText((actual == total) ?
                        // {0}: search title, {1}: total count
                        I18n.tr("Other results for {0} ({1})", title, total) :
                        // {0}: search title, {1}: actual count, {2}: total count 
                        I18n.tr("Other results for {0} - Showing {1} of {2}", title, actual, total));
                break;
            case TORRENT:
                searchTitleLabel.setText((actual == total) ?
                        // {0}: search title, {1}: total count
                        I18n.tr("Torrent results for {0} ({1})", title, total) :
                        // {0}: search title, {1}: actual count, {2}: total count 
                        I18n.tr("Torrent results for {0} - Showing {1} of {2}", title, actual, total));
                break;
            default:
                throw new IllegalStateException("Invalid search category " + displayCategory);
            }
        }
    }
    
    /**
     * Updates the column header component in the scroll pane.  This depends on
     * the current results view and whether the sponsored results are visible. 
     */
    private void syncColumnHeader() {
        Component resultHeader = resultsContainer.getScrollPaneHeader();
        scrollPane.setColumnHeaderView(resultHeader);
        
        scrollPane.validate();
        
        // Resize and repaint table header.  This eliminates visual issues due
        // to a change in the table format, which can result in an incorrect
        // header height or header flickering when a category is selected.
        if (resultHeader instanceof JTableHeader) {
            ((JTableHeader) resultHeader).resizeAndRepaint();
        }
    }
    
    /**
     * Initializes the components and adds them to the container.  Called by
     * the constructor.  
     */
    private JPanel createSearchResultsPanel() {
        JPanel searchResultsComponentsPanel = new JPanel( new MigLayout("hidemode 2, insets 0 0 0 0, gap 0!, novisualpadding", 
                                                                        "[][grow]",        // col constraints
                                                                        "[][][][grow]") ); // row constraints
        
        searchResultsComponentsPanel.setMinimumSize(new Dimension(searchResultsComponentsPanel.getPreferredSize().width, 33));
        
        RectanglePainter tabHighlight = new RectanglePainter();
        tabHighlight.setFillPaint(new GradientPaint(20.0f, 0.0f, tabHighlightTopGradientColor, 
                                                    20.0f, 33.0f, tabHighlightBottomGradientColor));

        tabHighlight.setInsets(new Insets(0,0,1,0));
        tabHighlight.setBorderPaint(null);
        
        HeaderBar header = new HeaderBar(searchTitleLabel);
        header.setLayout(new MigLayout("insets 0, gap 0!, novisualpadding, alignx 100%, aligny 50%"));
        header.add(browseStatusPanel, "alignx 0%, growx, pushx");
        headerBarDecorator.decorateBasic(header);
        
        sortAndFilterPanel.layoutComponents(header);
        
        searchResultsComponentsPanel.add(header, "spanx 2, growx, growy, wrap");
        searchResultsComponentsPanel.add(filterPanel, "grow, spany 3");
        searchResultsComponentsPanel.add(messagePanel, "spanx 1, growx, wrap");
        messagePanelsGap = Box.createVerticalStrut(6);
        searchResultsComponentsPanel.add(messagePanelsGap, "hidemode 3, spanx 1, growx, wrap");
        searchResultsComponentsPanel.add(scrollPane , "hidemode 3, grow, spany");

        scrollablePanel.setScrollableTracksViewportHeight(false);

        scrollablePanel.setLayout(new BorderLayout());
        scrollablePanel.add(resultsContainer, BorderLayout.CENTER);
        scrollPane.setViewportView(scrollablePanel);
        
        syncScrollPieces();
        
        return searchResultsComponentsPanel;
    }
    
    /**
     * Updates the view components in the scroll pane. 
     */
    private void syncScrollPieces() {
        scrollablePanel.setScrollable(resultsContainer.getScrollable());
        syncColumnHeader();
    }
    
    /**
     * Panel used as the viewport view in the scroll pane.  This contains the
     * results table and sponsored results panel in a single, scrollable area.
     */
    private class ScrollablePanel extends JXPanel {
        private Scrollable scrollable;

        public void setScrollable(Scrollable scrollable) {
            this.scrollable = scrollable;
        }
        
        @Override
        public Dimension getPreferredSize() {
            if(scrollable == null) {
                return super.getPreferredSize();
            } else {
                int width = super.getPreferredSize().width;
                int height = ((JComponent)scrollable).getPreferredSize().height;
                
                // the list view has some weird rendering sometimes (double space after last result)
                // so don't fill full screen on list view
                if(! (scrollable instanceof ListViewTable)) {
                    int headerHeight = 0;
                    
                    //the table headers aren't being set on the scrollpane, so if its visible check its
                    // height and subtract it from the viewport size
                    JTableHeader header = ((JTable)scrollable).getTableHeader();
                    if(header != null && header.isShowing()) {
                        headerHeight = header.getHeight();
                    }
                    
                    // if the height of table is less than the scrollPane height, set preferred height
                    // to same size as scrollPane
                    if(height < scrollPane.getSize().height - headerHeight) {
                        height = scrollPane.getSize().height - headerHeight;
                    }
                }
                return new Dimension(width, height);
            }
        }
        
        @Override
        public Dimension getPreferredScrollableViewportSize() {
            return getPreferredSize();
        }

        @Override
        public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
            if(scrollable == null) {
                return super.getScrollableUnitIncrement(visibleRect, orientation, direction);
            } else {
                return scrollable.getScrollableUnitIncrement(visibleRect, orientation, direction);
            }
        }

        @Override
        public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
            if(scrollable == null) {
                return super.getScrollableBlockIncrement(visibleRect, orientation, direction);
            } else {
                return scrollable.getScrollableBlockIncrement(visibleRect, orientation, direction);
            }
        }
    }

    /**
     * Sets an indicator to determine whether the application has finished
     * loading, and updates the user message.
     */
    public void setLifeCycleComplete(boolean lifeCycleComplete) {
        this.lifeCycleComplete = lifeCycleComplete;
        updateMessages();
    }

    /**
     * Sets an indicator to determine whether the application is fully 
     * connected to the P2P Network, and updates the user message.
     */
    public void setFullyConnected(boolean fullyConnected) {
        this.fullyConnected = fullyConnected;
        updateMessages();        
    }
    
    public void setBrowseStatus(BrowseStatus browseStatus){
        this.browseStatus = browseStatus;
        updateMessages();        
    }
    
    /**
     * Updates the user message based on the current state of the application. 
     */
    private void updateMessages() {
        browseStatusPanel.setBrowseStatus(browseStatus);

        // let's check whether we need to show the user any messages
        if (!fullyConnected && (receivedSearchResults || receivedSponsoredResults)) {
            messagePanel.setMessageType(MessageType.CONNECTING_TO_ULTRAPEERS);
            messagePanelsGap.setVisible(true);
        } else if (fullyConnected && receivedSearchResults && messagePanel.isShowClassicSearchResultsHint()) {
            messagePanel.setMessageType(MessageType.CLASSIC_SEARCH_RESULTS_HINT);            
            messagePanelsGap.setVisible(true);
        } else {
            messagePanel.setMessageType(MessageType.NONE);
            messagePanelsGap.setVisible(false);
        }
       
        // let's check whether we need to put an overlay over the search results panel
        if ( (!lifeCycleComplete || !fullyConnected ) && (!receivedSearchResults && !receivedSponsoredResults) ) {
            setOverlayType(OverlayType.AWAITING_CONNECTIONS);
        } else if (browseStatus != null && !browseStatus.getState().isOK()) {
            browseFailedPanel.update(browseStatus.getState(), browseStatus.getBrowseSearch(), browseStatus.getFailedFriends());
            setOverlayType(OverlayType.NO_FRIENDS_ON_LIMEWIRE);           
        } else {
            setOverlayType(OverlayType.NONE);            
        }
    }
}
