/*
 * Copyright (C) 2017 FAUmachine Team <info@faumachine.org>.
 * This program is free software. You can redistribute it and/or modify it
 * under the terms of the GNU General Public License, either version 2 of
 * the License, or (at your option) any later version. See COPYING.
 */

/*
 * Syntax:
 * 
 * special
 * 	: "@if" cond
 * 	  comp
 * 	  "@endif"
 * 	| comp
 * 	;
 *
 * comp:
 *	: def_list_e
 *	  "@comp" ident include-file
 *	;
 *
 * def_list_e
 * 	: //LAMBDA
 *	| def_list_e def
 *	;
 * def
 *	: "@if" cond
 *	  "@define" ident rep
 *	  "@endif"
 *	| "@if" cond
 *	  "@define" ident rep
 *	  "@else"
 *	  "@define" ident rep
 *	  "@endif"
 *	;
 *
 * include
 * 	: @include
 * 	;
 *
 * state
 * 	: @state
 * 	;
 *
 * export
 * 	: @export
 * 	;
 *
 * behavior
 * 	: @behavior
 * 	;
 */

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct block {
	struct block *parent;

	struct inst *first;
	struct inst *last;
};
struct inst {
	struct inst *prev;
	struct inst *next;

	enum {
		INST_IF,
		INST_ELIF,
		INST_ELSE,
		INST_DEFINE,
		INST_ERROR,
		INST_COMP,
	} type;
	union {
		struct {
			char *condition;
			struct block block;
		} inst_if;
		struct {
			char *condition;
			struct block block;
		} inst_elif;
		struct {
			struct block block;
		} inst_else;
		struct {
			char *ident;
			char *replace;
		} inst_define;
		struct {
			char *string;
		} inst_error;
		struct {
			int inspect;
			char *name;
			char *include;
		} inst_comp;
	};
};

static struct block block = { NULL, NULL, NULL };
static struct block *block_curr = &block;

static void
_do_include(char **include, int *nincludesp, struct block *blk)
{
	struct inst *inst;
	int i;

	for (inst = blk->first; inst; inst = inst->next) {
		switch (inst->type) {
		case INST_IF:
			_do_include(include, nincludesp, &inst->inst_if.block);
			break;
		case INST_ELIF:
			_do_include(include, nincludesp, &inst->inst_elif.block);
			break;
		case INST_ELSE:
			_do_include(include, nincludesp, &inst->inst_else.block);
			break;
		case INST_DEFINE:
		case INST_ERROR:
			break;
		case INST_COMP:
			for (i = 0; ; i++) {
				if (i == *nincludesp) {
					include[i] = inst->inst_comp.include;
					*nincludesp += 1;
					break;
				}
				if (strcmp(include[i], inst->inst_comp.include) == 0) {
					break;
				}
			}
		}
	}
}

static void
do_include(void)
{
	char *include[256];
	int nincludes;
	int i;

	nincludes = 0;
	_do_include(include, &nincludes, &block);

	printf("#define INCLUDE\n");
	for (i = 0; i < nincludes; i++) {
		printf("#include \"%s\"\n", include[i]);
	}
	printf("#undef INCLUDE\n");
}

static void
do_block(char *define[], int *ndefinesp, struct block *blk, int inspect)
{
	struct inst *inst;
	int i;

	for (inst = blk->first; inst; inst = inst->next) {
		switch (inst->type) {
		case INST_IF:
			printf("#if %s\n", inst->inst_if.condition);
			do_block(define, ndefinesp, &inst->inst_if.block, inspect);
			if (inst->next == NULL
			 || (inst->next->type != INST_ELIF
			  && inst->next->type != INST_ELSE)) {
				printf("#endif\n");
			}
			break;
		case INST_ELIF:
			printf("#elif %s\n", inst->inst_elif.condition);
			do_block(define, ndefinesp, &inst->inst_elif.block, inspect);
			if (inst->next == NULL
			 || (inst->next->type != INST_ELIF
			  && inst->next->type != INST_ELSE)) {
				printf("#endif\n");
			}
			break;
		case INST_ELSE:
			printf("#else\n");
			do_block(define, ndefinesp, &inst->inst_else.block, inspect);
			printf("#endif\n");
			break;
		case INST_DEFINE:
			printf("#define %s %s\n",
					inst->inst_define.ident,
					inst->inst_define.replace);
			for (i = 0; ; i++) {
				if (i == *ndefinesp) {
					define[i] = inst->inst_define.ident;
					*ndefinesp += 1;
					break;
				}
				if (strcmp(define[i], inst->inst_define.ident) == 0) {
					break;
				}
			}
			break;
		case INST_ERROR:
			printf("#error\n");
			break;
		case INST_COMP:
			printf("#define DEFAULT_NAME_(x) %s_ ## x\n", inst->inst_comp.name);
			printf("#define NAME %s\n", inst->inst_comp.name);
			printf("#define NAME_(x) %s_ ## x\n", inst->inst_comp.name);
			printf("#define CONFIG_INSPECTION 0\n");
			printf("#include \"%s\"\n", inst->inst_comp.include);
			printf("#undef CONFIG_INSPECTION\n");
			printf("#undef NAME_\n");
			if (inspect && inst->inst_comp.inspect) {
				printf("#define NAME_(x) %s_inspect_ ## x\n", inst->inst_comp.name);
				printf("#define CONFIG_INSPECTION 1\n");
				printf("#include \"%s\"\n", inst->inst_comp.include);
				printf("#undef CONFIG_INSPECTION\n");
				printf("#undef NAME_\n");
			}
			printf("#undef NAME\n");
			printf("#undef DEFAULT_NAME_\n");
			for (i = *ndefinesp - 1; 0 <= i; i--) {
				printf("#undef %s\n", define[i]);
			}
			*ndefinesp = 0;
			break;
		}
	}
}

static void
do_state(void)
{
	char *define[256];
	int ndefines;

	ndefines = 0;
	printf("#define STATE\n");
	do_block(define, &ndefines, &block, 0);
	printf("#undef STATE\n");
}

static void
do_export(void)
{
	char *define[256];
	int ndefines;

	ndefines = 0;
	printf("#define EXPORT\n");
	do_block(define, &ndefines, &block, 1);
	printf("#undef EXPORT\n");
}

static void
do_behavior(void)
{
	char *define[256];
	int ndefines;

	ndefines = 0;
	printf("#define BEHAVIOR\n");
	do_block(define, &ndefines, &block, 1);
	printf("#undef BEHAVIOR\n");
}

int
main(int argc, char **argv)
{
	enum {
		STATE_STD,
		STATE_IF,
		STATE_ELSE,
		STATE_IF_IF,
		STATE_IF_ELSE,
		STATE_ELSE_IF,
		STATE_ELSE_ELSE,
	} state;
	int ln;
	char line[1024];
	char *p1;
	char *p2;
	struct inst *inst;

	printf("/*\n");
	printf(" * WARNING: Generated code. Don't edit!\n");
	printf(" */\n");
	printf("\n");

	state = STATE_STD;
	ln = 1;
	while (fgets(line, sizeof(line), stdin) != NULL) {
		if (strchr(line, '\n') == NULL) {
			fprintf(stderr, "%d: Missing \\n at end of file or line too long.\n", ln);
			exit(1);
		}
		if (strncmp(line, "@if", 3) == 0) {
			if (state == STATE_STD) {
				state = STATE_IF;
			} else if (state == STATE_IF) {
				state = STATE_IF_IF;
			} else if (state == STATE_ELSE) {
				state = STATE_ELSE_IF;
			} else {
				fprintf(stderr, "%d: Syntax error.\n", ln);
				exit(1);
			}

			p1 = strtok(line + 3, "\n");

			inst = malloc(sizeof(*inst));
			assert(inst);

			inst->type = INST_IF;
			inst->inst_if.condition = strdup(p1);
			assert(inst->inst_if.condition);
			inst->inst_if.block.parent = block_curr;
			inst->inst_if.block.first = NULL;
			inst->inst_if.block.last = NULL;

			inst->prev = block_curr->last;
			inst->next = NULL;
			if (inst->prev) {
				inst->prev->next = inst;
			} else {
				block_curr->first = inst;
			}
			block_curr->last = inst;

			block_curr = &inst->inst_if.block;

		} else if (strncmp(line, "@elif", 5) == 0) {
			block_curr = block_curr->parent;

			inst = malloc(sizeof(*inst));
			assert(inst);

			p1 = strtok(line + 5, "\n");

			inst->type = INST_ELIF;
			inst->inst_elif.condition = strdup(p1);
			assert(inst->inst_elif.condition);
			inst->inst_elif.block.parent = block_curr;
			inst->inst_elif.block.first = NULL;
			inst->inst_elif.block.last = NULL;

			inst->prev = block_curr->last;
			inst->next = NULL;
			if (inst->prev) {
				inst->prev->next = inst;
			} else {
				block_curr->first = inst;
			}
			block_curr->last = inst;

			block_curr = &inst->inst_elif.block;
			
		} else if (strncmp(line, "@else", 5) == 0) {
			if (state == STATE_IF) {
				state = STATE_ELSE;
			} else if (state == STATE_IF_IF) {
				state = STATE_IF_ELSE;
			} else if (state == STATE_ELSE_IF) {
				state = STATE_ELSE_ELSE;
			} else {
				fprintf(stderr, "%d: Syntax error.\n", ln);
				exit(1);
			}

			block_curr = block_curr->parent;

			inst = malloc(sizeof(*inst));
			assert(inst);

			inst->type = INST_ELSE;
			inst->inst_else.block.parent = block_curr;
			inst->inst_else.block.first = NULL;
			inst->inst_else.block.last = NULL;

			inst->prev = block_curr->last;
			inst->next = NULL;
			if (inst->prev) {
				inst->prev->next = inst;
			} else {
				block_curr->first = inst;
			}
			block_curr->last = inst;

			block_curr = &inst->inst_else.block;

		} else if (strncmp(line, "@endif", 6) == 0) {
			if (state == STATE_IF
			 || state == STATE_ELSE) {
				state = STATE_STD;
			} else if (state == STATE_IF_IF
				|| state == STATE_IF_ELSE
				|| state == STATE_ELSE_IF
				|| state == STATE_ELSE_ELSE) {
				state = STATE_IF;
			} else {
				fprintf(stderr, "%d: Syntax error.\n", ln);
				exit(1);
			}

			block_curr = block_curr->parent;

		} else if (strncmp(line, "@error", 6) == 0) {
			inst = malloc(sizeof(*inst));
			assert(inst);

			inst->type = INST_ERROR;
			inst->inst_error.string = strdup(line + 6);
			assert(inst->inst_error.string);

			inst->prev = block_curr->last;
			inst->next = NULL;
			if (inst->prev) {
				inst->prev->next = inst;
			} else {
				block_curr->first = inst;
			}
			block_curr->last = inst;

		} else if (strncmp(line, "@comp", 5) == 0) {
			p1 = strtok(line + 5, "\t\n \"");
			p2 = strtok(NULL, "\t\n \"");

			inst = malloc(sizeof(*inst));
			assert(inst);

			inst->type = INST_COMP;
			inst->inst_comp.inspect = 0;
			inst->inst_comp.name = strdup(p1);
			assert(inst->inst_comp.name);
			inst->inst_comp.include = strdup(p2);
			assert(inst->inst_comp.include);

			inst->prev = block_curr->last;
			inst->next = NULL;
			if (inst->prev) {
				inst->prev->next = inst;
			} else {
				block_curr->first = inst;
			}
			block_curr->last = inst;

		} else if (strncmp(line, "@icomp", 6) == 0) {
			p1 = strtok(line + 6, "\t\n \"");
			p2 = strtok(NULL, "\t\n \"");

			inst = malloc(sizeof(*inst));
			assert(inst);

			inst->type = INST_COMP;
			inst->inst_comp.inspect = 1;
			inst->inst_comp.name = strdup(p1);
			assert(inst->inst_comp.name);
			inst->inst_comp.include = strdup(p2);
			assert(inst->inst_comp.include);

			inst->prev = block_curr->last;
			inst->next = NULL;
			if (inst->prev) {
				inst->prev->next = inst;
			} else {
				block_curr->first = inst;
			}
			block_curr->last = inst;

		} else if (strncmp(line, "@define", 7) == 0) {
			p1 = strtok(line + 7, "\t\n ");
			p2 = strtok(NULL, "\n");

			inst = malloc(sizeof(*inst));
			assert(inst);

			inst->type = INST_DEFINE;
			inst->inst_define.ident = strdup(p1);
			assert(inst->inst_define.ident);
			inst->inst_define.replace = strdup(p2);
			assert(inst->inst_define.replace);

			inst->prev = block_curr->last;
			inst->next = NULL;
			if (inst->prev) {
				inst->prev->next = inst;
			} else {
				block_curr->first = inst;
			}
			block_curr->last = inst;

		} else if (strncmp(line, "@include", 8) == 0) {
			do_include();

		} else if (strncmp(line, "@state", 6) == 0) {
			do_state();

		} else if (strncmp(line, "@export", 7) == 0) {
			do_export();

		} else if (strncmp(line, "@behavior", 9) == 0) {
			do_behavior();

		} else if (strncmp(line, "@", 1) == 0) {
			fprintf(stderr, "%d: Unknown @ command.\n", ln);
			exit(1);

		} else {
			/* Normal line. */
			printf("%s", line);
		}
		ln++;
	}

	return 0;
}
