#include <sys/types.h>
#include <sys/uio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <dirent.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <getopt.h>

#define BUFFER_SIZE 1024

typedef struct {
	uint16_t mode;
	uint32_t fn_len;
} header;

void error(void);
void add(int fd, char *fn);
void add_directory(int fd, int dfd, char *fn);
int _read(int fd, void *ptr, size_t len, int eof_ok);
char *read_header(int fd, header *h);
void extract(int fd, header *h, char *fn);
void extract_file(int fd, header *h, char *fn);
void extract_link(int fd, char *fn);
void skip(int fd, header *h);
void usage(int r);

void error(void) {
	perror("argh");
	exit(1);
}

void add(int fd, char *fn) {
	int rfd;
	size_t l, total = 0;
	struct stat sb;
	header h;
	char buf[BUFFER_SIZE], *linkbuf = NULL;
	uint64_t len;
	uint32_t lbs = 0;
	printf("%s\n", fn);
	rfd = open(fn, O_RDONLY | O_NOFOLLOW);
	if(rfd == -1)
		error();
	if(fstat(rfd, &sb) == -1)
		error();
	h.fn_len = strlen(fn);
	h.mode = sb.st_mode & 0177777;
	if(write(fd, (void *) &h, sizeof(header)) == -1)
		error();
	if(write(fd, (void *) fn, h.fn_len) == -1)
		error();
	if(S_ISDIR(sb.st_mode)) {
		add_directory(fd, rfd, fn);
	} else if(S_ISLNK(sb.st_mode)) {
		while(1) {
			lbs += BUFFER_SIZE;
			linkbuf = (char *) realloc((void *) linkbuf, lbs);
			if(!linkbuf)
				error();
			l = readlink(fn, linkbuf, lbs);
			if(l == -1)
				error();
			if(l < lbs)
				break;
			lbs -= BUFFER_SIZE - l;
		}
		if(write(fd, (void *) &lbs, sizeof(lbs)) == -1)
			error();
		if(write(fd, (void *) linkbuf, lbs) == -1)
			error();
		free((void *) linkbuf);
	} else {
		len = sb.st_size;
		if(write(fd, (void *) &len, sizeof(len)) == -1)
			error();
		while(l = read(rfd, (void *) buf, BUFFER_SIZE)) {
			if(write(fd, (void *) buf, l) == -1)
				error();
			total += l;
		}
		if(total != len) {
			fprintf(stderr, "argh: file size %i bytes, could only read %i bytes\n", sb.st_size, total);
			exit(1);
		}
	}
	close(rfd);
}

void add_directory(int fd, int dfd, char *fn) {
	struct dirent *entry;
	char *path;
	int fn_len = strlen(fn);
	DIR *d = fdopendir(dfd);
	if(!d)
		error();
	while(entry = readdir(d))
		if(strcmp(entry->d_name, ".") && strcmp(entry->d_name, "..")) {
			path = malloc(fn_len + entry->d_namlen + 2);
			if(!path)
				error();
			strncpy(path, fn, fn_len + 1);
			strncat(path, "/", 1);
			strncat(path, entry->d_name, entry->d_namlen);
			add(fd, path);
			free(path);
		}
	closedir(d);
}

int _read(int fd, void *ptr, size_t len, int eof_ok) {
	size_t l = read(fd, ptr, len);
	if(l < len && !(eof_ok && l == 0)) {
		if(l == -1)
			error();
		fprintf(stderr, "argh: unexpected end of data\n");
		exit(1);
	}
	return l;
}

char *read_header(int fd, header *h) {
	char *fn;
	if(!_read(fd, h, sizeof(header), 1))
		return NULL;
	fn = (char *) malloc(h->fn_len + 1);
	if(!fn)
		error();
	_read(fd, fn, h->fn_len, 0);
	fn[h->fn_len] = 0;
	return fn;
}

void extract(int fd, header *h, char *fn) {
	if(S_ISLNK(h->mode)) {
		extract_link(fd, fn);
	} else if(S_ISDIR(h->mode)) {
		printf("dir %s\n", fn);
		mkdir(fn, h->mode);
	} else
		extract_file(fd, h, fn);
}

void extract_file(int fd, header *h, char *fn) {
	char buf[BUFFER_SIZE];
	size_t c, total = 0;
	int wfd;
	uint64_t size;
	printf("file: %s\n", fn);
	_read(fd, &size, sizeof(size), 0);
	wfd = open(fn, O_WRONLY | O_CREAT | O_TRUNC);
	if(wfd == -1)
		error();
	fchmod(wfd, h->mode);
	while(total < size) {
		c = (BUFFER_SIZE < (size - total)) ? BUFFER_SIZE : size - total;
		_read(fd, (void *) buf, c, 0);
		if(write(wfd, (void *) buf, c) == -1)
			error();
		total += BUFFER_SIZE;
	}
}

void extract_link(int fd, char *fn) {
	uint32_t ls;
	char *buf;
	printf("link: %s\n", fn);
	_read(fd, &ls, sizeof(ls), 0);
	buf = (void *) malloc(ls);
	if(!buf)
		error();
	_read(fd, &buf, ls, 0);
	if(symlink(fn, buf) == -1)
		error();
}

void skip(int fd, header *h) {
	uint64_t l64;
	uint32_t l32;
	if(S_ISLNK(h->mode))
		_read(fd, &l32, sizeof(l32), 0);
	else if(S_ISDIR(h->mode))
		return;
	else
		_read(fd, &l64, sizeof(l64), 0);
	lseek(fd, S_ISLNK(h->mode) ? l32 : l64, SEEK_CUR);
}

void usage(int r) { 
	printf("usage (todo)\n");
	exit(r);
}

int main(int argc, char *argv[]) {
	header h;
	char *fn = NULL, *cfn, action = 0, ch;
	int fd = -1, i;
  static struct option longopts[] = {
		{ "add",      no_argument,       NULL, 'a' },
		{ "extract",  no_argument,       NULL, 'x' },
		{ "list",     no_argument,       NULL, 'l' },
		{ "file",     required_argument, NULL, 'f' },
		{ "help",     no_argument,       NULL, 'h' },
		{ NULL,       0,                 NULL, 0 }
	};
	while((ch = getopt_long(argc, argv, "axlf:h", longopts, NULL)) != -1)
		switch(ch) {
			case 'a':
			case 'x':
			case 'l':
				if(action)
					usage(1);
				action = ch;
				break;
			case 'f':
				if(fn)
					usage(1);
				fn = optarg;
				break;
		}
	argc -= optind;
	argv += optind;
	if(!fn)
		usage(1);
	switch(action) {
		case 'a':
			if(!argc)
				usage(1);
			fd = open(fn, O_WRONLY | O_CREAT | O_EXCL);
			if(fd == -1)
				error();
			fchmod(fd, 0644);
			for(i = 0; i < argc; i++)
				add(fd, argv[i]);
			break;
		case 'l':
		case 'x':
			if(argc)
				usage(1);
			fd = open(fn, O_RDONLY);
			if(fd == -1)
				error();
			while(cfn = read_header(fd, &h)) {
				if(action == 'x')
					extract(fd, &h, cfn);
				else
					skip(fd, &h);
				free((void *) cfn);
			}
			break;
		case 'h':
			usage(0);
		default:
			usage(1);
	}
	close(fd);
	return 0;
}

