import * as React from 'react';
import { AppState } from '../app.reducer';
import CommandService, { SearchResult as CommandSearchResult } from '@joplin/lib/services/CommandService';
import KeymapService from '@joplin/lib/services/KeymapService';
const { connect } = require('react-redux');
import { _ } from '@joplin/lib/locale';
import { themeStyle } from '@joplin/lib/theme';
import SearchEngine, { ComplexTerm } from '@joplin/lib/services/search/SearchEngine';
import gotoAnythingStyleQuery from '@joplin/lib/services/search/gotoAnythingStyleQuery';
import BaseModel, { ModelType } from '@joplin/lib/BaseModel';
import Tag from '@joplin/lib/models/Tag';
import Folder from '@joplin/lib/models/Folder';
import Note from '@joplin/lib/models/Note';
import ItemList from '../gui/ItemList';
import HelpButton from '../gui/HelpButton';
import { surroundKeywords, nextWhitespaceIndex, removeDiacritics, escapeRegExp, KeywordType } from '@joplin/lib/string-utils';
import { mergeOverlappingIntervals } from '@joplin/lib/ArrayUtils';
import markupLanguageUtils from '@joplin/lib/utils/markupLanguageUtils';
import focusEditorIfEditorCommand from '@joplin/lib/services/commands/focusEditorIfEditorCommand';
import Logger from '@joplin/utils/Logger';
import { MarkupLanguage, MarkupToHtml } from '@joplin/renderer';
import Resource from '@joplin/lib/models/Resource';
import { NoteEntity, ResourceEntity } from '@joplin/lib/services/database/types';
import Dialog from '@joplin/lib/components/Dialog';
import AsyncActionQueue from '@joplin/lib/AsyncActionQueue';

const logger = Logger.create('GotoAnything');

const PLUGIN_NAME = 'gotoAnything';

interface GotoAnythingSearchResult {
	id: string;
	title: string;
	parent_id: string;
	fields: string[];
	fragments?: string;
	path?: string;
	type?: number;
	item_id?: string;
	item_type?: ModelType;
}

// GotoAnything supports several modes:
//
// - Default: Search in note title, body. Can search for folders, tags, etc. This is the full
//   featured GotoAnything.
//
// - TitleOnly: Search in note titles only.
//
// These different modes can be set from the `gotoAnything` command.

export enum Mode {
	Default = 0,
	TitleOnly,
}

export interface UserDataCallbackEvent {
	type: ModelType;
	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
	item: any;
}

export type UserDataCallbackResolve = (event: UserDataCallbackEvent)=> void;
export type UserDataCallbackReject = (error: Error)=> void;
export interface UserDataCallback {
	resolve: UserDataCallbackResolve;
	reject: UserDataCallbackReject;
}

export interface GotoAnythingUserData {
	startString?: string;
	mode?: Mode;
	callback?: UserDataCallback;
}

interface Props {
	themeId: number;
	// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
	dispatch: Function;
	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
	folders: any[];
	showCompletedTodos: boolean;
	userData: GotoAnythingUserData;
}

interface State {
	query: string;
	results: GotoAnythingSearchResult[];
	selectedItemId: string;
	keywords: (string | ComplexTerm)[];
	listType: number;
	showHelp: boolean;
	resultsInBody: boolean;
	commandArgs: string[];
}

interface CommandQuery {
	name: string;
	args: string[];
}

const getContentMarkupLanguageAndBody = (result: GotoAnythingSearchResult, notesById: Record<string, NoteEntity>, resources: ResourceEntity[]) => {
	if (result.item_type === ModelType.Resource) {
		const resource = resources.find(r => r.id === result.item_id);
		if (!resource) {
			logger.warn('Could not find resources associated with result:', result);
			return { markupLanguage: MarkupLanguage.Markdown, content: '' };
		} else {
			return { markupLanguage: MarkupLanguage.Markdown, content: resource.ocr_text };
		}
	} else { // a note
		const note = notesById[result.id];
		return { markupLanguage: note.markup_language, content: note.body };
	}
};

// A result row contains an `id` property (the note ID) and, if the current row
// is a resource, an `item_id` property, which is the resource ID. In that case,
// the row also has an `id` property, which is the note that contains the
// resource.
//
// It means a result set may include multiple results with the same `id`
// property, if it contains one or more resources that are in a note that's
// already in the result set. For that reason, when we need a unique ID for the
// result, we use this function - which returns either the item_id, if present,
// or the note ID.
const getResultId = (result: GotoAnythingSearchResult) => {
	// This ID used as a DOM ID for accessibility purposes, so it is prefixed to prevent
	// name collisions.
	return `goto-anything-result-${result.item_id ? result.item_id : result.id}`;
};

const itemListId = 'goto-anything-item-list';

class GotoAnything {

	// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
	public dispatch: Function;
	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
	public static Dialog: any;
	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
	public static manifest: any;

	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
	public onTrigger(event: any) {
		this.dispatch({
			type: 'PLUGINLEGACY_DIALOG_SET',
			open: true,
			pluginName: PLUGIN_NAME,
			userData: event.userData,
		});
	}

}

class DialogComponent extends React.PureComponent<Props, State> {

	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
	private styles_: any;
	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
	private inputRef: any;
	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
	private itemListRef: any;
	private listUpdateQueue_: AsyncActionQueue;
	private markupToHtml_: MarkupToHtml;
	private userCallback_: UserDataCallback|null = null;
	private mode_: Mode;

	public constructor(props: Props) {
		super(props);

		const startString = props?.userData?.startString ? props?.userData?.startString : '';

		this.userCallback_ = props?.userData?.callback;
		this.listUpdateQueue_ = new AsyncActionQueue(100);

		this.mode_ = props?.userData?.mode ? props.userData.mode : Mode.Default;

		this.state = {
			query: startString,
			results: [],
			selectedItemId: null,
			keywords: [],
			listType: BaseModel.TYPE_NOTE,
			showHelp: false,
			resultsInBody: false,
			commandArgs: [],
		};

		this.styles_ = {};

		this.inputRef = React.createRef();
		this.itemListRef = React.createRef();

		this.input_onChange = this.input_onChange.bind(this);
		this.input_onKeyDown = this.input_onKeyDown.bind(this);
		this.renderItem = this.renderItem.bind(this);
		this.listItem_onClick = this.listItem_onClick.bind(this);
		this.helpButton_onClick = this.helpButton_onClick.bind(this);

		if (startString) this.scheduleListUpdate();
	}

	public style() {
		const styleKey = [this.props.themeId, this.state.listType, this.state.resultsInBody ? '1' : '0'].join('-');

		if (this.styles_[styleKey]) return this.styles_[styleKey];

		const theme = themeStyle(this.props.themeId);

		let itemHeight = this.state.resultsInBody ? 84 : 64;

		if (this.state.listType === BaseModel.TYPE_COMMAND) {
			itemHeight = 40;
		}

		this.styles_[styleKey] = {
			dialogBox: { ...theme.dialogBox, minWidth: '50%', maxWidth: '50%' },
			input: { ...theme.inputStyle, flex: 1 },
			row: {
				overflow: 'hidden',
				height: itemHeight,
				display: 'flex',
				justifyContent: 'center',
				flexDirection: 'column',
				paddingLeft: 10,
				paddingRight: 10,
				borderBottomWidth: 1,
				borderBottomStyle: 'solid',
				borderBottomColor: theme.dividerColor,
				boxSizing: 'border-box',
			},
			inputHelpWrapper: { display: 'flex', flexDirection: 'row', alignItems: 'center' },
		};

		delete this.styles_[styleKey].dialogBox.maxHeight;

		const rowTextStyle = {
			fontSize: theme.fontSize,
			color: theme.color,
			fontFamily: theme.fontFamily,
			whiteSpace: 'nowrap',
			opacity: 0.7,
			userSelect: 'none',
		};

		const rowTitleStyle = { ...rowTextStyle, fontSize: rowTextStyle.fontSize * 1.4,
			marginBottom: this.state.resultsInBody ? 6 : 4,
			color: theme.colorFaded };

		const rowFragmentsStyle = { ...rowTextStyle, fontSize: rowTextStyle.fontSize * 1.2,
			marginBottom: this.state.resultsInBody ? 8 : 6,
			color: theme.colorFaded };

		this.styles_[styleKey].rowSelected = { ...this.styles_[styleKey].row, backgroundColor: theme.selectedColor };
		this.styles_[styleKey].rowPath = rowTextStyle;
		this.styles_[styleKey].rowTitle = rowTitleStyle;
		this.styles_[styleKey].rowFragments = rowFragmentsStyle;
		this.styles_[styleKey].itemHeight = itemHeight;

		return this.styles_[styleKey];
	}

	public componentDidMount() {
		this.props.dispatch({
			type: 'VISIBLE_DIALOGS_ADD',
			name: 'gotoAnything',
		});
	}

	public componentWillUnmount() {
		void this.listUpdateQueue_.reset();

		this.props.dispatch({
			type: 'VISIBLE_DIALOGS_REMOVE',
			name: 'gotoAnything',
		});
	}

	private modalLayer_onDismiss = () => {
		this.props.dispatch({
			pluginName: PLUGIN_NAME,
			type: 'PLUGINLEGACY_DIALOG_SET',
			open: false,
		});
	};

	private helpButton_onClick() {
		this.setState({ showHelp: !this.state.showHelp });
	}

	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
	private input_onChange(event: any) {
		this.setState({ query: event.target.value });

		this.scheduleListUpdate();
	}

	public scheduleListUpdate() {
		this.listUpdateQueue_.push(() => this.updateList());
	}

	public async keywords(searchQuery: string) {
		const parsedQuery = await SearchEngine.instance().parseQuery(searchQuery);
		return SearchEngine.instance().allParsedQueryTerms(parsedQuery);
	}

	public markupToHtml() {
		if (this.markupToHtml_) return this.markupToHtml_;
		this.markupToHtml_ = markupLanguageUtils.newMarkupToHtml();
		return this.markupToHtml_;
	}

	private parseCommandQuery(query: string): CommandQuery {
		const fullQuery = query;
		const splitted = fullQuery.split(/\s+/);
		return {
			name: splitted.length ? splitted[0] : '',
			args: splitted.slice(1),
		};
	}

	public async updateList() {
		let resultsInBody = false;

		if (!this.state.query) {
			this.setState({ results: [], keywords: [] });
		} else {
			let results: GotoAnythingSearchResult[] = [];
			let listType = null;
			let searchQuery = '';
			let keywords = null;
			let commandArgs: string[] = [];

			if (this.state.query.indexOf(':') === 0) { // COMMANDS
				const commandQuery = this.parseCommandQuery(this.state.query.substr(1));

				listType = BaseModel.TYPE_COMMAND;
				keywords = [commandQuery.name];
				commandArgs = commandQuery.args;

				const commandResults = CommandService.instance().searchCommands(commandQuery.name, true);

				results = commandResults.map((result: CommandSearchResult) => {
					return {
						id: result.commandName,
						title: result.title,
						parent_id: null as string,
						fields: [] as string[],
						type: BaseModel.TYPE_COMMAND,
					};
				});
			} else if (this.state.query.indexOf('#') === 0) { // TAGS
				listType = BaseModel.TYPE_TAG;
				searchQuery = `*${this.state.query.split(' ')[0].substr(1).trim()}*`;
				results = await Tag.searchAllWithNotes({ titlePattern: searchQuery });
			} else if (this.state.query.indexOf('@') === 0) { // FOLDERS
				listType = BaseModel.TYPE_FOLDER;
				searchQuery = this.state.query.substr(1).trim();
				const normalizedSearchQuery = removeDiacritics(searchQuery).toLowerCase();

				results = [];
				for (const folder of this.props.folders) {
					if (folder.deleted_time) continue;

					const normalizedTitle = removeDiacritics(folder.title).toLowerCase();
					if (normalizedSearchQuery && normalizedTitle.indexOf(normalizedSearchQuery) < 0) continue;

					const path = Folder.folderPathString(this.props.folders, folder.parent_id);
					results.push({ ...folder, path: path ? path : '/' });
				}
			} else { // Note TITLE or BODY
				listType = BaseModel.TYPE_NOTE;
				searchQuery = gotoAnythingStyleQuery(this.state.query);
				// SearchEngine returns the title normalized, that is why we need to
				// override this field below with the original title
				// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
				results = (await SearchEngine.instance().search(searchQuery)) as any[];

				// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
				resultsInBody = !!results.find((row: any) => row.fields.includes('body'));

				if (this.mode_ === Mode.TitleOnly) {
					resultsInBody = false;
					results = results.filter(r => {
						return r.fields.includes('title');
					});
				}

				const resourceIds = results.filter(r => r.item_type === ModelType.Resource).map(r => r.item_id);
				const resources = await Resource.resourceOcrTextsByIds(resourceIds);

				if (!resultsInBody || this.state.query.length <= 1) {
					// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
					const notes = await Note.byIds(results.map((result: any) => result.id), { fields: ['id', 'title'] });
					for (let i = 0; i < results.length; i++) {
						const row = results[i];
						const path = Folder.folderPathString(this.props.folders, row.parent_id);
						const originalNote = notes.find(note => note.id === row.id);
						results[i] = { ...row, path: path, title: originalNote.title };
					}
				} else {
					const limit = 20;

					// Note: any filtering must be done **before** fetching the notes, because we're
					// going to apply a limit to the number of fetched notes.
					// https://github.com/laurent22/joplin/issues/9944
					if (!this.props.showCompletedTodos) {
						// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
						results = results.filter((row: any) => !row.is_todo || !row.todo_completed);
					}

					// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
					const notes = await Note.byIds(results.map((result: any) => result.id).slice(0, limit), { fields: ['id', 'body', 'markup_language', 'is_todo', 'todo_completed', 'title'] });
					// Can't make any sense of this code so...
					// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
					const notesById = notes.reduce((obj, { id, body, markup_language, title }) => ((obj[[id] as any] = { id, body, markup_language, title }), obj), {});

					// Filter out search results that are associated with non-existing notes.
					// https://github.com/laurent22/joplin/issues/5417
					results = results.filter(r => !!notesById[r.id])
						.map(r => ({ ...r, title: notesById[r.id].title }));

					const keywordRegexes = (await this.keywords(searchQuery)).map(term => {
						if (typeof term === 'string') {
							return new RegExp(escapeRegExp(term), 'ig');
						} else if (term.valueRegex) {
							return new RegExp(removeDiacritics(term.valueRegex), 'ig');
						} else {
							return new RegExp(escapeRegExp(term.value), 'ig');
						}
					});

					for (let i = 0; i < results.length; i++) {
						const row = results[i];
						const path = Folder.folderPathString(this.props.folders, row.parent_id);

						if (row.fields.includes('body')) {
							let fragments = '...';

							const loadFragments = (markupLanguage: MarkupLanguage, content: string) => {
								const indices = [];
								const body = this.markupToHtml().stripMarkup(markupLanguage, content, { collapseWhiteSpaces: true });
								const normalizedBody = removeDiacritics(body);

								// Iterate over all matches in the body for each search keyword
								for (const keywordRegex of keywordRegexes) {
									for (const match of normalizedBody.matchAll(keywordRegex)) {
										// Populate 'indices' with [begin index, end index] of each note fragment
										// Begins at the regex matching index, ends at the next whitespace after seeking 15 characters to the right
										indices.push([match.index, nextWhitespaceIndex(body, match.index + match[0].length + 15)]);
										if (indices.length > 20) break;
									}
								}

								// Merge multiple overlapping fragments into a single fragment to prevent repeated content
								// e.g. 'Joplin is a free, open source' and 'open source note taking application'
								// will result in 'Joplin is a free, open source note taking application'
								const mergedIndices = mergeOverlappingIntervals(indices, 3);
								// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
								fragments = mergedIndices.map((f: any) => body.slice(f[0], f[1])).join(' ... ');
								// Add trailing ellipsis if the final fragment doesn't end where the note is ending
								if (mergedIndices.length && mergedIndices[mergedIndices.length - 1][1] !== body.length) fragments += ' ...';
							};

							if (i < limit) { // Display note fragments of search keyword matches
								const { markupLanguage, content } = getContentMarkupLanguageAndBody(
									row,
									notesById,
									resources,
								);

								// Don't load fragments for long notes -- doing so can lead to UI freezes.
								if (content.length < 100_000) {
									loadFragments(markupLanguage, content);
								}
							}

							results[i] = { ...row, path, fragments };
						} else {
							results[i] = { ...row, path: path, fragments: '' };
						}
					}
				}
			}

			// make list scroll to top in every search
			this.makeItemIndexVisible(0);

			const keywordsWithoutEmptyString = keywords?.filter(v => !!v);

			this.setState({
				listType: listType,
				results: results,
				keywords: keywordsWithoutEmptyString ? keywordsWithoutEmptyString : await this.keywords(searchQuery),
				selectedItemId: results.length === 0 ? null : getResultId(results[0]),
				resultsInBody: resultsInBody,
				commandArgs: commandArgs,
			});
		}
	}

	private makeItemIndexVisible(index: number) {
		// Looks like it's not always defined
		// https://github.com/laurent22/joplin/issues/5184#issuecomment-879714850
		if (!this.itemListRef || !this.itemListRef.current) {
			logger.warn('Trying to set item index but the item list is not defined. Index: ', index);
			return;
		}

		this.itemListRef.current.makeItemIndexVisible(index);
	}

	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
	public async gotoItem(item: any) {
		this.props.dispatch({
			pluginName: PLUGIN_NAME,
			type: 'PLUGINLEGACY_DIALOG_SET',
			open: false,
		});

		if (this.userCallback_) {
			logger.info('gotoItem: user callback', item);

			this.userCallback_.resolve({
				type: this.state.listType,
				item: { ...item },
			});
			return;
		}

		if (item.type === BaseModel.TYPE_COMMAND) {
			logger.info('gotoItem: execute command', item);
			void CommandService.instance().execute(item.id, ...item.commandArgs);
			void focusEditorIfEditorCommand(item.id, CommandService.instance());
			return;
		}

		if (this.state.listType === BaseModel.TYPE_NOTE || this.state.listType === BaseModel.TYPE_FOLDER) {
			const folderPath = await Folder.folderPath(this.props.folders, item.parent_id);

			for (const folder of folderPath) {
				this.props.dispatch({
					type: 'FOLDER_SET_COLLAPSED',
					id: folder.id,
					collapsed: false,
				});
			}
		}

		if (this.state.listType === BaseModel.TYPE_NOTE) {
			logger.info('gotoItem: note', item);

			this.props.dispatch({
				type: 'FOLDER_AND_NOTE_SELECT',
				folderId: item.parent_id,
				noteId: item.id,
			});

			CommandService.instance().scheduleExecute('focusElement', 'noteBody');
		} else if (this.state.listType === BaseModel.TYPE_TAG) {
			logger.info('gotoItem: tag', item);

			this.props.dispatch({
				type: 'TAG_SELECT',
				id: item.id,
			});
		} else if (this.state.listType === BaseModel.TYPE_FOLDER) {
			logger.info('gotoItem: folder', item);

			this.props.dispatch({
				type: 'FOLDER_SELECT',
				id: item.id,
			});
		}
	}

	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
	private listItem_onClick(event: any) {
		const targetResultId = event.currentTarget.getAttribute('id');
		void this.gotoItem(this.selectedItem(targetResultId));
	}

	public renderItem(item: GotoAnythingSearchResult, index: number) {
		const theme = themeStyle(this.props.themeId);
		const style = this.style();
		const resultId = getResultId(item);
		const isSelected = resultId === this.state.selectedItemId;
		const rowStyle = isSelected ? style.rowSelected : style.row;

		const wrapKeywordMatches = (unescapedContent: string) => {
			return surroundKeywords(
				this.state.keywords as KeywordType,
				unescapedContent,
				`<span class="match-highlight" style="font-weight: bold; color: ${theme.searchMarkerColor}; background-color: ${theme.searchMarkerBackgroundColor}">`,
				'</span>',
				{ escapeHtml: true },
			);
		};

		const titleHtml = wrapKeywordMatches(item.title);

		const fragmentsHtml = !item.fragments ? null : wrapKeywordMatches(item.fragments);

		const folderIcon = <i style={{ fontSize: theme.fontSize, marginRight: 2 }} className="fa fa-book" role='img' aria-label={_('Notebook')} />;
		const pathComp = !item.path ? null : <div style={style.rowPath}>{folderIcon} {item.path}</div>;
		const fragmentComp = !fragmentsHtml ? null : <div style={style.rowFragments} dangerouslySetInnerHTML={{ __html: (fragmentsHtml) }}></div>;

		return (
			<div
				key={resultId}
				className={isSelected ? 'selected' : null}
				style={rowStyle}
				onClick={this.listItem_onClick}

				data-id={item.id}
				data-parent-id={item.parent_id}
				data-type={item.type}

				role='option'
				id={resultId}
				aria-posinset={index + 1}
			>
				<div style={style.rowTitle} dangerouslySetInnerHTML={{ __html: titleHtml }}></div>
				{this.mode_ === Mode.TitleOnly ? null : fragmentComp}
				{this.mode_ === Mode.TitleOnly ? null : pathComp}
			</div>
		);
	}

	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
	public selectedItemIndex(results: any[] = undefined, itemId: string = undefined) {
		if (typeof results === 'undefined') results = this.state.results;
		if (typeof itemId === 'undefined') itemId = this.state.selectedItemId;
		for (let i = 0; i < results.length; i++) {
			const r = results[i];
			if (getResultId(r) === itemId) return i;
		}
		return -1;
	}

	public selectedItem(itemId: string = undefined) {
		const index = this.selectedItemIndex(undefined, itemId);
		if (index < 0) return null;
		return { ...this.state.results[index], commandArgs: this.state.commandArgs };
	}

	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
	private input_onKeyDown(event: any) {
		const keyCode = event.keyCode;

		if (this.state.results.length > 0 && (keyCode === 40 || keyCode === 38)) { // DOWN / UP
			event.preventDefault();

			const inc = keyCode === 38 ? -1 : +1;
			let index = this.selectedItemIndex();
			if (index < 0) return; // Not possible, but who knows

			index += inc;
			if (index < 0) index = 0;
			if (index >= this.state.results.length) index = this.state.results.length - 1;

			const newId = getResultId(this.state.results[index]);

			this.makeItemIndexVisible(index);

			this.setState({ selectedItemId: newId });
		}

		if (keyCode === 13) { // ENTER
			event.preventDefault();

			const item = this.selectedItem();
			if (!item) return;

			void this.gotoItem(item);
		}
	}

	private calculateMaxHeight(itemHeight: number) {
		const listContainer: HTMLElement|null = this.itemListRef.current?.container;
		const containerWindow = listContainer?.ownerDocument?.defaultView ?? window;
		const maxItemCount = Math.floor((0.7 * containerWindow.innerHeight) / itemHeight);
		return maxItemCount * itemHeight;
	}

	public renderList() {
		const style = this.style();

		const itemListStyle = {
			marginTop: 5,
			height: Math.min(style.itemHeight * this.state.results.length, this.calculateMaxHeight(style.itemHeight)),
		};

		return (
			<ItemList
				ref={this.itemListRef}
				id={itemListId}
				role='listbox'
				aria-label={_('Search results')}
				itemHeight={style.itemHeight}
				items={this.state.results}
				style={itemListStyle}
				itemRenderer={this.renderItem}
			/>
		);
	}

	private helpText() {
		if (this.mode_ === Mode.TitleOnly) {
			return _('Type a note title to search for it.');
		} else {
			return _('Type a note title or part of its content to jump to it. Or type # followed by a tag name, or @ followed by a notebook name. Or type : to search for commands.');
		}
	}

	public render() {
		const style = this.style();
		const helpTextId = 'goto-anything-help-text';
		const helpComp = (
			<div
				className='help-text'
				aria-live='polite'
				id={helpTextId}
				style={style.help}
				hidden={!this.state.showHelp}
			>{this.helpText()}</div>
		);

		return (
			<Dialog className='go-to-anything-dialog' onCancel={this.modalLayer_onDismiss} contentStyle={style.dialogBox}>
				{helpComp}
				<div style={style.inputHelpWrapper}>
					<input
						autoFocus
						type='text'
						style={style.input}
						ref={this.inputRef}
						value={this.state.query}
						onChange={this.input_onChange}
						onKeyDown={this.input_onKeyDown}

						aria-describedby={helpTextId}
						aria-autocomplete='list'
						aria-controls={itemListId}
						aria-activedescendant={this.state.selectedItemId}
					/>
					<HelpButton
						onClick={this.helpButton_onClick}
						aria-controls={helpTextId}
						aria-expanded={this.state.showHelp}
					/>
				</div>
				{this.renderList()}
			</Dialog>
		);
	}

}

const mapStateToProps = (state: AppState) => {
	return {
		folders: state.folders,
		themeId: state.settings.theme,
		showCompletedTodos: state.settings.showCompletedTodos,
		highlightedWords: state.highlightedWords,
	};
};

GotoAnything.Dialog = connect(mapStateToProps)(DialogComponent);

GotoAnything.manifest = {

	name: PLUGIN_NAME,
	menuItems: [
		{
			id: 'gotoAnything',
			name: 'main',
			parent: 'go',
			label: _('Goto Anything...'),
			accelerator: () => KeymapService.instance().getAccelerator('gotoAnything'),
			screens: ['Main'],
		},
		{
			id: 'commandPalette',
			name: 'main',
			parent: 'tools',
			label: _('Command palette'),
			accelerator: () => KeymapService.instance().getAccelerator('commandPalette'),
			screens: ['Main'],
			userData: {
				startString: ':',
			},
		},
		{
			id: 'controlledApi',
		},
	],

};

export default GotoAnything;
