/**
 * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module widget/widgetresize
 */

import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import Resizer from './widgetresize/resizer';
import DomEmitterMixin from '@ckeditor/ckeditor5-utils/src/dom/emittermixin';
import global from '@ckeditor/ckeditor5-utils/src/dom/global';
import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
import MouseObserver from '@ckeditor/ckeditor5-engine/src/view/observer/mouseobserver';
import mix from '@ckeditor/ckeditor5-utils/src/mix';
import { throttle } from 'lodash-es';

import '../theme/widgetresize.css';

/**
 * The widget resize feature plugin.
 *
 * Use the {@link module:widget/widgetresize~WidgetResize#attachTo} method to create a resizer for the specified widget.
 *
 * @extends module:core/plugin~Plugin
 * @mixes module:utils/observablemixin~ObservableMixin
 */
export default class WidgetResize extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'WidgetResize';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editing = this.editor.editing;
		const domDocument = global.window.document;

		/**
		 * The currently visible resizer.
		 *
		 * @observable
		 * @member {module:widget/widgetresize/resizer~Resizer|null} #visibleResizer
		 */
		this.set( 'visibleResizer', null );

		/**
		 * References an active resizer.
		 *
		 * Active resizer means a resizer which handle is actively used by the end user.
		 *
		 * @protected
		 * @observable
		 * @member {module:widget/widgetresize/resizer~Resizer|null} #_activeResizer
		 */
		this.set( '_activeResizer', null );

		/**
		 * A map of resizers created using this plugin instance.
		 *
		 * @protected
		 * @type {Map.<module:engine/view/containerelement~ContainerElement, module:widget/widgetresize/resizer~Resizer>}
		 */
		this._resizers = new Map();

		editing.view.addObserver( MouseObserver );

		this._observer = Object.create( DomEmitterMixin );

		this.listenTo( editing.view.document, 'mousedown', this._mouseDownListener.bind( this ), { priority: 'high' } );

		this._observer.listenTo( domDocument, 'mousemove', this._mouseMoveListener.bind( this ) );
		this._observer.listenTo( domDocument, 'mouseup', this._mouseUpListener.bind( this ) );

		const redrawFocusedResizer = () => {
			if ( this.visibleResizer ) {
				this.visibleResizer.redraw();
			}
		};

		this._redrawFocusedResizerThrottled = throttle( redrawFocusedResizer, 200 );

		// Redraws occurring upon a change of visible resizer must not be throttled, as it is crucial for the initial
		// render. Without it the resizer frame would be misaligned with resizing host for a fraction of second.
		this.on( 'change:visibleResizer', redrawFocusedResizer );

		// Redrawing on any change of the UI of the editor (including content changes).
		this.editor.ui.on( 'update', this._redrawFocusedResizerThrottled );

		// Remove view widget-resizer mappings for widgets that have been removed from the document.
		// https://github.com/ckeditor/ckeditor5/issues/10156
		// https://github.com/ckeditor/ckeditor5/issues/10266
		this.editor.model.document.on( 'change', () => {
			for ( const [ viewElement, resizer ] of this._resizers ) {
				if ( !viewElement.isAttached() ) {
					this._resizers.delete( viewElement );
					resizer.destroy();
				}
			}
		}, { priority: 'lowest' } );

		// Resizers need to be redrawn upon window resize, because new window might shrink resize host.
		this._observer.listenTo( global.window, 'resize', this._redrawFocusedResizerThrottled );

		const viewSelection = this.editor.editing.view.document.selection;

		viewSelection.on( 'change', () => {
			const selectedElement = viewSelection.getSelectedElement();

			this.visibleResizer = this.getResizerByViewElement( selectedElement ) || null;
		} );
	}

	/**
	 * @inheritDoc
	 */
	destroy() {
		this._observer.stopListening();

		for ( const resizer of this._resizers.values() ) {
			resizer.destroy();
		}

		this._redrawFocusedResizerThrottled.cancel();
	}

	/**
	 * @param {module:widget/widgetresize~ResizerOptions} [options] Resizer options.
	 * @returns {module:widget/widgetresize/resizer~Resizer}
	 */
	attachTo( options ) {
		const resizer = new Resizer( options );
		const plugins = this.editor.plugins;

		resizer.attach();

		if ( plugins.has( 'WidgetToolbarRepository' ) ) {
			// Hiding widget toolbar to improve the performance
			// (https://github.com/ckeditor/ckeditor5-widget/pull/112#issuecomment-564528765).
			const widgetToolbarRepository = plugins.get( 'WidgetToolbarRepository' );

			resizer.on( 'begin', () => {
				widgetToolbarRepository.forceDisabled( 'resize' );
			}, { priority: 'lowest' } );

			resizer.on( 'cancel', () => {
				widgetToolbarRepository.clearForceDisabled( 'resize' );
			}, { priority: 'highest' } );

			resizer.on( 'commit', () => {
				widgetToolbarRepository.clearForceDisabled( 'resize' );
			}, { priority: 'highest' } );
		}

		this._resizers.set( options.viewElement, resizer );

		const viewSelection = this.editor.editing.view.document.selection;
		const selectedElement = viewSelection.getSelectedElement();

		// If the element the resizer is created for is currently focused, it should become visible.
		if ( this.getResizerByViewElement( selectedElement ) == resizer ) {
			this.visibleResizer = resizer;
		}

		return resizer;
	}

	/**
	 * Returns a resizer created for a given view element (widget element).
	 *
	 * @param {module:engine/view/containerelement~ContainerElement} viewElement View element associated with the resizer.
	 * @returns {module:widget/widgetresize/resizer~Resizer|undefined}
	 */
	getResizerByViewElement( viewElement ) {
		return this._resizers.get( viewElement );
	}

	/**
	 * Returns a resizer that contains a given resize handle.
	 *
	 * @protected
	 * @param {HTMLElement} domResizeHandle
	 * @returns {module:widget/widgetresize/resizer~Resizer}
	 */
	_getResizerByHandle( domResizeHandle ) {
		for ( const resizer of this._resizers.values() ) {
			if ( resizer.containsHandle( domResizeHandle ) ) {
				return resizer;
			}
		}
	}

	/**
	 * @protected
	 * @param {module:utils/eventinfo~EventInfo} event
	 * @param {Event} domEventData Native DOM event.
	 */
	_mouseDownListener( event, domEventData ) {
		const resizeHandle = domEventData.domTarget;

		if ( !Resizer.isResizeHandle( resizeHandle ) ) {
			return;
		}

		this._activeResizer = this._getResizerByHandle( resizeHandle );

		if ( this._activeResizer ) {
			this._activeResizer.begin( resizeHandle );

			// Do not call other events when resizing. See: #6755.
			event.stop();
			domEventData.preventDefault();
		}
	}

	/**
	 * @protected
	 * @param {module:utils/eventinfo~EventInfo} event
	 * @param {Event} domEventData Native DOM event.
	 */
	_mouseMoveListener( event, domEventData ) {
		if ( this._activeResizer ) {
			this._activeResizer.updateSize( domEventData );
		}
	}

	/**
	 * @protected
	 */
	_mouseUpListener() {
		if ( this._activeResizer ) {
			this._activeResizer.commit();
			this._activeResizer = null;
		}
	}
}

mix( WidgetResize, ObservableMixin );

/**
 * Interface describing a resizer. It allows to specify the resizing host, custom logic for calculating aspect ratio, etc.
 *
 * @interface ResizerOptions
 */

/**
 * Editor instance associated with the resizer.
 *
 * @member {module:core/editor/editor~Editor} module:widget/widgetresize~ResizerOptions#editor
 */

/**
 * @member {module:engine/model/element~Element} module:widget/widgetresize~ResizerOptions#modelElement
 */

/**
 * A view of an element to be resized. Typically it's the main widget's view instance.
 *
 * @member {module:engine/view/containerelement~ContainerElement} module:widget/widgetresize~ResizerOptions#viewElement
 */

/**
 * A callback to be executed once the resizing process is done.
 *
 * It receives a `Number` (`newValue`) as a parameter.
 *
 * For example, {@link module:image/imageresize~ImageResize} uses it to execute the resize image command
 * which puts the new value into the model.
 *
 * ```js
 * {
 *	editor,
 *	modelElement: data.item,
 *	viewElement: widget,
 *
 *	onCommit( newValue ) {
 *		editor.execute( 'resizeImage', { width: newValue } );
 *	}
 * };
 * ```
 *
 *
 * @member {Function} module:widget/widgetresize~ResizerOptions#onCommit
 */

/**
 * @member {Function} module:widget/widgetresize~ResizerOptions#getResizeHost
 */

/**
 * @member {Function} module:widget/widgetresize~ResizerOptions#isCentered
 */
