/* Copyright 2011,2012 Bernhard R. Link <brlink@debian.org>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
 * SOFTWARE IN THE PUBLIC INTEREST, INC. BE LIABLE FOR ANY CLAIM, DAMAGES OR
 * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 */
#include "config.h"

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

#include "globals.h"
#include "eff.h"
#include "tags.h"

int variable_count = 0;
struct variable *switches = NULL;

static int new_switch(const char *name, bool iswrite) {
	struct variable *s, **pp = &switches;
	int id = 0;

	if ((*name < 'a' || *name > 'z') && (*name < 'A' || *name > 'Z')  &&
			*name != '_') {
		fail("Invalid Switch name '%s'", name);
	}

	while ((s = *pp) != NULL) {
		if (strcmp(s->name, name) == 0) {
			s->usage[iswrite?1:0] = true;
			return s->id;
		}
		id++;
		assert (s->id == id);
		pp = &s->next;
	}
	id++;
	variable_count ++;
	assert (id == variable_count);
	s = zNEW(1, struct variable);
	s->name = strdup(name);
	s->id = id;
	s->usage[iswrite?1:0] = true;
	if (s->name == NULL)
		oom();
	*pp = s;
	return id;
}

static bool field_condition(struct tag *t, char *options) {
	int min, max;
	char *name;

	while (*options == ' ' || *options == '\t')
		options ++;
	if (*options >= '0' && *options <='9') {
		min = *options - '0';
		options++;
		while (*options == ' ' || *options == '\t')
			options ++;
		if (*options != '<')
			fail("Invalid first value in '%s' or missing '<' after it", options);
		options++;
		if (*options == '=')
			options ++;
		else
			min++;
		while (*options == ' ' || *options == '\t')
			options ++;
	} else
		min = 0;
	name = options;
	while (*options != '\0' && *options != ' ' && *options != '\t')
		options++;
	if (*options != '\0') {
		*options = '\0';
		options++;
		while (*options == ' ' || *options == '\t')
			options ++;
		if (*options == '\0')
			max = 255;
		else if (*options != '>') {
			fail("Trailing garbage after switch name: '%s'", options);
		} else {
			options++;
			max = 0;
			if (*options == '=')
				options++;
			else
				max--;
			while (*options == ' ' || *options == '\t')
				options ++;
			if (*options >= '0' && *options <='9') {
				max += *options - '0';
				if (max < 0)
					fail("Invalid maximum number in condition: '%s'", options);
				options++;
				while (*options == ' ' || *options == '\t')
					options ++;
				if (*options != '\0')
					fail("Trailing garbage after switch name: '%s'", options);
			} else
				fail("Missing maximum number for condition: '%s'", options);
		}
	} else
		max = 255;
	t->switch_condition = new_switch(name, false);
	t->switch_condition_min = min;
	t->switch_condition_max = max;
	return true;
}

static bool field_action(struct tag *t, char *options) {
	char *e;
	int switch_id;

	if (t->switch_set_action != 0) {
		fail("Too many actions");
	}

	while (*options == ' ' || *options == '\t')
		options ++;
	if (strncmp(options, "set", 3) != 0) {
		fail("Unsupported action (only 'set' yet supported): '%s'",
				options);
	}
	options += 3;
	while (*options == ' ' || *options == '\t')
		options ++;
	e = options;
	while (*e != '\0' && *e != ' ' && *e != '\t')
		e++;
	if (*e == '\0')
		fail("Missing value after set: '%s' (only variable name found)",
				options);
	*e = '\0'; e++;
	if ((*e < '0' && *e > '9' ) || e[1] != '\0')
		fail("Unsupported value: '%s' (only 0 to 9 yet supported)",
				e);
	switch_id = new_switch(options, true);
	t->switch_set_action = switch_id;
	t->switch_set_value = *e - '0';
	return true;
}

static bool field_ignore(struct tag *t, char *options) {
	return true;
}
static bool field_match(struct tag *t, char *options) {
	struct pattern *p;
	size_t len;

	if (strncmp(options, "regexp:", 7) != 0) {
		fail("Unsupported match type (only 'regexp:' yet supported): '%s'",
				options);
	}
	options += 7;
	len = strlen(options);
	p = zNEWvarstruct(len + 1, struct pattern, pattern);
	memcpy(p->pattern, options, len + 1);
	p->next = t->patterns;
	t->patterns = p;
	return true;
}

static bool field_extract(struct tag *t, char *options) {
	char *e;

	if (strncmp(options, "simple", 6) != 0) {
		fail("Unsupported extract type (only 'simple' yet supported): '%s'",
				options);
	}
	options += 6;
	options += strspn(options, " \t");
	t->extractstart = strtol(options, &e, 10);
	if (e == options || t->extractstart < 0)
		fail("Missing start number in simple extract");
	options = e;
	options += strspn(options, " \t");
	if (t->extractstart != 0) {
		if (options[0] != '\'' || options[1] == '\0' || options[2] != '\'')
			fail("Missing first character in simple extract");
		t->extractstartchar = options[1];
		options += 3;
		options += strspn(options, " \t");
	}
	if (options[0] == '\'') {
		if (options[1] == '\0' || options[2] != '\'') {
			fail("Malformed reset character in simple extract");
		}
		t->extractresetchar = options[1];
		options += 3;
		options += strspn(options, " \t");
	} else {
		t->extractresetchar = '\0';
	}
	t->extractend = strtol(options, &e, 10);
	if (e == options || t->extractend <= 0)
		fail("Missing end number in simple extract");
	options = e;
	options += strspn(options, " \t");
	if (options[0] != '\'' || options[1] == '\0' || options[2] != '\'')
		fail("Missing first character in simple extract");
	t->extractendchar = options[1];
	options += 3;
	options += strspn(options, " \t");
	if (*options != '\0')
		fail("Unexpected trailing garbage in simple extract");
	return true;
}

static bool field_merge(struct tag *t, char *options) {
	size_t len;

	if (t->mergename1 != NULL) {
		fprintf(stderr, "multiple merges not yet supported (in '%s')",
				t->name);
		return false;
	}

	if (strncmp(options, "same", 4) != 0) {
		fail("Unsupported merge mode (only 'same' yet supported): '%s'",
				options);
	}
	t->mergemode = merge_same;
	options += 4;
	options += strspn(options, " \t");
	len = strcspn(options, " \t");
	t->mergename1 = strndup(options, len);
	if (t->mergename1 == NULL)
		oom();
	options += len;
	options += strspn(options, " \t");
	len = strcspn(options, " \t");
	if (len == 0)
		fail("Nothing given to merge in merge field");
	t->mergename2 = strndup(options, len);
	if (t->mergename2 == NULL)
		oom();
	options += len;
	options += strspn(options, " \t");
	t->mergename3 = NULL;
	if (options[0] == 'o' && options[1] == 'r') {
		options +=2;
		options += strspn(options, " \t");
		len = strcspn(options, " \t");
		if (len == 0)
			fail("Nothing given to merge in merge field");
		t->mergename3 = strndup(options, len);
		options += len;
		options += strspn(options, " \t");
	}
	if (*options != '\0')
		fail("Trailing garbage after merge data: '%s'", options);
	return true;
}

static const struct field {
	size_t namelen;
	const char *name;
	bool (*parse)(struct tag *, char *options);
} fields[] = {
#define FIELD(name, function) {sizeof(name), name ":", function}
	FIELD("Since", field_ignore),
	FIELD("Match", field_match),
	FIELD("Extract", field_extract),
	FIELD("Merge", field_merge),
	FIELD("Action", field_action),
	FIELD("Condition", field_condition),
	{0, NULL, NULL}
#undef FIELD
};

struct tag *tags = NULL;

bool parse_tag_description(FILE *f, const char *filename) {
	struct tag *tag;
	const struct field *field;
	char *line = NULL, *p, *o;
	const char *basename, *e;
	size_t n = 0, l;
	ssize_t got;

	tag = zNEW(1, struct tag);
	basename = strrchr(filename, '/');
	if (basename == NULL)
		basename = filename;
	else
		basename++;
	e = strchr(basename, '.');
	if (e == NULL || (strcmp(e, ".description") != 0 && strcmp(e, ".switch") != 0)) {
		fail("Cannot extract tag name from filename '%s'", filename);
	}
	if (e[1] == 's')
		tag->name = strdup("");
	else
		tag->name = strndup(basename, e-basename);
	if (tag->name == NULL)
		oom();

	while ((got = getline(&line, &n, f)) > 0) {
		if (line[got-1] != '\n')
			fail("Incomplete line in '%s'", filename);
		line[--got] = '\0';
		if (got > 0 && line[got-1] == '\r')
			fail("Strange newlines in '%s'", filename);
		if (strlen(line) != got)
			fail("binary data in '%s'", filename);
		if (line[0] == '#' && line[1] == '#') {
			if (line[2] != ' ' && line[2] != '\t' )
				fail("missing space after '##' in '%s'", filename);
			p = line + 2;
			p += strspn(p, " \t");
			l = strcspn(p, " \t");
			o = p + l + strspn(p + l, " \t");
			for (field = fields; field->name != NULL; field++) {
				if (field->namelen != l)
					continue;
				if (memcmp(field->name, p, l) != 0)
					continue;
				if (!field->parse(tag, o))
					return false;
				break;
			}
			if (field->name == NULL)
				fail("Unknown directive '%.*s' in '%s'",
						(int)(l), p, filename);
		}
	}
	if (ferror(f) != 0) {
		free(tag);
		return false;
	}
	tag->next = tags;
	tags = tag;
	return true;
}

void tag_write(struct tag *tag, struct effwriter *file) {
	eff_writestart(file, "goal");
	if (tag->name[0] != '\0') {
		eff_writestart(file, "name");
		eff_writestring(file, tag->name, strlen(tag->name));
		eff_writeend(file);
	}
	if (tag->extractend > 0) {
		eff_writestart(file, "extract:simple");
		eff_writeuint8(file, tag->extractstart);
		eff_writeuint8(file, tag->extractend);
		eff_writeuint8(file, tag->extractresetchar);
		eff_writeuint8(file, tag->extractstartchar);
		eff_writeuint8(file, tag->extractendchar);
		eff_writeend(file);
	}
	switch (tag->mergemode) {
		case merge_none:
			break;
		case merge_same:
			eff_writestart(file, "merge:same");
			eff_writeuint32(file, tag->mergeid1);
			eff_writeuint32(file, tag->mergeid2);
			eff_writeuint32(file, tag->mergeid3);
			eff_writeend(file);
			break;
	}
	if (tag->switch_set_action != 0) {
		eff_writestart(file, "set");
		eff_writeuint32(file, tag->switch_set_action);
		eff_writeuint8(file, tag->switch_set_value);
		eff_writeend(file);
	}
	if (tag->switch_condition != 0) {
		eff_writestart(file, "condition");
		eff_writeuint32(file, tag->switch_condition);
		eff_writeuint8(file, tag->switch_condition_min);
		eff_writeuint8(file, tag->switch_condition_max);
		eff_writeend(file);
	}
	eff_writeend(file);
}
