import React from 'react';
import PropTypes from 'prop-types';

//search api
import SearchAPI from '../../api/SearchAPI';

//data utilities
import CollectionUtil from '../../util/CollectionUtil';
import ComponentUtil from '../../util/ComponentUtil';
import IDUtil from '../../util/IDUtil';
import TimeUtil from '../../util/TimeUtil';
import LocalStorageHandler from '../../util/LocalStorageHandler';

import Query from '../../model/Query';

//ui controls for assembling queries
import SearchTermInput from './SearchTermInput'
import FieldCategorySelector from './FieldCategorySelector';
import DateFieldSelector from './DateFieldSelector';
import DateRangeSelector from './DateRangeSelector';
import KeywordFieldSelector from './KeywordFieldSelector';
import KeywordTermLimitSelector from './KeywordTermLimitSelector';
import AggregationList from './AggregationList';

//visualisations
import Histogram from '../stats/Histogram';
import TermHistogram from '../stats/TermHistogram';
import QuerySingleLineChart from '../stats/QuerySingleLineChart';
import TermQuerySingleLineChart from '../stats/TermQuerySingleLineChart';

//simple visual component
import MessageHelper from '../helpers/MessageHelper';

//third party
import ReactTooltip from 'react-tooltip';

import classNames from 'classnames';

export default class QueryBuilder extends React.Component {

	//should have an initial query in the props, then only updates it in the state
	constructor(props) {
		super(props);
		this.state = {
			displayFacets : this.props.collectionConfig.facets ? true : false,
			showTimeLine: LocalStorageHandler.checkLocalStorageKey('state-show-timeline') === true ?
			LocalStorageHandler.getJSONFromLocalStorage('state-show-timeline') :
			this.props.showTimeLine,
			showKeywordHistogram: LocalStorageHandler.checkLocalStorageKey('state-show-keyword-histogram') === true ?
			LocalStorageHandler.getJSONFromLocalStorage('state-show-keyword-histogram') :
			this.props.showKeywordHistogram,
			dateGraphType : null,
			termGraphType : null,
			isSearching : false,
			selectedKeywordField: this.props.selectedKeywordField ? this.props.selectedKeywordField : null, //this is selected by the user
			termLimit: 20, //this can be changed by the user
			query : this.props.query, //this is only set by the owner after choosing a collection or loading the page
			aggregations : {},
			searchTerm: this.props.query ? this.props.query.term : null,
			isQueryAccessDenied : this.props.isQueryAccessDenied || false,
			facetsLocked : false,

		};
		this.CLASS_PREFIX = 'qb';
	}

	/*---------------------------------- COMPONENT INIT --------------------------------------*/

	//TODO also provide an option to directly pass a config, this is pretty annoying with respect to reusability
	componentDidMount() {
		//do an initial search in case there are search params in the URL
		//NB: make sure the search form is rendered (and not e.g. the query access denied message)
		if(this.props.query && !this.props.isQueryAccessDenied) {
			this.state.searchTerm = this.props.query.term || null;
			this.doSearch(this.props.query);
		}
	}

    switchDateGraphType(typeOfGraph) {
        this.setState({
                dateGraphType : typeOfGraph
            }
        )
    }

    switchTermGraphType(typeOfGraph) {
        this.setState({
                termGraphType : typeOfGraph
            }
        )
    }
	/*---------------------------------- SEARCH --------------------------------------*/

	doSearch(query) {
		if(this.props.onStartSearch && typeof(this.props.onStartSearch) === 'function') {
     	   this.props.onStartSearch();
    	}
    	if(query.storeAfterExecution) { //whenever the query is stored (via the SearchAPI) make sure to delete the stored-priority-query
    		LocalStorageHandler.removeJSONByKeyInLocalStorage('stored-priority-query');
    	}
		this.setState(
			{isSearching : true},
			() => {
				SearchAPI.search(
					query,
					this.props.collectionConfig,
					this.onOutput.bind(this),
					query.storeAfterExecution
				)
			}
		)
	}

	newSearch = term => {
		this.state.searchTerm = term //FIXME this is a really nasty use of setting a state variable...
		const q = this.state.query;

		//reset certain query properties
		if(this.state.totalHits <= 0) {
			q.dateRange = null; // only reset when there are no results
		}

		if (!this.state.facetsLocked)
		{
		    q.selectedFacets = {}; // reset the facets if not locked
		}

		q.offset = 0;
		q.term = this.getCurrentSearchTerm(); // make sure the term is always a string, otherwise 0 results by default
		q.includeMediaObjects = this.props.collectionConfig.includeMediaObjects(q.term); // influenced by term, so check it always
        this.doSearch(q);
	};

	blockSearch = e => e.preventDefault();

	clearSearch = () => this.onOutput(null);

	//this resets the paging
	toggleSearchLayer(e) {
		const q = this.state.query;
		const searchLayers = this.state.query.searchLayers;
		searchLayers[e.target.id] = !searchLayers[e.target.id];

		//reset certain query properties
		q.searchLayers = searchLayers;
		q.offset = 0;
		q.term = this.getCurrentSearchTerm();

		this.doSearch(q);
	}

	//this sets the state of the 'lock facets' option
	onFacetLockChange(e) {
	    this.state.facetsLocked = e.target.checked
	}


	/*---------------------------------- FUNCTION THAT RECEIVES DATA FROM CHILD COMPONENTS --------------------------------------*/

	onComponentOutput(componentClass, data) {
		if(componentClass === 'AggregationList') {
			const q = this.state.query;
			//reset the following query params
			q.desiredFacets = data.desiredFacets;
			q.selectedFacets = data.selectedFacets;
			q.offset = 0;
			q.term = this.getCurrentSearchTerm();
			this.doSearch(q);
		} else if(componentClass === 'DateRangeSelector') {

			const q = this.state.query;

			//reset the following params
			q.dateRange = Object.assign(data, {field: q.dateRange ? q.dateRange.field : null });
			q.offset = 0;
			q.term = this.getCurrentSearchTerm();

			this.doSearch(q)

		} else if(componentClass === 'KeywordFieldSelector') {

			const df = this.state.query.desiredFacets;
			const sf = this.state.query.selectedFacets;

			this.state.termLimit = 20  // reset the term limit when changing the keyword

			//check if the keyword has changed or been removed
			if ((this.state.selectedKeywordField && data == null) || (data && data.field != this.state.selectedKeywordField)){

			//now check if the old selection is one of the facets defined for the
			//collection. If not, delete the old selection from the desired and selected facets
			let isCoreFacet = false;
            this.props.collectionConfig.getFacets().forEach(element => {if(element.field === this.state.selectedKeywordField ) {
                    isCoreFacet = true;
                } });

			if (!isCoreFacet){
                let index = -1;

                df.forEach(element => {if(element.field === this.state.selectedKeywordField) {
                        index = df.indexOf(element);
                    } });
                if(index !== -1) {
                    df.splice(index,1);
                }

                if(this.state.selectedKeywordField in sf){
                    delete sf[this.state.selectedKeywordField];
                }
                }
			//add the new selection
			if(data) {
				let selectionAlreadyPresent = false

                df.forEach(element => {if(element.field === data.field) {
                        selectionAlreadyPresent = true;
                    } });

                if(!selectionAlreadyPresent){
                    //add the desired term aggregation (of the type string)
                    df.push({
                        field: data.field,
                        title : this.props.collectionConfig.toPrettyFieldName(data.field),
                        id : data.field,
                        type : 'string'
                    });
				}
                }

                const q = this.state.query;

                //reset the following params

                q.desiredFacets = df;
                q.offset = 0;
                q.term = this.getCurrentSearchTerm();

                //save the keyword in the state
                    this.state.selectedKeywordField = (data && data.field) ? data.field : null

                this.doSearch(q);
                }
                //save the term limit in the state
                if(data && data.termLimit){
                    this.setState({termLimit: data.termLimit});
                }


		}else if(componentClass === 'KeywordTermLimitSelector') {
                //save the term limit in the state
                if(data && data.termLimit){
                    this.setState({termLimit: data.termLimit});
                }

		}else if(componentClass === 'DateFieldSelector') {
			//first delete the old selection from the desired facets
			const df = this.state.query.desiredFacets;
			let index = -1;
			df.forEach(element => {if(element.type === 'date_histogram') {
					index = df.indexOf(element);
				}});

			if(index !== -1) {
				df.splice(index,1);
			}

			//add the new selection
			if(data !== null) {
				//add the desired date aggregation (of the type date_histogram)
				df.push({
					field: data.field,
					title : this.props.collectionConfig.toPrettyFieldName(data.field),
					id : data.field,
					type : 'date_histogram'
				});
			}

			const q = this.state.query;

			//reset the following params

			q.dateRange = data;
			q.desiredFacets = df;
			q.offset = 0;
			q.term = this.getCurrentSearchTerm();

			this.doSearch(q);
		} else if(componentClass === 'FieldCategorySelector') {
			const q = this.state.query;
			q.fieldCategory = data;
			q.offset = 0;
			q.term = this.getCurrentSearchTerm();

			this.doSearch(q)
		} else if(componentClass === 'SearchTermInput') {
		    //called when entities have been added via autocomplete or deleted
		    const entityAdded = (!this.state.query.entities || this.state.query.entities.length < data.length)
			const q = this.state.query;
            q.entities = data;
			q.offset = 0;
			if(entityAdded){
			    q.term = "";  //clear the search term as the search field has been wiped by the text entered to find the entity
			    this.state.searchTerm = "";
			}
			//if an entity has been deleted then we keep the search term

			this.doSearch(q)
		}
	}

	/*---------------------------------- FUNCTIONS THAT COMMUNICATE TO THE PARENT --------------------------------------*/

	//this function is piped back to the owner via onOutput()
	gotoPage = pageNumber => {
		const q = this.state.query;
		q.offset = (pageNumber-1) * this.props.pageSize;
		q.term = this.getCurrentSearchTerm();

		this.doSearch(q);
	}

	//this function is piped back to the owner via onOutput()
	sortResults(sortParams) {
		const q = this.state.query;
		q.sort = sortParams;
		q.offset = 0;
		q.term = this.getCurrentSearchTerm();

		this.doSearch(q);
	}

	resetDateRange() {
		const q = this.state.query;
		q.dateRange = null;
		q.offset = 0;
		q.term = this.getCurrentSearchTerm();

		this.doSearch(q);
	}

	toggleTimeLine = () => {
		LocalStorageHandler.storeJSONInLocalStorage('state-show-timeline', !this.state.showTimeLine);
    	this.setState({showTimeLine:!this.state.showTimeLine});
    }

	toggleKeywordHistogram = () => {
		LocalStorageHandler.storeJSONInLocalStorage('state-show-keyword-histogram', !this.state.showKeywordHistogram);
    	this.setState({showKeywordHistogram:!this.state.showKeywordHistogram});
    }
	/* ----------------------------------------- DATA FUNCTIONS FOR THE RENDER ------------------------------ */

	//FIXME not a pure function
    calcTotalDatesOutsideOfRange = (currentDateAggregation, dateRange) => {
    	if(!currentDateAggregation || !dateRange) return 0;

		const startMillis = dateRange.start;
		const endMillis = dateRange.end;

		const datesOutsideOfRange = currentDateAggregation.filter(x => {

			if(startMillis != null && x.date_millis < startMillis) {
				return true;
			}
			return endMillis !== null && x.date_millis > endMillis;

		});

		if(datesOutsideOfRange.length > 0) {
            return datesOutsideOfRange.map((x => x.doc_count)).reduce(function(accumulator, currentValue) {
    			return accumulator + currentValue;
			});
        }

        return 0;
    }

    calcDateCounts = dateAggregation => {
    	if(!dateAggregation || dateAggregation.length === 0) {
			return -1;
		}
		return dateAggregation.map(
			(x => x.doc_count)).reduce(function(accumulator, currentValue) {
				return accumulator + currentValue;
			}
		);
    };

    getCurrentDateAggregation = (aggregations, dateRange) => {
    	if(dateRange && dateRange.field !== 'null_option' && aggregations[dateRange.field] !== undefined) {
    		return aggregations[dateRange.field].filter(value => value && value.key != 'Empty field');
    	}
    	return null;
    };

    getCurrentKeywordAggregation = (aggregations, selectedKeywordField) => {
    	if(selectedKeywordField!== 'null_option' && aggregations[selectedKeywordField] !== undefined) {
    		return aggregations[selectedKeywordField].filter(value => value && value.key != 'Empty field');
    	}
    	return null;
    };
	getCurrentSearchTerm = () => {
    	return this.state.searchTerm || "";
    };

     /* ----------------------------------------- COMPONENT OUTPUT FUNCTION ------------------------------ */

    //communicates all that is required for a parent component to draw hits & statistics
    onOutput(resultsObj) { // instance of SearchResults
        //this propagates the query output back to the recipe, who will delegate it further to any configured visualisation
        if (this.props.onOutput) {
            this.props.onOutput(this.constructor.name, resultsObj);
        }
        if (resultsObj && !resultsObj.error) {
            this.setState(
            	{
	            	//so involved components know that a new search was done
	            	searchId: resultsObj.searchId,
	            	//refresh params of the query object
	            	query : resultsObj.query,

	                //actual OUTPUT of the query
	                aggregations: ComponentUtil.filterWeirdDates(resultsObj.aggregations, resultsObj.query.dateRange, this.props.collectionConfig),
	                totalHits: resultsObj.totalHits, //shown in the stats
	                totalUniqueHits: resultsObj.totalUniqueHits, //shown in the stats
	                isQueryAccessDenied : false
            	},
            	() => {
            		this.setState({isSearching: false});
            	}
            );
        } else {
        	console.debug('ERROR?', resultsObj)
        	//Note: searchLayers & desiredFacets & selectedSortParams stay the same
        	const q = this.state.query;
        	//q.dateRange = null;
        	q.selectedFacets = {};
        	//q.fieldCategory = null;

            this.setState(
            	{
            		searchId: null,
	            	query : q,

	                //query OUTPUT is all empty
					aggregations: null,
	                totalHits: 0,
	                totalUniqueHits: 0,
	                isQueryAccessDenied: resultsObj && resultsObj.error === 'Access denied'
            	},
            	() => {
            		this.setState({isSearching: false});
            	}
            );
        }
    }

    /* ----------------------------------------- RENDER FUNCTIONS ------------------------------ */

    renderNoResultsMessage = (aggregations, query, onClearSearch) => {
    	if(aggregations && query) {
			return (
				<div className={classNames("alert alert-danger")}>
					{MessageHelper.renderNoSearchResultsMessage(query, onClearSearch)}
				</div>
			)
    	}
    	return null;
    };

    renderPagingOutOfBoundsMessage = () => (
        <div className="alert alert-danger">
            {MessageHelper.renderPagingOutOfBoundsMessage(() => this.gotoPage(1))}
        </div>
	);

	//if the search API returned an access denied error, return a helpful message for the user
    renderQueryAccessDeniedMessage = onClearSearch => ( //TODO it should clear the entire search instead of going to page 1
        <div className="alert alert-danger">
            {MessageHelper.renderQueryAccessDeniedMessage(onClearSearch)}
        </div>
	);


    renderQueryResultHits = totalHits => {
    	return (
    		<span className={IDUtil.cssClassName('total-count', this.CLASS_PREFIX)} title="Total number of results based on keyword and selected filters">
				Results
				<span className={IDUtil.cssClassName('count', this.CLASS_PREFIX)}>
					{ComponentUtil.formatNumber(totalHits)}
				</span>
			</span>
		);
    };

    renderFieldCategorySelector = (query, collectionConfig, onOutput) => {
    	return (
			<FieldCategorySelector
				queryId={query.id}
				fieldCategory={query.fieldCategory}
				collectionConfig={collectionConfig}
				onOutput={onOutput}
			/>
		);
    };

    renderDateFieldSelector = (searchId, query, aggregations, collectionConfig, onOutput) => {
    	if(collectionConfig.getDateFields() == null) return null;

    	return (
            <DateFieldSelector
            	searchId={searchId} //for determining when the component should rerender
            	queryId={query.id} //used for the guid (is it still needed?)
                dateRange={query.dateRange} //for activating the selected date field
                aggregations={aggregations} //to fetch the date aggregations
                collectionConfig={collectionConfig} //for determining available date fields & aggregations
                onOutput={onOutput} //for communicating output to the  parent component
            />
        );
    };

    renderDateRangeSelector = (searchId, query, aggregations, collectionConfig, onOutput) => {
    	if(collectionConfig.getDateFields() == null) return null;

    	return (
            <DateRangeSelector
            	searchId={searchId} //for determining when the component should rerender
            	queryId={query.id} //used for the guid (is it still needed?)
                dateRange={query.dateRange} //for activating the selected date field
                aggregations={aggregations} //to fetch the date aggregations
                collectionConfig={collectionConfig} //for determining available date fields & aggregations
                onOutput={onOutput} //for communicating output to the  parent component
            />
        );
    };

    renderKeywordFieldSelector = (searchId, query, selectedKeywordDataField, collectionConfig, onOutput) => {

    	if(collectionConfig.getKeywordFields() == null) return null;

    	return (
            <KeywordFieldSelector
            	searchId={searchId} //for determining when the component should rerender
                collectionConfig={collectionConfig} //for determining available keyword fields & aggregations
                allowHeavyFacets={(query !== undefined && query.term !== undefined && query.term.length > 2)} //for determining whether it is smart to allow all facets to be selected
                selectedField = {selectedKeywordDataField} //selected field
                onOutput={onOutput} //for communicating output to the  parent component
            />
        );
    };

    renderKeywordTermLimit = (searchId, termLimit, dataLimit, onOutput) => {

      	return (
            <KeywordTermLimitSelector
            	searchId={searchId} //for determining when the component should rerender
                termLimit = {termLimit}  //selected term limit
                dataLimit = {dataLimit}  //limit to number of terms in data
                onOutput={onOutput} //for communicating output to the  parent component
            />
        );
    };

    renderAggregationList = (searchId, query, aggregations, collectionConfig, onComponentOutput) => (
        <div className="aggregation-list">
            <AggregationList
                searchId={searchId} //for determining when the component should rerender
                allowHeavyFacets={(query !== undefined && query.term !== undefined && query.term.length > 2)} //for determining whether it is smart to allow all facets to be selected
                queryId={query.id} //TODO implement in the list component
                aggregations={aggregations} //part of the search results
                desiredFacets={query.desiredFacets}
                selectedFacets={query.selectedFacets}
                collectionConfig={collectionConfig} //for the aggregation creator only
                onOutput={onComponentOutput} //for communicating output to the  parent component
                key={searchId}
            />
        </div>
    );

    renderSearchTermInput = (collectionConfig, fieldCategories, entities, term, newSearchFunc, onComponentOutput) => (
       <SearchTermInput
	       collectionConfig={collectionConfig}
	       fieldCategories = {fieldCategories ? fieldCategories : []}
	       entities = {entities}
	       term = {term}
	       newSearch = {newSearchFunc} //for changed search term
	       onSuggestionOutput = {onComponentOutput} //for selection of suggestion
       />
    );

    //FIXME for motu & arttube this is needed. Now it is deactivated though!
    //renders the checkboxes for selecting layers
    renderSearchLayerOptions = () => {
		if(this.state.query.searchLayers && 1===2) { //DEACTIVATED
			const layers = Object.keys(this.state.query.searchLayers).map((layer, index) => {
				return (
					<label key={'layer__' + index} className="checkbox-inline">
						<input id={layer} type="checkbox" checked={this.state.query.searchLayers[layer] === true}
							onChange={this.toggleSearchLayer.bind(this)}/>
							{CollectionUtil.getSearchLayerName(this.props.collectionConfig.getSearchIndex(), layer)}
					</label>
				)
			});
			if(layers) {
				return (
			 		<div className={IDUtil.cssClassName('search-layers', this.CLASS_PREFIX)}>
			 			{layers}
			 		</div>
			 	)
			}
		}
		return null;
    };

    renderTimeLine = (dateAggregation, graphType, searchId, query, collectionConfig) => {
    	if(!dateAggregation) {
    		return null;
    	} else if (dateAggregation.length === 0) {
		    return MessageHelper.renderNoDocumentsWithDateFieldMessage();
    	}
        if (graphType === 'lineChart') {
            return (
                <div className={IDUtil.cssClassName('graph', this.CLASS_PREFIX)}>
                    <button
                    	onClick={this.switchDateGraphType.bind(this, 'histogram')}
                    	type="button"
                    	className="btn btn-primary btn-xs">
                    	Histogram
                    </button>
                    <QuerySingleLineChart
                    	query={query}
                        data={dateAggregation}
                        onClick={this.filterByDateRange}
						collectionConfig={collectionConfig}
					/>
                </div>
            );
        } else {
            return (
                <div className={IDUtil.cssClassName('graph', this.CLASS_PREFIX)}>
                    <button
                    	onClick={this.switchDateGraphType.bind(this, 'lineChart')}
                    	type="button"
                    	className="btn btn-primary btn-xs">
                    	Line chart
                    </button>
                    <Histogram
                        query={query}
                        data={dateAggregation}
                        collectionConfig={collectionConfig}
                        onClick={this.filterByDateRange}
                        title={collectionConfig.toPrettyFieldName(query.dateRange.field)}
                    />
                </div>
            );
        }
    };

    renderTermHistogram = (selectedKeywordField, termLimit, termAggregation, graphType, searchId, query, collectionConfig) => {
    	if(!termAggregation) {
    		return null;
    	} else if (termAggregation.length === 0) {
		    return MessageHelper.renderNoDocumentsWithTermFieldMessage();
    	}

        if (graphType === 'lineChart') {
            return (
                <div className={IDUtil.cssClassName('graph', this.CLASS_PREFIX)}>
                    <button
                    	onClick={this.switchTermGraphType.bind(this, 'histogram')}
                    	type="button"
                    	className="btn btn-primary btn-xs">
                    	Histogram
                    </button>
                    <TermQuerySingleLineChart
                        title={collectionConfig.toPrettyFieldName(selectedKeywordField)}
                    	query={query}
                        data={termAggregation}
                        selectedKeywordField={selectedKeywordField}
                        termLimit={termLimit}
						collectionConfig={collectionConfig}
                        onClick={this.filterByKeywordField}
					/>
                </div>
            );
       } else {
            return (
                <div className={IDUtil.cssClassName('graph', this.CLASS_PREFIX)}>
                    <button
                        onClick={this.switchTermGraphType.bind(this, 'lineChart')}
                        type="button"
                        className="btn btn-primary btn-xs">
                        Line chart
                    </button>
                    <TermHistogram
                        title={collectionConfig.toPrettyFieldName(selectedKeywordField)}
                        query={query}
                        data={termAggregation}
                        selectedKeywordField={selectedKeywordField}
                        termLimit={termLimit}
                        collectionConfig={collectionConfig}
                        onClick={this.filterByKeywordField}
                    />
                </div>
            );
        }
    };

    filterByDateRange = e => {
        const q = Query.construct(this.state.query);
        q.dateRange.start = e+"-01-01"
        q.dateRange.end = e+"-12-31"
        this.doSearch(q)
    };

    filterByKeywordField = e => {
        const q = Query.construct(this.state.query);

        //overwrite any existing filter values for this facet - check this behaviour with users
        q.selectedFacets[this.state.selectedKeywordField] = [e]

        this.doSearch(q)
    };

    renderDateRangeCrumb = (query, onResetDateRange) => {
        let info = 'Everything ';
        if (query.dateRange.start && query.dateRange.end) {
            info += 'since ' +
                query.dateRange.start +
                ' until ' +
                query.dateRange.end;
        } else if (query.dateRange.start) {
            info += 'since ' + query.dateRange.start;
        } else {
            info += 'until ' + query.dateRange.end;
        }

        info += ' (using: ' + query.dateRange.field + ')';

    	return (
    		<div className={IDUtil.cssClassName('breadcrumbs', this.CLASS_PREFIX)}>
				<div key="date_crumb" className={IDUtil.cssClassName('crumb', this.CLASS_PREFIX)}
					title="Clear current date range">
					<em>Selected date range:&nbsp;</em>
					{info}
					&nbsp;
					<i className="fas fa-times" onClick={onResetDateRange}/>
				</div>
			</div>
    	);
    };

    renderDateTotalStats = (dateCounts, query) => {
    	return (
    		<div>
        		<span title="Total number of dates found based on selected date field" className={IDUtil.cssClassName('date-count', this.CLASS_PREFIX)}>
        			{ComponentUtil.formatNumber(dateCounts)}
        		</span>
    			&nbsp;
    			<span data-tip data-for={'__qb__tt' + query.id}>
					<i className="fas fa-info-circle"/>
				</span>
				<ReactTooltip id={'__qb__tt' + query.id} getContent={
					() => MessageHelper.renderDateTotalStatsTooltip(IDUtil.cssClassName('tooltip'))}
				/>
			</div>
		);
    };

    renderDateRangeStats = (dateCounts, outOfRangeCount) => {
    	return (
    		<div className={IDUtil.cssClassName('date-range-stats', this.CLASS_PREFIX)}>
    			<span>
    				Inside range:
    				<span className={IDUtil.cssClassName('date-count', this.CLASS_PREFIX)}
    				      title="Number of dates found inside selected date range">
    					{ComponentUtil.formatNumber(dateCounts - outOfRangeCount)}
    				</span>
    			</span>
    			<span>
    				Outside range:
    				<span className={IDUtil.cssClassName('date-count', this.CLASS_PREFIX)}
    				      title="Number of dates found outside selected date range">
    					{ComponentUtil.formatNumber(outOfRangeCount)}
					</span>
				</span>
    		</div>
    	);
    };

    renderKeywordControls = (currentKeywordAggregation, query, selectedKeywordField, termLimit, dataLimit, showKeywordHistogram, collectionConfig, searchId, totalHits, graphType, onOutput) => {
    	if(searchId == null || totalHits === 0) return null; //results are mandatory
		const keywordFieldSelector = this.renderKeywordFieldSelector(searchId, query, selectedKeywordField, collectionConfig, onOutput);
		const keywordTermLimit = this.renderKeywordTermLimit(searchId, termLimit, dataLimit, onOutput);
		const graph = (showKeywordHistogram && selectedKeywordField) ? this.renderTermHistogram(selectedKeywordField, termLimit, currentKeywordAggregation, graphType, searchId, query, collectionConfig) : null;

    	return (
        	<div className={IDUtil.cssClassName('result-dates', this.CLASS_PREFIX)}>
        		<div className={IDUtil.cssClassName('result-dates-header', this.CLASS_PREFIX)}>
        		    <div className={IDUtil.cssClassName('keyword-field', this.CLASS_PREFIX)}>
                		 <span data-tip data-for={'__qb__tt-keyword-select'}>
                             <i className="fa fa-info-circle" aria-hidden="true"/>
				        </span>
                        <ReactTooltip id={'__qb__tt-keyword-select'} getContent={
                        	() => MessageHelper.renderKeywordSelectorTooltip(IDUtil.cssClassName('tooltip'))}
                        />
                		{keywordFieldSelector}
                		{keywordTermLimit}
                	</div>

                	{/* Show chart button */}
            		{selectedKeywordField &&
            			<button className="btn" onClick={this.toggleKeywordHistogram}>
            				{showKeywordHistogram ? "Hide chart" : "Show chart"}
        				</button>
            		}
                </div>

	            {
	            	(showKeywordHistogram && selectedKeywordField && <div className={IDUtil.cssClassName('result-dates-content', this.CLASS_PREFIX)}>
	            		<div className={IDUtil.cssClassName('date-graph', this.CLASS_PREFIX)}>
	                		{graph}
	                	</div>
	            	</div>)
	        	}
			</div>
		)
    }

    renderDateControls = (aggregations, query, collectionConfig, searchId, totalHits, showTimeLine, graphType, onOutput) => {
    	if(searchId == null || (!query.dateRange && totalHits === 0)) return null; //date range & results are mandatory

    	const currentDateAggregation = this.getCurrentDateAggregation(aggregations, query.dateRange);

		const dateFieldSelector = this.renderDateFieldSelector(searchId, query, aggregations, collectionConfig, onOutput);
		const dateRangeSelector = this.renderDateRangeSelector(searchId, query,	aggregations, collectionConfig,	onOutput);


		let dateRangeCrumb = null;
		let outOfRangeCount = 0;
		const dateCounts = this.calcDateCounts(currentDateAggregation);

		if(query.dateRange && (query.dateRange.start || query.dateRange.end)) {
			outOfRangeCount = this.calcTotalDatesOutsideOfRange(currentDateAggregation, query.dateRange);
        	dateRangeCrumb = this.renderDateRangeCrumb(query, this.resetDateRange.bind(this)); //TODO pass param
        }

        //render date stats
		const dateTotalStats = dateCounts !== -1 ? this.renderDateTotalStats(dateCounts, query) : null;
		const dateRangeStats = dateCounts !== -1 ? this.renderDateRangeStats(dateCounts, outOfRangeCount) : null;

		const graph = showTimeLine ? this.renderTimeLine(currentDateAggregation, graphType,	searchId, query, collectionConfig) : null;

    	return (
        	<div className={IDUtil.cssClassName('result-dates', this.CLASS_PREFIX)}>
        		<div className={IDUtil.cssClassName('result-dates-header', this.CLASS_PREFIX)}>

                	<div className={IDUtil.cssClassName('date-field', this.CLASS_PREFIX)}>
                		 <span data-tip data-for={'__qb__tt-date-range'}>
                             <i className="fas fa-info-circle" aria-hidden="true"/>
				        </span>
                        <ReactTooltip id={'__qb__tt-date-range'} getContent={
                        	() => MessageHelper.renderDatFieldTooltip(IDUtil.cssClassName('tooltip'))}
                        />
                		{dateFieldSelector}{dateTotalStats && " ► "}
                		{dateTotalStats}
                	</div>

                	{(query.dateRange && query.dateRange.field) &&
                    	(<div className={IDUtil.cssClassName('date-range', this.CLASS_PREFIX)}>
                			Date range
                			{dateRangeSelector}
                			{dateRangeStats && " ► "}
                			{dateRangeStats}
                		</div>)
                    }

                	{/* Show chart button */}
            		{(query.dateRange && query.dateRange.field) &&
            			<button className="btn" onClick={this.toggleTimeLine}>
            				{showTimeLine ? "Hide chart" : "Show chart"}
        				</button>
            		}
                </div>

	            {(showTimeLine && query.dateRange && query.dateRange.field) &&
	            	<div className={IDUtil.cssClassName('result-dates-content', this.CLASS_PREFIX)}>
	            		<div className={IDUtil.cssClassName('date-graph', this.CLASS_PREFIX)}>
	                		{graph}
	                	</div>
	                	{dateRangeCrumb}
	            	</div>
	        	}
			</div>
		)
    }

    renderLockFacetsCheckBox = () => {

        return (
            <div className={IDUtil.cssClassName('lock-facets', this.CLASS_PREFIX)}>
                <label htmlFor="lock-facets-id">Lock facet selections
                </label>
                    <input
                        id="lock-facets_id"
                        type="checkbox"
                        onChange={this.onFacetLockChange.bind(this)}
                        />
            </div>)

    }

    render() {
        if (this.props.collectionConfig && this.state.query && this.state.isQueryAccessDenied === false) {
        	const layerOptions = this.renderSearchLayerOptions(); //FIXME always returns null (keeping it for other clients to fix later on)

			//draw the field category selector
			const fieldCategorySelector = this.renderFieldCategorySelector(
				this.state.query,
				this.props.collectionConfig,
				this.onComponentOutput.bind(this)
			);

			const dateControls = this.props.dateRangeSelector ? this.renderDateControls(
				this.state.aggregations,
				this.state.query,
				this.props.collectionConfig,
				this.state.searchId,
				this.state.totalHits,
				this.state.showTimeLine,
				this.state.dateGraphType,
				this.onComponentOutput.bind(this)
			) : null;

			//render the keyword field selector and term aggregation chart
			//first set the term limit appropriately to the data
			const currentKeywordAggregation = this.getCurrentKeywordAggregation(this.state.aggregations, this.state.selectedKeywordField)

			const keywordControls = this.renderKeywordControls(
			    currentKeywordAggregation,
				this.state.query,
			    this.state.selectedKeywordField,
			    this.state.termLimit,
			    currentKeywordAggregation ? currentKeywordAggregation.length : this.state.termLimit, //data limit
			    this.state.showKeywordHistogram,
			    this.props.collectionConfig,
			    this.state.searchId,
			    this.state.totalHits,
			    this.state.termGraphType,
			    this.onComponentOutput.bind(this))

            const queryResultCount = this.state.totalHits === 0 || this.state.totalHits ? this.renderQueryResultHits(this.state.totalHits) : null;

			const aggregationBox = this.state.aggregations && this.state.searchId && this.state.query && this.props.collectionConfig ? this.renderAggregationList(
				this.state.searchId,
				this.state.query,
				this.state.aggregations,
				this.props.collectionConfig,
				this.onComponentOutput.bind(this)
			) : null;

            //render the search term input. This allows the user to enter a search term
            //If a person-related field category (cluster) is selected (these are specified in the collection config),
            //then the input will offer auto-complete options from the GTAA

            if(!this.props.query.entities)
            {
                this.props.query.entities = []
            }
			const searchTermInput = this.renderSearchTermInput(
				this.props.collectionConfig,
				this.state.query.fieldCategory,
				this.props.query.entities,
				this.props.query.term,
				this.newSearch, //calling the new search function if a term is entered
				this.onComponentOutput.bind(this) //calling the addEntities function if a suggestion is selected
			);

            //render the checkbox for saving facet values
            const lockFacetsCheckbox = this.renderLockFacetsCheckBox()

            const noResultsMessage = this.state.isSearching === false && this.state.searchId != null && this.state.totalHits === 0 ? this.renderNoResultsMessage(
        		this.state.aggregations,
        		this.state.query,
        		this.clearSearch
        	) : null;

        	//if the search API returned a paging out of bounds error, return a helpful message for the user
	        const pagingOutOfBounds = this.props.isPagingOutOfBounds ? this.renderPagingOutOfBoundsMessage() : null;

			return (
				<div className={IDUtil.cssClassName('query-builder')}>
						<div className={IDUtil.cssClassName('query-row', this.CLASS_PREFIX)}>
                                {/* Search keywords input */}
                                {searchTermInput}
							{/* Metadata field selector */}
							<div className={IDUtil.cssClassName('selector-holder', this.CLASS_PREFIX)}>
								<div className={IDUtil.cssClassName('in-label', this.CLASS_PREFIX)}>in</div>
								    <div className={IDUtil.cssClassName('tt', this.CLASS_PREFIX)}>
                                        <span data-tip data-for={'__fs__tt-category-selector'}>
                                            <i className="fas fa-info-circle" aria-hidden="true"/>
                                        </span>
                                        <ReactTooltip id={'__fs__tt-category-selector'} place='right' getContent={
                                            () => MessageHelper.renderCategorySelectorTooltip(IDUtil.cssClassName('tooltip'))}
                                        />
                                    </div>
								{fieldCategorySelector}
							</div>
						</div>
						{lockFacetsCheckbox}
					{layerOptions}
					<div>
	                	{dateControls}
	                	{keywordControls}
	                    <div className="separator"/>
	                    {queryResultCount}
	                    <div className="row">
	                    	<div className="col-md-4">
	                    		{aggregationBox}
	                    	</div>
	                    	<div className="col-md-8">
	                    		{this.props.resultList}
	                    		{noResultsMessage}
	                    		{pagingOutOfBounds}
	                    	</div>
	                    </div>
	                </div>
				</div>
			)
		} else if (this.state.isQueryAccessDenied) {
			//if the user tries to access a query that is not shared by its owner (OR does not exist)
	        return this.renderQueryAccessDeniedMessage(this.clearSearch)
		} else {
			return (<div>Loading collection configuration...</div>);
		}
	}
}

QueryBuilder.propTypes = {
	header: PropTypes.bool, //whether to show a header with a title
	aggregationView: PropTypes.string.isRequired, //always set to 'list' (used to support 'box' as well)
	dateRangeSelector: PropTypes.bool, //whether or not to show a date range selector
	showTimeLine: PropTypes.bool, //whether or not to show the timeline component
	showKeywordHistogram: PropTypes.bool, //whether or not to show the keyword histogram component
    query: PropTypes.object, //the initial query that is run when this component has mounted (optional)
    collectionConfig: PropTypes.shape({
        clientId: PropTypes.string,
        collectionId: PropTypes.string,
        collectionMetadata: PropTypes.object,
        dateFields: PropTypes.array,
        docType: PropTypes.string,
        doubleFields: PropTypes.array,
        keywordFields: PropTypes.array,
        longFields:PropTypes.array,
        nestedFields: PropTypes.array,
        nonAnalyzedFields: PropTypes.array,
        stringFields: PropTypes.array,
        textFields: PropTypes.array,
        user: PropTypes.object
    }).isRequired, //needed for each search query
    resultList : PropTypes.object, //markup with the entire result list
    isPagingOutOfBounds: PropTypes.bool, //whenever the user paged too far (ES limitation)
    isQueryAccessDenied: PropTypes.bool, //when a user tries to access someone else's query that is not shared
    pageSize: PropTypes.number, //result list size
    onOutput: PropTypes.func.isRequired, //calls this function after the search results are received, so the owner can process/visualise them
    onStartSearch : PropTypes.func //calls this function whenever a search call starts, so the owner can draw a loading graphic,
};
