Source: node.js

const ProcessGraphError = require('./error');
const Utils = require('./utils');

/**
 * A Process graph node.
 * 
 * @class
 */
class ProcessGraphNode {

	constructor(node, id, parent = null) {
		if (typeof id !== 'string' || id.length === 0) {
			throw new ProcessGraphError('NodeIdInvalid');
		}
		if (!Utils.isObject(node)) {
			throw new ProcessGraphError('NodeInvalid', {node_id: id});
		}
		if (typeof node.process_id !== 'string') {
			throw new ProcessGraphError('ProcessIdMissing', {node_id: id});
		}

		this.id = id;
		this.processGraph = parent;
		this.source = node;
		this.process_id = node.process_id;
		this.arguments = Utils.isObject(node.arguments) ? Utils.deepClone(node.arguments) : {};
		this.description = node.description || null;
		this.isResultNode = node.result || false;
		this.expectsFrom = []; // From which node do we expect results from
		this.receivedFrom = []; // From which node have received results from so far
		this.passesTo = [];
		this.computedResult = undefined;
	}

	toJSON() {
		let args = Utils.mapObjectValues(this.arguments, arg => Utils.isObject(arg) && typeof arg.toJSON === 'function' ? arg.toJSON() : arg);
		return Object.assign({}, this.source, {
			process_id: this.process_id,
			description: this.description,
			arguments: args,
			result: this.isResultNode
		});
	}

	getProcessGraph() {
		return this.processGraph;
	}

	getParent() {
		if (this.processGraph !== null) {
			return this.processGraph.getParentNode();
		}
		return null;
	}

	getArgumentNames() {
		return Object.keys(this.arguments);
	}

	hasArgument(name) {
		return typeof this.arguments[name] !== 'undefined';
	}

	getArgumentType(name) {
		return Utils.getType(this.getRawArgument(name));
	}

	getRawArgument(name) {
		return Utils.isObject(this.source.arguments) ? this.source.arguments[name] : undefined;
	}

	getParsedArgument(name) {
		return this.arguments[name];
	}

	getArgument(name, defaultValue = undefined) {
		if (typeof this.arguments[name] === 'undefined') {
			return defaultValue;
		}
		return this.evaluateArgument(this.arguments[name]);
	}

	getArgumentRefs(name) {
		return Utils.getRefs(this.getRawArgument(name), false);
	}

	getRefs() {
		return Utils.getRefs(this.source.arguments, false);
	}

	getProcessGraphParameterValue(name) {
		// 1. Check local parameter, then check parents
		// 2. Check parents
		// 3. Try to get default value
		// 4. Fail if no value is available
		let defaultValue;
		let pg = this.processGraph;
		do {
			if (pg.hasArgument(name)) {
				return pg.getArgument(name);
			}
			if (pg.hasParameterDefault(name)) {
				defaultValue = pg.getParameterDefault(name);
			}
			pg = pg.getParent();
		} while (pg !== null);

		if (typeof defaultValue !== 'undefined') {
			return defaultValue;
		}

		if (!this.processGraph.allowUndefinedParameterRefs) {
			throw new ProcessGraphError('ProcessGraphParameterMissing', {
				argument: name,
				node_id: this.id,
				process_id: this.process_id
			});
		}
	}

	evaluateArgument(arg) {
		var type = Utils.getType(arg);
		switch(type) {
			case 'result':
				return this.processGraph.getNode(arg.from_node).getResult();
			case 'callback':
				return arg;
			case 'parameter':
				return this.getProcessGraphParameterValue(arg.from_parameter);
			case 'array':
			case 'object':
				let copy = type === 'array' ? [] : {};
				for(var i in arg) {
					copy[i] = this.evaluateArgument(arg[i]);
				}
				return copy;
			default:
				return arg;
		}
	}

	isStartNode() {
		return (this.expectsFrom.length === 0);
	}

	addPreviousNode(node) {
		if (!this.expectsFrom.find(other => other.id === node.id)) {
			this.expectsFrom.push(node);
		}
	}

	getPreviousNodes() {
		// Sort nodes to ensure a consistent execution order
		return this.expectsFrom.sort((a,b) => a.id.localeCompare(b.id));
	}

	addNextNode(node) {
		if (!this.passesTo.find(other => other.id === node.id)) {
			this.passesTo.push(node);
		}
	}

	getNextNodes() {
		// Sort nodes to ensure a consistent execution order
		return this.passesTo.sort((a,b) => a.id.localeCompare(b.id));
	}

	reset() {
		this.computedResult = undefined;
		this.receivedFrom = [];
	}

	getDescription() {
		return this.description;
	}

	setDescription(description) {
		if (typeof description === 'string') {
			this.description = description;
		}
		else {
			this.description = null;
		}
	}

	setResult(result) {
		this.computedResult = result;
	}

	getResult() {
		return this.computedResult;
	}

	solveDependency(dependencyNode) {
		if (dependencyNode !== null && this.expectsFrom.includes(dependencyNode)) {
			this.receivedFrom.push(dependencyNode);
		}
		return (this.expectsFrom.length === this.receivedFrom.length); // all dependencies solved?
	}

}

module.exports = ProcessGraphNode;