/*
Copyright (C) 2000-2010  Ministere de la culture et de la communication (France), AJLSM
See LICENCE file
*/
package fr.gouv.culture.sdx.search.lucene.query;

import fr.gouv.culture.sdx.documentbase.LuceneDocumentBase;
import fr.gouv.culture.sdx.exception.SDXException;
import fr.gouv.culture.sdx.exception.SDXExceptionCode;
import fr.gouv.culture.sdx.framework.Framework;
import fr.gouv.culture.sdx.search.TermHighlighter;
import fr.gouv.culture.sdx.search.lucene.DateField;
import fr.gouv.culture.sdx.search.lucene.Field;
import fr.gouv.culture.sdx.search.lucene.XMLFieldFilter;
import fr.gouv.culture.sdx.utils.Utilities;
import fr.gouv.culture.sdx.utils.constants.Node;

import org.apache.avalon.framework.service.ServiceException;
import org.apache.cocoon.ProcessingException;
import org.apache.excalibur.xml.sax.SAXParser;
import org.apache.lucene.search.Hits;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Date;
import java.util.Enumeration;

/**
 *	The results of a search.
 *
 *	This object also contains information on the search acceptRequest used to obtain them.
 */
public class Results extends AbstractResponse implements fr.gouv.culture.sdx.search.Results {

    /** The search query */
    private Query query;

    /** The sort specification. */
    private SortSpecification sorts;

    /** The sorted results. */
    private ResultDocuments sortedResults;

    /** The score of the first document, which will always be the highest score */
    private float topScore = 1;

    private TermHighlighter highlighter = null;

    private Hits hits = null;

    /**Creates a Results object
     *
     * <p>
     * A super.getLog() must be set and then this object must be setUp.
     *
     * @see #enableLogging
     * @see #setUp
     */
    public Results() {
    }

    public Results(Results results) throws SDXException, IOException {
        setUp(results.getSearchLocations(), results.getHits(), null, results.getQuery());
    }

    /**
     *  Builds the results of a acceptRequest from the Lucene results and a sort specification.
     *
     *  @param	sLocs	        The SearchLocations object (indices searched).
     *	@param	searchHits		Les resultats Lucene.
     *	@param	sorts			Les specifications de tri.
     *	@param	query			La requete qui a permis d'obtenir ces resultats.
     */
    public void setUp(SearchLocations sLocs, Hits searchHits, SortSpecification sorts, Query query) throws SDXException, IOException {
        if (sLocs == null) throw new SDXException(null, SDXExceptionCode.ERROR_SEARCHLOCATIONS_NULL, null, null);
        if (searchHits == null) throw new SDXException(super.getLog(), SDXException.ERROR_SEARCH_HITS_NULL, null, null);
        this.hits = searchHits;
        this.query = query;
        this.sorts = sorts;
        super._searchLocations = sLocs;
        if (this.sorts == null) this.sorts = new SortSpecification();
        this.sorts.enableLogging(super.getLog());
        try {
            if (searchHits.length() > 0) topScore = searchHits.score(0);
        } catch (IOException e) {
            String[] args = new String[1];
            args[0] = e.getMessage();
            throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_SETUP_RESULTS, args, e);
        }

        sortedResults = this.sorts.sortResults(searchHits);
        super.setNbPages(countPages());
    }

    /**
     *	Builds the results of a acceptRequest from the Lucene results.
     *
     *  @param	sLocs	        The SearchLocations object (indices searched).
     *	@param	searchHits		Les résultats Lucene.
     *	@param	query			La requete qui a permis d'obtenir ces resultats.
     */
    public void setUp(SearchLocations sLocs, Hits searchHits, Query query) throws SDXException {
        if (searchHits == null) throw new SDXException(super.getLog(), SDXException.ERROR_SEARCH_HITS_NULL, null, null);

        this.query = query;
        super._searchLocations = sLocs;
        this.sorts = new SortSpecification();
        this.sorts.enableLogging(super.getLog());

        try {
            if (searchHits.length() > 0) topScore = searchHits.score(0);
        } catch (IOException e) {
            String[] args = new String[1];
            args[0] = e.getMessage();
            throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_SETUP_RESULTS, args, e);
        }

        sortedResults = sorts.sortResults(searchHits);
        super.setNbPages(countPages());
    }

    /**
     *	Retourne tous les resultats sous la forme d'un element DOM.
     *
     *	@param	factory		Le document servant de manufacture.
     */
/*
	public Element toDOM(Document factory) throws SDXException, SQLException, Exception
	{
		return toDOM(factory, 1, false);
	}

*/
    public Hits getHits() {
        return hits;
    }

    /**
     *	Returns an XML representation of the results.
     *
     *	@param	hdl		The ContentHandler to feed with events.
     */
    public void toSAX(ContentHandler hdl) throws SAXException {
        try {
            toSAX(hdl, 1, false);
        } catch (SDXException e) {
            throw new SAXException(e);
        }

    }

    /**
     *	Retourne tous les resultats sous la forme d'un element DOM.
     *
     *	@param	factory			Le document servant de manufacture.
     *	@param	getDocuments	Indique si on doit retourner les documents au complet
     */
/*
	public Element toDOM(Document factory, boolean getDocuments) throws SDXException, SQLException, Exception
	{
		return toDOM(factory, 1, getDocuments);
	}
*/

    /**
     *	Returns an XML representation of the results.
     *
     *	@param	hdl		        The ContentHandler to feed with events.
     *	@param	getDocuments	Indicates if the actual complete documents should be returned
     */
    public void toSAX(ContentHandler hdl, boolean getDocuments) throws SDXException, Exception {
        toSAX(hdl, 1, getDocuments);
    }

    /**
     *	Retourne une partie des resultats sous la forme d'un element DOM.
     *
     *	@param	factory			Le document servant de manufacture.
     *	@param	pageNo			Le numero de la page a retourner.
     */
/*
	public Element toDOM(Document factory, int pageNo) throws SDXException, SQLException, Exception
	{
		return toDOM(factory, pageNo, false);
	}
*/

    /**
     *	Returns an XML representation of the results.
     *
     *	@param	hdl		        The ContentHandler to feed with events.
     *	@param	pageNo			The page number of the desired results.
     */
    public void toSAX(ContentHandler hdl, int pageNo) throws SDXException, Exception {
        toSAX(hdl, pageNo, false);
    }

    /**
     *	Retourne une partie des resultats sous la forme d'un element DOM.
     *
     *	@param	factory			Le document servant de manufacture.
     *	@param	pageNo			La page a retourner.
     *	@param	getDocuments	Indique si on doit retourner les documents.
     */
/*
	public Element toDOM(Document factory, int pageNo, boolean getDocuments) throws SDXException, SQLException, Exception
	{
		int nbHits = sortedResults.getLength();
		if ( pageNo < 1 ) pageNo = 1;
		if ( pageNo > nbPages ) pageNo = nbPages;
		int sIdx = ( (pageNo - 1) * hitsPerPage );
		if ( sIdx < 0 ) sIdx = 0;

		int eIdx = ( pageNo * hitsPerPage ) - 1;
		if ( eIdx >= nbHits ) eIdx = nbHits - 1;

		String nsURI = Utilities.getSDXNamespaceURI();
		String nsPrefix = Utilities.getSDXNamespacePrefix();

		Element top = factory.createElementNS(nsURI, nsPrefix + ":results");
		top.setAttribute("xmlns:" + nsPrefix, nsURI);
		top.setAttribute("id", String.valueOf(id));
		top.setAttribute("start", String.valueOf(sIdx+1));
		top.setAttribute("end", String.valueOf(eIdx+1));
		top.setAttribute("nb", String.valueOf(nbHits));
		top.setAttribute("nbPages", String.valueOf(nbPages));
		top.setAttribute("currentPage", String.valueOf(pageNo));

		// On ajoute aussi une representation DOM de la requete
		top.appendChild(query.toDOM(factory));

		// S'il y a lieu, on ajoute une representation DOM des tris
		if ( sorts != null ) top.appendChild(sorts.toDOM(factory));

		// Si necessaire, une connexion a la base de donnees.
		Connection connection = null;
		DBConnection db = null;

		try
		{
			if ( getDocuments )
			{
				db = pool.getConnection(db.getEnvironment().getPoolName());
				connection = db.getConnection();
			}

			if ( nbHits > 0 && hitsPerPage != 0 )
			{
				Element result;
				Element fieldEl;
				org.apache.lucene.document.Document resultDoc;
				boolean error = false;
				Enumeration fields;
				Field field;
				String fieldName;
				String previousDocId = null;
				org.apache.lucene.document.Document nextDoc;
				int fieldType;

				float maxScore = sortedResults.getMaxScore();

				for ( int i=sIdx; i<=eIdx; i++ )
				{
					result = factory.createElementNS(nsURI, nsPrefix + ":result");
					result.setAttribute("no", String.valueOf(i+1));

					try
					{
						ResultDocument currentDocument = sortedResults.getDocument(i);
						result.setAttribute("score", String.valueOf(currentDocument.getScore()));
						float pctScore = 100 * (currentDocument.getScore() / maxScore);
						result.setAttribute("pctScore", String.valueOf(Math.round(Math.floor(pctScore))));

						// Ajout de l'identifiant du document precedent
						if ( previousDocId != null ) result.setAttribute("previousDocument", previousDocId);

						// Ajout de l'identifiant du document suivant
						if ( i < nbHits - 1 )
							result.setAttribute("nextDocument", sortedResults.getDocument(i+1).getFieldValue("sdxdocid"));


						resultDoc = currentDocument.getDocument();
						fields = resultDoc.fields();
						while ( fields.hasMoreElements() )
						{
							field = (Field)fields.nextElement();
							fieldName = field.name();
							if ( field.isStored() )
							{
								fieldEl = factory.createElementNS(nsURI, nsPrefix + ":field");
								fieldEl.setAttribute("name", fieldName);
								fieldEl.setAttribute("indexed", String.valueOf(field.isIndexed()));
								fieldEl.setAttribute("tokenized", String.valueOf(field.isTokenized()));
								fieldType = db.getFieldType(fieldName);
								if ( fieldType == DBField.DATE )
									fieldEl.appendChild(factory.createTextNode(Utilities.formatDate(DateField.stringToDate(resultDoc.get(fieldName)))));
								else

								//TODO: toujours la meme valeur lors des occurrences multiples...
									fieldEl.appendChild(factory.createTextNode(resultDoc.get(fieldName)));
								result.appendChild(fieldEl);
							}
							if ( getDocuments && fieldName.equals("sdxdocid") )
							{
								// On doit aller chercher le document dans la base.
								result.appendChild(factory.importNode(db.getDocumentAsDOM(resultDoc.get(fieldName), connection), true));
							}
							if ( fieldName.equals("sdxdocid") ) previousDocId = resultDoc.get(fieldName);
						}
					}
					catch ( IOException e )
					{
						error = true;
					}

					top.appendChild(result);
					if ( error ) top.setAttribute("error", "yes");
				}
			}
			return top;
		}
		catch ( SQLException e )
		{
			throw e;
		}
		finally
		{
            try { if ( connection != null ) { pool.releaseConnection(db); } } catch ( Exception e ) { throw e  ;}
		}


	}
*/

    /**
     *	Returns an XML representation of the results.
     *
     *	@param	hdl		        The ContentHandler to feed with events.
     *	@param	pageNo			The requested page number of the desired results. Reaffected when number of results doesn't fit.
     *	@param	getDocuments	Indicates if the actual complete documents should be returned
     */
    public void toSAX(ContentHandler hdl, int pageNo, boolean getDocuments) throws SDXException, SAXException {
        //TODOException?: better exception handling here, should only throw an SDXException?-rbp
        int nbHits = sortedResults.getLength();

        int sIdx;
        int eIdx;

        if (nbHits > 0) {
            //Reaffecting pageNo when it doesn't fit
            if (pageNo < 1) pageNo = 1;
            if (pageNo > super.getNbPages()) pageNo = super.getNbPages();

            //Computing start and end (1-based) indices
            sIdx = ((pageNo - 1) * getHitsPerPage()) + 1;
            eIdx = (pageNo * getHitsPerPage());

            //Unlikely events (negative hitsPerPage) : keept as is
            if (sIdx < 1) sIdx = 1;
            if (eIdx > nbHits) eIdx = nbHits;
        }
        //special case when no result : everything set to 0.
        else {
            pageNo = 0;
            sIdx = 0;
            eIdx = 0;
        }

        // SAX element creation-rbp12/03/02

        //Creation of local variables which are later passed into startElement() and endElement() methods-rbp12/03/02
        String sdxNsUri = Framework.SDXNamespaceURI;
        String sdxNsPrefix = Framework.SDXNamespacePrefix;

        String localName = Node.Name.RESULTS;
        String qName = sdxNsPrefix + ":" + localName;
        AttributesImpl atts = new AttributesImpl();
        atts.addAttribute("", Node.Name.QID, Node.Name.QID, Node.Type.CDATA, String.valueOf(super.getId()));
        atts.addAttribute("", Node.Name.PAGE, Node.Name.PAGE, Node.Type.CDATA, String.valueOf(pageNo));
        atts.addAttribute("", Node.Name.HPP, Node.Name.HPP, Node.Type.CDATA, String.valueOf(super.getHitsPerPage()));
        atts.addAttribute("", Node.Name.PAGES, Node.Name.PAGES, Node.Type.CDATA, String.valueOf(super.getNbPages()));
        atts.addAttribute("", Node.Name.NB, Node.Name.NB, Node.Type.CDATA, String.valueOf(nbHits));
        atts.addAttribute("", Node.Name.START, Node.Name.START, Node.Type.CDATA, String.valueOf(sIdx));
        atts.addAttribute("", Node.Name.END, Node.Name.END, Node.Type.CDATA, String.valueOf(eIdx));
        // for backward compatibility
        atts.addAttribute("", Node.Name.ID, Node.Name.ID, Node.Type.CDATA, String.valueOf(super.getId()));
        atts.addAttribute("", Node.Name.CURRENT_PAGE, Node.Name.CURRENT_PAGE, Node.Type.CDATA, String.valueOf(pageNo));
        atts.addAttribute("", Node.Name.NB_PAGES, Node.Name.NB_PAGES, Node.Type.CDATA, String.valueOf(super.getNbPages()));

        //startElement() method is called for "results" and local variables are passed
        hdl.startElement(sdxNsUri, localName, qName, atts);

        // Add a SAX representation of the query
        if (query != null) query.toSAX(hdl);

        // When relevant, add a SAX representation of the sorts.
        if (sorts != null) sorts.toSAX(hdl);

        // Si necessaire, une connexion a la base de donnees.
//TODO?		Connection connection = null;
//TODO?		DBConnection db = null;

        try {
//TODO?			if ( getDocuments )
//			{
//				db = pool.getConnection(db.getEnvironment().getPoolName());
//				connection = db.getConnection();
//			}

            if (nbHits > 0 && super.getNbPages() != 0) {

                org.apache.lucene.document.Document resultDoc = null;
//                boolean error = false;
                Enumeration fields = null;
                org.apache.lucene.document.Field field;
                String fieldName;
                String previousDocId = null;
//                org.apache.lucene.document.Document nextDoc;
                int fieldType;

                float maxScore = getMaxScore();

                for (int i = sIdx; i <= eIdx; i++) {
                    ResultDocument currentDocument = sortedResults.getDocument(i - 1);
                    if (currentDocument != null) {
                        String childOneLocalName = Node.Name.RESULT;
                        String childOneQName = sdxNsPrefix + ":" + childOneLocalName;
                        AttributesImpl childOneAtts = new AttributesImpl();
                        childOneAtts.addAttribute("", Node.Name.NO, Node.Name.NO, Node.Type.CDATA, String.valueOf(i));
                        childOneAtts.addAttribute("", Node.Name.SCORE, Node.Name.SCORE, Node.Type.CDATA, String.valueOf(currentDocument.getScore()));
                        float pctScore = 100 * (currentDocument.getScore() / maxScore);
                        childOneAtts.addAttribute("", Node.Name.PCT_SCORE, Node.Name.PCT_SCORE, Node.Type.CDATA, String.valueOf(Math.round(Math.floor(pctScore))));

/*
                        // Ajout de l'identifiant du document precedent
                        if (previousDocId != null)
                            childOneAtts.addAttribute("", "previousDocument", "previousDocument", ConfigurationNode.Type.CDATA, previousDocId);
                        // Ajout de l'identifiant du document suivant
                        if (i < nbHits - 1 && sortedResults.getDocument(i + 1) != null)
                            childOneAtts.addAttribute("", "nextDocument", "nextDocument", ConfigurationNode.Type.CDATA, sortedResults.getDocument(i + 1).getFieldValue("sdxdocid"));
*/
                        //startElement() method is called for "result" and local variables are passed-rbp12/03/02
                        hdl.startElement(sdxNsUri, childOneLocalName, childOneQName, childOneAtts);


                        resultDoc = currentDocument.getDocument();
                        //List of processed fields
                        //                       Hashtable processedFields = new Hashtable();
                        if (resultDoc != null)
                            fields = resultDoc.fields();
                        if (fields != null) {
                            while (fields.hasMoreElements()) {
                                field = (org.apache.lucene.document.Field) fields.nextElement();
                                if (field != null) {
                                    fieldName = field.name();
//                                if (!processedFields.containsKey(fieldName) && field.isStored()) {
                                    //                                //adding the field to indicate that it was processed
//                                    if (processedFields != null) processedFields.put(fieldName, "");

//                                    String[] fieldValues = resultDoc.getValues(fieldName);
//                                    for (int j = 0; j < fieldValues.length; j++) {
                                    String childTwoLocalName = Node.Name.FIELD;
                                    String childTwoQName = sdxNsPrefix + ":" + childTwoLocalName;
                                    AttributesImpl childTwoAtts = new AttributesImpl();
                                    fieldType = super._searchLocations.getFieldType(fieldName);
	                                String value = "";
	                                value = field.stringValue();
	                                childTwoAtts.addAttribute("", Node.Name.NAME, Node.Name.NAME, Node.Type.CDATA, fieldName);
	                                if(fieldType != Field.XML){
	                                    // try { esc=java.net.URLEncoder.encodeURL(value, Framework.URL_ENCODING); } catch (java.io.UnsupportedEncodingException e) {}  // 1.4
	                                    if (fieldType == Field.DATE) {
	                                        Date date = DateField.stringToDate(value);
	                                        long milliSecs = 0;
	                                        if (date != null)
	                                            milliSecs = date.getTime();
	                                        value = fr.gouv.culture.sdx.utils.Date.formatDate(date);
	                                        childTwoAtts.addAttribute("", "timeInMilliseconds", "timeInMilliseconds", Node.Type.CDATA, String.valueOf(milliSecs));
	                                        //DEBUG                                   System.out.println("ms time on results : " + milliSecs);
	                                    }

	                                    String esc = "";
	                                    esc = Utilities.encodeURL(value, super.getEncoding());
	                                    childTwoAtts.addAttribute("", Node.Name.VALUE, Node.Name.VALUE, Node.Type.CDATA, value);
	                                    if (esc != null)
	                                        childTwoAtts.addAttribute("", Node.Name.ESCAPED_VALUE, Node.Name.ESCAPED_VALUE, Node.Type.CDATA, esc);
                                    }
                                    String fieldTypeName = "";
                                    fieldTypeName = super._searchLocations.getTypeName(fieldName);
                                    if (fieldTypeName != null)
                                        childTwoAtts.addAttribute("", Node.Name.TYPE, Node.Name.TYPE, Node.Type.CDATA, fieldTypeName);
                                    childTwoAtts.addAttribute("", Node.Name.INDEXED, Node.Name.INDEXED, Node.Type.CDATA, String.valueOf(field.isIndexed()));
                                    childTwoAtts.addAttribute("", Node.Name.TOKENIZED, Node.Name.TOKENIZED, Node.Type.CDATA, String.valueOf(field.isTokenized()));

                                    //startElement() method is called for "field" and local variables are passed-rbp12/03/02
                                    hdl.startElement(sdxNsUri, childTwoLocalName, childTwoQName, childTwoAtts);

                                    if(fieldType == Field.XML){
                                    	XMLFieldFilter filter = new XMLFieldFilter();
                                    	filter.setContentHandler(hdl);
                                    	try{
                                    		SAXParser parser = (SAXParser)_manager.lookup(SAXParser.ROLE);
                                    		parser.parse(new InputSource(new ByteArrayInputStream(value.getBytes("UTF-8"))),filter);

                                    	}catch(ServiceException e){
                                    		throw new SDXException("Error loading the parser for xml field",e);
                                    	}catch(IOException e){
                                    		throw new SDXException("Error parsing the xml field",e);
                                    	}
                                    }
                                    else{
	                                    // deprecated
	                                    char[] chars = value.toCharArray();
	                                    hdl.characters(chars, 0, chars.length);
                                    }

                                    //endElement() method is called for "field" and local variables are passed-rbp12/03/02
                                    hdl.endElement(sdxNsUri, childTwoLocalName, childTwoQName);
//                                    }


                                    //                               }

                                    if (fieldName.equals(LuceneDocumentBase.INTERNAL_FIELD_NAME_SDXDOCID)) previousDocId = resultDoc.get(fieldName);
                                }


                            }
                        }
                        //endElement() method is called for "result" and local variables are passed-rbp12/03/02
                        hdl.endElement(sdxNsUri, childOneLocalName, childOneQName);
                    }




                    //TODO?: Sax Error Handling -what to do here?-rbp12/03/02
                    //if ( error ) top.setAttribute("error", "yes");
                }
            }

            //endElement() method is called for "results" and local variables are passed-rbp12/03/02
            hdl.endElement(sdxNsUri, localName, qName);
        } finally {
//TODO?            try { if ( connection != null ) { pool.releaseConnection(db); } } catch ( Exception e ) { throw e  ;}
        }


    }

    /**
     *	Ecrit une representation XML de ces resultats.
     *
     *	@param	os				Le flux de sortie pour l'ecriture.
     *	@param	pageNo			Le numero de page a envoyer.
     *	@param	getDocuments	Si on doit inclure les documents ou non.
     */
/* No need for this anymore as it is being handled in the servlet class, SearchServlet.java.-rbp21/03/02
    public void writeXML(OutputStream os, int pageNo, boolean getDocuments) throws SDXException, SQLException, Exception
	{
		XMLSerializer ser = new XMLSerializer(os, new OutputFormat("xml", "UTF-8", true));
		try
		{
			ser.serialize(toDOM(new DocumentImpl(), pageNo, getDocuments));
		}
		catch ( IOException e )
		{
			throw new SDXException(e, "fr", "Impossible de serialiser le document XML");
		}
	}
 */


    /**
     *	Ecrit une representation XML de ces resultats.
     *
     *	@param	os				Le flux de sortie pour l'ecriture.
     *	@param	getDocuments	Si on doit inclure les documents ou non.
     */
/*No need for this anymore as it is being handled in the servlet class, SearchServlet.java.-rbp21/03/02
	public void writeXML(OutputStream os, boolean getDocuments) throws SDXException, SQLException, Exception
	{
		XMLSerializer ser = new XMLSerializer(os, new OutputFormat("xml", "UTF-8", true));
		try
		{
			ser.serialize(toDOM(new DocumentImpl(), getDocuments));
		}
		catch ( IOException e )
		{
			throw new SDXException(e, "fr", "Impossible de serialiser le document XML");
		}
	}
*/


    /**
     *	Counts and returns the number of pages for these results.
     */
    public int countPages() {
        if (super.getHitsPerPage() == 0) return 1;
        int nbHits = sortedResults.getLength();
        if (nbHits == 0)
            return 0;
        else {
            float temp = ((float) nbHits / (float) super.getHitsPerPage());
            if (temp != Math.round(temp))
                return (int) (Math.round(Math.floor(temp) + 1));
            else
                return Math.round(temp);
        }
    }

    /**
     *	Indicates that all the results should be returned .
     */
    public void setAllHits() {
        super.setHitsPerPage(sortedResults.getLength());
        super.setNbPages(1);
    }

    /**
     *	Set the number of hits per page.
     *
     *	@param	nb		The number of hits.
     */
    public void setHitsPerPage(int nb) {
        if (nb < 0)
            setAllHits();
        else {
            super.setHitsPerPage(nb);
            super.setNbPages(countPages());
        }
    }


    /**
     *	Retourne la requete sous forme d'un element DOM.
     *
     *	@param	factory		Le document qui doit inclure cet element.
     */
/*
	public Element getQueryAsDOM(Document factory) throws SDXException
	{
		return query.toDOM(factory);
	}
*/

    /**
     *	Returns an XML representation of the Query.
     *
     *	@param	hdl		        The ContentHandler to feed with events.
     */
    public void getQueryAsSAX(ContentHandler hdl) throws SDXException, SAXException, ProcessingException {
        query.toSAX(hdl);
    }

    /**
     *	Returns the document id for each document of the result set.
     */
    public String[] getDocIds() throws SDXException, IOException {
        int nbHits = sortedResults.getLength();
        String[] docIds = new String[nbHits];
        org.apache.lucene.document.Document doc = null;
        for (int i = 0; i < nbHits; i++) {
            doc = sortedResults.getDocument(i).getDocument();
            docIds[i] = doc.get(LuceneDocumentBase.INTERNAL_FIELD_NAME_SDXDOCID);
        }
        return docIds;
    }

    /**
     *	Returns the search acceptRequest which created these results.
     */
    public Query getQuery() {
        return query;
    }

    /**
     *	Retourne une representation XML des informations sur les documents precedents et suivants.
     *
     *	Cette representation inclut l'identifiant ainsi que le numero de page du document
     *	precedent et du document suivant, par rapport au document dont le numero est fourni.
     *
     *	@param	docNo		Le numero du document dans l'ordre des resultats (commence a 1).
     *	@param	factory		Le document permettant de creer des elements.
     */
/*
	public Element getNavigationAsDOM(Document factory, int docNo) throws SDXException, IOException
	{
		if ( factory == null ) throw new SDXException("fr", "Incapable de construire un DOM");

		// On va voir si le document existe
		int nbHits = sortedResults.getLength();
		if ( docNo <= nbHits && docNo > 0 && sortedResults.getDocument(docNo-1) != null )
		{
			String nsURI = Utilities.getSDXNamespaceURI();
			String nsPrefix = Utilities.getSDXNamespacePrefix();

			Element top = factory.createElementNS(nsURI, nsPrefix + ":navigation");
			top.setAttribute("xmlns:" + nsPrefix, nsURI);
			top.setAttribute("docNo", "" + docNo);
			top.setAttribute("page", "" + getPage(docNo));
			top.setAttribute("queryId", id);

			Element sub;
			if ( docNo > 1 )
			{
				sub = factory.createElementNS(nsURI, nsPrefix + ":previous");
				sub.setAttribute("docId", sortedResults.getDocument(docNo-2).getFieldValue("sdxdocid"));
				sub.setAttribute("no", "" + String.valueOf(docNo-1));
				sub.setAttribute("page", "" + getPage(docNo-1));
				top.appendChild(sub);
			}

			if ( docNo < nbHits )
			{
				sub = factory.createElementNS(nsURI, nsPrefix + ":next");
				sub.setAttribute("docId", sortedResults.getDocument(docNo).getFieldValue("sdxdocid"));
				sub.setAttribute("no", "" + String.valueOf(docNo+1));
				sub.setAttribute("page", "" + getPage(docNo+1));
				top.appendChild(sub);
			}

			return top;
		}
		else throw new SDXException("fr", "Le document '" + docNo + "' n'existe pas dans ces resultats.");
	}
*/

    /**
     *	Returns a representation XML of information on the preceding and following documents.
     *
     * This representation includes the _identifier as well as the page number of the
     * preceding and the following documents as compared to the document whose number is provided
     *
     *	@param	docNo	The 1-based index of the document in the sorted results
     *	@param 	hdl 	The ContentHandler which will receive the events
     */
    public void getNavigationAsSAX(ContentHandler hdl, int docNo) throws SDXException, SAXException, ProcessingException, IOException {

        if (hdl == null) throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_CONTENT_HANDLER_NULL, null, null);
        // On va voir si le document existe
        int nbHits = sortedResults.getLength();
        if (docNo <= nbHits && docNo > 0 && sortedResults.getDocument(docNo - 1) != null) {
            String sdxNsUri = Framework.SDXNamespaceURI;
            String sdxNsPrefix = Framework.SDXNamespacePrefix;

            String localName = Node.Name.NAVIGATION;
            String qName = sdxNsPrefix + ":" + localName;
            AttributesImpl atts = new AttributesImpl();
            atts.addAttribute("", Node.Name.QID, Node.Name.QID, Node.Type.CDATA, String.valueOf(super.getId()));
            atts.addAttribute("", Node.Name.PAGE, Node.Name.PAGE, Node.Type.CDATA, "" + getPage(docNo));
            atts.addAttribute("", Node.Name.NO, Node.Name.NO, Node.Type.CDATA, "" + docNo);
            // not documented, preserved for compatibility
            atts.addAttribute("", Node.Name.QUERY_ID, Node.Name.QUERY_ID, Node.Type.CDATA, String.valueOf(super.getId()));
            atts.addAttribute("", Node.Name.DOC_NO, Node.Name.DOC_NO, Node.Type.CDATA, "" + docNo);

            //startElement() method is called for "navigation" and local variables are passed
            hdl.startElement(sdxNsUri, localName, qName, atts);

            // Add a SAX representation of the query
            if (query != null) query.toSAX(hdl); //Note : quite redundant with main element ; required for backward compatibility. -pb

            if (docNo > 1 && sortedResults.getDocument(docNo - 2) != null) {
                String childOneLocalName = Node.Name.PREVIOUS;
                String childOneQName = sdxNsPrefix + ":" + childOneLocalName;
                AttributesImpl childOneAtts = new AttributesImpl();
                childOneAtts.addAttribute("", Node.Name.ID, Node.Name.ID, Node.Type.CDATA, sortedResults.getDocument(docNo - 2).getFieldValue(LuceneDocumentBase.INTERNAL_FIELD_NAME_SDXDOCID));
                childOneAtts.addAttribute("", Node.Name.BASE, Node.Name.BASE, Node.Type.CDATA, sortedResults.getDocument(docNo - 2).getFieldValue(LuceneDocumentBase.INTERNAL_FIELD_NAME_SDXDBID));
                childOneAtts.addAttribute("", Node.Name.APP, Node.Name.APP, Node.Type.CDATA, sortedResults.getDocument(docNo - 2).getFieldValue(LuceneDocumentBase.INTERNAL_FIELD_NAME_SDXAPPID));
                childOneAtts.addAttribute("", Node.Name.NO, Node.Name.NO, Node.Type.CDATA, "" + String.valueOf(docNo - 1));
                childOneAtts.addAttribute("", Node.Name.PAGE, Node.Name.PAGE, Node.Type.CDATA, "" + getPage(docNo - 1));
                // not documented, preserved for compatibility
                childOneAtts.addAttribute("", Node.Name.DOC_ID, Node.Name.DOC_ID, Node.Type.CDATA, sortedResults.getDocument(docNo - 2).getFieldValue(LuceneDocumentBase.INTERNAL_FIELD_NAME_SDXDOCID));
                //startElement() method is called for "previous" and local variables are passed
                hdl.startElement(sdxNsUri, childOneLocalName, childOneQName, childOneAtts);

                //endElement() method is called for "previous" and local variables are passed
                hdl.endElement(sdxNsUri, childOneLocalName, childOneQName);

            }

            if (docNo < nbHits && sortedResults.getDocument(docNo) != null) {

                String childTwoLocalName = Node.Name.NEXT;
                String childTwoQName = sdxNsPrefix + ":" + childTwoLocalName;
                AttributesImpl childTwoAtts = new AttributesImpl();
                childTwoAtts.addAttribute("", Node.Name.ID, Node.Name.ID, Node.Type.CDATA, sortedResults.getDocument(docNo).getFieldValue(LuceneDocumentBase.INTERNAL_FIELD_NAME_SDXDOCID));
                childTwoAtts.addAttribute("", Node.Name.BASE, Node.Name.BASE, Node.Type.CDATA, sortedResults.getDocument(docNo).getFieldValue(LuceneDocumentBase.INTERNAL_FIELD_NAME_SDXDBID));
                childTwoAtts.addAttribute("", Node.Name.APP, Node.Name.APP, Node.Type.CDATA, sortedResults.getDocument(docNo).getFieldValue(LuceneDocumentBase.INTERNAL_FIELD_NAME_SDXAPPID));
                childTwoAtts.addAttribute("", Node.Name.NO, Node.Name.NO, Node.Type.CDATA, "" + String.valueOf(docNo + 1));
                childTwoAtts.addAttribute("", Node.Name.PAGE, Node.Name.PAGE, Node.Type.CDATA, "" + getPage(docNo + 1));
                // not documented, preserved for compatibility
                childTwoAtts.addAttribute("", Node.Name.DOC_ID, Node.Name.DOC_ID, Node.Type.CDATA, sortedResults.getDocument(docNo).getFieldValue(LuceneDocumentBase.INTERNAL_FIELD_NAME_SDXDOCID));

                //startElement() method is called for "next" and local variables are passed
                hdl.startElement(sdxNsUri, childTwoLocalName, childTwoQName, childTwoAtts);

                //endElement() method is called for "next" and local variables are passed
                hdl.endElement(sdxNsUri, childTwoLocalName, childTwoQName);

            }

            //endElement() method is called for "navigation" and local variables are passed
            hdl.endElement(sdxNsUri, localName, qName);


        } else if (docNo > -1) {
            /* String[] args = new String[1];
             args[0] = Integer.toString(docNo);
             throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_DOCUMENT_NOT_IN_RESULTS, args, null);*/
        }
    }

    /**
     *	Returns a representation XML of information on the preceding and following documents.
     *
     * This representation includes the _identifier as well as the page number of the
     * preceding and the following documents as compared to the document whose number is provided
     *
     *	@param 	hdl 	The ContentHandler which will receive the events
     *	@param	id	The id of the document
     */
    public void getNavigationAsSAX(ContentHandler hdl, String id) throws SDXException, SAXException, ProcessingException, IOException {
        String[] docIds = getDocIds();
        for (int i = 0; i < docIds.length; i++) {
            if (id.equals(docIds[i])) {
                //results are 1-based
                getNavigationAsSAX(hdl, i + 1);
                return;
            }
        }
        /* String[] args = new String[1];
         args[0] = id;
         throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_DOCUMENT_NOT_IN_RESULTS, args, null);*/
    }

    /**
     *	Returns the page number of a document identified by its sequence number.
     *
     *	@param	docNo		The sequence number of the document .
     */
    private int getPage(int docNo) {
        if (docNo < 1) docNo = 1;
        float temp = ((float) docNo / (float) super.getHitsPerPage());
        if (temp != Math.round(temp))
            return (int) (Math.round(Math.floor(temp) + 1));
        else
            return Math.round(temp);
    }

    /**
     *	Resorts the results with a sort specification.
     *
     *	@param	sorts		The sort specification.
     */
    public void reSort(SortSpecification sorts) throws IOException, SDXException {
        this.sorts = sorts;
        if (this.sorts == null) this.sorts = new SortSpecification();
        this.sorts.enableLogging(super.getLog());
        sortedResults = this.sorts.sortResults(sortedResults);
    }

    /**
     *	Returns the number of results.
     */
    public int count() {
        if (sortedResults == null)
            return 0;
        else
            return sortedResults.getLength();
    }

    /**
     * Returns the score of the first document, which is always the highest score.
     */
    public float getMaxScore() {
        return topScore;
    }

    public TermHighlighter getHighliter() {
        return this.highlighter;
    }

    public void setHighliter(TermHighlighter hliter) {
        this.highlighter = hliter;
    }

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

}
