/*
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.exception.SDXException;
import fr.gouv.culture.sdx.exception.SDXExceptionCode;
import fr.gouv.culture.sdx.framework.Framework;
import fr.gouv.culture.sdx.search.lucene.DateField;
import fr.gouv.culture.sdx.search.lucene.Field;
import fr.gouv.culture.sdx.utils.AbstractSdxObject;
import fr.gouv.culture.sdx.utils.constants.Node;
import org.apache.lucene.search.Hits;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;

import java.io.IOException;
import java.text.Collator;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Hashtable;
import java.util.Vector;


/*
 *	Des indications pour trier des resultats de recherche.
 *
 *	Les specifications de tri sont une liste ordonnee
 *	de cles de tri (classe SortKey).
 */

/**
 * Indications for sorting of search results.
 *
 * The specifications of sorting are an ordered list
 * of sort keys (SortKey class).
 */
public class SortSpecification extends AbstractSdxObject implements Comparator {

    public static final String CLASS_NAME_SUFFIX = "SortSpecification";
    /** int to specifiy and an ascending sort. */
    public final static int SORT_ORDER_ASCENDANT = 0;

    /** int to specifiy and an descending sort. */
    public final static int SORT_ORDER_DESCENDANT = 1;

    /** The field for the sorting by relevance. */
    public final static String SORT_RANKING = "sdxscore";

    /** The int to specify an unlimited sort */
    public final static int SORT_UNLIMITED = -1;

    /** The list of sort keys. */
    private Vector sortKeys = new Vector();

    /** A list of collators needed for sorting. */
    private Hashtable collators;

    /** The maximum number of documents to sort.
     * Set as SORT_UNLIMITED if unlimited
     */
    private int max_sort;

    /**
     *	Build a specification for sorting.
     */
    public SortSpecification() throws SDXException {
        collators = new Hashtable();
        this.max_sort = SortSpecification.SORT_UNLIMITED;
    }

    /**
     *	Adds a sort key to the specification by a field and an order.
     *
     *	@param	field		The field.
     *	@param	order		The order.
     */
    public void addSortKey(Field field, int order) throws SDXException {
        if (field != null)
            sortKeys.add(new SortKey(field, order));
    }

    /**
     *	Adds an ascending sort key to the specification by a field.
     *
     *	@param	field		The field.
     */
    public void addSortKey(Field field) throws SDXException {
        if (field != null)
            sortKeys.add(new SortKey(field, SORT_ORDER_ASCENDANT));
    }

    /**
     *	Adds a sort key to the specification,
     *  a searchLocation is needed to obtains information about locale and analysers
     *
     *	@param	fieldName		The field.
     *  @param  order           order
     *  @param  locations
     */

    public void addSortKey(String fieldName, int order, SearchLocations locations) throws SDXException {
        if (locations == null) return;
        Field f = locations.getField(fieldName);
        if (f != null)
            sortKeys.add(new SortKey(f, order));
    }

    /**
     *	Adds a sort key to the specification,
     *  a searchLocation is needed to obtains information about locale and analysers
     *
     *	@param	fieldName		The field.
     *  @param  locations
     */

    public void addSortKey(String fieldName, SearchLocations locations) throws SDXException {
        addSortKey(fieldName, SORT_ORDER_ASCENDANT, locations);
    }


    /**
     * Sorts the search results.
     *
     *@param hits   The search hits
     */
    public ResultDocuments sortResults(Hits hits) throws SDXException {
        if (hits == null) throw new SDXException(super.getLog(), SDXException.ERROR_HITS_NULL, null, null);
        ResultDocuments rDocs = null;

        if (sortKeys.size() == 0 || (sortKeys.size() == 1 && ((SortKey) sortKeys.get(0)).isRankingSort())) {
            // Aucune cle de tri ou tri de pertinence seulement, alors on doit simplement
            // copier la liste de resultats
            rDocs = new ResultDocuments();
            rDocs.enableLogging(super.getLog());
            rDocs.setUp(hits);
            return rDocs;
        } else {
            // On doit effectuer un tri
            int nbHits = hits.length();
            ResultDocument[] ret = new ResultDocument[nbHits];
            for (int i = 0; i < nbHits; i++) {
                ResultDocument rDoc = new ResultDocument();
                rDoc.enableLogging(super.getLog());
                try {
                    rDoc.setUp(hits.doc(i), hits.score(i));
                } catch (IOException e) {
                    //we could not get the document at the index specified from the hits
                    String[] args = new String[1];
                    args[0] = e.getMessage();
                    throw new SDXException(null, SDXExceptionCode.ERROR_GET_LUCENE_HITS_DOC, args, e);
                }
                ret[i] = rDoc;
            }

            Arrays.sort(ret, this);
            rDocs = new ResultDocuments();
            rDocs.enableLogging(super.getLog());
            rDocs.setUp(ret);
            return rDocs;
        }
    }

    /**
     *	Effectue le tri de resultats de recherche.
     */
/*	public ResultDocument[] sortResults(Hits hits) throws SDXException, IOException
	{
		if ( hits == null ) throw new SDXException("fr", "Aucun resultat a trier");

		int nbHits = hits.length();

		ResultDocument[] ret = new ResultDocument[nbHits];

		for (int i=0; i<nbHits; i++)
			ret[i] = new ResultDocument(hits.doc(i), hits.score(i));

		if ( sortKeys.size() == 0 || ( sortKeys.size() == 1 && ((SortKey)sortKeys.get(0)).isRankingSort() ) )
		{
			// Aucune cle de tri ou tri de pertinence seulement, alors on doit simplement
			// copier la liste de resultats
		}
		else
		{
			// On doit effectuer un tri
			Arrays.sort(ret, this);
		}
		return ret;
	}
 */

    /**
     *  Sorts the search results.
     *
     *@param res   The result documents
     */
    public ResultDocuments sortResults(ResultDocuments res) throws SDXException, IOException {

        if (res == null) throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_RESULT_DOCS_NULL, null, null);
        if (sortKeys.size() == 0) {
            // Aucune cle de tri, alors on doit simplement copier la liste de resultats
            return res;
        } else {
            // On doit effectuer un tri
            ResultDocument[] docs = res.getDocuments();
            if(this.getMaxSort() == SortSpecification.SORT_UNLIMITED)
            	Arrays.sort(docs, this);
            else if( this.getMaxSort() > 0 )
            	Arrays.sort(docs,
            				0,
							((this.getMaxSort()< docs.length) ? this.getMaxSort() : docs.length) - 1,
							this);
            res.setSortedResults(docs);
            return res;
        }
    }

    /**
     *	Effectue le tri de resultats de recherche.
     */
/*	public ResultDocument[] sortResults(ResultDocument[] res) throws SDXException, IOException
	{
		if ( res == null ) throw new SDXException("fr", "Aucun resultat a trier");

		if ( sortKeys.size() == 0 || ( sortKeys.size() == 1 && ((SortKey)sortKeys.get(0)).isRankingSort() ) )
		{
			// Aucune cle de tri ou tri de pertinence seulement, alors on doit simplement
			// copier la liste de resultats
		}
		else
		{
			// On doit effectuer un tri
			Arrays.sort(res, this);
		}
		return res;
	}
 */
    /**
     *	Carries out the comparison of two documents with the aim of sorting them .
     *
     *	@param	doc1	The first document.
     *	@param	doc2	The second document.
     */
    public int compare(Object doc1, Object doc2) {
        /*
			Returns a negative integer,
			zero, or a positive integer as the first argument is less than,
			equal to, or greater than the second.
		 */

        // On va faire une boucle sur les cles de tri et en sortir des que l'on trouve quelque
        // chose de different de 0 (l'egalite)
        int ret = 0;
        for (int i = 0; i < sortKeys.size(); i++) {
            ret = compare((ResultDocument) doc1, (ResultDocument) doc2, (SortKey) sortKeys.get(i));
            if (ret != 0) return ret;
        }
        return ret;
    }

    /**
     *	Compare two documents according to a sort key.
     *
     *	@param	doc1	The first document.
     *	@param	doc2	The second document.
     *	@param	key		The sort key.
     */
    private int compare(ResultDocument doc1, ResultDocument doc2, SortKey key) {
        int ret = 0;
        int order = key.getOrder();
        // Le cas du tri de pertinence est particulier
        if (key.isRankingSort()) {
            switch (order) {
                case SORT_ORDER_ASCENDANT:
                default:
                    if (doc1.getScore() < doc2.getScore())
                        return -1;
                    else if (doc1.getScore() > doc2.getScore())
                        return 1;
                    else
                        return 0;
                case SORT_ORDER_DESCENDANT:
                    if (doc1.getScore() > doc2.getScore())
                        return -1;
                    else if (doc1.getScore() < doc2.getScore())
                        return 1;
                    else
                        return 0;
            }
        } else {
            // Pour le reste, on ne va traiter que les comparaisons de chaines pour l'instant
            Field field = ((Field) key.getField());

            Collator col = (Collator) collators.get(field);
            if (col == null) {
                col = field.getCollator();
                collators.put(field, col);
            }
            /*TODO : we'll need to test the below code, i am not sure if it will work*/
            if (field.getFieldType() == Field.DATE) {
            	String doc1_value=doc1.getFieldValue(field.getCode());
            	String doc2_value=doc2.getFieldValue(field.getCode());
            	if(doc1_value.equals(""))
            		if(doc2_value.equals(""))
            			ret = 0;
            		else
            			ret = -1;
            	else if(doc2_value.equals(""))
            		ret = 1;
            	else
            		ret = col.compare(fr.gouv.culture.sdx.utils.Date.formatDate(DateField.stringToDate(doc1_value)),
            				fr.gouv.culture.sdx.utils.Date.formatDate(DateField.stringToDate(doc2_value)));
            } else
                ret = col.compare(doc1.getFieldValue(field.getCode()), doc2.getFieldValue(field.getCode()));
            if (order == SORT_ORDER_DESCENDANT)
                return -ret;
            else
                return ret;
        }
    }

    /**
     *	Retourne une representation XML de cette specification de tri.
     *
     *	@param	factory		Le document permettant de creer des elements.
     */
/*
	public Element toDOM(Document factory)
	{
		String nsURI = Utilities.getSDXNamespaceURI();
		String nsPrefix = Utilities.getSDXNamespacePrefix();

		Element top = factory.createElementNS(nsURI, nsPrefix + ":sort");

		if ( sortKeys != null )
			for (int i=0; i<sortKeys.size(); i++ )
				top.appendChild(((SortKey)sortKeys.get(i)).toDOM(factory));

		return top;
	}
 */

    /**
     *	Returns an XML representation of this sort specification.
     *
     *	@param	hdl		The ContentHandler to feed with events.
     */
    public void toSAX(ContentHandler hdl) throws SAXException {
        //Creation of local variables which are later passed into startElement() and endElement() methods-rbp13/03/02
        String sdxNsUri = Framework.SDXNamespaceURI;
        String sdxNsPrefix = Framework.SDXNamespacePrefix;

        String localName = Node.Name.SORT;
        String qName = sdxNsPrefix + ":" + localName;
        //an empty AttributesImpl object must be created to pass to the startElement method-rbp13/03/02
        AttributesImpl atts = new AttributesImpl();

        //startElement() method is called for "sort" and local variables are passed-rbp13/03/02
        hdl.startElement(sdxNsUri, localName, qName, atts);

        if (sortKeys != null)
            for (int i = 0; i < sortKeys.size(); i++)
                ((SortKey) sortKeys.get(i)).toSAX(hdl);

        //endElement() method is called for "sort" and local variables are passed-rbp13/03/02
        hdl.endElement(sdxNsUri, localName, qName);
    }

    public org.apache.lucene.search.Sort getLuceneSort() {
        return null;
    }

    /**
     * @return the maximum number of documents to sort. Set as SORT_UNLIMITED if unlimited.
     */
    public int getMaxSort(){
		return max_sort;
	}

    /**
     * Set the maximum number of documents to sort.
     * Set as SORT_UNLIMITED if unlimited.
     */
    public void setMaxSort(int max_sort){
    	if(max_sort>=0)
    		this.max_sort = max_sort;
    	else
    		this.max_sort = SortSpecification.SORT_UNLIMITED;
	}

    /**
     * Set the maximum number of documents to sort.
     * Set as SORT_UNLIMITED if unlimited.
     * Convenience method to parse a String to an int
     * and handle the NumberformatException.
     */
    public void setMaxSort(String max_sort){
    	try{
    		setMaxSort(Integer.parseInt(max_sort));
    	}catch(NumberFormatException e){
    		/*TODO : do something useful : log, etc...*/
    	}
	}

    /**
     *	A sort key, basically it is a field and an order.
     */
    private class SortKey {

        /** The field on which to sort*/
        private Field field;

        /** The order ascending or descending. */
        private int order;

        /** Indication of relavance ranking */
        private boolean rankingSort = false;

        /**
         *	Creates a sort key.
         *
         *	@param	field	The sort field.
         *	@param	order	The sort order.
         */
        public SortKey(Field field, int order) throws SDXException {
            if (checkOrder(order)) {
                this.field = field;
                this.order = order;
                if (field.getCode().equals(SortSpecification.SORT_RANKING)) rankingSort = true;
            }
        }

        /**
         *	Checks if the order of specified sorting is valid .
         *
         *	@param	o	The order to verify.
         */
        private boolean checkOrder(int o) throws SDXException {
            switch (o) {
                case SortSpecification.SORT_ORDER_ASCENDANT:
                case SortSpecification.SORT_ORDER_DESCENDANT:
                    return true;
                default:
                    throw new SDXException(getLog(), SDXExceptionCode.ERROR_INVALID_SORT_ORDER, null, null);
//                    return false;
            }
        }

        /**
         *	Indicates whether sorting on relavance.
         */
        public boolean isRankingSort() {
            return rankingSort;
        }

        /**
         *	Returns the order of this sort key.
         *
         * @see #SORT_ORDER_ASCENDANT
         * @see #SORT_ORDER_DESCENDANT
         */
        public int getOrder() {
            return order;
        }

        /**
         *	Returns the field of the sort key.
         */
        public Field getField() {
            return field;
        }

        /**
         *	Retourne une representation XML de cette cle de tri.
         *
         *	@param factory		Le document permettant de creer des elements.
         */
/*
		public Element toDOM(Document factory)
		{
			String nsURI = Utilities.getSDXNamespaceURI();
			String nsPrefix = Utilities.getSDXNamespacePrefix();

			Element top = factory.createElementNS(nsURI, nsPrefix + ":key");

			if ( rankingSort ) top.setAttribute("field", SortSpecification.SORT_RANKING);
			else top.setAttribute("field", field.getCode());
			if ( order == SortSpecification.SORT_ORDER_ASCENDANT ) top.setAttribute("order", "asc");
			else top.setAttribute("order", "desc");

			return top;
		}
 */

        /**
         *	Returns an XML representation of this sort key.
         *
         *	@param	hdl		The ContentHandler to feed with events.
         */
        public void toSAX(ContentHandler hdl) throws SAXException {
            //Creation of local variables which are later passed into startElement() and endElement() methods-rbp13/03/02
            String sdxNsUri = Framework.SDXNamespaceURI;
            String sdxNsPrefix = Framework.SDXNamespacePrefix;

            String localName = Node.Name.FIELD;
            String qName = sdxNsPrefix + ":" + localName;
            AttributesImpl atts = new AttributesImpl();

            if (rankingSort)
                atts.addAttribute("", Node.Name.NAME, Node.Name.NAME, Node.Type.CDATA, SortSpecification.SORT_RANKING);
            else
                atts.addAttribute("", Node.Name.NAME, Node.Name.NAME, Node.Type.CDATA, field.getCode());

            if (order == SortSpecification.SORT_ORDER_ASCENDANT)
                atts.addAttribute("", Node.Name.ORDER, Node.Name.ORDER, Node.Type.CDATA, Node.Value.ASCENDING);
            else
                atts.addAttribute("", Node.Name.ORDER, Node.Name.ORDER, Node.Type.CDATA, Node.Value.DESCENDING);

            //startElement() method is called for "key" and local variables are passed-rbp13/03/02
            hdl.startElement(sdxNsUri, localName, qName, atts);
            //endElement() method is called for "key" and local variables are passed-rbp13/03/02
            hdl.endElement(sdxNsUri, localName, qName);

        }
    }

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

	/* (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() {
	}
}
