/*!
 * 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
 */

/**
 * Module for working with tables . Delete, insert , merger, division of cells , rows and columns.
 * When creating elements such as <table> for each of them
 * creates a new instance Jodit.modules.TableProcessor and it can be accessed via $('table').data('table-processor')
 *
 * @module Table
 * @param {Object} parent Jodit main object
 * @param {HTMLTableElement} table Table for which to create a module
 */

import * as consts from '../constants';
import { Dom } from './Dom';
import { $$, each, trim } from './helpers/';

export class Table {
	public static addSelected(td: HTMLTableCellElement) {
		td.setAttribute(consts.JODIT_SELECTED_CELL_MARKER, '1');
	}
	public static restoreSelection(td: HTMLTableCellElement) {
		td.removeAttribute(consts.JODIT_SELECTED_CELL_MARKER);
	}

	/**
	 *
	 * @param {HTMLTableElement} table
	 * @return {HTMLTableCellElement[]}
	 */
	public static getAllSelectedCells(
		table: HTMLElement | HTMLTableElement
	): HTMLTableCellElement[] {
		return table
			? ($$(
					`td[${consts.JODIT_SELECTED_CELL_MARKER}],th[${
						consts.JODIT_SELECTED_CELL_MARKER
					}]`,
					table
			  ) as HTMLTableCellElement[])
			: [];
	}

	/**
	 * @param {HTMLTableElement} table
	 * @return {number}
	 */
	public static getRowsCount(table: HTMLTableElement) {
		return table.rows.length;
	}

	/**
	 * @param {HTMLTableElement} table
	 * @return {number}
	 */
	public static getColumnsCount(table: HTMLTableElement) {
		const matrix = Table.formalMatrix(table);
		return matrix.reduce((max_count, cells) => {
			return Math.max(max_count, cells.length);
		}, 0);
	}

	/**
	 *
	 * @param {HTMLTableElement} table
	 * @param {function(HTMLTableCellElement, int, int, int, int):boolean} [callback] if return false cycle break
	 * @return {Array}
	 */
	public static formalMatrix(
		table: HTMLTableElement,
		callback?: (
			cell: HTMLTableCellElement,
			row: number,
			col: number,
			colSpan: number,
			rowSpan: number
		) => false | void
	): HTMLTableCellElement[][] {
		const matrix: HTMLTableCellElement[][] = [[]];
		const rows = Array.prototype.slice.call(table.rows);

		const setCell = (
			cell: HTMLTableCellElement,
			i: number
		): false | HTMLTableCellElement[][] | void => {
			if (matrix[i] === undefined) {
				matrix[i] = [];
			}

			const colSpan: number = cell.colSpan,
				rowSpan = cell.rowSpan;
			let column: number,
				row: number,
				currentColumn: number = 0;

			while (matrix[i][currentColumn]) {
				currentColumn += 1;
			}

			for (row = 0; row < rowSpan; row += 1) {
				for (column = 0; column < colSpan; column += 1) {
					if (matrix[i + row] === undefined) {
						matrix[i + row] = [];
					}
					if (
						callback &&
						callback(
							cell,
							i + row,
							currentColumn + column,
							colSpan,
							rowSpan
						) === false
					) {
						return false;
					}
					matrix[i + row][currentColumn + column] = cell;
				}
			}
		};

		for (let i = 0, j; i < rows.length; i += 1) {
			const cells = Array.prototype.slice.call(rows[i].cells);
			for (j = 0; j < cells.length; j += 1) {
				if (setCell(cells[j], i) === false) {
					return matrix;
				}
			}
		}

		return matrix;
	}

	/**
	 * Get cell coordinate in formal table (without colspan and rowspan)
	 */
	public static formalCoordinate(
		table: HTMLTableElement,
		cell: HTMLTableCellElement,
		max = false
	): number[] {
		let i: number = 0,
			j: number = 0,
			width: number = 1,
			height: number = 1;

		Table.formalMatrix(
			table,
			(
				td: HTMLTableCellElement,
				ii: number,
				jj: number,
				colSpan: number | void,
				rowSpan: number | void
			): false | void => {
				if (cell === td) {
					i = ii;
					j = jj;
					width = colSpan || 1;
					height = rowSpan || 1;
					if (max) {
						j += (colSpan || 1) - 1;
						i += (rowSpan || 1) - 1;
					}
					return false;
				}
			}
		);

		return [i, j, width, height];
	}

	/**
	 * Inserts a new line after row what contains the selected cell
	 *
	 * @param {HTMLTableElement} table
	 * @param {Boolean|HTMLTableRowElement} [line=false] Insert a new line after/before this
	 * line contains the selected cell
	 * @param {Boolean} [after=true] Insert a new line after line contains the selected cell
	 */
	public static appendRow(
		table: HTMLTableElement,
		line: false | HTMLTableRowElement = false,
		after = true
	) {
		const doc: Document = table.ownerDocument || document,
			columnsCount: number = Table.getColumnsCount(table),
			row: HTMLTableRowElement = doc.createElement('tr');

		for (let j: number = 0; j < columnsCount; j += 1) {
			row.appendChild(doc.createElement('td'));
		}

		if (after && line && line.nextSibling) {
			line.parentNode &&
				line.parentNode.insertBefore(row, line.nextSibling);
		} else if (!after && line) {
			line.parentNode && line.parentNode.insertBefore(row, line);
		} else {
			($$(':scope>tbody', table)[0] || table).appendChild(row);
		}
	}

	/**
	 * Remove row
	 *
	 * @param {HTMLTableElement} table
	 * @param {int} rowIndex
	 */
	public static removeRow(table: HTMLTableElement, rowIndex: number) {
		const box = Table.formalMatrix(table);
		let dec: boolean;
		const row = table.rows[rowIndex];

		each<HTMLTableCellElement>(
			box[rowIndex],
			(j: number, cell: HTMLTableCellElement) => {
				dec = false;
				if (rowIndex - 1 >= 0 && box[rowIndex - 1][j] === cell) {
					dec = true;
				} else if (box[rowIndex + 1] && box[rowIndex + 1][j] === cell) {
					if (
						cell.parentNode === row &&
						cell.parentNode.nextSibling
					) {
						dec = true;
						let nextCell = j + 1;
						while (box[rowIndex + 1][nextCell] === cell) {
							nextCell += 1;
						}

						const nextRow: HTMLTableRowElement = Dom.next(
							cell.parentNode,
							(elm: Node | null) =>
								elm &&
								elm.nodeType === Node.ELEMENT_NODE &&
								elm.nodeName === 'TR',
							table
						) as HTMLTableRowElement;

						if (box[rowIndex + 1][nextCell]) {
							nextRow.insertBefore(
								cell,
								box[rowIndex + 1][nextCell]
							);
						} else {
							nextRow.appendChild(cell);
						}
					}
				} else {
					Dom.safeRemove(cell);
				}
				if (
					dec &&
					(cell.parentNode === row || cell !== box[rowIndex][j - 1])
				) {
					const rowSpan: number = cell.rowSpan;
					if (rowSpan - 1 > 1) {
						cell.setAttribute('rowspan', (rowSpan - 1).toString());
					} else {
						cell.removeAttribute('rowspan');
					}
				}
			}
		);

		Dom.safeRemove(row);
	}

	/**
	 * Insert column before / after all the columns containing the selected cells
	 *
	 */
	public static appendColumn(
		table: HTMLTableElement,
		j: number,
		after = true
	) {
		const box: HTMLTableCellElement[][] = Table.formalMatrix(table);
		let i: number;

		if (j === undefined) {
			j = Table.getColumnsCount(table) - 1;
		}

		for (i = 0; i < box.length; i += 1) {
			const cell: HTMLTableCellElement = (
				table.ownerDocument || document
			).createElement('td');
			const td: HTMLTableCellElement = box[i][j];
			let added: boolean = false;
			if (after) {
				if (
					(box[i] && td && j + 1 >= box[i].length) ||
					td !== box[i][j + 1]
				) {
					if (td.nextSibling) {
						td.parentNode &&
							td.parentNode.insertBefore(cell, td.nextSibling);
					} else {
						td.parentNode && td.parentNode.appendChild(cell);
					}
					added = true;
				}
			} else {
				if (
					j - 1 < 0 ||
					(box[i][j] !== box[i][j - 1] && box[i][j].parentNode)
				) {
					td.parentNode &&
						td.parentNode.insertBefore(cell, box[i][j]);
					added = true;
				}
			}
			if (!added) {
				box[i][j].setAttribute(
					'colspan',
					(
						parseInt(box[i][j].getAttribute('colspan') || '1', 10) +
						1
					).toString()
				);
			}
		}
	}

	/**
	 * Remove column by index
	 *
	 * @param {HTMLTableElement} table
	 * @param {int} [j]
	 */
	public static removeColumn(table: HTMLTableElement, j: number) {
		const box: HTMLTableCellElement[][] = Table.formalMatrix(table);

		let dec: boolean;
		each(box, (i: number, cells: HTMLTableCellElement[]) => {
			const td: HTMLTableCellElement = cells[j];
			dec = false;
			if (j - 1 >= 0 && box[i][j - 1] === td) {
				dec = true;
			} else if (j + 1 < cells.length && box[i][j + 1] === td) {
				dec = true;
			} else {
				Dom.safeRemove(td);
			}
			if (dec && (i - 1 < 0 || td !== box[i - 1][j])) {
				const colSpan: number = td.colSpan;
				if (colSpan - 1 > 1) {
					td.setAttribute('colspan', (colSpan - 1).toString());
				} else {
					td.removeAttribute('colspan');
				}
			}
		});
	}

	/**
	 * Define bound for selected cells
	 *
	 * @param {HTMLTableElement} table
	 * @param {Array.<HTMLTableCellElement>} selectedCells
	 * @return {number[][]}
	 */
	public static getSelectedBound(
		table: HTMLTableElement,
		selectedCells: HTMLTableCellElement[]
	): number[][] {
		const bound = [[Infinity, Infinity], [0, 0]];
		const box = Table.formalMatrix(table);
		let i: number, j: number, k: number;

		for (i = 0; i < box.length; i += 1) {
			for (j = 0; j < box[i].length; j += 1) {
				if (selectedCells.indexOf(box[i][j]) !== -1) {
					bound[0][0] = Math.min(i, bound[0][0]);
					bound[0][1] = Math.min(j, bound[0][1]);
					bound[1][0] = Math.max(i, bound[1][0]);
					bound[1][1] = Math.max(j, bound[1][1]);
				}
			}
		}
		for (i = bound[0][0]; i <= bound[1][0]; i += 1) {
			for (k = 1, j = bound[0][1]; j <= bound[1][1]; j += 1) {
				while (box[i][j - k] && box[i][j] === box[i][j - k]) {
					bound[0][1] = Math.min(j - k, bound[0][1]);
					bound[1][1] = Math.max(j - k, bound[1][1]);
					k += 1;
				}
				k = 1;
				while (box[i][j + k] && box[i][j] === box[i][j + k]) {
					bound[0][1] = Math.min(j + k, bound[0][1]);
					bound[1][1] = Math.max(j + k, bound[1][1]);
					k += 1;
				}
				k = 1;
				while (box[i - k] && box[i][j] === box[i - k][j]) {
					bound[0][0] = Math.min(i - k, bound[0][0]);
					bound[1][0] = Math.max(i - k, bound[1][0]);
					k += 1;
				}
				k = 1;
				while (box[i + k] && box[i][j] === box[i + k][j]) {
					bound[0][0] = Math.min(i + k, bound[0][0]);
					bound[1][0] = Math.max(i + k, bound[1][0]);
					k += 1;
				}
			}
		}

		return bound;
	}

	/**
	 *
	 * @param {HTMLTableElement} table
	 */
	public static normalizeTable(table: HTMLTableElement) {
		let i: number, j: number, min: number, not: boolean;

		const __marked: HTMLTableCellElement[] = [],
			box: HTMLTableCellElement[][] = Table.formalMatrix(table);

		// remove extra colspans
		for (j = 0; j < box[0].length; j += 1) {
			min = 1000000;
			not = false;
			for (i = 0; i < box.length; i += 1) {
				if (box[i][j] === undefined) {
					continue; // broken table
				}
				if (box[i][j].colSpan < 2) {
					not = true;
					break;
				}
				min = Math.min(min, box[i][j].colSpan);
			}
			if (!not) {
				for (i = 0; i < box.length; i += 1) {
					if (box[i][j] === undefined) {
						continue; // broken table
					}
					Table.__mark(
						box[i][j],
						'colspan',
						box[i][j].colSpan - min + 1,
						__marked
					);
				}
			}
		}

		// remove extra rowspans
		for (i = 0; i < box.length; i += 1) {
			min = 1000000;
			not = false;
			for (j = 0; j < box[i].length; j += 1) {
				if (box[i][j] === undefined) {
					continue; // broken table
				}
				if (box[i][j].rowSpan < 2) {
					not = true;
					break;
				}
				min = Math.min(min, box[i][j].rowSpan);
			}
			if (!not) {
				for (j = 0; j < box[i].length; j += 1) {
					if (box[i][j] === undefined) {
						continue; // broken table
					}
					Table.__mark(
						box[i][j],
						'rowspan',
						box[i][j].rowSpan - min + 1,
						__marked
					);
				}
			}
		}

		// remove rowspans and colspans equal 1 and empty class
		for (i = 0; i < box.length; i += 1) {
			for (j = 0; j < box[i].length; j += 1) {
				if (box[i][j] === undefined) {
					continue; // broken table
				}
				if (
					box[i][j].hasAttribute('rowspan') &&
					box[i][j].rowSpan === 1
				) {
					box[i][j].removeAttribute('rowspan');
				}
				if (
					box[i][j].hasAttribute('colspan') &&
					box[i][j].colSpan === 1
				) {
					box[i][j].removeAttribute('colspan');
				}
				if (
					box[i][j].hasAttribute('class') &&
					!box[i][j].getAttribute('class')
				) {
					box[i][j].removeAttribute('class');
				}
			}
		}

		Table.__unmark(__marked);
	}

	/**
	 * It combines all of the selected cells into one. The contents of the cells will also be combined
	 *
	 * @param {HTMLTableElement} table
	 *
	 */
	public static mergeSelected(table: HTMLTableElement) {
		const html: string[] = [],
			bound: number[][] = Table.getSelectedBound(
				table,
				Table.getAllSelectedCells(table)
			);
		let w: number = 0,
			first: HTMLTableCellElement | null = null,
			first_j: number = 0,
			td: HTMLTableCellElement,
			cols: number = 0,
			rows: number = 0;

		const __marked: HTMLTableCellElement[] = [];

		if (bound && (bound[0][0] - bound[1][0] || bound[0][1] - bound[1][1])) {
			Table.formalMatrix(
				table,
				(
					cell: HTMLTableCellElement,
					i: number,
					j: number,
					cs: number,
					rs: number
				) => {
					if (i >= bound[0][0] && i <= bound[1][0]) {
						if (j >= bound[0][1] && j <= bound[1][1]) {
							td = cell;

							if ((td as any).__i_am_already_was) {
								return;
							}

							(td as any).__i_am_already_was = true;

							if (i === bound[0][0] && td.style.width) {
								w += td.offsetWidth;
							}

							if (
								trim(
									cell.innerHTML.replace(/<br(\/)?>/g, '')
								) !== ''
							) {
								html.push(cell.innerHTML);
							}

							if (cs > 1) {
								cols += cs - 1;
							}
							if (rs > 1) {
								rows += rs - 1;
							}

							if (!first) {
								first = cell as HTMLTableCellElement;
								first_j = j;
							} else {
								Table.__mark(td, 'remove', 1, __marked);
							}
						}
					}
				}
			);

			cols = bound[1][1] - bound[0][1] + 1;
			rows = bound[1][0] - bound[0][0] + 1;

			if (first) {
				if (cols > 1) {
					Table.__mark(first, 'colspan', cols, __marked);
				}
				if (rows > 1) {
					Table.__mark(first, 'rowspan', rows, __marked);
				}

				if (w) {
					Table.__mark(
						first,
						'width',
						((w / table.offsetWidth) * 100).toFixed(
							consts.ACCURACY
						) + '%',
						__marked
					);
					if (first_j) {
						Table.setColumnWidthByDelta(
							table,
							first_j,
							0,
							true,
							__marked
						);
					}
				}

				(first as HTMLTableCellElement).innerHTML = html.join('<br/>');

				delete (first as any).__i_am_already_was;

				Table.__unmark(__marked);

				Table.normalizeTable(table);

				each(Array.from(table.rows), (index, tr) => {
					if (!tr.cells.length) {
						Dom.safeRemove(tr);
					}
				});
			}
		}
	}

	/**
	 * Divides all selected by `jodit_focused_cell` class table cell in 2 parts vertical. Those division into 2 columns
	 */
	public static splitHorizontal(table: HTMLTableElement) {
		let coord: number[],
			td: HTMLTableCellElement,
			tr: HTMLTableRowElement,
			parent: HTMLTableRowElement,
			after: HTMLTableCellElement;

		const __marked: HTMLTableCellElement[] = [];

		const doc: Document = table.ownerDocument || document;

		Table.getAllSelectedCells(table).forEach(
			(cell: HTMLTableCellElement) => {
				td = doc.createElement('td');
				td.appendChild(doc.createElement('br'));
				tr = doc.createElement('tr');

				coord = Table.formalCoordinate(table, cell);

				if (cell.rowSpan < 2) {
					Table.formalMatrix(table, (tdElm, i, j) => {
						if (
							coord[0] === i &&
							coord[1] !== j &&
							tdElm !== cell
						) {
							Table.__mark(
								tdElm,
								'rowspan',
								tdElm.rowSpan + 1,
								__marked
							);
						}
					});
					Dom.after(
						Dom.closest(cell, 'tr', table) as HTMLTableRowElement,
						tr
					);
					tr.appendChild(td);
				} else {
					Table.__mark(cell, 'rowspan', cell.rowSpan - 1, __marked);
					Table.formalMatrix(
						table,
						(tdElm: HTMLTableCellElement, i: number, j: number) => {
							if (
								i > coord[0] &&
								i < coord[0] + cell.rowSpan &&
								coord[1] > j &&
								(tdElm.parentNode as HTMLTableRowElement)
									.rowIndex === i
							) {
								after = tdElm;
							}
							if (coord[0] < i && tdElm === cell) {
								parent = table.rows[i];
							}
						}
					);
					if (after) {
						Dom.after(after, td);
					} else {
						parent.insertBefore(td, parent.firstChild);
					}
				}

				if (cell.colSpan > 1) {
					Table.__mark(td, 'colspan', cell.colSpan, __marked);
				}

				Table.__unmark(__marked);
				Table.restoreSelection(cell);
			}
		);
		this.normalizeTable(table);
	}

	/**
	 * It splits all the selected cells into 2 parts horizontally. Those. are added new row
	 *
	 * @param {HTMLTableElement} table
	 */
	public static splitVertical(table: HTMLTableElement) {
		let coord: number[], td: HTMLTableCellElement, percentage: number;

		const __marked: HTMLTableCellElement[] = [];
		const doc: Document = table.ownerDocument || document;

		Table.getAllSelectedCells(table).forEach(
			(cell: HTMLTableCellElement) => {
				coord = Table.formalCoordinate(table, cell);
				if (cell.colSpan < 2) {
					Table.formalMatrix(table, (tdElm, i, j) => {
						if (
							coord[1] === j &&
							coord[0] !== i &&
							tdElm !== cell
						) {
							Table.__mark(
								tdElm,
								'colspan',
								tdElm.colSpan + 1,
								__marked
							);
						}
					});
				} else {
					Table.__mark(cell, 'colspan', cell.colSpan - 1, __marked);
				}

				td = doc.createElement('td');
				td.appendChild(doc.createElement('br'));

				if (cell.rowSpan > 1) {
					Table.__mark(td, 'rowspan', cell.rowSpan, __marked);
				}

				const oldWidth = cell.offsetWidth; // get old width

				Dom.after(cell, td);

				percentage = oldWidth / table.offsetWidth / 2;

				Table.__mark(
					cell,
					'width',
					(percentage * 100).toFixed(consts.ACCURACY) + '%',
					__marked
				);
				Table.__mark(
					td,
					'width',
					(percentage * 100).toFixed(consts.ACCURACY) + '%',
					__marked
				);
				Table.__unmark(__marked);

				Table.restoreSelection(cell);
			}
		);
		Table.normalizeTable(table);
	}

	/**
	 * Set column width used delta value
	 *
	 * @param {HTMLTableElement} table
	 * @param {int} j column
	 * @param {int} delta
	 * @param {boolean} noUnmark
	 * @param {HTMLTableCellElement[]} __marked
	 */
	public static setColumnWidthByDelta(
		table: HTMLTableElement,
		j: number,
		delta: number,
		noUnmark: boolean,
		__marked: HTMLTableCellElement[]
	) {
		const box = Table.formalMatrix(table);
		let i: number, w: number, percent: number;

		for (i = 0; i < box.length; i += 1) {
			w = box[i][j].offsetWidth;
			percent = ((w + delta) / table.offsetWidth) * 100;
			Table.__mark(
				box[i][j],
				'width',
				percent.toFixed(consts.ACCURACY) + '%',
				__marked
			);
		}

		if (!noUnmark) {
			Table.__unmark(__marked);
		}
	}

	/**
	 *
	 * @param {HTMLTableCellElement} cell
	 * @param {string} key
	 * @param {string} value
	 * @param {HTMLTableCellElement[]} __marked
	 * @private
	 */
	private static __mark(
		cell: HTMLTableCellElement,
		key: string,
		value: string | number,
		__marked: HTMLTableCellElement[]
	) {
		__marked.push(cell);
		if (!(cell as any).__marked_value) {
			(cell as any).__marked_value = {};
		}
		(cell as any).__marked_value[key] = value === undefined ? 1 : value;
	}

	private static __unmark(__marked: HTMLTableCellElement[]) {
		__marked.forEach(cell => {
			if ((cell as any).__marked_value) {
				each(
					(cell as any).__marked_value,
					(key: string, value: number) => {
						switch (key) {
							case 'remove':
								Dom.safeRemove(cell);
								break;
							case 'rowspan':
								if (value > 1) {
									cell.setAttribute(
										'rowspan',
										value.toString()
									);
								} else {
									cell.removeAttribute('rowspan');
								}
								break;
							case 'colspan':
								if (value > 1) {
									cell.setAttribute(
										'colspan',
										value.toString()
									);
								} else {
									cell.removeAttribute('colspan');
								}
								break;
							case 'width':
								cell.style.width = value.toString();
								break;
						}
						delete (cell as any).__marked_value[key];
					}
				);
				delete (cell as any).__marked_value;
			}
		});
	}
}
