package fr.gouv.culture.sdx.search.lucene.query;

import java.util.StringTokenizer;

import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.sax.TransformerHandler;
import javax.xml.transform.stream.StreamResult;

import fr.gouv.culture.sdx.exception.SDXException;

/**
 * Helper class to build a ComplexQuery from a series of search criterias.
 * 
 * 
 */
public class LinearComplexQueryBuilder extends ComplexQuery {
    
    /** The current level of the query (max 3) */
    private int currentLevel = 1;

    /** The last created ComplexQuery for level 2 (AND) */
    private ComplexQuery currentQueryAND;
    
    /** The last created ComplexQuery for level 3 (NOT) */
    private ComplexQuery currentQueryNOT;
    
    /** The last created ComplexQuery for level 1 (OR) => the current object! */
    private ComplexQuery currentQueryOR = this;
    
    /** A counter for the number of queries added */
    private int queryCounter = 0;

    /**
     * Adds a query linked to the previous one by an OR.
     * @param q
     * @throws SDXException
     */
    public void addQuery(Query q) throws SDXException {
        addQuery(q, Query.OPERATOR_OR);
    }

    /**
     * Adds a query.
     * 
     * If it is the first query added, we will always consider an OR operator.
     * 
     * @param q			The query to add
     * @param op		The operator linking this query to the previous one
     * 
     * @throws SDXException
     */
    public void addQuery(Query q, int op) throws SDXException {
        
        if ( q == null ) return;	// TODO: throw an exception here?
        
        // Increment the query counter
        queryCounter++;

        // Adjust the operator if needed
        if ( queryCounter == 1 ) op = Query.OPERATOR_OR;

        /*
         * The exact behaviour depends on the provided operator:
         * 
         * 	OR 	=> always add the query at the top level
         *	AND	=> always add at the second level, and move the previous component if needed
         *	NOT	=> always add at the thrid level, and move the previous component if needed
         */
        switch (op) {
        
        	case Query.OPERATOR_AND:
        	    switch (currentLevel) {
        	    	case 2:
        	    	    // We are following another AND, we just add it
        	    	    currentQueryAND.addComponent(q);
        	    	    break;
        	    	case 3:
        	    	    // We are following a NOT, we must close the previous branch
        	    	    // and link to the last level 2 query
        	    	    currentQueryAND.addComponent(q);
        	    	    break;
        	    	case 1:
        	    	default:		// Should not happen anyway
        	    	    // We are following an OR, so we must get the last
        	    	    // component and put it at level 2, with the current Query
        	    	    initQueryAND();
        	    		createNewLevel(currentQueryOR, currentQueryAND, q);
        	    	    break;
        	    }
        		currentLevel = 2;	// Always for a AND
        	    break;
        	case Query.OPERATOR_NOT:
        	    switch (currentLevel) {
        	    	case 2:
        	    	    // We are following a AND, so we must get the last component
        	    	    // and put it at level 3
        	    	    initQueryNOT();
        	    	    createNewLevel(currentQueryAND, currentQueryNOT, q);
        	    	    break;
        	    	case 3:
        	    	    // We are following another NOT, so we just add the query
        	    	    currentQueryNOT.addComponent(q);
        	    	    break;
        	    	case 1:
        	    	default:		// Should not happen anyway
        	    	    // We are following a OR, so we must create two new levels
        	    	    initQueryAND();
        	    		initQueryNOT();
        	    		Query last = currentQueryOR.removeLastComponent();
        	    		currentQueryOR.addComponent(currentQueryAND);
        	    		currentQueryAND.addComponent(currentQueryNOT);
        	    		currentQueryNOT.addComponent(last);
        	    		currentQueryNOT.addComponent(q);
        	    	    break;
        	    }
        		currentLevel = 3;	// Always for a NOT
        	    break;
        	case Query.OPERATOR_OR:
        	default:	// Abnormal situation, we'll consider it is a OR
        	    // For an OR, we don't care about the current level, we always add at the top
        	    currentQueryOR.addComponent(q);
        		currentLevel = 1;	// Always for a OR
        	    break;
        }
    }
    
    /**
     * Creates a new level and move down the last element.
     * @param up	The top level (will have its last element removed)
     * @param down	The level to add
     * @param newQ	The new query to add
     */
    private void createNewLevel(ComplexQuery up, ComplexQuery down, Query newQ) {
        Query last = up.removeLastComponent();
        up.addComponent(down);
        down.addComponent(last);
        down.addComponent(newQ);
    }
    
    /**
     * Initializes the current AND query.
     * @throws SDXException
     */
    private void initQueryAND() throws SDXException {
        currentQueryAND = initQuery(Query.OPERATOR_AND);
    }

    /**
     * Initializes the current NOT query.
     * @throws SDXException
     */
    private void initQueryNOT() throws SDXException {
        currentQueryNOT = initQuery(Query.OPERATOR_NOT);
    }
    
    /**
     * Initializes a ComplexQuery and returns it.
     * @param op	The operator for the ComplexQuery
     * @return
     * @throws SDXException
     */
    private ComplexQuery initQuery(int op) throws SDXException {
        ComplexQuery cq = new ComplexQuery();
        cq.enableLogging(this.getLog());
        cq.setUp(this.searchLocations, op);
        return cq;
    }

    /**
     * Used for command line testing.
     * @param args	A list of search criterais such as "or|A not|B and|C and|D or|E"
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
        // Accept arguments like:
        //	op|query => or|A not|B and|C and|D or|E
        System.out.print("Traitement de : ");
        SearchLocations sl = new SearchLocations();
        LinearComplexQueryBuilder lcqb = new LinearComplexQueryBuilder();
        lcqb.setUp(sl, Query.OPERATOR_OR);
        for (int i=0; i<args.length; i++) {
            StringTokenizer st = new StringTokenizer(args[i], "|");
            if ( st.countTokens() == 2 ) {
                String sOp = st.nextToken();
                int op = Query.OPERATOR_OR;
                if ( sOp.equalsIgnoreCase("and") ) op = Query.OPERATOR_AND;
                else if ( sOp.equalsIgnoreCase("not") ) op = Query.OPERATOR_NOT;

                String sQuery = st.nextToken();
                System.out.print(args[i] + " ");
                
                SimpleQuery sq = new SimpleQuery();
                sq.setUp(sl, "test", sQuery);
                
                lcqb.addQuery(sq, op);
            }
        }
        System.out.println("");
        SAXTransformerFactory tf = (SAXTransformerFactory)SAXTransformerFactory.newInstance();
        TransformerHandler th = tf.newTransformerHandler();
        Transformer tr = th.getTransformer();
        tr.setOutputProperty(OutputKeys.INDENT, "yes");
        th.setResult(new StreamResult(System.out));
        th.startDocument();
        lcqb.toSAX(th);
        th.endDocument();
    }
}
