/*!
 * VisualEditor Table Selection class.
 *
 * @copyright See AUTHORS.txt
 */

/**
 * @class
 * @extends ve.dm.Selection
 * @constructor
 * @param {ve.Range} tableRange Table range
 * @param {number} fromCol Starting column
 * @param {number} fromRow Starting row
 * @param {number} [toCol] End column
 * @param {number} [toRow] End row
 */
ve.dm.TableSelection = function VeDmTableSelection( tableRange, fromCol, fromRow, toCol, toRow ) {
	if ( ve.dm.Document && arguments[ 0 ] instanceof ve.dm.Document ) {
		throw new Error( 'Got obsolete ve.dm.Document argument' );
	}
	if ( arguments.length > 5 ) {
		throw new Error( 'Got obsolete argument (probably `expand`)' );
	}

	// Parent constructor
	ve.dm.TableSelection.super.call( this );

	this.tableRange = tableRange;

	toCol = toCol === undefined ? fromCol : toCol;
	toRow = toRow === undefined ? fromRow : toRow;

	this.fromCol = fromCol;
	this.fromRow = fromRow;
	this.toCol = toCol;
	this.toRow = toRow;
	this.startCol = fromCol < toCol ? fromCol : toCol;
	this.startRow = fromRow < toRow ? fromRow : toRow;
	this.endCol = fromCol < toCol ? toCol : fromCol;
	this.endRow = fromRow < toRow ? toRow : fromRow;
	this.intendedFromCol = this.fromCol;
	this.intendedFromRow = this.fromRow;
	this.intendedToCol = this.toCol;
	this.intendedToRow = this.toRow;
};

/* Inheritance */

OO.inheritClass( ve.dm.TableSelection, ve.dm.Selection );

/* Static Properties */

ve.dm.TableSelection.static.name = 'table';

/* Static Methods */

/**
 * @inheritdoc
 */
ve.dm.TableSelection.static.newFromHash = function ( hash ) {
	return new ve.dm.TableSelection(
		ve.Range.static.newFromHash( hash.tableRange ),
		hash.fromCol,
		hash.fromRow,
		hash.toCol,
		hash.toRow
	);
};

/**
 * Retrieves all cells within a given selection.
 *
 * @static
 * @param {ve.dm.TableMatrix} matrix The table matrix
 * @param {Object} selectionOffsets Selection col/row offsets (startRow/endRow/startCol/endCol)
 * @param {boolean} [includePlaceholders] Include placeholders in result
 * @return {ve.dm.TableMatrixCell[]} List of table cells
 */
ve.dm.TableSelection.static.getTableMatrixCells = function ( matrix, selectionOffsets, includePlaceholders ) {
	const cells = [],
		visited = {};

	for ( let row = selectionOffsets.startRow; row <= selectionOffsets.endRow; row++ ) {
		for ( let col = selectionOffsets.startCol; col <= selectionOffsets.endCol; col++ ) {
			let cell = matrix.getCell( row, col );
			if ( !cell ) {
				continue;
			}
			if ( !includePlaceholders && cell.isPlaceholder() ) {
				cell = cell.owner;
			}
			if ( !visited[ cell.key ] ) {
				cells.push( cell );
				visited[ cell.key ] = true;
			}
		}
	}
	return cells;
};

/* Methods */

/**
 * Expand the selection to cover all merged cells
 *
 * @private
 * @param {ve.dm.Document} doc The document to which this selection applies
 * @return {ve.dm.TableSelection} Expanded table selection
 */
ve.dm.TableSelection.prototype.expand = function ( doc ) {
	const matrix = this.getTableNode( doc ).getMatrix(),
		colBackwards = this.fromCol > this.toCol,
		rowBackwards = this.fromRow > this.toRow;
	let lastCellCount = 0,
		startCol = Infinity,
		startRow = Infinity,
		endCol = -Infinity,
		endRow = -Infinity,
		cells = this.getMatrixCells( doc );

	while ( cells.length > lastCellCount ) {
		for ( let i = 0; i < cells.length; i++ ) {
			const cell = cells[ i ];
			startCol = Math.min( startCol, cell.col );
			startRow = Math.min( startRow, cell.row );
			endCol = Math.max( endCol, cell.col + cell.node.getColspan() - 1 );
			endRow = Math.max( endRow, cell.row + cell.node.getRowspan() - 1 );
		}

		lastCellCount = cells.length;
		cells = this.constructor.static.getTableMatrixCells( matrix, {
			startCol: startCol,
			startRow: startRow,
			endCol: endCol,
			endRow: endRow
		} );
	}
	return new this.constructor(
		this.tableRange,
		colBackwards ? endCol : startCol,
		rowBackwards ? endRow : startRow,
		colBackwards ? startCol : endCol,
		rowBackwards ? startRow : endRow
	);
};

/**
 * @inheritdoc
 */
ve.dm.TableSelection.prototype.toJSON = function () {
	return {
		type: this.constructor.static.name,
		tableRange: this.tableRange,
		fromCol: this.fromCol,
		fromRow: this.fromRow,
		toCol: this.toCol,
		toRow: this.toRow
	};
};

/**
 * @inheritdoc
 */
ve.dm.TableSelection.prototype.getDescription = function () {
	return (
		'Table: ' +
		this.tableRange.from + ' - ' + this.tableRange.to +
		', ' +
		'c' + this.fromCol + ' r' + this.fromRow +
		' - ' +
		'c' + this.toCol + ' r' + this.toRow
	);
};

/**
 * @inheritdoc
 */
ve.dm.TableSelection.prototype.collapseToStart = function () {
	return new this.constructor( this.tableRange, this.startCol, this.startRow, this.startCol, this.startRow );
};

/**
 * @inheritdoc
 */
ve.dm.TableSelection.prototype.collapseToEnd = function () {
	return new this.constructor( this.tableRange, this.endCol, this.endRow, this.endCol, this.endRow );
};

/**
 * @inheritdoc
 */
ve.dm.TableSelection.prototype.collapseToFrom = function () {
	return new this.constructor( this.tableRange, this.fromCol, this.fromRow, this.fromCol, this.fromRow );
};

/**
 * @inheritdoc
 */
ve.dm.TableSelection.prototype.collapseToTo = function () {
	return new this.constructor( this.tableRange, this.toCol, this.toRow, this.toCol, this.toRow );
};

/**
 * @inheritdoc
 * @param {ve.dm.Document} doc The document to which this selection applies
 */
ve.dm.TableSelection.prototype.getRanges = function ( doc ) {
	const ranges = [],
		cells = this.getMatrixCells( doc );
	for ( let i = 0, l = cells.length; i < l; i++ ) {
		ranges.push( cells[ i ].node.getRange() );
	}
	return ranges;
};

/**
 * @inheritdoc
 */
ve.dm.TableSelection.prototype.getCoveringRange = function () {
	// Note that this returns the table range, and not the minimal range covering
	// all cells, as that would be far more expensive to compute.
	return this.tableRange;
};

/**
 * Get all the ranges required to build a table slice from the selection
 *
 * In addition to the outer ranges of the cells, this also includes the start and
 * end tags of table rows, sections and the table itself.
 *
 * @param {ve.dm.Document} doc The document to which this selection applies
 * @return {ve.Range[]} Ranges
 */
ve.dm.TableSelection.prototype.getTableSliceRanges = function ( doc ) {
	const ranges = [],
		matrix = this.getTableNode( doc ).getMatrix();

	// Arrays are non-overlapping so avoid duplication
	// by indexing by range.start
	function pushNode( n ) {
		const range = n.getOuterRange();
		ranges[ range.start ] = new ve.Range( range.start, range.start + 1 );
		ranges[ range.end - 1 ] = new ve.Range( range.end - 1, range.end );
	}

	// Get the start and end tags of every parent of the cell
	// up to and including the TableNode
	for ( let i = this.startRow; i <= this.endRow; i++ ) {
		let node = matrix.getRowNode( i );
		if ( !node ) {
			continue;
		}
		pushNode( node );
		while ( ( node = node.getParent() ) && node ) {
			pushNode( node );
			if ( node instanceof ve.dm.TableNode ) {
				break;
			}
		}
	}

	return ranges
		// Condense sparse array
		.filter( ( r ) => r )
		// Add cell ranges
		.concat( this.getOuterRanges( doc ) )
		// Sort
		.sort( ( a, b ) => a.start - b.start );
};

/**
 * Get outer ranges of the selected cells
 *
 * @param {ve.dm.Document} doc The document to which this selection applies
 * @return {ve.Range[]} Outer ranges
 */
ve.dm.TableSelection.prototype.getOuterRanges = function ( doc ) {
	const ranges = [],
		cells = this.getMatrixCells( doc );
	for ( let i = 0, l = cells.length; i < l; i++ ) {
		ranges.push( cells[ i ].node.getOuterRange() );
	}
	return ranges;
};

/**
 * Retrieves all cells within a given selection.
 *
 * @param {ve.dm.Document} doc The document to which this selection applies
 * @param {boolean} [includePlaceholders] Include placeholders in result
 * @return {ve.dm.TableMatrixCell[]} List of table cells
 */
ve.dm.TableSelection.prototype.getMatrixCells = function ( doc, includePlaceholders ) {
	return this.constructor.static.getTableMatrixCells(
		this.getTableNode( doc ).getMatrix(),
		{
			startCol: this.startCol,
			startRow: this.startRow,
			endCol: this.endCol,
			endRow: this.endRow
		},
		includePlaceholders
	);
};

/**
 * Check the selected cells are all editable
 *
 * @param {ve.dm.Document} doc The document to which this selection applies
 * @return {boolean} Cells are all editable
 */
ve.dm.TableSelection.prototype.isEditable = function ( doc ) {
	return this.getMatrixCells( doc ).every( ( cell ) => cell.node.isCellEditable() );
};

/**
 * @inheritdoc
 */
ve.dm.TableSelection.prototype.isCollapsed = function () {
	return false;
};

/**
 * @inheritdoc
 */
ve.dm.TableSelection.prototype.translateByTransaction = function ( tx ) {
	const newRange = tx.translateRange(
		this.tableRange,
		// Table selections should always exclude insertions
		true
	);

	if ( newRange.isCollapsed() ) {
		return new ve.dm.NullSelection();
	}
	return new this.constructor( newRange, this.fromCol, this.fromRow, this.toCol, this.toRow );
};

/**
 * @inheritdoc
 */
ve.dm.TableSelection.prototype.translateByTransactionWithAuthor = function ( tx, authorId ) {
	const newRange = tx.translateRangeWithAuthor( this.tableRange, authorId );

	if ( newRange.isCollapsed() ) {
		return new ve.dm.NullSelection();
	}
	return new this.constructor( newRange, this.fromCol, this.fromRow, this.toCol, this.toRow );
};

/**
 * Check if the selection spans a single cell
 *
 * @param {ve.dm.Document} doc The document to which this selection applies
 * @return {boolean} The selection spans a single cell
 */
ve.dm.TableSelection.prototype.isSingleCell = function ( doc ) {
	// Quick check for single non-merged cell
	return ( this.fromRow === this.toRow && this.fromCol === this.toCol ) ||
		// Check for a merged single cell by ignoring placeholders
		this.getMatrixCells( doc ).length === 1;
};

/**
 * Check if the selection is mergeable or unmergeable
 *
 * The selection must span more than one matrix cell, but only
 * one table section.
 *
 * @param {ve.dm.Document} doc The document to which this selection applies
 * @return {boolean} The selection is mergeable or unmergeable
 */
ve.dm.TableSelection.prototype.isMergeable = function ( doc ) {
	if ( !this.isEditable( doc ) ) {
		return false;
	}

	if ( this.getMatrixCells( doc, true ).length <= 1 ) {
		return false;
	}

	const matrix = this.getTableNode( doc ).getMatrix();

	let lastSectionNode;
	// Check all sections are the same
	for ( let r = this.endRow; r >= this.startRow; r-- ) {
		const rowNode = matrix.getRowNode( r );
		if ( !rowNode ) {
			continue;
		}
		const sectionNode = rowNode.findParent( ve.dm.TableSectionNode );
		if ( lastSectionNode && sectionNode !== lastSectionNode ) {
			// Can't merge across sections
			return false;
		}
		lastSectionNode = sectionNode;
	}
	return true;
};

/**
 * Get the selection's table node
 *
 * @param {ve.dm.Document} doc The document to which this selection applies
 * @return {ve.dm.TableNode} Table node
 */
ve.dm.TableSelection.prototype.getTableNode = function ( doc ) {
	return doc.getBranchNodeFromOffset( this.tableRange.start + 1 );
};

/**
 * Get a new selection with adjusted row and column positions
 *
 * Placeholder cells are skipped over so this method can be used for cursoring.
 *
 * @param {ve.dm.Document} doc The document to which this selection applies
 * @param {number} fromColOffset Starting column offset
 * @param {number} fromRowOffset Starting row offset
 * @param {number} [toColOffset] End column offset
 * @param {number} [toRowOffset] End row offset
 * @param {number} [wrap] Wrap to the next/previous row if column limits are exceeded
 * @return {ve.dm.TableSelection} Adjusted selection
 */
ve.dm.TableSelection.prototype.newFromAdjustment = function ( doc, fromColOffset, fromRowOffset, toColOffset, toRowOffset, wrap ) {
	if ( toColOffset === undefined ) {
		toColOffset = fromColOffset;
	}

	if ( toRowOffset === undefined ) {
		toRowOffset = fromRowOffset;
	}

	const matrix = this.getTableNode( doc ).getMatrix();
	let wrapDir;

	function adjust( mode, cell, offset ) {
		const dir = offset > 0 ? 1 : -1;
		let nextCell,
			col = cell.col,
			row = cell.row;

		while ( offset !== 0 ) {
			if ( mode === 'col' ) {
				col += dir;
				// Out of bounds
				if ( col >= matrix.getColCount( row ) ) {
					if ( wrap && row < matrix.getRowCount() - 1 ) {
						// Subtract columns in current row
						col -= matrix.getColCount( row );
						row++;
						wrapDir = 1;
					} else {
						break;
					}
				} else if ( col < 0 ) {
					if ( wrap && row > 0 ) {
						row--;
						// Add columns in previous row
						col += matrix.getColCount( row );
						wrapDir = -1;
					} else {
						break;
					}
				}
			} else {
				row += dir;
				if ( row >= matrix.getRowCount() || row < 0 ) {
					// Out of bounds
					break;
				}
			}
			nextCell = matrix.getCell( row, col );
			// Skip if same as current cell (i.e. merged cells), or null
			if ( !nextCell || nextCell.equals( cell ) ) {
				continue;
			}
			offset -= dir;
			cell = nextCell;
		}
		return cell;
	}

	let fromCell = matrix.getCell( this.intendedFromRow, this.intendedFromCol );
	if ( fromColOffset ) {
		fromCell = adjust( 'col', fromCell, fromColOffset );
	}
	if ( fromRowOffset ) {
		fromCell = adjust( 'row', fromCell, fromRowOffset );
	}

	let toCell = matrix.getCell( this.intendedToRow, this.intendedToCol );
	if ( toColOffset ) {
		toCell = adjust( 'col', toCell, toColOffset );
	}
	if ( toRowOffset ) {
		toCell = adjust( 'row', toCell, toRowOffset );
	}

	// Collapse to end/start if wrapping forwards/backwards
	if ( wrapDir > 0 ) {
		fromCell = toCell;
	} else if ( wrapDir < 0 ) {
		toCell = fromCell;
	}

	let selection = new this.constructor(
		this.tableRange,
		fromCell.col,
		fromCell.row,
		toCell.col,
		toCell.row
	);
	selection = selection.expand( doc );
	return selection;
};

/**
 * Check if a given cell is within this selection
 *
 * @param {ve.dm.TableMatrixCell} cell Table matrix cell
 * @return {boolean} Cell is within this selection
 */
ve.dm.TableSelection.prototype.containsCell = function ( cell ) {
	return cell.node.findParent( ve.dm.TableNode ).getOuterRange().equals( this.tableRange ) &&
		cell.col >= this.startCol && cell.col <= this.endCol &&
		cell.row >= this.startRow && cell.row <= this.endRow;
};

/**
 * @inheritdoc
 */
ve.dm.TableSelection.prototype.equals = function ( other ) {
	return this === other || (
		!!other &&
		other.constructor === this.constructor &&
		this.tableRange.equals( other.tableRange ) &&
		this.fromCol === other.fromCol &&
		this.fromRow === other.fromRow &&
		this.toCol === other.toCol &&
		this.toRow === other.toRow
	);
};

/**
 * Get the number of rows covered by the selection
 *
 * @return {number} Number of rows covered
 */
ve.dm.TableSelection.prototype.getRowCount = function () {
	return this.endRow - this.startRow + 1;
};

/**
 * Get the number of columns covered by the selection
 *
 * @return {number} Number of columns covered
 */
ve.dm.TableSelection.prototype.getColCount = function () {
	return this.endCol - this.startCol + 1;
};

/**
 * Check if the table selection covers one or more full rows
 *
 * @param {ve.dm.Document} doc The document to which this selection applies
 * @return {boolean} The table selection covers one or more full rows
 */
ve.dm.TableSelection.prototype.isFullRow = function ( doc ) {
	const matrix = this.getTableNode( doc ).getMatrix();
	return this.getColCount() === matrix.getMaxColCount();
};

/**
 * Check if the table selection covers one or more full columns
 *
 * @param {ve.dm.Document} doc The document to which this selection applies
 * @return {boolean} The table selection covers one or more full columns
 */
ve.dm.TableSelection.prototype.isFullCol = function ( doc ) {
	const matrix = this.getTableNode( doc ).getMatrix();
	return this.getRowCount() === matrix.getRowCount();
};

/* Registration */

ve.dm.selectionFactory.register( ve.dm.TableSelection );