package org.limewire.core.impl.search;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

import org.limewire.collection.glazedlists.GlazedListsFactory;
import org.limewire.core.api.URN;
import org.limewire.core.api.search.GroupedSearchResult;
import org.limewire.core.api.search.Search;
import org.limewire.core.api.search.SearchDetails;
import org.limewire.core.api.search.SearchListener;
import org.limewire.core.api.search.SearchResult;
import org.limewire.core.api.search.SearchResultList;
import org.limewire.io.GUID;
import org.limewire.listener.EventListener;
import org.limewire.listener.EventListenerList;

import ca.odell.glazedlists.BasicEventList;
import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.util.concurrent.Lock;

/**
 * Implementation of SearchResultList for the live core.  CoreSearchResultList
 * handles search results from multiple background threads for Gnutella and 
 * friend results.
 */
class CoreSearchResultList implements SearchResultList {

    private final Search search;
    private final SearchDetails searchDetails;
    
    private final EventListenerList<Collection<GroupedSearchResult>> listListeners;
    private final Comparator<Object> resultFinder;
    private final SearchListener searchListener;
    
    private final EventList<GroupedSearchResult> groupedUrnResultList;
    private final EventList<GroupedSearchResult> threadSafeResultList;
    
    private volatile int resultCount;
    
    /**
     * Constructs a SearchResultList for the specified search.
     */
    public CoreSearchResultList(Search search, SearchDetails searchDetails) {
        this.search = search;
        this.searchDetails = searchDetails;
        
        listListeners = new EventListenerList<Collection<GroupedSearchResult>>();
        resultFinder = new UrnResultFinder();
        searchListener = new SearchListenerImpl();
        
        // Create list of grouped results.
        groupedUrnResultList = new BasicEventList<GroupedSearchResult>();
        threadSafeResultList = GlazedListsFactory.threadSafeList(groupedUrnResultList);
        
        // Add search listener.
        search.addSearchListener(searchListener);
    }
    
    @Override
    public GUID getGuid() {
        if (search instanceof CoreSearch) {
            return ((CoreSearch) search).getQueryGuid();
        } else {
            return null;
        }
    }
    
    @Override
    public int getResultCount() {
        return resultCount;
    }
    
    @Override
    public Search getSearch() {
        return search;
    }
    
    @Override
    public String getSearchQuery() {
        return searchDetails.getSearchQuery();
    }
    
    @Override
    public GroupedSearchResult getGroupedResult(URN urn) {
        Lock lock = groupedUrnResultList.getReadWriteLock().writeLock();
        lock.lock();
        try {
            int idx = Collections.binarySearch(groupedUrnResultList, urn, resultFinder);
            if (idx >= 0) {
                return groupedUrnResultList.get(idx);
            } else {            
                return null;
            }
        } finally {
            lock.unlock();
        }
    }
    
    @Override
    public EventList<GroupedSearchResult> getGroupedResults() {
        return threadSafeResultList;
    }
    
    @Override
    public void addListener(EventListener<Collection<GroupedSearchResult>> listener) {
        listListeners.addListener(listener);
    }

    @Override
    public boolean removeListener(EventListener<Collection<GroupedSearchResult>> listener) {
        return listListeners.removeListener(listener);
    }
    
    @Override
    public void clear() {
        Lock lock = groupedUrnResultList.getReadWriteLock().writeLock();
        lock.lock();
        try {
            groupedUrnResultList.clear();
            resultCount = 0;
        } finally {
            lock.unlock();
        }
    }
    
    @Override
    public void dispose() {
        search.removeSearchListener(searchListener);
    }
    
    /**
     * Adds the specified result to the list.
     */
    void addResult(SearchResult result) {
        addResultsInternal(Collections.singletonList(result));
    }
    
    /**
     * Adds the specified collection of results to the list.
     */
    void addResults(Collection<? extends SearchResult> results) {
        addResultsInternal(results);
    }
    
    /**
     * Adds the specified collection of results to the list of grouped results.
     * This method obtains a write lock on the list because results may be 
     * generated by different threads for Gnutella and friend results.
     */
    private void addResultsInternal(Collection<? extends SearchResult> results) {
        // Create list of new results.
        List<GroupedSearchResult> newResults = new ArrayList<GroupedSearchResult>();
        
        // Obtain write lock on result list.
        Lock lock = groupedUrnResultList.getReadWriteLock().writeLock();
        lock.lock();
        try {
            for (SearchResult result : results) {
                URN urn = result.getUrn();
                // Some results can be missing a URN, specifically secure results.
                // For now, we drop these.  We should figure out a way to show 
                // them later on.
                if (urn != null) {
                    int idx = Collections.binarySearch(groupedUrnResultList, urn, resultFinder);
                    if (idx >= 0) {
                        // Found URN so add result to grouping.
                        GroupedSearchResultImpl gsr = (GroupedSearchResultImpl) groupedUrnResultList.get(idx);
                        gsr.addNewSource(result, searchDetails.getSearchQuery());
                        groupedUrnResultList.set(idx, gsr);
                        newResults.add(gsr);

                    } else {
                        // URN not found so add new result at insertion point.
                        // This keeps the list in sorted order.
                        idx = -(idx + 1);
                        GroupedSearchResult gsr = new GroupedSearchResultImpl(result,
                                searchDetails.getSearchQuery());
                        groupedUrnResultList.add(idx, gsr);
                        newResults.add(gsr);
                    }
                    resultCount++;
                }
            }
        } finally {
            // Release lock.
            lock.unlock();
        }
        
        // Forward added results to list listeners.
        if (newResults.size() > 0) {
            notifyResultsAdded(newResults);
        }
    }
    
    /**
     * Forwards the specified collection of added results to all registered
     * listeners. 
     */
    private void notifyResultsAdded(Collection<GroupedSearchResult> results) {
        listListeners.broadcast(results);
    }
    
    /**
     * Handler for search events to add results to the list.
     */
    private class SearchListenerImpl implements SearchListener {

        @Override
        public void handleSearchResult(Search search, SearchResult searchResult) {
            addResult(searchResult);
        }

        @Override
        public void handleSearchResults(Search search,
                Collection<? extends SearchResult> searchResults) {
            addResults(searchResults);
        }

        @Override
        public void searchStarted(Search search) {
        }

        @Override
        public void searchStopped(Search search) {
        }
    }
    
    /**
     * Comparator to search for GroupedSearchResult objects by URN.  This is
     * only used to perform a binary search by URN, never to perform the sort,
     * so the compare() method does not need to be symmetric.
     */
    private static class UrnResultFinder implements Comparator<Object> {
        @Override
        public int compare(Object o1, Object o2) {
            return ((GroupedSearchResult) o1).getUrn().compareTo((URN) o2);
        }
    }
}
