/*
 *  PHEX - The pure-java Gnutella-servent.
 *  Copyright (C) 2001 - 2006 Phex Development Group
 *
 *  This program 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 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program 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 this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * 
 *  --- CVS Information ---
 *  $Id: Search.java 3362 2006-03-30 22:27:26Z gregork $
 */
package phex.query;

import java.util.ArrayList;

import phex.common.IntObj;
import phex.download.RemoteFile;
import phex.event.*;
import phex.msg.QueryMsg;
import phex.msg.QueryResponseMsg;
import phex.utils.Logger;
import phex.utils.NLogger;

public abstract class Search
{
    public static final long DEFAULT_SEARCH_TIMEOUT = 5 * 60 * 1000; // 5 minutes
    
    /**
     * The time when the search was started.
     */
    protected long startTime;
    
    /**
     * The dynamic query engine that actually runs the search in case
     * a dynamic query is used. This can attribute can be null in case
     * no dynamic query is used (if we are a leaf). 
     */
    protected DynamicQueryEngine queryEngine;

    /**
     * The MsgQuery object that forms the query for this search.
     */
    protected QueryMsg queryMsg;
    
    /**
     * A cached buffer object that contains the current progress of the search.
     * The value is not always up to date and only validated and updated when
     * calling getProgressObj();
     */
    private IntObj progressObj;

    /**
     * The status of the search.
     */
    protected boolean isSearching;
    
    /**
     * Associated class that is able to hold search results. Access to this
     * should be locked by holding 'this'. 
     */
    protected SearchResultHolder searchResultHolder;

    /**
     * All listeners interested in events of this search.
     */
    private ArrayList listenerList = new ArrayList( 2 );

    protected Search()
    {
        isSearching = false;
        progressObj = new IntObj();
        searchResultHolder = new SearchResultHolder();
    }
    
    public int getQueryHitCount()
    {
        return searchResultHolder.getQueryHitCount();
    }

    /**
     * Tries a very basic calculation about the search progress.
     * @return
     */
    public int getProgress()
    {
        if ( !isSearching )
        {
            return 100;
        }
        if ( queryEngine != null )
        {
            return queryEngine.getProgress();
        }
        else
        {
            long currentTime = System.currentTimeMillis();
            // time progress...
            int timeProgress = (int)(100 - (double)( startTime + DEFAULT_SEARCH_TIMEOUT - currentTime )
                / (double)DEFAULT_SEARCH_TIMEOUT * (double)100 );
            // return the max of all these
            return Math.min( timeProgress, 100);
        }
    }
    
    public IntObj getProgressObj()
    {
        int progress = getProgress();
        if ( progressObj.value != progress )
        {
            progressObj.value = progress;
        }
        return progressObj;
    }
   
    public boolean isSearching()
    {
        return isSearching;
    }
    
    public void checkForSearchTimeout( long currentTime )
    {
        if ( queryEngine != null )
        {
            if ( queryEngine.isQueryFinished() )
            {
                stopSearching();
            }
        }
        else if ( currentTime > startTime + DEFAULT_SEARCH_TIMEOUT )
        {
            // timed out stop search
            stopSearching();
        }
    }

    public void startSearching()
    {
        AsynchronousDispatcher.invokeLater( new Runnable()
        {
            public void run()
            {
                startTime = System.currentTimeMillis();
                // set the creation time just before we send the query this
                // will prevent the query to timeout before it could be send
                queryMsg.setCreationTime( startTime );
                Logger.logMessage( Logger.FINER, Logger.SEARCH,
                    "Sending Query " + queryMsg );
                queryEngine = QueryManager.getInstance().sendMyQuery( queryMsg );
                isSearching = true;
                fireSearchStarted();
            }
        } );
    }

    public void stopSearching()
    {
        if ( !isSearching )
        {// already stoped
            return;
        }
        isSearching = false;
        if ( queryEngine != null )
        {
            queryEngine.stopQuery();
        }
        fireSearchStoped();
    }

    public abstract void processResponse( QueryResponseMsg msg );
    
    ///////////////////// START event handling methods ////////////////////////

    public void addSearchChangeListener( SearchDataListener listener )
    {
        listenerList.add( listener );
    }

    public void removeSearchChangeListener( SearchDataListener listener )
    {
        listenerList.remove( listener );
    }

    protected void fireSearchStarted()
    {
        SearchDataEvent searchChangeEvent =
            new SearchDataEvent( this, SearchDataEvent.SEARCH_STARTED );
        fireSearchChangeEvent( searchChangeEvent );
    }

    protected void fireSearchStoped()
    {
        SearchDataEvent searchChangeEvent =
            new SearchDataEvent( this, SearchDataEvent.SEARCH_STOPED );
        fireSearchChangeEvent( searchChangeEvent );
    }

    public void fireSearchChanged()
    {
        SearchDataEvent searchChangeEvent =
            new SearchDataEvent( this, SearchDataEvent.SEARCH_CHANGED );
        fireSearchChangeEvent( searchChangeEvent );
    }

    protected void fireSearchHitsAdded( RemoteFile[] newHits )
    {
        SearchDataEvent searchChangeEvent = new SearchDataEvent( this,
            SearchDataEvent.SEARCH_HITS_ADDED, newHits );
        fireSearchChangeEvent( searchChangeEvent );        
    }

    private void fireSearchChangeEvent( final SearchDataEvent searchChangeEvent )
    {
        // invoke update in event dispatcher
        AsynchronousDispatcher.invokeLater(
        new Runnable()
        {
            public void run()
            {
                try
                {
                    Object[] listeners = listenerList.toArray();
                    SearchDataListener listener;
    
                    // Process the listeners last to first, notifying
                    // those that are interested in this event
                    for ( int i = listeners.length - 1; i >= 0; i-- )
                    {
                        if ( listeners[ i ] instanceof SearchDataListener )
                        {
                            listener = (SearchDataListener)listeners[ i ];
                            listener.searchDataChanged( searchChangeEvent );
                        }
                        
                    }
                }
                catch ( Throwable th )
                {
                    NLogger.error(Search.class, th, th);
                }
            }
        });
    }
    
    ///////////////////// END event handling methods ////////////////////////
}