/*!
 * 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 * as consts from '../constants';
import { Plugin } from '../modules/Plugin';
import { Dom } from '../modules/Dom';
import { Table } from '../modules/Table';
import {
	$$,
	getContentWidth,
	offset,
	scrollIntoView
} from '../modules/helpers/';
import { setTimeout } from '../modules/helpers/async';
import { IControlType } from '../types/toolbar';
import { IBound, IDictionary } from '../types/types';
import { IJodit } from '../types';

declare module '../Config' {
	interface Config {
		/**
		 * Use module {@link TableProcessor|TableProcessor}
		 */
		useTableProcessor: boolean;
		useExtraClassesOptions: boolean;
	}
}

Config.prototype.useTableProcessor = true;
Config.prototype.useExtraClassesOptions = true;

Config.prototype.controls.table = {
	data: {
		cols: 10,
		rows: 10,
		classList: {
			'table table-bordered': 'Bootstrap Bordered',
			'table table-striped': 'Bootstrap Striped',
			'table table-dark': 'Bootstrap Dark'
		}
	},
	popup: (editor: IJodit, current, control, close, button) => {
		const default_rows_count: number =
				control.data && control.data.rows ? control.data.rows : 10,
			default_cols_count: number =
				control.data && control.data.cols ? control.data.cols : 10;

		const generateExtraClasses = (): string => {
			if (!editor.options.useExtraClassesOptions) {
				return '';
			}

			const out: string[] = [];

			if (control.data) {
				const classList: IDictionary<string> = control.data.classList;

				Object.keys(classList).forEach((classes: string) => {
					out.push(
						`<label><input value="${classes}" type="checkbox"/>${
							classList[classes]
						}</label>`
					);
				});
			}
			return out.join('');
		};

		const form: HTMLFormElement = editor.create.fromHTML(
				'<form class="jodit_form jodit_form_inserter">' +
					'<label>' +
					'<span>1</span> &times; <span>1</span>' +
					'</label>' +
					'<div class="jodit_form-table-creator-box">' +
					'<div class="jodit_form-container"></div>' +
					'<div class="jodit_form-options">' +
					generateExtraClasses() +
					'</div>' +
					'</div>' +
					'</form>'
			) as HTMLFormElement,
			rows: HTMLSpanElement = form.querySelectorAll('span')[0],
			cols: HTMLSpanElement = form.querySelectorAll('span')[1],
			blocksContainer: HTMLDivElement = form.querySelector(
				'.jodit_form-container'
			) as HTMLDivElement,
			mainBox: HTMLDivElement = form.querySelector(
				'.jodit_form-table-creator-box'
			) as HTMLDivElement,
			options: HTMLDivElement = form.querySelector(
				'.jodit_form-options'
			) as HTMLDivElement,
			cells: HTMLDivElement[] = [];

		const generateRows = (need_rows: number) => {
			const cnt: number = need_rows * default_cols_count;

			if (cells.length > cnt) {
				for (let i = cnt; i < cells.length; i += 1) {
					Dom.safeRemove(cells[i]);
					delete cells[i];
				}
				cells.length = cnt;
			}

			for (let i = 0; i < cnt; i += 1) {
				if (!cells[i]) {
					const div = editor.create.div();

					div.setAttribute('data-index', i.toString());
					cells.push(div);
				}
			}

			cells.forEach((cell: HTMLDivElement) => {
				blocksContainer.appendChild(cell);
			});

			const width = (cells[0].offsetWidth || 18) * default_cols_count;

			blocksContainer.style.width = width + 'px';
			mainBox.style.width = width + options.offsetWidth + 1 + 'px';
		};

		const mouseenter = (e: MouseEvent, index?: number): void => {
			const dv: HTMLDivElement = e.target as HTMLDivElement;

			if (!dv || dv.tagName !== 'DIV') {
				return;
			}

			let k =
				index === undefined || isNaN(index)
					? parseInt(dv.getAttribute('data-index') || '0', 10)
					: index || 0;

			const rows_count = Math.ceil((k + 1) / default_cols_count),
				cols_count = (k % default_cols_count) + 1;

			for (let i = 0; i < cells.length; i += 1) {
				if (
					cols_count >= (i % default_cols_count) + 1 &&
					rows_count >= Math.ceil((i + 1) / default_cols_count)
				) {
					cells[i].className = 'hovered';
				} else {
					cells[i].className = '';
				}
			}

			cols.innerText = cols_count.toString();
			rows.innerText = rows_count.toString();
		};

		blocksContainer.addEventListener('mousemove', mouseenter);

		editor.events.on(
			blocksContainer,
			'touchstart mousedown',
			(e: MouseEvent) => {
				const dv: HTMLDivElement = e.target as HTMLDivElement;

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

				if (dv.tagName !== 'DIV') {
					return;
				}

				let k = parseInt(dv.getAttribute('data-index') || '0', 10);

				const rows_count = Math.ceil((k + 1) / default_cols_count),
					cols_count = (k % default_cols_count) + 1;

				const crt = editor.create.inside,
					tbody: HTMLTableSectionElement = crt.element('tbody'),
					table: HTMLTableElement = crt.element('table');

				table.appendChild(tbody);

				table.style.width = '100%';

				let first_td: HTMLTableCellElement | null = null,
					tr: HTMLTableRowElement,
					td: HTMLTableCellElement;

				for (let i = 1; i <= rows_count; i += 1) {
					tr = crt.element('tr');

					for (let j = 1; j <= cols_count; j += 1) {
						td = crt.element('td');

						if (!first_td) {
							first_td = td;
						}

						td.appendChild(crt.element('br'));
						tr.appendChild(crt.text('\n'));
						tr.appendChild(crt.text('\t'));
						tr.appendChild(td);
					}

					tbody.appendChild(crt.text('\n'));
					tbody.appendChild(tr);
				}

				const crnt = editor.selection.current();

				if (crnt && editor.selection.isCollapsed()) {
					const block: HTMLElement | false = Dom.closest(
						crnt,
						node => Dom.isBlock(node, editor.editorWindow),
						editor.editor
					) as HTMLElement | false;

					if (
						block &&
						block !== editor.editor &&
						!block.nodeName.match(
							/^TD|TH|TBODY|TABLE|THEADER|TFOOTER$/
						)
					) {
						editor.selection.setCursorAfter(block);
					}
				}

				$$('input[type=checkbox]:checked', options).forEach(
					(input: HTMLElement) => {
						(input as HTMLInputElement).value
							.split(/[\s]+/)
							.forEach((className: string) => {
								table.classList.add(className);
							});
					}
				);

				editor.selection.insertNode(crt.text('\n'));
				editor.selection.insertNode(table, false);

				if (first_td) {
					editor.selection.setCursorIn(first_td);
					scrollIntoView(
						first_td,
						editor.editor,
						editor.editorDocument
					);
				}

				close();
			}
		);

		if (button && button.parentToolbar) {
			editor.events
				.off(
					button.parentToolbar.container as object,
					'afterOpenPopup.tableGenerator'
				)
				.on(
					button.parentToolbar.container as object,
					'afterOpenPopup.tableGenerator',
					() => {
						generateRows(default_rows_count);
						if (cells[0]) {
							cells[0].className = 'hovered';
						}
					},
					'',
					true
				);
		}

		return form;
	},
	tooltip: 'Insert table'
} as IControlType;

/**
 * Process tables in editor
 */
export class TableProcessor extends Plugin {
	static isCell(tag: Node | null): boolean {
		return !!tag && /^TD|TH$/i.test(tag.nodeName);
	}

	private __key: string = 'table_processor_observer';

	private __selectMode: boolean = false;

	private __resizerDelta: number = 0;
	private __resizerHandler: HTMLElement;

	showResizer() {
		clearTimeout(this.hideTimeout);
		this.__resizerHandler.style.display = 'block';
	}

	hideResizer() {
		clearTimeout(this.hideTimeout);
		this.hideTimeout = setTimeout(() => {
			this.__resizerHandler.style.display = 'none';
		}, this.jodit.defaultTimeout);
	}

	private hideTimeout: number;

	private __drag: boolean = false;

	private __wholeTable: boolean | null;
	private __workCell: HTMLTableCellElement;
	private __workTable: HTMLTableElement;

	private __minX: number;
	private __maxX: number;

	/**
	 *
	 * @param {HTMLTableElement} [table]
	 * @param {HTMLTableCellElement} [currentCell]
	 * @private
	 */
	private __deSelectAll(
		table?: HTMLTableElement,
		currentCell?: HTMLTableCellElement | false
	) {
		const cells: HTMLTableCellElement[] = table
			? Table.getAllSelectedCells(table)
			: Table.getAllSelectedCells(this.jodit.editor);

		if (cells.length) {
			cells.forEach((cell: HTMLTableCellElement) => {
				if (!currentCell || currentCell !== cell) {
					Table.restoreSelection(cell);
				}
			});
		}
	}

	/**
	 *
	 * @param {HTMLTableCellElement} cell
	 * @param {boolean|null} [wholeTable=null] true - resize whole table by left side,
	 * false - resize whole table by right side, null - resize column
	 * @private
	 */
	private __setWorkCell(
		cell: HTMLTableCellElement,
		wholeTable: boolean | null = null
	) {
		this.__wholeTable = wholeTable;
		this.__workCell = cell;
		this.__workTable = Dom.up(
			cell,
			(elm: Node | null) => elm && elm.nodeName === 'TABLE',
			this.jodit.editor
		) as HTMLTableElement;
	}

	private __addResizer = () => {
		if (!this.__resizerHandler) {
			this.__resizerHandler = this.jodit.container.querySelector(
				'.jodit_table_resizer'
			) as HTMLElement;

			if (!this.__resizerHandler) {
				this.__resizerHandler = this.jodit.create.div(
					'jodit_table_resizer'
				);

				let startX: number = 0;

				this.jodit.events
					.on(
						this.__resizerHandler,
						'mousedown.table touchstart.table',
						(event: MouseEvent) => {
							this.__drag = true;

							startX = event.clientX;

							this.jodit.lock(this.__key);
							this.__resizerHandler.classList.add(
								'jodit_table_resizer-moved'
							);

							let box: ClientRect,
								tableBox: ClientRect = this.__workTable.getBoundingClientRect();

							this.__minX = 0;
							this.__maxX = 1000000;

							if (this.__wholeTable !== null) {
								tableBox = (this.__workTable
									.parentNode as HTMLElement).getBoundingClientRect();
								this.__minX = tableBox.left;
								this.__maxX = tableBox.left + tableBox.width;
							} else {
								// find maximum columns
								const coordinate: number[] = Table.formalCoordinate(
									this.__workTable,
									this.__workCell,
									true
								);

								Table.formalMatrix(
									this.__workTable,
									(td, i, j) => {
										if (coordinate[1] === j) {
											box = td.getBoundingClientRect();
											this.__minX = Math.max(
												box.left + consts.NEARBY / 2,
												this.__minX
											);
										}
										if (coordinate[1] + 1 === j) {
											box = td.getBoundingClientRect();
											this.__maxX = Math.min(
												box.left +
													box.width -
													consts.NEARBY / 2,
												this.__maxX
											);
										}
									}
								);
							}

							return false;
						}
					)
					.on(this.__resizerHandler, 'mouseenter.table', () => {
						clearTimeout(this.hideTimeout);
					})
					.on(
						this.jodit.editorWindow,
						'mousemove.table touchmove.table',
						(event: MouseEvent) => {
							if (this.__drag) {
								let x = event.clientX;

								const workplacePosition: IBound = offset(
									(this.__resizerHandler.parentNode ||
										this.jodit.ownerDocument
											.documentElement) as HTMLElement,
									this.jodit,
									this.jodit.ownerDocument,
									true
								);

								if (x < this.__minX) {
									x = this.__minX;
								}
								if (x > this.__maxX) {
									x = this.__maxX;
								}

								this.__resizerDelta =
									x -
									startX +
									(!this.jodit.options.iframe
										? 0
										: workplacePosition.left);
								this.__resizerHandler.style.left =
									x -
									(this.jodit.options.iframe
										? 0
										: workplacePosition.left) +
									'px';

								const sel = this.jodit.selection.sel;

								sel && sel.removeAllRanges();

								if (event.preventDefault) {
									event.preventDefault();
								}
							}
						}
					);

				this.jodit.workplace.appendChild(this.__resizerHandler);
			}
		}
	};

	/**
	 * Calc helper resizer position
	 *
	 * @param {HTMLTableElement} table
	 * @param {HTMLTableCellElement} cell
	 * @param {int} [offsetX=0]
	 * @param {int} [delta=0]
	 *
	 * @private
	 */
	private __calcResizerPosition(
		table: HTMLTableElement,
		cell: HTMLTableCellElement,
		offsetX: number = 0,
		delta: number = 0
	) {
		const box = offset(cell, this.jodit, this.jodit.editorDocument);

		if (offsetX <= consts.NEARBY || box.width - offsetX <= consts.NEARBY) {
			const workplacePosition: IBound = offset(
					(this.__resizerHandler.parentNode ||
						this.jodit.ownerDocument
							.documentElement) as HTMLElement,
					this.jodit,
					this.jodit.ownerDocument,
					true
				),
				parentBox: IBound = offset(
					table,
					this.jodit,
					this.jodit.editorDocument
				);

			this.__resizerHandler.style.left =
				(offsetX <= consts.NEARBY ? box.left : box.left + box.width) -
				workplacePosition.left +
				delta +
				'px';

			this.__resizerHandler.style.height = parentBox.height + 'px';
			this.__resizerHandler.style.top =
				parentBox.top - workplacePosition.top + 'px';

			this.showResizer();

			if (offsetX <= consts.NEARBY) {
				const prevTD = Dom.prev(
					cell,
					TableProcessor.isCell,
					cell.parentNode as HTMLElement
				) as HTMLTableCellElement;
				if (prevTD) {
					this.__setWorkCell(prevTD);
				} else {
					this.__setWorkCell(cell, true);
				}
			} else {
				const nextTD = Dom.next(
					cell,
					TableProcessor.isCell,
					cell.parentNode as HTMLElement
				);
				this.__setWorkCell(cell, !nextTD ? false : null);
			}
		} else {
			this.hideResizer();
		}
	}

	/**
	 *
	 * @param {string} command
	 */
	private onExecCommand = (command: string): false | void => {
		if (
			/table(splitv|splitg|merge|empty|bin|binrow|bincolumn|addcolumn|addrow)/.test(
				command
			)
		) {
			command = command.replace('table', '');
			const cells = Table.getAllSelectedCells(this.jodit.editor);
			if (cells.length) {
				const cell: HTMLTableCellElement | undefined = cells.shift();

				if (!cell) {
					return;
				}

				const table = Dom.closest(
					cell,
					'table',
					this.jodit.editor
				) as HTMLTableElement;

				switch (command) {
					case 'splitv':
						Table.splitVertical(table);
						break;
					case 'splitg':
						Table.splitHorizontal(table);
						break;
					case 'merge':
						Table.mergeSelected(table);
						break;
					case 'empty':
						Table.getAllSelectedCells(this.jodit.editor).forEach(
							td => (td.innerHTML = '')
						);
						break;
					case 'bin':
						Dom.safeRemove(table);
						break;
					case 'binrow':
						Table.removeRow(
							table,
							(cell.parentNode as HTMLTableRowElement).rowIndex
						);
						break;
					case 'bincolumn':
						Table.removeColumn(table, cell.cellIndex);
						break;
					case 'addcolumnafter':
					case 'addcolumnbefore':
						Table.appendColumn(
							table,
							cell.cellIndex,
							command === 'addcolumnafter'
						);
						break;
					case 'addrowafter':
					case 'addrowbefore':
						Table.appendRow(
							table,
							cell.parentNode as HTMLTableRowElement,
							command === 'addrowafter'
						);
						break;
				}
			}
			return false;
		}
	};

	observe(table: HTMLTableElement) {
		(table as any)[this.__key] = true;

		let start: HTMLTableCellElement;

		this.jodit.events
			.on(
				table,
				'mousedown.table touchstart.table',
				(event: MouseEvent) => {
					if (this.jodit.options.readonly) {
						return;
					}

					const cell: HTMLTableCellElement = Dom.up(
						event.target as HTMLElement,
						TableProcessor.isCell,
						table
					) as HTMLTableCellElement;
					if (
						cell &&
						cell instanceof
							(this.jodit.editorWindow as any).HTMLElement
					) {
						if (!cell.firstChild) {
							cell.appendChild(
								this.jodit.editorDocument.createElement('br')
							);
						}

						start = cell;
						Table.addSelected(cell);
						this.__selectMode = true;
					}
				}
			)
			.on(table, 'mouseleave.table', (e: MouseEvent) => {
				if (
					this.__resizerHandler &&
					this.__resizerHandler !== e.relatedTarget
				) {
					this.hideResizer();
				}
			})
			.on(
				table,
				'mousemove.table touchmove.table',
				(event: MouseEvent) => {
					if (this.jodit.options.readonly) {
						return;
					}

					if (this.__drag || this.jodit.isLockedNotBy(this.__key)) {
						return;
					}

					const cell = Dom.up(
						event.target as HTMLElement,
						TableProcessor.isCell,
						table
					) as HTMLTableCellElement;

					if (cell) {
						if (this.__selectMode) {
							if (cell !== start) {
								this.jodit.lock(this.__key);

								const sel = this.jodit.selection.sel;
								sel && sel.removeAllRanges();

								if (event.preventDefault) {
									event.preventDefault();
								}
							}
							this.__deSelectAll(table);
							const bound = Table.getSelectedBound(table, [
									cell,
									start
								]),
								box = Table.formalMatrix(table);

							for (
								let i = bound[0][0];
								i <= bound[1][0];
								i += 1
							) {
								for (
									let j = bound[0][1];
									j <= bound[1][1];
									j += 1
								) {
									Table.addSelected(box[i][j]);
								}
							}

							const max = box[bound[1][0]][bound[1][1]],
								min = box[bound[0][0]][bound[0][1]];

							this.jodit.events.fire(
								'showPopup',
								table,
								(): IBound => {
									const minOffset: IBound = offset(
										min,
										this.jodit,
										this.jodit.editorDocument
									);
									const maxOffset: IBound = offset(
										max,
										this.jodit,
										this.jodit.editorDocument
									);

									return {
										left: minOffset.left,
										top: minOffset.top,
										width:
											maxOffset.left -
											minOffset.left +
											maxOffset.width,
										height:
											maxOffset.top -
											minOffset.top +
											maxOffset.height
									};
								}
							);

							event.stopPropagation();
						} else {
							this.__calcResizerPosition(
								table,
								cell,
								event.offsetX
							);
						}
					}
				}
			);

		this.__addResizer();
	}

	/**
	 *
	 * @param {Jodit} editor
	 */
	afterInit(editor: IJodit): void {
		if (!editor.options.useTableProcessor) {
			return;
		}

		editor.events
			.on(this.jodit.ownerWindow, 'mouseup.table touchend.table', () => {
				if (this.__selectMode || this.__drag) {
					this.__selectMode = false;
					this.jodit.unlock();
				}
				if (this.__resizerHandler && this.__drag) {
					this.__drag = false;
					this.__resizerHandler.classList.remove(
						'jodit_table_resizer-moved'
					);

					// resize column
					if (this.__wholeTable === null) {
						const __marked: HTMLTableCellElement[] = [];

						Table.setColumnWidthByDelta(
							this.__workTable,
							Table.formalCoordinate(
								this.__workTable,
								this.__workCell,
								true
							)[1],
							this.__resizerDelta,
							true,
							__marked
						);
						const nextTD = Dom.next(
							this.__workCell,
							TableProcessor.isCell,
							this.__workCell.parentNode as HTMLElement
						) as HTMLTableCellElement;

						Table.setColumnWidthByDelta(
							this.__workTable,
							Table.formalCoordinate(this.__workTable, nextTD)[1],
							-this.__resizerDelta,
							false,
							__marked
						);
					} else {
						const width = this.__workTable.offsetWidth,
							parentWidth = getContentWidth(
								this.__workTable.parentNode as HTMLElement,
								this.jodit.editorWindow
							);

						// right side
						if (!this.__wholeTable) {
							this.__workTable.style.width =
								((width + this.__resizerDelta) / parentWidth) *
									100 +
								'%';
						} else {
							const margin = parseInt(
								this.jodit.editorWindow.getComputedStyle(
									this.__workTable
								).marginLeft || '0',
								10
							);
							this.__workTable.style.width =
								((width - this.__resizerDelta) / parentWidth) *
									100 +
								'%';
							this.__workTable.style.marginLeft =
								((margin + this.__resizerDelta) / parentWidth) *
									100 +
								'%';
						}
					}

					editor.setEditorValue();
					editor.selection.focus();
				}
			})
			.on(this.jodit.ownerWindow, 'scroll.table', () => {
				if (this.__drag) {
					const parent = Dom.up(
						this.__workCell,
						(elm: Node | null) => elm && elm.nodeName === 'TABLE',
						editor.editor
					) as HTMLElement;
					if (parent) {
						const parentBox = parent.getBoundingClientRect();
						this.__resizerHandler.style.top = parentBox.top + 'px';
					}
				}
			})
			.on(
				this.jodit.ownerWindow,
				'mousedown.table touchend.table',
				(event: MouseEvent) => {
					// need use event['originalEvent'] because of IE can not set target from
					// another window to current window
					const current_cell: HTMLTableCellElement = Dom.closest(
						(event as any).originalEvent.target as HTMLElement,
						'TD|TH',
						this.jodit.editor
					) as HTMLTableCellElement;
					let table: HTMLTableElement | null = null;

					if (
						current_cell instanceof
						(this.jodit.editorWindow as any).HTMLTableCellElement
					) {
						table = Dom.closest(
							current_cell,
							'table',
							this.jodit.editor
						) as HTMLTableElement;
					}

					if (table) {
						this.__deSelectAll(
							table,
							current_cell instanceof
								(this.jodit.editorWindow as any)
									.HTMLTableCellElement
								? current_cell
								: false
						);
					} else {
						this.__deSelectAll();
					}
				}
			)
			.on('afterGetValueFromEditor.table', (data: { value: string }) => {
				const rxp = new RegExp(
					`([\s]*)${consts.JODIT_SELECTED_CELL_MARKER}="1"`,
					'g'
				);

				if (rxp.test(data.value)) {
					data.value = data.value.replace(rxp, '');
				}
			})
			.on('change.table afterCommand.table afterSetMode.table', () => {
				($$('table', editor.editor) as HTMLTableElement[]).forEach(
					(table: HTMLTableElement) => {
						if (!(table as any)[this.__key]) {
							this.observe(table);
						}
					}
				);
			})
			.on('beforeSetMode.table', () => {
				Table.getAllSelectedCells(editor.editor).forEach(td => {
					Table.restoreSelection(td);
					Table.normalizeTable(Dom.closest(
						td,
						'table',
						editor.editor
					) as HTMLTableElement);
				});
			})
			.on('keydown.table', (event: KeyboardEvent) => {
				if (event.which === consts.KEY_TAB) {
					($$('table', editor.editor) as HTMLTableElement[]).forEach(
						(table: HTMLTableElement) => {
							this.__deSelectAll(table);
						}
					);
				}
			})
			.on('beforeCommand.table', this.onExecCommand.bind(this));
	}

	beforeDestruct(jodit: IJodit): void {
		if (jodit.events) {
			jodit.events.off(this.jodit.ownerWindow, '.table');
			jodit.events.off('.table');
		}
	}
}
