/*
 * Copyright (c) 2015, 2016 Jonas 'Sortie' Termansen.
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 * manifest.c
 * Manifest handling functions.
 */

#include <sys/types.h>

#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <ioleast.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "execute.h"
#include "fileops.h"
#include "manifest.h"

bool has_manifest(const char* manifest)
{
	char* path;
	if ( asprintf(&path, "/tix/manifest/%s", manifest) < 0 )
	{
		warn("asprintf");
		_exit(2);
	}
	bool result = access(path, F_OK) == 0;
	free(path);
	return result;
}

struct hardlink
{
	dev_t dev;
	ino_t ino;
	char* path;
};

void install_manifest(const char* manifest,
                      const char* from_prefix,
                      const char* to_prefix)
{
	printf(" - Installing %s...\n", manifest);
	struct hardlink* hardlinks = NULL;
	size_t hardlinks_used = 0;
	size_t hardlinks_length = 0;
	size_t buffer_size = 1 << 16;
	char* buffer = malloc(buffer_size);
	if ( !buffer )
	{
		warn("malloc");
		_exit(2);
	}
	mode_t old_umask = umask(0000);
	char* inmanifest;
	if ( asprintf(&inmanifest, "%s/tix/manifest/%s", from_prefix, manifest) < 0 )
	{
		warn("asprintf");
		_exit(2);
	}
	char* outmanifest;
	if ( asprintf(&outmanifest, "%s/tix/manifest/%s", to_prefix, manifest) < 0 )
	{
		warn("asprintf");
		_exit(2);
	}
	FILE* fpin = fopen(inmanifest, "r");
	if ( !fpin )
	{
		warn("%s", inmanifest);
		_exit(2);
	}
	FILE* fpout = fopen(outmanifest, "w");
	if ( !fpout )
	{
		warn("%s", outmanifest);
		_exit(2);
	}
	char* line = NULL;
	size_t line_size = 0;
	ssize_t line_length;
	while ( 0 < (line_length = getline(&line, &line_size, fpin)) )
	{
		if ( line[line_length-1] == '\n' )
			line[--line_length] = '\0';
		if ( fprintf(fpout, "%s\n", line) < 0 )
		{
			warn("write: %s", outmanifest);
			_exit(2);
		}
		if ( line[0] != '/' )
			continue;
		char* in_path;
		if ( asprintf(&in_path, "%s%s", from_prefix, line) < 0 )
		{
			warn("asprintf");
			_exit(2);
		}
		char* out_path = line;
		if ( asprintf(&out_path, "%s%s", to_prefix, line) < 0 )
		{
			warn("asprintf");
			_exit(2);
		}
		struct stat inst;
		if ( lstat(in_path, &inst) < 0 )
		{
			warn("%s", in_path);
			_exit(2);
		}
		struct hardlink* hardlink = NULL;
		if ( S_ISREG(inst.st_mode) && 2 <= inst.st_nlink )
		{
			for ( size_t i = 0; i < hardlinks_used; i++ )
			{
				if ( hardlinks[i].dev != inst.st_dev ||
				     hardlinks[i].ino != inst.st_ino )
					continue;
				hardlink = &hardlinks[i];
				break;
			}
		}
		if ( hardlink )
		{
			unlink(out_path);
			if ( link(hardlink->path, out_path) < 0 )
			{
				warn("link: %s -> %s", hardlink->path, out_path);
				_exit(2);
			}
		}
		else if ( S_ISDIR(inst.st_mode) )
		{
			if ( mkdir(out_path, inst.st_mode & 07777) < 0 && errno != EEXIST )
			{
				warn("mkdir: %s", out_path);
				_exit(2);
			}
		}
		else if ( S_ISREG(inst.st_mode) )
		{

			int in_fd = open(in_path, O_RDONLY);
			if ( in_fd < 0 )
			{
				warn("%s", in_path);
				_exit(2);
			}
			unlink(out_path);
			int out_fd = open(out_path, O_WRONLY | O_CREAT | O_TRUNC,
			                  inst.st_mode & 07777);
			if ( out_fd < 0 )
			{
				warn("%s", out_path);
				_exit(2);
			}
			while ( true )
			{
				ssize_t amount = read(in_fd, buffer, buffer_size);
				if ( amount < 0 )
				{
					warn("read: %s", in_path);
					_exit(2);
				}
				if ( amount == 0 )
					break;
				if ( writeall(out_fd, buffer, (size_t) amount) < (size_t) amount )
				{
					warn("write: %s", out_path);
					_exit(2);
				}
			}
			close(out_fd);
			close(in_fd);
			if ( 2 <= inst.st_nlink )
			{
				if ( hardlinks_used == hardlinks_length )
				{
					// TODO: Multiplication overflow.
					size_t new_length = hardlinks_length ? 2 * hardlinks_length : 16;
					struct hardlink* new_hardlinks = (struct hardlink*)
						reallocarray(hardlinks, new_length, sizeof(struct hardlink));
					if ( !new_hardlinks )
					{
						warn("malloc");
						_exit(2);
					}
					hardlinks = new_hardlinks;
					hardlinks_length = new_length;
				}
				hardlinks[hardlinks_used].ino = inst.st_ino;
				hardlinks[hardlinks_used].dev = inst.st_dev;
				if ( !(hardlinks[hardlinks_used].path = strdup(out_path)) )
				{
					warn("strdup");
					_exit(2);
				}
				hardlinks_used++;
			}
		}
		else if ( S_ISLNK(inst.st_mode) )
		{
			ssize_t amount = readlink(in_path, buffer, buffer_size - 1);
			if ( amount < 0 )
			{
				warn("readlink: %s", in_path);
				_exit(2);
			}
			buffer[amount] = '\0';
			unlink(out_path);
			if ( symlink(buffer, out_path) < 0 && errno != EEXIST )
			{
				warn("symlink: %s", out_path);
				_exit(2);
			}
		}
		else
		{
			warnx("%s: Don't know how to copy this object", in_path);
			_exit(2);
		}
		free(in_path);
		free(out_path);
	}
	free(line);
	if ( ferror(fpin) )
	{
		warn("%s", inmanifest);
		_exit(2);
	}
	fclose(fpin);
	if ( fclose(fpout) == EOF )
	{
		warn("close: %s", outmanifest);
		_exit(2);
	}
	free(inmanifest);
	free(outmanifest);
	umask(old_umask);
	free(buffer);
	for ( size_t i = 0; i < hardlinks_used; i++ )
		free(hardlinks[i].path);
	free(hardlinks);
}

bool check_installed(const char* path, const char* package)
{
	FILE* fp = fopen(path, "r");
	if ( !fp )
	{
		if ( errno != ENOENT )
			warn("%s", path);
		return false;
	}
	char* line = NULL;
	size_t line_size = 0;
	ssize_t line_length;
	while ( 0 < (line_length = getline(&line, &line_size, fp)) )
	{
		if ( line[line_length-1] == '\n' )
			line[--line_length] = '\0';
		if ( !strcmp(line, package) )
		{
			free(line);
			fclose(fp);
			return true;
		}
	}
	if ( ferror(fp) )
		warn("%s", path);
	free(line);
	fclose(fp);
	return false;
}

static char* shell_single_quote(const char* string)
{
	char* result;
	size_t result_size;
	FILE* fp = open_memstream(&result, &result_size);
	if (!fp)
		return NULL;
	fputc('\'', fp);
	for ( size_t i = 0; string[i]; i++ )
	{
		if ( string[i] == '\'' )
			fputs("\'\\\'\'", fp);
		else
			fputc((unsigned char) string[i], fp);
	}
	fputc('\'', fp);
	fflush(fp);
	int waserr = ferror(fp);
	fclose(fp);
	if (waserr) {
		free(result);
		return NULL;
	}
	return result;
}

static char* sort_file_cmd(const char* file)
{
	char* file_esc = shell_single_quote(file);
	if ( !file_esc )
		return NULL;
	char* cmd;
	if ( asprintf(&cmd, "sort -- %s", file_esc) < 0 )
	{
		free(file_esc);
		return NULL;
	}
	free(file_esc);
	return cmd;
}

void install_ports(const char* from_prefix, const char* to_prefix)
{
	char* inst_in_path;
	char* inst_out_path;
	if ( asprintf(&inst_in_path, "%s/tix/installed.list", from_prefix) < 0 ||
	     asprintf(&inst_out_path, "%s/tix/installed.list", to_prefix) < 0 )
	{
		warn("asprintf");
		_exit(2);
	}
	if ( access_or_die(inst_in_path, F_OK) < 0 )
	{
		free(inst_in_path);
		free(inst_out_path);
		return;
	}
	char* cmd = sort_file_cmd(inst_in_path);
	if ( !cmd )
	{
		warn("sort_file_cmd");
		_exit(2);
	}
	FILE* fp = popen(cmd, "r");
	if ( !fp )
	{
		warn("%s", cmd);
		_exit(2);
	}
	char* line = NULL;
	size_t line_size = 0;
	ssize_t line_length;
	while ( 0 < (line_length = getline(&line, &line_size, fp)) )
	{
		if ( line[line_length-1] == '\n' )
			line[--line_length] = '\0';
		if ( !check_installed(inst_out_path, line) )
		{
			FILE* inst_out_fp = fopen(inst_out_path, "a");
			if ( !inst_out_fp ||
				 fprintf(inst_out_fp, "%s\n", line) < 0 ||
				 fflush(inst_out_fp) == EOF )
			{
				warn("%s", inst_out_path);
				pclose(fp);
				_exit(2);
			}
			fclose(inst_out_fp);
		}
		char* tixinfo_in;
		char* tixinfo_out;
		if ( asprintf(&tixinfo_in, "%s/tix/tixinfo/%s", from_prefix, line) < 0 ||
		     asprintf(&tixinfo_out, "%s/tix/tixinfo/%s", to_prefix, line) < 0 )
		{
			warn("asprintf");
			pclose(fp);
			_exit(2);
		}
		execute((const char*[]) { "cp", "--", tixinfo_in, tixinfo_out, NULL }, "_e");
		free(tixinfo_in);
		free(tixinfo_out);
		install_manifest(line, from_prefix, to_prefix);
	}
	free(line);
	if ( ferror(fp) )
	{
		warn("%s", cmd);
		pclose(fp);
		_exit(2);
	}
	pclose(fp);
	free(cmd);
	free(inst_in_path);
	free(inst_out_path);
}
