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

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

import SearchAPI from './api/SearchAPI';
import PlayoutAPI from './api/PlayoutAPI';
import ProjectAPI from './api/ProjectAPI';
import QueryAPI from './api/QueryAPI';
import AnnotationAPI from './api/AnnotationAPI';
import CollectionRegistryAPI from './api/CollectionRegistryAPI';

import IDUtil from './util/IDUtil';
import CollectionUtil from './util/CollectionUtil';
import ComponentUtil from './util/ComponentUtil';
import AnnotationUtil from './util/AnnotationUtil';
import LocalStorageHandler from './util/LocalStorageHandler';

import FlexModal from './components/FlexModal';

import FlexRouter from './util/FlexRouter';

import CollectionSelector from './components/collection/CollectionSelector';
import BookmarkSelector from './components/bookmark/BookmarkSelector';

import ToolHeader from './components/shared/ToolHeader';
import CollectionBar from './components/search/CollectionBar';
import QueryBuilder from './components/search/QueryBuilder';
import QueryEditor from './components/search/QueryEditor';
import SearchHit from './components/search/SearchHit';
import QuickViewer from './components/search/QuickViewer';
import Paging from './components/search/Paging';
import Sorting from './components/search/Sorting';

import MessageHelper from './components/helpers/MessageHelper';
import Loading from './components/shared/Loading';
import classNames from 'classnames';
import PaginationUtil from './util/PaginationUtil';

export const SINGLE_SEARCH_RESOURCE_VIEWER_POPUP = 'single-search-result-viewer';

export default class SingleSearchRecipe extends React.Component {

	constructor(props) {
		super(props);

		this.state = {
			initializing : true,

			showCollectionModal : false, //for the collection selector
			showBookmarkModal : false, //for the bookmark group selector
			showQuickViewModal : false, //for the quickview result preview
			savedBookmarkModal: false,
			collectionList : null,
			browseSelection : false, //always start off with the search results

			activeProject : null,
			userProjects : null,

			isPagingOutOfBounds: false,

			isQueryAccessDenied: false,
			isSearching : false, //awaiting the search API
			pageSize : 20, //amount of search results on page

			collectionConfig : null, //loaded after mounting, without it nothing works
			currentOutput: null, //contains the current search results
			initialQuery : null, //yikes this is only used for storing the initial query
			lastQuerySaved : null,
			selectedOnPage : {}, // key = resourceId, value = true/false; this is a subselection from the 'stored-selections', which contains selections from everywhere
			allRowsSelected : false, // are all search results selected

			gtaaId : null //selected entity from dropdown, passed through from SearchTermInput
		};

		this.CLASS_PREFIX = "ssr";
		PropTypes.checkPropTypes(SingleSearchRecipe.propTypes, this.props, 'prop', this.constructor.name);
	}

	static elementInViewport(el) {
		const rect = el.getBoundingClientRect();
		return (
			rect.top >= 0 && rect.left >= 0 && rect.top <= (window.innerHeight || document.documentElement.clientHeight)
		)
	}

	static afterRenderingHits() {
		const imgDefer = document.getElementsByTagName('img');
		for (let i=0; i<imgDefer.length; i++) {
			if(imgDefer[i].getAttribute('data-src') && SingleSearchRecipe.elementInViewport(imgDefer[i])) {
				imgDefer[i].setAttribute('src', imgDefer[i].getAttribute('data-src'));
				// prevent multiple conversions
				imgDefer[i].removeAttribute('data-src');
			}
		}
	}

	componentWillUnmount() {
		window.onscroll = null;
	}

	componentDidMount = async () => {
		//makes sure that the images are loaded only when visible
		window.addEventListener('scroll', () => {SingleSearchRecipe.afterRenderingHits()});

		this.init();
	}

	//new init function that properly loads page content (and actually handles errors)
	init = async () => {
		//first construct a project from local storage (if any)
		const project = LocalStorageHandler.getJSONFromLocalStorage('stored-active-project') ?
			Project.construct(LocalStorageHandler.getJSONFromLocalStorage('stored-active-project')) :
			null
		;

		//then see which query should be executed
		let initialQuery = null;
		let queryLoadError = -1; //everything is fine
		try {
			initialQuery = await this.determineInitialQuery(
				this.props.user,
				this.props.params
			);
		} catch(err) {
			queryLoadError = err;
		}
		//if the initialQuery is null, use the ID of the last used collection
		//if this ends up being null that should be fine (only collection selector will show)
		const selectedCollectionId = initialQuery ?
			initialQuery.collectionId :
			LocalStorageHandler.getJSONFromLocalStorage('active-collection-id')
		;
		//first get the collectionConfig
		const collectionConfig = selectedCollectionId ? await this.generateCollectionConfig(
			this.props.clientId,
			this.props.user,
			selectedCollectionId
		) : null;

		//only construct an initial collection query if there is no specific load error
		if(!initialQuery && queryLoadError === -1) {
			initialQuery = Query.construct({size : this.state.pageSize}, collectionConfig);
		}

		//now load all the user's projects
		const userProjects = await this.loadUserProjects(this.props.user);

		//and the user's bookmarks
		//TODO do not load these bookmarks by default! It is very slow
		const bookmarks = await this.loadUserBookmarks(this.props.user, project);

		//and finally the list of collections
		const collectionList = await this.loadCollectionList();

		this.setState({
			initializing : false, //done initializing!
			activeProject : project,
			collectionConfig : collectionConfig,
			collectionId : selectedCollectionId,
			initialQuery : initialQuery,
			userProjects : userProjects,
			activeBookmarks : bookmarks,
			collectionList : collectionList,
			isQueryAccessDenied : queryLoadError !== -1, //NB either 403 or 404 at the moment!
			currentOutput : null
		});
	};

	// ---------------------------------- SYNCHRONOUS LOADING FUNCTIONS -------------------------

	//TODO really make sure this works well. Also figure out if prioritised queries still are necessary!
	determineInitialQuery = async (user, urlParams) => {
		let initialQuery = null;
		if (urlParams && urlParams.queryId) {
			// the stored-priority-query else the query in stored-search-results
			if(urlParams.queryId === 'cache') {
				const storedResults = LocalStorageHandler.getJSONFromLocalStorage('stored-search-results');
				initialQuery = storedResults ? storedResults.query : null;
			} else if (urlParams.queryId === 'prio') {
				//FIXME get rid of this weird type of query, see CollectionConfig.__getSearchReferences()
				//also see MetadataTable.performSearchReference()
				initialQuery = LocalStorageHandler.getJSONFromLocalStorage('stored-priority-query');
			}  else { // have a query ID
			    const userQuery = await this.loadUserQuery(urlParams.queryId, user.id === 'ANONYMOUS' ? null : user.id , null);
			    if(userQuery && userQuery.query && userQuery.queryType === 'layered search') {
			        initialQuery = userQuery.query;
			    } else if (userQuery instanceof APIError) {
			    	throw userQuery.toHTTPErrorCode();
			    }
			}
		}
		return initialQuery;
	};

	generateCollectionConfig = (clientId, user, collectionId) => {
		return new Promise(resolve => {
			if (!clientId || !user || !collectionId) resolve(null);

			CollectionUtil.generateCollectionConfig(
				clientId,
				user,
				collectionId,
				resolve
			);
		});
	};

	loadUserProject = (user, projectId) => {
		return new Promise(resolve => {
			if (!user || !user.id || !projectId) resolve(null);
			ProjectAPI.get(user.id, projectId, resolve);
		});
	};

	loadUserQuery = (queryId, user, projectId) => {
		return new Promise(resolve => {
			if (!queryId) resolve(null);
			QueryAPI.get(queryId, user, projectId, resolve);
		});
	};

	loadUserProjects = user => {
		return new Promise(resolve => {
			if (!user || !user.id) resolve([]); //always make sure to return a list
			ProjectAPI.list(user.id, null, resolve);
		});
	};

	loadUserBookmarks = (user, project) => {
		return new Promise(resolve => {
			if(!project || !project.id || !user || !user.id) resolve(null);
			AnnotationAPI.getBookmarks(user.id, project.id, resolve);
		});
	};

	loadCollectionList = () => {
		return new Promise(resolve => {
			CollectionRegistryAPI.listCollections(resolve);
		})
	};

	onLoadPlayoutAccess = (accessApproved, desiredState) => {
		this.setState(
			desiredState, () => {
				if(desiredState.currentOutput === null) {
					//clear the stored query and start a fresh single search query
					LocalStorageHandler.removeJSONByKeyInLocalStorage('stored-search-results');
					FlexRouter.gotoSingleSearch()
				}

				// show media visible on screen
				SingleSearchRecipe.afterRenderingHits();
			}
		);
	};

	/* ------------------------------- CHILD COMPONENT CALLBACKS ----------------------- */

	//NOTE: the original idea was to control the output of all child components in this function and orchestrate what to do based on the recipe
	onComponentOutput = (componentClass, data) => {
		if(componentClass === 'QueryBuilder') {
			this.onSearched(data);
		} else if (componentClass === 'CollectionSelector') {
			this.onCollectionSelected(data);
		} else if(componentClass === 'SearchHit') {
			this.onItemSelected(data.resource, data.selected);
		} else if(componentClass === 'BookmarkSelector') {
			this.onBookmarkGroupSelected(data);
		} else if(componentClass === 'QueryEditor') {
			this.onQuerySaved(data)
		} else if(componentClass === 'CollectionBar') {
			this.onSearched(data); //data is always null (used to reset the search...)
		} else if(componentClass === 'ToolHeader') {
			this.onProjectSelected(data)
		}
	};

	onCollectionSelected = collectionMetadata => {
		const collectionConfig = CollectionUtil.createCollectionConfig(
			this.props.clientId,
			this.props.user,
			collectionMetadata.index,
			collectionMetadata
			);
		//set the default query for the selected collection; creates a new query builder
		LocalStorageHandler.storeJSONInLocalStorage('active-collection-id', collectionConfig.collectionId);
		const previousSearchTerm = this.state.currentOutput && this.state.currentOutput.query ?
		this.state.currentOutput.query.term :
		null;
		this.setState(
		{
			collectionId: collectionConfig.collectionId,
			collectionConfig: collectionConfig,
			initialQuery: Query.construct({size: this.state.pageSize, term : previousSearchTerm}, collectionConfig),
			currentOutput: null,
			browseSelection: false,
			isQueryAccessDenied: false
		},
		() => {
			ComponentUtil.hideModal(this, 'showCollectionModal', 'collection__modal', true);
		}
		);
	};

	onProjectSelected = async (project) => {
		this.reloadBookmarks(this.props.user, project);
		LocalStorageHandler.storeJSONInLocalStorage('stored-active-project', project);
	};

	onItemSelected = (searchResult, isSelected) => {
		if(searchResult) {
			//update the list of selected items (showing on the page)
			const selectedOnPage = this.state.selectedOnPage;
			if(isSelected) {
				selectedOnPage[searchResult.resourceId] = true;
			} else {
				delete selectedOnPage[searchResult.resourceId]
			}

			// make sure to update the list of stored bookmarks with the changed selection
			this.updateSelectedItems(searchResult, isSelected);

			//determine whether the last selected item in the selection was selected, so we need to switch back to showing the result list
			let browseSelection = this.state.browseSelection;
			if(browseSelection) {
				const selRowsInLocalStorage = LocalStorageHandler.getJSONFromLocalStorage('stored-selections') || [];
				browseSelection = selRowsInLocalStorage.length > 0;
			}

			this.setState({
				selectedOnPage : selectedOnPage,
				allRowsSelected : isSelected ? this.areAllItemsSelected() : false,
				browseSelection : browseSelection,
			});
		}
	};

	/* ------------------------------- SEARCH RELATED FUNCTIONS ----------------------- */

	onStartSearch = () => {
		this.setState({
			isSearching : true
		})
	};

	onSearched = (resultsObj, activeResource=null, paging=false) => {
		const desiredState = {
			currentOutput: resultsObj,
			browseSelection : false
		};

		// if search is not the result of paging then clear selectedOnPage.
		!paging ? desiredState.selectedOnPage = {} : desiredState;

		//reset the poster images to the placeholder
		const imgDefer = document.getElementsByTagName('img');
		for (let i=0; i<imgDefer.length; i++) {
			if(imgDefer[i].getAttribute('data-src')) {
				imgDefer[i].setAttribute('src', '/static/images/placeholder.2b77091b.svg');
			}
		}

		//request access for the thumbnails if needed
		if (this.state.collectionConfig.requiresPlayoutAccess() && this.state.collectionConfig.getThumbnailContentServerId()) {
			PlayoutAPI.requestAccess(
				this.state.collectionConfig.getThumbnailContentServerId(),
				'thumbnails',
				desiredState,
				this.onLoadPlayoutAccess
			)
		} else {
			this.onLoadPlayoutAccess(true, desiredState);
		}
		this.setState({
			selectedOnPage : this.getAlreadySelectedItems(resultsObj),
			allRowsSelected: this.areAllItemsSelected(),
			isSearching: false,
			isPagingOutOfBounds : resultsObj ? resultsObj.pagingOutOfBounds === true : false
		})

		//If the quickview modal was open during the loading of the new page of results...
		//It has to pop up again after loading new results.
		//TODO merge with the desiredState
		if(this.state.showQuickViewModal && activeResource) {
			this.openQuickViewModal(activeResource);
		}
	};

	gotoPage = pageNumber => {

		this.setState({
			isSearching : true
		}, () => {
			if(this.state.currentOutput && this.state.currentOutput.query) {
				PaginationUtil.loadSearchResultPage(
					this.state.currentOutput,
					this.state.collectionConfig,
					pageNumber,
					this.onPaged
				);
			}
		})
	};

	onPaged = (activeResource, newSearchResults) => {
		if(this.state.showQuickViewModal) {
			this.openQuickViewModal(activeResource); //makes sure the quick viewer stays open
		}
		if(newSearchResults) { //make sure the owner get's notified of new search results
			this.onSearched(newSearchResults, activeResource, true) //calls the owners onSearched
		}
	}

	//NOTE: The sortMode is translated to sort params inside the QueryBuilder component
	//sortMode = {type : date/rel, order : desc/asc}
	sortResults = (queryId, sortParams) => {
		this.setState({
			isSearching : true
		}, () => {
			if (this.state.currentOutput) {
				const sr = this.state.currentOutput;
				sr.query.sort = sortParams;
				sr.query.offset = 0;
				SearchAPI.search(sr.query, sr.collectionConfig, data => this.onSearched(data), true)
			}
		})
	};


	/* ------------------------------- TABLE ACTION FUNCTIONS ----------------------- */

	//hides search results and shows all selected items
	browseSelection = () => {
		this.setState({
			browseSelection : !this.state.browseSelection
		}, ()=>{
			// show media visible on screen
			SingleSearchRecipe.afterRenderingHits();
		});
	};


	//checks if the search results contain resources that were already selected in another query
	getAlreadySelectedItems = resultsObj => { //instance of SearchResults
		if(!resultsObj || !resultsObj.results) {
			return {};
		}
		const rowsInStorage = LocalStorageHandler.getJSONFromLocalStorage('stored-selections') || [];

		const selRows = {};
		resultsObj.results.forEach(searchResult => rowsInStorage.filter(obj => {
			if(obj.resourceId === searchResult.resourceId) {
				selRows[searchResult.resourceId] = true;
			}}
		));
		return selRows;
	};

	//TODO check this function, it is a bit duplicate, similar code is also in ...
	areAllItemsSelected = () => {
		const storeSelectedRows = LocalStorageHandler.getJSONFromLocalStorage('stored-selections')
		if(!storeSelectedRows || !(this.state.currentOutput && this.state.currentOutput.results)) {
			return false;
		}
		return this.state.currentOutput.results.every(
			itemOnPage => storeSelectedRows.find(storedObj => storedObj.resourceId === itemOnPage.resourceId)
		);
	};

	clearSelectedItems = () => {
		LocalStorageHandler.removeJSONByKeyInLocalStorage('stored-selections');
		this.setState({
			selectedOnPage : {},
			allRowsSelected : false,
			browseSelection : false,
		});
	};

	toggleSelectAllItems = e => {
		e.preventDefault();
		let rows = this.state.selectedOnPage;
		const rowsOnLocalStorage = LocalStorageHandler.getJSONFromLocalStorage('stored-selections') || null;

		if(this.state.allRowsSelected) {
			this.state.currentOutput.results.forEach(result => {
				const isChecked = Object.keys(rows).findIndex(resourceId => resourceId === result.resourceId);
				if(isChecked !== -1) {
					LocalStorageHandler.removeItemInLocalStorage('stored-selections', result, 'resourceId');
				}
			});
			rows = {};
		} else {
			this.state.currentOutput.results.forEach(result => {
				rows[result.resourceId] = !this.state.allRowsSelected;
				const isChecked = rowsOnLocalStorage ? rowsOnLocalStorage.findIndex(
					storedObj => storedObj.resourceId === result.resourceId
					) : -1;

				if(isChecked === -1) {
					this.updateSelectedItems(result, true);
				}
			});
		}
		this.setState({
			allRowsSelected : !this.state.allRowsSelected,
			selectedOnPage : rows
		});
	};

	updateSelectedItems = (resource, select) => {
		if(select) {
			resource.query = this.state.currentOutput.query;
			LocalStorageHandler.pushItemToLocalStorage('stored-selections', resource, 'resourceId');
		} else {
			LocalStorageHandler.removeItemInLocalStorage('stored-selections', resource, 'resourceId');
		}
	};

	/* ------------------------------- BOOKMARK RELATED FUNCTIONS ----------------------- */

	reloadBookmarks = async (user, project) =>{
		const bookmarks = await this.loadUserBookmarks(user, project);
		this.setState({ activeBookmarks : bookmarks, activeProject : project})
	};

	bookmarkSelectedItems = () => this.setState({showBookmarkModal : true, showBookmarkItems : false, browseSelection : false});

	// makes sure that all selected resources are ADDED to the selected groups
	bookmarkToGroupInProject = (allGroups, selectedGroups, selectedProject) => {
		const selectedRows = LocalStorageHandler.getJSONFromLocalStorage('stored-selections');
		ComponentUtil.hideModal(this, 'showBookmarkModal', 'bookmark__modal', true, () => {
			let saveCount = 0;
			//run through all the selected groups
			allGroups.filter(group => selectedGroups[group.id] === true).forEach(group => {
				//then add all the selected resources to the group's list of targets
				const targets = group.target.concat(
					selectedRows.map(result => AnnotationUtil.generateResourceLevelTarget(
						result.collectionId, result.resourceId
					))
				);

				//make sure to remove duplicate targets (could happen in case a target was already in a group)
				const temp = {};
				const dedupedTargets = [];
				targets.forEach((t) => {
					if(!temp[t.source]) {
						temp[t.source] = true;
						dedupedTargets.push(t);
					}
				});
				group.target = dedupedTargets;

				//FIXME this code is not entirely safe: what if somehow the saveAnnotation does not return?
				AnnotationAPI.saveAnnotation(group, () => {
					if(++saveCount === Object.keys(selectedGroups).length) {
						this.onSaveBookmarks(selectedProject);
					}
				});
			});

		});
	};

	onBookmarkGroupSelected = data => {
		if(data && data.allGroups && data.selectedGroups && data.selectedProject) {
			this.bookmarkToGroupInProject(data.allGroups, data.selectedGroups, data.selectedProject);
		}
	};

	onSaveBookmarks = (selectedProject) => {
		this.setState({
			selectedOnPage : {},
			allRowsSelected : false,
			browseSelection : false,
			savedBookmarkModal: true,
			activeProject: selectedProject
		}, () => {
			this.reloadBookmarks(this.props.user, selectedProject);
			LocalStorageHandler.removeJSONByKeyInLocalStorage('stored-selections');
		})
	};

	/* ------------------------------- QUERY SAVING RELATED FUNCTIONS ----------------------- */

	saveQuery = () => this.setState({showQueryModal : true});

	onQuerySaved = data => {
		if(!data) return;
		ComponentUtil.hideModal(this, 'showQueryModal', 'query__modal', true, () => {
			this.setState({
				savedQueryModal : true,
				lastQuerySaved : data.queryName,
				activeProject: data.project
			});
		});
	};

	/* ------------------------------- QUICKVIEW FUNCTIONS -------------------------- */

	openQuickViewModal = searchResult => {
		this.setState({
			quickViewData: searchResult, //instance of SearchResult
			showQuickViewModal : true
		});
	};

	/* -------------------------------- RENDER THE MODALS --------------------------*/

	renderCollectionModal = () => {
		return (
			<FlexModal
				elementId="collection__modal"
				stateVariable="showCollectionModal"
				owner={this}
				size="large"
				title="Choose a collection">
					<CollectionSelector
						onOutput={this.onComponentOutput}
						showSelect={true}
						collectionList={this.state.collectionList}
						showBrowser={true}/>
			</FlexModal>
		);
	};

	renderQuickViewModal = (browseSelection, selected, searchResult, collectionConfig) => { //quickViewData is an instance of SearchResult
		return (
			<FlexModal
				elementId="quickview__modal"
				stateVariable="showQuickViewModal"
				owner={this}
				size="large"
				title={searchResult.title}>
				<QuickViewer
					browseSelection={browseSelection} // should the quickviewer page through the selection or result list
					searchResult={searchResult} // i.e SearchResult
					selected={selected} // if the current quick view result is in the stored rows
					collectionConfig={collectionConfig}

					onPaged={this.onPaged} //called after a page action has been completed
					onLoading={this.onStartSearch} // needs to be called back on paging to a new search result page
					onSelect={this.onItemSelected} // same function as used by the search hit
				/>
			</FlexModal>
		)
	};

	onGotoResourceViewer = (data, query, unique) => {
		if(data.resourceId) {
			//FlexRouter.popupResourceViewer(this.props.recipe.ingredients.resourceViewerPath, data, query.term,SINGLE_SEARCH_RESOURCE_VIEWER_POPUP + (unique ? data.resourceId : ''),false, () => this.reloadBookmarks(this.props.user, this.state.activeProject));
			FlexRouter.gotoResourceViewer('/tool/resource-viewer', data, query.term);
		}
	};

	renderQueryModal = (currentOutput, activeProject, userProjects) => {
		const projectTitle = activeProject ?
			`Save query parameters to your user project: ${activeProject.name}` :
			'Save query parameters to a new user project';
			currentOutput.query.id = null;  //as saving, is a new query so reset id
		return (
			<FlexModal
				elementId="query__modal"
				stateVariable="showQueryModal"
				owner={this}
				size="large"
				title={projectTitle}>
					<QueryEditor
						query={currentOutput.query}
						userProjects={userProjects}
						collectionConfig={currentOutput.collectionConfig}
						user={this.props.user}
						project={activeProject}
						onOutput={this.onComponentOutput}/>
			</FlexModal>
		);
	};

	renderBookmarkModal = (collectionConfig, activeProject, userProjects) => {
		return (
			<FlexModal
				elementId="bookmark__modal"
				stateVariable="showBookmarkModal"
				owner={this}
				size="large"
				title="Bookmark your selection">
					<BookmarkSelector
						onOutput={this.onComponentOutput}
						user={this.props.user}
						project={activeProject}
						userProjects={userProjects}
						collectionId={collectionConfig.collectionId}
						/>
			</FlexModal>
		)
	};

	renderSavedBookmarkModal = () => (
		<FlexModal
			elementId="saved-bookmark__modal"
			stateVariable="savedBookmarkModal"
			owner={this}
			size="large"
			title="Bookmarks saved successfully">
			Your selection of bookmarks were saved succesfully to project &quot;{this.state.activeProject.name}&quot;
			<p><a target="_blank" rel="noopener noreferrer" className="bg__workspace-bookmarks-link" href={"/workspace/projects/" + this.state.activeProject.id + "/bookmarks"}>Go to saved bookmarks</a></p>
		</FlexModal>
	);


	renderSavedQueryModal = () => (
		<FlexModal
			elementId="query-saved__modal"
			stateVariable="savedQueryModal"
			owner={this}
			size="large"
			title="Query saved successfully">
			<p>Your query ({this.state.lastQuerySaved}) was saved successfully to project &quot;{this.state.activeProject.name}&quot;</p>
			<p><a target="_blank" rel="noopener noreferrer" className="bg__workspace-queries-link" href={"/workspace/projects/" + this.state.activeProject.id + "/queries"}>Go to saved queries</a></p>
		</FlexModal>
	);

	/* --------------------------- RENDER HEADER --------------------- */

	renderHeader = (name, activeProject, userProjects, user) => (
		<ToolHeader
		name={name}
		activeProject={activeProject}
		projects={userProjects}
		user={user}
		onOutput={this.onComponentOutput}
		/>
	);

	/* --------------------------- RENDER COLLECTION BAR --------------------- */


	renderCollectionBar = (user, collectionConfig, activeProject) => (
			<CollectionBar
				user={user}
				collectionConfig={collectionConfig}
				selectCollection={ComponentUtil.showModal.bind(this, this, 'showCollectionModal')}
				resetSearch={this.onComponentOutput}
				saveQuery={this.saveQuery}
			/>
	);

	/* --------------------------- RENDER RESULT LIST ----------------------------*/

	renderResultTable = (state, storedSelectedRows, visitedResults) => {
		//only render when there is a collectionConfig and searchAPI output
		if(!(
			state.collectionId &&
			state.collectionConfig &&
			state.currentOutput &&
			state.currentOutput.results &&
			state.currentOutput.results.length > 0
		)) {
			return null
		}

		let listComponent = null;

		if (state.browseSelection) {//storedSelectedRows && storedSelectedRows.length > 0
			listComponent = this.renderSelectionOverview(storedSelectedRows, state.currentOutput.query, state.collectionConfig, visitedResults)
		} else {
			//populate the list of search results
			listComponent = state.currentOutput.results.map(
				(result, index) => this.renderSearchResult(
					result,
					index,
					state.currentOutput.query,
					state.collectionConfig,
					storedSelectedRows,
					state.activeBookmarks,
					visitedResults.includes(result.resourceId)
				)
			);
		}

		const tableHeader = this.renderTableHeader(
			state.currentOutput,
			state.collectionConfig,
			storedSelectedRows,
			state.browseSelection,
			state.selectedOnPage,
			state.pageSize
		);

		const tableFooter = this.renderTableFooter(
			state.currentOutput,
			state.pageSize
		);

		return (
			<div className={classNames(IDUtil.cssClassName('result-list', this.CLASS_PREFIX))}>
				{tableHeader}
				{listComponent}
				{tableFooter}
			</div>
		)
	};

	/* --------------------------- RENDERING LISTS -------------------------------*/

	renderSelectionOverview = (storedSelectedRows, query, collectionConfig, visitedResults) => {
		return (
			<div className="table-actions-header bookmarked-results">
				<h4 className="header-selected-items">
					Selected items
					<i className="fas fa-times" onClick={this.browseSelection}/>
				</h4>
				<div className="selected-items">
					{storedSelectedRows.map((result, index) => {
						return this.renderSelectedItems(result, index, query, collectionConfig, visitedResults.includes(result.resourceId))
					})}
				</div>
			</div>
			);
	};

	renderSelectedItems = (result, index, query, collectionConfig, visited) => {
		return (
			<SearchHit
			key={'saved__' + index}
				searchResult={result} //instance of SearchResult
				collectionConfig={collectionConfig}
				query={query}
				visited={visited}
				bookmarked={null}
				selectable={this.state.activeProject ? true : false}
				isSelected={true}
				onQuickView={this.openQuickViewModal}
				onOutput={this.onComponentOutput}
				onGotoResourceViewer={this.onGotoResourceViewer}
				/>
				)
	};

	renderSearchResult = (result, index, query, collectionConfig, storedSelectedRows, activeBookmarks, visited) => {
		const bookmark = activeBookmarks ? activeBookmarks.find(item => item.resourceId === result.resourceId) : null;
		const isSelectedItem = storedSelectedRows.find(item => item.resourceId === result.resourceId) !== undefined;

		return (
			<SearchHit
				key={'__' + index}
				searchResult={result}
				collectionConfig={collectionConfig}
				query={query}
				visited={visited}
				bookmark={bookmark}
				selectable={this.state.activeProject ? true : false}
				isSelected={isSelectedItem}
				onQuickView={this.openQuickViewModal}
				onOutput={this.onComponentOutput}
				onGotoResourceViewer={this.onGotoResourceViewer}
			/>
		)
	};

	/* ---------------------------- RENDER THE TABLE ------------------------- */

	renderPagingButtons = (currentPage, totalHits, pageSize) => {
		if(currentPage <= 0) {
			return null;
		}
		return (
			<Paging
				currentPage={currentPage}
				numPages={Math.ceil(totalHits / pageSize)}
				gotoPage={this.gotoPage}
			/>
		)
	};

	renderSortButtons = (collectionConfig, query) => {
		if(!query.sort) {
			return null;
		}
		return (
			<Sorting
				sortResults={this.sortResults}
				sortParams={query.sort}
				collectionConfig={collectionConfig}
				dateField={query.dateRange ? query.dateRange.field : null}
			/>
		)
	};

	//FIXME the ugly HTML & lack of proper class names
	renderTableHeader = (currentOutput, collectionConfig, storedSelectedRows, browseSelection, selectedOnPage, pageSize) => {
		return (
			<div className="table-actions-header">
				{this.renderTableSelectAll(currentOutput, selectedOnPage)}
				{this.renderTableDropdown(storedSelectedRows, browseSelection)}
				<div style={{textAlign: 'center'}}>
					{this.renderPagingButtons(currentOutput.currentPage, currentOutput.totalHits, pageSize)}
					<div style={{float: 'right'}}>
						{this.renderSortButtons(collectionConfig, currentOutput.query)}
					</div>
				</div>
			 </div>
		)
	};

	renderTableFooter = (currentOutput, pageSize) => {
		return (
			<div className="table-actions-footer">
				{this.renderPagingButtons(currentOutput.currentPage, currentOutput.totalHits, pageSize)}
			</div>
		)
	};

	renderTableSelectAll = (currentOutput, selectedOnPage) => {
		const currentSelectedIds = this.state.selectedOnPage ? Object.keys(this.state.selectedOnPage) : []
		const allChecked = currentOutput.results.map(item => currentSelectedIds.findIndex(it => it === item.resourceId));
		const isChecked = allChecked.findIndex(item => item === -1) === -1;
		return (
			<div title={"Select " + (isChecked ? "none" : "all")} onClick={this.toggleSelectAllItems} className="select-all">
				<input type="checkbox" readOnly={true} checked={isChecked ? 'checked' : ''} id={'cb__select-all'} />
				<label htmlFor={'cb__select-all'}><span/></label>
			</div>
		);
	};

	renderTableDropdown = (storedSelectedRows, browseSelection) => {
		let dropdown = null;
		if(storedSelectedRows && storedSelectedRows.length > 0) {

			const actions = (
				<div className="dropdown-menu" aria-labelledby="dropdownBookmarking">
					<button className="dropdown-item" type="button" onClick={this.browseSelection}>
						{browseSelection ? 'Hide' : 'Show'} selected item(s)
					</button>
					<button className="dropdown-item" type="button" onClick={this.bookmarkSelectedItems}>
						Bookmark selection
					</button>
					<button
						className="dropdown-item"
						type="button"
						onClick={this.clearSelectedItems}>
						Clear selection
					</button>
				</div>
			);

			dropdown = (
				<div className="dropdown bookmark-dropdown-menu">
					<button className="btn btn-secondary dropdown-toggle" type="button" id="dropdownBookmarking"
					data-toggle="dropdown"
					title="Selected items to bookmark"
					aria-haspopup="true" aria-expanded="false">
					<i className="fas fa-check-square" style={{color: 'white', fontSize:'16px', paddingRight:'12px'}} />{storedSelectedRows.length}
					</button>
					{actions}
				</div>
				);
		}

		return (
			<div className="table-actions">
				{dropdown}
			</div>
		);
	};

	/* ---------------------------- RENDER QUERY BUILDER -------------------------- */

	renderSearchComponent = (
		collectionConfig, initialQuery, isSearching, resultList, isPagingOutOfBounds, isQueryAccessDenied, pageSize
		) => {
		const loadingMessage = isSearching ? <Loading message="Loading results..."/> : null;
		const queryBuilder = (
			<QueryBuilder
				key={collectionConfig.getCollectionId()} //for resetting all the states held within after selecting a new collection
				header={true}
				aggregationView={this.props.recipe.ingredients.aggregationView}
				dateRangeSelector={true}
				showTimeLine={true}
				showKeywordHistogram={true}

				query={initialQuery}

				collectionConfig={collectionConfig}
				onStartSearch={this.onStartSearch}
				onOutput={this.onComponentOutput}
				resultList={resultList} //inject the resultlist, so it can be properly positioned
				isPagingOutOfBounds={isPagingOutOfBounds}
				isQueryAccessDenied={isQueryAccessDenied}
				pageSize={pageSize}
			/>
		);

		return (
			<div className="search-component">
				{loadingMessage}
				{queryBuilder}
			</div>
		)
	};

	/* ---------------------------- MAIN RENDER FUNCTION -------------------------- */

	showHelp = () => {
		const event = new Event('BG__SHOW_HELP');
		window.dispatchEvent(event);
	};

	/* ---------------------------- MAIN RENDER FUNCTION -------------------------- */

	render() {
		if(this.state.initializing) return <Loading message="Initializing collection & user data..."/>;

		const storedSelectedRows = LocalStorageHandler.getJSONFromLocalStorage('stored-selections') || [];
		const visitedResults = LocalStorageHandler.getJSONFromLocalStorage('stored-visited-results') || [];

		// all the different modals
		const collectionModal = this.state.showCollectionModal && this.state.collectionList ? this.renderCollectionModal() : null;

		const quickViewModal = this.state.showQuickViewModal && this.state.quickViewData ? this.renderQuickViewModal(
			this.state.browseSelection,
			storedSelectedRows.findIndex(elem => elem.resourceId === this.state.quickViewData.resourceId) >= 0,
			this.state.quickViewData,
			this.state.collectionConfig
		) : null;

		const queryModal = this.state.showQueryModal && this.state.currentOutput && this.state.userProjects ? this.renderQueryModal(
			this.state.currentOutput,
			this.state.activeProject,
			this.state.userProjects
		) : null;

		const bookmarkModal = this.state.showBookmarkModal && this.state.userProjects && this.state.userProjects.length > 0 ?
		this.renderBookmarkModal(
			this.state.collectionConfig,
			this.state.activeProject,
			this.state.userProjects
		) : null;

		const header = this.renderHeader(
			this.props.recipe.name,
			this.state.activeProject,
			this.state.userProjects,
			this.props.user
		);

		// Component loaded only after header <ToolHeader> is rendered
		// to avoid 'glitch' when 'Collection Selection btn' loads first
		// because of async loading.
		const collectionBar = header ?
			this.renderCollectionBar(
				this.props.user,
				this.state.collectionConfig,
				this.state.activeProject
			) :
			null;

		const savedQueryModal = this.state.savedQueryModal ? this.renderSavedQueryModal() : null;
		const savedBookmarkModal = this.state.savedBookmarkModal ? this.renderSavedBookmarkModal() : null;

		//the query builder & loading message
		const searchComponent = this.state.collectionConfig ? this.renderSearchComponent(
			this.state.collectionConfig,
			this.state.initialQuery,
			this.state.isSearching,
			this.renderResultTable(this.state, storedSelectedRows, visitedResults),
			this.state.isPagingOutOfBounds,
			this.state.isQueryAccessDenied,
			this.state.pageSize
		) : null;

		return (
			<div className={IDUtil.cssClassName('single-search-recipe')}>
				{header}
				{collectionBar}
				{searchComponent}
				{collectionModal}
				{queryModal}
				{bookmarkModal}
				{quickViewModal}
				{savedQueryModal}
				{savedBookmarkModal}
			</div>
		);
	}
}

SingleSearchRecipe.propTypes = {
	clientId : PropTypes.string,

	user: PropTypes.shape({
		id: PropTypes.string.isRequired,
		name: PropTypes.string,
		attributes: PropTypes.shape({
			allowPersonalCollections: PropTypes.bool
		})
	}),

	params: PropTypes.shape({
		queryId: PropTypes.string
	}).isRequired,

	recipe: PropTypes.shape({
		description: PropTypes.string,
		id: PropTypes.string,
		inRecipeList: PropTypes.bool,
		name: PropTypes.string.isRequired, // Use for when rendering header (isRequired by <ToolHeader/>)
		phase: PropTypes.string,
		recipeDescription: PropTypes.string,
		type: PropTypes.string,
		url: PropTypes.string,
		ingredients: PropTypes.shape({
			resourceViewerPath: PropTypes.string.isRequired,
			collection: PropTypes.string,
			collectionSelector: PropTypes.bool,
			dateRangeSelector: PropTypes.string.isRequired, // Use by <QueryBuilder/> to render Date Controls
			aggregationView: PropTypes.string,
			useProjects: PropTypes.bool
		}).isRequired
	}).isRequired

};

SingleSearchRecipe.defaultProps = {
	queryId: 'cache'
};
