package org.limewire.ui.swing.mainframe;

import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

import net.miginfocom.swing.MigLayout;

import org.jdesktop.application.Resource;
import org.jdesktop.swingx.JXButton;
import org.jdesktop.swingx.JXPanel;
import org.limewire.core.api.search.Search;
import org.limewire.core.api.search.SearchCategory;
import org.limewire.core.api.search.SearchListener;
import org.limewire.core.api.search.SearchResult;
import org.limewire.core.api.search.SearchDetails.SearchType;
import org.limewire.core.api.search.browse.BrowseSearch;
import org.limewire.ui.swing.components.Disposable;
import org.limewire.ui.swing.components.FlexibleTabList;
import org.limewire.ui.swing.components.FlexibleTabListFactory;
import org.limewire.ui.swing.components.NoOpAction;
import org.limewire.ui.swing.components.SelectableJXButton;
import org.limewire.ui.swing.components.TabActionMap;
import org.limewire.ui.swing.components.decorators.ButtonDecorator;
import org.limewire.ui.swing.friends.refresh.AllFriendsRefreshManager;
import org.limewire.ui.swing.library.LibraryMediator;
import org.limewire.ui.swing.nav.NavCategory;
import org.limewire.ui.swing.nav.NavItem;
import org.limewire.ui.swing.nav.NavItemListener;
import org.limewire.ui.swing.nav.NavMediator;
import org.limewire.ui.swing.nav.NavSelectable;
import org.limewire.ui.swing.nav.NavigationListener;
import org.limewire.ui.swing.nav.Navigator;
import org.limewire.ui.swing.nav.NavigatorUtils;
import org.limewire.ui.swing.painter.factories.BarPainterFactory;
import org.limewire.ui.swing.painter.factories.SearchTabPainterFactory;
import org.limewire.ui.swing.search.DefaultSearchInfo;
import org.limewire.ui.swing.search.DefaultSearchRepeater;
import org.limewire.ui.swing.search.KeywordAssistedSearchBuilder;
import org.limewire.ui.swing.search.SearchBar;
import org.limewire.ui.swing.search.SearchHandler;
import org.limewire.ui.swing.search.SearchInfo;
import org.limewire.ui.swing.search.SearchNavItem;
import org.limewire.ui.swing.search.SearchNavigator;
import org.limewire.ui.swing.search.SearchResultMediator;
import org.limewire.ui.swing.search.KeywordAssistedSearchBuilder.CategoryOverride;
import org.limewire.ui.swing.search.model.SearchResultsModel;
import org.limewire.ui.swing.util.GuiUtils;
import org.limewire.ui.swing.util.I18n;
import org.limewire.util.StringUtils;

import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;

@Singleton
class TopPanel extends JXPanel implements SearchNavigator {
    
    @Resource private Icon browseIcon;
    
    private final JXButton friendButton;
    private final SearchBar searchBar;    

    private final FlexibleTabList searchList;
    private final Navigator navigator;
    private final NavItem libraryNav;
    private final Provider<KeywordAssistedSearchBuilder> keywordAssistedSearchBuilder;

    private final String repeatSearchTitle = I18n.tr("Repeat Search");
    private final String refreshBrowseTitle = I18n.tr("Refresh");
    
    /** Maximum number of tabs open in this session*/
    private volatile long maxTabCount = 0;

    private final AllFriendsRefreshManager allFriendsRefreshManager;    
    
    @Inject
    public TopPanel(SearchHandler searchHandler,
                    Navigator navigator,
                    SearchBar searchBar,
                    FlexibleTabListFactory tabListFactory,
                    BarPainterFactory barPainterFactory,
                    SearchTabPainterFactory tabPainterFactory,
                    final LibraryMediator myLibraryMediator,
                    Provider<KeywordAssistedSearchBuilder> keywordAssistedSearchBuilder,
                    Provider<FriendsButton> friendsButtonProvider,
                    AllFriendsRefreshManager allFriendsRefreshManager,
                    ButtonDecorator buttonDecorator) {        
        GuiUtils.assignResources(this);
        this.searchBar = searchBar;
        this.navigator = navigator;
        this.searchBar.addSearchActionListener(new Searcher(searchHandler));        
        this.keywordAssistedSearchBuilder = keywordAssistedSearchBuilder;
        this.allFriendsRefreshManager = allFriendsRefreshManager;        
        setName("WireframeTop");
        
        setBackgroundPainter(barPainterFactory.createTopBarPainter());
        
        libraryNav = navigator.createNavItem(NavCategory.LIBRARY, LibraryMediator.NAME, myLibraryMediator);
        JXButton libraryButton = new SelectableJXButton(NavigatorUtils.getNavAction(libraryNav));
        
        libraryButton.setName("WireframeTop.libraryButton");
        
        libraryButton.setText(I18n.tr("My Files"));
        buttonDecorator.decorateBasicHeaderButton(libraryButton);
                
        friendButton = friendsButtonProvider.get();
        friendButton.setName("WireframeTop.friendsButton");
               
        searchList = tabListFactory.create();
        searchList.setName("WireframeTop.SearchList");
        searchList.setCloseAllText(I18n.tr("Close All Searches"));
        searchList.setCloseOneText(I18n.tr("Close Search"));
        searchList.setCloseOtherText(I18n.tr("Close Other Searches"));
        searchList.setRemovable(true);
        searchList.setSelectionPainter(tabPainterFactory.createSelectionPainter());
        searchList.setHighlightPainter(tabPainterFactory.createHighlightPainter());
        searchList.setNormalPainter(tabPainterFactory.createNormalPainter());
        searchList.setTabInsets(new Insets(0,10,2,10));
        
        setLayout(new MigLayout("gap 0, insets 0, fill, alignx leading"));
        add(libraryButton, "gapleft 5, gapbottom 2, gaptop 0");
        add(friendButton, "gapleft 3, gapbottom 2, gaptop 0");

        add(searchBar, "gapleft 11, gapbottom 2, gaptop 1");
        add(searchList.getComponent(), "gapleft 10, gaptop 4, gapbottom 0, grow, push");
        
        navigator.addNavigationListener(new NavigationListener() {
            @Override
            public void categoryRemoved(NavCategory category, boolean wasSelected) {
                if(wasSelected && category == NavCategory.SEARCH_RESULTS) {
                    goHome();
                }
            }
            
            @Override public void categoryAdded(NavCategory category) {}
            @Override public void itemAdded(NavCategory category, NavItem navItem) {}
            @Override public void itemRemoved(NavCategory category, NavItem navItem, boolean wasSelected) {}
            
            @Override public void itemSelected(NavCategory category, NavItem navItem, NavSelectable selectable, NavMediator navMediator) {
                if(category != NavCategory.SEARCH_RESULTS) {
                    TopPanel.this.searchBar.setText("");
                }
            }
      });
    }
    
    @Override
    public boolean requestFocusInWindow() {
        return searchBar.requestFocusInWindow();
    }
    

    @Override
    public SearchNavItem addSearch(String title, final JComponent searchPanel, final BrowseSearch search, SearchResultsModel model) {
        return addSearch(title, searchPanel, search, model, createBrowseActions(search, model), browseIcon, false);
    }

    @Override
    public SearchNavItem addSearch(String title, final JComponent searchPanel, final Search search, SearchResultsModel model) {
        return addSearch(title, searchPanel, search, model, createSearchActions(search, model), null, true);
    }
    
    @Override
    public void openAdvancedSearch() {
        searchBar.selectAdvancedSearch();
    }
    
    private SearchNavItem addSearch(String title, final JComponent searchPanel, final Search search, SearchResultsModel model, 
            List<Action> contextActions, Icon icon, boolean stopSpinnerAfter50Results) {
        NavItem item = navigator.createNavItem(NavCategory.SEARCH_RESULTS, title, new SearchResultMediator(searchPanel));
        SearchAction action = new SearchAction(item);
        action.putValue(Action.LARGE_ICON_KEY, icon);
        search.addSearchListener(action);

        Action moreTextAction = new NoOpAction();
        action.putValue(Action.LONG_DESCRIPTION, action.getValue(Action.NAME));      

        TabActionMap actionMap = new TabActionMap(action, action, moreTextAction, contextActions);       
        searchList.addTabActionMapAt(actionMap, 0);
        maybeIncrementMaxTabCount();
        
        String searchText = "";
        if(model.getSearchType() == SearchType.KEYWORD) {
            searchText = title;
        }
        item.addNavItemListener(new SearchTabNavItemListener(action, actionMap, (Disposable)searchPanel, searchText, search.getCategory()));
        
        return new SearchNavItemImpl(item, action, stopSpinnerAfter50Results);
    }

    public void goHome() {
        libraryNav.select();
    }

    private List<Action> createSearchActions(Search search, SearchResultsModel model){
        final Action moreResults = new MoreResultsAction(search).register();
        final Action repeatSearch = new RepeatSearchAction(repeatSearchTitle, search, model).register();
        final Action stopSearch = new StopSearchAction(search).register();
        return Arrays.asList(stopSearch, repeatSearch, TabActionMap.SEPARATOR, moreResults);
    }
    
    private List<Action> createBrowseActions(Search search, SearchResultsModel model){
        final Action refresh = new RepeatSearchAction(refreshBrowseTitle, search, model).register();
        return Arrays.asList(refresh);
        
    }
    
    /** Keeps track of the maximum number of tabs open in this session for inspections*/
    private void maybeIncrementMaxTabCount() {
        if(searchList.getTabs().size() > maxTabCount){
            maxTabCount = searchList.getTabs().size();
        }
    }
    
    /**
     * Specialized NavItemListener that handles disposing the SearchPanel after a search is closed, 
     * removes the panel from the search list. and updates the search bar text and category when a 
     * new search tab is selected. 
     */
    private class SearchTabNavItemListener implements NavItemListener {
        private final SearchAction action;
        private final TabActionMap actionMap;
        private final String searchText;
        private final Disposable panel;
        private final SearchCategory category;

        private SearchTabNavItemListener(SearchAction action, TabActionMap actionMap, Disposable panel, String searchText, SearchCategory category) {
            this.action = action;
            this.actionMap = actionMap;
            this.searchText = searchText;
            this.panel = panel;
            this.category = category;
        }

        @Override
        public void itemRemoved(boolean wasSelected) {
            searchList.removeTabActionMap(actionMap);
            panel.dispose();
            if(searchList.getTabs().size() == 0){
                searchBar.setText(null);
            }
        }

        @Override
        public void itemSelected(boolean selected) {
            searchBar.setText(searchText);
            searchBar.setCategory(category);
            action.putValue(Action.SELECTED_KEY, selected);
        }
    }

    private final static class SearchNavItemImpl implements SearchNavItem {
        private final NavItem item;
        private final SearchAction action;
        private final boolean stopSpinnerAfter50Results;

        private SearchNavItemImpl(NavItem item, SearchAction action, boolean stopSpinnerAfter50Results) {
            this.item = item;
            this.action = action;
            this.stopSpinnerAfter50Results = stopSpinnerAfter50Results;
        }

        @Override
        public void sourceCountUpdated(int newSourceCount) {
            if(!item.isSelected()) {
                action.putValue(TabActionMap.NEW_HINT, true);
            }

            if (stopSpinnerAfter50Results && newSourceCount >= 50) {
                action.killBusy();
            }
        }

        @Override
        public String getId() {
            return item.getId();
        }

        @Override
        public void remove() {
            item.remove();
        }

        @Override
        public void select() {
            select(null);
        }

        @Override
        public void select(NavSelectable selectable) {
            item.select();
        }

        @Override
        public void addNavItemListener(NavItemListener listener) {
            item.addNavItemListener(listener);
        }

        @Override
        public void removeNavItemListener(NavItemListener listener) {
            item.removeNavItemListener(listener);
        }

        @Override
        public boolean isSelected() {
            return item.isSelected();
        }
    }

    private class Searcher implements ActionListener {
        private final SearchHandler searchHandler;
        
        Searcher(SearchHandler searchHandler) {
            this.searchHandler = searchHandler;
        }
        
        @Override
        public void actionPerformed(ActionEvent e) {
            // Get search text, and do search if non-empty.
            String searchText = searchBar.getSearchText();
            if (!searchText.isEmpty()) {
                
                SearchCategory category = searchBar.getCategory();
                String query = searchText;
                
                // Check if the category was overridden by a keyword 
                CategoryOverride categoryOverride = keywordAssistedSearchBuilder.get().parseCategoryOverride(query);
                
                // Do not allow searches in the Other category
                if (categoryOverride != null && categoryOverride.getCategory() != SearchCategory.OTHER) {
                    
                    // Set new category and trim the category text out of the search string
                    //  for the actual search
                    category = categoryOverride.getCategory();
                    query = categoryOverride.getCutQuery();

                    // Update the UI with the new category
                    searchBar.setCategory(category);
                }
                    
                // Attempt to parse an advanced search from the search query
                SearchInfo search = keywordAssistedSearchBuilder.get().attemptToCreateAdvancedSearch(
                        query, category);
                
                // Fall back on the normal search
                if (search == null) {
                    search = DefaultSearchInfo.createKeywordSearch(query, category);
                } 
                
                if(searchHandler.doSearch(search)) {
                    searchBar.selectAllSearchText();
                }
            }
        }
    }
    
    private class SearchAction extends AbstractAction implements SearchListener {
        private final NavItem item;
        private Timer busyTimer;
        
        public SearchAction(NavItem item) {
            super(item.getId());
            this.item = item;

            // Make sure this syncs up with any changes in selection.
            addPropertyChangeListener(new PropertyChangeListener() {
                @Override
                public void propertyChange(PropertyChangeEvent evt) {
                    if (evt.getPropertyName().equals(Action.SELECTED_KEY)) {
                        if (evt.getNewValue().equals(Boolean.TRUE)) {
                            SearchAction.this.item.select();
                            putValue(TabActionMap.NEW_HINT, null);
                        }
                    }
                }
            });
        }
        
        @Override
        public void actionPerformed(ActionEvent e) {
            if (e.getActionCommand().equals(TabActionMap.SELECT_COMMAND)) {
                item.select();
                if(!StringUtils.isEmpty(searchBar.getSearchText())) {
                        searchBar.requestSearchFocus();
                }
            } else if (e.getActionCommand().equals(TabActionMap.REMOVE_COMMAND)) {
                item.remove();
            }
        }

        @Override
        public void handleSearchResult(Search search, SearchResult searchResult) {
        }
        
        @Override
        public void handleSearchResults(Search search, Collection<? extends SearchResult> searchResults) {
        }
        
        void killBusy() {
            if (busyTimer != null && busyTimer.isRunning()) {
                busyTimer.stop();
            }
            putValue(TabActionMap.BUSY_KEY, Boolean.FALSE);
        }
        
        @Override
        public void searchStarted(Search search) {
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    busyTimer = new Timer(60000, new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
                            putValue(TabActionMap.BUSY_KEY, Boolean.FALSE);
                        }
                    });
                    busyTimer.setRepeats(false);
                    busyTimer.start();
                    putValue(TabActionMap.BUSY_KEY, Boolean.TRUE);
                }
            });
        }
        
        @Override
        public void searchStopped(Search search) {
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    killBusy();
                }
            });
        }
    }
    
    private static abstract class SearchListenerAction extends AbstractAction implements SearchListener{
        protected final Search search;

        public SearchListenerAction(String tr, Search search) {
            super(tr);
            this.search = search;
        }        
    
        /**
         * Registers the SearchListenerAction as a SearchListener on search.
         * 
         * @return this
         */
        public SearchListenerAction register(){
            search.addSearchListener(this);
            return this;
        }
        
        @Override
        public void handleSearchResult(Search search, SearchResult searchResult) {}
        @Override
        public void handleSearchResults(Search search, Collection<? extends SearchResult> searchResults) {}
    }

    /**
     * Action which repeats a search.  Since it makes no sense to
     * repeat an unstarted search, this class only enables searches to be
     * repeated if they have already started.
     */
    private static class MoreResultsAction extends SearchListenerAction {

        MoreResultsAction(Search search) {
            super(I18n.tr("Find More Results"), search);
            setEnabled(false);
        }

        @Override
        public void actionPerformed(ActionEvent event) {
            search.repeat();
        }

        @Override
        public void searchStarted(Search search) {
            setEnabled(true);
        }

        @Override public void searchStopped(Search search) { }
    }
    
    /**
     * Clears the current search results and repeats the search
     */
    private class RepeatSearchAction extends SearchListenerAction {
        private SearchResultsModel model;

        RepeatSearchAction(String title, Search search, SearchResultsModel model) {
            super(title, search);
            this.model = model;
            setEnabled(false);
        }
        
        @Override
        public void actionPerformed(ActionEvent e) {
            if(model.getSearchType() == SearchType.ALL_FRIENDS_BROWSE){
                allFriendsRefreshManager.refresh();
            } else {            
                new DefaultSearchRepeater(search, model).refresh();
            }
        }
        
        @Override
        public void searchStopped(Search search) {
        }

        @Override
        public void searchStarted(Search search) {
            setEnabled(true);            
        }
        
    }
    
    /**Stops the search*/
    private static class StopSearchAction extends SearchListenerAction {
        StopSearchAction(Search search) {
            super(I18n.tr("Stop Search"), search);
            setEnabled(false);
        }
                
        @Override
        public void actionPerformed(ActionEvent e) {
            search.stop();
        }          

        @Override
        public void searchStarted(Search search) {
            setEnabled(true);
        }

        @Override
        public void searchStopped(Search search) {
            setEnabled(false);
        } 
    }
    

}
