/*
*  Parser for the LibreSignage markup transpiler. This file contains
*  function and object implementations for parsing a token stream
*  generated by lexer.process() into an Abstract Syntax Tree (AST).
*/

var lexeme = require('./lexeme.js');
var lexer = require('./lexer.js');
var ldefs = require('./lexer_defs.js');

var err = require('./error.js');
var astree = require('./ast.js');

var Assert = require('assert');

var RULES = {
	'BRACK_OPEN': { // Match an opening or closing tag.
		match: false,
		opt: false,
		cnt: 1,
		exp: {
			'0': {
				match: true,
				opt: true,
				cnt: Infinity,
				exp: ['WHITESPACE']
			},
			'1': {
				match: true,
				opt: true,
				cnt: 1,
				exp: ['FORWARD_SLASH'],
				id: 'slash'
			},
			'2': {
				match: true,
				opt: false,
				cnt: 1,
				exp: ['TAGNAME'],
				id: 'tag'
			},
			'3': {
				match: true,
				opt: true,
				cnt: Infinity,
				exp: ['WHITESPACE']
			},
			'5': { // Match argument-value pairs.
				match: false,
				opt: true,
				cnt: Infinity,
				id: 'var',
				makearray: true,
				exp: {
					'1': {
						match: true,
						opt: false,
						cnt: 1,
						exp: ['IDENTIFIER'],
						id: 'name'
					},
					'2': {
						match: true,
						opt: true,
						cnt: Infinity,
						exp: ['WHITESPACE']
					},
					'3': {
						match: true,
						opt: false,
						cnt: 1,
						exp: ['ASSIGN']
					},
					'4': {
						match: true,
						opt: true,
						cnt: Infinity,
						exp: ['WHITESPACE']
					},
					'5': {
						match: true,
						opt: false,
						cnt: 1,
						exp: ['LITERAL', 'STR_LITERAL'],
						id: 'value'
					},
					'6': {
						match: true,
						opt: true,
						cnt: Infinity,
						exp: ['WHITESPACE']
					}
				}
			},
			'6': {
				match: true,
				opt: false,
				cnt: 1,
				exp: ['BRACK_CLOSE']
			}
		},
		valid: (ast, self, matched) => {
			let data = [];
			if ('slash' in matched) {
				if ('var' in matched) {
					throw new err.MarkupSyntaxError(
						'ESYN',
						matched['var'][0]['name'],
						'Parameters in a closing tag.'
					);
				}
				try {
					ast.close_node(matched.tag.raw);
				} catch (e) {
					// Convert ASTErrors to MarkupSyntaxErrors.
					if (e instanceof astree.ASTError) {
						switch (e.err) {
							case 'ERN':
								throw new err.MarkupSyntaxError(
									'ESYN',
									self,
									`Unexpected closing tag ` +
									`'${e.data.got}'.`
								);
							case 'EUN':
								throw new err.MarkupSyntaxError(
									'ESYN',
									self,
									`Unexpected closing tag ` +
									`'${e.data.got}'. Expected ` +
									`'${e.data.expected}'.`
								);
						}
					} else {
						throw e;
					}
				}
			} else {
				ast.open_node(matched.tag.raw, self);
			}
			if ('var' in matched) {
				for (let v of matched['var']) {
					data[v.name.raw] = v.value.raw;
				}
				ast.set_data(data);
			}
		}
	},
	'BRACK_CLOSE': {
		exp: {},
		valid: (ast, self, matched) => {
			throw new err.MarkupSyntaxError(
				'ETOK',
				self,
				self.raw
			);
		}
	},
	'LITERAL': { // Match an inner literal.
		match: false,
		opt: false,
		cnt: 1,
		exp: {
			0: {
				match: true,
				opt: true,
				cnt: Infinity,
				id: 'str',
				makearray: true,
				exp: ['WHITESPACE', 'LITERAL']
			}
		},
		valid: (ast, self, matched) => {
			let tmp = self.raw;
			if ('str' in matched) {
				for (let l of matched['str']) {
					tmp += l.raw;
				}
			}
			ast.open_node('__inner__', self);
			ast.set_data({'text': tmp});
			ast.close_node('__inner__');
		}
	}
}

/*
*  These always raise an ETOK exception since they shouldn't
*  be encountered out of context.
*/
RULES['TAGNAME'] = RULES['BRACK_CLOSE'];
RULES['IDENTIFIER'] = RULES['BRACK_CLOSE'];
RULES['STR_LITERAL'] = RULES['BRACK_CLOSE'];
RULES['FORWARD_SLASH'] = RULES['BRACK_CLOSE'];

/*
*  Handle WHITESPACE as LITERAL.
*/
RULES['WHITESPACE'] = RULES['LITERAL'];

function pmatch_rule_jrepr(rule, sep) {
	/*
	*  Join the 'repr' values of all tokens in 'rule.exp'
	*  into a string with 'sep' between each one.
	*/
	let ret = '';
	for (let t of rule.exp) {
		if (ret.length) {
			ret += sep + ldefs.tokens[t].repr;
		} else {
			ret += ldefs.tokens[t].repr;
		}
	}
	return ret;
}

function pmatch_type(rule, lexeme) {
	return rule.exp.includes(lexeme.type);
}

function pmatch_store_data(rule, data, dest) {
	/*
	*  Store matched rule data in 'dest'. If rule.makearray == true,
	*  an array is created in dest[rule.id] and the data is pushed
	*  into it.
	*/
	if (rule.id) {
		if (rule.makearray) {
			if (!dest[rule.id]) {
				dest[rule.id] = [];
			}
			dest[rule.id].push(data);
		} else {
			dest[rule.id] = data;
		}
	}
}

function pmatch_rule(rule, lexemes, at, matched) {
	/*
	*  Attempt to match the parser rule 'rule' at
	*  position 'at' in the lexemes array. If a rule
	*  matches, this function returns the next unmatched
	*  position in the lexemes array. The matched lexemes
	*  that have a defined ID in the matched rule are put
	*  into the 'matched' dictionary.
	*/
	let new_i = 0;
	let i = at;
	let m = {};

	for (let j in rule.exp) {
		if (!rule.exp[j].match) {
			// Process subrule.
			for (let k = 0; k < rule.exp[j].cnt; k++) {
				m = {};
				new_i = pmatch_rule(rule.exp[j], lexemes, i, m);
				if (new_i == i) {
					// Optional subrule didn't match -> continue to next.
					break;
				} else {
					i = new_i;
				}
				pmatch_store_data(rule.exp[j], m, matched);
			}
		} else {
			for (let k = 0; k < rule.exp[j].cnt; k++) {
				if (i > lexemes.length - 1) {
					// Token stream ended unexpectedly.
					if (rule.exp[j].opt) {
						/*
						*  Optional rule -> continue to the next one.
						*  This makes sure an EEOL exception isn't
						*  thrown when the current token is an
						*  optional one, which would cause silly
						*  errors like "Unexpected End Of File.
						*  Expected '(whitespace)'."
						*/
						break;
					} else if (rule.opt) {
						/*
						*  Optional parent rule -> return without
						*  changes to the parser state.
						*/
						return at;
					} else {
						throw new err.MarkupSyntaxError(
							'EEOL',
							lexemes[lexemes.length - 1],
							`Expected ` +
							`'${pmatch_rule_jrepr(rule.exp[j])}'`
						);
					}
				}
				if (pmatch_type(rule.exp[j], lexemes[i])) {
					pmatch_store_data(rule.exp[j], lexemes[i], matched);
					i++;
				} else if (
					rule.exp[j].opt
					|| rule.exp[j].cnt === Infinity
				) {
					/*
					*  Optional token or a token without a
					*  specific count didn't match -> continue
					*  to the next one.
					*/
					break;
				} else if (rule.opt && !rule.exp[j].opt) {
					/*
					*  The parent subrule is optional and the
					*  unmatched rule isn't -> Return without
					*  changes to the parser state.
					*/
					return at;
				} else {
					/*
					*  Required token doesn't match -> throw
					*  an error.
					*/
					throw new err.MarkupSyntaxError(
						'ETOK',
						lexemes[i],
						`'${lexemes[i].raw}'. Expected ` +
						`'${pmatch_rule_jrepr(rule.exp[j])}'`
					);
				}
			}
		}
	}
	return i;
}

module.exports.parse = function(str) {
	/*
	*  Parse the input string 'str' into an
	*  Abstract Syntax Tree and return the AST
	*  object.
	*/
	let lexemes = lexer.process(str);
	let self = null;
	let rule = null;
	let matched = {};
	let ast = new astree.AbstractSyntaxTree();

	let i = 0;
	while (i < lexemes.length) {
		Assert.ok(
			lexemes[i].type in RULES,
			`No parser rule defined for '${lexemes[i].type}'.`
		);

		self = lexemes[i];
		rule = RULES[lexemes[i].type];
		i++;

		i = pmatch_rule(rule, lexemes, i, matched);
		rule.valid(ast, self, matched);

		matched = {};
		self = null;
	}
	if (ast.cnode.name != '__root__') {
		throw new err.MarkupSyntaxError(
			'EEOL',
			lexemes[lexemes.length - 1],
			`Missing closing tag for '${ast.cnode.name}'.`
		);
	}
	return ast;
}
