#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "rmtfs.h"

#define MAX_CALLERS 10
#define STORAGE_MAX_SIZE (16 * 1024 * 1024)

#define BY_PARTLABEL_PATH "/dev/disk/by-partlabel"

#define MIN(x, y) ((x) < (y) ? (x) : (y))

struct partition {
	const char *path;
	const char *actual;
	const char *partlabel;
};

struct rmtfd {
	unsigned id;
	unsigned node;
	int fd;
	unsigned dev_error;
	const struct partition *partition;

	void *shadow_buf;
	size_t shadow_len;
};

static const char *storage_dir = "/boot";
static int storage_read_only;
static int storage_use_partitions;

static const struct partition partition_table[] = {
	{ "/boot/modem_fs1", "modem_fs1", "modemst1" },
	{ "/boot/modem_fs2", "modem_fs2", "modemst2" },
	{ "/boot/modem_fsc", "modem_fsc", "fsc" },
	{ "/boot/modem_fsg", "modem_fsg", "fsg" },
	{ "/boot/modem_tunning", "modem_tunning", "tunning" },
	{}
};

static struct rmtfd rmtfds[MAX_CALLERS];

static int storage_populate_shadow_buf(struct rmtfd *rmtfd, const char *file);

int storage_init(const char *storage_root, bool read_only, bool use_partitions)
{
	int i;

	if (storage_root)
		storage_dir = storage_root;

	if (use_partitions) {
		if (!storage_root)
			storage_dir = BY_PARTLABEL_PATH;
		storage_use_partitions = true;
	}

	storage_read_only = read_only;

	for (i = 0; i < MAX_CALLERS; i++) {
		rmtfds[i].id = i;
		rmtfds[i].fd = -1;
		rmtfds[i].shadow_buf = NULL;
	}

	return 0;
}

struct rmtfd *storage_open(unsigned node, const char *path)
{
	char *fspath;
	const struct partition *part;
	struct rmtfd *rmtfd = NULL;
	const char *file;
	size_t pathlen;
	int saved_errno;
	int ret;
	int fd;
	int i;

	for (part = partition_table; part->path; part++) {
		if (strcmp(part->path, path) == 0)
			goto found;
	}

	fprintf(stderr, "[RMTFS storage] request for unknown partition '%s', rejecting\n", path);
	return NULL;

found:
	/* Check if this node already has the requested path open */
	for (i = 0; i < MAX_CALLERS; i++) {
		if ((rmtfds[i].fd != -1 || rmtfds[i].shadow_buf) &&
		    rmtfds[i].node == node &&
		    rmtfds[i].partition == part)
			return &rmtfds[i];
	}

	for (i = 0; i < MAX_CALLERS; i++) {
		if (rmtfds[i].fd == -1 && !rmtfds[i].shadow_buf) {
			rmtfd = &rmtfds[i];
			break;
		}
	}
	if (!rmtfd) {
		fprintf(stderr, "[storage] out of free rmtfd handles\n");
		return NULL;
	}

	if (storage_use_partitions)
		file = part->partlabel;
	else
		file = part->actual;

	pathlen = strlen(storage_dir) + strlen(file) + 2;
	fspath = alloca(pathlen);
	snprintf(fspath, pathlen, "%s/%s", storage_dir, file);
	if (!storage_read_only) {
		fd = open(fspath, O_RDWR);
		if (fd < 0) {
			saved_errno = errno;
			fprintf(stderr, "[storage] failed to open '%s' (requested '%s'): %s\n",
					fspath, part->path, strerror(saved_errno));
			errno = saved_errno;
			return NULL;
		}
		rmtfd->fd = fd;
		rmtfd->shadow_len = 0;
	} else {
		ret = storage_populate_shadow_buf(rmtfd, fspath);
		if (ret < 0) {
			saved_errno = errno;
			fprintf(stderr, "[storage] failed to open '%s' (requested '%s'): %s\n",
					fspath, part->path, strerror(saved_errno));
			errno = saved_errno;
			return NULL;
		}
	}

	rmtfd->node = node;
	rmtfd->partition = part;

	return rmtfd;
}

void storage_close(struct rmtfd *rmtfd)
{
	if (rmtfd->fd >= 0) {
		close(rmtfd->fd);
		rmtfd->fd = -1;
	}

	free(rmtfd->shadow_buf);
	rmtfd->shadow_buf = NULL;
	rmtfd->shadow_len = 0;

	rmtfd->partition = NULL;
}

struct rmtfd *storage_get(unsigned node, int caller_id)
{
	struct rmtfd *rmtfd;

	if (caller_id >= MAX_CALLERS)
		return NULL;

	rmtfd = &rmtfds[caller_id];
	if (rmtfd->node != node)
		return NULL;

	return rmtfd;
}

int storage_get_caller_id(const struct rmtfd *rmtfd)
{
	return rmtfd->id;
}

int storage_get_error(const struct rmtfd *rmtfd)
{
	return rmtfd->dev_error;
}

void storage_exit(void)
{
	int i;

	for (i = 0; i < MAX_CALLERS; i++)
		storage_close(&rmtfds[i]);
}

ssize_t storage_pread(const struct rmtfd *rmtfd, void *buf, size_t nbyte, off_t offset)
{
	ssize_t n;

	if (!storage_read_only) {
		n = pread(rmtfd->fd, buf, nbyte, offset);
	} else {
		n = MIN(nbyte, rmtfd->shadow_len - offset);
		if (n > 0)
			memcpy(buf, (char*)rmtfd->shadow_buf + offset, n);
		else
			n = 0;
	}

	if (n < nbyte)
		memset((char*)buf + n, 0, nbyte - n);

	return nbyte;
}

ssize_t storage_pwrite(struct rmtfd *rmtfd, const void *buf, size_t nbyte, off_t offset)
{
	size_t new_len = offset + nbyte;
	void *new_buf;

	if (!storage_read_only)
		return pwrite(rmtfd->fd, buf, nbyte, offset);

	if (new_len >= STORAGE_MAX_SIZE) {
		fprintf(stderr, "write to %zd bytes exceededs max size\n", new_len);
		errno = -EINVAL;
		return -1;
	}

	if (new_len > rmtfd->shadow_len) {
		new_buf = realloc(rmtfd->shadow_buf, new_len);
		if (!new_buf) {
			errno = -ENOMEM;
			return -1;
		}

		rmtfd->shadow_buf = new_buf;
		rmtfd->shadow_len = new_len;
	}

	memcpy((char*)rmtfd->shadow_buf + offset, buf, nbyte);

	return nbyte;
}

int storage_sync(struct rmtfd *rmtfd)
{
	if (storage_read_only)
		return 0;
	
	return fdatasync(rmtfd->fd);
}

static int storage_populate_shadow_buf(struct rmtfd *rmtfd, const char *file)
{
	ssize_t len;
	ssize_t n;
	void *buf;
	int ret;
	int fd;

	fd = open(file, O_RDONLY);
	if (fd < 0)
		return -1;

	len = lseek(fd, 0, SEEK_END);
	if (len < 0) {
		ret = -1;
		goto err_close_fd;
	}

	lseek(fd, 0, SEEK_SET);

	buf = calloc(1, len);
	if (!buf) {
		ret = -1;
		goto err_close_fd;
	}

	n = read(fd, buf, len);
	if (n < 0) {
		ret = -1;
		goto err_close_fd;
	}

	rmtfd->shadow_buf = buf;
	rmtfd->shadow_len = n;

	ret = 0;

err_close_fd:
	close(fd);

	return ret;
}
