/*
 * Copyright (c) 2013, 2014, 2015, 2023 Jonas 'Sortie' Termansen.
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 * filesystem.cpp
 * Filesystem.
 */

#include <sys/stat.h>
#include <sys/types.h>

#include <assert.h>
#include <endian.h>
#include <errno.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include <time.h>

#include "fat.h"

#include "block.h"
#include "device.h"
#include "filesystem.h"
#include "inode.h"
#include "util.h"

// TODO: Be more precise.
static bool is_8_3_char(char c)
{
		return ('A' <= c && c <= 'Z') || ('0' <= c && c <= '9');
}

// TODO: Is this fully precise? What about .FOO?
bool is_8_3(const char* name)
{
	if ( !name[0] )
		return false;
	size_t b = 0;
	while ( name[b] && is_8_3_char(name[b]) )
		b++;
	if ( 8 < b )
		return false;
	if ( !name[b] )
		return true;
	if ( name[b] != '.' )
		return false;
	size_t e = 0;
	while ( name[b+1+e] && is_8_3_char(name[b+1+e]) )
		e++;
	if ( 3 < e )
		return false;
	if ( name[b+1+e] )
		return false;
	return true;
}

void encode_8_3(const char* decoded, char encoded[8 + 3])
{
	size_t i = 0;
	for ( size_t o = 0; o < 8 + 3; o++ )
	{
		char c = ' ';
		if ( decoded[i] == '.' && o == 8 )
			i++;
		if ( decoded[i] && decoded[i] != '.' )
			c = decoded[i++];
		if ( (unsigned char) c == 0xE5 )
			c = 0x05;
		encoded[o] = c;
	}
}

void decode_8_3(const char encoded[8 + 3], char decoded[8 + 1 + 3 + 1])
{
	size_t o = 0;
	for ( size_t i = 0; i < 8; i++ )
	{
		char c = encoded[i];
		if ( !c || c == ' ' )
			break;
		if ( c == 0x05 )
			c = (char) 0xE5;
		decoded[o++] = c;
	}
	for ( size_t i = 8; i < 8 + 3; i++ )
	{
		char c = encoded[i];
		if ( !c || c == ' ' )
			break;
		if ( i == 8 )
			decoded[o++] = '.';
		if ( c == 0x05 )
			c = (char) 0xE5;
		decoded[o++] = c;
	}
	decoded[o] = '\0';
}

uint8_t timespec_to_fat_tenths(struct timespec* ts)
{
	// TODO: Work with a struct tm instead.
	uint16_t hundreds = ts->tv_nsec / 10000000;
	struct tm tm;
	gmtime_r(&ts->tv_sec, &tm);
	if ( tm.tm_sec & 1 )
		hundreds += 100;
	return hundreds;
}

uint16_t timespec_to_fat_time(struct timespec* ts)
{
	// TODO: Work with a struct tm instead.
	struct tm tm;
	gmtime_r(&ts->tv_sec, &tm);
	return (tm.tm_sec / 2) << 0 | tm.tm_min << 5 | tm.tm_hour << 11;
}

uint16_t timespec_to_fat_date(struct timespec* ts)
{
	// TODO: Work with a struct tm instead.
	struct tm tm;
	gmtime_r(&ts->tv_sec, &tm);
	return tm.tm_mday << 0 | (tm.tm_mon + 1) << 5 | (tm.tm_year - 80) << 9;
}

// TODO: Rename tenths to a better name.
void timespec_to_fat(const struct timespec* ts, uint16_t* date, uint16_t* time,
                     uint8_t* tenths)
{
	struct tm tm;
	gmtime_r(&ts->tv_sec, &tm);
	// TODO: Endian.
	*date = tm.tm_mday << 0 | (tm.tm_mon + 1) << 5 | (tm.tm_year - 80) << 9;
	*time = (tm.tm_sec / 2) << 0 | tm.tm_min << 5 | tm.tm_hour << 11;
	*tenths = ts->tv_nsec / 10000000 + (tm.tm_sec & 1 ? 100 : 0);
}

Filesystem::Filesystem(Device* device, const char* mount_path)
{
	this->bpb_block = device->GetBlock(0);
	assert(bpb_block); // TODO: This can fail.
	this->bpb = (struct fat_bpb*) bpb_block->block_data;
	this->device = device;
	this->mount_path = mount_path;
	this->mode_reg = S_IFREG | 0644;
	this->mode_dir = S_IFDIR | 0755;
	this->block_size = device->block_size;
	this->bytes_per_sector =
		bpb->bytes_per_sector_low | bpb->bytes_per_sector_high << 8;
	this->root_dirent_count =
		bpb->root_dirent_count_low | bpb->root_dirent_count_high << 8;
	uint32_t root_dir_sectors =
		divup<uint32_t>(root_dirent_count * sizeof(fat_dirent), bytes_per_sector);
	this->sectors_per_fat =
		bpb->sectors_per_fat ? bpb->sectors_per_fat : bpb->fat32_sectors_per_fat;
	this->total_sectors =
		bpb->total_sectors_low | bpb->total_sectors_high << 8;
	if ( !this->total_sectors )
		this->total_sectors = bpb->total_sectors_large;
	this->fat_sector = bpb->reserved_sectors;
	this->root_sector = fat_sector + bpb->fat_count * sectors_per_fat;
	this->data_sector = root_sector + root_dir_sectors;
	uint32_t data_sectors = total_sectors - data_sector;
	this->cluster_count = data_sectors / bpb->sectors_per_cluster;
	this->cluster_size = bpb->sectors_per_cluster * bytes_per_sector;
	this->fat_type = cluster_count < 4085 ? 12 : cluster_count < 65525 ? 16 : 32;
	// Use cluster 1 as the root inode on FAT12/FAT16 since it's not a valid
	// cluster for use in the FAT.
	this->root_inode_id = fat_type == 32 ? bpb->fat32_root_cluster : 1;
	// TODO: Okay we actually need to compare with their lower bounds.
	this->eio_cluster =
		fat_type == 12 ? 0xFF7 : fat_type == 16 ? 0xFFF7 : 0xFFFFFF7;
	this->eof_cluster =
		fat_type == 12 ? 0xFFF : fat_type == 16 ? 0xFFFF : 0xFFFFFFF;
	// TODO: Obtain and verify this from the fsinfo.
	this->free_search = 0;
	this->mru_inode = NULL;
	this->lru_inode = NULL;
	this->dirty_inode = NULL;
	for ( size_t i = 0; i < INODE_HASH_LENGTH; i++ )
		this->hash_inodes[i] = NULL;
	this->dirty = false;

	if ( device->write )
	{
		BeginWrite();
		// TODO: Mark as mounted in fat[1] if FAT16/32.
		FinishWrite();
		Sync();
	}
}

Filesystem::~Filesystem()
{
	Sync();
	while ( mru_inode )
		delete mru_inode;
	if ( device->write )
	{
		BeginWrite();
		// TODO: Mark as unounted in fat[1] if FAT16/32.
		FinishWrite();
		Sync();
	}
	bpb_block->Unref();
}

void Filesystem::BeginWrite()
{
	bpb_block->BeginWrite();
}

void Filesystem::FinishWrite()
{
	dirty = true;
	bpb_block->FinishWrite();
}

void Filesystem::Sync()
{
	// TODO: Replacement concept?
	while ( dirty_inode )
		dirty_inode->Sync();
	if ( dirty )
	{
		bpb_block->Sync();
		dirty = false;
	}
	device->Sync();
}

Inode* Filesystem::GetInode(uint32_t inode_id, Block* dirent_block,
                            struct fat_dirent* dirent)
{
#if 0
	if ( !inode_id || num_inodes <= inode_id )
		return errno = EBADF, (Inode*) NULL;
	if ( !inode_id )
		return errno = EBADF, (Inode*) NULL;
#endif

	size_t bin = inode_id % INODE_HASH_LENGTH;
	for ( Inode* iter = hash_inodes[bin]; iter; iter = iter->next_hashed )
		if ( iter->inode_id == inode_id )
			return iter->Refer(), iter;

	if ( inode_id != root_inode_id && !dirent_block )
		return errno = EBADF, (Inode*) NULL;

	Inode* inode = new Inode(this, inode_id);
	if ( !inode )
		return (Inode*) NULL;
	inode->first_cluster =
		inode_id == root_inode_id && fat_type != 32 ? 0 : inode_id;
	if ( (inode->data_block = dirent_block) )
		inode->data_block->Refer();
	inode->dirent = dirent;
	inode->Prelink();

	return inode;
}

uint32_t Filesystem::AllocateCluster()
{
	for ( size_t i = 0; i < cluster_count; i++ )
	{
		size_t n = 2 + (free_search + i) % cluster_count;
		if ( !ReadFAT(n) )
		{
			free_search = (i + 1) % cluster_count;
			if ( fat_type == 32 )
			{
				Block* block = device->GetBlock(bpb->fat32_fsinfo);
				if ( block )
				{
					struct fat_fsinfo* fsinfo =
						(struct fat_fsinfo*) block->block_data;
					block->BeginWrite();
					uint32_t free_count = le32toh(fsinfo->free_count);
					if ( free_count )
						free_count--;
					fsinfo->free_count = htole32(free_count);
					fsinfo->next_free = n;
					block->FinishWrite();
					block->Unref();
				}
			}
			return n;
		}
	}
	return errno = ENOSPC, 0;
}

void Filesystem::FreeCluster(uint32_t cluster)
{
	if ( fat_type != 32 )
		return;
	Block* block = device->GetBlock(bpb->fat32_fsinfo);
	if ( !block )
		return;
	struct fat_fsinfo* fsinfo = (struct fat_fsinfo*) block->block_data;
	block->BeginWrite();
	uint32_t free_count = le32toh(fsinfo->free_count);
	if ( free_count < cluster_count )
		free_count++;
	fsinfo->free_count = htole32(free_count);
	if ( !fsinfo->free_count || le32toh(fsinfo->next_free) == cluster + 1 )
	{
		fsinfo->next_free = htole32(cluster);
		free_search = cluster - 2;
	}
	block->FinishWrite();
	block->Unref();
}

uint32_t Filesystem::ReadFAT(uint32_t cluster)
{
	// TODO: Bounds check.
	if ( fat_type == 12 )
	{
		size_t position = cluster + (cluster / 2);
		size_t lba = position / bytes_per_sector;
		size_t offset = position % bytes_per_sector;
		Block* block = device->GetBlock(fat_sector + lba);
		if ( !block )
			return eio_cluster;
		uint8_t lower = block->block_data[offset];
		if ( ++offset == bytes_per_sector )
		{
			block->Unref();
			if ( !(block = device->GetBlock(fat_sector + lba)) )
				return eio_cluster;
			offset = 0;
		}
		uint8_t higher = block->block_data[offset];
		block->Unref();
		uint16_t value = lower | higher << 8;
		if ( cluster & 1 )
			return value >> 4;
		else
			return value & 0xFFF;
	}
	size_t fat_size = fat_type / 8;
	size_t position = cluster * fat_size;
	size_t lba = position / bytes_per_sector;
	size_t entry = (position % bytes_per_sector) / fat_size;
	Block* block = device->GetBlock(fat_sector + lba);
	if ( !block )
		return eio_cluster;
	uint32_t result = 0;
	if ( fat_type == 16 )
		result = ((uint16_t*) block->block_data)[entry];
	else if ( fat_type == 32 )
		result = ((uint32_t*) block->block_data)[entry] & 0x0FFFFFFF;
	block->Unref();
	return result;
}

bool Filesystem::WriteFAT(uint32_t cluster, uint32_t value)
{
	assert(device->write);
	// TODO: Bounds check.
	if ( fat_type == 12 )
	{
		size_t position = cluster + (cluster / 2);
		size_t lba = position / bytes_per_sector;
		size_t offset = position % bytes_per_sector;
		Block* block = device->GetBlock(fat_sector + lba);
		if ( !block )
			return false;
		value = cluster & 1 ? value << 4 : value;
		uint16_t mask = cluster & 1 ? 0xFFF0 : 0x0FFF;
		block->BeginWrite();
		block->block_data[offset] &= ~mask;
		block->block_data[offset] |= value;
		if ( ++offset == bytes_per_sector )
		{
			block->FinishWrite();
			block->Unref();
			if ( !(block = device->GetBlock(fat_sector + lba)) )
				return false;
			offset = 0;
			block->BeginWrite();
		}
		block->block_data[offset] &= ~(mask >> 8);
		block->block_data[offset] |= value >> 8;
		block->FinishWrite();
		block->Unref();
		return true;
	}

	// TODO: Mirror to the other FATs.
	size_t fat_size = fat_type / 8;
	size_t position = cluster * fat_size;
	size_t lba = position / bytes_per_sector;
	size_t entry = (position % bytes_per_sector) / fat_size;
	Block* block = device->GetBlock(fat_sector + lba);
	if ( !block )
		return false;
	block->BeginWrite();
	if ( fat_type == 16 )
		((uint16_t*) block->block_data)[entry] = value;
	else if ( fat_type == 32 )
	{
		uint32_t old = ((uint32_t*) block->block_data)[entry] & 0xF0000000;
		((uint32_t*) block->block_data)[entry] = value | old;
	}
	block->FinishWrite();
	block->Unref();
	return true;
}

uint32_t Filesystem::CalculateFreeCount()
{
	if ( fat_type == 32 )
	{
		// TODO: Verify the fsinfo.
		Block* block = device->GetBlock(bpb->fat32_fsinfo);
		if ( !block )
			return 0xFFFFFFFF;
		const struct fat_fsinfo* fsinfo =
			(const struct fat_fsinfo*) block->block_data;
		uint32_t result = le32toh(fsinfo->free_count);
		block->Unref();
		if ( result != 0xFFFFFFFF )
			return result;
	}
	// TODO: Cache these.
	size_t count = 0;
	for ( size_t i = 0; i < cluster_count; i++ )
		if ( !ReadFAT(2 + i) )
			count++;
	return count;
}
