/*!
 * VisualEditor Document class.
 *
 * @copyright See AUTHORS.txt
 */

/**
 * Generic document.
 *
 * @class
 * @mixes OO.EventEmitter
 *
 * @constructor
 * @param {ve.BranchNode} documentNode Document node
 */
ve.Document = function VeDocument( documentNode ) {
	// Mixin constructors
	OO.EventEmitter.call( this );

	// Properties
	this.documentNode = documentNode;
	this.documentNode.setDocument( this );
};

/* Inheritance */

OO.mixinClass( ve.Document, OO.EventEmitter );

/* Events */

/**
 * A node has been attached with ve.Node#setDocument . Its descendants are guaranteed
 * to be attached too (and the event is emitted for descendants first, and for siblings
 * in their order in the children list)
 *
 * @event ve.Document#nodeAttached
 * @param {ve.Node} node The node that has been attached
 */

/**
 * A node has been detached with ve.Node#setDocument . Its descendants are guaranteed
 * to be detached too (and the event is emitted for descendants first, and for siblings
 * in their order in the children list)
 *
 * @event ve.Document#nodeDetached
 * @param {ve.Node} node The node that has been detached
 */

/* Methods */

/**
 * Get the root of the document's node tree.
 *
 * @return {ve.BranchNode} Root of node tree
 */
ve.Document.prototype.getDocumentNode = function () {
	return this.documentNode;
};

/**
 * Get a node at an offset.
 *
 * @param {number} offset Offset to get node at
 * @return {ve.Node|null} Node at offset
 */
ve.Document.prototype.getBranchNodeFromOffset = function ( offset ) {
	let node = this.getDocumentNode().getNodeFromOffset( offset );
	if ( node && !node.hasChildren() ) {
		node = node.getParent();
	}
	return node;
};

/**
 * Gets a list of nodes and the ranges within them that a selection of the document covers.
 *
 * @param {ve.Range} range Range within document to select nodes
 * @param {string} [mode='leaves'] Type of selection to perform:
 *
 * - `leaves`: Return all leaf nodes in the given range (descends all the way down)
 * - `branches`': Return all branch nodes in the given range
 * - `covered`: Do not descend into nodes that are entirely covered by the range. The result
 *   is similar to that of 'leaves' except that if a node is entirely covered, its
 *   children aren't returned separately.
 * - `siblings`: Return a set of adjacent siblings covered by the range (descends as long as the
 *   range is in a single node)
 * @return {Array} List of objects describing nodes in the selection and the ranges therein:
 *
 * - `node`: Reference to a ve.Node
 * - `range`: ve.Range, missing if the entire node is covered
 * - `index`: Index of the node in its parent, missing if node has no parent
 * - `indexInNode`: If range is a zero-length range between two children of node,
 *   this is set to the index of the child following range (or to
 *   `node.children.length + 1` if range is between the last child and
 *   the end). If range is a zero-length range inside an empty non-content branch node, this is 0.
 *   Missing in all other cases.
 * - `nodeRange`: Range covering the inside of the entire node, not including wrapper
 * - `nodeOuterRange`: Range covering the entire node, including wrapper
 * - `parentOuterRange`: Outer range of node's parent. Missing if there is no parent
 *   or if indexInNode is set.
 *
 * @throws {Error} Invalid mode
 * @throws {Error} Invalid start offset
 * @throws {Error} Invalid end offset
 * @throws {Error} Failed to select any nodes
 */
ve.Document.prototype.selectNodes = function ( range, mode ) {
	const doc = this.getDocumentNode(),
		start = range.start,
		end = range.end,
		stack = [ {
			// Node we are currently stepping through
			// Note each iteration visits a child of node, not node itself
			node: doc,
			// Index of the child in node we're visiting
			index: 0,
			// First offset inside node
			startOffset: 0
		} ];
	let retval = [],
		currentFrame = stack[ 0 ],
		startFound = false;

	mode = mode || 'leaves';
	if ( mode !== 'leaves' && mode !== 'branches' && mode !== 'covered' && mode !== 'siblings' ) {
		throw new Error( 'Invalid mode: ' + mode );
	}

	if ( start < 0 || start > doc.getLength() ) {
		throw new Error( 'Invalid start offset: ' + start );
	}
	if ( end < 0 || end > doc.getLength() ) {
		throw new Error( 'Invalid end offset: ' + end );
	}

	if ( !doc.children || doc.children.length === 0 ) {
		// Document has no children. This is weird
		const nodeRange = new ve.Range( 0, doc.getLength() );
		return [ {
			node: doc,
			range: new ve.Range( start, end ),
			index: 0,
			nodeRange: nodeRange,
			nodeOuterRange: nodeRange
		} ];
	}
	let left = doc.children[ 0 ].isWrapped() ? 1 : 0;

	do {
		const node = currentFrame.node.children[ currentFrame.index ];
		const prevNode = currentFrame.node.children[ currentFrame.index - 1 ];
		let nextNode = currentFrame.node.children[ currentFrame.index + 1 ];
		const right = left + node.getLength();
		// Is the start inside node?
		const startInside = start >= left && start <= right;
		// Is the end inside node?
		const endInside = end >= left && end <= right;
		// Does the node have wrapping elements around it
		let isWrapped = node.isWrapped();
		// Is there an unwrapped node right before this node?
		const isPrevUnwrapped = prevNode ? !prevNode.isWrapped() : false;
		// Is there an unwrapped node right after this node?
		const isNextUnwrapped = nextNode ? !nextNode.isWrapped() : false;
		// Is this node an empty non-content branch node?
		const isEmptyBranch = ( node.getLength() === 0 || node.shouldIgnoreChildren() ) &&
			!node.isContent() && !node.canContainContent();
		// Is the start between prevNode's closing and node or between the parent's opening and node?
		const startBetween = ( isWrapped ? start === left - 1 : start === left ) && !isPrevUnwrapped;
		// Is the end between node and nextNode's opening or between node and the parent's closing?
		const endBetween = ( isWrapped ? end === right + 1 : end === right ) && !isNextUnwrapped;
		let parentRange = new ve.Range(
			currentFrame.startOffset,
			currentFrame.startOffset + currentFrame.node.getLength()
		);

		let parentFrame;
		if ( isWrapped && end === left - 1 && currentFrame.index === 0 ) {
			// The selection ends here with an empty range at the beginning of the node
			// TODO duplicated code
			isWrapped = currentFrame.node.isWrapped();
			retval.push( {
				node: currentFrame.node,
				indexInNode: 0,
				range: new ve.Range( end, end ),
				nodeRange: parentRange,
				nodeOuterRange: new ve.Range(
					parentRange.start - isWrapped, parentRange.end + isWrapped
				)
			} );
			parentFrame = stack[ stack.length - 2 ];
			if ( parentFrame ) {
				retval[ retval.length - 1 ].index = parentFrame.index;
			}
			return retval;
		}

		if ( start === end && ( startBetween || endBetween ) && isWrapped ) {
			// Empty range in the parent, outside of any child
			isWrapped = currentFrame.node.isWrapped();
			retval = [ {
				node: currentFrame.node,
				indexInNode: currentFrame.index + ( endBetween ? 1 : 0 ),
				range: new ve.Range( start, end ),
				nodeRange: parentRange,
				nodeOuterRange: new ve.Range(
					parentRange.start - isWrapped, parentRange.end + isWrapped
				)
			} ];
			parentFrame = stack[ stack.length - 2 ];
			if ( parentFrame ) {
				retval[ 0 ].index = parentFrame.index;
			}
			return retval;
		} else if ( startBetween ) {
			// start is between the previous sibling and node
			// so the selection covers all or part of node

			// Descend if
			// - we are in leaves mode, OR
			// - we are in covered mode and the end is inside node OR
			// - we are in branches mode and node is a branch (can have grandchildren)
			// AND
			// the node is non-empty and doesn't handle its own children
			if ( ( mode === 'leaves' ||
					( mode === 'covered' && endInside ) ||
					( mode === 'branches' && node.canHaveChildrenNotContent() ) ) &&
				node.children && node.children.length && !node.shouldIgnoreChildren()
			) {
				// Descend into node
				currentFrame = {
					node: node,
					index: 0,
					startOffset: left
				};
				stack.push( currentFrame );
				startFound = true;
				// If the first child of node has an opening, skip over it
				if ( node.children[ 0 ].isWrapped() ) {
					left++;
				}
				continue;
			} else if ( !endInside ) {
				// All of node is covered
				retval.push( {
					node: node,
					// No 'range' because the entire node is covered
					index: currentFrame.index,
					nodeRange: new ve.Range( left, right ),
					nodeOuterRange: new ve.Range( left - isWrapped, right + isWrapped ),
					parentOuterRange: new ve.Range(
						parentRange.start - currentFrame.node.isWrapped(),
						parentRange.end + currentFrame.node.isWrapped()
					)
				} );
				startFound = true;
			} else {
				// Part of node is covered
				return [ {
					node: node,
					range: new ve.Range( start, end ),
					index: currentFrame.index,
					nodeRange: new ve.Range( left, right ),
					nodeOuterRange: new ve.Range( left - isWrapped, right + isWrapped ),
					parentOuterRange: new ve.Range(
						parentRange.start - currentFrame.node.isWrapped(),
						parentRange.end + currentFrame.node.isWrapped()
					)
				} ];
			}
		} else if ( startInside && endInside ) {
			if ( node.children && node.children.length &&
				( mode !== 'branches' || node.canHaveChildrenNotContent() ) ) {
				// Descend into node
				currentFrame = {
					node: node,
					index: 0,
					startOffset: left
				};
				stack.push( currentFrame );
				// If the first child of node has an opening, skip over it
				if ( node.children[ 0 ].isWrapped() ) {
					left++;
				}
				continue;
			} else {
				// node is a leaf node and the range is entirely inside it
				retval = [ {
					node: node,
					range: new ve.Range( start, end ),
					index: currentFrame.index,
					nodeRange: new ve.Range( left, right ),
					nodeOuterRange: new ve.Range( left - isWrapped, right + isWrapped ),
					parentOuterRange: new ve.Range(
						parentRange.start - currentFrame.node.isWrapped(),
						parentRange.end + currentFrame.node.isWrapped()
					)
				} ];
				if ( isEmptyBranch ) {
					retval[ 0 ].indexInNode = 0;
				}
				return retval;
			}
		} else if ( startInside ) {
			if ( ( mode === 'leaves' ||
					mode === 'covered' ||
					( mode === 'branches' && node.canHaveChildrenNotContent() ) ) &&
				node.children && node.children.length
			) {
				// node is a branch node and the start is inside it
				// Descend into it
				currentFrame = {
					node: node,
					index: 0,
					startOffset: left
				};
				stack.push( currentFrame );
				// If the first child of node has an opening, skip over it
				if ( node.children[ 0 ].isWrapped() ) {
					left++;
				}
				continue;
			} else {
				// node is a leaf node and the start is inside it
				// Add to retval and keep going
				retval.push( {
					node: node,
					range: new ve.Range( start, right ),
					index: currentFrame.index,
					nodeRange: new ve.Range( left, right ),
					nodeOuterRange: new ve.Range( left - isWrapped, right + isWrapped ),
					parentOuterRange: new ve.Range(
						parentRange.start - currentFrame.node.isWrapped(),
						parentRange.end + currentFrame.node.isWrapped()
					)
				} );
				startFound = true;
			}
		} else if ( endBetween ) {
			// end is between node and the next sibling
			// start is not inside node, so the selection covers
			// all of node, then ends

			if (
				( mode === 'leaves' || ( mode === 'branches' && node.canHaveChildrenNotContent() ) ) &&
				node.children && node.children.length
			) {
				// Descend into node
				currentFrame = {
					node: node,
					index: 0,
					startOffset: left
				};
				stack.push( currentFrame );
				// If the first child of node has an opening, skip over it
				if ( node.children[ 0 ].isWrapped() ) {
					left++;
				}
				continue;
			} else {
				// All of node is covered
				retval.push( {
					node: node,
					// No 'range' because the entire node is covered
					index: currentFrame.index,
					nodeRange: new ve.Range( left, right ),
					nodeOuterRange: new ve.Range( left - isWrapped, right + isWrapped ),
					parentOuterRange: new ve.Range(
						parentRange.start - currentFrame.node.isWrapped(),
						parentRange.end + currentFrame.node.isWrapped()
					)
				} );
				return retval;
			}
		} else if ( endInside ) {
			if ( ( mode === 'leaves' ||
					mode === 'covered' ||
					( mode === 'branches' && node.canHaveChildrenNotContent() ) ) &&
				node.children && node.children.length
			) {
				// node is a branch node and the end is inside it
				// Descend into it
				currentFrame = {
					node: node,
					index: 0,
					startOffset: left
				};
				stack.push( currentFrame );
				// If the first child of node has an opening, skip over it
				if ( node.children[ 0 ].isWrapped() ) {
					left++;
				}
				continue;
			} else {
				// node is a leaf node and the end is inside it
				// Add to retval and return
				retval.push( {
					node: node,
					range: new ve.Range( left, end ),
					index: currentFrame.index,
					nodeRange: new ve.Range( left, right ),
					nodeOuterRange: new ve.Range( left - isWrapped, right + isWrapped ),
					parentOuterRange: new ve.Range(
						parentRange.start - currentFrame.node.isWrapped(),
						parentRange.end + currentFrame.node.isWrapped()
					)
				} );
				return retval;
			}
		} else if ( startFound && end > right ) {
			// Neither the start nor the end is inside node, but we found the start earlier,
			// so node must be between the start and the end
			// Add the entire node, so no range property

			if (
				( mode === 'leaves' || ( mode === 'branches' && node.canHaveChildrenNotContent() ) ) &&
				node.children && node.children.length
			) {
				// Descend into node
				currentFrame = {
					node: node,
					index: 0,
					startOffset: left
				};
				stack.push( currentFrame );
				// If the first child of node has an opening, skip over it
				if ( node.children[ 0 ].isWrapped() ) {
					left++;
				}
				continue;
			} else {
				// All of node is covered
				retval.push( {
					node: node,
					// No 'range' because the entire node is covered
					index: currentFrame.index,
					nodeRange: new ve.Range( left, right ),
					nodeOuterRange: new ve.Range( left - isWrapped, right + isWrapped ),
					parentOuterRange: new ve.Range(
						parentRange.start - currentFrame.node.isWrapped(),
						parentRange.end + currentFrame.node.isWrapped()
					)
				} );
			}
		}

		// Move to the next node
		if ( nextNode ) {
			// The next node exists
			// Advance the index; the start of the next iteration will essentially
			// do node = nextNode;
			currentFrame.index++;
			// Advance to the first offset inside nextNode
			left = right +
				// Skip over node's closing, if present
				( node.isWrapped() ? 1 : 0 ) +
				// Skip over nextNode's opening, if present
				( nextNode.isWrapped() ? 1 : 0 );
		} else {
			// There is no next node, move up the stack until there is one
			left = right +
				// Skip over node's closing, if present
				( node.isWrapped() ? 1 : 0 );
			while ( !nextNode ) {
				// Check if the start is right past the end of this node, at the end of
				// the parent
				if ( node.isWrapped() && start === left ) {
					// TODO duplicated code
					parentRange = new ve.Range( currentFrame.startOffset,
						currentFrame.startOffset + currentFrame.node.getLength()
					);
					isWrapped = currentFrame.node.isWrapped();
					retval = [ {
						node: currentFrame.node,
						indexInNode: currentFrame.index + 1,
						range: new ve.Range( left, left ),
						nodeRange: parentRange,
						nodeOuterRange: new ve.Range(
							parentRange.start - isWrapped, parentRange.end + isWrapped
						)
					} ];
					parentFrame = stack[ stack.length - 2 ];
					if ( parentFrame ) {
						retval[ 0 ].index = parentFrame.index;
					}
				}

				// Move up the stack
				stack.pop();
				if ( stack.length === 0 ) {
					// This shouldn't be possible
					return retval;
				}
				currentFrame = stack[ stack.length - 1 ];
				currentFrame.index++;
				nextNode = currentFrame.node.children[ currentFrame.index ];
				// Skip over the parent node's closing
				// (this is present for sure, because the parent has children)
				left++;
			}

			// Skip over nextNode's opening if present
			if ( nextNode.isWrapped() ) {
				left++;
			}
		}
	} while ( end >= left - 1 );
	if ( retval.length === 0 ) {
		throw new Error( 'Failed to select any nodes' );
	}
	return retval;
};

/**
 * Get groups of sibling nodes covered by the given range.
 *
 * @param {ve.Range} range
 * @return {Array} Array of objects. Each object has the following keys:
 *
 *  - nodes: Array of sibling nodes covered by a part of range
 *  - parent: Parent of all of these nodes
 *  - grandparent: parent's parent
 */
ve.Document.prototype.getCoveredSiblingGroups = function ( range ) {
	const leaves = this.selectNodes( range, 'leaves' ),
		groups = [];
	let lastEndOffset = 0;
	for ( let i = 0; i < leaves.length; i++ ) {
		if ( leaves[ i ].nodeOuterRange.end <= lastEndOffset ) {
			// This range is contained within a range we've already processed
			continue;
		}
		let node = leaves[ i ].node;
		// Traverse up to a content branch from content elements
		if ( node.isContent() ) {
			node = node.getParent();
		}
		const parentNode = node.getParent();
		if ( !parentNode ) {
			break;
		}
		// Group this with its covered siblings
		groups.push( {
			parent: parentNode,
			grandparent: parentNode.getParent(),
			nodes: []
		} );
		// Seek forward to the last covered sibling
		let siblingNode = node;
		do {
			// Add this to its sibling's group
			groups[ groups.length - 1 ].nodes.push( siblingNode );
			i++;
			if ( leaves[ i ] === undefined ) {
				break;
			}
			// Traverse up to a content branch from content elements
			siblingNode = leaves[ i ].node;
			if ( siblingNode.isContent() ) {
				siblingNode = siblingNode.getParent();
			}
		} while ( siblingNode.getParent() === parentNode );
		i--;
		lastEndOffset = parentNode.getOuterRange().end;
	}
	return groups;
};

/**
 * Test whether a range lies within a single leaf node.
 *
 * @param {ve.Range} range The range to test
 * @return {boolean} Whether the range lies within a single node
 */
ve.Document.prototype.rangeInsideOneLeafNode = function ( range ) {
	const selected = this.selectNodes( range, 'leaves' );
	return selected.length === 1 && selected[ 0 ].nodeRange.containsRange( range ) && selected[ 0 ].indexInNode === undefined;
};

/**
 * Callback when a node is attached with ve.Node#setDocument
 *
 * The node and all its children are guaranteed to be attached
 *
 * @param {ve.Node} node The node attached
 * @fires ve.Document#nodeAttached
 */
ve.Document.prototype.nodeAttached = function ( node ) {
	this.emit( 'nodeAttached', node );
};

/**
 * Callback when a node is attached with ve.Node#setDocument
 *
 * The node and all its children are guaranteed to be attached
 *
 * @param {ve.Node} node The node detached
 * @fires ve.Document#nodeDetached
 */
ve.Document.prototype.nodeDetached = function ( node ) {
	this.emit( 'nodeDetached', node );
};
