import { MenuItemLocation } from '@joplin/lib/services/plugins/api/types';
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerService';
import { useContext, useEffect } from 'react';
import bridge from '../../../../../services/bridge';
import { ContextMenuOptions, ContextMenuItemType, buildMenuItems } from '../../../utils/contextMenuUtils';
import { menuItems } from '../../../utils/contextMenu';
import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
import CommandService from '@joplin/lib/services/CommandService';
import type { ContextMenuParams, Event as ElectronEvent, MenuItemConstructorOptions } from 'electron';

import Resource from '@joplin/lib/models/Resource';
import { TinyMceEditorEvents } from './types';
import { HtmlToMarkdownHandler, MarkupToHtmlHandler } from '../../../utils/types';
import { Editor } from 'tinymce';
import { EditDialogControl } from './useEditDialog';
import { Dispatch } from 'redux';
import { _ } from '@joplin/lib/locale';
import type { MenuItem as MenuItemType } from 'electron';
import isItemId from '@joplin/lib/models/utils/isItemId';
import { WindowIdContext } from '../../../../NewWindowOrIFrame';
import Logger from '@joplin/utils/Logger';

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

const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
const menuUtils = new MenuUtils(CommandService.instance());

interface ContextMenuActionOptions {
	current: ContextMenuOptions;
}

const contextMenuActionOptions: ContextMenuActionOptions = { current: null };

export default function(editor: Editor, plugins: PluginStates, dispatch: Dispatch, htmlToMd: HtmlToMarkdownHandler, mdToHtml: MarkupToHtmlHandler, editDialog: EditDialogControl) {
	const windowId = useContext(WindowIdContext);
	useEffect(() => {
		if (!editor) return () => {};

		const targetWindow = bridge().windowById(windowId);
		if (!targetWindow) return () => {};

		const contextMenuItems = menuItems(dispatch);

		const makeMainMenuItems = async (element: Element) => {
			let itemType: ContextMenuItemType = ContextMenuItemType.None;
			let resourceId = '';
			let linkUrl = null;

			const pathToId = (path: string) => {
				const id = Resource.pathToId(path);
				return isItemId(id) ? id : '';
			};

			if (element.nodeName === 'IMG') {
				itemType = ContextMenuItemType.Image;
				resourceId = pathToId((element as HTMLImageElement).src);
			} else if (element.nodeName === 'A') {
				resourceId = pathToId((element as HTMLAnchorElement).href);
				itemType = resourceId ? ContextMenuItemType.Resource : ContextMenuItemType.Link;
				linkUrl = element.getAttribute('href') || '';
			} else {
				itemType = ContextMenuItemType.Text;
			}

			contextMenuActionOptions.current = {
				itemType,
				resourceId,
				filename: null,
				mime: null,
				linkToCopy: linkUrl,
				linkToOpen: linkUrl,
				textToCopy: null,
				htmlToCopy: editor.selection ? editor.selection.getContent() : '',
				insertContent: (content: string) => {
					editor.insertContent(content);
				},
				isReadOnly: false,
				fireEditorEvent: (event: TinyMceEditorEvents) => {
					editor.fire(event);
				},
				htmlToMd,
				mdToHtml,
			};

			return buildMenuItems(contextMenuItems, contextMenuActionOptions.current);
		};

		const makeEditableMenuItems = (element: Element) => {
			if (editDialog.isEditable(element)) {
				return [
					new MenuItem({
						type: 'normal',
						label: _('Edit'),
						click: () => {
							editDialog.editExisting(element);
						},
					}),
					new MenuItem({ type: 'separator' }),
				];
			}
			return [];
		};

		const showContextMenu = async (element: HTMLElement, misspelledWord: string|null, dictionarySuggestions: string[]) => {
			const menu = new Menu();
			const menuItems: MenuItemType[] = [];
			const toMenuItems = (specs: MenuItemConstructorOptions[]) => {
				return specs.map(spec => new MenuItem(spec));
			};

			menuItems.push(...makeEditableMenuItems(element));
			menuItems.push(...(await makeMainMenuItems(element)));
			const spellCheckerMenuItems = SpellCheckerService.instance().contextMenuItems(misspelledWord, dictionarySuggestions);
			menuItems.push(
				...toMenuItems(spellCheckerMenuItems),
			);
			menuItems.push(
				...toMenuItems(menuUtils.pluginContextMenuItems(plugins, MenuItemLocation.EditorContextMenu)),
			);

			for (const item of menuItems) {
				menu.append(item);
			}
			menu.popup({ window: targetWindow });
		};

		let lastTarget: EventTarget|null = null;
		const onElectronContextMenu = async (event: ElectronEvent, params: ContextMenuParams) => {
			if (!lastTarget) return;
			const element = lastTarget as HTMLElement;
			lastTarget = null;

			event.preventDefault();
			await showContextMenu(element, params.misspelledWord, params.dictionarySuggestions);
		};

		const onBrowserContextMenu = async (event: PointerEvent) => {
			const isKeyboard = event.buttons === 0;
			if (isKeyboard) {
				// Context menu events from the keyboard seem to always use <body> as the
				// event target. Since which context menu is displayed depends on what the
				// target is, using event.target for keyboard-triggered contextmenu events
				// would prevent keyboard-only users from accessing certain functionality.
				// To fix this, use the selection instead.
				lastTarget = editor.selection.getNode();
			} else {
				lastTarget = event.target;
			}

			// Plugins in the Rich Text Editor (e.g. the mermaid renderer) can sometimes
			// create custom right-click events. These don't trigger the Electron 'context-menu'
			// event. As such, the context menu must be shown manually.
			const isFromPlugin = !event.isTrusted;
			if (isFromPlugin) {
				event.preventDefault();
				await showContextMenu(lastTarget as HTMLElement, null, []);
				lastTarget = null;
			}
		};

		try {
			targetWindow.webContents.prependListener('context-menu', onElectronContextMenu);
		} catch (error) {
			logger.error('Failed to register context menu', error);
		}
		editor.on('contextmenu', onBrowserContextMenu);

		return () => {
			editor.off('contextmenu', onBrowserContextMenu);

			try {
				if (!targetWindow.isDestroyed() && targetWindow?.webContents?.off) {
					targetWindow.webContents.off('context-menu', onElectronContextMenu);
				}
			} catch (error) {
				// This can happen if the window closes after the isDestroyed check, but before webContents.off
				// finishes running.
				logger.error('Error removing context menu listener', error);
			}
		};
	}, [editor, plugins, dispatch, htmlToMd, mdToHtml, editDialog, windowId]);
}
