Source: builder/node.js

const Utils = require("@openeo/js-commons/src/utils");
const Parameter = require("./parameter");

/**
 * A class that represents a process node and also a result from a process.
 */
class BuilderNode {

	/**
	 * Creates a new process node for the builder.
	 * 
	 * @param {Builder} parent
	 * @param {string} processId 
	 * @param {object.<string, *>} [processArgs={}]
	 * @param {?string} [processDescription=null]
	 * @param {?string} [processNamespace=null]
	 */
	constructor(parent, processId, processArgs = {}, processDescription = null, processNamespace = null) {
		/**
		 * The parent builder.
		 * @type {Builder}
		 */
		this.parent = parent;

		/**
		 * The specification of the process associated with this node.
		 * @type {Process}
		 * @readonly
		 */
		this.spec = this.parent.spec(processId, processNamespace);
		if (!this.spec) {
			throw new Error("Process doesn't exist: " + processId);
		}

		/**
		 * The unique identifier for the node (not the process ID!).
		 * @type {string}
		 */
		this.id = parent.generateId(processId);
		/**
		 * The namespace of the process - EXPERIMENTAL!
		 * @type {string}
		 */
		this.namespace = processNamespace;
		/**
		 * The arguments for the process.
		 * @type {object.<string, *>}
		 */
		this.arguments = Array.isArray(processArgs) ? this.namedArguments(processArgs) : processArgs;
		/**
		 * @ignore
		 */
		this._description = processDescription;
		/**
		 * Is this the result node?
		 * @type {boolean}
		 */
		this.result = false;

		this.addParametersToProcess(this.arguments);
	}

	/**
	 * Converts a sorted array of arguments to an object with the respective parameter names.
	 * 
	 * @param {Array} processArgs 
	 * @returns {object.<string, *>}
	 * @throws {Error}
	 */
	namedArguments(processArgs) {
		if (processArgs.length > (this.spec.parameters || []).length) {
			throw new Error("More arguments specified than parameters available.");
		}
		let obj = {};
		if (Array.isArray(this.spec.parameters)) {
			for(let i = 0; i < this.spec.parameters.length; i++) {
				obj[this.spec.parameters[i].name] = processArgs[i];
			}
		}
		return obj;
	}

	/**
	 * Checks the arguments given for parameters and add them to the process.
	 * 
	 * @param {object.<string, *>|Array} processArgs 
	 */
	addParametersToProcess(processArgs) {
		for(let key in processArgs) {
			let arg = processArgs[key];
			if (arg instanceof Parameter) {
				if (Utils.isObject(arg.spec.schema)) {
					this.parent.addParameter(arg.spec);
				}
			}
			else if (arg instanceof BuilderNode) {
				this.addParametersToProcess(arg.arguments);
			}
			else if (Array.isArray(arg) || Utils.isObject(arg)) {
				this.addParametersToProcess(arg);
			}
		}
	}

	/**
	 * Gets/Sets a description for the node.
	 * 
	 * Can be used in a variety of ways:
	 * 
	 * By default, this is a function: 
	 * `node.description()` - Returns the description.
	 * `node.description("foo")` - Sets the description to "foo". Returns the node itself for method chaining.
	 * 
	 * You can also "replace" the function (not supported in TypeScript!),
	 * then it acts as normal property and the function is not available any longer:
	 * `node.description = "foo"` - Sets the description to "foo".
	 * Afterwards you can call `node.description` as normal object property.
	 * 
	 * @param {string|undefined} description - Optional: If given, set the value.
	 * @returns {string|BuilderNode}
	 */
	description(description) {
		if (typeof description === 'undefined') {
			return this._description;
		}
		else {
			this._description = description;
			return this;
		}
	}

	/**
	 * Converts the given argument into something serializable...
	 * 
	 * @protected
	 * @param {*} arg - Argument
	 * @param {string} name - Parameter name
	 * @returns {*}
	 */
	exportArgument(arg, name) {
		const Formula = require('./formula');
		if (Utils.isObject(arg)) {
			if (arg instanceof BuilderNode || arg instanceof Parameter) {
				return arg.ref();
			}
			else if (arg instanceof Formula) {
				let builder = this.createBuilder(this, name);
				arg.setBuilder(builder);
				arg.generate();
				return builder.toJSON();
			}
			else if (arg instanceof Date) {
				return arg.toISOString();
			}
			else if (typeof arg.toJSON === 'function') {
				return arg.toJSON();
			}
			else {
				let obj = {};
				for(let key in arg) {
					if (typeof arg[key] !== 'undefined') {
						obj[key] = this.exportArgument(arg[key], name);
					}
				}
				return obj;
			}
		}
		else if (Array.isArray(arg)) {
			return arg.map(element => this.exportArgument(element), name);
		}
		// export child process graph
		else if (typeof arg === 'function') {
			return this.exportCallback(arg, name);
		}
		else {
			return arg;
		}
	}

	/**
	 * Creates a new Builder, usually for a callback.
	 * 
	 * @protected
	 * @param {?BuilderNode} [parentNode=null]
	 * @param {?string} [parentParameter=null]
	 * @returns {BuilderNode}
	 */
	createBuilder(parentNode = null, parentParameter = null) {
		const Builder = require('./builder');
		let builder = new Builder(this.parent.processes, this.parent);
		if (parentNode !== null && parentParameter !== null) {
			builder.setParent(parentNode, parentParameter);
		}
		return builder;
	}

	/**
	 * Returns the serializable process for the callback function given.
	 * 
	 * @protected
	 * @param {Function} arg - callback function
	 * @param {string} name - Parameter name
	 * @returns {object.<string, *>}
	 * @throws {Error}
	 */
	exportCallback(arg, name) {
		let builder = this.createBuilder(this, name);
		let params = builder.getParentCallbackParameters();
		// Bind builder to this, so that this.xxx can be used for processes
		// Also pass builder as last parameter so that we can grab it in arrow functions
		let node = arg.bind(builder)(...params, builder);
		if (Array.isArray(node) && builder.supports('array_create')) {
			node = builder.array_create(node);
		}
		else if (!Utils.isObject(node) && builder.supports('constant')) {
			node = builder.constant(node);
		}
		if (node instanceof BuilderNode) {
			node.result = true;
			return builder.toJSON();
		}
		else {
			throw new Error("Callback must return BuilderNode");
		}
	}

	/**
	 * Returns a JSON serializable representation of the data that is API compliant.
	 * 
	 * @returns {object.<string, *>}
	 */
	toJSON() {
		let obj = {
			process_id: this.spec.id,
			arguments: {}
		};
		if (this.namespace) {
			obj.namespace = this.namespace;
		}
		for(let name in this.arguments) {
			if (typeof this.arguments[name] !== 'undefined') {
				obj.arguments[name] = this.exportArgument(this.arguments[name], name);
			}
		}
		if (typeof this.description !== 'function') {
			obj.description = this.description;
		}
		else if (typeof this._description === 'string') {
			obj.description = this._description;
		}
		if (this.result) {
			obj.result = true;
		}
		return obj;
	}

	/**
	 * Returns the reference object for this node.
	 * 
	 * @returns {FromNode}
	 */
	ref() {
		return { from_node: this.id };
	}

}

module.exports = BuilderNode;