package org.limewire.ui.swing.search;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Collection;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

import org.limewire.core.api.connection.ConnectionStrength;
import org.limewire.core.api.connection.GnutellaConnectionManager;
import org.limewire.core.api.lifecycle.LifeCycleEvent;
import org.limewire.core.api.lifecycle.LifeCycleManager;
import org.limewire.core.api.search.Search;
import org.limewire.core.api.search.SearchFactory;
import org.limewire.core.api.search.SearchListener;
import org.limewire.core.api.search.SearchResult;
import org.limewire.listener.EventListener;
import org.limewire.listener.SwingEDTEvent;
import org.limewire.ui.swing.nav.NavItem;
import org.limewire.ui.swing.nav.NavItemListener;
import org.limewire.ui.swing.search.model.SearchResultsModel;
import org.limewire.ui.swing.search.model.SearchResultsModelFactory;
import org.limewire.ui.swing.util.SwingUtils;

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

/**
 * The implementation of SearchHandler used to initiate regular text searches.
 * Calling the <code>doSearch(SearchInfo)</code> method will create a new  
 * search results tab in the UI, and start the search operation.
 */
@Singleton
class TextSearchHandlerImpl implements SearchHandler {
    
    private final SearchFactory searchFactory;
    private final Provider<SearchResultsPanelFactory> searchResultPanelFactory;
    private final SearchNavigator searchNavigator;
    private final Provider<SearchResultsModelFactory> searchResultsModelFactory;
    private final LifeCycleManager lifeCycleManager;
    private final GnutellaConnectionManager connectionManager;
    
    private SearchResultsModelFactory modelFactory;
    private SearchResultsPanelFactory panelFactory;
    
    /**
     * Constructs a TextSearchHandlerImpl with the specified services and
     * factories.
     */
    @Inject
    TextSearchHandlerImpl(SearchFactory searchFactory,
            Provider<SearchResultsPanelFactory> searchResultPanelFactory,
            SearchNavigator searchNavigator,
            Provider<SearchResultsModelFactory> searchResultsModelFactory,
            LifeCycleManager lifeCycleManager, 
            GnutellaConnectionManager connectionManager) {
        this.searchNavigator = searchNavigator;
        this.searchFactory = searchFactory;
        this.searchResultPanelFactory = searchResultPanelFactory;
        this.searchResultsModelFactory = searchResultsModelFactory;
        this.lifeCycleManager = lifeCycleManager;
        this.connectionManager = connectionManager;
    }

    /**
     * Performs a search operation using the specified SearchInfo object.  
     * The method always returns true.
     */
    @Override
    public boolean doSearch(final SearchInfo info) {
        // Create search request.
        Search search = searchFactory.createSearch(info);
        
        String panelTitle = info.getTitle();
        
        if(modelFactory == null)
            modelFactory = searchResultsModelFactory.get();
        if(panelFactory == null)
            panelFactory = searchResultPanelFactory.get();
        // Create search results data model and display panel.
        SearchResultsModel searchModel = modelFactory.createSearchResultsModel(info, search);
        SearchResultsPanel searchPanel = panelFactory.createSearchResultsPanel(searchModel);
        
        // Add search results display to the UI, and select its navigation item.
        SearchNavItem item = searchNavigator.addSearch(panelTitle, searchPanel, search, searchModel);
        item.select();

        // Add listeners for connection events.
        addConnectionWarnings(search, searchPanel, item);
        
        // Start search operation.
        startSearch(searchModel, searchPanel, item);
        return true;
    }

    /**
     * Initiates a search using the specified search model, search panel,
     * and navigation item.
     */
    private void startSearch(final SearchResultsModel searchModel, 
            final SearchResultsPanel searchPanel, final SearchNavItem navItem) {
        // prevent search from starting until lifecycle manager completes loading
        if (lifeCycleManager.isStarted()) {
            searchModel.start(new SwingSearchListener(searchModel, navItem));
        } else {
             searchPanel.setLifeCycleComplete(false);
             final EventListener<LifeCycleEvent> listener = new EventListener<LifeCycleEvent>() {
                 @Override
				@SwingEDTEvent
                 public void handleEvent(LifeCycleEvent event) {
                     if(event == LifeCycleEvent.STARTED) {
                         searchPanel.setLifeCycleComplete(true);
                         searchModel.start(new SwingSearchListener(searchModel, navItem));
                         lifeCycleManager.removeListener(this);
                     }
                 }
             };
             lifeCycleManager.addListener(listener);
             navItem.addNavItemListener(new NavItemListener() {                 
                 @Override
				public void itemRemoved(boolean wasSelected) {
                     lifeCycleManager.removeListener(listener);
                 }
                 
                 @Override
				public void itemSelected(boolean selected) {}
             });
        }
    }
    
    /**
     * Notifies the specified SearchResultsPanel if the specified
     * ConnectionStrength indicates a full connection.  Returns true if fully
     * connected, false otherwise.
     */
    private boolean setConnectionStrength(ConnectionStrength type, SearchResultsPanel searchPanel) {
        switch(type) {
        case TURBO:
        case FULL:
            searchPanel.setFullyConnected(true);
            return true;
        }
        
        return false;
    }
    
    /**
     * Removes the specified listeners from the Search request and connection 
     * manager.
     */
    private void removeListeners(Search search, AtomicReference<SearchListener> searchListenerRef, AtomicReference<PropertyChangeListener> connectionListenerRef) {
        SearchListener searchListener = searchListenerRef.get();
        if(searchListener != null) {
            search.removeSearchListener(searchListener);
            searchListenerRef.set(null);
        }
        
        PropertyChangeListener connectionListener = connectionListenerRef.get();
        if(connectionListener != null) {
            connectionManager.removePropertyChangeListener(connectionListener);
            connectionListenerRef.set(null);
        }
    }

    /**
     * Adds listeners to the Search request and connection manager to update 
     * the UI based on connection events.
     */
    private void addConnectionWarnings(final Search search, 
            final SearchResultsPanel searchPanel, NavItem navItem) {
        // Define atomic listener references.
        final AtomicReference<SearchListener> searchListenerRef = new AtomicReference<SearchListener>();
        final AtomicReference<PropertyChangeListener> connectionListenerRef = new AtomicReference<PropertyChangeListener>();

        // Create property change listener for connection strength.  This
        // updates an indicator in SearchResultsPanel, and removes all 
        // warning listeners when the strength is full.
        connectionListenerRef.set(new PropertyChangeListener() {
            @Override
            public void propertyChange(PropertyChangeEvent evt) {
                if(evt.getPropertyName().equals("strength")) {
                    if(setConnectionStrength((ConnectionStrength)evt.getNewValue(), searchPanel)) {
                        removeListeners(search, searchListenerRef, connectionListenerRef);
                    }
                }
            }
        });  

        // Create search listener to monitor number of results.  If a certain
        // number of search results comes in, for now 10, we assume that while
        // not fully connected, we have a good enough search going so we remove
        // all warning listeners.
        searchListenerRef.set(new SearchListener() {
            private final AtomicInteger numberOfResults = new AtomicInteger(0);
            
            @Override
            public void handleSearchResult(Search search, SearchResult searchResult) {
                if(numberOfResults.addAndGet(1) > 10) {
                    SwingUtils.invokeNowOrLater(new Runnable() {
                        @Override
						public void run() {
                            // while not fully connected, assume the
                            // connections we have are enough
                            // based on the number of results coming in.
                            searchPanel.setFullyConnected(true);
                        }
                    });
                    removeListeners(search, searchListenerRef, connectionListenerRef);
                }
            }
            
            @Override
            public void handleSearchResults(Search search, Collection<? extends SearchResult> searchResults) {
                if(numberOfResults.addAndGet(searchResults.size()) > 10) {
                    SwingUtils.invokeNowOrLater(new Runnable() {
                        @Override
						public void run() {
                            // while not fully connected, assume the
                            // connections we have are enough
                            // based on the number of results coming in.
                            searchPanel.setFullyConnected(true);
                        }
                    });
                    removeListeners(search, searchListenerRef, connectionListenerRef);
                }
            }

            @Override public void searchStarted(Search search) {}
            @Override public void searchStopped(Search search) {}                
        });
        
        // Initialize search panel indicator, and install connection listener.
        searchPanel.setFullyConnected(false);
        connectionManager.addPropertyChangeListener(connectionListenerRef.get());

        // If not fully connected, add search listener and navigation listener.
        if(setConnectionStrength(connectionManager.getConnectionStrength(), searchPanel)) {
            removeListeners(search, searchListenerRef, connectionListenerRef);
        } else {
            search.addSearchListener(searchListenerRef.get());
            navItem.addNavItemListener(new NavItemListener() {
                @Override
                public void itemRemoved(boolean wasSelected) {
                    removeListeners(search, searchListenerRef, connectionListenerRef);
                }
                @Override public void itemSelected(boolean selected) {}
            });
        }
    }
}
