/* build.c
 *
 * Copyright (c) 2014-2017 Tharre
 *
 * This software may be modified and distributed under the terms
 * of the MIT license.  See the LICENSE file for details.
 */

#define _XOPEN_SOURCE 700
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <time.h>
#include <inttypes.h>

#include <libgen.h> /* dirname(), basename() */

#include "sha1.h"
#include "build.h"
#include "util.h"
#include "filepath.h"
#include "DSV.h"
#define _FILENAME "build.c"
#include "dbg.h"

typedef struct do_attr {
	char *specific;
	char *general;
	char *chosen;
} do_attr;

typedef struct dep_info {
	const char *target;
	char *path;
	unsigned char *hash;
	uint32_t magic;
	struct timespec ctime;
	int32_t flags;
#define DEP_SOURCE (1 << 1)
} dep_info;

static do_attr *get_doscripts(const char *target);
static void free_do_attr(do_attr *thing);
static char **parse_shebang(char *target, char *doscript, char *temp_output);
static char **parsecmd(char *cmd, size_t *i, size_t keep_free);
static char *get_relpath(const char *target);
static char *xrealpath(const char *path);
static char *get_dep_path(const char *target);
static void store_dep_information(dep_info *dep);
static int handle_ident(dep_info *dep, int ident);
static int handle_c(dep_info *dep);
static void update_dep_info(dep_info *dep, const char *target);


/* Runs the given doscript, on the given target. Returns nonzero if the script
   produced the target and zero otherwise. */
static bool run_doscript(const char *doscript, const char *target) {
	char *reltarget = get_relpath(target);
	printf("\033[32mredo  \033[1m\033[37m%s\033[0m\n", reltarget);
	free(reltarget);

	char *temp_output = concat(target, ".redoing.tmp");

	pid_t pid = fork();
	if (pid == -1) {
		fatal("redo: failed to fork() new process");
	} else if (pid == 0) {
		/* child */
		char *abstemp = xrealpath(temp_output);
		if (!abstemp)
			fatal("redo: failed to get realpath() of %s", temp_output);

		/* change directory to our target */
		char *dirc = xstrdup(doscript);
		char *ddoscript = dirname(dirc);
		if (chdir(ddoscript) == -1)
			fatal("redo: failed to change directory to %s", ddoscript);

		free(dirc);

		char **argv = parse_shebang(xbasename(target),
				xbasename(doscript), abstemp);

		/* set "REDO_PARENT_TARGET" */
		if (setenv("REDO_PARENT_TARGET", target, 1))
			fatal("redo: failed to setenv() REDO_PARENT_TARGET to %s",
					target);

		execv(argv[0], argv);

		/* execv should never return */
		fatal("redo: failed to replace child process with %s", argv[0]);
	}

	/* parent */
	int status;
	if (waitpid(pid, &status, 0) == -1)
		fatal("redo: waitpid() failed");

	/* check how our child exited */
	if (WIFEXITED(status)) {
		if (WEXITSTATUS(status))
			die("redo: invoked .do script %s failed: %d\n", doscript,
			    WEXITSTATUS(status));
	} else {
		/* something very wrong happened with the child */
		die("redo: invoked .do script did not terminate correctly\n");
	}

	if (fsize(temp_output) > 0) {
		if (rename(temp_output, target))
			fatal("redo: failed to rename %s to %s", temp_output, target);

		free(temp_output);
		return 1;
	} else {
		if (remove(temp_output) && errno != ENOENT)
			fatal("redo: failed to remove %s", temp_output);

		free(temp_output);
		return 0;
	}
}

/* Let target depend on dependency */
static void depends_on(const char *target) {
	dep_info dep = { .path = get_dep_path(target) };

	if (!fexists(dep.path)) {
		update_dep_info(&dep, target);
		store_dep_information(&dep);
		free(dep.hash);
	}
	free(dep.path);
}

/* Rebuild target dependency, returning nonzero if it's changed in the process,
  or zero otherwise. */
int rebuild(dep_info *dep) {
	if (dep->flags & (1 << DEP_SOURCE))
		die("Source %s not found and will not be rebuilt.\n", dep->target);

	/* remove old dependency record */
	if (remove(dep->path) && errno != ENOENT)
		fatal("redo: failed to remove %s", dep->path);

	char *prereq = concat(dep->path, ".prereq");
	if (remove(prereq) && errno != ENOENT)
		fatal("redo: failed to remove %s", prereq);

	free(prereq);

	do_attr *doscripts = get_doscripts(dep->target);
	if (!doscripts->chosen) {
		/* if no do script exists, it must be a source (and thus exist) */
		if (!fexists(dep->target))
			die("%s couldn't be built as no suitable .do script exists\n",
					dep->target);

		dep->flags |= DEP_SOURCE;
	} else if (!run_doscript(doscripts->chosen, dep->target)) {
		/* do script produced no output */
		free_do_attr(doscripts);
		return 1;
	} else {
		depends_on(doscripts->chosen);

		/* depend on the .do script */
		add_prereq_path(doscripts->chosen, dep->target, 'c');

		/* redo-ifcreate on specific if general was chosen */
		if (doscripts->general == doscripts->chosen)
			add_prereq_path(doscripts->specific, dep->target, 'e');
	}

	uint32_t old_magic = dep->magic;
	unsigned char *old_hash = dep->hash;
	update_dep_info(dep, dep->target);

	if (old_hash && memcmp(dep->hash, old_hash, 20)) {
		/* if the hash didn't change, don't change the magic number */
		dep->magic = old_magic;
	}
	free(old_hash);

	store_dep_information(dep);
	free_do_attr(doscripts);
	return 0;
}

/* Read and parse shebang and return an argv-like pointer array containing the
   arguments. If no valid shebang could be found assume "/bin/sh -e" instead. */
static char **parse_shebang(char *target, char *doscript, char *temp_output) {
	FILE *fp = fopen(doscript, "rb");
	if (!fp)
		fatal("redo: failed to open %s", doscript);

	char *buf = xmalloc(1024);

	buf[ fread(buf, 1, 1023, fp) ] = '\0';
	if (ferror(fp))
		fatal("redo: failed to read from %s", doscript);

	fclose(fp);

	char **argv;
	size_t i = 0;
	if (buf[0] == '#' && buf[1] == '!') {
		argv = parsecmd(&buf[2], &i, 5);
	} else {
		argv = xmalloc(7 * sizeof(char*));
		argv[i++] = "/bin/sh";
		argv[i++] = "-e";
	}

	argv[i++] = doscript;
	argv[i++] = target;
	char *basename = remove_ext(target);
	argv[i++] = basename;
	argv[i++] = temp_output;
	argv[i] = NULL;

	return argv;
}

/* Breaks cmd at spaces and stores a pointer to each argument in the returned
   array. The index i is incremented to point to the next free pointer. The
   returned array is guaranteed to have at least keep_free entries left. */
static char **parsecmd(char *cmd, size_t *i, size_t keep_free) {
	size_t argv_len = 16;
	char **argv = xmalloc(argv_len * sizeof(char*));
	size_t j = 0;
	bool prev_space = true;
	for (;; ++j) {
		switch (cmd[j]) {
		case ' ':
			cmd[j] = '\0';
			prev_space = true;
			break;
		case '\n':
		case '\r':
			cmd[j] = '\0';
		case '\0':
			return argv;
		default:
			if (!prev_space)
				break;
			/* check if we have enough space */
			while (*i+keep_free >= argv_len) {
				argv_len *= 2;
				argv = xrealloc(argv, argv_len * sizeof(char*));
			}

			prev_space = false;
			argv[*i] = &cmd[j];
			++*i;
		}
	}
}

/* Return a struct with all the possible .do scripts, and the chosen one. */
static do_attr *get_doscripts(const char *target) {
	do_attr *ds = xmalloc(sizeof(do_attr));

	ds->specific = concat(target, ".do");
	char *dirc = xstrdup(target);
	char *dt = dirname(dirc);

	ds->general = concat(dt, "/default", take_extension(target), ".do");
	free(dirc);

	if (fexists(ds->specific))
		ds->chosen = ds->specific;
	else if (fexists(ds->general))
		ds->chosen = ds->general;
	else
		ds->chosen = NULL;

	return ds;
}

/* Free the do_attr struct. */
static void free_do_attr(do_attr *thing) {
	free(thing->specific);
	free(thing->general);
	free(thing);
}

/* Custom version of realpath that doesn't fail if the last part of path
   doesn't exist and allocates memory for the result itself. */
static char *xrealpath(const char *path) {
	char *dirc = xstrdup(path);
	char *dname = dirname(dirc);
	char *absdir = realpath(dname, NULL);

	char *abstarget = NULL;
	if (absdir)
		abstarget = concat(absdir, "/", xbasename(path));

	free(dirc);
	free(absdir);
	return abstarget;
}

/* Return the relative path against "REDO_ROOT" of target. Returns NULL if
   realpath() fails. */
static char *get_relpath(const char *target) {
	char *root = getenv("REDO_ROOT");
	char *abstarget = xrealpath(target);

	char *path;
	if (!abstarget) {
		path = xstrdup(relpath((char*)target, root));
	} else {
		path = xstrdup(relpath(abstarget, root));
		free(abstarget);
	}

	return path;
}

/* Return the dependency record path of target. */
static char *get_dep_path(const char *target) {
	char *root = getenv("REDO_ROOT");
	char *dep_path;
	char *reltarget = get_relpath(target);

	char *redodir = is_absolute(reltarget) ? "/.redo/abs/" : "/.redo/rel/";
	dep_path = concat(root, redodir, reltarget);

	/* create directory */
	mkpath(dep_path, 0755); /* TODO: should probably be somewhere else */

	free(reltarget);
	return dep_path;
}

/* Declare that parent depends on target in the specific way ident.
 * Parent must be a path pointing to a (maybe non-existent) file in a valid,
 * exisiting directory. Target can be any string.
 * This relation is saved in the dependency store like this:
 * .redo/{abs,rel}/<parent>.prereq:
 *     <ident>:<target>
 */
void add_prereq(const char *target, const char *parent, int ident) {
	char *base_path = get_dep_path(parent);

	char *dep_path = concat(base_path, ".prereq");

	int fd = open(dep_path, O_WRONLY | O_APPEND | O_CREAT, 0644);
	if (fd < 0)
		fatal("redo: failed to open %s", dep_path);

	size_t bufsize = strlen(target)*2 + 3;
	char *buf = xmalloc(bufsize);

	buf[0] = ident;
	buf[1] = ':';
	size_t encoded_len = encode_string(buf+2, target) + 3;
	buf[encoded_len-1] = '\n';

	if (write(fd, buf, encoded_len) < (ssize_t) encoded_len)
		fatal("redo: failed to write to %s", dep_path);

	if (close(fd))
		fatal("redo: failed to close %s", dep_path);

	free(buf);
	free(dep_path);
	free(base_path);
}

/* Works like add_prereq(), except that it uses the relative path of target to
 * REDO_ROOT instead of just the target. Target must be a path pointing to a
 * (maybe non-existant) file in a valid existing directory.
 */
void add_prereq_path(const char *target, const char *parent, int ident) {
	char *reltarget = get_relpath(target);

	add_prereq(reltarget, parent, ident);
	free(reltarget);
}

static uint32_t get_magic_number() {
	uint32_t magic;
	if (sscanf(getenv("REDO_MAGIC"), "%"SCNu32, &magic) < 1)
		die("redo: failed to parse REDO_MAGIC (%s)", getenv("REDO_MAGIC"));

	return magic;
}

/* Update hash, ctime and magic number stored in the given dep_info struct */
static void update_dep_info(dep_info *dep, const char *target) {
	FILE *fp = fopen(target, "rb");
	if (!fp)
		fatal("redo: failed to open %s", target);

	dep->hash = hash_file(fp);
	struct stat st;
	if (fstat(fileno(fp), &st))
		fatal("redo: failed to aquire stat() %s", target);

	dep->ctime = st.st_ctim;
	dep->magic = get_magic_number();
	fclose(fp);
}

/* Store the given dependency information in the filesystem. */
static void store_dep_information(dep_info *dep) {
	FILE *fd = fopen(dep->path, "w+");
	if (!fd)
		fatal("redo: failed to open %s", dep->path);

	char hash[41];
	sha1_to_hex(dep->hash, hash);
	char *flags = (dep->flags & DEP_SOURCE) ? "s" : "l";

	/* TODO: casting time_t to long long is probably not entirely portable */
	if (fprintf(fd, "%s:%lld.%.9ld:%"PRIu32":%s\n", hash, (long long)dep->ctime.tv_sec,
				dep->ctime.tv_nsec, dep->magic, flags) < 0)
		fatal("redo: failed to write to %s", dep->path);

	if (fclose(fd))
		fatal("redo: failed to close %s", dep->path);
}

int update_target(const char *target, int ident) {
	dep_info dep = {
		.target = target,
		.path = get_dep_path(target),
	};

	int retval = handle_ident(&dep, ident);
	free(dep.path);
	free(dep.hash);

	return retval;
}

static int handle_ident(dep_info *dep, int ident) {
	switch(ident) {
	case 'a':
		return rebuild(dep);
	case 'e':
		if (fexists(dep->target))
			return rebuild(dep);

		return 0;
	case 'c':
		return handle_c(dep);
	default:
		die("redo: unknown identifier '%c'\n", ident);
	}
}

static int handle_c(dep_info *dep) {
	struct dsv_ctx ctx_dep, ctx_prereq;
	int retval = 0;

	/* check if the dependency record exists and is valid */
	FILE *depfd = fopen(dep->path, "rb");
	if (!depfd) {
		if (errno == ENOENT) {
			/* dependency record does not exist */
			log_warn("%s ood: dependency record doesn't exist\n", dep->target);
			return rebuild(dep);
		} else {
			fatal("redo: failed to open %s", dep->path);
		}
	}

	dsv_init(&ctx_dep, 4);

	if (dsv_parse_file(&ctx_dep, depfd)) {
		/* parsing failed */
		log_info("%s ood: parsing of dependency file failed\n", dep->target);
		fclose(depfd);
		retval = rebuild(dep);
		goto exit;
	}
	fclose(depfd);

	/* validate magic number */
	if (sscanf(ctx_dep.fields[2], "%"SCNu32, &dep->magic) < 1) {
		retval = rebuild(dep);
		goto exit2;
	}

	/* parse flags */
	if (ctx_dep.fields[3][0] == 's')
		dep->flags |= DEP_SOURCE;

	FILE *targetfd = fopen(dep->target, "rb");
	if (!targetfd) {
		if (errno != ENOENT) {
			fatal("redo: failed to open %s", dep->target);
		} else {
			log_info("%s ood: target file nonexistent\n", dep->target);
			retval = rebuild(dep);
			goto exit2;
		}
	}

	if (dep->magic == get_magic_number()) {
		/* magic number matches */
		log_info("%s ood: magic number matches\n", dep->target);
		retval = 1;
		goto exit3;
	}

	struct stat curr_st;
	if (sscanf(ctx_dep.fields[1], "%lld.%ld", (long long*)&dep->ctime.tv_sec,
				&dep->ctime.tv_nsec) < 2) {
		/* ctime parsing failed */
		log_info("%s ood: ctime parsing failed\n", dep->target);
		retval = rebuild(dep);
		goto exit3;
	}

	if (fstat(fileno(targetfd), &curr_st))
		fatal("redo: failed to stat() %s", dep->target);

	/* store the hash now, as rebuild() will need it for comparison */
	unsigned char *old_hash = xmalloc(20);
	hex_to_sha1(ctx_dep.fields[0], old_hash); /* TODO: error checking */
	dep->hash = old_hash;

	if (dep->ctime.tv_sec != curr_st.st_ctim.tv_sec
			|| dep->ctime.tv_nsec != curr_st.st_ctim.tv_nsec) {
		/* ctime doesn't match */
		dep->ctime = curr_st.st_ctim;

		/* so check the hash */
		dep->hash = hash_file(targetfd);

		if (memcmp(old_hash, dep->hash, 20)) {
			/* target hash doesn't match */
			log_info("%s ood: hashes don't match\n", dep->target);
			free(old_hash);
			retval = rebuild(dep);
			goto exit3;
		}
		free(old_hash);

		/* update ctime */
		store_dep_information(dep);
	}

	/* make sure all prereq dependencies are met */
	char *prereq_path = concat(dep->path, ".prereq");
	FILE *prereqfd = fopen(prereq_path, "rb");
	if (!prereqfd) {
		if (errno != ENOENT)
			fatal("redo: failed to open %s", prereq_path);

		/* no .prereq file exists; so we don't do anything */
		goto exit4;
	}

	dsv_init(&ctx_prereq, 2);

	while (!dsv_parse_file(&ctx_prereq, prereqfd)) {
		char *target = make_abs(getenv("REDO_ROOT"), ctx_prereq.fields[1]);
		int outofdate = update_target(target, ctx_prereq.fields[0][0]);

		free(target);
		free(ctx_prereq.fields[0]);
		free(ctx_prereq.fields[1]);

		if (outofdate) {
			log_info("%s ood: subtarget(s) ood\n", dep->target);
			retval = rebuild(dep);
			break;
		}
	}

	dsv_free(&ctx_prereq);
	fclose(prereqfd);
exit4:
	free(prereq_path);
exit3:
	fclose(targetfd);
exit2:
	for (size_t i = 0; i < ctx_dep.fields_count; ++i)
		free(ctx_dep.fields[i]);
exit:
	dsv_free(&ctx_dep);
	return retval;
}