/**
 * https://tools.ietf.org/html/rfc5228#section-8
 */

import { getMatchTypes } from 'Sieve/Utils';

import {
	BRACKET_COMMENT,
	HASH_COMMENT,
	IDENTIFIER,
	MULTI_LINE,
	NUMBER,
	QUOTED_STRING,
	STRING_LIST,
	TAG
} from 'Sieve/RegEx';

import {
	GrammarBracketComment,
	GrammarCommand,
	GrammarHashComment,
	GrammarMultiLine,
	GrammarNumber,
	GrammarQuotedString,
	GrammarStringList,
	TestCommand,
	GrammarTestList
} from 'Sieve/Grammar';

import { availableCommands } from 'Sieve/Commands';
import { ConditionalCommand, RequireCommand } from 'Sieve/Commands/Controls';
import { NotTest } from 'Sieve/Commands/Tests';

const
	T_UNKNOWN           = 0,
	T_STRING_LIST       = 1,
	T_QUOTED_STRING     = 2,
	T_MULTILINE_STRING  = 3,
	T_HASH_COMMENT      = 4,
	T_BRACKET_COMMENT   = 5,
	T_BLOCK_START       = 6,
	T_BLOCK_END         = 7,
	T_LEFT_PARENTHESIS  = 8,
	T_RIGHT_PARENTHESIS = 9,
	T_COMMA             = 10,
	T_SEMICOLON         = 11,
	T_TAG               = 12,
	T_IDENTIFIER        = 13,
	T_NUMBER            = 14,
	T_WHITESPACE        = 15,

	TokensRegEx = '(' + [
		/* T_STRING_LIST       */ STRING_LIST,
		/* T_QUOTED_STRING     */ QUOTED_STRING,
		/* T_MULTILINE_STRING  */ MULTI_LINE,
		/* T_HASH_COMMENT      */ HASH_COMMENT,
		/* T_BRACKET_COMMENT   */ BRACKET_COMMENT,
		/* T_BLOCK_START       */ '\\{',
		/* T_BLOCK_END         */ '\\}',
		/* T_LEFT_PARENTHESIS  */ '\\(', // anyof / allof
		/* T_RIGHT_PARENTHESIS */ '\\)', // anyof / allof
		/* T_COMMA             */ ',',
		/* T_SEMICOLON         */ ';',
		/* T_TAG               */ TAG,
		/* T_IDENTIFIER        */ IDENTIFIER,
		/* T_NUMBER            */ NUMBER,
		/* T_WHITESPACE        */ '(?: |\\r\\n|\\t)+',
		/* T_UNKNOWN           */ '[^ \\r\\n\\t]+'
	].join(')|(') + ')',

	TokenError = [
		/* T_STRING_LIST       */ '',
		/* T_QUOTED_STRING     */ '',
		/* T_MULTILINE_STRING  */ '',
		/* T_HASH_COMMENT      */ '',
		/* T_BRACKET_COMMENT   */ '',
		/* T_BLOCK_START       */ 'Block start not part of control command',
		/* T_BLOCK_END         */ 'Block end has no matching block start',
		/* T_LEFT_PARENTHESIS  */ 'Test start not part of anyof/allof test',
		/* T_RIGHT_PARENTHESIS */ 'Test end not part of test-list',
		/* T_COMMA             */ 'Comma not part of test-list',
		/* T_SEMICOLON         */ 'Semicolon not at end of command',
		/* T_TAG               */ '',
		/* T_IDENTIFIER        */ '',
		/* T_NUMBER            */ '',
		/* T_WHITESPACE        */ '',
		/* T_UNKNOWN           */ ''
	];

export const parseScript = (script, name = 'script.sieve') => {
	script = script.replace(/\r?\n/g, '\r\n');

	// Only activate available commands
	const Commands = availableCommands();

	let match,
		line = 1,
		tree = [],

		// Create one regex to find the tokens
		// Use exec() to forward since lastIndex
		regex = RegExp(TokensRegEx, 'gm'),

		levels = [],
		command = null,
		requires = [],
		args = [];

	const
		error = message => {
//			throw new SyntaxError(message + ' at ' + regex.lastIndex + ' line ' + line, name, line)
			throw new SyntaxError(message + ' on line ' + line
				+ ' around:\n\n' + script.substr(regex.lastIndex - 20, 30), name, line)
		},
		pushArg = arg => {
			command || error('Argument not part of command');
			let prev_arg = args[args.length-1];
			if (getMatchTypes(0).includes(arg)) {
				command.match_type = arg;
			} else if (':value' === prev_arg || ':count' === prev_arg) {
				// Sieve relational [RFC5231] match types
				/^(gt|ge|lt|le|eq|ne)$/.test(arg.value) || error('Invalid relational match-type ' + arg);
				command.match_type = prev_arg + ' ' + arg;
				--args.length;
//				requires.push('relational');
			} else if (':comparator' === prev_arg) {
				command.comparator = arg;
				--args.length;
			} else {
				args.push(arg);
			}
		},
		pushArgs = () => {
			if (args.length) {
				command && command.pushArguments(args);
				args = [];
			}
		};

	levels.up = () => {
		levels.pop();
		return levels[levels.length - 1];
	};

	while ((match = regex.exec(script))) {
		// the last element in match will contain the matched value and the key will be the type
		let type = match.findIndex((v,i) => 0 < i && undefined !== v),
			value = match[type];

		// create the part
		switch (type)
		{
		case T_IDENTIFIER: {
			pushArgs();
			value = value.toLowerCase();
			let new_command;
			if ('if' === value) {
				new_command = new ConditionalCommand(value);
			} else if ('elsif' === value || 'else' === value) {
//				(prev_command instanceof ConditionalCommand) || error('Not after IF condition');
				new_command = new ConditionalCommand(value);
			} else if (Commands[value]) {
				if ('allof' === value || 'anyof' === value) {
//					(command instanceof ConditionalCommand || command instanceof NotTest) || error('Test-list not in conditional');
				}
				new_command = new Commands[value]();
			} else {
				if (command && (
				    command instanceof ConditionalCommand
				 || command instanceof NotTest
				 || command.tests instanceof GrammarTestList)) {
					console.error('Unknown test: ' + value);
					new_command = new TestCommand(value);
				} else {
					console.error('Unknown command: ' + value);
					new_command = new GrammarCommand(value);
				}
			}

			if (new_command instanceof TestCommand) {
				if (command instanceof ConditionalCommand || command instanceof NotTest) {
					// if/elsif/else new_command
					// not new_command
					command.test = new_command;
				} else if (command.tests instanceof GrammarTestList) {
					// allof/anyof .tests[] new_command
					command.tests.push(new_command);
				} else {
					error('Test "' + value + '" not allowed in "' + command.identifier + '" command');
				}
			} else if (command) {
				if (command.commands) {
					command.commands.push(new_command);
				} else {
					error('commands not allowed in "' + command.identifier + '" command');
				}
			} else {
				tree.push(new_command);
			}
			levels.push(new_command);
			command = new_command;
			if (command.require) {
				(Array.isArray(command.require) ? command.require : [command.require])
					.forEach(string => requires.push(string));
			}
			if (command.comparator) {
				requires.push('comparator-' + command.comparator);
			}
			break; }

		// Arguments
		case T_TAG:
			pushArg(value.toLowerCase());
			break;
		case T_STRING_LIST:
			pushArg(GrammarStringList.fromString(value));
			break;
		case T_MULTILINE_STRING:
			pushArg(GrammarMultiLine.fromString(value));
			break;
		case T_QUOTED_STRING:
			pushArg(new GrammarQuotedString(value.substr(1,value.length-2)));
			break;
		case T_NUMBER:
			pushArg(new GrammarNumber(value));
			break;

		// Comments
		case T_BRACKET_COMMENT:
		case T_HASH_COMMENT: {
			let obj = (T_HASH_COMMENT == type)
				? new GrammarHashComment(value.substr(1).trim())
				: new GrammarBracketComment(value.substr(2, value.length-4));
			if (command) {
				if (!command.comments) {
					command.comments = [];
				}
				(command.commands || command.comments).push(obj);
			} else {
				tree.push(obj);
			}
			break; }

		case T_WHITESPACE:
//			(command ? command.commands : tree).push(value.trim());
			command || tree.push(value.trim());
			break;

		// Command end
		case T_SEMICOLON:
			command || error(TokenError[type]);
			pushArgs();
			if (command instanceof RequireCommand) {
				command.capabilities.forEach(string => requires.push(string.value));
			}
			command = levels.up();
			break;

		// Command block
		case T_BLOCK_START:
			pushArgs();
			// https://tools.ietf.org/html/rfc5228#section-2.9
			// Action commands do not take tests or blocks
			while (command && !(command instanceof ConditionalCommand)) {
				command = levels.up();
			}
			command || error(TokenError[type]);
			break;
		case T_BLOCK_END:
			(command instanceof ConditionalCommand) || error(TokenError[type]);
//			prev_command = command;
			command = levels.up();
			break;

		// anyof / allof ( ... , ... )
		case T_LEFT_PARENTHESIS:
		case T_RIGHT_PARENTHESIS:
		case T_COMMA:
			pushArgs();
			// Must be inside PARENTHESIS aka test-list
			while (command && !(command.tests instanceof GrammarTestList)) {
				command = levels.up();
			}
			command || error(TokenError[type]);
			break;

		case T_UNKNOWN:
			error('Invalid token ' + value);
		}

		// Set current script position
		line += (value.split('\n').length - 1); // (value.match(/\n/g) || []).length;
	}

	tree.requires = requires;
	return tree;
};
