/*
*  Abstract Syntax Tree implementation and related
*  error object definition for LibreSignage.
*/

var util = require('util');

/*
*  ASTError code definitions.
*/
module.exports.ECODES = {
	ERN: { // Can't close the root node.
		code: 0,
		proto: ['got']
	},
	EUN: { // Attempted to close and unexpected node.
		code: 1,
		proto: ['got', 'expected']
	}
}

module.exports.ASTError = class ASTError extends Error {
	/*
	*  Abstract Syntax Tree error class. 'code' is one
	*  of the error code names in the ECODES object and
	*  data is an object with error code specific data.
	*  The allowed data keys are defined in the 'proto'
	*  array of each ECODES definition.
	*/
	constructor(err, data, message, file, line) {
		super(message, file, line);
		if (Error.captureStackTrace) {
			Error.captureStackTrace(this, ASTError);
		}
		if (!this.check_data(data, err)) {
			throw new Error(
				`Invalid data object for AST error '${err}'.`
			);
		}
		this.err = err;
		this.data = data;
	}

	check_data(data, err) {
		/*
		*  Check 'data' against the 'proto' array corresponding
		*  to the error name 'err'. Returns true if all the keys
		*  defined in 'proto' are found in 'data'.
		*/
		let proto = module.exports.ECODES[err].proto;
		if (Object.keys(data).length != proto.length) {
			return false;
		}
		for (let k of proto) {
			if (!(k in data)) {
				return false;
			}
		}
		return true;
	}
}

module.exports.AbstractSyntaxTree = class AbstractSyntaxTree {
	constructor() {
		this.nodes = {
			'name': '__root__',
			'nodes': [],
			'data': null,
			'parent': null,
			'lexeme': null
		};
		this.cnode = this.nodes;
	}

	open_node(name, lexeme) {
		/*
		*  Open a new node.
		*/
		let tmp = {
			'name': name,
			'nodes': [],
			'data': null,
			'parent': this.cnode,
			'lexeme': lexeme
		};
		this.cnode.nodes.push(tmp);
		this.cnode = tmp;
	}

	close_node(name) {
		/*
		*  Close the current node.
		*/
		if (!this.cnode.parent) {
			throw new module.exports.ASTError(
				'ERN',
				{'got': name}
			);
		}
		if (this.cnode.name != name) {
			throw new module.exports.ASTError(
				'EUN',
				{
					'got': name,
					'expected': this.cnode.name
				}
			);
		}
		this.cnode = this.cnode.parent;
	}

	set_data(data) {
		/*
		*  Set the freeform data object of the current node.
		*/
		this.cnode.data = data;
	}

	dump() {
		console.log(util.inspect(this.nodes, {'depth': null}));
	}
}
