/*!
 * Jodit Editor (https://xdsoft.net/jodit/)
 * Licensed under GNU General Public License version 2 or later or a commercial license or MIT;
 * For GPL see LICENSE-GPL.txt in the project root for license information.
 * For MIT see LICENSE-MIT.txt in the project root for license information.
 * For commercial licenses see https://xdsoft.net/jodit/commercial/
 * Copyright (c) 2013-2019 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
 */

import { Config } from '../../Config';
import { IDialogOptions } from '../../types/dialog';
import { KEY_ESC } from '../../constants';
import { IDictionary, IJodit } from '../../types';
import { IControlType } from '../../types/toolbar';
import { IViewBased } from '../../types/view';
import { $$, asArray, css } from '../helpers/';
import { View } from '../view/view';
import { Dom } from '../Dom';
import { isJoditObject } from '../helpers/checker/isJoditObject';

/**
 * @property {object} dialog module settings {@link Dialog|Dialog}
 * @property {int} dialog.zIndex=1000 Default Z-index for dialog window. {@link Dialog|Dialog}'s settings
 * @property {boolean} dialog.resizable=true This dialog can resize by trigger
 * @property {boolean} dialog.draggable=true This dialog can move by header
 * @property {boolean} dialog.fullsize=false A dialog window will open in full screen by default
 * @property {Buttons} dialog.buttons=['close.dialog', 'fullsize.dialog']
 */

declare module '../../Config' {
	interface Config {
		dialog: IDialogOptions;
	}
}

Config.prototype.dialog = {
	resizable: true,
	draggable: true,
	buttons: ['dialog.close'],
	removeButtons: []
};

Config.prototype.controls.dialog = {
	close: {
		icon: 'cancel',
		exec: dialog => {
			(dialog as Dialog).close();
		}
	},
	fullsize: {
		icon: 'fullsize',
		getLabel: (editor, btn: IControlType, button) => {
			if (
				Config.prototype.controls.fullsize &&
				Config.prototype.controls.fullsize.getLabel &&
				typeof Config.prototype.controls.fullsize.getLabel ===
				'function'
			) {
				return Config.prototype.controls.fullsize.getLabel(
					editor,
					btn,
					button
				);
			}
		},
		exec: dialog => {
			dialog.toggleFullSize();
		}
	}
} as IDictionary<IControlType>;

type Content = string | HTMLElement | Array<string | HTMLElement>;

/**
 * Module to generate dialog windows
 *
 * @param {Object} parent Jodit main object
 * @param {Object} [opt] Extend Options
 */
export class Dialog extends View {
	/**
	 * @property {HTMLDivElement} resizer
	 */
	private resizer: HTMLDivElement;
	public toolbar: ToolbarCollection;

	private offsetX: number;
	private offsetY: number;

	private destination: HTMLElement = document.body;
	private destroyAfterClose: boolean = false;

	private moved: boolean = false;

	private iSetMaximization: boolean = false;

	private resizable: boolean = false;
	private draggable: boolean = false;
	private startX: number = 0;
	private startY: number = 0;
	private startPoint = { x: 0, y: 0, w: 0, h: 0 };

	private lockSelect = () => {
		this.container.classList.add('jodit_dialog_box-moved');
	};
	private unlockSelect = () => {
		this.container.classList.remove('jodit_dialog_box-moved');
	};

	private setElements(
		root: HTMLDivElement | HTMLHeadingElement,
		elements: Content
	) {
		const elements_list: HTMLElement[] = [];

		asArray(elements).forEach(elm => {
			const element: HTMLElement =
				typeof elm === 'string' ? this.create.fromHTML(elm) : elm;

			elements_list.push(element);

			if (element.parentNode !== root) {
				root.appendChild(element);
			}
		});

		Array.from(root.childNodes).forEach((elm: ChildNode) => {
			if (elements_list.indexOf(elm as HTMLElement) === -1) {
				root.removeChild(elm);
			}
		});
	}

	private onMouseUp = () => {
		if (this.draggable || this.resizable) {
			this.draggable = false;
			this.resizable = false;
			this.unlockSelect();
			if (this.jodit && this.jodit.events) {
				/**
				 * Fired when dialog box is finished to resizing
				 * @event endResize
				 */
				this.jodit.events.fire(this, 'endResize endMove');
			}
		}
	};

	/**
	 *
	 * @param {MouseEvent} e
	 */
	private onHeaderMouseDown = (e: MouseEvent) => {
		const target: HTMLElement = e.target as HTMLElement;
		if (
			!this.options.draggable ||
			(target && target.nodeName.match(/^(INPUT|SELECT)$/))
		) {
			return;
		}
		this.draggable = true;
		this.startX = e.clientX;
		this.startY = e.clientY;
		this.startPoint.x = css(this.dialog, 'left') as number;
		this.startPoint.y = css(this.dialog, 'top') as number;

		this.setMaxZIndex();
		e.preventDefault();

		this.lockSelect();

		if (this.jodit && this.jodit.events) {
			/**
			 * Fired when dialog box is started moving
			 * @event startMove
			 */
			this.jodit.events.fire(this, 'startMove');
		}
	};

	private onMouseMove = (e: MouseEvent) => {
		if (this.draggable && this.options.draggable) {
			this.setPosition(
				this.startPoint.x + e.clientX - this.startX,
				this.startPoint.y + e.clientY - this.startY
			);

			if (this.jodit && this.jodit.events) {
				/**
				 * Fired when dialog box is moved
				 * @event move
				 * @param {int} dx Delta X
				 * @param {int} dy Delta Y
				 */
				this.jodit.events.fire(
					this,
					'move',
					e.clientX - this.startX,
					e.clientY - this.startY
				);
			}

			e.stopImmediatePropagation();
			e.preventDefault();
		}

		if (this.resizable && this.options.resizable) {
			this.setSize(
				this.startPoint.w + e.clientX - this.startX,
				this.startPoint.h + e.clientY - this.startY
			);
			if (this.jodit && this.jodit.events) {
				/**
				 * Fired when dialog box is resized
				 * @event resizeDialog
				 * @param {int} dx Delta X
				 * @param {int} dy Delta Y
				 */
				this.jodit.events.fire(
					this,
					'resizeDialog',
					e.clientX - this.startX,
					e.clientY - this.startY
				);
			}
			e.stopImmediatePropagation();
			e.preventDefault();
		}
	};
	/**
	 *
	 * @param {MouseEvent} e
	 */
	private onKeyDown = (e: KeyboardEvent) => {
		if (this.isOpened() && e.which === KEY_ESC) {
			const me = this.getMaxZIndexDialog();

			if (me) {
				me.close();
			} else {
				this.close();
			}

			e.stopImmediatePropagation();
		}
	};

	private onResize = () => {
		if (
			this.options &&
			this.options.resizable &&
			!this.moved &&
			this.isOpened() &&
			!this.offsetX &&
			!this.offsetY
		) {
			this.setPosition();
		}
	};

	private onResizerMouseDown(e: MouseEvent) {
		this.resizable = true;
		this.startX = e.clientX;
		this.startY = e.clientY;
		this.startPoint.w = this.dialog.offsetWidth;
		this.startPoint.h = this.dialog.offsetHeight;

		this.lockSelect();

		if (this.jodit.events) {
			/**
			 * Fired when dialog box is started resizing
			 * @event startResize
			 */
			this.jodit.events.fire(this, 'startResize');
		}
	}

	public options: IDialogOptions;

	/**
	 * @property {HTMLDivElement} dialog
	 */
	public dialog: HTMLDivElement;

	public dialogbox_header: HTMLHeadingElement;
	public dialogbox_content: HTMLDivElement;
	public dialogbox_footer: HTMLDivElement;
	public dialogbox_toolbar: HTMLDivElement;

	public document: Document = document;
	public window: Window = window;

	/**
	 * Specifies the size of the window
	 *
	 * @param {number} [w] - The width of the window
	 * @param {number} [h] - The height of the window
	 */
	public setSize(w?: number | string, h?: number | string) {
		if (w) {
			css(this.dialog, 'width', w);
		}
		if (h) {
			css(this.dialog, 'height', h);
		}
	}

	/**
	 * Specifies the position of the upper left corner of the window . If x and y are specified,
	 * the window is centered on the center of the screen
	 *
	 * @param {Number} [x] - Position px Horizontal
	 * @param {Number} [y] - Position px Vertical
	 */
	public setPosition(x?: number, y?: number) {
		const
			w: number = this.window.innerWidth,
			h: number = this.window.innerHeight;

		let
			left: number = w / 2 - this.dialog.offsetWidth / 2,
			top: number = h / 2 - this.dialog.offsetHeight / 2;

		if (left < 0) {
			left = 0;
		}

		if (top < 0) {
			top = 0;
		}

		if (x !== undefined && y !== undefined) {
			this.offsetX = x;
			this.offsetY = y;
			this.moved = Math.abs(x - left) > 100 || Math.abs(y - top) > 100;
		}

		this.dialog.style.left = (x || left) + 'px';
		this.dialog.style.top = (y || top) + 'px';
	}

	/**
	 * Specifies the dialog box title . It can take a string and an array of objects
	 *
	 * @param {string|string[]|Element|Element[]} content - A string or an HTML element ,
	 * or an array of strings and elements
	 * @example
	 * ```javascript
	 * var dialog = new Jodi.modules.Dialog(parent);
	 * dialog.setTitle('Hello world');
	 * dialog.setTitle(['Hello world', '<button>OK</button>', $('<div>some</div>')]);
	 * dialog.open();
	 * ```
	 */
	public setTitle(content: Content) {
		this.setElements(this.dialogbox_header, content);
	}

	/**
	 * It specifies the contents of the dialog box. It can take a string and an array of objects
	 *
	 * @param {string|string[]|Element|Element[]} content A string or an HTML element ,
	 * or an array of strings and elements
	 * @example
	 * ```javascript
	 * var dialog = new Jodi.modules.Dialog(parent);
	 * dialog.setTitle('Hello world');
	 * dialog.setContent('<form onsubmit="alert(1);"><input type="text" /></form>');
	 * dialog.open();
	 * ```
	 */
	public setContent(content: Content) {
		this.setElements(this.dialogbox_content, content);
	}

	/**
	 * Sets the bottom of the dialog. It can take a string and an array of objects
	 *
	 * @param {string|string[]|Element|Element[]} content - A string or an HTML element ,
	 * or an array of strings and elements
	 * @example
	 * ```javascript
	 * var dialog = new Jodi.modules.Dialog(parent);
	 * dialog.setTitle('Hello world');
	 * dialog.setContent('<form><input id="someText" type="text" /></form>');
	 * dialog.setFooter([
	 *  $('<a class="jodit_button">OK</a>').click(function () {
	 *      alert($('someText').val())
	 *      dialog.close();
	 *  })
	 * ]);
	 * dialog.open();
	 * ```
	 */
	public setFooter(content: Content) {
		this.setElements(this.dialogbox_footer, content);
		this.dialog.classList.toggle('with_footer', !!content);
	}

	/**
	 * Return current Z-index
	 * @return {number}
	 */
	public getZIndex(): number {
		return parseInt(this.container.style.zIndex || '0', 10);
	}

	/**
	 * Get dialog instance with maximum z-index displaying it on top of all the dialog boxes
	 *
	 * @return {Dialog}
	 */
	public getMaxZIndexDialog() {
		let maxzi: number = 0,
			dlg: Dialog,
			zIndex: number,
			res: Dialog = this;

		$$('.jodit_dialog_box', this.destination).forEach(
			(dialog: HTMLElement) => {
				dlg = (dialog as any).__jodit_dialog as Dialog;
				zIndex = parseInt(css(dialog, 'zIndex') as string, 10);
				if (dlg.isOpened() && !isNaN(zIndex) && zIndex > maxzi) {
					res = dlg;
					maxzi = zIndex;
				}
			}
		);

		return res;
	}

	/**
	 * Sets the maximum z-index dialog box, displaying it on top of all the dialog boxes
	 */
	public setMaxZIndex() {
		let maxzi: number = 0,
			zIndex: number = 0;

		$$('.jodit_dialog_box', this.destination).forEach(dialog => {
			zIndex = parseInt(css(dialog, 'zIndex') as string, 10);
			maxzi = Math.max(isNaN(zIndex) ? 0 : zIndex, maxzi);
		});

		this.container.style.zIndex = (maxzi + 1).toString();
	}

	/**
	 * Expands the dialog on full browser window
	 *
	 * @param {boolean} condition true - fullsize
	 * @return {boolean} true - fullsize
	 */
	public maximization(condition?: boolean): boolean {
		if (typeof condition !== 'boolean') {
			condition = !this.container.classList.contains(
				'jodit_dialog_box-fullsize'
			);
		}

		this.container.classList.toggle('jodit_dialog_box-fullsize', condition);

		[this.destination, this.destination.parentNode].forEach(
			(box: Node | null) => {
				box &&
				(box as HTMLElement).classList &&
				(box as HTMLElement).classList.toggle(
					'jodit_fullsize_box',
					condition
				);
			}
		);

		this.iSetMaximization = condition;

		return condition;
	}

	/**
	 * It opens a dialog box to center it, and causes the two event.
	 *
	 * @param {string|string[]|Element|Element[]} [content]  specifies the contents of the dialog box.
	 * Can be false или undefined. see {@link Dialog~setContent|setContent}
	 * @param {string|string[]|Element|Element[]} [title]  specifies the title of the dialog box, @see setTitle
	 * @param {boolean} [destroyAfter] true - After closing the window , the destructor will be called.
	 * see {@link Dialog~destruct|destruct}
	 * @param {boolean} [modal] - true window will be opened in modal mode
	 * @fires {@link event:beforeOpen} id returns 'false' then the window will not open
	 * @fires {@link event:afterOpen}
	 */
	public open(
		content?: Content,
		title?: Content,
		destroyAfter?: boolean,
		modal?: boolean
	) {
		/**
		 * Called before the opening of the dialog box
		 *
		 * @event beforeOpen
		 */
		if (this.jodit && this.jodit.events) {
			if (this.jodit.events.fire(this, 'beforeOpen') === false) {
				return;
			}
		}

		this.destroyAfterClose = destroyAfter === true;

		if (title !== undefined) {
			this.setTitle(title);
		}

		if (content) {
			this.setContent(content);
		}

		this.container.classList.add('active');
		if (modal) {
			this.container.classList.add('jodit_modal');
		}

		this.setPosition(this.offsetX, this.offsetY);
		this.setMaxZIndex();

		if (this.options.fullsize) {
			this.maximization(true);
		}

		/**
		 * Called after the opening of the dialog box
		 *
		 * @event afterOpen
		 */
		if (this.jodit && this.jodit.events) {
			this.jodit.events.fire('afterOpen', this);
		}
	}

	/**
	 * Open if the current window
	 *
	 * @return {boolean} - true window open
	 */
	public isOpened(): boolean {
		return (
			!this.isDestructed &&
			this.container &&
			this.container.classList.contains('active')
		);
	}

	/**
	 * Closes the dialog box , if you want to call the method {@link Dialog~destruct|destruct}
	 *
	 * @see destroy
	 * @method close
	 * @fires beforeClose
	 * @fires afterClose
	 * @example
	 * ```javascript
	 * //You can close dialog two ways
	 * var dialog = new Jodit.modules.Dialog();
	 * dialog.open('Hello world!', 'Title');
	 * var $close = Jodit.modules.helper.dom('<a href="javascript:void(0)" style="float:left;" class="jodit_button">
	 *     <i class="icon icon-check"></i>&nbsp;' + Jodit.prototype.i18n('Ok') + '</a>');
	 * $close.addEventListener('click', function () {
	 *     dialog.close();
	 * });
	 * dialog.setFooter($close);
	 * // and second way, you can close dialog from content
	 * dialog.open('<a onclick="var event = doc.createEvent('HTMLEvents'); event.initEvent('close_dialog', true, true);
	 * this.dispatchEvent(event)">Close</a>', 'Title');
	 * ```
	 */
	public close = (e?: MouseEvent) => {
		if (this.isDestructed) {
			return;
		}

		if (e) {
			e.stopImmediatePropagation();
			e.preventDefault();
		}

		/**
		 * Called up to close the window
		 *
		 * @event beforeClose
		 * @this {Dialog} current dialog
		 */
		if (this.jodit && this.jodit.events) {
			this.jodit.events.fire('beforeClose', this);
		}

		this.container &&
		this.container.classList &&
		this.container.classList.remove('active');

		if (this.iSetMaximization) {
			this.maximization(false);
		}

		if (this.destroyAfterClose) {
			this.destruct();
		}

		/**
		 * It called after the window is closed
		 *
		 * @event afterClose
		 * @this {Dialog} current dialog
		 */
		if (this.jodit && this.jodit.events) {
			this.jodit.events.fire(this, 'afterClose');
			this.jodit.events.fire(this.ownerWindow, 'jodit_close_dialog');
		}
	};

	constructor(jodit?: IViewBased, options: any = Config.prototype.dialog) {
		super(jodit, options);

		if (isJoditObject(jodit)) {
			this.window = jodit.ownerWindow;
			this.document = jodit.ownerDocument;

			jodit.events.on('beforeDestruct', () => {
				this.destruct();
			});
		}

		const self: Dialog = this;

		const opt =
			jodit && (jodit as View).options
				? (jodit as IJodit).options.dialog
				: Config.prototype.dialog;

		self.options = { ...opt, ...self.options } as IDialogOptions;

		self.container = this.create.fromHTML(
			'<div style="z-index:' +
			self.options.zIndex +
			'" class="jodit jodit_dialog_box">' +
			'<div class="jodit_dialog_overlay"></div>' +
			'<div class="jodit_dialog">' +
			'<div class="jodit_dialog_header non-selected">' +
			'<div class="jodit_dialog_header-title"></div>' +
			'<div class="jodit_dialog_header-toolbar"></div>' +
			'</div>' +
			'<div class="jodit_dialog_content"></div>' +
			'<div class="jodit_dialog_footer"></div>' +
			(self.options.resizable
				? '<div class="jodit_dialog_resizer"></div>'
				: '') +
			'</div>' +
			'</div>'
		) as HTMLDivElement;

		if (jodit && (<IViewBased>jodit).id) {
			self.container.setAttribute(
				'data-editor_id',
				(<IViewBased>jodit).id
			);
		}

		Object.defineProperty(self.container, '__jodit_dialog', {
			value: self
		});

		self.dialog = self.container.querySelector(
			'.jodit_dialog'
		) as HTMLDivElement;
		self.resizer = self.container.querySelector(
			'.jodit_dialog_resizer'
		) as HTMLDivElement;

		if (self.jodit && self.jodit.options && self.jodit.options.textIcons) {
			self.container.classList.add('jodit_text_icons');
		}

		self.dialogbox_header = self.container.querySelector(
			'.jodit_dialog_header>.jodit_dialog_header-title'
		) as HTMLHeadingElement;
		self.dialogbox_content = self.container.querySelector(
			'.jodit_dialog_content'
		) as HTMLDivElement;
		self.dialogbox_footer = self.container.querySelector(
			'.jodit_dialog_footer'
		) as HTMLDivElement;
		self.dialogbox_toolbar = self.container.querySelector(
			'.jodit_dialog_header>.jodit_dialog_header-toolbar'
		) as HTMLDivElement;

		self.destination.appendChild(self.container);

		self.container.addEventListener('close_dialog', self.close as any);

		self.toolbar = JoditToolbarCollection.makeCollection(self);
		self.toolbar.build(self.options.buttons, self.dialogbox_toolbar);

		self.events
			.on(this.window, 'mousemove', self.onMouseMove)
			.on(this.window, 'mouseup', self.onMouseUp)
			.on(this.window, 'keydown', self.onKeyDown)
			.on(this.window, 'resize', self.onResize);

		const headerBox: HTMLDivElement | null = self.container.querySelector(
			'.jodit_dialog_header'
		);

		headerBox &&
		headerBox.addEventListener(
			'mousedown',
			self.onHeaderMouseDown.bind(self)
		);

		if (self.options.resizable) {
			self.resizer.addEventListener(
				'mousedown',
				self.onResizerMouseDown.bind(self)
			);
		}

		Jodit.plugins.fullsize(self);
	}

	/**
	 * It destroys all objects created for the windows and also includes all the handlers for the window object
	 */
	destruct() {
		if (this.isDestructed) {
			return;
		}

		if (this.toolbar) {
			this.toolbar.destruct();
			delete this.toolbar;
		}

		if (this.events) {
			this.events
				.off(this.window, 'mousemove', this.onMouseMove)
				.off(this.window, 'mouseup', this.onMouseUp)
				.off(this.window, 'keydown', this.onKeyDown)
				.off(this.window, 'resize', this.onResize);
		}

		if (!this.jodit && this.events) {
			this.events.destruct();
			delete this.events;
		}

		if (this.container) {
			Dom.safeRemove(this.container);
			delete this.container;
		}

		super.destruct();
	}
}

import { Jodit } from '../../Jodit';
import { JoditToolbarCollection, ToolbarCollection } from '..';
