package org.webdsl.search;

import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Pattern;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.queryParser.QueryParser.Operator;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanClause.Occur;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.ConstantScoreQuery;
import org.apache.lucene.search.Explanation;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.regex.JavaUtilRegexCapabilities;
import org.apache.lucene.search.regex.RegexQuery;
import org.apache.lucene.search.similar.MoreLikeThis;
import org.apache.lucene.search.spans.SpanMultiTermQueryWrapper;
import org.apache.lucene.search.spans.SpanNearQuery;
import org.apache.lucene.search.spans.SpanOrQuery;
import org.apache.lucene.search.spans.SpanQuery;
import org.apache.lucene.util.Version;
import org.hibernate.search.FullTextQuery;
import org.hibernate.search.FullTextSession;
import org.hibernate.search.SearchFactory;
import org.hibernate.search.query.dsl.QueryBuilder;
import org.hibernate.search.query.dsl.RangeMatchingContext;
import org.hibernate.search.query.dsl.RangeMatchingContext.FromRangeContext;
import org.hibernate.search.query.dsl.RangeTerminationExcludable;
import org.hibernate.search.query.dsl.impl.WebDSLFacetTool;
import org.hibernate.search.query.facet.Facet;
import org.hibernate.search.query.facet.FacetingRequest;
import org.hibernate.search.store.DirectoryProvider;
import org.hibernate.search.util.PassThroughAnalyzer;
import org.webdsl.WebDSLEntity;
import org.webdsl.logging.Logger;
import org.webdsl.search.QueryDef.QueryType;

import utils.HibernateUtil;

import com.browseengine.bobo.api.BoboBrowser;
import com.browseengine.bobo.api.BoboIndexReader;
import com.browseengine.bobo.api.Browsable;
import com.browseengine.bobo.api.BrowseException;
import com.browseengine.bobo.api.BrowseFacet;
import com.browseengine.bobo.api.BrowseRequest;
import com.browseengine.bobo.api.BrowseResult;
import com.browseengine.bobo.api.BrowseSelection;
import com.browseengine.bobo.api.BrowseSelection.ValueOperation;
import com.browseengine.bobo.api.FacetAccessible;
import com.browseengine.bobo.api.FacetSpec;
import com.browseengine.bobo.api.FacetSpec.FacetSortSpec;
import com.browseengine.bobo.facets.FacetHandler;
import com.browseengine.bobo.facets.impl.MultiValueFacetHandler;

public abstract class AbstractEntitySearcher<EntityClass extends WebDSLEntity, F extends AbstractEntitySearcher<?,?>> {

    protected static final Version LUCENEVERSION         = Version.LUCENE_35;
    protected static final int LIMIT                     = 50;
    protected static final int OFFSET                    = 0;
    protected static final Operator OP                   = Operator.OR;
    protected static final boolean ALLOWLUCENESYNTAX     = true;
    protected static final String HIGHLIGHTOPENTAG       = "<span class=\"highlightcontent\">";
    protected static final String HIGHLIGHTCLOSETAG      = "</span>";
    protected static final String HIGHLIGHTSEP           = " ...";
    protected static final Analyzer passThroughAnalyzer  = new PassThroughAnalyzer();

    protected int limit = LIMIT;
    protected int offset = OFFSET;
    protected Operator defaultOperator = OP;

    protected boolean updateFullTextQuery, updateBoboBrowseRequest, updateSorting, updateLuceneQueryWithFacetSelection, updateNamespaceConstraint, updateFieldConstraints, updateLuceneQuery = true, updateParamMap = true, updateHighlightQuery = true;
    protected boolean allowLuceneSyntax = ALLOWLUCENESYNTAX, nonDefaultSearchFields = false, updateBoboBrowseResult = true;

    protected HashMap<String, String> fieldConstraints;
    protected HashMap<String, Facet> rangeFacetMap;
    protected HashMap<String, String> rangeFacetRequests;
    protected HashMap<String, Integer> discreteFacetRequests;
    protected Map<String, String> paramMap;
    private BrowseResult boboBrowseResult;
    private BrowseRequest boboBrowseRequest;

    protected List<WebDSLFacet> filteredFacetsList = new LinkedList<WebDSLFacet>( );

    protected Query luceneQueryNoFacetFilters, highlightQuery, luceneQuery = null;
    protected BooleanQuery rangeFacetQuery;
    protected FullTextQuery fullTextQuery = null;
    protected FullTextSession fullTextSession;

    protected String[] searchFields, defaultSearchFields, mltSearchFields, untokenizedFields;

    protected String sortFields = "", sortDirections = "";
    protected String moreLikeThisParams = "";
    protected String mainQuery = null;

    protected QueryDef rootQD, currentQD, parentQD;

    protected String namespaceConstraint = "";

    protected Analyzer analyzer, highlightAnalyzer;
    protected Class<?> entityClass;
    protected long searchTime = 0;
    protected Sort sortObj;

    public AbstractEntitySearcher( ) { }

    @SuppressWarnings( "unchecked" )
    public F addFieldFilter( String fieldname, String terms ) {
        if ( fieldConstraints == null )
            fieldConstraints = new HashMap<String, String>( );

        fieldConstraints.put( fieldname, terms );
        updateFieldConstraints = updateParamMap = true;
        return ( F ) this;
    }
    
    public F addFieldFilter( String fieldname, Object terms ) {
        return addFieldFilter( fieldname, String.valueOf( terms ) );
    }

    public List<String> getFilteredFields( ){
        List<String> toReturn = new ArrayList<String>( );
        if ( fieldConstraints != null )
            toReturn.addAll( fieldConstraints.keySet( ) );
        return toReturn;
    }

    public String getFieldFilterValue( String fieldname ) {
        if ( fieldConstraints == null )
            return null;
        return fieldConstraints.get( fieldname );
    }
    @SuppressWarnings( "unchecked" )
    public F removeFieldFilter( String fieldname ){
        if( fieldConstraints != null && fieldConstraints.remove( fieldname ) != null )
           updateFieldConstraints = updateParamMap = true;
        return ( F ) this;
    }
    @SuppressWarnings( "unchecked" )
    public F clearFieldFilters( ){
        if( fieldConstraints != null && fieldConstraints.size( ) > 0 ){
            fieldConstraints.clear( );
            updateFieldConstraints = updateParamMap = true;
        }
        return ( F ) this;
    }

    @SuppressWarnings( "unchecked" )
    public F allowLuceneSyntax( boolean b ) {
        if ( this.allowLuceneSyntax != b ) {
            this.allowLuceneSyntax = b;
            updateLuceneQuery = updateParamMap = true;
        }
        return ( F ) this;
    }
    @SuppressWarnings( "unchecked" )
    public F must( ) {
        addSubQuery( Occur.MUST );
        return ( F ) this;
    }
    @SuppressWarnings( "unchecked" )
    public F mustNot( ) {
        addSubQuery( Occur.MUST_NOT );
        return ( F ) this;
    }
    @SuppressWarnings( "unchecked" )
    public F should( ) {
        addSubQuery( Occur.SHOULD );
        return ( F ) this;
    }
    private final void addSubQuery( Occur oc ) {
        currentQD = new QueryDef( oc, parentQD );
    }

    @SuppressWarnings( "unchecked" )
    public F startMustClause( ) {
        addClause( Occur.MUST );
        return ( F ) this;
    }
    @SuppressWarnings( "unchecked" )
    public F startMustNotClause( ) {
        addClause( Occur.MUST_NOT );
        return ( F ) this;
    }
    @SuppressWarnings( "unchecked" )
    public F startShouldClause( ) {
        addClause( Occur.SHOULD );
        return ( F ) this;
    }
    @SuppressWarnings( "unchecked" )
    public F endClause( ) {
        parentQD = ( parentQD == rootQD ) ? parentQD : parentQD.parent;
        return ( F ) this;
    }
    private final void addClause( Occur oc ) {
        currentQD = new QueryDef( oc, parentQD );
        parentQD = currentQD;
    }

    private void addFacetClausesToQuery( ) {
        if ( rangeFacetMap == null )
            rangeFacetMap = new HashMap<String, Facet>( );

        if ( filteredFacetsList.isEmpty( ) ) {
            luceneQuery = luceneQueryNoFacetFilters;
            return;
        }
        
        rangeFacetQuery = new BooleanQuery();

        BooleanQuery shouldFacetQuery = new BooleanQuery( );
        BooleanQuery newQuery = new BooleanQuery( );

        HashMap<String, BooleanQuery> shouldFacetQueryMap = new HashMap<String, BooleanQuery>( 10 );
        String key;
        for( WebDSLFacet facet : filteredFacetsList ) {
            if ( rangeFacetRequests != null && rangeFacetRequests.containsKey( facet.getFieldName( ) ) ) {
                key = facet.getFieldName( ) + "-" + facet.getValue( );
                Facet actualFacet = rangeFacetMap.get( key );
                if ( actualFacet == null ) {
                    // Facets are not yet retrieved during this object's life cycle, probably this is a search query reconstructed from param map.
                    getFacets( facet.getFieldName( ) );
                    actualFacet = rangeFacetMap.get( key );
                }

                if ( actualFacet == null ) {
                    org.webdsl.logging.Logger.warn( "Facet '" + key + "'to narrow not found, should not happen!" );
                    continue;
                }
                if ( facet.occur.equals( Occur.SHOULD ) ) {
                    shouldFacetQuery = shouldFacetQueryMap.get( facet.fieldName );
                    if ( shouldFacetQuery == null ) {
                        shouldFacetQuery = new BooleanQuery( );
                        shouldFacetQueryMap.put( facet.fieldName, shouldFacetQuery );

                        rangeFacetQuery.add( shouldFacetQuery, Occur.MUST);
                    }
                    shouldFacetQuery.add( actualFacet.getFacetQuery( ), facet.occur );
                } else {
                	Query rangeQuery = actualFacet.getFacetQuery( );
                    newQuery.add( ignoreScoreQuery( rangeQuery ), facet.occur );
                    rangeFacetQuery.add( rangeQuery, facet.occur );
                }
            } else {
                if ( facet.occur.equals( Occur.SHOULD ) ) {
                    shouldFacetQuery = shouldFacetQueryMap.get( facet.fieldName );
                    if ( shouldFacetQuery == null ) {
                        shouldFacetQuery = new BooleanQuery( );
                        shouldFacetQueryMap.put( facet.fieldName, shouldFacetQuery );
                    }
                    shouldFacetQuery.add( new TermQuery( new Term( facet.getFieldName( ), facet.getValue( ) ) ), facet.occur );
                } else {
                    newQuery.add( ignoreScoreQuery( new TermQuery( new Term( facet.getFieldName( ), facet.getValue( ) ) ) ), facet.occur );
                }
            }
        }

        for ( BooleanQuery bq : shouldFacetQueryMap.values( ) ) {
            newQuery.add( ignoreScoreQuery( bq ), Occur.MUST );
        }
        newQuery.add( luceneQueryNoFacetFilters, Occur.MUST );

        luceneQuery = newQuery;
    }
    
    private static final Query ignoreScoreQuery(Query q){
    	ConstantScoreQuery csq = new ConstantScoreQuery( q );
    	csq.setBoost(0);
    	return csq;
    }

    private void applyFieldConstraints( ) {
        int cnt = 0;
        for ( String field : fieldConstraints.keySet( ) ) {
            enableFieldConstraintFilter( field, fieldConstraints.get( field ), cnt );
            cnt++;
        }
    }

    private void applyNamespaceConstraint( ) {
        fullTextQuery.enableFullTextFilter( "namespaceConstraintFilter" )
        .setParameter( "namespace", namespaceConstraint );
    }

    @SuppressWarnings( "unchecked" )
    public F boost( String field, Float boost ) {
        currentQD.boost( field, boost );
        updateLuceneQuery = updateParamMap = true;
        return ( F ) this;
    }
    public void closeReader( IndexReader reader ) {
        if ( reader != null )
        getFullTextSession( ).getSearchFactory( ).getReaderProvider( ).closeReader( reader );
    }

    private final QueryParser getQueryParser( QueryDef qd ){
        QueryParser toReturn;
        if ( qd.boosts == null || qd.boosts.isEmpty( ) )
            toReturn = new SpecialMultiFieldQueryParser( LUCENEVERSION, qd.fields, analyzer );
        else
            toReturn = new SpecialMultiFieldQueryParser( LUCENEVERSION, qd.fields, analyzer, qd.boosts );
        toReturn.setDefaultOperator( defaultOperator );

        return toReturn;
    }

    private Query createMatchAllQuery( ){
        return getFullTextSession( ).getSearchFactory( )
                .buildQueryBuilder( ).forEntity( entityClass ).get( ).all( )
                .createQuery( );
    }

    private Query createMultiFieldPhraseQuery ( QueryDef qd ) throws ParseException {
        return getQueryParser( qd).parse( "\"" + SpecialMultiFieldQueryParser.escape( qd.query ) + "\"~" + qd.slop );
    }

    private Query createMultiFieldQuery( QueryDef qd ) throws ParseException {
        if ( allowLuceneSyntax )
            return getQueryParser( qd ).parse( qd.query );
        else {
            return getQueryParser( qd ).parse( SpecialMultiFieldQueryParser.escape( qd.query ) );
         }
    }

    private static String decodeValue( String str ) {
        return str.replaceAll( "\\\\c",",").replaceAll("\\\\\\\\ ", "\\\\" );
    }

    @SuppressWarnings( "unchecked" )
    public F strictMatching( boolean strict) {
    	Operator newOperator = strict ? Operator.AND: Operator.OR;
    	
        if ( !defaultOperator.equals( newOperator ) ) {
            defaultOperator = newOperator;
            updateLuceneQuery = updateParamMap = true;
        }
        return ( F ) this;
    }

    private void enableFieldConstraintFilter( String field, String value, int cnt ) {
        if ( cnt < 5 ) {
        fullTextQuery.enableFullTextFilter( "fieldConstraintFilter" + cnt )
            .setParameter( "field", field )
            .setParameter( "value", value )
            .setParameter( "analyzer", analyzer )
            .setParameter( "allowLuceneSyntax", allowLuceneSyntax );
        } else {
            org.webdsl.logging.Logger.error( "At most 5 field filters can be enabled. Filter on field:'"+ field + "', value:'" + value + "' is ignored!." );
        }
    }

    public final String asString( ){
        return utils.URLFilter.paramMapToPostParamsEncoding( this.toParamMap( ) );
    }

    public static AbstractEntitySearcher<?,?> fromString( String str ){
        return fromParamMap( utils.URLFilter.URLEncodingToParamMap( str ) );
    }

    public final Map<String,String> toParamMap( ) {
        if ( !updateParamMap ) {
            return paramMap;
        }

        paramMap = new HashMap<String, String>( );
        StringBuilder sb = new StringBuilder( 128 );

        paramMap.put( "type", entityClass.getSimpleName( ) );

        //search fields
        if ( nonDefaultSearchFields ) {
        sb.setLength(0);
        for( int cnt = 0 ; cnt < searchFields.length-1; cnt++ )
            sb.append( searchFields[cnt] ).append( "," );
        sb.append( searchFields[searchFields.length-1] );
        paramMap.put( "sf", sb.toString().intern() );
        }
        //search query definitions
        QueryDef usableRootQD = rootQD;
        if ( rootQD.queryType.equals( QueryType.NOQUERY ) && rootQD.children.size( ) == 1 && rootQD.children.get( 0 ).occur.equals( Occur.SHOULD ) )
            usableRootQD = rootQD.children.get( 0 );

        if( usableRootQD.children.isEmpty( ) ) {
            switch ( usableRootQD.queryType ) {
            case TEXT:
                paramMap.put( "q", usableRootQD.query );
                break;
            case PHRASE:
                paramMap.put( "pq", usableRootQD.query );
                paramMap.put( "sl", String.valueOf( usableRootQD.slop ) );
                break;
            case REGEX:
                paramMap.put( "rq", usableRootQD.query );
                break;
            case MATCH_ALL:
            	paramMap.put("maq","");
            default:
                paramMap.put( "lq", getLuceneQueryAsString( ) );
                paramMap.put( "mq", mainQuery);
                break;
            }
        } else {
            paramMap.put( "lq", getLuceneQueryAsString( ) );
            paramMap.put( "mq", mainQuery);
        }

        //field boosts ( are encoded in lucene query if lucene query is used for serialization )
        if ( !paramMap.containsKey( "lq" ) ){
            if ( usableRootQD.boosts !=null && !usableRootQD.boosts.isEmpty( ) ) {
                sb.setLength(0);
                for ( String field : usableRootQD.boosts.keySet( ) ) sb.append( field ).append( "," );
                paramMap.put( "bf", sb.toString( ) );
                sb.setLength(0);
                for ( Float value : usableRootQD.boosts.values( ) ) sb.append( value ).append( "," );
                paramMap.put( "bv", sb.toString( ) );
            }
        }

        //default operator
        if ( !OP.equals( defaultOperator ) )
            paramMap.put( "op", defaultOperator.toString( ) );
        //constraint fields, values
        if ( fieldConstraints!=null && !fieldConstraints.isEmpty( ) ) {
        	sb.setLength(0);
            for ( String field : fieldConstraints.keySet( ) ) sb.append( field ).append( "," );
            paramMap.put( "cf", sb.toString( ) );

            sb.setLength(0);
            for ( String value : fieldConstraints.values( ) ) sb.append( value ).append( "," );
            paramMap.put( "cv", sb.toString( ) );
        }
        //range facet fields, params
        if ( rangeFacetRequests !=null && !rangeFacetRequests.isEmpty( ) ) {
        	sb.setLength(0);
            for( String field : rangeFacetRequests.keySet( ) ) sb.append( field ).append( "," );
            paramMap.put( "rff", sb.toString( ) );

            sb.setLength(0);
            for( String param : rangeFacetRequests.values( ) ) sb.append( encodeValue( param ) ).append( "," );
            paramMap.put( "rfp", sb.toString( ) );
        }
        //discrete facet fields, params
        if ( discreteFacetRequests !=null && !discreteFacetRequests.isEmpty( ) ) {
        	sb.setLength(0);
            for( String field : discreteFacetRequests.keySet( ) ) sb.append( field ).append( "," );
            paramMap.put( "dff", sb.toString( ) );

            sb.setLength(0);
            for( Integer param : discreteFacetRequests.values( ) ) sb.append( param ).append( "," );
            paramMap.put( "dfp", sb.toString( ) );

        }
        //limit
        if ( LIMIT != limit )
            paramMap.put( "lim", String.valueOf( limit ) );
        //offset
        if ( OFFSET != offset )
            paramMap.put( "offset", String.valueOf( offset ) );
        //narrowed facets
        if ( !filteredFacetsList.isEmpty( ) ) {
            sb.setLength(0);
            StringBuilder sb2 = new StringBuilder( 128 );
            StringBuilder sb3 = new StringBuilder( 128 );
            StringBuilder sb4 = new StringBuilder( 128 );
            for( WebDSLFacet f : filteredFacetsList ) {
                sb.append( f.fieldName ).append( "," );
                sb2.append( encodeValue( f.value ) ).append( "," );
                sb3.append( f.occur.name( ) ).append( "," );
                sb4.append( f.count ).append( "," );
            }
            paramMap.put( "ffld", sb.toString( ) );
            paramMap.put( "fvl", sb2.toString( ) );
            paramMap.put( "focc", sb3.toString( ) );
            paramMap.put( "fcnt", sb4.toString( ) );
        }

        //sort fields
        if ( !sortFields.isEmpty( ) )
            paramMap.put( "sortby", sortFields );

        //sort directions
        if ( !sortDirections.isEmpty( ) )
            paramMap.put( "dirs", sortDirections );

        //more like this
        if ( !moreLikeThisParams.isEmpty( ) )
            paramMap.put( "mlt", moreLikeThisParams );

        //allow lucene syntax?
        if ( ALLOWLUCENESYNTAX != allowLuceneSyntax )
            paramMap.put( "allowlcn", String.valueOf( allowLuceneSyntax ) );

        //namespace constraint?
        if ( !namespaceConstraint.isEmpty( ) )
            paramMap.put( "ns", namespaceConstraint );

        updateParamMap = false;
        return paramMap;
    }

    public static AbstractEntitySearcher<?,?> fromParamMap( Map<String,String> paramMap ) {
        AbstractEntitySearcher<?, ?> searcher = null;
        try {
            String key, value;
            searcher = ( AbstractEntitySearcher<?, ?>) Class.forName( "webdsl.generated.search."+ paramMap.get("type")+"Searcher" ).newInstance( );
            for ( Map.Entry<String,String> e : paramMap.entrySet( ) ) {
                key = e.getKey( );
                value = e.getValue( );
                if( "type".equals( key ) ){
                    continue;
                } else if ( "sf".equals( key ) ) {
                    // search fields
                    searcher.fields( new ArrayList<String>( Arrays.asList( value.split( "," ) ) ) );
                } else if ( "q".equals( key ) ) {
                    //ordinary query
                    searcher.query( value );
                } else if ( "lq".equals( key ) ) {
                    //search query, lucene string representation
                    searcher.currentQD.parsedQuery( value );
                } else if ( "rq".equals( key ) ) {
                    //regex query
                    searcher.currentQD.regexQuery( value );
                } else if ( "pq".equals( key ) ) {
                    //phrase query
                    searcher.phraseQuery( value, Integer.parseInt( paramMap.get( "sl" ) ) );
                } else if ( "op".equals( key ) && value.equals("AND" ) ) {
                    //change default operator to AND
                    searcher.strictMatching( true );
                } else if ( "mq".equals( key ) ) {
                    searcher.mainQuery = value;
                } else if ( "maq".equals( key ) ) {
                	searcher.matchAllQuery();
                } else if ( "cf".equals( key ) ) {
                    //constraint fields, values
                    String[] a1 = value.split( "," );
                    String[] a2 = paramMap.get( "cv").split("," );
                    for( int i=0; i < a1.length; i++ )
                        searcher.addFieldFilter( a1[i], a2[i] );
                } else if ( "rff".equals( key ) ) {
                    //range facet fields, params
                    String[] a1 = value.split( "," );
                    String[] a2 = paramMap.get( "rfp").split("," );
                    String field;
                    String param;
                    searcher.rangeFacetRequests = new HashMap<String, String>( );
                    for( int i=0; i < a1.length; i++ ) {
                        field = a1[i];
                        param = decodeValue( a2[i] );
                        searcher.enableFaceting( field, param );
                    }

                } else if ( "dff".equals( key ) ) {
                    //discrete facet fields, params
                    String[] a1 = value.split( "," );
                    String[] a2 = paramMap.get( "dfp").split("," );
                    String field;
                    int param;
                    searcher.discreteFacetRequests = new HashMap<String, Integer>( );
                    for( int i=0; i < a1.length; i++ ) {
                        field = a1[i];
                        param = Integer.parseInt( a2[i] );
                        searcher.enableFaceting( field, param );
                    }

                } else if ( "lim".equals( key ) ) {
                    //limit
                    searcher.setLimit( Integer.parseInt( value ) );
                } else if ( "offset".equals( key ) ) {
                    //offset
                    searcher.setOffset( Integer.parseInt( value ) );
                } else if ( "bf".equals( key ) ) {
                    //boost fields, values
                    String[] a1 = value.split( "," );
                    String[] a2 = paramMap.get( "bv").split("," );
                    for( int i=0; i < a1.length; i++ )
                        searcher.boost( a1[i], Float.parseFloat( a2[i] ) );
                } else if ( "ffld".equals( key ) ) {
                    String[] a1 = value.split( "," );
                    String[] a2 = paramMap.get( "fvl").split("," );
                    String[] a3 = paramMap.get( "focc").split("," );
                    String[] a4 = paramMap.get( "fcnt").split("," );
                    for( int i=0; i < a1.length; i++ ) {
                        searcher.addFacetSelection( new WebDSLFacet( a1[i], decodeValue( a2[i] ), Occur.valueOf( a3[i] ), Integer.parseInt( a4[i] ) ) );
                    }
                } else if ( "sortby".equals( key ) ) {
                    //sort fields, directions
                    String[] a1 = value.split( "," );
                    String[] a2 = paramMap.get( "dirs").split("," );
                    for( int i=0; i < a1.length; i++ )
                        searcher.sort( a1[i], Boolean.parseBoolean( a2[i] ) );
                } else if ( "mlt".equals( key ) ) {
                    //more like this
                    String[] a1 = value.split( "," );
                    searcher.moreLikeThis( a1[0], Integer.parseInt( a1[1] ), Integer.parseInt( a1[2] ), Integer.parseInt( a1[3] ), Integer.parseInt( a1[4] ), Integer.parseInt( a1[5] ), Integer.parseInt( a1[6] ) );
                } else if ( "allowlcn".equals( key ) ) {
                    // allow Lucene syntax?
                    searcher.allowLuceneSyntax( Boolean.parseBoolean( value ) );
                } else if ( "ns".equals( key ) ) {
                    //namespace
                    searcher.setNamespace( value );
                }
            }
        } catch ( Exception e ) {
            org.webdsl.logging.Logger.error( "Error during entity searcher decoding!", e);
        }

        searcher.paramMap = paramMap;
        searcher.updateParamMap = false;
        return searcher;

    }

    private String encodeValue( String str ) {
        return str.replaceAll( "\\\\", "\\\\\\\\ ").replaceAll(",", "\\\\c" );
    }

    @SuppressWarnings( "unchecked" )
    public F enableFaceting( String field, Integer topN ) {
        if ( discreteFacetRequests == null )
            discreteFacetRequests = new HashMap<String, Integer>( );

        if ( topN == 0 )
            discreteFacetRequests.remove( field );
        else if( topN != discreteFacetRequests.put( field, topN ) )
            updateBoboBrowseRequest = true;

        updateParamMap = true;

        return ( F ) this;
    }

    // example ranges: "[ TO 100 ],{100 TO 201}[201 TO ]"   "[-200 TO -100 ][-100 TO 0 ][ 0 TO ]"
    // [ and ] mean include in min/max, { and } mean exclude in min/max
    // Bug on numeric fields: http://opensource.atlassian.com/projects/hibernate/browse/HSEARCH-770
    // Therefore using custom Hibernate Search jar, but still includes entities with null values on the faceting field in counts :(
    @SuppressWarnings( "unchecked" )
    public F enableFaceting( String field, String rangesAsString ) {
        if ( rangeFacetRequests == null )
            rangeFacetRequests = new HashMap<String, String>( );

        rangeFacetRequests.put( field, rangesAsString );
        updateParamMap = true;

        return ( F ) this;
    }

    private void enableFacets( ) {
        for( Map.Entry<String, String> facetEntry : rangeFacetRequests.entrySet( ) ) {
            if ( facetEntry.getValue( ).contains( " TO " ) )
                this.enableRangeFacet( facetEntry.getKey( ), facetEntry.getValue( ) );
            // now using bobo for discrete facets
            //else
                //this.enableDiscreteFacet( facetEntry.getKey( ), Integer.parseInt( facetEntry.getValue( ) ) );

        }
    }

    private void enableRangeFacet( String field, String rangesAsString ) {
        FacetingRequest facetReq  = WebDSLFacetTool.toFacetingRequest( field, rangesAsString, entityClass, fieldType( field ), getFullTextSession( ) );

        fullTextQuery.getFacetManager( ).enableFaceting( facetReq );
    }

    public List<WebDSLFacet> getFacets( String field ) {

        if ( discreteFacetRequests != null && discreteFacetRequests.containsKey( field ) ) {
            return getBoboFacets( field );
        } else if ( rangeFacetRequests != null && rangeFacetRequests.containsKey( field ) ) {
            String facetName = WebDSLFacetTool.facetName( field );
            List<Facet> facets;
            if ( validateQuery( ) ) {
                facets = fullTextQuery.getFacetManager( ).getFacets( facetName );
                //If no facets are returned, facets are probably not enabled yet
                if ( facets.isEmpty( ) ) {
                    enableFacets( );
                    facets = fullTextQuery.getFacetManager( ).getFacets( facetName );
                }
            }
            else
                return new ArrayList<WebDSLFacet>( );

            return toWebDSLFacets( facets );
        } else
            return new ArrayList<WebDSLFacet>( );
    }

    public List<WebDSLFacet> getFacetSelection( ) {
        return filteredFacetsList;
    }

    public List<WebDSLFacet> getFacetSelection( String field ){
        List<WebDSLFacet> toReturn = new ArrayList<WebDSLFacet>( );
        for ( WebDSLFacet webDSLFacet : filteredFacetsList ) {
            if ( webDSLFacet.fieldName.equals( field ) )
                toReturn.add( webDSLFacet );
        }
        return toReturn;
    }

    @SuppressWarnings( "unchecked" )
    public F addFacetSelection( WebDSLFacet facet ) {
        //if already narrowed on this facet, don't add it again
        //A facet already appears in the list if field and value are equal
        if ( !filteredFacetsList.contains( facet ) ){
            filteredFacetsList.add( facet );
        } else {
            return ( F ) this;
        }
        
        if(rangeFacetRequests == null || !rangeFacetRequests.containsKey(facet.fieldName)){
        	addBoboFacetSelection( facet );
        }
        
        facet.isSelected = true;

        updateLuceneQueryWithFacetSelection = updateParamMap = true;
        return ( F ) this;
    }

    @SuppressWarnings( "unchecked" )
    public F addFacetSelection( List<WebDSLFacet> facets ) {
        for ( WebDSLFacet f : facets ) {
            addFacetSelection( f );
        }
        return ( F ) this;
    }

    @SuppressWarnings( "unchecked" )
    public F removeFacetSelection( WebDSLFacet facet ) {
        filteredFacetsList.remove( facet );
        removeBoboFacetSelection( facet );
        updateLuceneQueryWithFacetSelection = updateParamMap = true;
        return ( F ) this;
    }

    @SuppressWarnings( "unchecked" )
    public F clearFacetSelection( ) {
        if(filteredFacetsList.size() < 1)
            return ( F ) this;

        filteredFacetsList.clear( );
        getBoboBrowseRequest( ).clearSelections( );
        updateLuceneQueryWithFacetSelection = updateParamMap = true;
        return ( F ) this;
    }

    @SuppressWarnings( "unchecked" )
    public F clearFacetSelection( String field ) {
        if(filteredFacetsList.size() < 1)
            return ( F ) this;

        Iterator<WebDSLFacet> it = filteredFacetsList.iterator( );
        while ( it.hasNext( ) ){
            if ( it.next( ).fieldName.equals( field ) )
                it.remove( );
        }
        getBoboBrowseRequest( ).removeSelection( field );
        updateLuceneQueryWithFacetSelection = updateParamMap = true;
        return ( F ) this;
    }

    public F defaultFields( ){
        return ( F ) fields( new ArrayList<String>( Arrays.asList( defaultSearchFields ) ) );
    }

    public F field( String field ) {
        return ( F ) fields( new ArrayList<String>( Arrays.asList( field ) ) );
    }

    @SuppressWarnings( "unchecked" )
    public F fields( List<String> fields ) {
        searchFields = fields.toArray( new String[fields.size( )] );
        //no need to clone searchFields, because variable searchFields is only reassigned and never modified.
        currentQD.fields = searchFields;

        //Untokenized fields should be excluded from more like this queries
        for ( int i = 0; i < untokenizedFields.length; i++ )
            fields.remove( untokenizedFields[i] );
        mltSearchFields = fields.toArray( new String[fields.size( )] );

        nonDefaultSearchFields = updateLuceneQuery = updateParamMap = true;
        return ( F ) this;
    }

    public List<String> getFields( ){
        return Arrays.asList( currentQD.fields );
    }

    public abstract Class<?> fieldType( String field );

    public abstract boolean instanceOf( String s );

    public Analyzer getAnalyzer( ){
        return analyzer;
    }

    @SuppressWarnings( "unchecked" )
    public F setNamespace( String namespace ) {
        if ( !namespaceConstraint.equals( namespace ) ) {
            //first remove old namespace filter if set
            if ( !namespaceConstraint.isEmpty( ) )
                fullTextQuery.disableFullTextFilter( "namespaceConstraintFilter" );
        }
        else {
            return ( F ) this;
        }
        namespaceConstraint = namespace;
        updateNamespaceConstraint = updateParamMap = true;
        return ( F ) this;

    }
    public F setNamespace( Object namespace ) {
        return setNamespace( String.valueOf( namespace) );
    }

    @SuppressWarnings( "unchecked" )
    public F removeNamespace( ) {
        fullTextQuery.disableFullTextFilter( "namespaceConstraintFilter" );
        namespaceConstraint = "";
        updateParamMap = true;
        return ( F ) this;

    }

    public String getNamespace( ) {
        return namespaceConstraint;
    }

    @SuppressWarnings( "unchecked" )
    public F setOffset( int offset ) {
        this.offset = offset;
        updateParamMap = true;
        return ( F ) this;
    }

    public int getOffset( ){
        return this.offset;
    }

    protected FullTextSession getFullTextSession() {
        if( fullTextSession == null ) {
            fullTextSession = org.hibernate.search.Search.getFullTextSession( HibernateUtil.getCurrentSession() );
            updateFullTextQuery = true;
        }
        return fullTextSession;
    }

    public IndexReader getReader( ) {
        SearchFactory searchFactory = getFullTextSession( ).getSearchFactory( );
        DirectoryProvider<?>[] providers = searchFactory
                .getDirectoryProviders( entityClass );

        return searchFactory.getReaderProvider( ).openReader( providers );
    }

    public static String escapeQuery( String query ) {
        return QueryParser.escape( query );
    }


    public String highlight( String field, String toHighLight ) {
        return highlight( field, toHighLight, HIGHLIGHTOPENTAG, HIGHLIGHTCLOSETAG, 3, 80, HIGHLIGHTSEP, false, false );
    }
    public String highlight( String field, String toHighLight, String preTag, String postTag ) {
        return highlight( field, toHighLight, preTag, postTag, 3, 80, " ...", false, false  );
    }
    public String highlight( String field, String toHighLight, String preTag, String postTag, int fragments, int fragmentLength, String separator ) {
        return highlight( field, toHighLight, preTag, postTag, fragments, fragmentLength, separator, false, false );
    }

    public String highlightLargeText( String field, String toHighLight ) {
        return highlight( field, toHighLight, HIGHLIGHTOPENTAG, HIGHLIGHTCLOSETAG, 3, 80, HIGHLIGHTSEP, true, false );
    }
    public String highlightLargeText( String field, String toHighLight, String preTag, String postTag ) {
        return highlight( field, toHighLight, preTag, postTag, 3, 80, " ...", true, false );
    }
    public String highlightLargeText( String field, String toHighLight, String preTag, String postTag, int fragments, int fragmentLength, String separator ) {
        return highlight( field, toHighLight, preTag, postTag, fragments, fragmentLength, separator, true, false );
    }

    public String highlightHTML( String field, String toHighLight ) {
        return highlight( field, toHighLight, HIGHLIGHTOPENTAG, HIGHLIGHTCLOSETAG, 3, 80, HIGHLIGHTSEP, false, true );
    }
    public String highlightHTML( String field, String toHighLight, String preTag, String postTag ) {
        return highlight( field, toHighLight, preTag, postTag, 3, 80, " ...", false, true );
    }
    public String highlightHTML( String field, String toHighLight, String preTag, String postTag, int fragments, int fragmentLength, String separator ) {
        return highlight( field, toHighLight, preTag, postTag, fragments, fragmentLength, separator, false, true );
    }

    public String highlightLargeHTML( String field, String toHighLight ) {
        return highlight( field, toHighLight, HIGHLIGHTOPENTAG, HIGHLIGHTCLOSETAG, 3, 80, HIGHLIGHTSEP, true, true );
    }
    public String highlightLargeHTML( String field, String toHighLight, String preTag, String postTag ) {
        return highlight( field, toHighLight, preTag, postTag, 3, 80, " ...", true, true );
    }
    public String highlightLargeHTML( String field, String toHighLight, String preTag, String postTag, int fragments, int fragmentLength, String separator ) {
        return highlight( field, toHighLight, preTag, postTag, fragments, fragmentLength, separator, true, true );
    }

    private String highlight ( String field, String toHighLight, String preTag, String postTag, int fragments, int fragmentLength, String separator, boolean noMaxCharsToAnalyze, boolean stripHTML ) {
        validateQuery( );
        IndexReader ir = getReader( );
        try{
          return ResultHighlighter.highlight( ir, highlightAnalyzer, luceneQueryNoFacetFilters, field, toHighLight, preTag, postTag, fragments, fragmentLength, separator, noMaxCharsToAnalyze, stripHTML );
        } finally{
            closeReader( ir );
        }
    }

    public List<Float> scores( ){
        List<Float> toReturn = new ArrayList<Float>( );
        validateQuery( );
        fullTextQuery.setProjection( FullTextQuery.SCORE );
        for ( Object obj : fullTextQuery.list( ) ) {
            toReturn.add( ( Float ) ( (Object[] ) obj )[0] );
        };
        fullTextQuery.setProjection( FullTextQuery.THIS );
        return toReturn;

    }

    //Expensive, but useful for debugging search behaviour, returns Lucene explanations in html ( use rawoutput to display them )
    public List<String> explanations( ){
        List<String> toReturn = new ArrayList<String>( );
        validateQuery( );
        fullTextQuery.setProjection( FullTextQuery.EXPLANATION );
        for ( Object obj : fullTextQuery.list( ) ) {
            toReturn.add( ( (Explanation ) ( (Object[] ) obj )[0] ).toHtml( ) );
        };
        fullTextQuery.setProjection( FullTextQuery.THIS );
        return toReturn;

    }

    @SuppressWarnings( "unchecked" )
    public List<EntityClass> results( ) {
        try {
            searchTime = System.currentTimeMillis( );
            if ( validateQuery( ) ) {
                List<EntityClass> toReturn = fullTextQuery.list( );
                searchTime = System.currentTimeMillis( ) - searchTime;
                //log( "got result list in " + searchTime +"ms" );
                return toReturn;
            }
        } catch( Exception ex ) {
            org.webdsl.logging.Logger.error( "ERROR WHILE LISTING SEARCH RESULTS", ex );
        }
        //Something went wrong
        searchTime = 0;
        return new ArrayList<EntityClass>( );
    }

    @SuppressWarnings( "unchecked" )
    public F setLimit( int limit ) {
        this.limit = limit;
        updateParamMap = true;
        return ( F ) this;
    }

    public int getLimit( ){
        return this.limit;
    }

    public F moreLikeThis( String likeText ) {
        int minWordLen = 5, maxWordLen = 30, minDocFreq = 1, minTermFreq = 3, maxQueryTerms = 6, maxDocFreqPct = 100;
        return ( F ) moreLikeThis( likeText, minWordLen, maxWordLen, minDocFreq, maxDocFreqPct,
                minTermFreq, maxQueryTerms );
    }

    @SuppressWarnings( "unchecked" )
    public F moreLikeThis(
            String likeText, int minWordLen, int maxWordLen, int minDocFreq, int maxDocFreqPct, int minTermFreq, int maxQueryTerms ) {

        moreLikeThisParams = likeText + "," + minWordLen + "," + maxWordLen + "," + minDocFreq + "," + maxDocFreqPct + "," + minTermFreq + "," + maxQueryTerms;

        IndexReader ir = getReader( );
        MoreLikeThis mlt = new MoreLikeThis( ir );
        mlt.setFieldNames( mltSearchFields );
        mlt.setAnalyzer( analyzer );
        mlt.setMinWordLen( minWordLen );
        mlt.setMaxWordLen( maxWordLen );
        mlt.setMaxDocFreqPct( maxDocFreqPct );
        mlt.setMinDocFreq( minDocFreq );
        mlt.setMinTermFreq( minTermFreq );
        mlt.setMaxQueryTerms( maxQueryTerms );

        try {
            currentQD.parsedQuery( mlt.like( new StringReader( likeText ) ) );
        } catch ( IOException e ) {
            Logger.error(e);
        } finally {
            closeReader( ir );
        }
        updateLuceneQuery = true;
        return ( F ) this;
    }

    public int count( ) {
        try {
            if ( validateQuery( ) ) {
                return fullTextQuery.getResultSize( );
            }
        } catch( Exception ex ) {
            Logger.error(ex);
        }
        //Something went wrong
        return 0;
    }

    public String searchTime( ) {
        return searchTime + " ms";
    }

    public int searchTimeMillis( ) {
        return ( int ) this.searchTime;
    }

    public float searchTimeSeconds( ) {
        return ( float ) ( this.searchTime / 1000 );
    }

    public F rangeQuery( Object min, Object max ) {
        //min and max are included by default
        return rangeQuery( min, max, true, true );
    }

    @SuppressWarnings( "unchecked" )
    public F rangeQuery( Object min, Object max, boolean includeMin, boolean includeMax ) {
        currentQD.range( min, max, includeMin, includeMax );

        updateLuceneQuery = updateFullTextQuery = updateParamMap = true;
        return ( F ) this;
    }

    private void sort( String field, boolean reverse ) {

        if ( sortObj == null ) {
            this.sortFields = field;
            this.sortDirections = String.valueOf( reverse );
            sortObj = new Sort( );
            sortObj.setSort( new SortField[0] );
        }
        else {
            //If sort field already exists, don't do anything
            if ( sortFields.matches( "(^|.*,)" + field + "(,.*|$)" ) ) {
                return;
            }
            this.sortFields += "," + field;
            this.sortDirections += "," + String.valueOf( reverse );
        }
        SortField[] sfs = sortObj.getSort( );
        SortField[] newSfs = Arrays.copyOf( sfs, sfs.length+1 );
        newSfs[sfs.length] = new SortField( field, sortType( field ), reverse );
        sortObj.setSort( newSfs );

        updateSorting = updateParamMap = true;
    }

    @SuppressWarnings( "unchecked" )
    public F sortAsc( String field ) {
        sort( field, false );
        return ( F )this;
    }
    @SuppressWarnings( "unchecked" )
    public F sortDesc( String field ) {
        sort( field, true );
        return ( F )this;
    }

    @SuppressWarnings( "unchecked" )
    public F clearSorting( ) {
        this.sortFields = "";
        this.sortDirections = "";
        sortObj = new Sort( );
        sortObj.setSort( new SortField[0] );
        updateSorting = updateParamMap = true;

        return ( F )this;
    }

    public int sortType( String field ) {
        Class<?> tp = fieldType( field );
        if ( tp.isAssignableFrom( String.class ) )
            return SortField.STRING;
        if ( tp.isAssignableFrom( Integer.class ) )
            return SortField.INT;
        if ( tp.isAssignableFrom( Float.class ) )
            return SortField.FLOAT;
        else
            return SortField.STRING;
    }

    public abstract F reset( );

    public String getQuery( ) {
        return mainQuery;
    }

    @SuppressWarnings( "unchecked" )
    public F query( String query ) {
        if(mainQuery == null)
            mainQuery = query;
        currentQD.query( query );
        updateLuceneQuery = updateParamMap = true;
        return ( F ) this;
    }
    
    public F query( Object query ) {
        return query( String.valueOf( query ) );
    }

    @SuppressWarnings( "unchecked" )
    public F phraseQuery( String query, int slop ) {
        if(mainQuery == null)
            mainQuery = query;
        currentQD.phraseQuery( query, slop );
        updateLuceneQuery = updateParamMap = true;
        return ( F ) this;
    }
    
    public F phraseQuery( Object query, int slop ) {
        return phraseQuery( String.valueOf( query ), slop);
    }

    @SuppressWarnings( "unchecked" )
    public F regexQuery( String regex ) {
        if(mainQuery == null)
            mainQuery = regex;
        currentQD.regexQuery( regex );
        updateLuceneQuery = updateParamMap = true;
        return ( F ) this;
    }

//    @SuppressWarnings( "unchecked" )
//    public F fuzzyQuery( String query, float similarity ) {
//        currentQD.phraseQuery( query, slop );
//        updateLuceneQuery = updateParamMap = true;
//        return ( F ) this;
//    }

    @SuppressWarnings( "unchecked" )
    public F matchAllQuery( ) {
        currentQD.matchAllQuery( );
        updateLuceneQuery = updateParamMap = true;
        return ( F ) this;
    }

    private ArrayList<WebDSLFacet> toWebDSLFacets( List<Facet> facets ) {
        String key;
        ArrayList<WebDSLFacet> webdslFacets = new ArrayList<WebDSLFacet>( );

        if ( rangeFacetMap == null )
            rangeFacetMap = new HashMap<String, Facet>( );

        WebDSLFacet toAdd;
        for ( Facet facet : facets ) {
            key = facet.getFieldName( ) + "-" + facet.getValue( );
            if ( !rangeFacetMap.containsKey( key ) )
                rangeFacetMap.put( key, facet );
            toAdd = new WebDSLFacet( facet );
            toAdd.isSelected = filteredFacetsList.contains( toAdd );
            webdslFacets.add( toAdd );
        }
        return webdslFacets;
    }
    protected static void log( String a ) {
        org.webdsl.logging.Logger.error( a );
    }

    private String getLuceneQueryAsString( ) {
        if ( updateLuceneQuery ) {
            try {
                luceneQuery = createLuceneQuery( rootQD );
                luceneQueryNoFacetFilters = luceneQuery;
            } catch ( ParseException e ) {
                // TODO Auto-generated catch block
                Logger.error(e);
                return luceneQuery.toString( "Error occurred during query parsing" );
            }
            updateLuceneQuery = false;
            updateHighlightQuery = updateBoboBrowseResult = updateFullTextQuery = updateLuceneQueryWithFacetSelection = true;
        }
        return luceneQueryNoFacetFilters.toString( );
    }

    private Query getParsedQuery( QueryDef qd ) throws ParseException {
       return new QueryParser( LUCENEVERSION, "", passThroughAnalyzer ).parse( qd.query );
    }

    public String luceneQuery( ) {
        validateQuery( );
        return luceneQuery.toString( );
    }

    private boolean validateQuery( ) {
//        long tmp = System.currentTimeMillis( );
        if ( updateLuceneQuery ) {
            try {
                luceneQuery = createLuceneQuery( rootQD );
                luceneQueryNoFacetFilters = luceneQuery;
            } catch ( ParseException e ) {
                // TODO Auto-generated catch block
                Logger.error(e);
                return false;
            }
//            log( "LUCENE QUERY: " + luceneQuery.toString( ) );
            updateLuceneQuery = false;
            updateHighlightQuery = updateBoboBrowseRequest = updateFullTextQuery = updateLuceneQueryWithFacetSelection = true;
        }
        if ( updateLuceneQueryWithFacetSelection ) {
            updateLuceneQueryWithFacetSelection = false;
            addFacetClausesToQuery( );
            updateFullTextQuery = true;
        }
        if ( updateFullTextQuery ) {
            fullTextQuery = getFullTextSession( ).createFullTextQuery( luceneQuery, entityClass );
            updateFullTextQuery = false;
            updateNamespaceConstraint = updateFieldConstraints  = updateSorting = true;
        }
        if ( updateFieldConstraints ) {
            updateFieldConstraints = false;
            if ( fieldConstraints != null ) {
                updateBoboBrowseRequest = true;
                applyFieldConstraints( );
            }
        }
        if ( updateNamespaceConstraint && !namespaceConstraint.isEmpty( ) ) {
            updateNamespaceConstraint = false;
            updateBoboBrowseRequest = true;
            applyNamespaceConstraint( );
        }
        if ( updateSorting && !sortFields.isEmpty( ) ) {
            updateSorting = false;
            fullTextQuery.setSort( sortObj );
        }
        fullTextQuery.setFirstResult( offset );
        fullTextQuery.setMaxResults( limit );
//        tmp = System.currentTimeMillis( ) - tmp;
//        log( "ValidateQuery in "+ tmp + "ms" );
        return true;
    }

    //Recursive function that combines all queries from QueryDef objects
    private Query createLuceneQuery( QueryDef qd ) throws ParseException {

//        log("*********createLuceneQuery on query def with children: " + qd.children.size());
//        log("going to parse querydef:" + qd.toString());

        Query toReturn;

        switch ( qd.queryType ){
            case PARSED_LUCENE : toReturn = qd.parsedQuery; break;
            case PARSED_STRING : toReturn = getParsedQuery( qd ); break;
            case RANGE         : toReturn = createRangeQuery( qd ); break;
            case PHRASE        : toReturn = createMultiFieldPhraseQuery( qd ); break;
//            case FUZZY         : toReturn = createMultiFieldcQuery( qd ); break;
            case TEXT          : toReturn = createMultiFieldQuery( qd ); break;
            case MATCH_ALL     : toReturn = createMatchAllQuery( ); break;
            case REGEX         : toReturn = createRegexQuery( qd ); break;
            default            : toReturn = null; break;
        }
//        if(toReturn == null)
//            log("toReturn after switch: NULL");
//        else
//            log("toReturn after switch: " + toReturn.toString());

        if( !qd.children.isEmpty( ) ) {
            BooleanQuery booleanQuery = new BooleanQuery( );
            if( toReturn != null )
                booleanQuery.add( toReturn, qd.occur );
//            int i=0;
            for ( QueryDef child : qd.children ){
//                log("createLuceneQuery on child" + i++ );
                booleanQuery.add( createLuceneQuery( child ), child.occur );
            }

            toReturn = booleanQuery;
        }

        return toReturn;
    }

    //Resource (memory) intensive and experimental feature
    private Query createRegexQuery ( QueryDef qd ) {
        BooleanQuery query = new BooleanQuery();
        List<SpanQuery> spanClausesList = new ArrayList<SpanQuery>();
        String[] queryStrings;
        SpanQuery[] spanClausesArray;
        RegexQuery regexQuery;
        for ( String fld : qd.fields ) {
            spanClausesList.clear();
            queryStrings = qd.query.split(" ");
            spanClausesArray = new SpanQuery[queryStrings.length];
            for ( String subquery : queryStrings ) {
                regexQuery = new RegexQuery( new Term( fld, subquery ) );
                regexQuery.setRegexImplementation( new JavaUtilRegexCapabilities() );
                //if emptyable, like a query '(optional)?' or 'bla|a*', make span optional by wrapping it SpanOrQuery
                if(Pattern.matches(subquery, "")){
                    spanClausesList.add( new SpanOrQuery( new SpanMultiTermQueryWrapper<RegexQuery>( regexQuery ) ) );
                } else {
                    spanClausesList.add( new SpanMultiTermQueryWrapper<RegexQuery>( regexQuery ) );
                }
            }

            spanClausesList.toArray( spanClausesArray );
            query.add( new SpanNearQuery( spanClausesArray, 0, true), Occur.SHOULD );
        }
        return query;
    }

    private Query createRangeQuery( QueryDef qd ) throws ParseException {
        QueryBuilder builder = getFullTextSession( ).getSearchFactory( ).buildQueryBuilder( ).forEntity( entityClass ).get( );
        RangeMatchingContext fieldContext = builder.range( ).onField( qd.fields[0] );
        for (int i = 1; i < qd.fields.length; i++) {
			fieldContext = fieldContext.andField(qd.fields[i]);
		}
        FromRangeContext<Object> fromContext = fieldContext.from(qd.min);
        RangeTerminationExcludable toContext = qd.includeMin? fromContext.to( qd.max ) : fromContext.excludeLimit( ).to( qd.max );
        return qd.includeMax ? toContext.createQuery( ) : toContext.excludeLimit( ).createQuery( );
    }

    protected abstract BoboIndexReader getBoboReader( String field );

    protected static FacetHandler<?> getFacetHandlerForField( String field ) {
        return new MultiValueFacetHandler( field, field );
    }

    private BrowseResult getBoboBrowseResult( String field ){
        if ( updateBoboBrowseResult || updateBoboBrowseRequest) {
            updateBoboBrowseResult( field );
        }
        return boboBrowseResult;
    }

    private List<WebDSLFacet> getBoboFacets( String field ) {
        validateQuery( );

        // obtain facet result
        FacetAccessible facets = getBoboBrowseResult( field ).getFacetMap( ).get( field );

        if ( facets == null ) {
            updateBoboBrowseResult( field );
            facets = getBoboBrowseResult( field ).getFacetMap( ).get( field );
        }

        List<BrowseFacet> facetVals = facets.getFacets( );
        List<WebDSLFacet> toReturn = new ArrayList<WebDSLFacet>( );
        WebDSLFacet facet;
        int index;
        for( BrowseFacet bf : facetVals )
        {
            facet = new WebDSLFacet( bf, field );
            index = filteredFacetsList.indexOf( facet );
            if ( index > -1 ) {
                facet = filteredFacetsList.get( index );
                facet.count = bf.getFacetValueHitCount();
            }
            toReturn.add( facet );
        }
//        log( "get bobo facets in " + ( System.currentTimeMillis() - time ) + "ms" );
        return toReturn;
    }

    private BrowseRequest getBoboBrowseRequest( ){
        if( boboBrowseRequest == null ){
            boboBrowseRequest = new BrowseRequest( );
        }
        if( updateBoboBrowseRequest ){
            updateBoboBrowseRequest();
        }
        return boboBrowseRequest;

    }

    private void addBoboFacetSelection( WebDSLFacet facet ){
        BrowseSelection sel = getBoboBrowseRequest( ).getSelection( facet.fieldName );
        if( sel == null )
            sel = new BrowseSelection( facet.fieldName );

        if( facet.isMust( ) ){
            getBoboBrowseRequest( ).getFacetSpec( facet.fieldName ).setExpandSelection( false );
            sel.setSelectionOperation( ValueOperation.ValueOperationAnd );
            sel.addValue( facet.value );
        } else if( facet.isShould( ) ){
            sel.setSelectionOperation( ValueOperation.ValueOperationOr );
            sel.addValue( facet.value );
        } else {
            sel.setSelectionOperation( ValueOperation.ValueOperationAnd );
            sel.addNotValue( facet.value );
        }
//        boboBrowseRequest.removeSelection( facet.fieldName );
        boboBrowseRequest.addSelection( sel );
        updateBoboBrowseResult = true;
    }

    private void removeBoboFacetSelection( WebDSLFacet facet ){
        getBoboBrowseRequest( ).removeSelection( facet.fieldName );
        for ( WebDSLFacet f : filteredFacetsList ) {
            if ( f.fieldName.equals( facet.fieldName ) )
            addBoboFacetSelection( f );
        }
        updateBoboBrowseResult = true;
    }

    private void updateBoboBrowseRequest(){
        FacetSpec facetSpec;

        if( discreteFacetRequests == null ) {
            discreteFacetRequests = new HashMap<String, Integer>( );
        }

        for ( Entry<String, Integer> rq : discreteFacetRequests.entrySet( ) ) {
            facetSpec = boboBrowseRequest.getFacetSpec( rq.getKey( ) );
            if ( facetSpec == null ) {
                facetSpec = new FacetSpec( );
                facetSpec.setOrderBy( FacetSortSpec.OrderHitsDesc );
                facetSpec.setExpandSelection( true );
                boboBrowseRequest.setFacetSpec( rq.getKey( ),facetSpec );
            }
            // If facetSpec already exists, the maxCount may be changed
            facetSpec.setMaxCount( rq.getValue( ) );
        }


        boboBrowseRequest.setOffset( 0 );
        boboBrowseRequest.setCount( 0 );
        boboBrowseRequest.setQuery( getBoboQuery() );

        updateBoboBrowseRequest = false;
    }

    private void updateBoboBrowseResult( String field ) {
        if( boboBrowseResult != null )
            boboBrowseResult.close();

        BoboIndexReader boboReader = getBoboReader( field );

        // perform browse
        Browsable browser;
        try {
            browser = new BoboBrowser( boboReader );
            boboBrowseResult = browser.browse( getBoboBrowseRequest() );
        } catch ( IOException e ) {
            Logger.error(e);
        } catch ( BrowseException e ) {
            Logger.error(e);
        } finally{
            updateBoboBrowseResult = false;
        }
    }

    private Query getBoboQuery( ) {
        Query boboQuery = luceneQueryNoFacetFilters;
        boolean hasNamespaceConstraint = ( namespaceConstraint != null && !namespaceConstraint.isEmpty( ) );
        boolean hasFieldConstraint = ( fieldConstraints != null && !fieldConstraints.isEmpty( ) );
        boolean hasRangeFacetSelection = (rangeFacetQuery != null && !rangeFacetQuery.clauses().isEmpty() );
        
        if ( hasNamespaceConstraint || hasFieldConstraint || hasRangeFacetSelection ) {
            //Apply field constraints and namespace constraints through query, when enabled
            BooleanQuery bq = new BooleanQuery( );
            bq.add( luceneQueryNoFacetFilters, Occur.MUST );

            if ( hasFieldConstraint ) {
                for ( Entry<String, String> kv : fieldConstraints.entrySet( ) ) {
                    QueryParser qp = new QueryParser( LUCENEVERSION, kv.getKey( ), analyzer );
                    try {
                        if ( allowLuceneSyntax )
                            bq.add( qp.parse( kv.getValue( ) ), Occur.MUST );
                        else
                            bq.add( qp.parse( QueryParser.escape( kv.getValue( ) ) ), Occur.MUST );
                    } catch ( ParseException e ) {
                        Logger.error(e);
                    }
                }
            }

            if ( hasNamespaceConstraint ) {
                bq.add( new TermQuery( new Term( SearchHelper.NAMESPACEFIELD, namespaceConstraint ) ), Occur.MUST );
            }
            if ( hasRangeFacetSelection ) {
                for( BooleanClause clause : rangeFacetQuery){
                	bq.add( clause );
                }
            }
            boboQuery = bq;
        }
        return boboQuery;
    }

}