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

/**
 * Branch node mixin.
 *
 * Extenders are expected to inherit from ve.Node.
 *
 * Branch nodes are immutable, which is why there are no methods for adding or removing children.
 * DataModel classes will add this functionality, and other subclasses will implement behavior that
 * mimics changes made to DataModel nodes.
 *
 * @class
 * @abstract
 * @constructor
 * @param {ve.Node[]} children Array of children to add
 */
ve.BranchNode = function VeBranchNode( children ) {
	this.children = Array.isArray( children ) ? children : [];
};

/* Setup */

OO.initClass( ve.BranchNode );

/* Methods */

/**
 * Traverse a branch node depth-first.
 *
 * @param {Function} callback Callback to execute for each traversed node
 * @param {ve.Node} callback.node Node being traversed
 */
ve.BranchNode.prototype.traverse = function ( callback ) {
	const children = this.getChildren();

	for ( let i = 0, len = children.length; i < len; i++ ) {
		callback.call( this, children[ i ] );
		if ( children[ i ].hasChildren() ) {
			children[ i ].traverse( callback );
		}
	}
};

/**
 * Check if the node has children.
 *
 * @return {boolean} Whether the node has children
 */
ve.BranchNode.prototype.hasChildren = function () {
	return true;
};

/**
 * Get child nodes.
 *
 * @return {ve.Node[]} List of child nodes
 */
ve.BranchNode.prototype.getChildren = function () {
	return this.children;
};

/**
 * Get the index of a child node.
 *
 * @param {ve.dm.Node} node Child node to find index of
 * @return {number} Index of child node or -1 if node was not found
 */
ve.BranchNode.prototype.indexOf = function ( node ) {
	return this.children.indexOf( node );
};

/**
 * Set the root node.
 *
 * @see ve.Node#setRoot
 * @param {ve.BranchNode|null} root Node to use as root
 */
ve.BranchNode.prototype.setRoot = function ( root ) {
	const oldRoot = this.root;
	if ( root === oldRoot ) {
		// Nothing to do, don't recurse into all descendants
		return;
	}
	if ( oldRoot ) {
		// Null the root, then recurse into children, then emit unroot.
		// That way, at emit time, all this node's ancestors and descendants have
		// null root.
		this.root = null;
		for ( let i = 0, len = this.children.length; i < len; i++ ) {
			this.children[ i ].setRoot( null );
		}
		this.emit( 'unroot', oldRoot );
	}
	this.root = root;
	if ( root ) {
		// We've set the new root, so recurse into children, then emit root.
		// That way, at emit time, all this node's ancestors and descendants have
		// the new root.
		for ( let i = 0, len = this.children.length; i < len; i++ ) {
			this.children[ i ].setRoot( root );
		}
		this.emit( 'root', root );
	}
};

/**
 * Set the document the node is a part of.
 *
 * @see ve.Node#setDocument
 * @param {ve.Document} doc Document this node is a part of
 */
ve.BranchNode.prototype.setDocument = function ( doc ) {
	const oldDoc = this.doc;
	if ( doc === this.doc ) {
		// Nothing to do, don't recurse into all descendants
		return;
	}
	let i, len;
	if ( oldDoc ) {
		// Null the doc, then recurse into children, then notify the doc.
		// That way, at notify time, all this node's ancestors and descendants have
		// null doc.
		this.doc = null;
		for ( i = 0, len = this.children.length; i < len; i++ ) {
			this.children[ i ].setDocument( null );
		}
		oldDoc.nodeDetached( this );
	}
	this.doc = doc;
	if ( doc ) {
		// We've set the new doc, so recurse into children, then notify the doc.
		// That way, at notify time, all this node's ancestors and descendants have
		// the new doc.
		for ( i = 0, len = this.children.length; i < len; i++ ) {
			this.children[ i ].setDocument( doc );
		}
		doc.nodeAttached( this );
	}
};

/**
 * Get a node from an offset.
 *
 * This method is pretty expensive. If you need to get different slices of the same content, get
 * the content first, then slice it up locally.
 *
 * @param {number} offset Offset get node for
 * @param {boolean} [shallow] Do not iterate into child nodes of child nodes
 * @return {ve.Node|null} Node at offset, or null if none was found
 * @throws {Error} If offset is out of bounds
 */
ve.BranchNode.prototype.getNodeFromOffset = function ( offset, shallow ) {
	let currentNode = this;
	if ( typeof offset !== 'number' ) {
		throw new Error( 'Offset must be a number' );
	}
	if ( offset === 0 ) {
		return currentNode;
	}
	if ( offset < 0 ) {
		throw new Error( 'Offset out of bounds' );
	}
	let nodeOffset = 0;
	// TODO a lot of logic is duplicated in selectNodes(), abstract that into a traverser or something
	SIBLINGS:
	while ( currentNode.children.length ) {
		for ( let i = 0, length = currentNode.children.length; i < length; i++ ) {
			const childNode = currentNode.children[ i ];
			if ( offset === nodeOffset ) {
				// The requested offset is right before childNode, so it's not
				// inside any of currentNode's children, but is inside currentNode
				return currentNode;
			}
			if ( childNode instanceof ve.ce.InternalListNode ) {
				break SIBLINGS;
			}
			const nodeLength = childNode.getOuterLength();
			if ( offset >= nodeOffset && offset < nodeOffset + nodeLength ) {
				if ( !shallow && childNode.hasChildren() && childNode.getChildren().length ) {
					// One of the children contains the node; increment to
					// enter the node, then iterate through children
					nodeOffset += 1;
					currentNode = childNode;
					continue SIBLINGS;
				} else {
					return childNode;
				}
			}
			nodeOffset += nodeLength;
		}
		if ( offset === nodeOffset ) {
			// The requested offset is right before currentNode.children[i], so it's
			// not inside any of currentNode's children, but is inside currentNode
			return currentNode;
		}
	}
	return null;
};
