/**
 * @file ext.c
 * @author Joe Bayer (joexbayer)
 * @brief Ext2 (like) filesystem driver.
 * @version 0.1
 * @date 2024-01-10
 * 
 * @copyright Copyright (c) 2024
 * 
 */

#include <fs/ext.h>
#include <fs/ext_error.h>
#include <fs/superblock.h>
#include <fs/path.h>
#include <fs/inode.h>
#include <fs/directory.h>
#include <memory.h>
#include <diskdev.h>
#include <terminal.h>
#include <bitmap.h>
#include <serial.h>
#include <pcb.h>
#include <ksyms.h>

#include <libc.h>
#include <rtc.h>

static struct superblock superblock;
static struct inode* root_dir;
static struct inode* current_dir;

static int FS_START_LOCATION = 0;
static int FS_INODE_BMAP_LOCATION = 0;
static int FS_BLOCK_BMAP_LOCATION = 0;

static char* months[] = {"NAN", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Nov", "Dec"};

#define CHECK_DISK() if(!disk_attached()){\
		dbgprintf("[FS]: No disk device found.\n");\
		return -1;\
	}

int init_ext()
{
	FS_START_LOCATION = (kernel_size/512)+2;
	FS_INODE_BMAP_LOCATION = FS_START_LOCATION+1;
	FS_BLOCK_BMAP_LOCATION = FS_INODE_BMAP_LOCATION+1;

	/* Read superblock and check magic. */
	read_block_offset((char*) &superblock, sizeof(struct superblock), 0, FS_START_LOCATION);
	if(superblock.magic != MAGIC){
		ext_create_file_system();
		return 1;
	}

	dbgprintf("[FS]: Found Filesystem with size: %d (%d total)\n", superblock.nblocks*BLOCK_SIZE, superblock.size);
	dbgprintf("[FS]: With a total of %d inodes (%d blocks)\n", superblock.ninodes, superblock.ninodes / INODES_PER_BLOCK);
	dbgprintf("[FS]: And total of %d block\n", superblock.nblocks);
	dbgprintf("[FS]: Max file size: %d bytes\n", NDIRECT*BLOCK_SIZE);

	superblock.inodes_start = FS_START_LOCATION + 3;
	superblock.blocks_start = superblock.inodes_start + (superblock.ninodes/ INODES_PER_BLOCK);

	superblock.block_map = create_bitmap(superblock.nblocks);
	superblock.inode_map = create_bitmap(superblock.ninodes);
	read_block_offset((char*) superblock.block_map, get_bitmap_size(superblock.nblocks), 0, FS_BLOCK_BMAP_LOCATION);
	read_block_offset((char*) superblock.inode_map, get_bitmap_size(superblock.ninodes), 0, FS_INODE_BMAP_LOCATION);


	root_dir = inode_get(superblock.root_inode, &superblock);
	dbgprintf("[FS]: Root inode: %d\n", superblock.root_inode);
	root_dir->nlink++;
	current_dir = root_dir;

	return 0;
}

inode_t ext_get_root()
{
	return root_dir->inode;
}

inode_t ext_get_current_dir()
{
	return $process->current->current_directory;
}


int ext_get_size()
{
	return superblock.size;
}

int ext_get_inodes()
{
	return superblock.ninodes;
}

int ext_get_blocks()
{
	return superblock.nblocks;
}

void ext_sync()
{
	inodes_sync(&superblock);
	write_block_offset((char*) &superblock, sizeof(struct superblock), 0, FS_START_LOCATION);
	write_block_offset((char*) superblock.inode_map, get_bitmap_size(superblock.ninodes), 0, FS_INODE_BMAP_LOCATION);
	write_block_offset((char*) superblock.block_map, get_bitmap_size(superblock.nblocks), 0, FS_BLOCK_BMAP_LOCATION);
}

static inline void __inode_add_dir(struct directory_entry* entry, struct inode* inode, struct superblock* sb)
{
	inode->pos = inode->size;
	inode_write((char*) entry, sizeof(struct directory_entry), inode, sb);
}

void ext_create_file_system()
{
	superblock.magic = MAGIC;
	superblock.size = (disk_size()) - (FS_START_LOCATION*BLOCK_SIZE);

	superblock.ninodes = (superblock.size / (sizeof(struct inode)+NDIRECT*BLOCK_SIZE));
	superblock.nblocks = superblock.ninodes*NDIRECT;

	superblock.inodes_start = FS_START_LOCATION + 3;
	superblock.blocks_start = superblock.inodes_start + (superblock.ninodes/ INODES_PER_BLOCK);

	superblock.block_map = create_bitmap(superblock.nblocks);
	superblock.inode_map = create_bitmap(superblock.ninodes);

	dbgprintf("[FS]: Creating Filesystem with size: %d (%d total)\n", superblock.nblocks*BLOCK_SIZE, superblock.size);
	dbgprintf("[FS]: With a total of %d inodes (%d blocks)\n", superblock.ninodes, superblock.ninodes / INODES_PER_BLOCK);
	dbgprintf("[FS]: And total of %d blocks\n", superblock.nblocks);
	dbgprintf("[FS]: Max file size: %d bytes\n", NDIRECT*BLOCK_SIZE);

	inode_t root_inode = alloc_inode(&superblock, FS_TYPE_DIRECTORY);
	root_dir = inode_get(root_inode, &superblock);
	root_dir->nlink++;

	superblock.root_inode = root_inode;

	struct directory_entry self = {
		.inode = root_inode,
		.name = ".",
		.flags = FS_DIR_FLAG_DIRECTORY

	};

	struct directory_entry back = {
		.inode = root_inode,
		.name = "..",
		.flags = FS_DIR_FLAG_DIRECTORY
	};

	current_dir = root_dir;

	__inode_add_dir(&back, root_dir, &superblock);
	__inode_add_dir(&self, root_dir, &superblock);

	inode_t bin_dir = ext_create_directory("bin", root_inode);
	struct inode* bin_dir_inode = inode_get(bin_dir, &superblock);

	inode_t test_file_inode = alloc_inode(&superblock, FS_TYPE_FILE);
	struct inode* test_file_disk_inode = inode_get(test_file_inode, &superblock);

	struct directory_entry hello_c = {
		.inode = test_file_inode,
		.name = "add.c",
		.flags = FS_DIR_FLAG_FILE
	};

	char* test_file_text = "\
		int add(int a, int b)\n\
		{\n\
			return a+b;\n\
		}\n\
		\n\
		int main()\n\
		{\n\
			int c;\n\
			\n\
			c = add(10, 13);\n\
			return c;\n\
		}\n";

	inode_write(test_file_text, strlen(test_file_text)+1, test_file_disk_inode, &superblock);

	/* Editor */
	inode_t home_inode = alloc_inode(&superblock, FS_TYPE_FILE);

	struct directory_entry home = {
		.inode = home_inode,
		.name = "edit.o",
		.flags = FS_DIR_FLAG_FILE
	};

	__inode_add_dir(&hello_c, root_dir, &superblock);
	__inode_add_dir(&home, bin_dir_inode, &superblock);


	root_dir->pos = 0;
}

int ext_create(char* name)
{
	if(strlen(name)+1 > FS_DIRECTORY_NAME_SIZE)
		return -FS_ERR_NAME_SIZE;

	inode_t inode_index = alloc_inode(&superblock, FS_TYPE_FILE);
	if(inode_index == 0){
		return -FS_ERR_CREATE;
	}

	struct inode* inode = inode_get(inode_index, &superblock);
	if(inode == NULL)
		return -FS_ERR_INODE_MISSING;

	struct directory_entry new = {
		.inode = inode->inode,
		.flags = FS_DIR_FLAG_FILE
	};
	memcpy(new.name, name, strlen(name));
	__inode_add_dir(&new, current_dir, &superblock);

	dbgprintf("[FS] Creating new file %s, inode: %d.\n", name, inode->inode);
	
	return 0;
} 

int ext_read(inode_t i, void* buf, int size)
{
	char* buffer = (char*)buf;
	struct inode* inode = inode_get(i, &superblock);
	if(inode == NULL)
		return -ERROR_NULL_POINTER;

	if(inode->nlink == 0){
		dbgprintf("[FS] Tried to read inode %d with no links.\n", inode->inode);
		return -FS_ERR_FILE_MISSING;
	}

	int ret = inode_read(buffer, size, inode, &superblock);
	
	dbgprintf("Read %d bytes from %d\n", ret, i);

	return ret;
}

/**
 * @brief Sets position attribute of inode
 * Cannot set posision past a files size.
 * @param i inode
 * @param pos position to set
 * @param opt UNUSED
 * @return int 
 */
int ext_seek(inode_t i, int pos, int opt)
{
	struct inode* inode = inode_get(i, &superblock);
	if(inode == NULL)
		return -ERROR_NULL_POINTER;
	
	if(pos > inode->size)
		return -1;
	
	inode->pos = pos;
	return 0;
}


int ext_write(inode_t i, void* buf, int size)
{
	char* buffer = (char*) buf;
	struct inode* inode = inode_get(i, &superblock);
	if(inode == NULL)
		return -ERROR_NULL_POINTER;
	
	if(inode->nlink == 0)
		return -FS_ERR_FILE_MISSING;

	inode->pos = 0; /* Should not set pos = 0*/
	
	dbgprintf("[FS] writing %d from inode %d\n", size, i);
	int ret = inode_write(buffer, size, inode, &superblock);
	
	return ret;
}

void ext_close(inode_t inode)
{
	struct inode* inode_disk = inode_get(inode, &superblock);
	if(inode_disk == NULL)
		return;
	
	if(inode_disk->nlink == 0){
		dbgprintf("[FS] Tried to close inode %d with no links.\n", inode_disk->inode);
		return;
	}

	dbgprintf("[FS] Closing inode %d\n", inode_disk->inode);
	inode_disk->nlink--;
}

inode_t ext_open_from_directory(char* name, inode_t i)
{
	struct directory_entry entry;
	struct inode* inode = inode_get(i, &superblock);
	if(inode == NULL)
		return -FS_ERR_INODE_MISSING;

	inode->pos = 0;
	int size = 0;

	if(strlen(name)+1 > FS_DIRECTORY_NAME_SIZE)
		return -FS_ERR_NAME_SIZE;

	while (size <= inode->size)
	{
		int ret = inode_read((char*) &entry, sizeof(struct directory_entry), inode, &superblock);
		if(ret <= 0){
			return 0;
		}
		
		dbgprintf("[FS] Read %d / %d\n", size, inode->size);
		int mem_ret = memcmp((void*)entry.name, (void*)name, strlen(entry.name));
		if(mem_ret == 0)
			goto ext_open_done;
		size += ret;
	}

	return 0;

ext_open_done:
	dbgprintf("[FS] Opened file %s inode: %d\n", name, entry.inode);

	inode = inode_get(entry.inode, &superblock);
	if(inode == NULL)
		return -FS_ERR_FILE_MISSING;
	
	return entry.inode;
}

inode_t ext_open(char* name, ext_flag_t flags)
{
	CHECK_DISK();

	dbgprintf("[FS] Opening file %s (0x%x)\n", name, name);
	dbgprintf("[FS] %x\n", name[0]);

	/* TODO: check flags for read / write access */

	int ret = inode_from_path(name);
	if(ret <= 0 && flags & FS_FLAG_CREATE){
		ext_create(name);
		return inode_from_path(name);
	}

	struct inode* inode = inode_get(ret, &superblock);
	if(inode == NULL)
		return -FS_ERR_FILE_MISSING;

	inode->pos = 0;
	inode->nlink++;

	return ret;
}

int ext_size(inode_t i)
{
	struct inode* inode = inode_get(i, &superblock);
	if(inode == NULL)
		return -ERROR_NULL_POINTER;

	return inode->size;
}

inode_t change_directory(char* path)
{
	inode_t ret = ext_open(path, 0);
	struct inode* inode = inode_get(ret, &superblock);
	if(inode == NULL)
		return -FS_ERR_FILE_MISSING;

	if(inode->type != FS_TYPE_DIRECTORY){
		return -FS_ERR_NOT_DIRECTORY;
	}

	dbgprintf("[FS] Changing directory to %s, inode: %d\n", path, inode->inode);

	/* This is this only for testing?? */
	current_dir = inode;

	return inode->inode;
}


/* returnrs 0 on success, < 0 on error */
inode_t ext_create_directory(char* name, inode_t current)
{
	if(strlen(name)+1 > FS_DIRECTORY_NAME_SIZE)
		return -FS_ERR_NAME_SIZE;

	inode_t inode_index = alloc_inode(&superblock, FS_TYPE_DIRECTORY);
	if(inode_index == 0){
		return -FS_ERR_CREATE_INODE;
	}

	struct inode* inode = inode_get(inode_index, &superblock);
	if(inode == NULL)
		return -FS_ERR_INODE_MISSING;

	struct inode* current_dir = inode_get(current, &superblock);
	if(inode == NULL)
		return -FS_ERR_INODE_MISSING;

	struct directory_entry self = {
		.inode = inode->inode,
		.name = ".",
		.flags = FS_DIR_FLAG_DIRECTORY
	};

	struct directory_entry back = {
		.inode = current_dir->inode,
		.name = "..",
		.flags = FS_DIR_FLAG_DIRECTORY
	};

	struct directory_entry new = {
		.inode = inode->inode,
		.flags = FS_DIR_FLAG_DIRECTORY
	};
	memcpy(new.name, name, strlen(name));

	__inode_add_dir(&back, inode, &superblock);
	__inode_add_dir(&self, inode, &superblock);
	__inode_add_dir(&new, current_dir, &superblock);

	dbgprintf("[FS] Created directory %s with inode %d.\n", name, inode->inode);

	return inode_index;
}



void listdir()
{
	if(!disk_attached()) return;

	struct directory_entry entry;
	struct inode* current_dir = inode_get(ext_get_current_dir(), &superblock);
	if(current_dir == NULL)
		return;

	int size = 0;

	twritef("Size  Date    Time    Name\n");
	current_dir->pos = 0;
	while (size < current_dir->size)
	{
		int ret = inode_read((char*) &entry, sizeof(struct directory_entry), current_dir, &superblock);
		struct inode* inode = inode_get(entry.inode, &superblock);
		struct time* time = &inode->time;
		twritef("%p %s %d, %d:%d - %s%s\n",
			inode->size,
			months[time->month],
			time->day, time->hour, time->minute,
			entry.name,
			inode->type == FS_TYPE_DIRECTORY ? "/" : ""
		);
		size += ret;
	}
}