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

/**
 * Generic DataModel node.
 *
 * @abstract
 * @extends ve.dm.Model
 * @mixes OO.EventEmitter
 * @mixes ve.Node
 *
 * @constructor
 * @param {Object} [element] Reference to element in linear model
 */
ve.dm.Node = function VeDmNode() {
	// Parent constructor
	ve.dm.Node.super.apply( this, arguments );

	// Mixin constructors
	ve.Node.call( this );
	OO.EventEmitter.call( this );

	// Properties
	this.length = 0;
};

/**
 * @event ve.dm.Node#attributeChange
 * @param {string} key
 * @param {any} oldValue
 * @param {any} newValue
 */

/**
 * @event ve.dm.Node#lengthChange
 * @param {number} diff
 */

/**
 * @event ve.dm.Node#update
 */

/* Inheritance */

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

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

OO.mixinClass( ve.dm.Node, OO.EventEmitter );

/* Static Properties */

/**
 * Whether this node handles its own children. After converting a DOM node to a linear model
 * node of this type, the converter checks this property. If it's false, the converter will descend
 * into the DOM node's children, recursively convert them, and attach the resulting nodes as
 * children of the linear model node. If it's true, the converter will not descend, and will
 * expect the node's toDataElement() to have handled the entire DOM subtree.
 *
 * The same is true when converting from linear model data to DOM: if this property is true,
 * toDomElements() will be passed the node's data element and all of its children and will be
 * expected to convert the entire subtree. If it's false, the converter will descend into the
 * child nodes and convert each one individually.
 *
 * If .static.childNodeTypes is set to [], this property is ignored and will be assumed to be true.
 *
 * @static
 * @property {boolean}
 * @inheritable
 */
ve.dm.Node.static.handlesOwnChildren = false;

/**
 * Whether this node's children should be ignored when iterating over the model, for example
 * content which is stored inline, but is actually conceptually out-of-band data, such as a
 * reference node which contains the reference content (like `<ref>` tags in MediaWiki).
 *
 * This property should not be used for data which isn't out-of-band but is less accessible in
 * the view, e.g. content in table cells or image captions.
 *
 * If true, this node will be treated as a leaf node even if it has children.
 * Often used in combination with handlesOwnChildren.
 *
 * @static
 * @property {boolean}
 * @inheritable
 */
ve.dm.Node.static.ignoreChildren = false;

/**
 * Whether this node can be deleted. If false, ve.dm.Transaction#newFromRemoval will silently
 * ignore any attepts to delete this node.
 *
 * @static
 * @property {boolean}
 * @inheritable
 */
ve.dm.Node.static.isDeletable = true;

/**
 * Whether this node type is internal. Internal node types are ignored by the converter.
 *
 * @static
 * @property {boolean}
 * @inheritable
 */
ve.dm.Node.static.isInternal = false;

/**
 * Whether this node type has a wrapping element in the linear model. Most node types are wrapped,
 * only special node types are not wrapped.
 *
 * @static
 * @property {boolean}
 * @inheritable
 */
ve.dm.Node.static.isWrapped = true;

/**
 * Whether this node type can be unwrapped by user input (e.g. backspace to unwrap a list item)
 *
 * @static
 * @property {boolean}
 * @inheritable
 */
ve.dm.Node.static.isUnwrappable = true;

/**
 * Whether this node type is a content node type. This means the node represents content, cannot
 * have children, and can only appear as children of a content container node. Content nodes are
 * also known as inline nodes.
 *
 * @static
 * @property {boolean}
 * @inheritable
 */
ve.dm.Node.static.isContent = false;

/**
 * Whether this node type is a metadata node. This means the node represents a leaf node that
 * has no explicit view representation, and should be treated differently for the purposes of
 * round-tripping, copy/paste etc.
 *
 * @static
 * @property {boolean}
 * @inheritable
 */
ve.dm.Node.static.isMetaData = false;

/**
 * For a non-content node type, whether this node type can be serialized in a content
 * position (e.g. for round tripping). This value is ignored if isContent is true.
 *
 * @static
 * @property {boolean}
 * @inheritable
 */
ve.dm.Node.static.canSerializeAsContent = false;

/**
 * Whether this node type can be focused. Focusable nodes react to selections differently.
 *
 * @static
 * @property {boolean}
 * @inheritable
 */
ve.dm.Node.static.isFocusable = false;

/**
 * Whether this node type is alignable.
 *
 * @static
 * @property {boolean}
 * @inheritable
 */
ve.dm.Node.static.isAlignable = false;

/**
 * Whether this node type can behave as a table cell.
 *
 * @static
 * @property {boolean}
 * @inheritable
 */
ve.dm.Node.static.isCellable = false;

/**
 * Whether this node type can contain content. The children of content container nodes must be
 * content nodes.
 *
 * @static
 * @property {boolean}
 * @inheritable
 */
ve.dm.Node.static.canContainContent = false;

/**
 * Whether this node type behaves like a list when diffing.
 *
 * @static
 * @property {boolean}
 * @inheritable
 */
ve.dm.Node.static.isDiffedAsList = false;

/**
 * Whether this node type behaves like a leaf when diffing.
 *
 * @static
 * @property {boolean}
 * @inheritable
 */
ve.dm.Node.static.isDiffedAsLeaf = false;

/**
 * Whether this node type has significant whitespace. Only applies to content container nodes
 * (i.e. can only be true if canContainContent is also true).
 *
 * If a content node has significant whitespace, the text inside it is not subject to whitespace
 * stripping and preservation.
 *
 * @static
 * @property {boolean}
 * @inheritable
 */
ve.dm.Node.static.hasSignificantWhitespace = false;

/**
 * Array of allowed child node types for this node type.
 *
 * An empty array means no children are allowed. null means any node type is allowed as a child.
 *
 * @static
 * @property {string[]|null}
 * @inheritable
 */
ve.dm.Node.static.childNodeTypes = null;

/**
 * Array of allowed parent node types for this node type.
 *
 * An empty array means this node type cannot be the child of any node. null means this node type
 * can be the child of any node type.
 *
 * @static
 * @property {string[]|null}
 * @inheritable
 */
ve.dm.Node.static.parentNodeTypes = null;

/**
 * Array of suggested parent node types for this node type.
 *
 * These parent node types are allowed but the editor will avoid creating them.
 *
 * An empty array means this node type should not be the child of any node. null means this node type
 * can be the child of any node type.
 *
 * @static
 * @property {string[]|null}
 * @inheritable
 */
ve.dm.Node.static.suggestedParentNodeTypes = null;

/**
 * Array of annotation types which can't be applied to this node
 *
 * @static
 * @property {string[]}
 * @inheritable
 */
ve.dm.Node.static.disallowedAnnotationTypes = [];

/**
 * Default attributes to set for newly created linear model elements. These defaults will be used
 * when creating a new element in ve.dm.NodeFactory#getDataElement when there is no DOM node or
 * existing linear model element to base the attributes on.
 *
 * This property is an object with attribute names as keys and attribute values as values.
 * Attributes may be omitted, in which case they'll simply be undefined.
 *
 * @static
 * @property {Object}
 * @inheritable
 */
ve.dm.Node.static.defaultAttributes = {};

/**
 * Sanitize the node's linear model data, typically if it was generated from an external source (e.g. copied HTML)
 *
 * @param {ve.dm.LinearData.Element} dataElement Linear model element, modified in place
 */
ve.dm.Node.static.sanitize = function () {
};

/**
 * Remap the internal list indexes stored in a linear model data element.
 *
 * The default implementation is empty. Nodes should override this if they store internal list
 * indexes in attributes. To remap, do something like
 * dataElement.attributes.foo = mapping[dataElement.attributes.foo];
 *
 * @static
 * @inheritable
 * @param {ve.dm.LinearData.Element} dataElement Data element (opening) to remap. Will be modified.
 * @param {Object} mapping Object mapping old internal list indexes to new internal list indexes
 * @param {ve.dm.InternalList} internalList Internal list the indexes are being mapped into.
 *  Used for refreshing attribute values that were computed with getNextUniqueNumber().
 */
ve.dm.Node.static.remapInternalListIndexes = function () {
};

/**
 * Remap the internal list keys stored in a linear model data element.
 *
 * The default implementation is empty. Nodes should override this if they store internal list
 * keys in attributes.
 *
 * @static
 * @inheritable
 * @param {ve.dm.LinearData.Element} dataElement Data element (opening) to remap. Will be modified.
 * @param {ve.dm.InternalList} internalList Internal list the keys are being mapped into.
 */
ve.dm.Node.static.remapInternalListKeys = function () {
};

/**
 * Determine if a hybrid element is inline and allowed to be inline in this context
 *
 * We generate block elements for block tags and inline elements for inline
 * tags; unless we're in a content location, in which case we have no choice
 * but to generate an inline element.
 *
 * @static
 * @param {HTMLElement[]} domElements DOM elements being converted
 * @param {ve.dm.ModelFromDomConverter} converter
 * @return {boolean} The element is inline
 */
ve.dm.Node.static.isHybridInline = function ( domElements, converter ) {
	// Force inline in content locations (but not wrappers)
	return ( converter.isExpectingContent() && !converter.isInWrapper() ) ||
		// ..also force inline in wrappers that we can't close
		( converter.isInWrapper() && !converter.canCloseWrapper() ) ||
		// ..otherwise just look at the tag names
		Array.prototype.every.call( domElements, ( el ) => !ve.isBlockElement( el ) );
};

/**
 * Get a clone of the node's document data element.
 *
 * The attributes object will be deep-copied and the .internal.generated
 * property will be removed if present.
 *
 * @static
 * @param {Object} element
 * @param {ve.dm.HashValueStore} store Hash-value store used by element
 * @param {boolean} preserveGenerated Preserve internal.generated property of element
 * @param {boolean} resetAttributes Reset attributes for an empty clone, as defined in #static-resetForClone
 * @return {Object} Cloned element object
 */
ve.dm.Node.static.cloneElement = function ( element, store, preserveGenerated, resetAttributes ) {
	const clone = ve.copy( element );
	let modified = false;

	if ( !preserveGenerated ) {
		ve.deleteProp( clone, 'internal', 'generated' );
	}
	const originalDomElements = store.value( clone.originalDomElementsHash );
	// Generate a new about attribute to prevent about grouping of cloned nodes
	if ( originalDomElements ) {
		// TODO: The '#mwtNNN' is required by Parsoid. Make the name used here
		// more generic and specify the #mwt pattern in MW code.
		const about = '#mwt' + Math.floor( 1000000000 * Math.random() );
		const domElements = originalDomElements.map( ( el ) => {
			const elClone = el.cloneNode( true );
			// Check for hasAttribute as comments don't have them
			if ( elClone.hasAttribute && elClone.hasAttribute( 'about' ) ) {
				elClone.setAttribute( 'about', about );
				modified = true;
			}
			return elClone;
		} );
		if ( modified ) {
			clone.originalDomElementsHash = store.hash( domElements, domElements.map( ve.getNodeHtml ).join( '' ) );
		}
	}
	if ( resetAttributes ) {
		this.resetAttributesForClone( clone, store );
	}
	return clone;
};

/**
 * Reset attributes for a cloned element.
 *
 * This will be used when an element needs to have certain attributes cleared
 * when creating a clone, e.g. when splitting a content branch node by pressing
 * enter, some attributes are preserved (list style) but some are cleared
 * (check list item state).
 *
 * @static
 * @param {Object} clonedElement Cloned element, modified in place
 * @param {ve.dm.HashValueStore} store Hash-value store used by element
 */
ve.dm.Node.static.resetAttributesForClone = function () {};

/**
 * Get the text content of the node for a given data element.
 *
 * Used by ve.dm.LinearData#getText (except in maintainIndices mode) when the user
 * wants a plain text representation of the document data.
 *
 * The default implementation returns an empty string, but node classes can override this to
 * return a string based on the data element, e.g. to return the character represented by an
 * MWEntityNode.
 *
 * @static
 * @param {Object} element Node data element
 * @return {string} Text content of the node
 */
ve.dm.Node.static.getText = function () {
	return '';
};

/* Methods */

/**
 * @inheritdoc
 */
ve.dm.Node.prototype.getStore = function () {
	return this.doc && this.doc.store;
};

/**
 * @see #static-cloneElement
 * Implementations should override the static method, not this one
 *
 * @param {boolean} preserveGenerated Preserve internal.generated property of element
 * @param {boolean} resetAttributes Reset attributes for an empty clone, as defined in #static-resetForClone
 * @return {Object} Cloned element object
 */
ve.dm.Node.prototype.getClonedElement = function ( preserveGenerated, resetAttributes ) {
	const store = this.getStore();
	if ( !store ) {
		throw new Error( 'Node must be attached to the document to be cloned.' );
	}
	return this.constructor.static.cloneElement( this.element, store, preserveGenerated, resetAttributes );
};

// eslint-disable-next-line jsdoc/require-returns
/**
 * @see ve.Node
 */
ve.dm.Node.prototype.getChildNodeTypes = function () {
	return this.constructor.static.childNodeTypes;
};

// eslint-disable-next-line jsdoc/require-returns
/**
 * @see ve.Node
 */
ve.dm.Node.prototype.getParentNodeTypes = function () {
	return this.constructor.static.parentNodeTypes;
};

// eslint-disable-next-line jsdoc/require-returns
/**
 * @see ve.Node
 */
ve.dm.Node.prototype.getSuggestedParentNodeTypes = function () {
	return this.constructor.static.suggestedParentNodeTypes;
};

// eslint-disable-next-line jsdoc/require-returns
/**
 * @see ve.Node
 */
ve.dm.Node.prototype.canHaveChildren = function () {
	return ve.dm.nodeFactory.canNodeHaveChildren( this.type );
};

// eslint-disable-next-line jsdoc/require-returns
/**
 * @see ve.Node
 */
ve.dm.Node.prototype.canHaveChildrenNotContent = function () {
	return ve.dm.nodeFactory.canNodeHaveChildrenNotContent( this.type );
};

// eslint-disable-next-line jsdoc/require-returns
/**
 * @see ve.Node
 */
ve.dm.Node.prototype.isInternal = function () {
	return this.constructor.static.isInternal;
};

// eslint-disable-next-line jsdoc/require-returns
/**
 * @see ve.Node
 */
ve.dm.Node.prototype.isMetaData = function () {
	return this.constructor.static.isMetaData;
};

// eslint-disable-next-line jsdoc/require-returns
/**
 * @see ve.Node
 */
ve.dm.Node.prototype.isWrapped = function () {
	return this.constructor.static.isWrapped;
};

// eslint-disable-next-line jsdoc/require-returns
/**
 * @see ve.Node
 */
ve.dm.Node.prototype.isUnwrappable = function () {
	return this.isWrapped() && this.constructor.static.isUnwrappable;
};

// eslint-disable-next-line jsdoc/require-returns
/**
 * @see ve.Node
 */
ve.dm.Node.prototype.canContainContent = function () {
	return this.constructor.static.canContainContent;
};

// eslint-disable-next-line jsdoc/require-returns
/**
 * @see ve.Node
 */
ve.dm.Node.prototype.isContent = function () {
	return this.constructor.static.isContent;
};

// eslint-disable-next-line jsdoc/require-returns
/**
 * @see ve.Node
 */
ve.dm.Node.prototype.isFocusable = function () {
	return this.constructor.static.isFocusable;
};

// eslint-disable-next-line jsdoc/require-returns
/**
 * @see ve.Node
 */
ve.dm.Node.prototype.isAlignable = function () {
	return this.constructor.static.isAlignable;
};

// eslint-disable-next-line jsdoc/require-returns
/**
 * @see ve.Node
 */
ve.dm.Node.prototype.isCellable = function () {
	return this.constructor.static.isCellable;
};

// eslint-disable-next-line jsdoc/require-returns
/**
 * @see ve.Node
 */
ve.dm.Node.prototype.isCellEditable = function () {
	return this.constructor.static.isCellEditable;
};

// eslint-disable-next-line jsdoc/require-returns
/**
 * @see ve.Node
 */
ve.dm.Node.prototype.isDiffedAsList = function () {
	return this.constructor.static.isDiffedAsList;
};

// eslint-disable-next-line jsdoc/require-returns
/**
 * @see ve.Node
 */
ve.dm.Node.prototype.isDiffedAsLeaf = function () {
	return this.constructor.static.isDiffedAsLeaf;
};

// eslint-disable-next-line jsdoc/require-returns
/**
 * @see ve.Node
 */
ve.dm.Node.prototype.isDiffedAsDocument = function () {
	return this.getChildNodeTypes() === null;
};

/**
 * Check if the node can have a slug before it.
 *
 * @return {boolean} Whether the node can have a slug before it
 */
ve.dm.Node.prototype.canHaveSlugBefore = function () {
	return !this.canContainContent() && this.getParentNodeTypes() === null;
};

/**
 * Check if the node can have a slug after it.
 *
 * @method
 * @return {boolean} Whether the node can have a slug after it
 */
ve.dm.Node.prototype.canHaveSlugAfter = ve.dm.Node.prototype.canHaveSlugBefore;

/**
 * A string identifier used to suppress slugs
 *
 * If sequential nodes have the same non-null suppressSlugType, then
 * no slug is shown, e.g. two floated images can return 'float' to
 * suppress the slug between them.
 *
 * @return {string|null} Type
 */
ve.dm.Node.prototype.suppressSlugType = function () {
	return null;
};

// eslint-disable-next-line jsdoc/require-returns
/**
 * @see ve.Node
 */
ve.dm.Node.prototype.hasSignificantWhitespace = function () {
	return this.constructor.static.hasSignificantWhitespace;
};

// eslint-disable-next-line jsdoc/require-returns
/**
 * @see ve.Node
 */
ve.dm.Node.prototype.handlesOwnChildren = function () {
	return this.constructor.static.handlesOwnChildren;
};

// eslint-disable-next-line jsdoc/require-returns
/**
 * @see ve.Node
 */
ve.dm.Node.prototype.shouldIgnoreChildren = function () {
	return this.constructor.static.ignoreChildren;
};

/**
 * Check if the node can be the root of a branch exposed in a ve.ce.Surface
 *
 * @return {boolean} Node can be the root of a surfaced branch
 */
ve.dm.Node.prototype.isSurfaceable = function () {
	return this.hasChildren() && !this.canContainContent() && !this.isMetaData() && !this.getChildNodeTypes();
};

/**
 * Find the first ancestor with matching type and attribute values.
 *
 * @param {string} type Node type to match
 * @param {Object} [attributes] Node attributes to match
 * @return {ve.dm.Node|null} Ancestor with matching type and attribute values
 */
ve.dm.Node.prototype.findMatchingAncestor = function ( type, attributes ) {
	return this.traverseUpstream( ( node ) => !node.matches( type, attributes ) );
};

/**
 * Check if the node has an ancestor with matching type and attribute values.
 *
 * @param {string} type Node type to match
 * @param {Object} [attributes] Node attributes to match
 * @return {boolean} Node has an ancestor with matching type and attribute values
 */
ve.dm.Node.prototype.hasMatchingAncestor = function ( type, attributes ) {
	return !!this.findMatchingAncestor( type, attributes );
};

/**
 * Check if the node matches type and attribute values.
 *
 * @param {string} type Node type to match
 * @param {Object} [attributes] Node attributes to match
 * @return {boolean} Node matches type and attribute values
 */
ve.dm.Node.prototype.matches = function ( type, attributes ) {
	if ( this.getType() !== type ) {
		return false;
	}

	if ( attributes ) {
		return this.compareAttributes( attributes );
	}
	return true;
};

/**
 * Check if specific attributes match those in the node
 *
 * @param {Object} attributes Node attributes to match
 * @return {boolean} Attributes sepcified match those in the node
 */
ve.dm.Node.prototype.compareAttributes = function ( attributes ) {
	for ( const key in attributes ) {
		if ( this.getAttribute( key ) !== attributes[ key ] ) {
			return false;
		}
	}
	return true;
};

// eslint-disable-next-line jsdoc/require-returns
/**
 * @see ve.Node
 */
ve.dm.Node.prototype.getLength = function () {
	return this.length;
};

/**
 * Set the inner length of the node.
 *
 * This should only be called after a relevant change to the document data. Calling this method will
 * not change the document data.
 *
 * @param {number} length Length of content
 * @fires ve.dm.Node#lengthChange
 * @fires ve.dm.Node#update
 * @throws {Error} Invalid content length error if length is less than 0
 */
ve.dm.Node.prototype.setLength = function ( length ) {
	if ( length < 0 ) {
		throw new Error( 'Length cannot be negative' );
	}
	// Compute length adjustment from old length
	const diff = length - this.length;
	// Set new length
	this.length = length;
	// Adjust the parent's length
	if ( this.parent ) {
		this.parent.adjustLength( diff );
	}
	// Emit events
	this.emit( 'lengthChange', diff );
	this.emit( 'update' );
};

/**
 * Adjust the length.
 *
 * This should only be called after a relevant change to the document data. Calling this method will
 * not change the document data.
 *
 * @param {number} adjustment Amount to adjust length by
 * @throws {Error} Invalid adjustment error if resulting length is less than 0
 */
ve.dm.Node.prototype.adjustLength = function ( adjustment ) {
	this.setLength( this.length + adjustment );
};

// eslint-disable-next-line jsdoc/require-returns
/**
 * @see ve.Node
 */
ve.dm.Node.prototype.getOffset = function () {
	if ( !this.parent || !this.getDocument() ) {
		return 0;
	}

	return this.getDocument().getOrInsertCachedData( () => {
		// Find our index in the parent and add up lengths while we do so
		const siblings = this.parent.children;
		let offset = this.parent.getOffset() + ( this.parent === this.root ? 0 : 1 );
		let i, len;
		for ( i = 0, len = siblings.length; i < len; i++ ) {
			if ( siblings[ i ] === this ) {
				break;
			}
			offset += siblings[ i ].getOuterLength();
		}
		if ( i === len ) {
			throw new Error( 'Node not found in parent\'s children array' );
		}
		return offset;
	}, this, 'getOffset' );
};

/**
 * Check if the node can be merged with another.
 *
 * For two nodes to be mergeable, the two nodes must either be the same node or:
 *  - Are comparable according to #compareForMerging (by default, have the same type)
 *  - Have the same depth
 *  - Have similar ancestry (each node upstream must have the same type)
 *
 * @param {ve.dm.Node} node Node to consider merging with
 * @return {boolean} Nodes can be merged
 */
ve.dm.Node.prototype.canBeMergedWith = function ( node ) {
	let n1 = this,
		n2 = node;

	// Content node can be merged with node that can contain content, for instance: TextNode
	// and ParagraphNode. When this method is called for such case (one node is a content node and
	// the other one can contain content) make sure to start traversal from node that can contain
	// content (instead of content node itself).
	if ( n1.canContainContent() && n2.isContent() ) {
		n2 = n2.getParent();
	} else if ( n2.canContainContent() && n1.isContent() ) {
		n1 = n1.getParent();
	}
	// Move up from n1 and n2 simultaneously until we find a common ancestor
	while ( n1 !== n2 ) {
		if (
			// Check if we have reached a root (means there's no common ancestor or unequal depth)
			( n1 === null || n2 === null ) ||
			// Ensure that nodes are comparable for merging
			!n1.compareForMerging( n2 )
		) {
			return false;
		}
		// Move up
		n1 = n1.getParent();
		n2 = n2.getParent();
	}
	return true;
};

/**
 * @see #static-getText
 *
 * @return {string} Text content of the node
 */
ve.dm.Node.prototype.getText = function () {
	return this.constructor.static.getText( this.element );
};

/**
 * Compare with another node for merging (see #canBeMergedWidth)
 *
 * The default implementation just compares node types.
 *
 * @param {ve.dm.Node} otherNode Other node to compare with
 * @return {boolean} Nodes are comparable
 */
ve.dm.Node.prototype.compareForMerging = function ( otherNode ) {
	return this.getType() === otherNode.getType();
};
