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

/**
 * DataModel branch node.
 *
 * Branch nodes can have branch or leaf nodes as children.
 *
 * @abstract
 * @extends ve.dm.Node
 * @mixes ve.BranchNode
 *
 * @constructor
 * @param {Object} [element] Reference to element in linear model
 * @param {ve.dm.Node[]} [children] Child nodes to attach
 */
ve.dm.BranchNode = function VeDmBranchNode( element, children ) {
	// Mixin constructor
	ve.BranchNode.call( this );

	// Parent constructor
	ve.dm.BranchNode.super.call( this, element );

	// Properties
	this.slugPositions = {};

	// TODO: children is only ever used in tests
	if ( Array.isArray( children ) && children.length ) {
		this.splice( 0, 0, ...children );
	}
};

/**
 * @event ve.dm.BranchNode#splice
 * @see #method-splice
 * @param {number} index
 * @param {number} deleteCount
 * @param {...ve.dm.BranchNode} [nodes]
 */

/* Inheritance */

OO.inheritClass( ve.dm.BranchNode, ve.dm.Node );

OO.mixinClass( ve.dm.BranchNode, ve.BranchNode );

/* Methods */

/**
 * Add a child node to the end of the list.
 *
 * @param {ve.dm.BranchNode} childModel Item to add
 * @return {number} New number of children
 */
ve.dm.BranchNode.prototype.push = function ( childModel ) {
	this.splice( this.children.length, 0, childModel );
	return this.children.length;
};

/**
 * Remove a child node from the end of the list.
 *
 * @return {ve.dm.BranchNode|undefined} Removed childModel
 */
ve.dm.BranchNode.prototype.pop = function () {
	if ( this.children.length ) {
		const childModel = this.children[ this.children.length - 1 ];
		this.splice( this.children.length - 1, 1 );
		return childModel;
	}
};

/**
 * Add a child node to the beginning of the list.
 *
 * @param {ve.dm.BranchNode} childModel Item to add
 * @return {number} New number of children
 */
ve.dm.BranchNode.prototype.unshift = function ( childModel ) {
	this.splice( 0, 0, childModel );
	return this.children.length;
};

/**
 * Remove a child node from the beginning of the list.
 *
 * @return {ve.dm.BranchNode|undefined} Removed childModel
 */
ve.dm.BranchNode.prototype.shift = function () {
	if ( this.children.length ) {
		const childModel = this.children[ 0 ];
		this.splice( 0, 1 );
		return childModel;
	}
};

/**
 * Add and/or remove child nodes at an offset.
 *
 * @param {number} index Index to remove and or insert nodes at
 * @param {number} deleteCount Number of nodes to remove
 * @param {...ve.dm.BranchNode} [nodes] Variadic list of nodes to insert
 * @fires ve.dm.BranchNode#splice
 * @return {ve.dm.BranchNode[]} Removed nodes
 */
ve.dm.BranchNode.prototype.splice = function ( index, deleteCount, ...nodes ) {
	let diff = 0;

	const removals = this.children.splice( index, deleteCount, ...nodes );
	removals.forEach( ( node ) => {
		node.detach();
		diff -= node.getOuterLength();
	} );

	nodes.forEach( ( node ) => {
		node.attach( this );
		diff += node.getOuterLength();
	} );

	this.adjustLength( diff, true );
	this.setupBlockSlugs();
	this.emit( 'splice', index, deleteCount, ...nodes );

	return removals;
};

/**
 * Setup a sparse array of booleans indicating where to place slugs
 *
 * TODO: The function name is misleading: in ContentBranchNodes it sets up inline slugs
 */
ve.dm.BranchNode.prototype.setupBlockSlugs = function () {
	const isBlock = this.canHaveChildrenNotContent();

	this.slugPositions = {};

	if ( isBlock && !this.isAllowedChildNodeType( 'paragraph' ) ) {
		// Don't put slugs in nodes which can't contain paragraphs
		return;
	}

	// Consider every position between two child nodes, before first child and after last child.
	// Skip over metadata children. Add slugs in appropriate places.

	// Support: Firefox
	// Note that this inserts a slug at position 0 if this content branch has no items or only
	// internal items, keeping the node from becoming invisible/unfocusable. In Firefox, backspace
	// after Ctrl+A leaves the document completely empty, so this ensures DocumentNode gets a slug.

	const len = this.children.length;
	let i = -1; // from -1 to len-1
	let j = 0; // from 0 to len
	while ( i < len ) {
		// If the next node is a meta item, find the first non-meta node after it, and consider that
		// one instead when deciding to insert a slug. Meta nodes themselves don't have slugs.
		while ( j < len && this.children[ j ].isMetaData() ) {
			j++;
		}

		// Can have slug at the beginning, or after every node which allows it (except internal nodes)
		const canHaveSlugAfter = i === -1 || ( this.children[ i ].canHaveSlugAfter() &&
			!this.children[ i ].isInternal() );
		// Can have slug at the end, or before every node which allows it
		const canHaveSlugBefore = j === len || this.children[ j ].canHaveSlugBefore();

		if ( canHaveSlugAfter && canHaveSlugBefore ) {
			const suppressSlugTypeAfter = this.children[ j ] && this.children[ j ].suppressSlugType();
			const suppressSlugTypeBefore = this.children[ i ] && this.children[ i ].suppressSlugType();
			// Slugs are suppressed if they have the same string type, e.g. for adjacent floated images
			if ( !( typeof suppressSlugTypeAfter === 'string' && suppressSlugTypeAfter === suppressSlugTypeBefore ) ) {
				this.slugPositions[ j ] = true;
			}
		}

		i = j;
		j++;
	}
};

/**
 * Check in the branch node has a slug at a particular offset
 *
 * @param {number} offset Offset to check for a slug at
 * @return {boolean} There is a slug at the offset
 */
ve.dm.BranchNode.prototype.hasSlugAtOffset = function ( offset ) {
	let startOffset = this.getOffset() + ( this.isWrapped() ? 1 : 0 );

	if ( offset === startOffset ) {
		return !!this.slugPositions[ 0 ];
	}
	for ( let i = 0; i < this.children.length; i++ ) {
		startOffset += this.children[ i ].getOuterLength();
		if ( offset === startOffset ) {
			return !!this.slugPositions[ i + 1 ];
		}
	}
	return false;
};

/**
 * Get all annotations and the ranges they cover
 *
 * @return {ve.dm.ElementLinearData.AnnotationRange[]} Contiguous annotation ranges, ordered by start then end
 */
ve.dm.BranchNode.prototype.getAnnotationRanges = function () {
	const annotationRanges = [];
	this.traverse( ( node ) => {
		if ( node.canContainContent() ) {
			annotationRanges.push(
				...this.getDocument().data.getAnnotationRanges( node.getRange() )
			);
		}
	} );
	return annotationRanges;
};