/*****************************************************************************

	unsort - reorder files semi-randomly
	Copyright (C) 2007, 2008  Wessel Dankers <wsl@fruit.je>

	This program is free software: you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation, either version 3 of the License, or
	(at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program.  If not, see <http://www.gnu.org/licenses/>.

*****************************************************************************/

#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <limits.h>
#include <sys/stat.h>
#include <sys/mman.h>

#include "error.h"
#include "filebuf.h"

const filebuf_t filebuf_0 = {0};

#define filebuf_off(buf, off) (((uint8_t *)(buf)) + (size_t)(off))

static int filebuf_tmp(void) {
	FILE *fh;
	int fd;
	fh = tmpfile();
	if(!fh)
		exit_perror(ERROR_SYSTEM, "Can't create temporary file");
	fd = dup(fileno(fh));
	if(fd == -1)
		exit_perror(ERROR_SYSTEM, "Can't dup(%d)", fileno(fh));
	fclose(fh);
	return fd;
}

static void write_all(int fd, void *buf, size_t len) {
	ssize_t r;
	while(len) {
		r = write(fd, buf, len);
		if(!r)
			exit_error(ERROR_SYSTEM, "Can't write to fd %d", fd);
		if(r < 0)
			exit_perror(ERROR_SYSTEM, "Can't write to fd %d", fd);
		buf = filebuf_off(buf, r);
		len -= (size_t)r;
	}
}

static bool filebuf_mmap(int fd, filebuf_t *fb, size_t len) {
	void *buf;
	buf = mmap(NULL, len, PROT_READ, MAP_SHARED, fd, (ssize_t)0);
	if(buf == MAP_FAILED)
		return true;
	fb->buf = buf;
	fb->len = len;
	return false;
}

#define BUFSIZE 1048576
#define MINREAD 4096

static void *streambuf = NULL;
static size_t streamleft = BUFSIZE;

static void filebuf_stream(int fd, filebuf_t *fb) {
	void *buf;
	size_t len = 0, left, edge, offset = 0;
	ssize_t r;
	int tmp;
	static long pg = -1;

	if(!streambuf)
		streambuf = xalloc(streamleft);

	buf = streambuf;
	left = streamleft;

	while(left >= MINREAD) {
		r = read(fd, filebuf_off(buf, len), left);
		if(r < 0)
			exit_perror(ERROR_SYSTEM, "Can't read from fd %d", fd);
		if(!r) {
			if(len) {
				streambuf = filebuf_off(streambuf, len);
				streamleft -= len;
			} else {
				buf = NULL;
			}
			fb->buf = buf;
			fb->len = len;
			return;
		}
		len += (size_t)r;
		left -= (size_t)r;
	}

	tmp = filebuf_tmp();
	write_all(tmp, buf, len);
	buf = NULL;

	if(pg == -1)
		pg = sysconf(_SC_PAGESIZE);

	left = 0;
	for(;;) {
		if(left < MINREAD) {
			if(buf && munmap(buf, (size_t)BUFSIZE) == -1)
				exit_perror(ERROR_SYSTEM, "Can't munmap %d bytes", BUFSIZE);
			edge = len % (size_t)pg;
			offset = len - edge;
			left = BUFSIZE - edge;
			if(ftruncate(tmp, (off_t)(offset + BUFSIZE)) == -1)
				exit_perror(ERROR_SYSTEM, "Can't extend fd %d to %ld bytes", tmp, (long)(offset + BUFSIZE));
			buf = mmap(NULL, (size_t)BUFSIZE, PROT_WRITE, MAP_SHARED, tmp, (off_t)offset);
			if(buf == MAP_FAILED)
				exit_perror(ERROR_SYSTEM, "Can't mmap fd %d for writing", fd);
		}
		r = read(fd, (char *)buf + len - offset, left);
		if(!r)
			break;
		if(r < 0)
			exit_perror(ERROR_SYSTEM, "Can't read from fd %d", tmp);
		len += (size_t)r;
		left -= (size_t)r;
	}

	if(buf && munmap(buf, (size_t)BUFSIZE) == -1)
		exit_perror(ERROR_SYSTEM, "Can't munmap %d bytes", BUFSIZE);
	if(ftruncate(tmp, (off_t)len) == -1)
		exit_perror(ERROR_SYSTEM, "Can't truncate fd %d to %ld bytes", tmp, (long)len);

	if(filebuf_mmap(tmp, fb, len))
		exit_perror(ERROR_SYSTEM, "Can't mmap fd %d", tmp);

	close(tmp);
}

void filebuf_init(filebuf_t *fb, int fd) {
	struct stat st;

	if(fstat(fd, &st) || !st.st_size || filebuf_mmap(fd, fb, (size_t)st.st_size))
		filebuf_stream(fd, fb);
}
