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

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

import SearchAPI from './api/SearchAPI';
import PlayoutAPI from './api/PlayoutAPI';
import ProjectAPI from './api/ProjectAPI';
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 { initHelp } from './components/workspace/helpers/helpDoc';

import MessageHelper from './components/helpers/MessageHelper';
import LoadingSpinner from './components/helpers/LoadingSpinner';
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 = {
			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 : LocalStorageHandler.getJSONFromLocalStorage('stored-active-project') ?
				Project.construct(LocalStorageHandler.getJSONFromLocalStorage('stored-active-project')) : null,
            userProjects : null,

			isPagingOutOfBounds: 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
		};

		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() {
		//init user docs (FIXME shouldn't this be part of the media suite code base?)
		initHelp("Search", "/feature-doc/howtos/single-search");

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

		const storedResults = LocalStorageHandler.getJSONFromLocalStorage('stored-search-results');
		const activeCollectionId = LocalStorageHandler.getJSONFromLocalStorage('active-collection-id');
		let collectionId = null;
		let initialQuery = null; // the stored-priority-query else the query in stored-search-results
		let loadingFromWorkSpace = false; // a query was selected in the workspace query table

		if (this.props.params && this.props.params.queryId) {
			if(this.props.params.queryId === 'cache') {
				initialQuery = storedResults ? storedResults.query : null;
				collectionId = initialQuery ? initialQuery.collectionId : null;
			} else if (this.props.params.queryId === 'prio') {
				initialQuery = LocalStorageHandler.getJSONFromLocalStorage('stored-priority-query');
				collectionId = initialQuery ? initialQuery.collectionId : null;
			} else if (this.props.params.queryId.indexOf('__') !== -1) {
				loadingFromWorkSpace = true;
				const tmp = this.props.params.queryId.split('__');
				if(tmp.length === 2) {
					const projectId = tmp[0];
					const queryId = tmp[1];
					//if the user supplied a query ID, look for it in the workspace API
					ProjectAPI.get(this.props.user.id, projectId, project => {
						if(project.queries) {
							const tmp = project.queries.find(nq => nq.query.id === queryId);
							if(tmp && tmp.query) {
								this.onReloadQueryData(tmp.query.collectionId, tmp.query);
							}
						}
					});
				}
			} else {
				//look if there is a default collection id configured
				collectionId = this.props.recipe.ingredients.collection;
			}
		} else if (activeCollectionId) {
            this.onReloadQueryData(activeCollectionId, false);
        }

        // Get projects list for current user
        ProjectAPI.list(this.props.user.id, null, projects => {
            this.setState({
                userProjects : projects || []
            })
        });

		//Always refresh the saved bookmarks on load, since they could have been updated in
		//either the workspace or the resource viewer
		this.loadBookmarks(this.state.activeProject);

		CollectionRegistryAPI.listCollections( collectionList => this.setState({collectionList}));

		if(!loadingFromWorkSpace) {
			this.onReloadQueryData(collectionId, initialQuery);
		}
	}

	onReloadQueryData = (collectionId, initialQuery) => {
		//load the collection config and finally
		if(collectionId) {
			CollectionUtil.generateCollectionConfig(
				this.props.clientId,
				this.props.user,
				collectionId,
				config => {
					if(!initialQuery) {
						initialQuery = Query.construct({size : this.state.pageSize}, config);
					}
					this.setState({
						collectionId : config.collectionId,
						collectionConfig : config,
						initialQuery : initialQuery,
						currentOutput : null
					});
				},
				true
			);
		}
	};

	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 controle 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);
        this.setState(
        	{
                collectionId: collectionConfig.collectionId,
                collectionConfig: collectionConfig,
                initialQuery: Query.construct({size: this.state.pageSize}, collectionConfig),
                currentOutput: null,
                browseSelection: false
            },
			() => {
				ComponentUtil.hideModal(this, 'showCollectionModal', 'collection__modal', true);
			}
        );
	};

	onProjectSelected = project => {
        this.loadBookmarks(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 ----------------------- */

	// current bookmarks per project
    loadBookmarks = project => {
    	if(!project || !project.id) return null;
        AnnotationAPI.getBookmarks(
            this.props.user.id,
            project.id,
            bookmarks => this.onLoadBookmarks(bookmarks, project)
        )
	};

	reloadBookmarks = ()=>{this.loadBookmarks(this.state.activeProject);};

	onLoadBookmarks = (bookmarks, 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.loadBookmarks(selectedProject);
		this.setState({
			selectedOnPage : {},
			allRowsSelected : false,
            browseSelection : false,
            savedBookmarkModal: true,
            activeProject: selectedProject
		}, () => {
			LocalStorageHandler.removeJSONByKeyInLocalStorage('stored-selections');
		})
	};

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

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

	onQuerySaved = data => {
        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(this.props.recipe.ingredients.resourceViewerPath && data.resourceId) {
			//FlexRouter.popupResourceViewer(this.props.recipe.ingredients.resourceViewerPath, data, query.term,SINGLE_SEARCH_RESOURCE_VIEWER_POPUP + (unique ? data.resourceId : ''),false, this.reloadBookmarks);
			FlexRouter.gotoResourceViewerOld(this.props.recipe.ingredients.resourceViewerPath, 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'
		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" 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}
            projects={userProjects}
            activeProject={activeProject}
            onOutput={this.onComponentOutput}
            user={user}
        />
    );

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


	renderCollectionBar = (collectionConfig, activeProject) => (
			<CollectionBar
				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="fa fa-remove" 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="fa 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) => {
		const loadingMessage = isSearching ? <LoadingSpinner 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}

				query={initialQuery || Query.construct(null, collectionConfig) }
				storeInitialQuery={true}

				collectionConfig={collectionConfig}
				onStartSearch={this.onStartSearch}
				onOutput={this.onComponentOutput}
			/>
		);

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

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

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

	renderTutorial = () => (
		<div className={IDUtil.cssClassName('tutorial',this.CLASS_PREFIX)}>
			 A detailed explanation and HowTos for this tool, can be found in the help menu <span onClick={this.showHelp}>?</span>
		</div>
	);

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

	render() {
		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.length > 0 ?
        	this.renderBookmarkModal(
        		this.state.collectionConfig,
        		this.state.activeProject,
        		this.state.userProjects
        	) : null;

        let header = null;
        if(this.state.userProjects !== null) {
            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.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
		) : null;

		//search result table with paging, sorting and ability to toggle a list of selected items
		const resultList = this.renderResultTable(this.state, storedSelectedRows, visitedResults);

		// show a short introduction/tutorial for this tool
		const tutorial = this.renderTutorial();

        //if the search API returned a paging out of bounds error, return a helpful message for the user
        const pagingOutOfBounds = this.state.isPagingOutOfBounds ? (
            <div className="col-md-9">
            	<br/>
                <div className="alert alert-danger">
                    {MessageHelper.renderPagingOutOfBoundsMessage(this.gotoPage.bind(this, undefined, 1))}
                </div>
            </div>
        ) : null;

		return (
			<div className={IDUtil.cssClassName('single-search-recipe')}>
				<div className="row">
					<div className="col-md-12">
						{header}
						{collectionBar}
						{searchComponent}
                        {resultList}
                        {pagingOutOfBounds}
					</div>
				</div>
                {tutorial}
				{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'
};
