/*
SDX: Documentary System in XML.
Copyright (C) 2000  Ministere de la culture et de la communication (France), AJLSM

Ministere de la culture et de la communication,
Mission de la recherche et de la technologie
3 rue de Valois, 75042 Paris Cedex 01 (France)
mrt@culture.fr, michel.bottin@culture.fr

AJLSM, 17, rue Vital Carles, 33000 Bordeaux (France)
sevigny@ajlsm.com

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
or connect to:
http://www.fsf.org/copyleft/gpl.html
*/
package fr.gouv.culture.sdx.search.lucene.query;

import java.io.IOException;

import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.Hits;
import org.apache.lucene.search.Searcher;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;

import fr.gouv.culture.sdx.exception.SDXException;
import fr.gouv.culture.sdx.exception.SDXExceptionCode;
import fr.gouv.culture.sdx.search.lucene.Field;
import fr.gouv.culture.sdx.search.lucene.filter.Filter;
import fr.gouv.culture.sdx.utils.AbstractSdxObject;
import fr.gouv.culture.sdx.utils.Utilities;
import fr.gouv.culture.sdx.utils.constants.Node;

/**
 *	Abstract class for basic SDX query functionalities.
 *
 * <p>
 * To implement a new SDX query type, it is better to extend this class which takes care
 * of:
 * <ul>
 * <li>Adding base queries, filters, sort specifications
 * <li>Logger management
 * <li>Query execution
 * <li>Search locations
 * </ul>
 *
 * In building a query, one should following the steps to avoid problems:
 * <ol>
 * <li>Create the query, using the constructor
 * <li>set the super.getLog() by calling the enableLogging
 * <li>build the query by calling the setUp method
 * <li>prepare the query with the prepare method
 * <li>use execute method to obtain results
 * </ol>
 *<p>use the toSAX method for info; this method will deliver
 *the best information after the prepare() method has been called</p>
 * @see #prepare
 * @see #execute
 * @see #toSAX
 */
public abstract class AbstractQuery extends AbstractSdxObject implements Query {


    /** A Lucene index to search. */
    protected SearchLocations searchLocations;

    /** The requested Text, especially for simple or field queries */
    protected String queryText;

    /** Results of a base query. */
    protected Results baseResults;

    /** The internal Lucene query for the base query. */
    protected fr.gouv.culture.sdx.search.lucene.query.Query baseQuery;

    /** The operator linking this query to the base query. */
    protected int baseOperator;

    /** The sort specification for this query. */
    protected SortSpecification sortSpecification;

    /** The filter for this query. */
    protected Filter filter;

    /** The internal Lucene query for this query. */
    protected org.apache.lucene.search.Query luceneQuery;

    /** The internal Lucene query wrapper for term collection/highlighting*/
    protected CachingRewrittenQueryWrapper cachedQuery = null;


    public void setUp(SearchLocations sLocs, org.apache.lucene.search.Query lquery) throws SDXException {
        setSearchLocations(sLocs);
        if (lquery != null)
            this.luceneQuery = lquery;
    }

    public void setLuceneQuery(org.apache.lucene.search.Query lquery) {
        if (lquery != null)
            this.luceneQuery = lquery;

    }

    /**
     *	Returns the internal Lucene query for this query.
     */
    public org.apache.lucene.search.Query getLuceneQuery() {
        return luceneQuery;
    }

    public org.apache.lucene.search.Query getCachedQuery() {
        return this.cachedQuery.getRewrittenQueries();
    }

    /**
     *	Executes the query and return results.
     */
    public Results execute() throws SDXException {


        // Make sure that the query is correctly prepared.
        this.filter = prepare();

        if (searchLocations == null) {
            throw new SDXException(null, SDXExceptionCode.ERROR_SEARCHLOCATIONS_NULL, null, null);
        }

        // Get Searcher
        Searcher searcher = searchLocations.getSearcher();

        if (searcher == null) { 
        	throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_SEARCHER_NULL, null, null);
        }

        //TODO?: should we be testing if luceneQuery is null below?-rbp
        // Define the proper Lucene query TODO future : use new queries from Lucene 1.2 (fuzzy, wildcard)
        if (baseQuery != null && !(this instanceof fr.gouv.culture.sdx.search.lucene.query.ComplexQuery)) {
            BooleanQuery newQuery = Utilities.newBooleanQuery();
            switch (baseOperator) {
                case fr.gouv.culture.sdx.search.lucene.query.Query.OPERATOR_OR:
                	// MAJ Lucene 2.1.0
                    // newQuery.add(luceneQuery, false, false);
                    // newQuery.add(baseQuery.getLuceneQuery(), false, false);
                	newQuery.add(luceneQuery, BooleanClause.Occur.SHOULD);
                    newQuery.add(baseQuery.getLuceneQuery(), BooleanClause.Occur.SHOULD);
                    break;
                case fr.gouv.culture.sdx.search.lucene.query.Query.OPERATOR_NOT:
                	// MAJ Lucene 2.1.0
                    // newQuery.add(luceneQuery, false, true);
                    // newQuery.add(baseQuery.getLuceneQuery(), true, false);
                	newQuery.add(luceneQuery, BooleanClause.Occur.MUST_NOT);
                    newQuery.add(baseQuery.getLuceneQuery(), BooleanClause.Occur.MUST);
                    break;
                case fr.gouv.culture.sdx.search.lucene.query.Query.OPERATOR_AND:	// Default value
                default:
                    baseOperator = fr.gouv.culture.sdx.search.lucene.query.Query.OPERATOR_AND;
                	// MAJ Lucence 2.1.0
                    // newQuery.add(luceneQuery, true, false);
                    // newQuery.add(baseQuery.getLuceneQuery(), true, false);
	                newQuery.add(luceneQuery, BooleanClause.Occur.MUST);
	                newQuery.add(baseQuery.getLuceneQuery(), BooleanClause.Occur.MUST);
                    break;
            }
            luceneQuery = newQuery;
        }

        try {

            // Then search the index
            // TODO (RBP) : Exception:should we be checking for null lucenQuery object here, i think so?
            if(luceneQuery instanceof BooleanQuery) {
            	BooleanQuery.setMaxClauseCount(Query.LUCENE_BOOLEAN_QUERY_MAX_CLAUSES);
            }
            this.cachedQuery = new CachingRewrittenQueryWrapper(luceneQuery);

            Hits searchHits;

            if (filter==null){
            	searchHits = searcher.search(this.cachedQuery);
            }
            else {
            	searchHits = searcher.search(this.cachedQuery);
            }
            /* TODO (RBP) : use Lucene sort
             * if (filter == null && sortSpecification == null) {
             *   searchHits = searcher.search(this.cachedQuery);
             * }
             * else if (sortSpecification==null) {
             *   searchHits = searcher.search(this.cachedQuery, filter);
             * }
             * else {
             *   searchHits = searcher.search(this.cachedQuery, filter, sortSpecification.getLuceneSort());
             * }
             */

            Results results = new Results();
            results.enableLogging(super.getLog());
            results.setUp(searchLocations, searchHits, sortSpecification, this);
            return results;
            
        } catch (IOException e) {
            String[] args = new String[3];
            if (luceneQuery != null) args[0] = this.luceneQuery.toString();
            args[1] = searcher.toString();
            args[2] = e.getMessage();
            throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_EXECUTE_QUERY, args, e);
        }

    }

    /**
     *	Transforme la requete pour echapper les caracteres speciaux.
     *
     *	Le seul traitement effectue est la suppression du trait d'union
     *	lorsqu'il se trouve entre deux lettres, sans espace.
     *
     *	@param	q		La requete originale.
     */
/*
	public String escapeQuery(String q)
	{
		StringBuffer s = new StringBuffer();
		for (int i=0; i<q.length(); i++)
		{
			char c = q.charAt(i);
			if ( c == '-' )
				if ( i == 0 ) s.append(c);
				else if ( s.charAt(i-1) == ' ' ) s.append(c);
				else s.append(' ');
			else s.append(c);
		}
		return s.toString();
	}

*/
    /**
     *	Returns a query operator from a given string representation.
     *
     * <p>
     * The values accepted are :
     * <ol>
     * <li><code>and</code> ; <code>et</code> for the logical AND operator</li>
     * <li><code>or</code> ; <code>ou</code> for the logical OR operator</li>
     * <li><code>not</code> ; <code>sauf</code> for the logical NOT operator</li>
     *
     *	@param	str		The string representation of the operator.
     *
     * @return     The operator ; if the string representation does'nt match any operator,
     *              OPERATOR_AND is returned.
     */
    public static int getOperator(String str) {
        if (str == null) return defaultOperator;

        if (str.equalsIgnoreCase("0")) return OPERATOR_OR;
        if (str.equalsIgnoreCase("1")) return OPERATOR_AND;
        if (str.equalsIgnoreCase("2")) return OPERATOR_NOT;

        if (str.equalsIgnoreCase("and")) return OPERATOR_AND;
        if (str.equalsIgnoreCase("et")) return OPERATOR_AND;
        if (str.equalsIgnoreCase("or")) return OPERATOR_OR;
        if (str.equalsIgnoreCase("ou")) return OPERATOR_OR;
        if (str.equalsIgnoreCase("not")) return OPERATOR_NOT;
        if (str.equalsIgnoreCase("sauf")) return OPERATOR_NOT;
        return defaultOperator;
    }

    /**
     *	Remplace les termes tronques par les valeurs possibles des index.
     *
     *	@param	field		Le nom du champ (peut etre null)
     *	@param	str			La requete originale
     */
/*
	public String parseWildcards(String field, String str)
	{
		return parseWildcards(field, str, false);
	}
*/

    /**
     *	Remplace les termes tronques par les valeurs possibles des index.
     *
     *	@param	field		Le nom du champ (peut etre null)
     *	@param	str			La requete originale
     *	@param	required	Indique s'il s'agit d'une liste de termes requis
     */
/*
	public String parseWildcards(String field, String str, boolean required)
	{
		if ( str == null ) return "";
		StringBuffer ret = new StringBuffer();
		StringTokenizer st = new StringTokenizer(str);
		while ( st.hasMoreTokens() )
		{
			String tk = st.nextToken();
			if ( (tk.indexOf('*') != -1 || tk.indexOf('?') != -1) && tk.length() > 2 )
			{
				// On doit verifier s'il y a un champ specifie
				StringTokenizer stColon = new StringTokenizer(tk, ":");
				String fieldName = "";
				if ( stColon.countTokens() > 1 )
				{
					fieldName = stColon.nextToken() + ":";
					tk = stColon.nextToken();
				}
				ret.append(" ");
				ret.append(fieldName);
				StringBuffer endBuffer = new StringBuffer();
				boolean mustBreak = false;
				while (true)
				{
					char first = tk.charAt(0);
					switch ( first )
					{
						case '+':
						case '-':
						case '"':
						case '(':
							ret.append(first);
							tk = tk.substring(1);
							break;
						default:
							mustBreak = true;
							break;
					}
					if ( mustBreak ) break;
				}
				mustBreak = false;
				while ( true )
				{
					char last = tk.charAt(tk.length()-1);
					switch ( last )
					{
						case '"':
						case ')':
							tk = tk.substring(0, tk.length()-1);
							endBuffer.append(last);
							break;
						default:
							mustBreak = true;
							break;
					}
					if ( mustBreak ) break;
				}
				try
				{
					Terms ts = new Terms(index, field, tk);
					TreeMap terms = ts.getList();
					Hashtable gotTerms = new Hashtable();
					if ( terms != null )
					{
						ret.append(" ");
						if ( required ) ret.append("+");
						ret.append("( ");
						String content;
						Iterator it = terms.keySet().iterator();
						while ( it.hasNext() )
						{
							TermInfo termInfo = (TermInfo)terms.get((String)it.next());
							content = termInfo.getContent();
							if ( gotTerms.get(content) == null )
							{
								ret.append(" ");
								ret.append(content);
								gotTerms.put(content, "");
							}
						}
						ret.append(" ) ");
						ret.append(endBuffer.toString());
						terms = null;
						gotTerms = null;
					}
				}
                catch ( SDXException e ) {
                    //more detailed message is necessary here based upon the above code
                    //1) create "arg" based upon error, and add to "args"
                    //2) create create new error code
                    String[] args = new String[1];
                    args[0] = e.getMessage();
                    throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_GENERIC, args, e);}
                }
			else
			{
				ret.append(" ");
				ret.append(tk);
			}
		}
		return ret.toString();
	}
*/

    /**
     * Adds a base query for this query.
     *
     * <p>
     * A base query is another query, given by its search results,
     * that will be combined with the current query with a logical
     * operator (AND, OR or NOT).
     * <p>
     * Using the AND operator is equivalent to "searching within results".
     * Using the OR operator lets one extend a query. Using the NOT
     * operator lets one eclude some previous results.
     *
     *	@param	results		Results from the base query.
     *	@param	operator	The operator to use.
     */
    public void addBaseQuery(Results results, int operator) {
        this.baseResults = results;
        this.baseOperator = operator;
        if (results != null) this.baseQuery = results.getQuery();
    }

    public void addBaseQuery(Query query, int operator) {
        if (query != null) {
            this.baseQuery = query;
            this.baseOperator = operator;
        }
    }

    /**
     *	Adds a sort specification for this query.
     *
     *	@param	ss		The sort specification.
     */
    public void addSortSpecification(SortSpecification ss) {
        this.sortSpecification = ss;
    }



    /**
     *	Adds a filter for this query.
     *
     *	@param	filter		The filter to add.
     */
    public void addFilter(Filter filter) {
        this.filter = filter;
    }

    /**
     *	Prepares a query before execution.
     *
     * <p>
     * Queries needing a specifc action can override this method that only returns
     * the current filter.
     */
    public Filter prepare() {
        return filter;
    }

    /**
     * Factorization of *Query.toSax() components
     *
     * @param atts    current atts to had attributes
     */
    protected AttributesImpl addAttributesLucene(AttributesImpl atts) throws SAXException {
        atts.addAttribute("", Node.Name.SEARCH_ENGINE, Node.Name.SEARCH_ENGINE, Node.Type.CDATA, Query.SEARCH_ENGINE);
        if (luceneQuery != null && searchLocations != null) {
            // FG: bad for performances but correct a NPE Field f=searchLocations.getDefaultField(); if (f != null)
            try {
                String fieldName = null;
                String luceneQueryString = null;
                Field field = searchLocations.getDefaultField();
                if (field != null) fieldName = field.getCode();
                if (fieldName != null) luceneQueryString = luceneQuery.toString(fieldName);
                if (luceneQueryString != null) {
                    atts.addAttribute("", Node.Name.LUCENE_QUERY, Node.Name.LUCENE_QUERY, Node.Type.CDATA, luceneQueryString);
                    atts.addAttribute("", Node.Name.ESCAPED_LUCENE_QUERY, Node.Name.ESCAPED_LUCENE_QUERY, Node.Type.CDATA, Utilities.encodeURL(luceneQueryString, super.getEncoding()));
                }
            } catch (SDXException sdxE) {
                throw new SAXException(sdxE.getMessage(), sdxE);
            }
        }
        return atts;
    }

    /**
     * Factorization of *Query.toSax() components
     *
     * @param atts    current atts to had attributes
     */
    protected AttributesImpl addAttributesText(AttributesImpl atts) throws SAXException {
        if (queryText != null) {
            atts.addAttribute("", Node.Name.TEXT, Node.Name.TEXT, Node.Type.CDATA, queryText);
            atts.addAttribute("", Node.Name.ESCAPED_TEXT, Node.Name.ESCAPED_TEXT, Node.Type.CDATA, Utilities.encodeURL(queryText, super.getEncoding()));
        }
        return atts;
    }


    /**
     * Formats a query in XML.
     *
     * @param   hdl     A content handler to receive XML data.
     */
    public abstract void toSAX(ContentHandler hdl) throws SAXException;

    /**Sets the SearchLocations object
     * @param sLocs The SearchLocations object
     */
    protected void setSearchLocations(SearchLocations sLocs) throws SDXException {
        if (sLocs == null)
            throw new SDXException(null, SDXExceptionCode.ERROR_SEARCHLOCATIONS_NULL, null, null);
        else
            this.searchLocations = sLocs;
    }

    /**Does nothing here, please see subclasses
     *
     */
    public void setUp() {
    }

    public SearchLocations getSearchLocations() {
        return this.searchLocations;
    }

    protected String getClassNameSuffix() {
        return Query.CLASS_NAME_SUFFIX;
    }

	/* (non-Javadoc)
	 * @see fr.gouv.culture.sdx.search.lucene.query.Query#getBoost()
	 */
	public float getBoost() {
		return luceneQuery.getBoost();
	}

	/* (non-Javadoc)
	 * @see fr.gouv.culture.sdx.search.lucene.query.Query#setBoost(float)
	 */
	public void setBoost(float boost) {
		if(luceneQuery != null)
			if(boost>=0)
				luceneQuery.setBoost(boost);
			else{
				String message= "The boost can't be negative : "+
								this.luceneQuery.toString();
				this.getLog().warn(message);
			}

	}
	/* (non-Javadoc)
	 * @see fr.gouv.culture.sdx.search.lucene.query.Query#setBoost(java.lang.String)
	 */
	public void setBoost(String boost) {
		try{
			setBoost(Float.parseFloat(boost));
		} catch(NumberFormatException e){
			String message= "The string is not a valid float.\n"+
							"The boost will not be applied:\n"+
							boost;
			this.getLog().warn(message,e);
		}
	}

	/* (non-Javadoc)
	 * @see fr.gouv.culture.sdx.utils.AbstractSdxObject#initToSax()
	 */
	protected boolean initToSax() {
		return true;
	}

	/**Init the LinkedHashMap _xmlizable_volatile_objects with the objects in order to describ them in XML
	 * Some objects need to be refresh each time a toSAX is called*/
	protected void initVolatileObjectsToSax() {

	}
}
