/*
 * Copyright 2010-2015 Institut Pasteur.
 * 
 * This file is part of Icy.
 * 
 * Icy is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Icy is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
 */
package icy.search;

import icy.system.IcyExceptionHandler;
import icy.system.thread.SingleProcessor;
import icy.system.thread.ThreadUtil;
import icy.util.StringUtil;

import java.util.ArrayList;
import java.util.List;

/**
 * The SearchResultProducer create {@link SearchResult} objects from given search keywords.<br>
 * These {@link SearchResult} are then consumed by a {@link SearchResultConsumer}.
 * 
 * @author Thomas Provoost & Stephane Dallongeville
 */
public abstract class SearchResultProducer implements Comparable<SearchResultProducer>
{
    private class SearchRunner implements Runnable
    {
        private final String[] words;
        private final SearchResultConsumer consumer;

        public SearchRunner(String[] words, SearchResultConsumer consumer)
        {
            super();

            this.words = words;
            this.consumer = consumer;
        }

        @Override
        public void run()
        {
            // perform search if we have at least one not empty keyword
            if ((words.length > 1) || !StringUtil.isEmpty(words[0]))
            {
                try
                {
                    doSearch(words, consumer);
                }
                catch (Throwable t)
                {
                    // just display the exception and continue
                    IcyExceptionHandler.showErrorMessage(t, true, true);
                }
            }
            else
            {
                final boolean notEmpty;

                synchronized (results)
                {
                    // clear the list if necessary
                    notEmpty = !results.isEmpty();
                    if (notEmpty)
                        results.clear();
                }

                // avoid death lock by sending event after synchronization
                if (notEmpty)
                    consumer.resultsChanged(SearchResultProducer.this);
            }

            // search completed (do it after searching set to false)
            consumer.searchCompleted(SearchResultProducer.this);
        }
    }

    /** Result list */
    protected List<SearchResult> results;

    /** Internals */
    protected final SingleProcessor processor;

    public SearchResultProducer()
    {
        super();

        results = new ArrayList<SearchResult>();
        processor = new SingleProcessor(true, this.getClass().getSimpleName());
    }

    /** Returns the result producer order */
    public int getOrder()
    {
        // default
        return 10;
    }

    /** Returns the result producer name */
    public abstract String getName();

    /**
     * Returns the tooltip displayed on the menu (in small under the label).
     */
    public String getTooltipText()
    {
        return "Click to run";
    }

    /** Returns the result list */
    public List<SearchResult> getResults()
    {
        return results;
    }

    /**
     * Performs the search request (asynchronous), mostly build the search result list.<br>
     * Only one search request should be processed at one time so take care of waiting for previous
     * search request completion.<br>
     * 
     * @param words
     *        Search keywords
     * @param consumer
     *        Search result consumer for this search request.<br>
     *        The consumer should be notified of new results by using the
     *        {@link SearchResultConsumer#resultsChanged(SearchResultProducer)} method.
     */
    public void search(String[] words, SearchResultConsumer consumer)
    {
        processor.submit(new SearchRunner(words, consumer));
    }

    /**
     * Performs the search request (internal).<br>
     * The method is responsible for filling the <code>results</code> list :<br>
     * - If no result correspond to the requested search then <code>results</code> should be
     * cleared.<br>
     * - Else it should contains the founds results.<br>
     * <code>results</code> variable access should be synchronized as it can be externally accessed.<br>
     * The method could return earlier if {@link #hasWaitingSearch()} returns true.
     * 
     * @param words
     *        Search keywords
     * @param consumer
     *        Search result consumer for this search request.<br>
     *        The consumer should be notified of new results by using the
     *        {@link SearchResultConsumer#resultsChanged(SearchResultProducer)} method.
     * @see #hasWaitingSearch()
     */
    public abstract void doSearch(String[] words, SearchResultConsumer consumer);

    /**
     * Wait for the search request to complete.
     */
    public void waitSearchComplete()
    {
        while (isSearching())
            ThreadUtil.sleep(1);
    }

    /**
     * Returns true if the search result producer is currently processing a search request.
     */
    public boolean isSearching()
    {
        return processor.isProcessing();
    }

    /**
     * Returns true if there is a waiting search pending.<br>
     * This method should be called during search process to cancel it if another search is waiting.
     */
    public boolean hasWaitingSearch()
    {
        return processor.hasWaitingTasks();
    }

    /**
     * Add the SearchResult to the result list.
     * 
     * @param result
     *        Result to add to the result list.
     * @param consumer
     *        If not null then consumer is notified about result change
     */
    public void addResult(SearchResult result, SearchResultConsumer consumer)
    {
        if (result != null)
        {
            synchronized (results)
            {
                results.add(result);
            }

            // notify change to consumer
            if (consumer != null)
                consumer.resultsChanged(this);
        }
    }

    @Override
    public int compareTo(SearchResultProducer o)
    {
        // sort on order
        return getOrder() - o.getOrder();
    }
}