/**
 * @license Copyright (c) 2017-2025, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md.
 */

'use strict';

/**
 * Symbols used to control tree walking behavior in leave callbacks
 */
const TREE_WALK_SYMBOLS = {
	REMOVE: Symbol( 'REMOVE' ),
	REPLACE: Symbol( 'REPLACE' ),
	KEEP: Symbol( 'KEEP' ),
	ABORT: Symbol( 'ABORT' )
};

/**
 * Walks through a tree structure, calling enter and leave callbacks for each node.
 * The leave callback can return an object with a symbol to control the node's fate:
 * - { symbol: TREE_WALK_SYMBOLS.REMOVE } - removes the node
 * - { symbol: TREE_WALK_SYMBOLS.REPLACE, value: newNode } - replaces the node
 * - { symbol: TREE_WALK_SYMBOLS.KEEP } or undefined - keeps the node unchanged
 *
 * @param tree - The tree or node to walk through
 * @param options - Walking options
 * @param [options.enter] - Callback called when entering a node (node, depth, parents)
 * @param [options.leave] - Callback called when leaving a node (node, depth, parents)
 * @param [depth=0] - Current depth (used internally for recursion)
 * @param [parents=[]] - Array of parent nodes (used internally for recursion)
 * @returns The potentially modified tree
 */
function walkXMLTree( tree, options = {}, depth = 0, parents = [] ) {
	const { enter, leave } = options;

	// Handle array of nodes
	if ( Array.isArray( tree ) ) {
		const result = [];

		for ( let i = 0; i < tree.length; i++ ) {
			const processedNode = walkXMLTree( tree[ i ], options, depth, parents );

			if ( Array.isArray( processedNode ) ) {
				result.push( ...processedNode );
			} else if ( processedNode !== undefined ) {
				result.push( processedNode );
			}
		}

		return result;
	}

	if ( enter ) {
		const enterResult = enter( tree, depth, parents );
		const processedResult = processCallbackResult( enterResult, tree );

		if ( processedResult === Symbol.for( 'ABORT_WALK' ) ) {
			return;
		}

		if ( processedResult !== tree ) {
			return processedResult;
		}
	}

	// Create a working copy of the node to potentially modify
	let workingNode = tree;

	// Recursively walk children if they exist
	if ( tree.children && Array.isArray( tree.children ) ) {
		const modifiedChildren = walkXMLTree( tree.children, options, depth + 1, [ ...parents, tree ] );

		// Only create a new node if children were actually modified
		if ( modifiedChildren !== tree.children ) {
			workingNode = { ...tree, children: modifiedChildren };
		}
	}

	// Call leave callback
	if ( leave ) {
		const leaveResult = leave( workingNode, depth, parents );

		return processCallbackResult( leaveResult, workingNode );
	}

	return workingNode;
};

/**
 * Processes a callback result and returns the appropriate action.
 *
 * @param result - The result from enter/leave callback
 * @param defaultValue - Value to return if no action is taken
 * @returns The processed result
 */
function processCallbackResult( result, defaultValue ) {
	if ( result && typeof result === 'object' && result.symbol ) {
		switch ( result.symbol ) {
			case TREE_WALK_SYMBOLS.KEEP:
				return defaultValue;

			case TREE_WALK_SYMBOLS.REMOVE:
				return undefined;

			case TREE_WALK_SYMBOLS.REPLACE:
				return result.value;

			case TREE_WALK_SYMBOLS.ABORT:
				return Symbol.for( 'ABORT_WALK' );
		}
	}

	return defaultValue;
}

/**
 * Helper functions for tree walking control
 */
const TreeWalkActions = {
	/**
	 * Creates an action to remove the current node.
	 *
	 * @returns Control object for node removal
	 */
	remove() {
		return { symbol: TREE_WALK_SYMBOLS.REMOVE };
	},

	/**
	 * Creates an action to replace the current node with a new value.
	 *
	 * @param value - The new value to replace the node with
	 * @returns Control object for node replacement
	 */
	replace( value ) {
		return { symbol: TREE_WALK_SYMBOLS.REPLACE, value };
	},

	/**
	 * Creates an action to keep the current node unchanged.
	 *
	 * @returns Control object for keeping the node
	 */
	keep() {
		return { symbol: TREE_WALK_SYMBOLS.KEEP };
	}
};

exports.walkXMLTree = walkXMLTree;
exports.TreeWalkActions = TreeWalkActions;
