/*
 * Copyright (C) 2016 Andre Noll <maan@tuebingen.mpg.de>
 *
 * Licensed under the GPL v3, see http://www.gnu.org/licenses/gpl-3.0.html
 */

%option stack
%option never-interactive
%option yylineno

%s SC_COMMAND
%s SC_OPTION
%s SC_ARG_INFO
%s SC_ARG_TYPE
%s SC_OPTION_FLAG
%s SC_CHAR
%s SC_INTEGER
%s SC_UNQUOTED_LINE
%s SC_UNQUOTED_LINE_CHECK_DEFAULT
%s SC_ERROR
%s SC_VALUES_ID
%s SC_VALUES_COMMA_OR_BRACE

%x SC_INTRODUCTION
%x SC_DESCRIPTION
%x SC_CLOSING
%x SC_HELP
%x SC_CONCLUSION
%x SC_SECTION
%{
	#include <ctype.h>
	#include <stdlib.h>
	#include <assert.h>
	#include <stdbool.h>

	#include "lsg.h"

	static char **charpp;
	static char *text_buf;
	static size_t text_buf_len;
	struct lsg_suite suite;

	#define CURCMD (suite.commands[suite.num_subcommands])
	#define CUROPT (CURCMD.options[CURCMD.num_options - 1])
	#define CURSECT (suite.sections[suite.num_sections - 1])

	static int yywrap(void) {return 1;}
	static void *xmalloc(size_t size)
	{
		void *p;

		assert(size > 0);
		p = malloc(size);
		assert(p);
		return p;
	}
	void *xrealloc(void *ptr, size_t size)
	{
		void *p;
		assert(size > 0);
		p = realloc(ptr, size);
		assert(p);
		return p;
	}

	/*
	 * Extract ID from a string like [foo ID] or bar = ID, or a substring
	 * of a list: ID1, ID2, ...
	 */
	static char *parse_identifier(char *p)
	{
		char *p2, *eq;

		if (*p == '[') {
			while (*p && isspace(*p))
				p++;
			while (*p && !isspace(*p))
				p++;
		} else {
			if ((eq = strchr(p + 1, '=')))
				p = eq + 1;
			else
				while (*p && (isspace(*p) || *p == ','))
					p++;
		}
		assert(*p);
		while (*p && isspace(*p))
			p++;
		assert(*p);
		p2 = p;
		while (isalnum(*p2) || *p2 == '_' || *p2 == '-')
			p2++;
		*p2 = '\0';
		return strdup(p);
	}

	static char *parse_simple_string(void)
	{
		char *result, *p = yytext;
		while (isspace(*p))
			p++;
		assert(*p == '[');
		p++;
		while (isspace(*p))
			p++;
		while (!isspace(*p)) /* skip "section" */
			p++;
		while (isspace(*p))
			p++;
		result = strdup(p);
		p = strrchr(result, ']');
		do {
			*p = '\0';
			p--;
		} while (isspace(*p));
		return result;
	}
	static void parse_id_string(void)
	{
		char *p, *q;
		int val_num = CUROPT.num_values++, num_vals = val_num + 1;
		bool backslash;

		CUROPT.values = xrealloc(CUROPT.values,
			num_vals * sizeof(char *));
		CUROPT.value_ids = xrealloc(CUROPT.value_ids,
			num_vals * sizeof(char *));
		CUROPT.value_literals = xrealloc(CUROPT.value_literals,
			num_vals * sizeof(char *));
		CUROPT.value_ids[val_num] = p = strdup(yytext);
		while (*p && !isspace(*p) && *p != '=')
			p++;
		*p = '\0';
		p++;
		while (*p && *p != '\"')
			p++;
		CUROPT.value_literals[val_num] = p;
		p++;
		CUROPT.values[val_num] = q = xmalloc(strlen(p) + 1);
		for (backslash = false; *p; p++) {
			if (*p == '\\') {
				if (!backslash) {
					backslash = true;
					continue;
				}
			} else if (*p == '"') {
				if (!backslash) {
					*q = '\0';
					return;
				}
			}
			*q++ = *p;
			backslash = false;
		}
		assert(false);
	}
	static char *parse_unquoted_line(void)
	{
		char *p = strdup(yytext);
		size_t n = strlen(p);
		for (; n > 0; n--) {
			if (isspace(p[n - 1]))
				continue;
			p[n] = '\0';
			break;
		}
		return p;
	}
	static void check_default_val(void)
	{
		int i;

		if (!CUROPT.default_val)
			return;
		if (!CUROPT.values)
			return;
		for (i = 0; i < CUROPT.num_values; i++) {
			char *val = CUROPT.values[i];
			char buf[40];
			if (strcmp(val, CUROPT.default_val))
				continue;
			sprintf(buf, "%i", i);
			free(CUROPT.default_val);
			CUROPT.default_val = strdup(buf);
			return;
		}
		fprintf(stderr, "option %s: bad default value %s\n",
			CUROPT.name.orig, CUROPT.default_val);
		exit(EXIT_FAILURE);
	}

%}
EQUALS [[:space:]]*=[[:space:]]*
IDENTIFIER [a-zA-Z]+[a-zA-Z0-9_-]*
C99_DECIMAL_CONSTANT -?([[:digit:]]{-}[0])[[:digit:]]*
C99_HEXADECIMAL_CONSTANT 0[xX][[:xdigit:]]+
C99_OCTAL_CONSTANT 0[01234567]*
INT_CONSTANT {C99_DECIMAL_CONSTANT}|{C99_HEXADECIMAL_CONSTANT}|{C99_OCTAL_CONSTANT}
STRING_VALUE \"([^\"\\\n]|(\\[\"\\]))*\"
SIMPLE_STRING [[:alnum:]]([[:alnum:]]|[[:space:]])*
%%

 /* skip comments and whitespace */
^[[:space:]]*#.*\n ;
[[:space:]]|\n+ ;

<INITIAL>\[[[:space:]]*suite[[:space:]]+{IDENTIFIER}[[:space:]]*\] {
	free(suite.name.orig);
	suite.name.orig = parse_identifier(yytext);
}

<INITIAL>caption{EQUALS} {
	charpp = &suite.caption;
	yy_push_state(SC_UNQUOTED_LINE);
}

<INITIAL>title{EQUALS} {
	charpp = &suite.title;
	yy_push_state(SC_UNQUOTED_LINE);
}

<INITIAL>mansect{EQUALS} {
	charpp = &suite.mansect;
	yy_push_state(SC_UNQUOTED_LINE);
}

<INITIAL>date{EQUALS} {
	charpp = &suite.date;
	yy_push_state(SC_UNQUOTED_LINE);
}

<INITIAL>version-string{EQUALS} {
	charpp = &suite.version_string;
	yy_push_state(SC_UNQUOTED_LINE);
}

<INITIAL>manual_title{EQUALS} {
	charpp = &suite.manual_title;
	yy_push_state(SC_UNQUOTED_LINE);
}

<INITIAL>aux_info_prefix{EQUALS} {
	charpp = &suite.aux_info_prefix;
	yy_push_state(SC_UNQUOTED_LINE);
}

<INITIAL>aux_info_default{EQUALS} {
	charpp = &suite.aux_info_default;
	yy_push_state(SC_UNQUOTED_LINE);
}

<INITIAL,SC_COMMAND,SC_OPTION>\[[[:space:]]*introduction[[:space:]]*\] {
	text_buf = NULL;
	text_buf_len = 0;
	yy_push_state(SC_INTRODUCTION);
}

<SC_INTRODUCTION>[[:space:]]*\[[[:space:]]*\/introduction[[:space:]]*\]\n {
	suite.introduction = text_buf;
	yy_pop_state();
}

<INITIAL,SC_COMMAND,SC_OPTION>\[[[:space:]]*conclusion[[:space:]]*\] {
	text_buf = NULL;
	text_buf_len = 0;
	yy_push_state(SC_CONCLUSION);
}

<SC_CONCLUSION>[[:space:]]*\[[[:space:]]*\/conclusion[[:space:]]*\]\n {
	suite.conclusion = text_buf;
	yy_pop_state();
}

<INITIAL>\[[[:space:]]*supercommand[[:space:]]+{IDENTIFIER}[[:space:]]*\] {
	struct lsg_command *cmd;

	if (!suite.commands)
		suite.commands = xmalloc(sizeof(*suite.commands));
	cmd = suite.commands;
	memset(cmd, 0, sizeof(*cmd));
	cmd->name.orig = parse_identifier(yytext);
	cmd->options = xmalloc(sizeof(*cmd->options));
	BEGIN(SC_COMMAND);
}

<INITIAL,SC_COMMAND,SC_OPTION>\[[[:space:]]*subcommand[[:space:]]+{IDENTIFIER}[[:space:]]*\] {
	int command_num = ++suite.num_subcommands;
	struct lsg_command *cmd;

	suite.commands = realloc(suite.commands,
		(suite.num_subcommands + 1) * sizeof(*suite.commands));
	cmd = suite.commands + command_num;
	memset(cmd, 0, sizeof(*cmd));
	cmd->name.orig = parse_identifier(yytext);
	cmd->options = xmalloc(sizeof(*cmd->options));
	BEGIN(SC_COMMAND);
}

<SC_COMMAND>\[[[:space:]]*description[[:space:]]*\] {
	text_buf = NULL;
	text_buf_len = 0;
	yy_push_state(SC_DESCRIPTION);
}

<SC_COMMAND,SC_OPTION>\[[[:space:]]*closing[[:space:]]*\] {
	text_buf = NULL;
	text_buf_len = 0;
	yy_push_state(SC_CLOSING);
}

<SC_OPTION>\[[[:space:]]*help[[:space:]]*\] {
	text_buf = NULL;
	text_buf_len = 0;
	yy_push_state(SC_HELP);
}
<INITIAL,SC_OPTION,SC_COMMAND>\[[[:space:]]*section[[:space:]]+{SIMPLE_STRING}[[:space:]]*\] {
	int sect_num = suite.num_sections++;
	suite.sections = realloc(suite.sections,
		 suite.num_sections * sizeof(*suite.sections));
	CURSECT.name.orig = parse_simple_string();
	text_buf = NULL;
	text_buf_len = 0;
	yy_push_state(SC_SECTION);
}

<SC_DESCRIPTION>[[:space:]]*\[[[:space:]]*\/description[[:space:]]*\]\n {
	CURCMD.description = text_buf;
	yy_pop_state();
}

<SC_CLOSING>[[:space:]]*\[[[:space:]]*\/closing[[:space:]]*\]\n {
	CURCMD.closing = text_buf;
	yy_pop_state();
}

<SC_HELP>[[:space:]]*\[[[:space:]]*\/help[[:space:]]*\]\n {
	CUROPT.help = text_buf;
	yy_pop_state();
}

<SC_SECTION>[[:space:]]*\[[[:space:]]*\/section[[:space:]]*\]\n {
	CURSECT.text = text_buf;
	yy_pop_state();
}

<SC_DESCRIPTION,SC_HELP,SC_SECTION,SC_CLOSING,SC_INTRODUCTION,SC_CONCLUSION>.*\n {
	size_t new_len = text_buf_len + yyleng;
	size_t num_tabs = 0;

	while (yytext[num_tabs] == '\t')
		num_tabs++;
	new_len = text_buf_len + yyleng - num_tabs;
	text_buf = realloc(text_buf, new_len + 1);
	memcpy(text_buf + text_buf_len, yytext + num_tabs, yyleng - num_tabs);
	text_buf[new_len] = '\0';
	text_buf_len = new_len;
}

<SC_COMMAND,SC_OPTION>\[[[:space:]]*option[[:space:]]+{IDENTIFIER}[[:space:]]*\] {
	int option_num = CURCMD.num_options++;
	struct lsg_option *opt;

	CURCMD.options = realloc(CURCMD.options,
		CURCMD.num_options * sizeof(*CURCMD.options));
	memset(&CUROPT, 0, sizeof(CUROPT));
	CUROPT.name.orig = parse_identifier(yytext);
	BEGIN(SC_OPTION);
}

<SC_OPTION>arg_info{EQUALS} BEGIN(SC_ARG_INFO);
<SC_ARG_INFO>no_arg CUROPT.arg_info = "LLS_NO_ARGUMENT"; BEGIN(SC_OPTION);
<SC_ARG_INFO>required_arg CUROPT.arg_info = "LLS_REQUIRED_ARGUMENT"; BEGIN(SC_OPTION);
<SC_ARG_INFO>optional_arg CUROPT.arg_info = "LLS_OPTIONAL_ARGUMENT"; BEGIN(SC_OPTION);

<SC_OPTION>arg_type{EQUALS} BEGIN(SC_ARG_TYPE);
<SC_ARG_TYPE>none CUROPT.arg_type = "LLS_NONE"; BEGIN(SC_OPTION);
<SC_ARG_TYPE>string CUROPT.arg_type = "LLS_STRING"; BEGIN(SC_OPTION);
<SC_ARG_TYPE>int32 CUROPT.arg_type = "LLS_INT32"; BEGIN(SC_OPTION);
<SC_ARG_TYPE>uint32 CUROPT.arg_type = "LLS_UINT32"; BEGIN(SC_OPTION);
<SC_ARG_TYPE>int64 CUROPT.arg_type = "LLS_INT64"; BEGIN(SC_OPTION);
<SC_ARG_TYPE>uint64 CUROPT.arg_type = "LLS_UINT64"; BEGIN(SC_OPTION);

<SC_OPTION>flag BEGIN(SC_OPTION_FLAG);
<SC_OPTION_FLAG>multiple CUROPT.multiple = true; BEGIN(SC_OPTION);
<SC_OPTION_FLAG>required CUROPT.required = true; BEGIN(SC_OPTION);
<SC_OPTION_FLAG>ignored CUROPT.ignored = true; BEGIN(SC_OPTION);

<SC_OPTION>default_val{EQUALS} {
	charpp = &CUROPT.default_val;
	if (!CUROPT.arg_type || strcmp(CUROPT.arg_type, "LLS_NONE") == 0) {
		fprintf(stderr, "default_value for option w/o arguments!?\n");
		exit(1);
	} else if (strcmp(CUROPT.arg_type, "LLS_STRING") == 0)
		yy_push_state(SC_UNQUOTED_LINE_CHECK_DEFAULT);
	else
		yy_push_state(SC_INTEGER);
}
<SC_OPTION>short_opt{EQUALS} BEGIN(SC_CHAR);
<SC_CHAR>[a-zA-Z0-9] CUROPT.short_opt = yytext[0]; BEGIN(SC_OPTION);

<SC_OPTION>values{EQUALS}\{ {
	if (!CUROPT.arg_type || strcmp(CUROPT.arg_type, "LLS_STRING")) {
		fprintf(stderr, "value list is only supported for string options\n");
		exit(EXIT_FAILURE);
	}
	if (!CUROPT.arg_info || !strcmp(CUROPT.arg_info, "LLS_NO_ARGUMENT")) {
		fprintf(stderr, "enum options must take an argument\n");
		exit(EXIT_FAILURE);
	}
	if (CUROPT.default_val) {
		fprintf(stderr, "value list must preceed default value\n");
		exit(EXIT_FAILURE);
	}
	BEGIN(SC_VALUES_ID);
}

<SC_VALUES_ID>{IDENTIFIER}{EQUALS}{STRING_VALUE} {
	parse_id_string();
	BEGIN(SC_VALUES_COMMA_OR_BRACE);
}

<SC_VALUES_COMMA_OR_BRACE>[,\}] {
	if (*yytext == ',')
		BEGIN(SC_VALUES_ID);
	else {
		check_default_val();
		BEGIN(SC_OPTION);
	}
}

<SC_OPTION>summary{EQUALS} {
	charpp = &CUROPT.summary;
	yy_push_state(SC_UNQUOTED_LINE);
}
<SC_OPTION>typestr{EQUALS} {
	charpp = &CUROPT.typestr;
	yy_push_state(SC_UNQUOTED_LINE);
}

<SC_COMMAND>purpose{EQUALS} {
	charpp = &CURCMD.purpose;
	yy_push_state(SC_UNQUOTED_LINE);
}

<SC_COMMAND>non-opts-name{EQUALS} {
	charpp = &CURCMD.non_opts_name;
	yy_push_state(SC_UNQUOTED_LINE);
}

<SC_COMMAND>synopsis{EQUALS} {
	charpp = &CURCMD.synopsis;
	yy_push_state(SC_UNQUOTED_LINE);
}

<SC_COMMAND>aux_info{EQUALS} {
	charpp = &CURCMD.aux_info;
	yy_push_state(SC_UNQUOTED_LINE);
}

<SC_UNQUOTED_LINE>.*\n {
	*charpp = parse_unquoted_line();
	yy_pop_state();
}
<SC_UNQUOTED_LINE_CHECK_DEFAULT>.*\n {
	*charpp = parse_unquoted_line();
	check_default_val();
	yy_pop_state();
}

<SC_INTEGER>{INT_CONSTANT} *charpp = strdup(yytext); yy_pop_state();

 /* This rule runs iff none of the above patterns matched */
. {
	fprintf(stderr, "parse error at line %d. Unmatched: \"%s\"\n",
		yyget_lineno(), yytext);
	BEGIN(SC_ERROR);
}
<SC_ERROR>.*\n {
	fprintf(stderr, "subsequent unparsed input: %s\n", yytext);
	exit(EXIT_FAILURE);
}
