/**
 * @file  kernel/vfs/tmpfs.c
 * @brief In-memory read-write filesystem.
 *
 * Generally provides the filesystem for "migrated" live CDs,
 * as well as /tmp and /var.
 *
 * @copyright
 * This file is part of ToaruOS and is released under the terms
 * of the NCSA / University of Illinois License - see LICENSE.md
 * Copyright (C) 2014-2021 K. Lange
 */
#include <stdint.h>
#include <errno.h>
#include <kernel/types.h>
#include <kernel/string.h>
#include <kernel/printf.h>
#include <kernel/vfs.h>
#include <kernel/process.h>
#include <kernel/tokenize.h>
#include <kernel/tmpfs.h>
#include <kernel/spinlock.h>
#include <kernel/mmu.h>
#include <kernel/time.h>
#include <kernel/procfs.h>

/* 4KB */
#define BLOCKSIZE 0x1000

#define TMPFS_TYPE_FILE 1
#define TMPFS_TYPE_DIR  2
#define TMPFS_TYPE_LINK 3

static struct tmpfs_dir * tmpfs_root = NULL;
static volatile intptr_t tmpfs_total_blocks = 0;

static fs_node_t * tmpfs_from_dir(struct tmpfs_dir * d);

static struct tmpfs_file * tmpfs_file_new(char * name) {
	struct tmpfs_file * t = malloc(sizeof(struct tmpfs_file));
	spin_init(t->lock);
	t->name = strdup(name);
	t->type = TMPFS_TYPE_FILE;
	t->length = 0;
	t->pointers = 2;
	t->block_count = 0;
	t->mask = 0;
	t->uid = 0;
	t->gid = 0;
	t->atime = now();
	t->mtime = t->atime;
	t->ctime = t->atime;
	t->blocks = malloc(t->pointers * sizeof(char *));
	for (size_t i = 0; i < t->pointers; ++i) {
		t->blocks[i] = 0;
	}

	return t;
}

static int symlink_tmpfs(fs_node_t * parent, char * target, char * name) {
	struct tmpfs_dir * d = (struct tmpfs_dir *)parent->device;

	spin_lock(d->lock);
	foreach(f, d->files) {
		struct tmpfs_file * t = (struct tmpfs_file *)f->value;
		if (!strcmp(name, t->name)) {
			spin_unlock(d->lock);
			return -EEXIST; /* Already exists */
		}
	}
	spin_unlock(d->lock);

	struct tmpfs_file * t = tmpfs_file_new(name);
	t->type = TMPFS_TYPE_LINK;
	t->target = strdup(target);

	t->mask = 0777;
	t->uid = this_core->current_process->user;
	t->gid = this_core->current_process->user;

	spin_lock(d->lock);
	list_insert(d->files, t);
	spin_unlock(d->lock);

	return 0;
}

static ssize_t readlink_tmpfs(fs_node_t * node, char * buf, size_t size) {
	struct tmpfs_file * t = (struct tmpfs_file *)(node->device);

	spin_lock(t->lock);
	if (t->type != TMPFS_TYPE_LINK) {
		spin_unlock(t->lock);
		printf("tmpfs: not a symlink?\n");
		return -1;
	}

	if (size < strlen(t->target) + 1) {
		memcpy(buf, t->target, size-1);
		buf[size-1] = '\0';
		spin_unlock(t->lock);
		return size-2;
	} else {
		size_t len = strlen(t->target);
		memcpy(buf, t->target, len + 1);
		spin_unlock(t->lock);
		return len;
	}
}

static struct tmpfs_dir * tmpfs_dir_new(char * name, struct tmpfs_dir * parent) {
	struct tmpfs_dir * d = malloc(sizeof(struct tmpfs_dir));
	spin_init(d->lock);
	d->name = strdup(name);
	d->type = TMPFS_TYPE_DIR;
	d->mask = 0;
	d->uid = 0;
	d->gid = 0;
	d->atime = now();
	d->mtime = d->atime;
	d->ctime = d->atime;
	d->files = list_create("tmpfs directory entries",d);
	return d;
}

static void tmpfs_file_free(struct tmpfs_file * t) {
	if (t->type == TMPFS_TYPE_LINK) {
		printf("tmpfs: bad link free?\n");
		free(t->target);
	}
	spin_lock(t->lock);
	for (size_t i = 0; i < t->block_count; ++i) {
		mmu_frame_release((uintptr_t)t->blocks[i] * 0x1000);
		tmpfs_total_blocks--;
	}
	spin_unlock(t->lock);
}

static void tmpfs_file_blocks_embiggen(struct tmpfs_file * t) {
	t->pointers *= 2;
	t->blocks = realloc(t->blocks, sizeof(char *) * t->pointers);
}

static char * tmpfs_file_getset_block(struct tmpfs_file * t, size_t blockid, int create) {
	if (create) {
		while (blockid >= t->pointers) {
			tmpfs_file_blocks_embiggen(t);
		}
		while (blockid >= t->block_count) {
			uintptr_t index = mmu_allocate_a_frame();
			tmpfs_total_blocks++;
			t->blocks[t->block_count] = index;
			t->block_count += 1;
		}
	} else {
		if (blockid >= t->block_count) {
			printf("tmpfs: not enough blocks?\n");
			return NULL;
		}
	}

	return (char *)mmu_map_from_physical(t->blocks[blockid] << 12);
}


static ssize_t read_tmpfs(fs_node_t *node, off_t offset, size_t size, uint8_t *buffer) {
	struct tmpfs_file * t = (struct tmpfs_file *)(node->device);

	spin_lock(t->lock);

	t->atime = now();

	uint64_t end;
	if ((size_t)offset + size > t->length) {
		end = t->length;
	} else {
		end = offset + size;
	}
	uint64_t start_block  = offset / BLOCKSIZE;
	uint64_t end_block    = end / BLOCKSIZE;
	uint64_t end_size     = end - end_block * BLOCKSIZE;
	uint64_t size_to_read = end - offset;
	if (start_block == end_block && (size_t)offset == end) {
		spin_unlock(t->lock);
		return 0;
	}
	if (start_block == end_block) {
		void *buf = tmpfs_file_getset_block(t, start_block, 0);
		memcpy(buffer, (uint8_t *)(((uintptr_t)buf) + ((uintptr_t)offset % BLOCKSIZE)), size_to_read);
		spin_unlock(t->lock);
		return size_to_read;
	} else {
		uint64_t block_offset;
		uint64_t blocks_read = 0;
		for (block_offset = start_block; block_offset < end_block; block_offset++, blocks_read++) {
			if (block_offset == start_block) {
				void *buf = tmpfs_file_getset_block(t, block_offset, 0);
				memcpy(buffer, (uint8_t *)(((uint64_t)buf) + ((uintptr_t)offset % BLOCKSIZE)), BLOCKSIZE - (offset % BLOCKSIZE));
			} else {
				void *buf = tmpfs_file_getset_block(t, block_offset, 0);
				memcpy(buffer + BLOCKSIZE * blocks_read - (offset % BLOCKSIZE), buf, BLOCKSIZE);
			}
		}
		if (end_size) {
			void *buf = tmpfs_file_getset_block(t, end_block, 0);
			memcpy(buffer + BLOCKSIZE * blocks_read - (offset % BLOCKSIZE), buf, end_size);
		}
	}
	spin_unlock(t->lock);
	return size_to_read;
}

static ssize_t write_tmpfs(fs_node_t *node, off_t offset, size_t size, uint8_t *buffer) {
	struct tmpfs_file * t = (struct tmpfs_file *)(node->device);

	spin_lock(t->lock);
	t->atime = now();
	t->mtime = t->atime;

	uint64_t end;
	if ((size_t)offset + size > t->length) {
		t->length = offset + size;
	}
	end = offset + size;
	uint64_t start_block  = offset / BLOCKSIZE;
	uint64_t end_block    = end / BLOCKSIZE;
	uint64_t end_size     = end - end_block * BLOCKSIZE;
	uint64_t size_to_read = end - offset;
	if (start_block == end_block) {
		void *buf = tmpfs_file_getset_block(t, start_block, 1);
		memcpy((uint8_t *)(((uint64_t)buf) + ((uintptr_t)offset % BLOCKSIZE)), buffer, size_to_read);
		spin_unlock(t->lock);
		return size_to_read;
	} else {
		uint64_t block_offset;
		uint64_t blocks_read = 0;
		for (block_offset = start_block; block_offset < end_block; block_offset++, blocks_read++) {
			if (block_offset == start_block) {
				void *buf = tmpfs_file_getset_block(t, block_offset, 1);
				memcpy((uint8_t *)(((uint64_t)buf) + ((uintptr_t)offset % BLOCKSIZE)), buffer, BLOCKSIZE - (offset % BLOCKSIZE));
			} else {
				void *buf = tmpfs_file_getset_block(t, block_offset, 1);
				memcpy(buf, buffer + BLOCKSIZE * blocks_read - (offset % BLOCKSIZE), BLOCKSIZE);
			}
		}
		if (end_size) {
			void *buf = tmpfs_file_getset_block(t, end_block, 1);
			memcpy(buf, buffer + BLOCKSIZE * blocks_read - (offset % BLOCKSIZE), end_size);
		}
	}
	spin_unlock(t->lock);
	return size_to_read;
}

static int chmod_tmpfs(fs_node_t * node, int mode) {
	struct tmpfs_file * t = (struct tmpfs_file *)(node->device);

	/* XXX permissions */
	t->mask = mode;

	return 0;
}

static int chown_tmpfs(fs_node_t * node, int uid, int gid) {
	struct tmpfs_file * t = (struct tmpfs_file *)(node->device);

	spin_lock(t->lock);
	if (uid != -1) t->uid = uid;
	if (gid != -1) t->gid = gid;
	spin_unlock(t->lock);

	return 0;
}

static int truncate_tmpfs(fs_node_t * node) {
	struct tmpfs_file * t = (struct tmpfs_file *)(node->device);
	spin_lock(t->lock);
	for (size_t i = 0; i < t->block_count; ++i) {
		mmu_frame_release((uintptr_t)t->blocks[i] * 0x1000);
		tmpfs_total_blocks--;
		t->blocks[i] = 0;
	}
	t->block_count = 0;
	t->length = 0;
	t->mtime = node->atime;
	spin_unlock(t->lock);
	return 0;
}

static void open_tmpfs(fs_node_t * node, unsigned int flags) {
	struct tmpfs_file * t = (struct tmpfs_file *)(node->device);

	t->atime = now();
}

static fs_node_t * tmpfs_from_file(struct tmpfs_file * t) {
	fs_node_t * fnode = malloc(sizeof(fs_node_t));
	spin_lock(t->lock);
	memset(fnode, 0x00, sizeof(fs_node_t));
	fnode->inode = 0;
	strcpy(fnode->name, t->name);
	fnode->device = t;
	fnode->mask = t->mask;
	fnode->uid = t->uid;
	fnode->gid = t->gid;
	fnode->atime = t->atime;
	fnode->ctime = t->ctime;
	fnode->mtime = t->mtime;
	fnode->flags   = FS_FILE;
	fnode->read    = read_tmpfs;
	fnode->write   = write_tmpfs;
	fnode->open    = open_tmpfs;
	fnode->close   = NULL;
	fnode->readdir = NULL;
	fnode->finddir = NULL;
	fnode->chmod   = chmod_tmpfs;
	fnode->chown   = chown_tmpfs;
	fnode->length  = t->length;
	fnode->truncate = truncate_tmpfs;
	fnode->nlink   = 1;
	spin_unlock(t->lock);
	return fnode;
}

static fs_node_t * tmpfs_from_link(struct tmpfs_file * t) {
	fs_node_t * fnode = tmpfs_from_file(t);
	fnode->flags   |= FS_SYMLINK;
	fnode->readlink = readlink_tmpfs;
	fnode->read     = NULL;
	fnode->write    = NULL;
	fnode->create   = NULL;
	fnode->mkdir    = NULL;
	fnode->readdir  = NULL;
	fnode->finddir  = NULL;
	return fnode;
}

static struct dirent * readdir_tmpfs(fs_node_t *node, uint64_t index) {
	struct tmpfs_dir * d = (struct tmpfs_dir *)node->device;
	uint64_t i = 0;

	if (index == 0) {
		struct dirent * out = malloc(sizeof(struct dirent));
		memset(out, 0x00, sizeof(struct dirent));
		out->d_ino = 0;
		strcpy(out->d_name, ".");
		return out;
	}

	if (index == 1) {
		struct dirent * out = malloc(sizeof(struct dirent));
		memset(out, 0x00, sizeof(struct dirent));
		out->d_ino = 0;
		strcpy(out->d_name, "..");
		return out;
	}

	index -= 2;

	if (index >= d->files->length) return NULL;

	foreach(f, d->files) {
		if (i == index) {
			struct tmpfs_file * t = (struct tmpfs_file *)f->value;
			struct dirent * out = malloc(sizeof(struct dirent));
			memset(out, 0x00, sizeof(struct dirent));
			out->d_ino = (uint64_t)t;
			strcpy(out->d_name, t->name);
			return out;
		} else {
			++i;
		}
	}
	return NULL;
}

static fs_node_t * finddir_tmpfs(fs_node_t * node, char * name) {
	if (!name) return NULL;

	struct tmpfs_dir * d = (struct tmpfs_dir *)node->device;

	spin_lock(d->lock);

	foreach(f, d->files) {
		struct tmpfs_file * t = (struct tmpfs_file *)f->value;
		if (!strcmp(name, t->name)) {
			fs_node_t * out = NULL;
			switch (t->type) {
				case TMPFS_TYPE_FILE:
					out = tmpfs_from_file(t);
					break;
				case TMPFS_TYPE_LINK:
					out = tmpfs_from_link(t);
					break;
				case TMPFS_TYPE_DIR:
					out = tmpfs_from_dir((struct tmpfs_dir *)t);
					break;
			}
			spin_unlock(d->lock);
			return out;
		}
	}

	spin_unlock(d->lock);
	return NULL;
}

static int unlink_tmpfs(fs_node_t * node, char * name) {
	struct tmpfs_dir * d = (struct tmpfs_dir *)node->device;
	int i = -1, j = 0;

	spin_lock(d->lock);
	foreach(f, d->files) {
		struct tmpfs_file * t = (struct tmpfs_file *)f->value;
		if (!strcmp(name, t->name)) {
			tmpfs_file_free(t);
			free(t);
			i = j;
			break;
		}
		j++;
	}

	if (i >= 0) {
		list_remove(d->files, i);
	} else {
		spin_unlock(d->lock);
		return -ENOENT;
	}

	spin_unlock(d->lock);
	return 0;
}

static int create_tmpfs(fs_node_t *parent, char *name, mode_t permission) {
	if (!name) return -EINVAL;

	struct tmpfs_dir * d = (struct tmpfs_dir *)parent->device;

	spin_lock(d->lock);
	foreach(f, d->files) {
		struct tmpfs_file * t = (struct tmpfs_file *)f->value;
		if (!strcmp(name, t->name)) {
			spin_unlock(d->lock);
			return -EEXIST; /* Already exists */
		}
	}
	spin_unlock(d->lock);

	struct tmpfs_file * t = tmpfs_file_new(name);
	t->mask = permission;
	t->uid = this_core->current_process->user;
	t->gid = this_core->current_process->user_group;

	spin_lock(d->lock);
	list_insert(d->files, t);
	spin_unlock(d->lock);

	return 0;
}

static int mkdir_tmpfs(fs_node_t * parent, char * name, mode_t permission) {
	if (!name) return -EINVAL;
	if (!strlen(name)) return -EINVAL;

	struct tmpfs_dir * d = (struct tmpfs_dir *)parent->device;

	spin_lock(d->lock);
	foreach(f, d->files) {
		struct tmpfs_file * t = (struct tmpfs_file *)f->value;
		if (!strcmp(name, t->name)) {
			spin_unlock(d->lock);
			return -EEXIST; /* Already exists */
		}
	}
	spin_unlock(d->lock);

	struct tmpfs_dir * out = tmpfs_dir_new(name, d);
	out->mask = permission;
	out->uid  = this_core->current_process->user;
	out->gid  = this_core->current_process->user;

	spin_lock(d->lock);
	list_insert(d->files, out);
	spin_unlock(d->lock);

	return 0;
}

static fs_node_t * tmpfs_from_dir(struct tmpfs_dir * d) {
	fs_node_t * fnode = malloc(sizeof(fs_node_t));
	spin_lock(d->lock);
	memset(fnode, 0x00, sizeof(fs_node_t));
	fnode->inode = 0;
	strcpy(fnode->name, "tmp");
	fnode->mask = d->mask;
	fnode->uid  = d->uid;
	fnode->gid  = d->gid;
	fnode->device  = d;
	fnode->atime   = d->atime;
	fnode->mtime   = d->mtime;
	fnode->ctime   = d->ctime;
	fnode->flags   = FS_DIRECTORY;
	fnode->read    = NULL;
	fnode->write   = NULL;
	fnode->open    = NULL;
	fnode->close   = NULL;
	fnode->readdir = readdir_tmpfs;
	fnode->finddir = finddir_tmpfs;
	fnode->create  = create_tmpfs;
	fnode->unlink  = unlink_tmpfs;
	fnode->mkdir   = mkdir_tmpfs;
	fnode->nlink   = 1; /* should be "number of children that are directories + 1" */
	fnode->symlink = symlink_tmpfs;

	fnode->chown   = chown_tmpfs;
	fnode->chmod   = chmod_tmpfs;
	spin_unlock(d->lock);

	return fnode;
}

fs_node_t * tmpfs_create(char * name) {
	tmpfs_root = tmpfs_dir_new(name, NULL);
	tmpfs_root->mask = 0777;
	tmpfs_root->uid  = 0;
	tmpfs_root->gid  = 0;

	return tmpfs_from_dir(tmpfs_root);
}

fs_node_t * tmpfs_mount(const char * device, const char * mount_path) {
	char * arg = strdup(device);
	char * argv[10];
	int argc = tokenize(arg, ",", argv);

	fs_node_t * fs = tmpfs_create(argv[0]);

	if (argc > 1) {
		if (strlen(argv[1]) < 3) {
			printf("tmpfs: ignoring bad permission option for tmpfs\n");
		} else {
			int mode = ((argv[1][0] - '0') << 6) |
			           ((argv[1][1] - '0') << 3) |
			           ((argv[1][2] - '0') << 0);
			fs->mask = mode;
		}
	}

	//free(arg);
	return fs;
}

static ssize_t tmpfs_func(fs_node_t * node, off_t offset, size_t size, uint8_t * buffer) {
	char * buf = malloc(4096);

	snprintf(buf, 4095,
		"UsedBlocks:\t%zd\n",
		tmpfs_total_blocks);

	size_t _bsize = strlen(buf);
	if ((size_t)offset > _bsize) {
		free(buf);
		return 0;
	}
	if (size > _bsize - (size_t)offset) size = _bsize - offset;

	memcpy(buffer, buf + offset, size);
	free(buf);
	return size;
}

static struct procfs_entry tmpfs_entry = {
	0,
	"tmpfs",
	tmpfs_func,
};

void tmpfs_register_init(void) {
	vfs_register("tmpfs", tmpfs_mount);
	procfs_install(&tmpfs_entry);
}

