package org.limewire.core.impl.search;

import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;

import org.limewire.core.api.FilePropertyKey;
import org.limewire.core.api.search.Search;
import org.limewire.core.api.search.SearchCategory;
import org.limewire.core.api.search.SearchDetails;
import org.limewire.core.api.search.SearchEvent;
import org.limewire.core.api.search.SearchListener;
import org.limewire.core.api.search.SearchResult;
import org.limewire.core.impl.library.FriendSearcher;
import org.limewire.core.impl.search.torrentweb.TorrentWebSearchFactory;
import org.limewire.core.settings.SearchSettings;
import org.limewire.io.GUID;
import org.limewire.io.IpPort;
import org.limewire.listener.EventBroadcaster;
import org.limewire.search.frostwire.WireShareFrostWireSearchBackend;
import org.limewire.search.frostwire.WireShareFrostWireSearchListener;
import org.limewire.search.frostwire.WireShareFrostWireSearchProvider;

import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.name.Named;
import com.limegroup.gnutella.RemoteFileDesc;
import com.limegroup.gnutella.SearchServices;
import com.limegroup.gnutella.messages.QueryReply;
import com.limegroup.gnutella.xml.LimeXMLDocumentFactory;

public class CoreSearch implements Search, SearchListener {

    private final SearchDetails searchDetails;
    private final SearchServices searchServices;
    private final QueryReplyListenerList listenerList;
    private final FriendSearcher friendSearcher;
    private final RemoteFileDescAdapter.Factory remoteFileDescAdapterFactory;

    /**
     * A search is considered processed when it is acted upon (started or stopped)
     * <pre>
     * -cannot repeat a search that has not yet been processed
     * -cannot start a search that has already been processed
     * -stopping a search only stops searches that have already been processed.
     * </pre>
     */
    final AtomicBoolean processingStarted = new AtomicBoolean(false);

    private final CopyOnWriteArrayList<SearchListener> searchListeners = new CopyOnWriteArrayList<SearchListener>();
    private final QrListener qrListener = new QrListener();
    private final FriendSearchListener friendSearchListener = new FriendSearchListenerImpl();
    private final ScheduledExecutorService backgroundExecutor;
    private final EventBroadcaster<SearchEvent> searchEventBroadcaster;
    private final AdvancedQueryStringBuilder compositeQueryBuilder;
    
    /**
     * The guid of the last active search.
     */
    volatile byte[] searchGuid;
    private final TorrentWebSearchFactory torrentWebSearchFactory;
    private final WireShareFrostWireSearchBackend frostWireSearchBackend;
    
    private volatile Search torrentWebSearch;
    private volatile WireShareFrostWireSearchBackend.WireShareFrostWireSearchSession frostWireSearch;

    @Inject
    public CoreSearch(@Assisted SearchDetails searchDetails,
            SearchServices searchServices,
            QueryReplyListenerList listenerList,
            FriendSearcher friendSearcher,
            @Named("backgroundExecutor") ScheduledExecutorService backgroundExecutor,
            EventBroadcaster<SearchEvent> searchEventBroadcaster,
            LimeXMLDocumentFactory xmlDocumentFactory,
            AdvancedQueryStringBuilder compositeQueryBuilder,
            RemoteFileDescAdapter.Factory remoteFileDescAdapterFactory,
            TorrentWebSearchFactory googleTorrentSearchFactory,
            WireShareFrostWireSearchBackend frostWireSearchBackend) {
        this.searchDetails = searchDetails;
        this.searchServices = searchServices;
        this.listenerList = listenerList;
        this.friendSearcher = friendSearcher;
        this.backgroundExecutor = backgroundExecutor;
        this.searchEventBroadcaster = searchEventBroadcaster;
        this.compositeQueryBuilder = compositeQueryBuilder;
        this.remoteFileDescAdapterFactory = remoteFileDescAdapterFactory;
        this.torrentWebSearchFactory = googleTorrentSearchFactory;
        this.frostWireSearchBackend = frostWireSearchBackend;
    }
    
    @Override
    public SearchCategory getCategory() {
        return searchDetails.getSearchCategory();
    }
    
    @Override
    public void addSearchListener(SearchListener searchListener) {
        searchListeners.add(searchListener);
    }
    
    @Override
    public void removeSearchListener(SearchListener searchListener) {
        searchListeners.remove(searchListener);
    }

    @Override
    public void start() {
        if (processingStarted.getAndSet(true)) {
            throw new IllegalStateException("cannot start search which has already been processed!");
        }
        
        for(SearchListener listener : searchListeners) {
            listener.searchStarted(this);
        }

        doSearch(true);
    }
    
    private void doSearch(boolean initial) {
        searchEventBroadcaster.broadcast(new SearchEvent(this, SearchEvent.Type.STARTED));
        
        searchGuid = searchServices.newQueryGUID();
        listenerList.addQueryReplyListener(searchGuid, qrListener);
        
        switch(searchDetails.getSearchType()) {
        case KEYWORD:
            doKeywordSearch(initial);
            break;
        case WHATS_NEW:
            doWhatsNewSearch(initial);
            break;
        }
    }
    
    private void doWhatsNewSearch(boolean initial) {
        searchServices.queryWhatIsNew(searchGuid,
                searchDetails.getSearchCategory());
        
        // TODO: Search friends too.
    }
    
    private void doKeywordSearch(boolean initial) {
        String query = searchDetails.getSearchQuery();
        String advancedQuery = "";
        Map<FilePropertyKey, String> advancedSearch = searchDetails.getAdvancedDetails();
        if(advancedSearch != null && advancedSearch.size() > 0) {
            if(query == null || query.equals("")) {
                query = compositeQueryBuilder.createSimpleCompositeQuery(advancedSearch);
            }
            advancedQuery = compositeQueryBuilder.createXMLQueryString(advancedSearch, searchDetails.getSearchCategory().getCategory());
        }
        
        String mutated = searchServices.mutateQuery(query);
        searchServices.query(searchGuid, mutated, advancedQuery,
                searchDetails.getSearchCategory());
        
        backgroundExecutor.execute(new Runnable() {
            @Override
            public void run() { 
                friendSearcher.doSearch(searchDetails, friendSearchListener);
            }
        });        

        if (SearchSettings.USE_FROSTWIRE_WEB_SEARCH.getValue()
                && query != null
                && query.trim().length() > 0) {
            frostWireSearch = frostWireSearchBackend.search(query, searchDetails.getSearchCategory(), new WireShareFrostWireSearchListener() {
                @Override
                public void onResults(Collection<? extends SearchResult> results) {
                    CoreSearch.this.handleSearchResults(CoreSearch.this, results);
                }

                @Override
                public void onProviderStopped(WireShareFrostWireSearchProvider provider) {
                }

                @Override
                public void onProviderError(WireShareFrostWireSearchProvider provider, String message) {
                }
            });
        }
        
        if (initial 
                && SearchSettings.USE_TORRENT_WEB_SEARCH.getValue()
                && searchDetails.getSearchCategory() == SearchCategory.TORRENT) {
            torrentWebSearch = torrentWebSearchFactory.create(searchDetails.getSearchQuery());
            torrentWebSearch.addSearchListener(this);
            torrentWebSearch.start();
        }
    }
    
    /**
     * Stops current search and repeats search.
     * 
     * @throws IllegalStateException If search processing has already begun (started or stopped)
     */
    @Override
    public void repeat() {
        if(!processingStarted.get()) {
            throw new IllegalStateException("must start!");
        }
        
        stop();
        
        for(SearchListener listener : searchListeners) {
            listener.searchStarted(CoreSearch.this);
        }
        
        doSearch(false);
    }

    @Override
    public void stop() {
        if(!processingStarted.compareAndSet(true, true)) {
            return;
        }
        
        if (torrentWebSearch != null) {
            torrentWebSearch.stop();
        }
        if (frostWireSearch != null) {
            frostWireSearch.stop();
        }
        
        searchEventBroadcaster.broadcast(new SearchEvent(this, SearchEvent.Type.STOPPED));
        listenerList.removeQueryReplyListener(searchGuid, qrListener);
        searchServices.stopQuery(new GUID(searchGuid));
        
        for(SearchListener listener : searchListeners) {
            listener.searchStopped(CoreSearch.this);
        }
    }
    
    public GUID getQueryGuid() {
        return new GUID(searchGuid);
    }
    
    @Override
    public void handleSearchResult(Search search, SearchResult searchResult) {
        for (SearchListener listener : searchListeners) {
            listener.handleSearchResult(this, searchResult);
        }   
    }
    
    @Override
    public void handleSearchResults(Search search, Collection<? extends SearchResult> searchResults) {
        for (SearchListener listener : searchListeners) {
            listener.handleSearchResults(this, searchResults);
        }
    }

    @Override
    public void searchStarted(Search search) {        
    }

    @Override
    public void searchStopped(Search search) {
    }
    
    private class QrListener implements QueryReplyListener {
        @Override
        public void handleQueryReply(RemoteFileDesc rfd, QueryReply queryReply,
                Set<? extends IpPort> locs) {
            
            RemoteFileDescAdapter rfdAdapter = remoteFileDescAdapterFactory.create(rfd, locs);
            
            handleSearchResult(CoreSearch.this, rfdAdapter);
        }
    }

    private class FriendSearchListenerImpl implements FriendSearchListener {
        public void handleFriendResults(Collection<SearchResult> results) {
            handleSearchResults(CoreSearch.this, results);
        }
    }

}
