 
import { Plugin } from '@ckeditor/ckeditor5-core';
import { ClickObserver } from '@ckeditor/ckeditor5-engine';
import { ButtonView, ContextualBalloon, clickOutsideHandler } from '@ckeditor/ckeditor5-ui';
import tooltipIcon from '../../themes/icons/tooltip.svg';
import ActionsView from './ui/actionsview';
import { isTooltipElement, isImageAllowed, TOOLTIP_COMMANDS,  HTML_ATTRIBUTE, HTML_CLASS, TOOLTIP_ATTRIBUTE, TOOLTIP_CUSTOM_PROPERTY} from './utils';
import * as QMSCKEditorConstant from '../common/qmsCKEditorConstant';
import * as QMSCKEditorService from '../common/qmsCKEditorService';

const VISUAL_SELECTION_MARKER_NAME = 'tooltip-ui';
 
export default class TooltipUI extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ ContextualBalloon ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'TooltipUI';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;

		editor.editing.view.addObserver(ClickObserver);

		/**
		 * The actions view displayed inside of the balloon.
		 *
		 * @member {module:link/ui/TooltipActionView~TooltipActionView}
		 */
		this.actionsView = this._createActionsView();
 
		/**
		 * The contextual balloon plugin instance.
		 *
		 * @private
		 * @member {module:ui/panel/balloon/contextualballoon~ContextualBalloon}
		 */
		this._balloon = editor.plugins.get(ContextualBalloon);

		// Create toolbar buttons.
		this._createToolbarLinkButton();

		// Attach lifecycle actions to the the balloon.
		this._enableUserBalloonInteractions();

		// Renders a fake visual selection marker on an expanded selection.
		editor.conversion.for('editingDowncast').markerToHighlight({
			model: VISUAL_SELECTION_MARKER_NAME,
			view: {
				classes: [ 'ck-fake-link-selection' ]
			}
		});

		// Renders a fake visual selection marker on a collapsed selection.
		editor.conversion.for('editingDowncast').markerToElement({
			model: VISUAL_SELECTION_MARKER_NAME,
			view: {
				name: 'span',
				classes: [ 'ck-fake-link-selection', 'ck-fake-link-selection_collapsed' ]
			}
		});
	}

	/**
	 * @inheritDoc
	 */
	destroy() {
		super.destroy();
	}

	/**
	 * Creates the {@link module:link/ui/TooltipActionView~TooltipActionView} instance.
	 *
	 * @private
	 * @returns {module:link/ui/TooltipActionView~TooltipActionView} The link actions view instance.
	 */
	_createActionsView() {
		const editor = this.editor;
		const actionsView = new ActionsView(editor.locale);
		const tooltipDialogCommand = editor.commands.get(TOOLTIP_COMMANDS.TOOLTIP_DIALOG);
		const removeTooltipCommand = editor.commands.get(TOOLTIP_COMMANDS.REMOVE_TOOLTIP);

		actionsView.editButtonView.bind('isEnabled').to(tooltipDialogCommand);
		actionsView.removeButtonView.bind('isEnabled').to(removeTooltipCommand);

		// Execute unlink command after clicking on the "Edit" button.
		this.listenTo(actionsView, 'edit', () => {
			const tooltip = { text: '', content: '' };
			const selectedElement = this._getSelectedTooltipElement();
			if(selectedElement){
				tooltip.content = selectedElement.getAttribute(HTML_ATTRIBUTE) || '';
				if(selectedElement.childCount > 0){
					tooltip.text = selectedElement.getChild(0).data;
				}
			}
			QMSCKEditorService.tooltipNotify(tooltip);
			window[QMSCKEditorConstant.QMSCK_TOOLTIP_IS_PROCESSING] = false;
			window.addEventListener(QMSCKEditorConstant.QMSCK_TOOLTIP_PLUGIN_RESP, (evt) => {
				if (window[QMSCKEditorConstant.QMSCK_TOOLTIP_IS_PROCESSING]) { 
					return; 
				}
				window[QMSCKEditorConstant.QMSCK_TOOLTIP_IS_PROCESSING] = true;
				const options = evt.detail;
				editor.commands.get(TOOLTIP_COMMANDS.TOOLTIP).execute(options);
			}, { once: true });
			this._hideUI();
		});

		// Execute unlink command after clicking on the "Unlink" button.
		this.listenTo(actionsView, 'remove', () => {
			editor.execute(TOOLTIP_COMMANDS.REMOVE_TOOLTIP);
			this._hideUI();
		});

		// Close the panel on esc key press when the **actions have focus**.
		actionsView.keystrokes.set('Esc', (data, cancel) => {
			this._hideUI();
			cancel();
		});
 
		return actionsView;
	}
 
	/**
	 * Creates a toolbar Link button. Clicking this button will show
	 * a {@link #_balloon} attached to the selection.
	 *
	 * @private
	 */
	_createToolbarLinkButton() {
		const editor = this.editor;
		const command = editor.commands.get(TOOLTIP_COMMANDS.TOOLTIP_DIALOG);
		const t = editor.t;
 
		editor.ui.componentFactory.add('tooltip', locale => {
			const button = new ButtonView(locale);

			button.isEnabled = true;
			button.label = t('Tooltip');
			button.icon = tooltipIcon;
			button.tooltip = true;
			button.isToggleable = true;

			// Bind button to the command.
			button.bind('isEnabled').to(command, 'isEnabled');
			button.bind('isOn').to(command, 'value', value => !!value);

			// Show the panel on button click.
			button.on('execute', () => {
				editor.execute(TOOLTIP_COMMANDS.TOOLTIP_DIALOG);
            });

			return button;
		});
	}
 
	_enableUserBalloonInteractions() {
		const viewDocument = this.editor.editing.view.document;

		// Handle click on view document and show panel when selection is placed inside the link element.
		// Keep panel open until selection will be inside the same link element.
		this.listenTo(viewDocument, 'click', () => {
			const parentLink = this._getSelectedTooltipElement();

			if (parentLink) {
				// Then show panel but keep focus inside editor editable.
				this._showUI();
			}
		});

		// Focus the form if the balloon is visible and the Tab key has been pressed.
		this.editor.keystrokes.set('Tab', (data, cancel) => {
			if (this._areActionsVisible && !this.actionsView.focusTracker.isFocused) {
				this.actionsView.focus();
				cancel();
			}
		}, {
			// Use the high priority because the link UI navigation is more important
			// than other feature's actions, e.g. list indentation.
			// https://github.com/ckeditor/ckeditor5-link/issues/146
			priority: 'high'
		});

		// Close the panel on the Esc key press when the editable has focus and the balloon is visible.
		this.editor.keystrokes.set('Esc', (data, cancel) => {
			if (this._isUIVisible) {
				this._hideUI();
				cancel();
			}
		});

		// Close on click outside of balloon panel element.
		clickOutsideHandler({
			emitter: this.actionsView,
			activator: () => this._isUIInPanel,
			contextElements: [ this._balloon.view.element ],
			callback: () => this._hideUI()
		});
	}

	/**
	 * Adds the {@link #actionsView} to the {@link #_balloon}.
	 *
	 * @protected
	 */
	_addActionsView() {
		if (this._areActionsInPanel) {
			return;
		}

		this._balloon.add({
			view: this.actionsView,
			position: this._getBalloonPositionData()
		});
	}
 
  
	_showUI(forceVisible = false) {
		// When there's no link under the selection, go straight to the editing UI.
		if (!this._getSelectedTooltipElement()) {
			// Show visual selection on a text without a link when the contextual balloon is displayed.
			// See https://github.com/ckeditor/ckeditor5/issues/4721.
			this._showFakeVisualSelection();

			this._addActionsView();

			// Be sure panel with link is visible.
			if (forceVisible) {
				this._balloon.showStack('main');
			}
 
		}
		// If there's a link under the selection...
		else {
			this._addActionsView();

			// Be sure panel with link is visible.
			if (forceVisible) {
				this._balloon.showStack('main');
			}
		}

		// Begin responding to ui#update once the UI is added.
		this._startUpdatingUI();
	}

	_hideUI() {
		if (!this._isUIInPanel) {
			return;
		}

		const editor = this.editor;

		this.stopListening(editor.ui, 'update');
		this.stopListening(this._balloon, 'change:visibleView');

		// Make sure the focus always gets back to the editable _before_ removing the focused form view.
		// Doing otherwise causes issues in some browsers. See https://github.com/ckeditor/ckeditor5-link/issues/193.
		editor.editing.view.focus();
 
		// Then remove the actions view because it's beneath the form.
		this._balloon.remove(this.actionsView);

		this._hideFakeVisualSelection();
	}

	/**
	 * Makes the UI react to the {@link module:core/editor/editorui~EditorUI#event:update} event to
	 * reposition itself when the editor UI should be refreshed.
	 *
	 * See: {@link #_hideUI} to learn when the UI stops reacting to the `update` event.
	 *
	 * @protected
	 */
	_startUpdatingUI() {
		const editor = this.editor;
		const viewDocument = editor.editing.view.document;

		let prevSelectedLink = this._getSelectedTooltipElement();
		let prevSelectionParent = getSelectionParent();

		const update = () => {
			const selectedLink = this._getSelectedTooltipElement();
			const selectionParent = getSelectionParent();

			// Hide the panel if:
			//
			// * the selection went out of the EXISTING link element. E.g. user moved the caret out
			//   of the link,
			// * the selection went to a different parent when creating a NEW link. E.g. someone
			//   else modified the document.
			// * the selection has expanded (e.g. displaying link actions then pressing SHIFT+Right arrow).
			//
			// Note: #_getSelectedTooltipElement will return a link for a non-collapsed selection only
			// when fully selected.
			if ((prevSelectedLink && !selectedLink) ||
				(!prevSelectedLink && selectionParent !== prevSelectionParent)) {
				this._hideUI();
			}
			// Update the position of the panel when:
			//  * link panel is in the visible stack
			//  * the selection remains in the original link element,
			//  * there was no link element in the first place, i.e. creating a new link
			else if (this._isUIVisible) {
				// If still in a link element, simply update the position of the balloon.
				// If there was no link (e.g. inserting one), the balloon must be moved
				// to the new position in the editing view (a new native DOM range).
				this._balloon.updatePosition(this._getBalloonPositionData());
			}

			prevSelectedLink = selectedLink;
			prevSelectionParent = selectionParent;
		};

		function getSelectionParent() {
			return viewDocument.selection.focus.getAncestors()
				.reverse()
				.find(node => node.is('element'));
		}

		this.listenTo(editor.ui, 'update', update);
		this.listenTo(this._balloon, 'change:visibleView', update);
	}
  
	get _areActionsInPanel() {
		return this._balloon.hasView(this.actionsView);
	}
 
	get _areActionsVisible() {
		return this._balloon.visibleView === this.actionsView;
	}
 
	get _isUIInPanel() {
		return this._areActionsInPanel;
	}
 
	get _isUIVisible() {
		const visibleView = this._balloon.visibleView;

		return visibleView == this._areActionsVisible;
	}

	/**
	 * Returns positioning options for the {@link #_balloon}. They control the way the balloon is attached
	 * to the target element or selection.
	 *
	 * If the selection is collapsed and inside a link element, the panel will be attached to the
	 * entire link element. Otherwise, it will be attached to the selection.
	 *
	 * @private
	 * @returns {module:utils/dom/position~Options}
	 */
	_getBalloonPositionData() {
		const view = this.editor.editing.view;
		const model = this.editor.model;
		const viewDocument = view.document;
		let target = null;

		if (model.markers.has(VISUAL_SELECTION_MARKER_NAME)) {
			// There are cases when we highlight selection using a marker (#7705, #4721).
			const markerViewElements = Array.from(this.editor.editing.mapper.markerNameToElements(VISUAL_SELECTION_MARKER_NAME));
			const newRange = view.createRange(
				view.createPositionBefore(markerViewElements[ 0 ]),
				view.createPositionAfter(markerViewElements[ markerViewElements.length - 1 ])
			);

			target = view.domConverter.viewRangeToDom(newRange);
		} else {
			const targetLink = this._getSelectedTooltipElement();
			const range = viewDocument.selection.getFirstRange();

			target = targetLink ?
				// When selection is inside link element, then attach panel to this element.
				view.domConverter.mapViewToDom(targetLink) :
				// Otherwise attach panel to the selection.
				view.domConverter.viewRangeToDom(range);
		}

		return { target };
	}

	/**
	 * Returns the link {@link module:engine/view/attributeelement~AttributeElement} under
	 * the {@link module:engine/view/document~Document editing view's} selection or `null`
	 * if there is none.
	 *
	 * **Note**: For a non–collapsed selection, the link element is only returned when **fully**
	 * selected and the **only** element within the selection boundaries.
	 *
	 * @private
	 * @returns {module:engine/view/attributeelement~AttributeElement|null}
	 */
	_getSelectedTooltipElement() {
		const view = this.editor.editing.view;
		const selection = view.document.selection;

		if (selection.isCollapsed) {
			return findTooltipElementAncestor(selection.getFirstPosition());
		} else {
			// The range for fully selected link is usually anchored in adjacent text nodes.
			// Trim it to get closer to the actual link element.
			const range = selection.getFirstRange().getTrimmed();
			const startLink = findTooltipElementAncestor(range.start);
			const endLink = findTooltipElementAncestor(range.end);

			if (!startLink || startLink != endLink) {
				return null;
			}

			// Check if the link element is fully selected.
			if (view.createRangeIn(startLink).getTrimmed().isEqual(range)) {
				return startLink;
			} else {
				return null;
			}
		}
	}

	/**
	 * Displays a fake visual selection when the contextual balloon is displayed.
	 *
	 * This adds a 'link-ui' marker into the document that is rendered as a highlight on selected text fragment.
	 *
	 * @private
	 */
	_showFakeVisualSelection() {
		const model = this.editor.model;

		model.change(writer => {
			const range = model.document.selection.getFirstRange();

			if (model.markers.has(VISUAL_SELECTION_MARKER_NAME)) {
				writer.updateMarker(VISUAL_SELECTION_MARKER_NAME, { range });
			} else {
				if (range.start.isAtEnd) {
					const startPosition = range.start.getLastMatchingPosition(
						({ item }) => !model.schema.isContent(item),
						{ boundaries: range }
					);

					writer.addMarker(VISUAL_SELECTION_MARKER_NAME, {
						usingOperation: false,
						affectsData: false,
						range: writer.createRange(startPosition, range.end)
					});
				} else {
					writer.addMarker(VISUAL_SELECTION_MARKER_NAME, {
						usingOperation: false,
						affectsData: false,
						range
					});
				}
			}
		});
	}

	/**
	 * Hides the fake visual selection created in {@link #_showFakeVisualSelection}.
	 *
	 * @private
	 */
	_hideFakeVisualSelection() {
		const model = this.editor.model;

		if (model.markers.has(VISUAL_SELECTION_MARKER_NAME)) {
			model.change(writer => {
				writer.removeMarker(VISUAL_SELECTION_MARKER_NAME);
			});
		}
	}
}

// Returns a link element if there's one among the ancestors of the provided `Position`.
//
// @private
// @param {module:engine/view/position~Position} View position to analyze.
// @returns {module:engine/view/attributeelement~AttributeElement|null} Link element at the position or null.
function findTooltipElementAncestor(position) {
	return position.getAncestors().find(ancestor => isTooltipElement(ancestor));
}
