/*-
 * Copyright (c) 2010, 2013 The NetBSD Foundation, Inc.
 * All rights reserved.
 *
 * This code is derived from software contributed to The NetBSD Foundation
 * by David A. Holland.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <errno.h>

#include "bool.h"
#include "utils.h"
#include "mode.h"
#include "place.h"
#include "files.h"
#include "directive.h"
#include "macro.h"
#include "eval.h"
#include "output.h"

struct ifstate {
	struct ifstate *prev;
	struct place startplace;
	bool curtrue;
	bool evertrue;
	bool seenelse;
};

static struct ifstate *ifstate;

////////////////////////////////////////////////////////////
// common parsing bits

static
void
uncomment(char *buf)
{
	char *s, *t, *u = NULL;
	bool incomment = false;
	bool inesc = false;
	bool inquote = false;
	char quote = '\0';

	for (s = t = buf; *s; s++) {
		if (incomment) {
			if (s[0] == '*' && s[1] == '/') {
				s++;
				incomment = false;
			}
		} else {
			if (!inquote && s[0] == '/' && s[1] == '*') {
				incomment = true;
			} else {
				if (inesc) {
					inesc = false;
				} else if (s[0] == '\\') {
					inesc = true;
				} else if (!inquote &&
					   (s[0] == '"' || s[0] == '\'')) {
					inquote = true;
					quote = s[0];
				} else if (inquote && s[0] == quote) {
					inquote = false;
				}

				if (t != s) {
					*t = *s;
				}
				if (!strchr(ws, *t)) {
					u = t;
				}
				t++;
			}
		}
	}
	if (u) {
		/* end string after last non-whitespace char */
		u[1] = '\0';
	} else {
		*t = '\0';
	}
}

static
void
oneword(const char *what, struct place *p2, char *line)
{
	size_t pos;

	pos = strcspn(line, ws);
	if (line[pos] != '\0') {
		p2->column += pos;
		complain(p2, "Garbage after %s argument", what);
		complain_fail();
		line[pos] = '\0';
	}
}

////////////////////////////////////////////////////////////
// if handling

static
struct ifstate *
ifstate_create(struct ifstate *prev, struct place *p, bool startstate)
{
	struct ifstate *is;

	is = domalloc(sizeof(*is));
	is->prev = prev;
	if (p != NULL) {
		is->startplace = *p;
	} else {
		place_setbuiltin(&is->startplace, 1);
	}
	is->curtrue = startstate;
	is->evertrue = is->curtrue;
	is->seenelse = false;
	return is;
}

static
void
ifstate_destroy(struct ifstate *is)
{
	dofree(is, sizeof(*is));
}

static
void
ifstate_push(struct place *p, bool startstate)
{
	struct ifstate *newstate;

	newstate = ifstate_create(ifstate, p, startstate);
	if (!ifstate->curtrue) {
		newstate->curtrue = false;
		newstate->evertrue = true;
	}
	ifstate = newstate;
}

static
void
ifstate_pop(void)
{
	struct ifstate *is;

	is = ifstate;
	ifstate = ifstate->prev;
	ifstate_destroy(is);
}

static
void
d_if(struct lineplace *lp, struct place *p2, char *line)
{
	bool doprint;
	char *expr;
	bool val;
	struct place p3 = *p2;
	size_t oldlen;

	doprint = ifstate->curtrue;

	expr = macroexpand(p2, line, strlen(line), true);

	oldlen = strlen(expr);
	uncomment(expr);
	/* trim to fit, so the malloc debugging won't complain */
	expr = dorealloc(expr, oldlen + 1, strlen(expr) + 1);

	if (ifstate->curtrue) {
		val = eval(&p3, expr);
	} else {
		val = 0;
	}
	ifstate_push(&lp->current, val);
	dostrfree(expr);

	if (doprint) {
		debuglog(&lp->current, "#if: %s",
			  ifstate->curtrue ? "taken" : "not taken");
	}
}

static
void
d_ifdef(struct lineplace *lp, struct place *p2, char *line)
{
	bool doprint;

	doprint = ifstate->curtrue;

	uncomment(line);
	oneword("#ifdef", p2, line);
	ifstate_push(&lp->current, macro_isdefined(line));

	if (doprint) {
		debuglog(&lp->current, "#ifdef %s: %s",
			 line, ifstate->curtrue ? "taken" : "not taken");
	}
}

static
void
d_ifndef(struct lineplace *lp, struct place *p2, char *line)
{
	bool doprint;

	doprint = ifstate->curtrue;

	uncomment(line);
	oneword("#ifndef", p2, line);
	ifstate_push(&lp->current, !macro_isdefined(line));

	if (doprint) {
		debuglog(&lp->current, "#ifndef %s: %s",
			 line, ifstate->curtrue ? "taken" : "not taken");
	}
}

static
void
d_elif(struct lineplace *lp, struct place *p2, char *line)
{
	bool doprint;
	char *expr;
	struct place p3 = *p2;
	size_t oldlen;

	if (ifstate->seenelse) {
		complain(&lp->current, "#elif after #else");
		complain_fail();
	}

	doprint = ifstate->curtrue;

	if (ifstate->evertrue) {
		ifstate->curtrue = false;
	} else {
		expr = macroexpand(p2, line, strlen(line), true);

		oldlen = strlen(expr);
		uncomment(expr);
		/* trim to fit, so the malloc debugging won't complain */
		expr = dorealloc(expr, oldlen + 1, strlen(expr) + 1);

		ifstate->curtrue = eval(&p3, expr);
		ifstate->evertrue = ifstate->curtrue;
		dostrfree(expr);
	}

	if (doprint) {
		debuglog2(&lp->current, &ifstate->startplace, "#elif: %s",
			  ifstate->curtrue ? "taken" : "not taken");
	}
}

static
void
d_else(struct lineplace *lp, struct place *p2, char *line)
{
	bool doprint;

	(void)p2;
	(void)line;

	if (ifstate->seenelse) {
		complain(&lp->current,
			 "Multiple #else directives in one conditional");
		complain_fail();
	}

	doprint = ifstate->curtrue;

	ifstate->curtrue = !ifstate->evertrue;
	ifstate->evertrue = true;
	ifstate->seenelse = true;

	if (doprint) {
		debuglog2(&lp->current, &ifstate->startplace, "#else: %s",
			  ifstate->curtrue ? "taken" : "not taken");
	}
}

static
void
d_endif(struct lineplace *lp, struct place *p2, char *line)
{
	(void)p2;
	(void)line;

	if (ifstate->prev == NULL) {
		complain(&lp->current, "Unmatched #endif");
		complain_fail();
	} else {
		debuglog2(&lp->current, &ifstate->startplace, "#endif");
		ifstate_pop();
	}
}

////////////////////////////////////////////////////////////
// macros

static
void
d_define(struct lineplace *lp, struct place *p2, char *line)
{
	size_t pos, argpos;
	struct place p3, p4;

	(void)lp;

	/*
	 * line may be:
	 *    macro expansion
	 *    macro(arg, arg, ...) expansion
	 */

	pos = strcspn(line, " \t\f\v(");
	if (line[pos] == '(') {
		line[pos++] = '\0';
		argpos = pos;
		pos = pos + strcspn(line+pos, "()");
		if (line[pos] == '(') {
			p2->column += pos;
			complain(p2, "Left parenthesis in macro parameters");
			complain_fail();
			return;
		}
		if (line[pos] != ')') {
			p2->column += pos;
			complain(p2, "Unclosed macro parameter list");
			complain_fail();
			return;
		}
		line[pos++] = '\0';
#if 0
		if (!strchr(ws, line[pos])) {
			p2->column += pos;
			complain(p2, "Trash after macro parameter list");
			complain_fail();
			return;
		}
#endif
	} else if (line[pos] == '\0') {
		argpos = 0;
	} else {
		line[pos++] = '\0';
		argpos = 0;
	}

	pos += strspn(line+pos, ws);

	p3 = *p2;
	p3.column += argpos;

	p4 = *p2;
	p4.column += pos;

	if (argpos) {
		debuglog(&lp->current, "Defining %s()", line);
		macro_define_params(p2, line, &p3,
				    line + argpos, &p4,
				    line + pos);
	} else {
		debuglog(&lp->current, "Defining %s", line);
		macro_define_plain(p2, line, &p4, line + pos);
	}
}

static
void
d_undef(struct lineplace *lp, struct place *p2, char *line)
{
	(void)lp;

	uncomment(line);
	oneword("#undef", p2, line);
	debuglog(&lp->current, "Undef %s", line);
	macro_undef(line);
}

////////////////////////////////////////////////////////////
// includes

static
bool
tryinclude(struct place *p, char *line)
{
	size_t len;

	len = strlen(line);
	if (len > 2 && line[0] == '"' && line[len-1] == '"') {
		line[len-1] = '\0';
		debuglog(p, "Entering include file \"%s\"", line+1);
		file_readquote(p, line+1);
		debuglog(p, "Leaving include file \"%s\"", line+1);
		line[len-1] = '"';
		return true;
	}
	if (len > 2 && line[0] == '<' && line[len-1] == '>') {
		line[len-1] = '\0';
		debuglog(p, "Entering include file <%s>", line+1);
		file_readbracket(p, line+1);
		debuglog(p, "Leaving include file <%s>", line+1);
		line[len-1] = '>';
		return true;
	}
	return false;
}

static
void
d_include(struct lineplace *lp, struct place *p2, char *line)
{
	char *text;
	size_t oldlen;

	uncomment(line);
	if (tryinclude(&lp->current, line)) {
		return;
	}
	text = macroexpand(p2, line, strlen(line), false);

	oldlen = strlen(text);
	uncomment(text);
	/* trim to fit, so the malloc debugging won't complain */
	text = dorealloc(text, oldlen + 1, strlen(text) + 1);

	if (tryinclude(&lp->current, text)) {
		dostrfree(text);
		return;
	}
	complain(&lp->current, "Illegal #include directive");
	complain(&lp->current, "Before macro expansion: #include %s", line);
	complain(&lp->current, "After macro expansion: #include %s", text);
	dostrfree(text);
	complain_fail();
}

static
void
d_line(struct lineplace *lp, struct place *p2, char *line)
{
	char *text;
	size_t oldlen;
	unsigned long val;
	char *moretext;
	size_t moretextlen;
	char *filename;

	text = macroexpand(p2, line, strlen(line), true);

	oldlen = strlen(text);
	uncomment(text);
	/* trim to fit, so the malloc debugging won't complain */
	text = dorealloc(text, oldlen + 1, strlen(text) + 1);

	/*
	 * What we should have here: either 1234 "file.c",
	 * or just 1234.
	 */

	errno = 0;
	val = strtoul(text, &moretext, 10);
	if (errno) {
		complain(&lp->current, "No line number in #line directive");
		goto fail;
	}
#if UINT_MAX < ULONG_MAX
	if (val > UINT_MAX) {
		complain(&lp->current,
			 "Line number in #line directive too large");
		goto fail;
	}
#endif
	moretext += strspn(moretext, ws);
	moretextlen = strlen(moretext);
	lp->current.column += (moretext - text);

	if (moretextlen > 2 &&
	    moretext[0] == '"' && moretext[moretextlen-1] == '"') {
		filename = dostrndup(moretext+1, moretextlen-2);
		place_changefile(&lp->nextline, filename);
		dostrfree(filename);
	}
	else if (moretextlen > 0) {
		complain(&lp->current,
			 "Invalid file name in #line directive");
		goto fail;
	}

	lp->nextline.line = val;
	dostrfree(text);
	return;

fail:
	complain(&lp->current, "Before macro expansion: #line %s", line);
	complain(&lp->current, "After macro expansion: #line %s", text);
	complain_fail();
	dostrfree(text);
}

////////////////////////////////////////////////////////////
// messages

static
void
d_warning(struct lineplace *lp, struct place *p2, char *line)
{
	char *msg;

	msg = macroexpand(p2, line, strlen(line), false);
	complain(&lp->current, "#warning: %s", msg);
	if (mode.werror) {
		complain_fail();
	}
	dostrfree(msg);
}

static
void
d_error(struct lineplace *lp, struct place *p2, char *line)
{
	char *msg;

	msg = macroexpand(p2, line, strlen(line), false);
	complain(&lp->current, "#error: %s", msg);
	complain_fail();
	dostrfree(msg);
}

////////////////////////////////////////////////////////////
// other

static
void
d_pragma(struct lineplace *lp, struct place *p2, char *line)
{
	(void)p2;

	complain(&lp->current, "#pragma %s", line);
	complain_fail();
}

////////////////////////////////////////////////////////////
// directive table

static const struct {
	const char *name;
	bool ifskip;
	void (*func)(struct lineplace *, struct place *, char *line);
} directives[] = {
	{ "define",  true,  d_define },
	{ "elif",    false, d_elif },
	{ "else",    false, d_else },
	{ "endif",   false, d_endif },
	{ "error",   true,  d_error },
	{ "if",      false, d_if },
	{ "ifdef",   false, d_ifdef },
	{ "ifndef",  false, d_ifndef },
	{ "include", true,  d_include },
	{ "line",    true,  d_line },
	{ "pragma",  true,  d_pragma },
	{ "undef",   true,  d_undef },
	{ "warning", true,  d_warning },
};
static const unsigned numdirectives = HOWMANY(directives);

static
void
directive_gotdirective(struct lineplace *lp, char *line)
{
	struct place p2;
	size_t len, skip;
	unsigned i;

	p2 = lp->current;
	for (i=0; i<numdirectives; i++) {
		len = strlen(directives[i].name);
		if (!strncmp(line, directives[i].name, len) &&
		    strchr(ws, line[len])) {
			if (directives[i].ifskip && !ifstate->curtrue) {
				return;
			}
			skip = len + strspn(line+len, ws);
			p2.column += skip;
			line += skip;

			len = strlen(line);
			len = notrailingws(line, len);
			if (len < strlen(line)) {
				line[len] = '\0';
			}
			directives[i].func(lp, &p2, line);
			return;
		}
	}
	/* ugh. allow # by itself, including with a comment after it */
	uncomment(line);
	if (line[0] == '\0') {
		return;
	}

	skip = strcspn(line, ws);
	complain(&lp->current, "Unknown directive #%.*s", (int)skip, line);
	complain_fail();
}

/*
 * Check for nested comment delimiters in LINE.
 */
static
size_t
directive_scancomments(const struct lineplace *lp, char *line, size_t len)
{
	size_t pos;
	bool incomment;
	struct place p2;

	p2 = lp->current;
	incomment = 0;
	for (pos = 0; pos+1 < len; pos++) {
		if (line[pos] == '/' && line[pos+1] == '*') {
			if (incomment) {
				complain(&p2, "Warning: %c%c within comment",
					 '/', '*');
				if (mode.werror) {
					complain_failed();
				}
			} else {
				incomment = true;
			}
			pos++;
		} else if (line[pos] == '*' && line[pos+1] == '/') {
			if (incomment) {
				incomment = false;
			} else {
				/* stray end-comment; should we care? */
			}
			pos++;
		}
		if (line[pos] == '\n') {
			p2.line++;
			p2.column = 0;
		} else {
			p2.column++;
		}
	}

	/* multiline comments are supposed to arrive in a single buffer */
	assert(!incomment);
	return len;
}

void
directive_gotline(struct lineplace *lp, char *line, size_t len)
{
	size_t skip;

	if (warns.nestcomment) {
		directive_scancomments(lp, line, len);
	}

	/* check if we have a directive line (# exactly in column 0) */
	if (len > 0 && line[0] == '#') {
		skip = 1 + strspn(line + 1, ws);
		assert(skip <= len);
		lp->current.column += skip;
		assert(line[len] == '\0');
		directive_gotdirective(lp, line+skip /*, length = len-skip */);
		lp->current.column += len-skip;
	} else if (ifstate->curtrue) {
		macro_sendline(&lp->current, line, len);
		lp->current.column += len;
	}
}

void
directive_goteof(struct place *p)
{
	while (ifstate->prev != NULL) {
		complain(p, "Missing #endif");
		complain(&ifstate->startplace, "...opened at this point");
		complain_failed();
		ifstate_pop();
	}
	macro_sendeof(p);
}

////////////////////////////////////////////////////////////
// module initialization

void
directive_init(void)
{
	ifstate = ifstate_create(NULL, NULL, true);
}

void
directive_cleanup(void)
{
	assert(ifstate->prev == NULL);
	ifstate_destroy(ifstate);
	ifstate = NULL;
}
