/**
 * Pre-order depth-first DOM traversal helper.
 * @module
 */

'use strict';

var DOMDataUtils = require('./DOMDataUtils.js').DOMDataUtils;
var DOMUtils = require('./DOMUtils.js').DOMUtils;
var JSUtils = require('./jsutils.js').JSUtils;
var WTUtils = require('./WTUtils.js').WTUtils;

/**
 * Class for helping us traverse the DOM.
 *
 * This class currently does a pre-order depth-first traversal.
 * See {@link DOMPostOrder} for post-order traversal.
 *
 * @class
 */
function DOMTraverser() {
	this.handlers = [];
}

/**
 * DOM traversal handler.
 * @callback module:utils/DOMTraverser~traverserHandler
 * @param {Node} node
 * @param {MWParserEnvironment} env
 * @param {boolean} atTopLevel
 * @param {Object} tplInfo Template information.
 * @return {Node|null|false|true}
 *   Return false if you want to stop any further callbacks from being
 *   called on the node.  Return the new node if you need to replace it or
 *   change its siblings; traversal will continue with the new node.
 */

/**
 * Add a handler to the DOM traverser.
 *
 * @param {string} nodeName
 * @param {traverserHandler} action
 *   A callback, called on each node we traverse that matches nodeName.
 */
DOMTraverser.prototype.addHandler = function(nodeName, action) {
	this.handlers.push({ action, nodeName });
};

/**
 * @private
 */
DOMTraverser.prototype.callHandlers = function(node, env, atTopLevel, tplInfo) {
	var name = (node.nodeName || '').toLowerCase();

	for (const handler of this.handlers) {
		if (handler.nodeName === null || handler.nodeName === name) {
			var result = handler.action(node, env, atTopLevel, tplInfo);
			if (result !== true) {
				if (result === undefined) {
					env.log("error",
						'DOMPostProcessor.traverse: undefined return!',
						'Bug in', handler.action.toString(),
						' when handling ', node.outerHTML);
				}
				// abort processing for this node
				return result;
			}
		}
	}

	return true;
};

/**
 * Traverse the DOM and fire the handlers that are registered.
 *
 * Handlers can return
 * - the next node to process
 *   - aborts processing for current node, continues with returned node
 *   - can also be `null`, so returning `workNode.nextSibling` works even when
 *     workNode is a last child of its parent
 * - `true`
 *   - continue regular processing on current node.
 *
 * @param {Node} workNode
 * @param {MWParserEnvironment} env
 * @param {Object} options
 * @param {boolean} atTopLevel
 * @param {Object} tplInfo Template information.
 * @return {Node|null|true}
 */
DOMTraverser.prototype.traverse = function(workNode, env, options, atTopLevel, tplInfo) {
	while (workNode !== null) {
		if (DOMUtils.isElt(workNode)) {
			// Identify the first template/extension node.
			// You'd think the !tplInfo check isn't necessary since
			// we don't have nested transclusions, however, you can
			// get extensions in transclusions.
			if (!tplInfo && WTUtils.isFirstEncapsulationWrapperNode(workNode)
					// Ensure this isn't just a meta marker, since we might
					// not be traversing after encapsulation.  Note that the
					// valid data-mw assertion is the same test as used in
					// cleanup.
					&& (!WTUtils.isTplMarkerMeta(workNode) || DOMDataUtils.validDataMw(workNode))
					// Encapsulation info on sections should not be used to
					// traverse with since it's designed to be dropped and
					// may have expanded ranges.
					&& !WTUtils.isParsoidSectionTag(workNode)) {
				var about = workNode.getAttribute("about") || '';
				tplInfo = {
					first: workNode,
					last: JSUtils.lastItem(WTUtils.getAboutSiblings(workNode, about)),
					clear: false,
				};
			}
		}

		// Call the handlers on this workNode
		var possibleNext = this.callHandlers(workNode, env, atTopLevel, tplInfo);

		// We may have walked passed the last about sibling or want to
		// ignore the template info in future processing.
		if (tplInfo && tplInfo.clear) {
			tplInfo = null;
		}

		if (possibleNext === true) {
			// the 'continue processing' case
			if (DOMUtils.isElt(workNode) && workNode.hasChildNodes()) {
				this.traverse(workNode.firstChild, env, options, atTopLevel, tplInfo);
			}
			possibleNext = workNode.nextSibling;
		}

		// Clear the template info after reaching the last about sibling.
		if (tplInfo && tplInfo.last === workNode) {
			tplInfo = null;
		}

		workNode = possibleNext;
	}
};

if (typeof module === "object") {
	module.exports.DOMTraverser = DOMTraverser;
}