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

( function() {
	'use strict';

	var fakeSelectedClass = 'cke_table-faked-selection',
		fakeSelectedEditorClass = fakeSelectedClass + '-editor',
		fakeSelectedTableDataAttribute = 'cke-table-faked-selection-table',
		fakeSelection = { active: false },
		tabletools,
		getSelectedCells,
		getCellColIndex,
		insertRow,
		insertColumn;

	function isWidget( element ) {
		return CKEDITOR.plugins.widget && CKEDITOR.plugins.widget.isDomWidget( element );
	}

	function getCellsBetween( first, last ) {
		var firstTable = first.getAscendant( 'table' ),
			lastTable = last.getAscendant( 'table' ),
			map = CKEDITOR.tools.buildTableMap( firstTable ),
			startRow = getRowIndex( first ),
			endRow = getRowIndex( last ),
			cells = [],
			markers = {},
			start,
			end,
			i,
			j,
			cell;

		// Support selection that began in outer's table, but ends in nested one.
		if ( firstTable.contains( lastTable ) ) {
			last = last.getAscendant( { td: 1, th: 1 } );
			endRow = getRowIndex( last );
		}

		// First fetch start and end offset.
		if ( startRow > endRow ) {
			i = startRow;
			startRow = endRow;
			endRow = i;

			i = first;
			first = last;
			last = i;
		}

		for ( i = 0; i < map[ startRow ].length; i++ ) {
			if ( first.$ === map[ startRow ][ i ] ) {
				start = i;
				break;
			}
		}

		for ( i = 0; i < map[ endRow ].length; i++ ) {
			if ( last.$ === map[ endRow ][ i ] ) {
				end = i;
				break;
			}
		}

		if ( start > end ) {
			i = start;
			start = end;
			end = i;
		}

		for ( i = startRow; i <= endRow; i++ ) {
			for ( j = start; j <= end; j++ ) {
				// Table maps treat cells with colspan/rowspan as a separate cells, e.g.
				// td[colspan=2] produces two adjacent cells in map. Therefore we mark
				// all cells to know which were already processed.
				cell = new CKEDITOR.dom.element( map[ i ][ j ] );

				if ( cell.$ && !cell.getCustomData( 'selected_cell' ) ) {
					cells.push( cell );
					CKEDITOR.dom.element.setMarker( markers, cell, 'selected_cell', true );
				}
			}
		}

		CKEDITOR.dom.element.clearAllMarkers( markers );

		return cells;
	}

	function detectLeftMouseButton( evt ) {
		return CKEDITOR.tools.getMouseButton( evt ) === CKEDITOR.MOUSE_BUTTON_LEFT;
	}

	// Checks whether a given range fully contains a table element (cell/tbody/table etc).
	// @param {CKEDITOR.dom.range} range
	// @returns {Boolean}
	function rangeContainsTableElement( range ) {
		if ( range ) {
			// Clone the range as we're going to enlarge it, and we don't want to modify the input.
			range = range.clone();

			range.enlarge( CKEDITOR.ENLARGE_ELEMENT );

			var enclosedNode = range.getEnclosedNode();

			return enclosedNode && enclosedNode.is && enclosedNode.is( CKEDITOR.dtd.$tableContent );
		}
	}

	function getFakeSelectedTable( editor ) {
		var selectedCell = editor.editable().findOne( '.' + fakeSelectedClass );

		return selectedCell && selectedCell.getAscendant( 'table' );
	}

	function clearFakeCellSelection( editor, reset ) {
		var selectedCells = editor.editable().find( '.' + fakeSelectedClass ),
			i;

		editor.fire( 'lockSnapshot' );

		editor.editable().removeClass( fakeSelectedEditorClass );

		for ( i = 0; i < selectedCells.count(); i++ ) {
			selectedCells.getItem( i ).removeClass( fakeSelectedClass );
		}

		if ( selectedCells.count() > 0 ) {
			selectedCells.getItem( 0 ).getAscendant( 'table' ).data( fakeSelectedTableDataAttribute, false );
		}

		editor.fire( 'unlockSnapshot' );

		if ( reset ) {
			fakeSelection = { active: false };

			// Reset fake selection only if it's really a table one.
			// Otherwise we'll make widget selection unusable.
			if ( editor.getSelection().isInTable() ) {
				editor.getSelection().reset();
			}
		}
	}

	function fakeSelectCells( editor, cells ) {
		var ranges = [],
			range,
			i;

		for ( i = 0; i < cells.length; i++ ) {
			range = editor.createRange();

			range.setStartBefore( cells[ i ] );
			range.setEndAfter( cells[ i ] );

			ranges.push( range );
		}

		editor.getSelection().selectRanges( ranges );
	}

	function restoreFakeSelection( editor ) {
		var cells = editor.editable().find( '.' + fakeSelectedClass );

		if ( cells.count() < 1 ) {
			return;
		}

		cells = getCellsBetween( cells.getItem( 0 ), cells.getItem( cells.count() - 1 ) );

		fakeSelectCells( editor, cells );
	}

	function fakeSelectByMouse( editor, cellOrTable, evt ) {
		var selectedCells = getSelectedCells( editor.getSelection( true ) ),
			cell = !cellOrTable.is( 'table' ) ? cellOrTable : null,
			cells;

		// getSelectedCells treats cells with cursor in them as also selected.
		// We don't.
		function areCellsReallySelected( selection, selectedCells ) {
			var ranges = selection.getRanges();

			if ( selectedCells.length > 1 || ( ranges[ 0 ] && !ranges[ 0 ].collapsed ) ) {
				return true;
			}

			return false;
		}

		// Only start selecting when the fakeSelection.active is true (left mouse button is pressed)
		// and there are some cells selected or the click was done in the table cell.
		if ( fakeSelection.active && !fakeSelection.first &&
			( cell || areCellsReallySelected( editor.getSelection(), selectedCells ) ) ) {
			fakeSelection.first = cell || selectedCells[ 0 ];
			fakeSelection.dirty = cell ? false : ( selectedCells.length !== 1 );

			return;
		}

		if ( !fakeSelection.active ) {
			return;
		}

		// We should check if the newly selected cell is still inside the same table (https://dev.ckeditor.com/ticket/17052, #493).
		if ( cell && fakeSelection.first.getAscendant( 'table' ).equals( cell.getAscendant( 'table' ) ) ) {
			cells = getCellsBetween( fakeSelection.first, cell );

			// The selection is inside one cell, so we should allow native selection,
			// but only in case if no other cell between mousedown and mouseup
			// was selected.
			// We don't want to clear selection if widget is event target (#1027).
			if ( !fakeSelection.dirty && cells.length === 1 && !( isWidget( evt.data.getTarget() ) ) ) {
				return clearFakeCellSelection( editor, evt.name === 'mouseup' );
			}

			fakeSelection.dirty = true;
			fakeSelection.last = cell;

			fakeSelectCells( editor, cells );
		}
	}

	function fakeSelectionChangeHandler( evt ) {
		var editor = evt.editor || evt.sender.editor,
			selection = editor && editor.getSelection(),
			ranges = selection && selection.getRanges() || [],
			cells,
			table,
			i;

		if ( !selection ) {
			return;
		}

		clearFakeCellSelection( editor );

		if ( !selection.isInTable() || !selection.isFake ) {
			return;
		}

		// In case of whole nested table selection, getSelectedCells returns also
		// cell which contains the table. We should filter it.
		if ( ranges.length === 1 && ranges[ 0 ]._getTableElement() &&
			ranges[ 0 ]._getTableElement().is( 'table' ) ) {
			table = ranges[ 0 ]._getTableElement();
		}

		cells = getSelectedCells( selection, table );

		editor.fire( 'lockSnapshot' );

		for ( i = 0; i < cells.length; i++ ) {
			cells[ i ].addClass( fakeSelectedClass );
		}

		if ( cells.length > 0 ) {
			editor.editable().addClass( fakeSelectedEditorClass );
			cells[ 0 ].getAscendant( 'table' ).data( fakeSelectedTableDataAttribute, '' );
		}

		editor.fire( 'unlockSnapshot' );
	}

	function getRowIndex( rowOrCell ) {
		return rowOrCell.getAscendant( 'tr', true ).$.rowIndex;
	}

	function fakeSelectionMouseHandler( evt ) {
		// Prevent of throwing error in console if target is undefined (#515).
		if ( !evt.data.getTarget().getName ) {
			return;
		}
		// Prevent applying table selection when widget is selected.
		// Mouseup remains a possibility to finish table selection when user release mouse button above widget in table.
		if ( evt.name !== 'mouseup' && isWidget( evt.data.getTarget() ) ) {
			return;
		}

		var editor = evt.editor || evt.listenerData.editor,
			selection = editor.getSelection( 1 ),
			selectedTable = getFakeSelectedTable( editor ),
			target = evt.data.getTarget(),
			cell = target && target.getAscendant( { td: 1, th: 1 }, true ),
			table = target && target.getAscendant( 'table', true ),
			tableElements = { table: 1, thead: 1, tbody: 1, tfoot: 1, tr: 1, td: 1, th: 1 };

		// Nested tables should be treated as the same one (e.g. user starts dragging from outer table
		// and ends in inner one).
		function isSameTable( selectedTable, table ) {
			if ( !selectedTable || !table ) {
				return false;
			}

			return selectedTable.equals( table ) || selectedTable.contains( table ) || table.contains( selectedTable ) ||
				selectedTable.getCommonAncestor( table ).is( tableElements );
		}

		function isOutsideTable( node ) {
			return !node.getAscendant( 'table', true ) && node.getDocument().equals( editor.document );
		}

		function canClearSelection( evt, selection, selectedTable, table ) {
			// User starts click outside the table or not in the same table as in the previous selection.
			if ( evt.name === 'mousedown' && ( detectLeftMouseButton( evt ) || !table ) ) {
				return true;
			}

			var isProperMouseEvent = evt.name === ( CKEDITOR.env.gecko ? 'mousedown' : 'mouseup' );
			// Covers a case when:
			// 1. User releases mouse button outside the table.
			// 2. User opens context menu outside of selection.
			// Use 'mousedown' for Firefox, as it doesn't fire 'mouseup' when mouse is released in context menu.
			return isProperMouseEvent && !isOutsideTable( evt.data.getTarget() ) &&
				!isInSelectedCell( evt.data.getTarget(), fakeSelectedClass );
		}

		function isInSelectedCell( target, fakeSelectedClass ) {
			var cell = target.getAscendant( { td: 1, th: 1 }, true );

			return cell && cell.hasClass( fakeSelectedClass );
		}

		if ( canClearSelection( evt, selection, selectedTable, table ) ) {
			clearFakeCellSelection( editor, true );
		}

		// Start fake selection only if the left mouse button is really pressed inside the table.
		if ( !fakeSelection.active && evt.name === 'mousedown' && detectLeftMouseButton( evt ) && table ) {
			fakeSelection = { active: true };

			// This listener covers case when mouse button is released outside the editor.
			CKEDITOR.document.on( 'mouseup', fakeSelectionMouseHandler, null, { editor: editor } );
		}

		// The separate condition for table handles cases when user starts/stop dragging from/in
		// spacing between cells.
		if ( cell || table ) {
			fakeSelectByMouse( editor, cell || table, evt );
		}

		if ( evt.name === 'mouseup' ) {
			// If the selection ended outside of the table, there's a chance that selection was messed,
			// e.g. by including text after the table. We should also cover selection inside nested tables
			// that ends in outer table. In these cases, we just reselect cells.
			if ( detectLeftMouseButton( evt ) &&
				( isOutsideTable( evt.data.getTarget() ) || isSameTable( selectedTable, table ) ) ) {
				restoreFakeSelection( editor );
			}

			fakeSelection = { active: false };

			CKEDITOR.document.removeListener( 'mouseup', fakeSelectionMouseHandler );
		}
	}

	function fakeSelectionDragHandler( evt ) {
		var cell = evt.data.getTarget().getAscendant( { td: 1, th: 1 }, true );

		if ( !cell || cell.hasClass( fakeSelectedClass ) ) {
			return;
		}

		// We're not supporting dragging in our table selection for the time being.
		evt.cancel();
		evt.data.preventDefault();
	}

	function copyTable( editor, isCut ) {
		var selection = editor.getSelection(),
			bookmarks = selection.createBookmarks(),
			doc = editor.document,
			range = editor.createRange(),
			docElement = doc.getDocumentElement().$,
			needsScrollHack = CKEDITOR.env.ie && CKEDITOR.env.version < 9,
			// [IE] Use span for copybin and its container to avoid bug with expanding editable height by
			// absolutely positioned element.
			copybinName = ( editor.blockless || CKEDITOR.env.ie ) ? 'span' : 'div',
			copybin,
			copybinContainer,
			scrollTop,
			listener;

		function cancel( evt ) {
			evt.cancel();
		}

		// We're still handling previous copy/cut.
		// When keystroke is used to copy/cut this will also prevent
		// conflict with copyTable called again for native copy/cut event.
		if ( doc.getById( 'cke_table_copybin' ) ) {
			return;
		}


		copybin = doc.createElement( copybinName );
		copybinContainer = doc.createElement( copybinName );

		copybinContainer.setAttributes( {
			id: 'cke_table_copybin',
			'data-cke-temp': '1'
		} );

		// Position copybin element outside current viewport.
		copybin.setStyles( {
			position: 'absolute',
			width: '1px',
			height: '1px',
			overflow: 'hidden'
		} );

		copybin.setStyle( editor.config.contentsLangDirection == 'ltr' ? 'left' : 'right', '-5000px' );

		copybin.setHtml( editor.getSelectedHtml( true ) );

		// Ignore copybin.
		editor.fire( 'lockSnapshot' );

		copybinContainer.append( copybin );
		editor.editable().append( copybinContainer );

		listener = editor.on( 'selectionChange', cancel, null, null, 0 );

		if ( needsScrollHack ) {
			scrollTop = docElement.scrollTop;
		}

		// Once the clone of the table is inside of copybin, select
		// the entire contents. This selection will be copied by the
		// native browser's clipboard system.
		range.selectNodeContents( copybin );
		range.select();

		if ( needsScrollHack ) {
			docElement.scrollTop = scrollTop;
		}

		setTimeout( function() {
			copybinContainer.remove();

			selection.selectBookmarks( bookmarks );
			listener.removeListener();

			editor.fire( 'unlockSnapshot' );

			if ( isCut ) {
				editor.extractSelectedHtml();
				editor.fire( 'saveSnapshot' );
			}
		}, 100 );
	}

	function fakeSelectionCopyCutHandler( evt ) {
		var editor = evt.editor || evt.sender.editor,
			selection = editor.getSelection();

		if ( !selection.isInTable() ) {
			return;
		}

		copyTable( editor, evt.name === 'cut' );
	}

	// A helper object abstracting table selection.
	// By calling setSelectedCells() method it will automatically determine what's
	// the first/last cell or row.
	//
	// Note: ATM the type does not make an actual selection, it just holds the data.
	//
	// @param {CKEDITOR.dom.element[]} [cells] An array of cells considered to be selected.
	function TableSelection( cells ) {
		this._reset();

		if ( cells ) {
			this.setSelectedCells( cells );
		}
	}

	TableSelection.prototype = {};

	// Resets the initial state of table selection.
	TableSelection.prototype._reset = function() {
		this.cells = {
			first: null,
			last: null,
			all: []
		};

		this.rows = {
			first: null,
			last: null
		};
	};

	// Sets the cells that are selected in the table. Based on this it figures out what cell is
	// the first, and the last. Also sets rows property accordingly.
	// Note: ATM the type does not make an actual selection, it just holds the data.
	//
	// @param {CKEDITOR.dom.element[]} [cells] An array of cells considered to be selected.
	TableSelection.prototype.setSelectedCells = function( cells ) {
		this._reset();
		// Make sure we're not modifying input array.
		cells = cells.slice( 0 );
		this._arraySortByDOMOrder( cells );

		this.cells.all = cells;

		this.cells.first = cells[ 0 ];
		this.cells.last = cells[ cells.length - 1 ];

		this.rows.first = cells[ 0 ].getAscendant( 'tr' );
		this.rows.last = this.cells.last.getAscendant( 'tr' );
	};

	// Returns a table map, returned by {@link CKEDITOR.tools#buildTableMap}.
	// @returns {HTMLElement[]}
	TableSelection.prototype.getTableMap = function() {
		function getRealCellPosition( cell ) {
			var table = cell.getAscendant( 'table' ),
				rowIndex = getRowIndex( cell ),
				map = CKEDITOR.tools.buildTableMap( table ),
				i;

			for ( i = 0; i < map[ rowIndex ].length; i++ ) {
				if ( new CKEDITOR.dom.element( map[ rowIndex ][ i ] ).equals( cell ) ) {
					return i;
				}
			}
		}

		var startIndex = getCellColIndex( this.cells.first ),
			endIndex = getRealCellPosition( this.cells.last );

		return CKEDITOR.tools.buildTableMap( this._getTable(), getRowIndex( this.rows.first ), startIndex,
			getRowIndex( this.rows.last ), endIndex );
	};

	TableSelection.prototype._getTable = function() {
		return this.rows.first.getAscendant( 'table' );
	};

	// @param {Number} count Number of rows to be inserted.
	// @param {Boolean} [insertBefore=false] If set to `true` new rows will be prepended.
	// @param {Boolean} [clearSelection=false] If set to `true`, it will set selected cells to the one inserted.
	TableSelection.prototype.insertRow = function( count, insertBefore, clearSelection ) {
		if ( typeof count === 'undefined' ) {
			count = 1;
		} else if ( count <= 0 ) {
			return;
		}

		var cellIndexFirst = this.cells.first.$.cellIndex,
			cellIndexLast = this.cells.last.$.cellIndex,
			selectedCells = clearSelection ? [] : this.cells.all,
			row,
			newCells;

		for ( var i = 0; i < count; i++ ) {
			// In case of clearSelection we need explicitly use cached cells, as selectedCells is empty.
			row = insertRow( clearSelection ? this.cells.all : selectedCells, insertBefore );

			// Append cells from added row.
			newCells = CKEDITOR.tools.array.filter( row.find( 'td, th' ).toArray(), function( cell ) {
				return clearSelection ?
					true : cell.$.cellIndex >= cellIndexFirst && cell.$.cellIndex <= cellIndexLast;
			} );

			// setSelectedCells will take care of refreshing the whole state at once.
			if ( insertBefore ) {
				selectedCells = newCells.concat( selectedCells );
			} else {
				selectedCells = selectedCells.concat( newCells );
			}
		}

		this.setSelectedCells( selectedCells );
	};

	// @param {Number} count Number of columns to be inserted.
	TableSelection.prototype.insertColumn = function( count ) {
		if ( typeof count === 'undefined' ) {
			count = 1;
		} else if ( count <= 0 ) {
			return;
		}

		var cells = this.cells,
			selectedCells = cells.all,
			minRowIndex = getRowIndex( cells.first ),
			maxRowIndex = getRowIndex( cells.last );

		function limitCells( cell ) {
			var parentRowIndex = getRowIndex( cell );

			return parentRowIndex >= minRowIndex && parentRowIndex <= maxRowIndex;
		}

		for ( var i = 0; i < count; i++ ) {
			// Prepend added cells, then pass it to setSelectionCells so that it will take care of refreshing
			// the whole state. Note that returned cells needs to be filtered, so that only cells that
			// should get selected are added to the selectedCells array.
			selectedCells = selectedCells.concat( CKEDITOR.tools.array.filter( insertColumn( selectedCells ), limitCells ) );
		}

		this.setSelectedCells( selectedCells );
	};

	// Clears the content of selected cells.
	//
	// @param {CKEDITOR.dom.element[]} [cells] If given, this cells will be cleared.
	TableSelection.prototype.emptyCells =  function( cells ) {
		cells = cells || this.cells.all;

		for ( var i = 0; i < cells.length; i++ ) {
			cells[ i ].setHtml( '' );
		}
	};

	// Sorts given arr according to DOM position.
	//
	// @param {CKEDITOR.dom.node[]} arr
	TableSelection.prototype._arraySortByDOMOrder = function( arr ) {
		arr.sort( function( el1, el2 ) {
			return el1.getPosition( el2 ) & CKEDITOR.POSITION_PRECEDING ? -1 : 1;
		} );
	};

	var fakeSelectionPasteHandler = {
		onPaste: pasteListener,
		// Check if the selection is collapsed on the beginning of the row (1) or at the end (2).
		isBoundarySelection: function( selection ) {
			var ranges = selection.getRanges(),
				range = ranges[ 0 ],
				row = range.endContainer.getAscendant( 'tr', true );

			if ( row && range.collapsed ) {
				if ( range.checkBoundaryOfElement( row, CKEDITOR.START ) ) {
					return 1;
				} else if ( range.checkBoundaryOfElement( row, CKEDITOR.END ) ) {
					return 2;
				}
			}

			return 0;
		},

		// Looks for a table in a given pasted content string. Returns it as a
		// CKEDITOR.dom.element instance or null if mixed content, or more than one table found.
		findTableInPastedContent: function( editor, dataValue ) {
			var dataProcessor = editor.dataProcessor,
				tmpContainer = new CKEDITOR.dom.element( 'body' );

			if ( !dataProcessor ) {
				dataProcessor = new CKEDITOR.htmlDataProcessor( editor );
			}

			// Pasted value must be filtered using dataProcessor to strip all unsafe code
			// before inserting it into temporary container.
			tmpContainer.setHtml( dataProcessor.toHtml( dataValue ), {
				fixForBody: false
			} );

			return tmpContainer.getChildCount() > 1 ? null : tmpContainer.findOne( 'table' );
		},

		// Performs an actual paste into selectedTableMap based on content in pastedTableMap.
		pasteTable: function( tableSel, selectedTableMap, pastedTableMap ) {
			var cellToReplace,
				// Index of first selected cell, it needs to be reused later, to calculate the
				// proper position of newly pasted cells.
				startIndex = getCellColIndex( tableSel.cells.first ),
				selectedTable = tableSel._getTable(),
				markers = {},
				currentRow,
				prevCell,
				cellToPaste,
				i,
				j;

			// And now paste!
			for ( i = 0; i < pastedTableMap.length; i++ ) {
				currentRow = new CKEDITOR.dom.element( selectedTable.$.rows[ tableSel.rows.first.$.rowIndex + i ] );

				for ( j = 0; j < pastedTableMap[ i ].length; j++ ) {
					cellToPaste = new CKEDITOR.dom.element( pastedTableMap[ i ][ j ] );

					if ( selectedTableMap[ i ] && selectedTableMap[ i ][ j ] ) {
						cellToReplace = new CKEDITOR.dom.element( selectedTableMap[ i ][ j ] );
					} else {
						cellToReplace = null;
					}

					// Only try to paste cells that aren't already pasted (it can occur if the pasted cell
					// has [colspan] or [rowspan]).
					if ( cellToPaste && !cellToPaste.getCustomData( 'processed' ) ) {
						// If the cell to being replaced has [colspan], it could have been already
						// replaced. In that case, it won't have parent.
						if ( cellToReplace && cellToReplace.getParent() ) {
							cellToPaste.replace( cellToReplace );
						} else if ( j === 0 || pastedTableMap[ i ][ j - 1 ] ) {
							if ( j !== 0 ) {
								prevCell = new CKEDITOR.dom.element( pastedTableMap[ i ][ j - 1 ] );
							} else {
								prevCell = null;
							}

							// If the cell that should be replaced is not in the table, we must cover at least 3 cases:
							// 1. Pasting cell in the same row as the previous pasted cell.
							// 2. Pasting cell into the next row at the proper position.
							// 3. If the selection started from the left edge of the table,
							// prepending the proper row with the cell.
							if ( prevCell && currentRow.equals( prevCell.getParent() ) ) {
								cellToPaste.insertAfter( prevCell );
							} else if ( startIndex > 0 ) {
								// It might happen that there's no cell with startIndex, as it might be used by a rowspan.
								if ( currentRow.$.cells[ startIndex ] ) {
									cellToPaste.insertAfter( new CKEDITOR.dom.element( currentRow.$.cells[ startIndex ] ) );
								} else {
									// Since rowspans are erased from current selection, we want need to append a cell.
									currentRow.append( cellToPaste );
								}
							} else {
								currentRow.append( cellToPaste, true );
							}
						}

						CKEDITOR.dom.element.setMarker( markers, cellToPaste, 'processed', true );
					} else if ( cellToPaste.getCustomData( 'processed' ) && cellToReplace ) {
						// If the cell was already pasted, but the cell to replace still exists (e.g. pasted
						// cell has [colspan]), remove it.
						cellToReplace.remove();
					}
				}
			}

			CKEDITOR.dom.element.clearAllMarkers( markers );
		}
	};

	function pasteListener( evt ) {
		var editor = evt.editor,
			selection = editor.getSelection(),
			selectedCells = getSelectedCells( selection ),
			pastedTable = this.findTableInPastedContent( editor, evt.data.dataValue ),
			boundarySelection = selection.isInTable( true ) && this.isBoundarySelection( selection ),
			tableSel,
			selectedTable,
			selectedTableMap,
			pastedTableMap;

		function getLongestRowLength( map ) {
			return Math.max.apply( null, CKEDITOR.tools.array.map( map, function( rowMap ) {
				return rowMap.length;
			}, 0 ) );
		}

		function selectCellContents( cell ) {
			var range = editor.createRange();

			range.selectNodeContents( cell );
			range.select();
		}

		// Do not customize paste process in following cases:
		// No cells are selected.
		if ( !selectedCells.length ||
			// It's single range that does not fully contain table element and is not boundary, e.g. collapsed selection within
			// cell, part of cell etc.
			( selectedCells.length === 1 && !rangeContainsTableElement( selection.getRanges()[ 0 ] )  && !boundarySelection ) ||
			// It's a boundary position but with no table pasted.
			( boundarySelection && !pastedTable ) ) {
			return;
		}

		selectedTable = selectedCells[ 0 ].getAscendant( 'table' );
		tableSel = new TableSelection( getSelectedCells( selection, selectedTable ) );

		function getLastArrayItem( arr ) {
			return arr[ arr.length - 1 ];
		}

		// Schedule selecting appropriate table cells after pasting. It covers both table and not-table
		// content (#520).
		editor.once( 'afterPaste', function() {
			var toSelect = pastedTableMap ?
				getCellsBetween( new CKEDITOR.dom.element( pastedTableMap[ 0 ][ 0 ] ),
					new CKEDITOR.dom.element( getLastArrayItem( getLastArrayItem( pastedTableMap ) ) ) ) :
				tableSel.cells.all;

			fakeSelectCells( editor, toSelect );
		} );

		// In case of mixed content or non table content just select first cell, and erase content of other selected cells.
		// Selection is left in first cell, so that default CKEditor logic puts pasted content in the selection (#520).
		if ( !pastedTable ) {
			selectCellContents( tableSel.cells.first );

			// Due to limitations of our undo manager, in case of mixed content
			// cells must be emptied after pasting (#520).
			editor.once( 'afterPaste', function() {
				editor.fire( 'lockSnapshot' );
				tableSel.emptyCells( tableSel.cells.all.slice( 1 ) );
				// Reselecting cells allows to create correct undo snapshot (#763).
				fakeSelectCells( editor, tableSel.cells.all );
				editor.fire( 'unlockSnapshot' );
			} );

			return;
		}

		// Preventing other paste handlers should be done after all early returns (#520).
		evt.stop();

		// In case of boundary selection, insert new row before/after selected one, select it
		// and resume the rest of the algorithm.
		if ( boundarySelection ) {
			tableSel.insertRow( 1, boundarySelection === 1, true );
			selection.selectElement( tableSel.rows.first );
		} else {
			// Otherwise simply clear all the selected cells.
			tableSel.emptyCells();
			// Reselecting cells allows to create correct undo snapshot (#763).
			fakeSelectCells( editor, tableSel.cells.all );
		}

		// Build table map only for selected fragment.
		selectedTableMap = tableSel.getTableMap();
		pastedTableMap = CKEDITOR.tools.buildTableMap( pastedTable );

		tableSel.insertRow( pastedTableMap.length - selectedTableMap.length );

		// Now we compare the dimensions of the pasted table and the selected one.
		// If the pasted one is bigger, we add missing rows and columns.
		tableSel.insertColumn( getLongestRowLength( pastedTableMap ) - getLongestRowLength( selectedTableMap ) );

		// Rebuild map for selected table.
		selectedTableMap = tableSel.getTableMap();

		this.pasteTable( tableSel, selectedTableMap, pastedTableMap );

		editor.fire( 'saveSnapshot' );

		// Manually fire afterPaste event as we stop pasting to handle everything via our custom handler.
		setTimeout( function() {
			editor.fire( 'afterPaste' );
		}, 0 );
	}

	function customizeTableCommand( editor, cmds, callback ) {
		editor.on( 'beforeCommandExec', function( evt ) {
			if ( CKEDITOR.tools.array.indexOf( cmds, evt.data.name ) !== -1 ) {
				evt.data.selectedCells = getSelectedCells( editor.getSelection() );
			}
		} );

		editor.on( 'afterCommandExec', function( evt ) {
			if ( CKEDITOR.tools.array.indexOf( cmds, evt.data.name ) !== -1 ) {
				callback( editor, evt.data );
			}
		} );
	}

	/**
	 * Namespace providing a set of helper functions for working with tables, exposed by
	 * [Table Selection](https://ckeditor.com/cke4/addon/tableselection) plugin.
	 *
	 * @since 4.7.0
	 * @singleton
	 * @class CKEDITOR.plugins.tableselection
	 */
	CKEDITOR.plugins.tableselection = {
		/**
		 * Fetches all cells between cells passed as parameters, including these cells.
		 *
		 * @param {CKEDITOR.dom.element} first The first cell to fetch.
		 * @param {CKEDITOR.dom.element} last The last cell to fetch.
		 * @return {CKEDITOR.dom.element[]} Array of fetched cells.
		 */
		getCellsBetween: getCellsBetween,

		/**
		 * Adds keyboard integration for table selection in a given editor.
		 *
		 * @param {CKEDITOR.editor} editor
		 * @private
		 */
		keyboardIntegration: function( editor ) {
			// Handle left, up, right, down, delete, backspace and enter keystrokes inside table fake selection.
			function getTableOnKeyDownListener( editor ) {
				var keystrokes = {
						37: 1, // Left Arrow
						38: 1, // Up Arrow
						39: 1, // Right Arrow,
						40: 1, // Down Arrow
						8: 1, // Backspace
						46: 1, // Delete
						13: 1 // Enter
					},
					tags = CKEDITOR.tools.extend( { table: 1 }, CKEDITOR.dtd.$tableContent );

				delete tags.td;
				delete tags.th;

				// Called when removing empty subseleciton of the table.
				// It should not allow for removing part of table, e.g. when user attempts to remove 2 cells
				// out of 4 in row. It should however remove whole row or table, if it was fully selected.
				function deleteEmptyTablePart( node, ranges ) {
					if ( !ranges.length ) {
						return null;
					}

					var rng = editor.createRange(),
						mergedRanges = CKEDITOR.dom.range.mergeRanges( ranges );

					// Enlarge each range, so that it wraps over tr.
					CKEDITOR.tools.array.forEach( mergedRanges, function( mergedRange ) {
						mergedRange.enlarge( CKEDITOR.ENLARGE_ELEMENT );
					} );

					var boundaryNodes = mergedRanges[ 0 ].getBoundaryNodes(),
						startNode = boundaryNodes.startNode,
						endNode = boundaryNodes.endNode;

					if ( startNode && startNode.is && startNode.is( tags ) ) {
						// A node that will receive selection after the firstRangeContainedNode is removed.
						var boundaryTable = startNode.getAscendant( 'table', true ),
							targetNode = startNode.getPreviousSourceNode( false, CKEDITOR.NODE_ELEMENT, boundaryTable ),
							selectBeginning = false,
							matchingElement = function( elem ) {
								// We're interested in matching only td/th but not contained by the startNode since it will be removed.
								// Technically none of startNode children should be visited but it will due to https://dev.ckeditor.com/ticket/12191.
								return !startNode.contains( elem ) && elem.is && elem.is( 'td', 'th' );
							};

						while ( targetNode && !matchingElement( targetNode ) ) {
							targetNode = targetNode.getPreviousSourceNode( false, CKEDITOR.NODE_ELEMENT, boundaryTable );
						}

						if ( !targetNode && endNode && endNode.is && !endNode.is( 'table' ) && endNode.getNext() ) {
							// Special case: say we were removing the first row, so there are no more tds before, check if there's a cell after removed row.
							targetNode = endNode.getNext().findOne( 'td, th' );
							// In that particular case we want to select beginning.
							selectBeginning = true;
						}

						if ( !targetNode ) {
							// As a last resort of defence we'll put the selection before (about to be) removed table.
							rng.setStartBefore( startNode.getAscendant( 'table', true ) );
							rng.collapse( true );
						} else {
							rng[ 'moveToElementEdit' + ( selectBeginning ? 'Start' : 'End' ) ]( targetNode );
						}

						mergedRanges[ 0 ].deleteContents();

						return [ rng ];
					}

					// By default return a collapsed selection in a first cell.
					if ( startNode ) {
						rng.moveToElementEditablePosition( startNode );
						return [ rng ];
					}
				}

				return function( evt ) {
					// Use getKey directly in order to ignore modifiers.
					// Justification: https://dev.ckeditor.com/ticket/11861#comment:13
					var key = evt.data.getKey(),
						keystroke = evt.data.getKeystroke(),
						selection,
						toStart = key === 37 || key == 38,
						ranges,
						firstCell,
						lastCell,
						i;

					// Handle only left/right/del/bspace keys.
					// Disable editing cells in readonly mode (#1489).
					if ( !keystrokes[ key ] || editor.readOnly ) {
						return;
					}

					selection = editor.getSelection();

					if ( !selection || !selection.isInTable() || !selection.isFake ) {
						return;
					}

					ranges = selection.getRanges();
					firstCell = ranges[ 0 ]._getTableElement();
					lastCell = ranges[ ranges.length - 1 ]._getTableElement();

					// Only prevent event when tableselection handle it. Which is non-enter button, or pressing enter button with enterkey plugin present (#1816).
					if ( key !== 13 || editor.plugins.enterkey ) {
						evt.data.preventDefault();
						evt.cancel();
					}

					if ( key > 36 && key < 41 ) {
						// Arrows.
						ranges[ 0 ].moveToElementEditablePosition( toStart ? firstCell : lastCell, !toStart );
						selection.selectRanges( [ ranges[ 0 ] ] );
					} else {
						// Delete, backspace, enter.

						// Do nothing for Enter with modifiers different than shift.
						if ( key === 13 && !( keystroke === 13 || keystroke === CKEDITOR.SHIFT + 13 ) ) {
							return;
						}

						for ( i = 0; i < ranges.length; i++ ) {
							clearCellInRange( ranges[ i ] );
						}

						var newRanges = deleteEmptyTablePart( firstCell, ranges );

						if ( newRanges ) {
							ranges = newRanges;
						} else {
							// If no new range was returned fallback to selecting first cell.
							ranges[ 0 ].moveToElementEditablePosition( firstCell );
						}

						selection.selectRanges( ranges );

						if ( key === 13 && editor.plugins.enterkey ) {
							// We need to lock undoManager to consider clearing table and inserting new paragraph as single operation, and have only one undo step (#1816).
							editor.fire( 'lockSnapshot' );
							keystroke === 13 ? editor.execCommand( 'enter' ) : editor.execCommand( 'shiftEnter' );
							editor.fire( 'unlockSnapshot' );
							editor.fire( 'saveSnapshot' );
						} else if ( key !== 13 ) {
							// Backspace and delete key should have saved snapshot.
							editor.fire( 'saveSnapshot' );
						}
					}
				};
			}

			function tableKeyPressListener( evt ) {
				var selection = editor.getSelection(),
					// Enter key also produces character, but Firefox doesn't think so (gh#415).
					isCharKey = evt.data.$.charCode || ( evt.data.getKey() === 13 ),
					ranges,
					firstCell,
					i;

				// Disable editing cells in readonly mode (#1489).
				if ( editor.readOnly ) {
					return;
				}

				// We must check if the event really did not produce any character as it's fired for all keys in Gecko.
				if ( !selection || !selection.isInTable() || !selection.isFake || !isCharKey ||
					evt.data.getKeystroke() & CKEDITOR.CTRL ) {
					return;
				}

				ranges = selection.getRanges();
				firstCell = ranges[ 0 ].getEnclosedNode().getAscendant( { td: 1, th: 1 }, true );

				for ( i = 0; i < ranges.length; i++ ) {
					clearCellInRange( ranges[ i ] );
				}

				// In case of selection of table element, there won't be any cell (#867).
				if ( firstCell ) {
					ranges[ 0 ].moveToElementEditablePosition( firstCell );
					selection.selectRanges( [ ranges[ 0 ] ] );
				}
			}

			function clearCellInRange( range ) {
				var node = range.getEnclosedNode();

				// Set text only in case of table cells, otherwise remove whole element (#867).
				// Check if `node.is` is function, as returned node might be CKEDITOR.dom.text (#2089).
				if ( node && typeof node.is === 'function' && node.is( { td: 1, th: 1 } ) ) {
					node.setText( '' );
				} else {
					range.deleteContents();
				}

				CKEDITOR.tools.array.forEach( range._find( 'td' ), function( cell ) {
					// Cells that were not removed, need to contain bogus BR (if needed), otherwise row might
					// collapse. (tp#2270)
					cell.appendBogus();
				} );
			}

			// Automatically select non-editable element when navigating into
			// it by left/right or backspace/del keys.
			var editable = editor.editable();
			editable.attachListener( editable, 'keydown', getTableOnKeyDownListener( editor ), null, null, -1 );
			editable.attachListener( editable, 'keypress', tableKeyPressListener, null, null, -1 );
		},

		/**
		 * Determines whether table selection is supported in the current environment.
		 *
		 * @property {Boolean}
		 * @private
		 */
		isSupportedEnvironment: !( CKEDITOR.env.ie && CKEDITOR.env.version < 11 )
	};

	CKEDITOR.plugins.add( 'tableselection', {
		requires: 'clipboard,tabletools',

		onLoad: function() {
			// We can't alias these features earlier, as they could be still not loaded.
			tabletools = CKEDITOR.plugins.tabletools;
			getSelectedCells = tabletools.getSelectedCells;
			getCellColIndex = tabletools.getCellColIndex;
			insertRow = tabletools.insertRow;
			insertColumn = tabletools.insertColumn;

			CKEDITOR.document.appendStyleSheet( this.path + 'styles/tableselection.css' );
		},

		init: function( editor ) {
			// Disable unsupported browsers.
			if ( !CKEDITOR.plugins.tableselection.isSupportedEnvironment ) {
				return;
			}

			// Add styles for fake visual selection.
			if ( editor.addContentsCss ) {
				editor.addContentsCss( this.path + 'styles/tableselection.css' );
			}

			editor.on( 'contentDom', function() {
				var editable = editor.editable(),
					mouseHost = editable.isInline() ? editable : editor.document,
					evtInfo = { editor: editor };

				// Explicitly set editor as DOM events generated on document does not convey information about it.
				editable.attachListener( mouseHost, 'mousedown', fakeSelectionMouseHandler, null, evtInfo );
				editable.attachListener( mouseHost, 'mousemove', fakeSelectionMouseHandler, null, evtInfo );
				editable.attachListener( mouseHost, 'mouseup', fakeSelectionMouseHandler, null, evtInfo );

				editable.attachListener( editable, 'dragstart', fakeSelectionDragHandler );
				editable.attachListener( editor, 'selectionCheck', fakeSelectionChangeHandler );

				CKEDITOR.plugins.tableselection.keyboardIntegration( editor );

				// Setup copybin.
				if ( CKEDITOR.plugins.clipboard && !CKEDITOR.plugins.clipboard.isCustomCopyCutSupported ) {
					editable.attachListener( editable, 'cut', fakeSelectionCopyCutHandler );
					editable.attachListener( editable, 'copy', fakeSelectionCopyCutHandler );
				}
			} );

			editor.on( 'paste', fakeSelectionPasteHandler.onPaste, fakeSelectionPasteHandler );

			customizeTableCommand( editor, [
				'rowInsertBefore',
				'rowInsertAfter',
				'columnInsertBefore',
				'columnInsertAfter',
				'cellInsertBefore',
				'cellInsertAfter'
			], function( editor, data ) {
				fakeSelectCells( editor, data.selectedCells );
			} );

			customizeTableCommand( editor, [
				'cellMerge',
				'cellMergeRight',
				'cellMergeDown'
			], function( editor, data ) {
				fakeSelectCells( editor, [ data.commandData.cell ] );
			} );

			customizeTableCommand( editor, [
				'cellDelete'
			], function( editor ) {
				clearFakeCellSelection( editor, true );
			} );
		}
	} );
}() );
