/**
 * Copyright © Volker Schukai and all contributing authors, {{copyRightYear}}. All rights reserved.
 * Node module: @schukai/monster
 *
 * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
 * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
 *
 * For those who do not wish to adhere to the AGPLv3, a commercial license is available.
 * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
 * For more information about purchasing a commercial license, please contact Volker Schukai.
 *
 * SPDX-License-Identifier: AGPL-3.0
 */

import { Base } from "./base.mjs";
import { isPrimitive } from "./is.mjs";
import { NodeList } from "./nodelist.mjs";
import { validateInstance } from "./validate.mjs";
import { instanceSymbol } from "../constants.mjs";

export { Node };

/**
 * @private
 * @type {symbol}
 */
const internalValueSymbol = Symbol("internalData");

/**
 * @private
 * @type {symbol}
 */
const treeStructureSymbol = Symbol("treeStructure");

/**
 * You can create the instance via the monster namespace `new Monster.Types.Node()`.
 *
 * @license AGPLv3
 * @since 1.26.0
 * @copyright Volker Schukai
 * @summary A Node Class
 * @see https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Iteration_protocols
 */
class Node extends Base {
	/**
	 * @param {*} [value]
	 */
	constructor(value) {
		super();
		this[internalValueSymbol] = value;

		this[treeStructureSymbol] = {
			parent: null,
			childNodes: new NodeList(),
			level: 0,
		};
	}

	/**
	 * This method is called by the `instanceof` operator.
	 * @return {symbol}
	 * @since 2.1.0
	 */
	static get [instanceSymbol]() {
		return Symbol.for("@schukai/monster/types/node");
	}

	/**
	 * @property {*}
	 */
	get value() {
		return this[internalValueSymbol];
	}

	/**
	 * @property {*}
	 */
	set value(value) {
		this[internalValueSymbol] = value;
	}

	/**
	 * @property {Monster.Types.Node|null}
	 */
	get parent() {
		return this[treeStructureSymbol].parent;
	}

	/**
	 * @property {integer}
	 */
	get level() {
		return this[treeStructureSymbol].level;
	}

	/**
	 *
	 * @property {NodeList}
	 */
	get childNodes() {
		return this[treeStructureSymbol].childNodes;
	}

	/**
	 *
	 * @property {NodeList}
	 */
	set childNodes(childNodes) {
		this[treeStructureSymbol].childNodes = validateInstance(
			childNodes,
			NodeList,
		);
		setChildLevelAndParent.call(this, this, 1, new Set());
	}

	/**
	 * @return {Monster.Types.Node}
	 * @param {Node} node
	 */
	appendChild(node) {
		this[treeStructureSymbol].childNodes.add(validateInstance(node, Node));
		node[treeStructureSymbol].parent = this;

		node[treeStructureSymbol].level = this.level + 1;
		setChildLevelAndParent.call(this, node, 1, new Set());
		return this;
	}

	/**
	 * @return {Monster.Types.Node}
	 * @param {Node} node
	 */
	removeChild(node) {
		this[treeStructureSymbol].childNodes.remove(validateInstance(node, Node));
		node[treeStructureSymbol].parent = null;

		node[treeStructureSymbol].level = 0;
		setChildLevelAndParent.call(this, node, -1, new Set());
		return this;
	}

	/**
	 *
	 * @return {boolean}
	 */
	hasChildNodes() {
		return this[treeStructureSymbol].childNodes.length > 0;
	}

	/**
	 * @return {Monster.Types.Node}
	 * @param {Node} node
	 */
	hasChild(node) {
		return this[treeStructureSymbol].childNodes.has(
			validateInstance(node, Node),
		);
	}

	/**
	 * @since 1.28.0
	 * @return {string}
	 */
	toString() {
		const parts = [];
		if (this[internalValueSymbol]) {
			let label = this[internalValueSymbol];
			if (!isPrimitive(label))
				label = JSON.stringify(this[internalValueSymbol]);

			parts.push(label);
		}

		if (!this.hasChildNodes()) {
			return parts.join("\n");
		}

		const count = this.childNodes.length;
		let counter = 0;

		for (const node of this.childNodes) {
			counter++;
			const prefix = (count === counter ? "└" : "├").padStart(
				2 * node.level,
				" |",
			);
			parts.push(prefix + node.toString());
		}

		return parts.join("\n");
	}
}

/**
 * @private
 * @param {Node} node
 * @param {int} operand
 * @param {Set} visitedNodes
 * @return {setChildLevelAndParent}
 */
function setChildLevelAndParent(node, operand, visitedNodes) {
	const self = this;

	if (visitedNodes.has(node)) {
		throw new Error(
			"the node has already been visited and cannot be traversed again",
		);
	}

	visitedNodes.add(node);

	if (node !== this) {
		node[treeStructureSymbol].parent = this;
	}

	node[treeStructureSymbol].childNodes.forEach(function (child) {
		child[treeStructureSymbol].parent = node;
		child[treeStructureSymbol].level =
			node[treeStructureSymbol].level + operand;
		setChildLevelAndParent.call(self, child, operand, visitedNodes);
	});
	return this;
}
