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

 /* We don't want symbols to clash with those of other flex users. */
%option prefix="lls_yy"

 /* Generate a scanner which is safe to use in multithreaded programs. */
%option reentrant

 /* To maintain state in a reentrant way. */
%option extra-type="struct lls_yy_config_file_extra *"

%option stack
%option never-interactive
%option yylineno

%x SC_ARG
%s SC_SCANNING

IDENTIFIER [a-zA-Z]+[a-zA-Z0-9_-]*
EQUALS [[:blank:]]*=[[:blank:]]*
OPTION [a-zA-Z]+[a-zA-Z0-9_-]*

%{
	#include <stdlib.h>
	#include <ctype.h>
	#include <assert.h>
	#include <stdbool.h>
	#include <inttypes.h>
	#include "lopsub-internal.h"
	#include "lopsub.h"

	struct lls_yy_config_file_extra {
		int argc;
		char **argv;
		const char *subcmd;
	};

	static int add_option(yyscan_t yyscanner);
	static int parse_arg(char **result, yyscan_t yyscanner);
	static int yywrap(yyscan_t yyscanner) {return 1;}
%}

%%

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

<INITIAL,SC_SCANNING>\[[[:blank:]]*{IDENTIFIER}[[:blank:]]*\][[:blank:]]*\n {
	int i, j;
	const char *subcmd = yyget_extra(yyscanner)->subcmd;

	assert(yytext[0] == '[');
	if (!subcmd)
		return 0;
	for (i = 1; i < yyleng; i++)
		if (!isspace(yytext[i]))
			break;
	for (j = i; j < yyleng; j++)
		if (yytext[j] == ']' || isspace(yytext[j]))
			break;
	assert(j < yyleng);
	yytext[j] = '\0';
	if (strcmp(yytext + i, subcmd))
		BEGIN(INITIAL);
	else
		BEGIN(SC_SCANNING);
}

<SC_SCANNING>{OPTION}[[:blank:]]*\n add_option(yyscanner);

<SC_SCANNING>{OPTION}({EQUALS}|[[:blank:]]+) {
	int ret = add_option(yyscanner);

	if (ret < 0)
		return ret;
	BEGIN(SC_ARG);
}

<SC_ARG>.*\n {
	struct lls_yy_config_file_extra *extra = yyget_extra(yyscanner);
	const char *opt = extra->argv[extra->argc - 1];
	char *arg, *result;
	size_t opt_len = strlen(opt), arg_len;
	int ret = parse_arg(&arg, yyscanner);

	if (ret < 0)
		return ret;
	arg_len = ret;
	result = malloc(opt_len + arg_len + 2);
	if (!result)
		return -E_LLS_NOMEM;
	strcpy(result, opt);
	result[opt_len] = '=';
	strcpy(result + opt_len + 1, arg);
	free(arg);
	result[opt_len + arg_len + 1] = '\0';
	free(extra->argv[extra->argc - 1]);
	extra->argv[extra->argc - 1] = result;
	BEGIN(SC_SCANNING);
}

<INITIAL>.*\n {}

 /* This rule runs iff none of the above patterns matched */
. {return -1;}
%%

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>

static int expand_result(yyscan_t yyscanner)
{
	struct lls_yy_config_file_extra *extra = yyget_extra(yyscanner);
	int nargc = extra->argc + 1;
	char **nrargv = realloc(extra->argv, (nargc + 1) * sizeof(char *));

	if (!nrargv)
		return -E_LLS_NOMEM;
	extra->argc = nargc;
	extra->argv = nrargv;
	extra->argv[extra->argc] = NULL;
	return 1;
}

static int add_option(yyscan_t yyscanner)
{
	struct lls_yy_config_file_extra *extra = yyget_extra(yyscanner);
	int ret;
	unsigned n;
	int len = yyget_leng(yyscanner);
	char *narg, *text = yyget_text(yyscanner);

	for (n = 0; n < len; n++)
		if (!isalnum(text[n]) && !(text[n] == '-'))
			break;
	assert(n > 0);
	ret = expand_result(yyscanner);
	if (ret < 0)
		return ret;
	narg = malloc(n + 2 + 1);
	if (!narg)
		return -E_LLS_NOMEM;
	narg[0] = narg[1] = '-';
	memcpy(narg + 2, text, n);
	narg[n + 2] = '\0';
	extra->argv[extra->argc - 1] = narg;
	return 1;
}

static int parse_arg(char **result, yyscan_t yyscanner)
{
	bool backslash = false, quote = false;
	const char *in;
	char *out;
	int ret;
	int len = yyget_leng(yyscanner);
	char *text = yyget_text(yyscanner);

	*result = malloc(len + 1);
	if (!*result)
		return -E_LLS_NOMEM;
	for (in = text, out = *result; *in; in++) {
		if (*in == '\\') {
			if (!backslash) {
				backslash = true;
				continue;
			}
		} else if (*in == 'n' || *in == 't') {
			if (backslash) { /* \n or \t */
				*out++ = (*in == 'n')? '\n' : '\t';
				backslash = false;
				continue;
			}
		} else if (*in == '"') {
			if (!backslash) {
				quote = !quote;
				continue;
			}
		} else if (isspace(*in)) {
			if (!backslash && !quote)
				break;
		}
		/* copy the character */
		*out++ = *in;
		backslash = false;
	}
	ret = -E_LLS_TRAILING_BACKSLASH;
	if (backslash)
		goto fail;
	ret = -E_LLS_UNMATCHED_QUOTE;
	if (quote)
		goto fail;
	/* look at first non-space character */
	for (; *in; in++) {
		if (isspace(*in))
			continue;
		if (*in == '#')
			break;
		ret = -E_LLS_TRAILING_GARBAGE;
		goto fail;
	}
	/* success */
	*out = '\0';
	return out - *result;
fail:
	assert(ret < 0);
	free(*result);
	*result = NULL;
	return ret;
}

void lls_free_argv(char **argv)
{
	int i;

	if (!argv)
		return;
	for (i = 0; argv[i]; i++)
		free(argv[i]);
	free(argv);
}

int lls_convert_config(const char *buf, size_t nbytes, const char *subcmd,
		 char ***result, char **errctx)
{
	int ret;
	YY_BUFFER_STATE yybs;
	yyscan_t yyscanner;
	struct lls_yy_config_file_extra extra;

	*result = NULL;
	if (errctx)
		*errctx = NULL;

	extra.argc = 1;
	extra.argv = malloc((extra.argc + 1) * sizeof(char *));
	if (!extra.argv)
		return -E_LLS_NOMEM;
	extra.argv[0] = strdup(__FUNCTION__);
	if (!extra.argv[0]) {
		free(extra.argv);
		return -E_LLS_NOMEM;
	}
	extra.argv[1] = NULL;
	extra.subcmd = subcmd;

	ret = yylex_init_extra(&extra, &yyscanner);
	if (ret != 0) {
		ret = -E_LLS_NOMEM;
		goto free_argv;
	}
	yy_push_state(subcmd? INITIAL : SC_SCANNING, yyscanner);
	yybs = yy_scan_bytes(buf, nbytes, yyscanner);
	if (!yybs) {
		ret = -E_LLS_YY_SCAN;
		goto destroy;
	}
	ret = yylex(yyscanner);
	if (ret < 0) {
		ret = -E_LLS_YY_LEX;
		if (errctx) {
			*errctx = malloc(100);
			if (*errctx)
				sprintf(*errctx, "yylex error");
		}
	}
	yy_delete_buffer(yybs, yyscanner);
destroy:
	yylex_destroy(yyscanner);
free_argv:
	if (ret < 0)
		lls_free_argv(extra.argv);
	else {
		*result = extra.argv;
		ret = extra.argc;
	}
	return ret;
}

#if 0
/* flex -o t.c config_file.l && cc -o tcf t.c */
int main(void)
{
	char buf[100 * 1024];
	int ret, len, i, argc;
	char **argv;

	ret = read(STDIN_FILENO, buf, sizeof(buf));
	if (ret <= 0)
		exit(EXIT_FAILURE);
	len = ret;
	ret = lls_convert_config(buf, len, NULL, &argv, NULL);
	if (ret < 0)
		exit(EXIT_FAILURE);
	argc = ret;
	for (i = 0; i < argc; i++)
		printf("argv[%d]: %s\n", i, argv[i]);
	return EXIT_SUCCESS;
}
#endif
