/*
 * Copyright (c) 2013, 2014, 2015, 2018, 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.
 *
 * inode.cpp
 * Filesystem inode.
 */

#define __STDC_CONSTANT_MACROS
#define __STDC_LIMIT_MACROS

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

#include <assert.h>
#include <errno.h>
#include <endian.h>
#include <fcntl.h>
#include <limits.h>
#include <pthread.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <time.h>

#include "fat.h"

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

#ifndef S_SETABLE
#define S_SETABLE 02777
#endif
#ifndef O_WRITE
#define O_WRITE (O_WRONLY | O_RDWR)
#endif

Inode::Inode(Filesystem* filesystem, uint32_t inode_id)
{
	this->prev_inode = NULL;
	this->next_inode = NULL;
	this->prev_hashed = NULL;
	this->next_hashed = NULL;
	this->prev_dirty = NULL;
	this->next_dirty = NULL;
	this->data_block = NULL;
	this->filesystem = filesystem;
	this->reference_count = 1;
	this->remote_reference_count = 0;
	this->implied_reference = 0;
	this->inode_id = inode_id;
	this->dirty = false;
	this->deleted = false;
}

Inode::~Inode()
{
	Sync();
	if ( data_block )
		data_block->Unref();
	Unlink();
}

uint32_t Inode::Mode()
{
	if ( inode_id == filesystem->root_inode_id )
		return filesystem->mode_dir;
	mode_t mode = dirent->attributes & FAT_ATTRIBUTE_DIRECTORY ?
	              filesystem->mode_dir : filesystem->mode_reg;
	if ( dirent->attributes & FAT_ATTRIBUTE_READ_ONLY )
		mode &= ~0222;
	return mode;
}

bool Inode::ChangeMode(mode_t mode)
{
	assert(filesystem->device->write);
	if ( inode_id == filesystem->root_inode_id )
		return errno = EPERM, false;
	mode_t base_mode = (dirent->attributes & FAT_ATTRIBUTE_DIRECTORY ?
	                    filesystem->mode_dir : filesystem->mode_reg) & 0777;
	uint8_t new_attributes = dirent->attributes;
	if ( mode == (base_mode & ~0222) )
		new_attributes |= FAT_ATTRIBUTE_READ_ONLY;
	else if ( mode == (base_mode | (base_mode & 0222)) )
		new_attributes &= ~FAT_ATTRIBUTE_READ_ONLY;
	else
		return errno = EPERM, false;
	if ( new_attributes == dirent->attributes )
		return true;
	if ( data_block )
		data_block->BeginWrite();
	dirent->attributes = new_attributes;
	if ( data_block )
		data_block->FinishWrite();
	return true;
}

uint32_t Inode::UserId()
{
	return filesystem->uid;
}

bool Inode::ChangeOwner(uid_t uid, gid_t gid)
{
	assert(filesystem->device->write);
	if ( inode_id == filesystem->root_inode_id )
		return errno = EPERM, false;
	if ( (uid != (uid_t) -1 && uid != filesystem->uid) ||
	     (gid != (gid_t) -1 && gid != filesystem->gid) )
		return errno = EPERM, false;
	return true;
}

uint32_t Inode::GroupId()
{
	return filesystem->gid;
}

void Inode::UTimens(const struct timespec times[2])
{
	if ( inode_id == filesystem->root_inode_id )
		return;
	if ( times[0].tv_nsec != UTIME_OMIT ||
	     times[1].tv_nsec != UTIME_OMIT )
	{
		struct timespec now;
		clock_gettime(CLOCK_REALTIME, &now);
		uint8_t tenths;
		uint16_t time;
		if ( data_block )
			data_block->BeginWrite();
		if ( times[0].tv_nsec == UTIME_NOW )
			timespec_to_fat(&now, &dirent->access_date, &time, &tenths);
		else if ( times[0].tv_nsec != UTIME_OMIT )
			timespec_to_fat(&times[0], &dirent->access_date, &time, &tenths);
		if ( times[1].tv_nsec == UTIME_NOW )
			timespec_to_fat(&now, &dirent->modified_date,
			                &dirent->modified_time, &tenths);
		else if ( times[1].tv_nsec != UTIME_OMIT )
			timespec_to_fat(&times[1], &dirent->modified_date,
			                &dirent->modified_time, &tenths);
		if ( data_block )
			data_block->FinishWrite();
	}
}

uint64_t Inode::Size()
{
	if ( inode_id == filesystem->root_inode_id )
		return 0;
	if ( dirent->attributes & FAT_ATTRIBUTE_DIRECTORY )
		return 0;
	return dirent->size;
}

Block* Inode::GetClusterSector(uint32_t cluster, uint8_t sector)
{
	uint32_t block_id;
	if ( inode_id == filesystem->root_inode_id && filesystem->fat_type != 32 )
		block_id = filesystem->root_sector + cluster;
	else
		block_id = filesystem->data_sector +
		           (cluster - 2) * filesystem->bpb->sectors_per_cluster +
		           sector;
	return filesystem->device->GetBlock(block_id);
}

// TODO: Yes. Do review this function carefully.
bool Inode::Iterate(Block** block_ptr, uint32_t* cluster_ptr,
                    uint8_t* sector_ptr, uint16_t* offset_ptr)
{
	// TODO: Restructure to cache this.
	if ( *block_ptr )
	{
		(*block_ptr)->Unref();
		*block_ptr = NULL;
	}
	if ( *offset_ptr == filesystem->bytes_per_sector )
	{
		*offset_ptr = 0;
		if ( inode_id == filesystem->root_inode_id &&
		     filesystem->fat_type != 32 )
		{
			// TODO: This kinda assumes the root directory is sector sized.
			uint32_t end = filesystem->root_dirent_count *
			               sizeof(struct fat_dirent);
			uint32_t end_lba = end / filesystem->bytes_per_sector;
			if ( end_lba <= *cluster_ptr )
				return errno = 0, false;
			(*cluster_ptr)++;
		}
		else
		{
			(*sector_ptr)++;
			if ( *sector_ptr == filesystem->bpb->sectors_per_cluster )
			{
				*sector_ptr = 0;
				*cluster_ptr = filesystem->ReadFAT(*cluster_ptr);
			}
		}
	}
	if ( inode_id != filesystem->root_inode_id || filesystem->fat_type == 32 )
	{
		if ( *cluster_ptr < 2 )
			return errno = EIO, false;
		if ( filesystem->eof_cluster <= *cluster_ptr )
			return errno = 0, false;
		if ( filesystem->eio_cluster <= *cluster_ptr )
			return errno = EIO, false;
	}
	if ( !(*block_ptr = GetClusterSector(*cluster_ptr, *sector_ptr)) )
		return false;
	return errno = 0, true;
}

uint32_t Inode::SeekCluster(uint32_t cluster_id)
{
	// TODO: Cache.
	uint32_t cluster = first_cluster;
	while ( cluster_id-- )
	{
		cluster = filesystem->ReadFAT(cluster);
		if ( cluster < 2 )
			return errno = EIO, filesystem->eio_cluster;
		if ( filesystem->eof_cluster <= cluster )
			return errno = EIO, filesystem->eio_cluster;
	}
	return cluster;
}

bool Inode::Truncate(uint64_t new_size_64)
{
	assert(filesystem->device->write);
	assert(S_ISREG(Mode()));
	uint32_t new_size = (uint32_t) new_size_64;
	if ( new_size_64 != new_size )
		return errno = E2BIG, false;
	uint32_t old_size = dirent->size;
	uint32_t pos = old_size < new_size ? old_size : new_size;
	uint32_t bytes_per_sector = filesystem->bytes_per_sector;
	uint32_t cluster_id = pos / filesystem->cluster_size;
	uint32_t cluster_offset = pos % filesystem->cluster_size;
	if ( cluster_id && !cluster_offset )
	{
		cluster_id--;
		cluster_offset = filesystem->cluster_size;
	}
	uint32_t cluster = SeekCluster(cluster_id);
	if ( cluster_id == filesystem->eio_cluster )
		return errno = EIO, false;
	if ( old_size < new_size )
	{
		while ( old_size < new_size )
		{
			if ( cluster_offset == filesystem->cluster_size )
			{
				// TODO: Zero the new sectors since the old contents may leak
				//       if we were to implement mmap.
				uint32_t next_cluster = filesystem->AllocateCluster();
				if ( !next_cluster )
					return false;
				filesystem->WriteFAT(next_cluster, filesystem->eof_cluster);
				filesystem->WriteFAT(cluster, next_cluster);
				cluster_offset = 0;
				cluster = next_cluster;
			}
			uint8_t sector = cluster_offset / bytes_per_sector;
			uint16_t sector_offset = cluster_offset % bytes_per_sector;
			Block* block = GetClusterSector(cluster, sector);
			if ( !block )
				return false;
			size_t left = new_size - old_size;
			size_t available = bytes_per_sector - sector_offset;
			size_t amount = left < available ? left : available;
			block->BeginWrite();
			memset(block->block_data + sector_offset, 0, amount);
			block->FinishWrite();
			old_size += amount;
			cluster_offset += amount;
			block->Unref();
		}
	}
	else if ( new_size < old_size )
	{
		uint32_t marker = filesystem->eof_cluster;
		while ( true )
		{
			uint32_t next_cluster = filesystem->ReadFAT(cluster);
			if ( next_cluster < 2 || filesystem->eio_cluster == next_cluster )
				return errno = EIO, false;
			if ( next_cluster != marker )
			{
				filesystem->WriteFAT(cluster, marker);
				filesystem->FreeCluster(next_cluster);
			}
			if ( filesystem->eof_cluster <= next_cluster )
				break;
			cluster = next_cluster;
			cluster_id++;
			marker = 0;
		}
	}
	else
		return true;
	if ( data_block )
		data_block->BeginWrite();
	dirent->size = new_size;
	if ( data_block )
		data_block->FinishWrite();
	return true;
}

Inode* Inode::Open(const char* elem, int flags, mode_t mode)
{
	if ( !S_ISDIR(Mode()) )
		return errno = ENOTDIR, (Inode*) NULL;
	if ( deleted )
		return errno = ENOENT, (Inode*) NULL;
	size_t elem_length = strlen(elem);
	if ( elem_length == 0 )
		return errno = ENOENT, (Inode*) NULL;
	if ( inode_id == filesystem->root_inode_id )
	{
		if ( !strcmp(elem, ".") || !strcmp(elem, "..") )
		{
			if ( (flags & O_CREAT) && (flags & O_EXCL) )
				return errno = EEXIST, (Inode*) NULL;
			if ( flags & O_WRITE && !filesystem->device->write )
				return errno = EROFS, (Inode*) NULL;
			Refer();
			return this;
			// TODO: Reopen the same inode.
		}
	}
	uint32_t cluster = first_cluster;
	uint8_t sector = 0;
	uint16_t offset = 0;
	Block* block = NULL;
	bool found_free = false;
	uint32_t free_cluster = 0;
	uint8_t free_sector = 0;
	uint16_t free_offset = 0;
	uint32_t last_cluster = 0;
	while ( Iterate(&block, &cluster, &sector, &offset) )
	{
		last_cluster = cluster;
		uint8_t* block_data = block->block_data + offset;
		struct fat_dirent* entry = (struct fat_dirent*) block_data;
		if ( !found_free &&
		     (!entry->name[0] || (unsigned char) entry->name[0] == 0xE5) )
		{
			found_free = true;
			free_cluster = cluster;
			free_sector = sector;
			free_offset = offset;
		}
		if ( !entry->name[0] )
			break;
		char name[8 + 1 + 3 + 1];
		if ( (unsigned char) entry->name[0] != 0xE5 &&
		     !(entry->attributes & FAT_ATTRIBUTE_VOLUME_ID) &&
		     (decode_8_3(entry->name, name), !strcmp(elem, name)) )
		{
			// TODO: Opening .. ends up with EIO failures.
			if ( (flags & O_CREAT) && (flags & O_EXCL) )
				return block->Unref(), errno = EEXIST, (Inode*) NULL;
			if ( (flags & O_DIRECTORY) &&
			     !(entry->attributes & FAT_ATTRIBUTE_DIRECTORY) )
				return block->Unref(), errno = ENOTDIR, (Inode*) NULL;
			uint32_t inode_id = entry->cluster_low | entry->cluster_high << 16;
			// TODO: If the inode is a directory, keep a reference open to this
			// parent directory so it can find the .. path back and keep track
			// of where the directory records with metadata is.
			Inode* inode = filesystem->GetInode(inode_id, block, entry);
			block->Unref();
			if ( !inode )
				return (Inode*) NULL;
			if ( flags & O_WRITE && !filesystem->device->write )
				return inode->Unref(), errno = EROFS, (Inode*) NULL;
			if ( S_ISREG(inode->Mode()) && (flags & O_WRITE) &&
			    (flags & O_TRUNC) && !inode->Truncate(0) )
				return (Inode*) NULL;
			return inode;
		}
		offset += sizeof(struct fat_dirent);
	}
	if ( block )
		block->Unref();
	if ( errno )
		return (Inode*) NULL;
	// TODO: Protect against . and ..
	// TODO: Switch to use Link()
	if ( flags & O_CREAT )
	{
		if ( !filesystem->device->write )
			return errno = EROFS, (Inode*) NULL;
		// TODO: Protect against . and ..
		if ( !is_8_3(elem) )
			return errno = ENAMETOOLONG, (Inode*) NULL;
		// TODO: Root directory support.
		if ( inode_id == filesystem->root_inode_id &&
		     filesystem->fat_type != 32 )
			return errno = ENOTSUP, (Inode*) NULL;
		if ( !found_free )
		{
			uint32_t new_cluster = filesystem->AllocateCluster();
			if ( !new_cluster )
				return (Inode*) NULL;
			for ( size_t i = 0; i < filesystem->bpb->sectors_per_cluster; i++ )
			{
				Block* block = GetClusterSector(new_cluster, i);
				if ( !block )
				{
					filesystem->FreeCluster(new_cluster);
					return (Inode*) NULL;
				}
				block->BeginWrite();
				memset(block->block_data, 0, filesystem->bytes_per_sector);
				block->FinishWrite();
				block->Unref();
			}
			filesystem->WriteFAT(new_cluster, filesystem->eof_cluster);
			filesystem->WriteFAT(last_cluster, new_cluster);
			found_free = true;
			free_cluster = new_cluster;
			free_sector = 0;
			free_offset = 0;
		}
		// TODO: Avoid shadowing with the class member.
		uint32_t inode_id = filesystem->AllocateCluster();
		// TODO: Actually zero this cluster entirely.
		if ( !inode_id )
			return (Inode*) NULL;
		mode_t attributes = mode & 0200 ? 0 : FAT_ATTRIBUTE_READ_ONLY;
		if ( S_ISDIR(mode) )
		{
			attributes |= FAT_ATTRIBUTE_DIRECTORY;
			Block* block = GetClusterSector(inode_id, 0);
			if ( !block )
				return filesystem->FreeCluster(inode_id), (Inode*) NULL;
			block->BeginWrite();
			memset(block->block_data, 0, filesystem->bytes_per_sector);
			struct fat_dirent* dirent = (struct fat_dirent*) block->block_data;
			// TODO: Mirror modified times in here.
			memcpy(dirent->name, ".          ", 11);
			dirent->attributes = attributes;
			dirent->cluster_high = htole16(inode_id >> 16);
			dirent->cluster_low = htole16(inode_id & 0xFFFF);
			dirent++;
			memcpy(dirent->name, "..         ", 11);
			dirent->attributes = FAT_ATTRIBUTE_DIRECTORY;
			if ( this->inode_id == filesystem->root_inode_id )
			{
				dirent->cluster_high = htole16(0);
				dirent->cluster_low = htole16(0);
			}
			else
			{
				dirent->cluster_high = htole16(this->inode_id >> 16);
				dirent->cluster_low = htole16(this->inode_id & 0xFFFF);
			}
			block->FinishWrite();
			block->Unref();
		}
		Block* block = GetClusterSector(free_cluster, free_sector);
		if ( !block )
			return filesystem->FreeCluster(inode_id), (Inode*) NULL;
		filesystem->WriteFAT(inode_id, filesystem->eof_cluster);
		struct timespec now;
		clock_gettime(CLOCK_REALTIME, &now);
		block->BeginWrite();
		struct fat_dirent* dirent =
			(struct fat_dirent*) (block->block_data + free_offset);
		encode_8_3(elem, dirent->name);
		dirent->attributes = attributes;
		dirent->creation_tenths = timespec_to_fat_tenths(&now);
		dirent->creation_time = htole16(timespec_to_fat_time(&now));
		dirent->creation_date = htole16(timespec_to_fat_date(&now));
		dirent->access_date = dirent->creation_date;
		dirent->cluster_high = htole16(inode_id >> 16);
		dirent->modified_time = dirent->creation_time;
		dirent->modified_date = dirent->creation_date;
		dirent->cluster_low = htole16(inode_id & 0xFFFF);
		dirent->size = htole16(0);
		block->FinishWrite();
		Inode* inode = filesystem->GetInode(inode_id, block, dirent);
		block->Unref();
		return inode;
	}
	return errno = ENOENT, (Inode*) NULL;
}

bool Inode::Link(const char* elem, Inode* dest, bool directories)
{
	if ( !S_ISDIR(Mode()) )
		return errno = ENOTDIR, false;
	if ( deleted )
		return errno = ENOENT, false;
	if ( directories && !S_ISDIR(dest->Mode()) )
		return errno = ENOTDIR, false;
	if ( !directories && S_ISDIR(dest->Mode()) )
		return errno = EISDIR, false;
	if ( !filesystem->device->write )
		return errno = EROFS, false;
	size_t elem_length = strlen(elem);
	if ( elem_length == 0 )
		return errno = ENOENT, false;
	uint32_t cluster = first_cluster;
	uint8_t sector = 0;
	uint16_t offset = 0;
	Block* block = NULL;
	bool found_free = false;
	uint32_t free_cluster = 0;
	uint8_t free_sector = 0;
	uint16_t free_offset = 0;
	uint32_t last_cluster = 0;
	while ( Iterate(&block, &cluster, &sector, &offset) )
	{
		last_cluster = cluster;
		uint8_t* block_data = block->block_data + offset;
		struct fat_dirent* entry = (struct fat_dirent*) block_data;
		if ( !found_free &&
		     (!entry->name[0] || (unsigned char) entry->name[0] == 0xE5) )
		{
			found_free = true;
			free_cluster = cluster;
			free_sector = sector;
			free_offset = offset;
		}
		if ( !entry->name[0] )
			break;
		char name[8 + 1 + 3 + 1];
		if ( (unsigned char) entry->name[0] != 0xE5 &&
		     !(entry->attributes & FAT_ATTRIBUTE_VOLUME_ID) &&
		     (decode_8_3(entry->name, name), !strcmp(elem, name)) )
			return block->Unref(), errno = EEXIST, false;
		offset += sizeof(struct fat_dirent);
	}
	if ( block )
		block->Unref();
	if ( errno )
		return false;
	// Files can only have a single link.
	if ( !dest->deleted && !directories )
		return errno = EPERM, false;
	if ( !is_8_3(elem) )
		return errno = ENAMETOOLONG, false;
	// TODO: Root directory support.
	if ( inode_id == filesystem->root_inode_id &&
	     filesystem->fat_type != 32 )
		return errno = ENOTSUP, false;
	if ( !found_free )
	{
		uint32_t new_cluster = filesystem->AllocateCluster();
		if ( !new_cluster )
			return (Inode*) NULL;
		for ( size_t i = 0; i < filesystem->bpb->sectors_per_cluster; i++ )
		{
			Block* block = GetClusterSector(new_cluster, i);
			if ( !block )
			{
				filesystem->FreeCluster(new_cluster);
				return false;
			}
			block->BeginWrite();
			memset(block->block_data, 0, filesystem->bytes_per_sector);
			block->FinishWrite();
			block->Unref();
		}
		filesystem->WriteFAT(new_cluster, filesystem->eof_cluster);
		filesystem->WriteFAT(last_cluster, new_cluster);
		found_free = true;
		free_cluster = new_cluster;
		free_sector = 0;
		free_offset = 0;
	}
	block = GetClusterSector(free_cluster, free_sector);
	if ( !block )
		return false;
	block->BeginWrite();
	struct fat_dirent* dirent =
		(struct fat_dirent*) (block->block_data + free_offset);
	if ( strcmp(elem, ".") != 0 && strcmp(elem, "..") != 0 )
	{
		assert(dest->deleted);
		memcpy(dirent, dest->dirent, sizeof(*dirent));
		dest->dirent = dirent;
		dest->data_block = block;
		block->Refer();
		dest->deleted = false;
	}
	else
	{
		memset(dirent, 0, sizeof(*dirent));
		dirent->attributes = FAT_ATTRIBUTE_DIRECTORY;
	}
	encode_8_3(elem, dirent->name);
	dirent->cluster_high = htole16(dest->inode_id >> 16);
	dirent->cluster_low = htole16(dest->inode_id & 0xFFFF);
	block->FinishWrite();
	Modified();
	return true;
}

Inode* Inode::UnlinkKeep(const char* elem, bool directories, bool force)
{
	// TODO: It looks like it's assumed . and .. will never get here. Except
	// that can happen with force during rename?
	Inode* inode = Open(elem, O_WRITE, 0);
	if ( !inode )
		return NULL;

	if ( !force && directories && !S_ISDIR(inode->Mode()) )
		return inode->Unref(), errno = ENOTDIR, (Inode*) NULL;
	if ( !force && directories && !inode->IsEmptyDirectory() )
		return inode->Unref(), errno = ENOTEMPTY, (Inode*) NULL;
	if ( !force && !directories && S_ISDIR(inode->Mode()) )
		return inode->Unref(), errno = EISDIR, (Inode*) NULL;
	if ( !filesystem->device->write )
		return inode->Unref(), errno = EROFS, (Inode*) NULL;

	if ( strcmp(elem, ".") != 0 && strcmp(elem, "..") != 0 )
	{
		assert(!inode->deleted);
		inode->data_block->BeginWrite();
		inode->dirent->name[0] = (char) 0xE5;
		memcpy(&inode->deleted_dirent, inode->dirent, sizeof(struct fat_dirent));
		inode->dirent = &inode->deleted_dirent;
		inode->data_block->FinishWrite();
		inode->data_block->Unref();
		inode->data_block = NULL;
		inode->deleted = true;
	}

	// TODO: If dirs keep ref to their parent dir alive, unref it here.

	Modified();

	return inode;
}

bool Inode::Unlink(const char* elem, bool directories, bool force)
{
	Inode* result = UnlinkKeep(elem, directories, force);
	if ( !result )
		return false;
	result->Unref();
	return true;
}

ssize_t Inode::ReadAt(uint8_t* buf, size_t s_count, off_t o_offset)
{
	if ( !S_ISREG(Mode()) )
		return errno = EISDIR, -1;
	if ( o_offset < 0 )
		return errno = EINVAL, -1;
	if ( SSIZE_MAX < s_count )
		s_count = SSIZE_MAX;
	// TODO: Downgrade to 32-bit.
	uint64_t sofar = 0;
	uint64_t count = (uint64_t) s_count;
	uint64_t offset = (uint64_t) o_offset;
	uint32_t file_size = Size();
	if ( file_size <= offset )
		return 0;
	if ( file_size - offset < count )
		count = file_size - offset;
	if ( !count )
		return 0;
	uint32_t cluster_id = offset / filesystem->cluster_size;
	uint32_t cluster_offset = offset % filesystem->cluster_size;
	uint32_t cluster = SeekCluster(cluster_id);
	if ( filesystem->eio_cluster <= cluster )
		return -1;
	while ( sofar < count )
	{
		if ( filesystem->cluster_size <= cluster_offset )
		{
			cluster = filesystem->ReadFAT(cluster);
			// TODO: Better checks.
			if ( filesystem->eio_cluster <= cluster )
				return sofar ? sofar : (errno = EIO, -1);
			cluster_offset = 0;
			cluster_id++;
		}
		uint8_t sector = cluster_offset / filesystem->bytes_per_sector;
		uint16_t block_offset = cluster_offset % filesystem->bytes_per_sector;
		uint32_t block_left = filesystem->bytes_per_sector - block_offset;
		Block* block = GetClusterSector(cluster, sector);
		if ( !block )
			return sofar ? sofar : -1;
		size_t amount = count - sofar < block_left ? count - sofar : block_left;
		memcpy(buf + sofar, block->block_data + block_offset, amount);
		sofar += amount;
		cluster_offset += amount;
		block->Unref();
	}
	return (ssize_t) sofar;
}

ssize_t Inode::WriteAt(const uint8_t* buf, size_t s_count, off_t o_offset)
{
	if ( !S_ISREG(Mode()) )
		return errno = EISDIR, -1;
	if ( o_offset < 0 )
		return errno = EINVAL, -1;
	if ( !filesystem->device->write )
		return errno = EROFS, -1;
	if ( SSIZE_MAX < s_count )
		s_count = SSIZE_MAX;
	Modified();
	// TODO: Downgrade to 32-bit.
	uint64_t sofar = 0;
	uint64_t count = (uint64_t) s_count;
	uint64_t offset = (uint64_t) o_offset;
	uint32_t file_size = Size();
	uint64_t end_at = offset + count;
	if ( offset < end_at )
		/* TODO: Overflow! off_t overflow? */{};
	if ( file_size < end_at && !Truncate(end_at) )
		return -1;
	uint32_t cluster_id = offset / filesystem->cluster_size;
	uint32_t cluster_offset = offset % filesystem->cluster_size;
	uint32_t cluster = SeekCluster(cluster_id);
	if ( filesystem->eio_cluster <= cluster )
		return -1;
	while ( sofar < count )
	{
		if ( filesystem->cluster_size <= cluster_offset )
		{
			cluster = filesystem->ReadFAT(cluster);
			// TODO: Better checks.
			if ( filesystem->eio_cluster <= cluster )
				return sofar ? sofar : (errno = EIO, -1);
			cluster_offset = 0;
			cluster_id++;
		}
		uint8_t sector = cluster_offset / filesystem->bytes_per_sector;
		uint16_t block_offset = cluster_offset % filesystem->bytes_per_sector;
		uint32_t block_left = filesystem->bytes_per_sector - block_offset;
		Block* block = GetClusterSector(cluster, sector);
		if ( !block )
			return sofar ? sofar : -1;
		size_t amount = count - sofar < block_left ? count - sofar : block_left;
		block->BeginWrite();
		memcpy(block->block_data + block_offset, buf + sofar, amount);
		block->FinishWrite();
		sofar += amount;
		cluster_offset += amount;
		block->Unref();
	}
	return (ssize_t) sofar;
}

bool Inode::Rename(Inode* olddir, const char* oldname, const char* newname)
{
	if ( deleted )
		return errno = ENOENT, false;
	if ( !strcmp(oldname, ".") || !strcmp(oldname, "..") ||
	     !strcmp(newname, ".") || !strcmp(newname, "..") )
		return errno = EPERM, false;
	Inode* src_inode = olddir->Open(oldname, O_RDONLY, 0);
	if ( !src_inode )
		return false;
	// TODO: Verify src_inode is not a subdir of this dir.
	if ( Inode* dst_inode = Open(newname, O_RDONLY, 0) )
	{
		bool same_inode = src_inode->inode_id == dst_inode->inode_id;
		dst_inode->Unref();
		if ( same_inode )
			return src_inode->Unref(), true;
	}
	// TODO: Prove that this cannot fail and handle such a situation.
	if ( S_ISDIR(src_inode->Mode()) )
	{
		if ( !Unlink(newname, true) && errno != ENOENT )
			return src_inode->Unref(), false;
		olddir->Unlink(oldname, true, true);
		Link(newname, src_inode, true);
		if ( olddir != this )
		{
			src_inode->Unlink("..", true, true);
			src_inode->Link("..", this, true);
		}
	}
	else
	{
		if ( !Unlink(newname, false) && errno != ENOENT )
			return src_inode->Unref(), false;
		olddir->Unlink(oldname, false);
		Link(newname, src_inode, false);
	}

	src_inode->Unref();
	return true;
}

bool Inode::Symlink(const char* elem, const char* dest)
{
	(void) elem;
	(void) dest;
	if ( !filesystem->device->write )
		return errno = EROFS, false;
	return errno = EPERM, false;
}

Inode* Inode::CreateDirectory(const char* path, mode_t mode)
{
	return Open(path, O_CREAT | O_EXCL, mode | S_IFDIR);
}

bool Inode::RemoveDirectory(const char* path)
{
	Inode* result = UnlinkKeep(path, true);
	if ( !result )
		return false;
	// There is no need to remove the . and .. and entries since there is no
	// link count and the directory is empty. We can just discard the data.
	result->Unref();
	return true;
}

bool Inode::IsEmptyDirectory()
{
	if ( !S_ISDIR(Mode()) )
		return errno = ENOTDIR, false;
	if ( deleted )
		return errno = ENOENT, false;
	if ( inode_id == filesystem->root_inode_id )
		return false;
	uint32_t cluster = first_cluster;
	uint8_t sector = 0;
	uint16_t offset = 0;
	Block* block = NULL;
	while ( Iterate(&block, &cluster, &sector, &offset) )
	{
		uint8_t* block_data = block->block_data + offset;
		struct fat_dirent* entry = (struct fat_dirent*) block_data;
		if ( !entry->name[0] )
			break;
		char name[8 + 1 + 3 + 1];
		if ( (unsigned char) entry->name[0] != 0xE5 &&
		     !(entry->attributes & FAT_ATTRIBUTE_VOLUME_ID) &&
		     (decode_8_3(entry->name, name),
              strcmp(name, ".") != 0 && strcmp(name, "..") != 0) )
			return block->Unref(), false;
		offset += sizeof(struct fat_dirent);
	}
	if ( block )
		block->Unref();
	if ( errno )
		return false;
	return true;
}

void Inode::Delete()
{
	assert(deleted);
	assert(dirent->name[0] == 0x00 || (unsigned char) dirent->name[0] == 0xE5);
	assert(!reference_count);
	assert(!remote_reference_count);
	uint32_t cluster = first_cluster;
	while ( true )
	{
		if ( cluster < 2 || filesystem->eio_cluster == cluster )
			break;
		if ( filesystem->eof_cluster <= cluster )
			break;
		uint32_t next_cluster = filesystem->ReadFAT(cluster);
		filesystem->WriteFAT(cluster, 0);
		filesystem->FreeCluster(cluster);
		cluster = next_cluster;
	}
}

void Inode::Refer()
{
	reference_count++;
}

void Inode::Unref()
{
	assert(0 < reference_count);
	reference_count--;
	if ( !reference_count && !remote_reference_count )
	{
		if ( deleted )
			Delete();
		delete this;
	}
}

void Inode::RemoteRefer()
{
	remote_reference_count++;
}

void Inode::RemoteUnref()
{
	assert(0 < remote_reference_count);
	remote_reference_count--;
	if ( !reference_count && !remote_reference_count )
	{
		if ( deleted )
			Delete();
		delete this;
	}
}

void Inode::Modified()
{
	if ( inode_id == filesystem->root_inode_id )
		return;
	struct timespec now;
	clock_gettime(CLOCK_REALTIME, &now);
	if ( data_block )
		data_block->BeginWrite();
	uint8_t tenths;
	timespec_to_fat(&now, &dirent->modified_date, &dirent->modified_time,
	                &tenths);
	if ( data_block )
		data_block->FinishWrite();
}

void Inode::BeginWrite()
{
	if ( data_block )
		data_block->BeginWrite();
}

// TODO: Uh. Use these?
void Inode::FinishWrite()
{
	//struct timespec now;
	//clock_gettime(CLOCK_REALTIME, &now);
	//data->i_ctime = now.tv_sec;
	if ( !dirty )
	{
		dirty = true;
		prev_dirty = NULL;
		next_dirty = filesystem->dirty_inode;
		if ( next_dirty )
			next_dirty->prev_dirty = this;
		filesystem->dirty_inode = this;
	}
	if ( data_block )
		data_block->FinishWrite();
	Use();
}

void Inode::Sync()
{
	if ( !dirty )
		return;
	if ( data_block )
		data_block->Sync();
	// TODO: The inode contents needs to be sync'd as well!
	(prev_dirty ? prev_dirty->next_dirty : filesystem->dirty_inode) = next_dirty;
	if ( next_dirty )
		next_dirty->prev_dirty = prev_dirty;
	prev_dirty = NULL;
	next_dirty = NULL;
	dirty = false;
}

void Inode::Use()
{
	if ( data_block )
		data_block->Use();
	Unlink();
	Prelink();
}

void Inode::Unlink()
{
	(prev_inode ? prev_inode->next_inode : filesystem->mru_inode) = next_inode;
	(next_inode ? next_inode->prev_inode : filesystem->lru_inode) = prev_inode;
	size_t bin = inode_id % INODE_HASH_LENGTH;
	(prev_hashed ? prev_hashed->next_hashed : filesystem->hash_inodes[bin]) = next_hashed;
	if ( next_hashed ) next_hashed->prev_hashed = prev_hashed;
}

void Inode::Prelink()
{
	prev_inode = NULL;
	next_inode = filesystem->mru_inode;
	if ( filesystem->mru_inode )
		filesystem->mru_inode->prev_inode = this;
	filesystem->mru_inode = this;
	if ( !filesystem->lru_inode )
		filesystem->lru_inode = this;
	size_t bin = inode_id % INODE_HASH_LENGTH;
	prev_hashed = NULL;
	next_hashed = filesystem->hash_inodes[bin];
	filesystem->hash_inodes[bin] = this;
	if ( next_hashed )
		next_hashed->prev_hashed = this;
}
