 
import { Plugin } from '@ckeditor/ckeditor5-core';
import { MouseObserver } from '@ckeditor/ckeditor5-engine';
import { keyCodes } from '@ckeditor/ckeditor5-utils';
import { Input, TwoStepCaretMovement, findAttributeRange  } from '@ckeditor/ckeditor5-typing';
import { ClipboardPipeline } from '@ckeditor/ckeditor5-clipboard';
import TooltipCommand from './tooltipcommand';
import RemoveTooltipCommand from './removetooltipcommand';
import TooltipDialogCommand from './tooltipdialogcommand';
import { TOOLTIP_COMMANDS, HTML_ATTRIBUTE, HTML_CLASS, HTML_ELEMENT, TOOLTIP_ATTRIBUTE, TOOLTIP_CUSTOM_PROPERTY, CK4_HTML_ATTRIBUTE, CK4_HTML_CLASS } from './utils';
import '../../themes/styles/tooltip.css';
 
export default class TooltipEditing extends Plugin {
 
	static get pluginName() {
		return 'TooltipEditing';
	}
 
	static get requires() {
		// Clipboard is required for handling cut and paste events while typing over the link.
		return [ TwoStepCaretMovement, Input, ClipboardPipeline ];
	}
 
	init() {
		const editor = this.editor;

		// Allow link attribute on all inline nodes.
		editor.model.schema.extend('$text', { allowAttributes: TOOLTIP_ATTRIBUTE });

		editor.conversion.for('downcast')
			.attributeToElement({ 
				model: TOOLTIP_ATTRIBUTE,
				view: (modelAttributeValue, { writer }) =>{
					const attributes = { class: `${HTML_CLASS} ${CK4_HTML_CLASS}` };
					attributes[HTML_ATTRIBUTE] = modelAttributeValue;
					attributes[CK4_HTML_ATTRIBUTE] = modelAttributeValue;
					const tooltipElement = writer.createAttributeElement('span', attributes, { priority: 5 });
					writer.setCustomProperty(TOOLTIP_CUSTOM_PROPERTY, true, tooltipElement);
					return tooltipElement;			
				}
			});
 
		editor.conversion.for( 'upcast' )
			.elementToElement( {
				view: {
					classes: CK4_HTML_CLASS
				},
				model: ( viewElement, { writer } ) => {
					const attributes = { class: `${HTML_CLASS} ${CK4_HTML_CLASS}` };
					attributes[HTML_ATTRIBUTE] = viewElement.getAttribute(CK4_HTML_ATTRIBUTE);
					attributes[CK4_HTML_ATTRIBUTE] = viewElement.getAttribute(CK4_HTML_ATTRIBUTE);
					return writer.createElement( HTML_ELEMENT, attributes);
				}
			})
			.elementToAttribute({
				view: {
					classes: CK4_HTML_CLASS
				},
				model: {
					key: TOOLTIP_ATTRIBUTE,
					value: (viewElement) =>  {
						return viewElement.getAttribute(HTML_ATTRIBUTE) ? viewElement.getAttribute(HTML_ATTRIBUTE) : viewElement.getAttribute(CK4_HTML_ATTRIBUTE);
					}
				}
			});

		// Create linking commands.
		editor.commands.add(TOOLTIP_COMMANDS.TOOLTIP, new TooltipCommand(editor));
		editor.commands.add(TOOLTIP_COMMANDS.REMOVE_TOOLTIP, new RemoveTooltipCommand(editor));
		editor.commands.add(TOOLTIP_COMMANDS.TOOLTIP_DIALOG, new TooltipDialogCommand(editor));
 
		// Enable two-step caret movement for `TOOLTIP_ATTRIBUTE` attribute.
		const twoStepCaretMovementPlugin = editor.plugins.get(TwoStepCaretMovement);
		twoStepCaretMovementPlugin.registerAttribute(TOOLTIP_ATTRIBUTE);
 
		// Change the attributes of the selection in certain situations after the link was inserted into the document.
		this._enableInsertContentSelectionAttributesFixer();

		// Handle a click at the beginning/end of a link element.
		this._enableClickingAfterLink();

		// Handle typing over the link.
		this._enableTypingOverLink();

		// Handle removing the content after the link element.
		this._handleDeleteContentAfterLink();
	}
 
	_enableInsertContentSelectionAttributesFixer() {
		const editor = this.editor;
		const model = editor.model;
		const selection = model.document.selection;
 
		this.listenTo(model, 'insertContent', () => {
			const nodeBefore = selection.anchor.nodeBefore;
			const nodeAfter = selection.anchor.nodeAfter;
 
			if (!selection.hasAttribute(TOOLTIP_ATTRIBUTE)) {
				return;
			}
 
			if (!nodeBefore) {
				return;
			}
 
			if (!nodeBefore.hasAttribute(TOOLTIP_ATTRIBUTE)) {
				return;
			}
 
			if (nodeAfter && nodeAfter.hasAttribute(TOOLTIP_ATTRIBUTE)) {
				return;
			}

			model.change(writer => {
				removeLinkAttributesFromSelection(writer);
			});
		}, { priority: 'low' });
	}
 
	_enableClickingAfterLink() {
		const editor = this.editor;
 
		editor.editing.view.addObserver(MouseObserver);

		let clicked = false;

		// Detect the click.
		this.listenTo(editor.editing.view.document, 'mousedown', () => {
			clicked = true;
		});

		// When the selection has changed...
		this.listenTo(editor.editing.view.document, 'selectionChange', () => {
			if (!clicked) {
				return;
			}

			// ...and it was caused by the click...
			clicked = false;

			const selection = editor.model.document.selection;

			// ...and no text is selected...
			if (!selection.isCollapsed) {
				return;
			}

			// ...and clicked text is the link...
			if (!selection.hasAttribute(TOOLTIP_ATTRIBUTE)) {
				return;
			}

			const position = selection.getFirstPosition();
			const tooltipRange = findAttributeRange(position, TOOLTIP_ATTRIBUTE, selection.getAttribute(TOOLTIP_ATTRIBUTE), editor.model);

			// ...check whether clicked start/end boundary of the link.
			// If so, remove the `TOOLTIP_ATTRIBUTE` attribute.
			if (position.isTouching(tooltipRange.start) || position.isTouching(tooltipRange.end)) {
				editor.model.change(writer => {
					removeLinkAttributesFromSelection(writer);
				});
			}
		});
	}

	/**
	 * Starts listening to {@link module:engine/model/model~Model#deleteContent} and {@link module:engine/model/model~Model#insertContent}
	 * and checks whether typing over the link. If so, attributes of removed text are preserved and applied to the inserted text.
	 *
	 * The purpose of this action is to allow modifying a text without loosing the `TOOLTIP_ATTRIBUTE` attribute (and other).
	 *
	 * See https://github.com/ckeditor/ckeditor5/issues/4762.
	 *
	 * @private
	 */
	_enableTypingOverLink() {
		const editor = this.editor;
		const view = editor.editing.view;

		// Selection attributes when started typing over the link.
		let selectionAttributes;

		// Whether pressed `Backspace` or `Delete`. If so, attributes should not be preserved.
		let deletedContent;

		// Detect pressing `Backspace` / `Delete`.
		this.listenTo(view.document, 'delete', () => {
			deletedContent = true;
		}, { priority: 'high' });

		// Listening to `model#deleteContent` allows detecting whether selected content was a link.
		// If so, before removing the element, we will copy its attributes.
		this.listenTo(editor.model, 'deleteContent', () => {
			const selection = editor.model.document.selection;

			// Copy attributes only if anything is selected.
			if (selection.isCollapsed) {
				return;
			}

			// When the content was deleted, do not preserve attributes.
			if (deletedContent) {
				deletedContent = false;
				return;
			}

			// Enabled only when typing.
			if (!isTyping(editor)) {
				return;
			}

			if (shouldCopyAttributes(editor.model)) {
				selectionAttributes = selection.getAttributes();
			}
		}, { priority: 'high' });

		// Listening to `model#insertContent` allows detecting the content insertion.
		// We want to apply attributes that were removed while typing over the link.
		this.listenTo(editor.model, 'insertContent', (evt, [ element ]) => {
			deletedContent = false;

			// Enabled only when typing.
			if (!isTyping(editor)) {
				return;
			}

			if (!selectionAttributes) {
				return;
			}

			editor.model.change(writer => {
				for (const [ attribute, value ] of selectionAttributes) {
					writer.setAttribute(attribute, value, element);
				}
			});

			selectionAttributes = null;
		}, { priority: 'high' });
	}

	/**
	 * Starts listening to {@link module:engine/model/model~Model#deleteContent} and checks whether
	 * removing a content right after the "qmsTooltip" attribute.
	 *
	 * If so, the selection should not preserve the `TOOLTIP_ATTRIBUTE` attribute. However, if
	 * the {@link module:typing/twostepcaretmovement~TwoStepCaretMovement} plugin is active and
	 * the selection has the "qmsTooltip" attribute due to overriden gravity (at the end), the `TOOLTIP_ATTRIBUTE` attribute should stay untouched.
	 *
	 * The purpose of this action is to allow removing the link text and keep the selection outside the link.
	 *
	 * See https://github.com/ckeditor/ckeditor5/issues/7521.
	 *
	 * @private
	 */
	_handleDeleteContentAfterLink() {
		const editor = this.editor;
		const model = editor.model;
		const selection = model.document.selection;
		const view = editor.editing.view;
 
		// A flag whether attributes `TOOLTIP_ATTRIBUTE` attribute should be preserved.
		let shouldPreserveAttributes = false;

		// A flag whether the `Backspace` key was pressed.
		let hasBackspacePressed = false;

		// Detect pressing `Backspace`.
		this.listenTo(view.document, 'delete', (evt, data) => {
			hasBackspacePressed = data.domEvent.keyCode === keyCodes.backspace;
		}, { priority: 'high' });

		// Before removing the content, check whether the selection is inside a link or at the end of link but with 2-SCM enabled.
		// If so, we want to preserve link attributes.
		this.listenTo(model, 'deleteContent', () => {
			// Reset the state.
			shouldPreserveAttributes = false;

			const position = selection.getFirstPosition();
			const qmsTooltip = selection.getAttribute(TOOLTIP_ATTRIBUTE);

			if (!qmsTooltip) {
				return;
			}

			const tooltipRange = findAttributeRange(position, TOOLTIP_ATTRIBUTE, qmsTooltip, model);

			// Preserve `TOOLTIP_ATTRIBUTE` attribute if the selection is in the middle of the link or
			// the selection is at the end of the link and 2-SCM is activated.
			shouldPreserveAttributes = tooltipRange.containsPosition(position) || tooltipRange.end.isEqual(position);
		}, { priority: 'high' });

		// After removing the content, check whether the current selection should preserve the `TOOLTIP_ATTRIBUTE` attribute.
		this.listenTo(model, 'deleteContent', () => {
			// If didn't press `Backspace`.
			if (!hasBackspacePressed) {
				return;
			}

			hasBackspacePressed = false;

			// Disable the mechanism if inside a link (`<$text url="foo">F[]oo</$text>` or <$text url="foo">Foo[]</$text>`).
			if (shouldPreserveAttributes) {
				return;
			}
 
		}, { priority: 'low' });
	}
}

 
function removeLinkAttributesFromSelection(writer) {
	writer.removeSelectionAttribute(TOOLTIP_ATTRIBUTE);
}

// Checks whether selection's attributes should be copied to the new inserted text.
//
// @param {module:engine/model/model~Model} model
// @returns {Boolean}
function shouldCopyAttributes(model) {
	const selection = model.document.selection;
	const firstPosition = selection.getFirstPosition();
	const lastPosition = selection.getLastPosition();
	const nodeAtFirstPosition = firstPosition.nodeAfter;

	// The text link node does not exist...
	if (!nodeAtFirstPosition) {
		return false;
	}

	// ...or it isn't the text node...
	if (!nodeAtFirstPosition.is('$text')) {
		return false;
	}

	// ...or isn't the link.
	if (!nodeAtFirstPosition.hasAttribute(TOOLTIP_ATTRIBUTE)) {
		return false;
	}

	// `textNode` = the position is inside the link element.
	// `nodeBefore` = the position is at the end of the link element.
	const nodeAtLastPosition = lastPosition.textNode || lastPosition.nodeBefore;

	// If both references the same node selection contains a single text node.
	if (nodeAtFirstPosition === nodeAtLastPosition) {
		return true;
	}

	// If nodes are not equal, maybe the link nodes has defined additional attributes inside.
	// First, we need to find the entire link range.
	const tooltipRange = findAttributeRange(firstPosition, TOOLTIP_ATTRIBUTE, nodeAtFirstPosition.getAttribute(TOOLTIP_ATTRIBUTE), model);

	// Then we can check whether selected range is inside the found link range. If so, attributes should be preserved.
	return tooltipRange.containsRange(model.createRange(firstPosition, lastPosition), true);
}

// Checks whether provided changes were caused by typing.
//
// @params {module:core/editor/editor~Editor} editor
// @returns {Boolean}
function isTyping(editor) {
	const input = editor.plugins.get('Input');

	return input.isInput(editor.model.change(writer => writer.batch));
}
