/*
 * Copyright (c) 2013, 2014, 2015, 2018 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 <fcntl.h>
#include <limits.h>
#include <pthread.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <time.h>

#include "ext-constants.h"
#include "ext-structs.h"

#include "block.h"
#include "blockgroup.h"
#include "device.h"
#include "extfs.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->data = NULL;
	this->filesystem = filesystem;
	this->reference_count = 1;
	this->remote_reference_count = 0;
	this->inode_id = inode_id;
	this->dirty = false;
}

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

uint32_t Inode::Mode()
{
	// TODO: Use i_mode_high.
	return data->i_mode;
}

void Inode::SetMode(uint32_t mode)
{
	assert(filesystem->device->write);
	BeginWrite();
	// TODO: Use i_mode_high.
	data->i_mode = (uint16_t) mode;
	FinishWrite();
}

uint32_t Inode::UserId()
{
	// TODO: Use i_uid_high.
	return data->i_uid;
}

void Inode::SetUserId(uint32_t user)
{
	assert(filesystem->device->write);
	BeginWrite();
	// TODO: Use i_uid_high.
	data->i_uid = (uint16_t) user;
	FinishWrite();
}

uint32_t Inode::GroupId()
{
	// TODO: Use i_gid_high.
	return data->i_gid;
}

void Inode::SetGroupId(uint32_t group)
{
	assert(filesystem->device->write);
	BeginWrite();
	// TODO: Use i_gid_high.
	data->i_gid = (uint16_t) group;
	FinishWrite();
}

uint64_t Inode::Size()
{
	bool largefile = filesystem->sb->s_feature_ro_compat &
	                 EXT2_FEATURE_RO_COMPAT_LARGE_FILE;
	if ( !EXT2_S_ISREG(data->i_mode) || !largefile )
		return data->i_size;
	uint64_t lower = data->i_size;
	uint64_t upper = data->i_dir_acl;
	uint64_t size = lower | (upper << 32ULL);
	return size;
}

void Inode::SetSize(uint64_t new_size)
{
	assert(filesystem->device->write);
	bool largefile = filesystem->sb->s_feature_ro_compat &
	                 EXT2_FEATURE_RO_COMPAT_LARGE_FILE;
	uint32_t lower = new_size & 0xFFFFFFFF;
	uint32_t upper = new_size >> 32;
	// TODO: Enforce filesize limit with no largefile support or non-files.
	data->i_size = lower;

	// TODO: Figure out these i_blocks semantics and how stuff is reserved.
	const uint64_t ENTRIES = filesystem->block_size / sizeof(uint32_t);
	uint64_t block_direct = sizeof(data->i_block) / sizeof(uint32_t) - 3;
	uint64_t block_singly = ENTRIES;
	uint64_t block_doubly = block_singly * block_singly;
	uint64_t max_direct = block_direct;
	uint64_t max_singly = max_direct + block_singly;
	uint64_t max_doubly = max_singly + block_doubly;
	uint64_t logical_blocks = divup(new_size, (uint64_t) filesystem->block_size);
	uint64_t actual_blocks  = logical_blocks;
	if ( max_direct <= logical_blocks )
		actual_blocks += divup(logical_blocks - max_direct, ENTRIES);
	if ( max_singly <= logical_blocks )
		actual_blocks += divup(logical_blocks - max_singly, ENTRIES * ENTRIES);
	if ( max_doubly <= logical_blocks )
		actual_blocks += divup(logical_blocks - max_doubly, ENTRIES * ENTRIES * ENTRIES);

	BeginWrite();
	data->i_blocks = (actual_blocks * filesystem->block_size) / 512;
	if ( EXT2_S_ISREG(data->i_mode) && largefile )
		data->i_dir_acl = upper;
	FinishWrite();

	Modified();
}

Block* Inode::GetBlockFromTable(Block* table, uint32_t index)
{
	if ( uint32_t block_id = ((uint32_t*) table->block_data)[index] )
		return filesystem->device->GetBlock(block_id);
	// TODO: If in read only mode, then perhaps return a zero block here.
	if ( !filesystem->device->write )
		return NULL;
	uint32_t group_id = (inode_id - 1) / filesystem->sb->s_inodes_per_group;
	assert(group_id < filesystem->num_groups);
	BlockGroup* block_group = filesystem->GetBlockGroup(group_id);
	uint32_t block_id = filesystem->AllocateBlock(block_group);
	block_group->Unref();
	if ( block_id )
	{
		Block* block = filesystem->device->GetBlockZeroed(block_id);
		table->BeginWrite();
		((uint32_t*) table->block_data)[index] = block_id;
		table->FinishWrite();
		return block;
	}
	return NULL;
}

Block* Inode::GetBlock(uint64_t offset)
{
	const uint64_t ENTRIES = filesystem->block_size / sizeof(uint32_t);
	uint64_t block_direct = sizeof(data->i_block) / sizeof(uint32_t) - 3;
	uint64_t block_singly = ENTRIES;
	uint64_t block_doubly = block_singly * block_singly;
	uint64_t block_triply = block_doubly * block_singly;
	uint64_t max_direct = block_direct;
	uint64_t max_singly = max_direct + block_singly;
	uint64_t max_doubly = max_singly + block_doubly;
	uint64_t max_triply = max_doubly + block_triply;
	uint32_t index;

	Block* table = data_block; table->Refer();
	Block* block;

	uint32_t inode_offset = (uintptr_t) data - (uintptr_t) data_block->block_data;
	uint32_t table_offset = offsetof(struct ext_inode, i_block);
	uint32_t inode_block_table_offset = (inode_offset + table_offset) / sizeof(uint32_t);

	// TODO: It would seem that somebody needs a lesson in induction. :-)
	if ( offset < max_direct )
	{
		offset -= 0;
		offset += inode_block_table_offset * 1;
	read_direct:
		index = offset;
		offset %= 1;
		block = GetBlockFromTable(table, index);
		table->Unref();
		if ( !block )
			return NULL;
		return block;
	}
	else if ( offset < max_singly )
	{
		offset -= max_direct;
		offset += (inode_block_table_offset + 12) * ENTRIES;
	read_singly:
		index = offset / ENTRIES;
		offset = offset % ENTRIES;
		block = GetBlockFromTable(table, index);
		table->Unref();
		if ( !block )
			return NULL;
		table = block;
		goto read_direct;
	}
	else if ( offset < max_doubly )
	{
		offset -= max_singly;
		offset += (inode_block_table_offset + 13) * ENTRIES * ENTRIES;
	read_doubly:
		index = offset / (ENTRIES * ENTRIES);
		offset = offset % (ENTRIES * ENTRIES);
		block = GetBlockFromTable(table, index);
		table->Unref();
		if ( !block )
			return NULL;
		table = block;
		goto read_singly;
	}
	else if ( offset < max_triply )
	{
		offset -= max_doubly;
		offset += (inode_block_table_offset + 14) * ENTRIES * ENTRIES * ENTRIES;
	/*read_triply:*/
		index = offset / (ENTRIES * ENTRIES * ENTRIES);
		offset = offset % (ENTRIES * ENTRIES * ENTRIES);
		block = GetBlockFromTable(table, index);
		table->Unref();
		if ( !block )
			return NULL;
		table = block;
		goto read_doubly;
	}

	table->Unref();
	return NULL;
}

bool Inode::FreeIndirect(uint64_t from, uint64_t offset, uint32_t block_id,
                         int indirection, uint64_t entry_span)
{
	assert(filesystem->device->write);
	const uint64_t ENTRIES = filesystem->block_size / sizeof(uint32_t);
	Block* block = filesystem->device->GetBlock(block_id);
	uint32_t* table = (uint32_t*) block->block_data;
	bool any_children = false;
	bool begun_writing = false;
	for ( uint64_t i = 0; i < ENTRIES; i++ )
	{
		if ( !table[i] )
			continue;
		uint64_t entry_offset = offset + entry_span * i;
		if ( entry_offset < from ||
		     (indirection &&
		      FreeIndirect(from, entry_offset, table[i], indirection-1,
		                   entry_span / ENTRIES)) )
		{
			any_children = true;
			continue;
		}
		filesystem->FreeBlock(table[i]);
		if ( !begun_writing )
		{
			block->BeginWrite();
			begun_writing = true;
		}
		table[i] = 0;
	}
	if ( begun_writing )
		block->FinishWrite();
	return any_children;
}

void Inode::Truncate(uint64_t new_size)
{
	assert(filesystem->device->write);
	uint64_t old_size = Size();
	bool could_be_embedded = old_size == 0 && EXT2_S_ISLNK(Mode()) && !data->i_blocks;
	bool is_embedded = 0 < old_size && old_size <= 60 && !data->i_blocks;
	if ( could_be_embedded || is_embedded )
	{
		if ( new_size <= 60 )
		{
			data_block->BeginWrite();
			unsigned char* block_data = (unsigned char*) &data->i_block[0];
			if ( old_size < new_size )
				memset(block_data + old_size, 0, new_size - old_size);
			if ( new_size < old_size )
				memset(block_data + new_size, 0, old_size - new_size);
			data->i_size = new_size;
			data_block->FinishWrite();
			return;
		}
		if ( is_embedded && !UnembedInInode() )
		{
			// Truncate() can't fail ATM so lose data instead of worse stuff.
			data_block->BeginWrite();
			unsigned char* block_data = (unsigned char*) &data->i_block[0];
			memset(block_data, 0, 60);
			data->i_size = 0;
			data_block->FinishWrite();
		}
	}
	// TODO: Enforce a filesize limit!
	SetSize(new_size);
	if ( old_size <= new_size )
		return;

	uint64_t old_num_blocks = divup(old_size, (uint64_t) filesystem->block_size);
	uint64_t new_num_blocks = divup(new_size, (uint64_t) filesystem->block_size);

	// Zero out the rest of the last partial block, if any.
	uint32_t partial = new_size % filesystem->block_size;
	if ( partial )
	{
		Block* partial_block = GetBlock(new_num_blocks-1);
		uint8_t* data = partial_block->block_data;
		partial_block->BeginWrite();
		memset(data + partial, 0, filesystem->block_size - partial);
		partial_block->FinishWrite();
	}

	const uint64_t ENTRIES = filesystem->block_size / sizeof(uint32_t);
	uint64_t block_direct = sizeof(data->i_block) / sizeof(uint32_t) - 3;
	uint64_t block_singly = ENTRIES;
	uint64_t block_doubly = block_singly * block_singly;
	uint64_t block_triply = block_doubly * block_singly;
	uint64_t max_direct = block_direct;
	uint64_t max_singly = max_direct + block_singly;
	uint64_t max_doubly = max_singly + block_doubly;
	uint64_t max_triply = max_doubly + block_triply;

	BeginWrite();

	for ( uint64_t i = new_num_blocks; i < old_num_blocks && i < 12; i++ )
	{
		if ( !data->i_block[i] )
			continue;
		filesystem->FreeBlock(data->i_block[i]);
		data->i_block[i] = 0;
	}

	if ( data->i_block[12] && !FreeIndirect(new_num_blocks, max_direct, data->i_block[12], 0, 1) )
	{
		filesystem->FreeBlock(data->i_block[12]);
		data->i_block[12] = 0;
	}

	if ( data->i_block[13] && !FreeIndirect(new_num_blocks, max_singly, data->i_block[13], 1, ENTRIES) )
	{
		filesystem->FreeBlock(data->i_block[13]);
		data->i_block[13] = 0;
	}

	if ( data->i_block[14] && !FreeIndirect(new_num_blocks, max_doubly, data->i_block[14], 2, ENTRIES * ENTRIES) )
	{
		filesystem->FreeBlock(data->i_block[14]);
		data->i_block[14] = 0;
	}

	(void) max_triply;

	FinishWrite();
}

Inode* Inode::Open(const char* elem, int flags, mode_t mode)
{
	if ( !EXT2_S_ISDIR(Mode()) )
		return errno = ENOTDIR, (Inode*) NULL;
	size_t elem_length = strlen(elem);
	if ( elem_length == 0 )
		return errno = ENOENT, (Inode*) NULL;
	uint64_t filesize = Size();
	uint64_t offset = 0;
	Block* block = NULL;
	uint64_t block_id = 0;
	while ( offset < filesize )
	{
		uint64_t entry_block_id = offset / filesystem->block_size;
		uint64_t entry_block_offset = offset % filesystem->block_size;
		if ( block && block_id != entry_block_id )
			block->Unref(),
			block = NULL;
		if ( !block && !(block = GetBlock(block_id = entry_block_id)) )
			return NULL;
		const uint8_t* block_data = block->block_data + entry_block_offset;
		const struct ext_dirent* entry = (const struct ext_dirent*) block_data;
		if ( entry->inode &&
		     entry->name_len == elem_length &&
		     memcmp(elem, entry->name, elem_length) == 0 )
		{
			uint8_t file_type = EXT2_FT_UNKNOWN;
			if ( filesystem->sb->s_feature_incompat & EXT2_FEATURE_INCOMPAT_FILETYPE )
				file_type = entry->file_type;
			uint32_t inode_id = entry->inode;
			block->Unref();
			if ( (flags & O_CREAT) && (flags & O_EXCL) )
				return errno = EEXIST, (Inode*) NULL;
			if ( (flags & O_DIRECTORY) &&
			     file_type != EXT2_FT_UNKNOWN &&
			     file_type != EXT2_FT_DIR &&
			     file_type != EXT2_FT_SYMLINK )
				return errno = ENOTDIR, (Inode*) NULL;
			Inode* inode = filesystem->GetInode(inode_id);
			if ( !inode )
				return (Inode*) NULL;
			if ( flags & O_DIRECTORY &&
			     !EXT2_S_ISDIR(inode->Mode()) &&
			     !EXT2_S_ISLNK(inode->Mode()) )
			{
				inode->Unref();
				return errno = ENOTDIR, (Inode*) NULL;
			}
			if ( flags & O_WRITE && !filesystem->device->write )
			{
				inode->Unref();
				return errno = EROFS, (Inode*) NULL;
			}
			if ( S_ISREG(inode->Mode()) && flags & O_WRITE && flags & O_TRUNC )
				inode->Truncate(0);
			return inode;
		}
		offset += entry->reclen;
	}
	if ( block )
		block->Unref();
	if ( flags & O_CREAT )
	{
		if ( !filesystem->device->write )
			return errno = EROFS, (Inode*) NULL;
		// TODO: Preferred block group!
		uint32_t result_inode_id = filesystem->AllocateInode();
		if ( !result_inode_id )
			return NULL;
		Inode* result = filesystem->GetInode(result_inode_id);
		if ( !result )
			return filesystem->FreeInode(result_inode_id), (Inode*) NULL;
		struct timespec now;
		clock_gettime(CLOCK_REALTIME, &now);
		result->BeginWrite();
		memset(result->data, 0, sizeof(*result->data));
		result->data->i_atime = now.tv_sec;
		result->data->i_ctime = now.tv_sec;
		result->data->i_mtime = now.tv_sec;
		// TODO: Set all the other inode properties!
		result->FinishWrite();
		result->SetMode((mode & S_SETABLE) | S_IFREG);
		result->SetUserId(request_uid);
		result->SetGroupId(request_gid);
		if ( !Link(elem, result, false) )
		{
			result->Unref();
			return NULL;
		}
		return result;
	}
	return errno = ENOENT, (Inode*) NULL;
}

bool Inode::Link(const char* elem, Inode* dest, bool directories)
{
	if ( !EXT2_S_ISDIR(Mode()) )
		return errno = ENOTDIR, false;
	if ( directories && !EXT2_S_ISDIR(dest->Mode()) )
		return errno = ENOTDIR, false;
	if ( !directories && EXT2_S_ISDIR(dest->Mode()) )
		return errno = EISDIR, false;

	// Search for a hole in which we can store the new directory entry and stop
	// if we meet an existing link with the requested name.
	size_t elem_length = strlen(elem);
	if ( elem_length == 0 )
		return errno = ENOENT, false;
	size_t new_entry_size = roundup(sizeof(struct ext_dirent) + elem_length, (size_t) 4);
	uint64_t filesize = Size();
	uint64_t offset = 0;
	Block* block = NULL;
	uint64_t block_id = 0;
	bool found_hole = false;
	bool splitting = false;
	uint64_t hole_block_id = 0;
	uint64_t hole_block_offset = 0;
	while ( offset < filesize )
	{
		uint64_t entry_block_id = offset / filesystem->block_size;
		uint64_t entry_block_offset = offset % filesystem->block_size;
		if ( block && block_id != entry_block_id )
			block->Unref(),
			block = NULL;
		if ( !block && !(block = GetBlock(block_id = entry_block_id)) )
			return NULL;
		const uint8_t* block_data = block->block_data + entry_block_offset;
		const struct ext_dirent* entry = (const struct ext_dirent*) block_data;
		if ( entry->name_len == elem_length &&
		     memcmp(elem, entry->name, elem_length) == 0 )
		{
			block->Unref();
			return errno = EEXIST, false;
		}
		if ( !found_hole )
		{
			size_t entry_size = roundup(sizeof(struct ext_dirent) + entry->name_len, (size_t) 4);
			if ( (!entry->inode || !entry->name[0]) && new_entry_size <= entry->reclen )
			{
				hole_block_id = entry_block_id;
				hole_block_offset = entry_block_offset;
				new_entry_size = entry->reclen;
				found_hole = true;
			}
			else if ( new_entry_size <= entry->reclen - entry_size )
			{
				hole_block_id = entry_block_id;
				hole_block_offset = entry_block_offset;
				new_entry_size = entry->reclen - entry_size;
				splitting = true;
				found_hole = true;
			}
		}
		offset += entry->reclen;
	}

	if ( !filesystem->device->write )
		return errno = EROFS, false;

	if ( UINT16_MAX <= dest->data->i_links_count )
		return errno = EMLINK, false;

	if ( 255 < elem_length )
		return errno = ENAMETOOLONG, false;

	// We'll append another block if we failed to find a suitable hole.
	if ( !found_hole )
	{
		hole_block_id = filesize / filesystem->block_size;
		hole_block_offset = filesize % filesystem->block_size;
		new_entry_size = filesystem->block_size;
	}

	if ( block && block_id != hole_block_id )
		block->Unref(),
		block = NULL;
	if ( !block && !(block = GetBlock(block_id = hole_block_id)) )
		return NULL;

	Modified();

	block->BeginWrite();

	uint8_t* block_data = block->block_data + hole_block_offset;
	struct ext_dirent* entry = (struct ext_dirent*) block_data;

	// If we found a directory entry with room at the end, split it!
	if ( splitting )
	{
		entry->reclen = roundup(sizeof(struct ext_dirent) + entry->name_len, (size_t) 4);
		assert(entry->reclen);
		block_data += entry->reclen;
		entry = (struct ext_dirent*) block_data;
	}

	// Write the new directory entry.
	entry->inode = dest->inode_id;
	entry->reclen = new_entry_size;
	entry->name_len = elem_length;
	if ( filesystem->sb->s_feature_incompat & EXT2_FEATURE_INCOMPAT_FILETYPE )
		entry->file_type = EXT2_FT_OF_MODE(dest->Mode());
	else
		entry->file_type = EXT2_FT_UNKNOWN;
	strncpy(entry->name, elem, new_entry_size - sizeof(struct ext_dirent));

	block->FinishWrite();
	block->Unref();

	dest->BeginWrite();
	dest->data->i_links_count++;
	dest->FinishWrite();

	if ( !found_hole )
		SetSize(Size() + filesystem->block_size);

	return true;
}

Inode* Inode::UnlinkKeep(const char* elem, bool directories, bool force)
{
	if ( !EXT2_S_ISDIR(Mode()) )
		return errno = ENOTDIR, (Inode*) NULL;
	size_t elem_length = strlen(elem);
	if ( elem_length == 0 )
		return errno = ENOENT, (Inode*) NULL;
	uint32_t block_size = filesystem->block_size;
	uint64_t filesize = Size();
	uint64_t num_blocks = divup(filesize, (uint64_t) block_size);
	uint64_t offset = 0;
	Block* block = NULL;
	uint64_t block_id = 0;
	struct ext_dirent* last_entry = NULL;
	while ( offset < filesize )
	{
		uint64_t entry_block_id = offset / block_size;
		uint64_t entry_block_offset = offset % block_size;
		if ( block && block_id != entry_block_id )
			last_entry = NULL,
			block->Unref(),
			block = NULL;
		if ( !block && !(block = GetBlock(block_id = entry_block_id)) )
			return NULL;
		uint8_t* block_data = block->block_data + entry_block_offset;
		struct ext_dirent* entry = (struct ext_dirent*) block_data;
		if ( entry->inode &&
		     entry->name_len == elem_length &&
		     memcmp(elem, entry->name, elem_length) == 0 )
		{
			Inode* inode = filesystem->GetInode(entry->inode);

			if ( !force && directories && !EXT2_S_ISDIR(inode->Mode()) )
			{
				inode->Unref();
				block->Unref();
				return errno = ENOTDIR, (Inode*) NULL;
			}

			if ( !force && directories && !inode->IsEmptyDirectory() )
			{
				inode->Unref();
				block->Unref();
				return errno = ENOTEMPTY, (Inode*) NULL;
			}

			if ( !force && !directories && EXT2_S_ISDIR(inode->Mode()) )
			{
				inode->Unref();
				block->Unref();
				return errno = EISDIR, (Inode*) NULL;
			}

			if ( !filesystem->device->write )
			{
				inode->Unref();
				block->Unref();
				return errno = EROFS, (Inode*) NULL;
			}

			Modified();

			inode->BeginWrite();
			inode->data->i_links_count--;
			inode->FinishWrite();

			block->BeginWrite();

			entry->inode = 0;
			entry->name_len = 0;
			entry->file_type = 0;

			// Merge the current entry with the previous if any.
			if ( last_entry )
			{
				last_entry->reclen += entry->reclen;
				memset(entry, 0, entry->reclen);
				entry = last_entry;
			}

			strncpy(entry->name + entry->name_len, "",
			        entry->reclen - sizeof(struct ext_dirent) - entry->name_len);

			// If the entire block is empty, we'll need to remove it.
			if ( !entry->name[0] && entry->reclen == block_size )
			{
				// If this is not the last block, we'll make it. This is faster
				// than shifting the entire directory a single block. We don't
				// actually copy this block to the end, since we'll truncate it
				// regardless.
				if ( entry_block_id + 1 != num_blocks )
				{
					Block* last_block = GetBlock(num_blocks-1);
					if ( last_block )
					{
						memcpy(block->block_data, last_block->block_data, block_size);
						last_block->Unref();
						Truncate(filesize - block_size);
					}
				}
				else
				{
					Truncate(filesize - block_size);
				}
			}

			block->FinishWrite();

			block->Unref();

			return inode;
		}
		offset += entry->reclen;
		last_entry = entry;
	}
	if ( block )
		block->Unref();
	return errno = ENOENT, (Inode*) NULL;
}

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 ( !EXT2_S_ISREG(Mode()) && !EXT2_S_ISLNK(Mode()) )
		return errno = EISDIR, -1;
	if ( o_offset < 0 )
		return errno = EINVAL, -1;
	if ( SSIZE_MAX < s_count )
		s_count = SSIZE_MAX;
	uint64_t sofar = 0;
	uint64_t count = (uint64_t) s_count;
	uint64_t offset = (uint64_t) o_offset;
	uint64_t file_size = Size();
	if ( file_size <= offset )
		return 0;
	if ( file_size - offset < count )
		count = file_size - offset;
	// TODO: This case also needs to be handled in SetSize, Truncate, WriteAt,
	//       and so on.
	if ( 0 < file_size && file_size <= 60 && !data->i_blocks )
	{
		assert(offset + count <= 60);
		unsigned char* block_data = (unsigned char*) &data->i_block[0];
		memcpy(buf, block_data + offset, count);
		return (ssize_t) count;
	}
	while ( sofar < count )
	{
		uint64_t block_id = offset / filesystem->block_size;
		uint32_t block_offset = offset % filesystem->block_size;
		uint32_t block_left = filesystem->block_size - block_offset;
		Block* block = GetBlock(block_id);
		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;
		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 ( !EXT2_S_ISREG(Mode()) && !EXT2_S_ISLNK(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();
	uint64_t sofar = 0;
	uint64_t count = (uint64_t) s_count;
	uint64_t offset = (uint64_t) o_offset;
	uint64_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);
	if ( 0 < end_at && end_at <= 60 && !data->i_blocks )
	{
		data_block->BeginWrite();
		unsigned char* block_data = (unsigned char*) &data->i_block[0];
		memcpy(block_data + offset, buf, count);
		if ( data->i_size < end_at )
			data->i_size = end_at;
		data_block->FinishWrite();
		return (ssize_t) count;
	}
	while ( sofar < count )
	{
		uint64_t block_id = offset / filesystem->block_size;
		uint32_t block_offset = offset % filesystem->block_size;
		uint32_t block_left = filesystem->block_size - block_offset;
		Block* block = GetBlock(block_id);
		if ( !block )
			return sofar ? (ssize_t) 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();
		block->Unref();
		sofar += amount;
		offset += amount;
	}
	return (ssize_t) sofar;
}

bool Inode::UnembedInInode()
{
	assert(data->i_blocks == 0 && 0 < data->i_size && data->i_size <= 60);
	unsigned char* block_data = (unsigned char*) &data->i_block[0];
	size_t content_size = Size();
	unsigned char content[60];
	memcpy(content, block_data, content_size);
	data_block->BeginWrite();
	memset(block_data, 0, 60);
	data->i_size = 0;
	data_block->FinishWrite();
	if ( WriteAt(content, content_size, 0) != (ssize_t) content_size )
	{
		Truncate(0);
		data_block->BeginWrite();
		memcpy(block_data, content, content_size);
		data->i_size = content_size;
		data->i_blocks = 0;
		data_block->FinishWrite();
		return false;
	}
	return true;
}

bool Inode::Rename(Inode* olddir, const char* oldname, const char* newname)
{
	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;
	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 ( EXT2_S_ISDIR(src_inode->Mode()) )
	{
		if ( !Unlink(newname, true) && errno != ENOENT )
			return src_inode->Unref(), false;
		Link(newname, src_inode, true);
		olddir->Unlink(oldname, true, 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;
		Link(newname, src_inode, false);
		olddir->Unlink(oldname, false);
	}

	src_inode->Unref();
	return true;
}

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

	// TODO: Preferred block group!
	uint32_t result_inode_id = filesystem->AllocateInode();
	if ( !result_inode_id )
		return NULL;

	Inode* result = filesystem->GetInode(result_inode_id);
	if ( !result )
		return filesystem->FreeInode(result_inode_id), false;
	struct timespec now;
	clock_gettime(CLOCK_REALTIME, &now);
	result->BeginWrite();
	memset(result->data, 0, sizeof(*result->data));
	result->data->i_atime = now.tv_sec;
	result->data->i_ctime = now.tv_sec;
	result->data->i_mtime = now.tv_sec;
	// TODO: Set all the other inode properties!
	result->FinishWrite();
	result->SetMode((0777 & S_SETABLE) | EXT2_S_IFLNK);
	result->SetUserId(request_uid);
	result->SetGroupId(request_gid);

	size_t dest_length = strlen(dest);
	if ( SSIZE_MAX < dest_length )
		return errno = EFBIG, -1;
	if ( result->WriteAt((const uint8_t*) dest, dest_length, 0) < (ssize_t) dest_length )
		return result->Unref(), false;

	if ( !Link(elem, result, false) )
		return result->Truncate(0), result->Unref(), false;

	result->Unref();
	return true;
}

Inode* Inode::CreateDirectory(const char* path, mode_t mode)
{
	if ( !filesystem->device->write )
		return errno = EROFS, (Inode*) NULL;

	// TODO: Preferred block group!
	uint32_t result_inode_id = filesystem->AllocateInode();
	if ( !result_inode_id )
		return NULL;

	Inode* result = filesystem->GetInode(result_inode_id);
	if ( !result )
		return filesystem->FreeInode(result_inode_id), (Inode*) NULL;
	struct timespec now;
	clock_gettime(CLOCK_REALTIME, &now);
	result->BeginWrite();
	memset(result->data, 0, sizeof(*result->data));
	result->data->i_atime = now.tv_sec;
	result->data->i_ctime = now.tv_sec;
	result->data->i_mtime = now.tv_sec;
	// TODO: Set all the other inode properties!
	result->FinishWrite();
	result->SetMode((mode & S_SETABLE) | EXT2_S_IFDIR);
	result->SetUserId(request_uid);
	result->SetGroupId(request_gid);

	// Increase the directory count statistics.
	uint32_t group_id = (result->inode_id - 1) / filesystem->sb->s_inodes_per_group;
	assert(group_id < filesystem->num_groups);
	BlockGroup* block_group = filesystem->GetBlockGroup(group_id);
	if ( !block_group )
		return result->Unref(), (Inode*) NULL;
	block_group->BeginWrite();
	block_group->data->bg_used_dirs_count++;
	block_group->FinishWrite();
	block_group->Unref();

	if ( !Link(path, result, true) )
		return result->Unref(), (Inode*) NULL;

	if ( !result->Link(".", result, true) )
	{
		Unlink(path, true, true);
		return result->Unref(), (Inode*) NULL;
	}

	if ( !result->Link("..", this, true) )
	{
		result->Unlink(".", true, true);
		Unlink(path, true, true);
		return result->Unref(), (Inode*) NULL;
	}

	return result;
}

bool Inode::RemoveDirectory(const char* path)
{
	Inode* result = UnlinkKeep(path, true);
	if ( !result )
		return false;
	result->Unlink("..", true, true);
	result->Unlink(".", true, true);
	result->Truncate(0);

	// Decrease the directory count statistics.
	uint32_t group_id = (result->inode_id - 1) / filesystem->sb->s_inodes_per_group;
	assert(group_id < filesystem->num_groups);
	BlockGroup* block_group = filesystem->GetBlockGroup(group_id);
	if ( block_group )
	{
		block_group->BeginWrite();
		block_group->data->bg_used_dirs_count--;
		block_group->FinishWrite();
		block_group->Unref();
	}

	result->Unref();

	return true;
}

bool Inode::IsEmptyDirectory()
{
	if ( !EXT2_S_ISDIR(Mode()) )
		return errno = ENOTDIR, false;
	uint32_t block_size = filesystem->block_size;
	uint64_t filesize = Size();
	uint64_t offset = 0;
	Block* block = NULL;
	uint64_t block_id = 0;
	while ( offset < filesize )
	{
		uint64_t entry_block_id = offset / block_size;
		uint64_t entry_block_offset = offset % block_size;
		if ( block && block_id != entry_block_id )
			block->Unref(),
			block = NULL;
		if ( !block && !(block = GetBlock(block_id = entry_block_id)) )
			return false;
		uint8_t* block_data = block->block_data + entry_block_offset;
		struct ext_dirent* entry = (struct ext_dirent*) block_data;
		if ( entry->inode &&
		     !((entry->name_len == 1 && entry->name[0] == '.') ||
		       (entry->name_len == 2 && entry->name[0] == '.' &&
		                                entry->name[1] == '.' )) )
		{
			block->Unref();
			return false;
		}
		offset += entry->reclen;
	}
	if ( block )
		block->Unref();
	return true;
}

void Inode::Delete()
{
	assert(!data->i_links_count);
	assert(!reference_count);
	assert(!remote_reference_count);

	Truncate(0);

	uint32_t deleted_inode_id = inode_id;

	struct timespec now;
	clock_gettime(CLOCK_REALTIME, &now);

	BeginWrite();
	memset(data, 0, sizeof(*data));
	data->i_dtime = now.tv_sec;
	FinishWrite();

	filesystem->FreeInode(deleted_inode_id);
}

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

void Inode::Unref()
{
	assert(0 < reference_count);
	reference_count--;
	if ( !reference_count && !remote_reference_count )
	{
		if ( !data->i_links_count )
			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 ( !data->i_links_count )
			Delete();
		delete this;
	}
}

void Inode::Modified()
{
	struct timespec now;
	clock_gettime(CLOCK_REALTIME, &now);
	BeginWrite();
	data->i_mtime = now.tv_sec;
	FinishWrite();
}

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

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;
	}
	data_block->FinishWrite();
	Use();
}

void Inode::Sync()
{
	if ( !dirty )
		return;
	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()
{
	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;
}
