#include <tools/ccanlint/ccanlint.h>
#include <tools/tools.h>
#include <ccan/talloc/talloc.h>
#include <ccan/str/str.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <limits.h>
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
#include <err.h>
#include <string.h>
#include <ctype.h>

static const char explain[] 
= "Headers usually start with the C preprocessor lines to prevent multiple\n"
  "inclusions.  These look like the following:\n"
  "#ifndef MY_HEADER_H\n"
  "#define MY_HEADER_H\n"
  "...\n"
  "#endif /* MY_HEADER_H */\n";

static char *report_idem(struct ccan_file *f, char *sofar)
{
	struct line_info *line_info;
	unsigned int i, first_preproc_line;
	const char *line, *sym;

	line_info = get_ccan_line_info(f);
	if (f->num_lines < 3)
		/* FIXME: We assume small headers probably uninteresting. */
		return sofar;

	for (i = 0; i < f->num_lines; i++) {
		if (line_info[i].type == DOC_LINE
		    || line_info[i].type == COMMENT_LINE)
			continue;
		if (line_info[i].type == CODE_LINE)
			return talloc_asprintf_append(sofar,
			      "%s:%u:expect first non-comment line to be #ifndef.\n", f->name, i+1);
		else if (line_info[i].type == PREPROC_LINE)
			break;
	}

	/* No code at all?  Don't complain. */
	if (i == f->num_lines)
		return sofar;

	first_preproc_line = i;
	for (i = first_preproc_line+1; i < f->num_lines; i++) {
		if (line_info[i].type == DOC_LINE
		    || line_info[i].type == COMMENT_LINE)
			continue;
		if (line_info[i].type == CODE_LINE)
			return talloc_asprintf_append(sofar,
			      "%s:%u:expect second line to be #define.\n", f->name, i+1);
		else if (line_info[i].type == PREPROC_LINE)
			break;
	}

	/* No code at all?  Weird. */
	if (i == f->num_lines)
		return sofar;

	/* We expect a condition on this line. */
	if (!line_info[i].cond) {
		return talloc_asprintf_append(sofar,
					      "%s:%u:expected #ifndef.\n",
					      f->name, first_preproc_line+1);
	}

	line = f->lines[i];

	/* We expect the condition to be ! IFDEF <symbol>. */
	if (line_info[i].cond->type != PP_COND_IFDEF
	    || !line_info[i].cond->inverse) {
		return talloc_asprintf_append(sofar,
					      "%s:%u:expected #ifndef.\n",
					      f->name, first_preproc_line+1);
	}

	/* And this to be #define <symbol> */
	if (!get_token(&line, "#"))
		abort();
	if (!get_token(&line, "define")) {
		return talloc_asprintf_append(sofar,
			      "%s:%u:expected '#define %s'.\n",
			      f->name, i+1, line_info[i].cond->symbol);
	}
	sym = get_symbol_token(f, &line);
	if (!sym || !streq(sym, line_info[i].cond->symbol)) {
		return talloc_asprintf_append(sofar,
			      "%s:%u:expected '#define %s'.\n",
			      f->name, i+1, line_info[i].cond->symbol);
	}

	/* Rest of code should all be covered by that conditional. */
	for (i++; i < f->num_lines; i++) {
		unsigned int val = 0;
		if (line_info[i].type == DOC_LINE
		    || line_info[i].type == COMMENT_LINE)
			continue;
		if (get_ccan_line_pp(line_info[i].cond, sym, &val, NULL)
		    != NOT_COMPILED)
			return talloc_asprintf_append(sofar,
			      "%s:%u:code outside idempotent region.\n",
			      f->name, i+1);
	}

	return sofar;
}

static void *check_idempotent(struct manifest *m)
{
	struct ccan_file *f;
	char *report = NULL;

	list_for_each(&m->h_files, f, list)
		report = report_idem(f, report);

	return report;
}

static const char *describe_idempotent(struct manifest *m, void *check_result)
{
	return talloc_asprintf(check_result, 
			       "Some headers not idempotent:\n"
			       "%s\n%s", (char *)check_result,
			       explain);
}

struct ccanlint idempotent = {
	.name = "Headers are #ifndef/#define idempotent wrapped",
	.total_score = 1,
	.check = check_idempotent,
	.describe = describe_idempotent,
};

REGISTER_TEST(idempotent, NULL);
