/*
 * Acess OS
 * Ext2 Driver Version 1
 */
/**
 * \file dir.c
 * \brief Second Extended Filesystem Driver
 * \todo Implement file full write support
 */
#define DEBUG	1
#define VERBOSE	0
#include "ext2_common.h"

// === MACROS ===
#define BLOCK_DIR_OFS(_data, _block)	((Uint16*)(_data)[(_block)])

// === PROTOTYPES ===
char	*Ext2_ReadDir(tVFS_Node *Node, int Pos);
tVFS_Node	*Ext2_FindDir(tVFS_Node *Node, const char *FileName);
 int	Ext2_MkNod(tVFS_Node *Node, const char *Name, Uint Flags);
 int	Ext2_Relink(tVFS_Node *Node, const char *OldName, const char *NewName);
 int	Ext2_Link(tVFS_Node *Parent, tVFS_Node *Node, const char *Name);
// --- Helpers ---
tVFS_Node	*Ext2_int_CreateNode(tExt2_Disk *Disk, Uint InodeId);

// === CODE ===
/**
 * \brief Reads a directory entry
 * \param Node	Directory node
 * \param Pos	Position of desired element
 */
char *Ext2_ReadDir(tVFS_Node *Node, int Pos)
{
	tExt2_Inode	inode;
	tExt2_DirEnt	dirent;
	Uint64	Base;	// Block's Base Address
	 int	block = 0;
	Uint	ofs = 0;
	 int	entNum = 0;
	tExt2_Disk	*disk = Node->ImplPtr;
	Uint	size;
	
	ENTER("pNode iPos", Node, Pos);
	
	// Read directory's inode
	Ext2_int_ReadInode(disk, Node->Inode, &inode);
	size = inode.i_size;
	
	LOG("inode.i_block[0] = 0x%x", inode.i_block[0]);
	
	// Find Entry
	// Get First Block
	// - Do this ourselves as it is a simple operation
	Base = inode.i_block[0] * disk->BlockSize;
	// Scan directory
	while(Pos -- && size > 0)
	{
		VFS_ReadAt( disk->FD, Base+ofs, sizeof(tExt2_DirEnt), &dirent);
		ofs += dirent.rec_len;
		size -= dirent.rec_len;
		entNum ++;
		
		if(ofs >= disk->BlockSize) {
			block ++;
			if( ofs > disk->BlockSize ) {
				Log_Warning("EXT2", "Directory Entry %i of inode %i extends over a block boundary, ignoring",
					entNum-1, Node->Inode);
			}
			ofs = 0;
			Base = Ext2_int_GetBlockAddr( disk, inode.i_block, block );
		}
	}
	
	// Check for the end of the list
	if(size <= 0) {
		LEAVE('n');
		return NULL;
	}
	
	// Read Entry
	VFS_ReadAt( disk->FD, Base+ofs, sizeof(tExt2_DirEnt), &dirent );
	//LOG("dirent.inode = %i", dirent.inode);
	//LOG("dirent.rec_len = %i", dirent.rec_len);
	//LOG("dirent.name_len = %i", dirent.name_len);
	dirent.name[ dirent.name_len ] = '\0';	// Cap off string
	
	
	// Ignore . and .. (these are done in the VFS)
	if( (dirent.name[0] == '.' && dirent.name[1] == '\0')
	||  (dirent.name[0] == '.' && dirent.name[1] == '.' && dirent.name[2]=='\0')) {
		LEAVE('p', VFS_SKIP);
		return VFS_SKIP;	// Skip
	}
	
	LEAVE('s', dirent.name);
	// Create new node
	return strdup(dirent.name);
}

/**
 * \brief Gets information about a file
 * \param Node	Parent Node
 * \param Filename	Name of wanted file
 * \return VFS Node of file
 */
tVFS_Node *Ext2_FindDir(tVFS_Node *Node, const char *Filename)
{
	tExt2_Disk	*disk = Node->ImplPtr;
	tExt2_Inode	inode;
	tExt2_DirEnt	dirent;
	Uint64	Base;	// Block's Base Address
	 int	block = 0;
	Uint	ofs = 0;
	 int	entNum = 0;
	Uint	size;
	 int	filenameLen = strlen(Filename);
	
	// Read directory's inode
	Ext2_int_ReadInode(disk, Node->Inode, &inode);
	size = inode.i_size;
	
	// Get First Block
	// - Do this ourselves as it is a simple operation
	Base = inode.i_block[0] * disk->BlockSize;
	// Find File
	while(size > 0)
	{
		VFS_ReadAt( disk->FD, Base+ofs, sizeof(tExt2_DirEnt), &dirent);
		dirent.name[ dirent.name_len ] = '\0';	// Cap off string
		// If it matches, create a node and return it
		if(dirent.name_len == filenameLen && strcmp(dirent.name, Filename) == 0)
			return Ext2_int_CreateNode( disk, dirent.inode );
		// Increment pointers
		ofs += dirent.rec_len;
		size -= dirent.rec_len;
		entNum ++;
		
		// Check for end of block
		if(ofs >= disk->BlockSize) {
			block ++;
			if( ofs > disk->BlockSize ) {
				Log_Warning("EXT2", "Directory Entry %i of inode %i extends over a block boundary, ignoring",
					entNum-1, Node->Inode);
			}
			ofs = 0;
			Base = Ext2_int_GetBlockAddr( disk, inode.i_block, block );
		}
	}
	
	return NULL;
}

/**
 * \fn int Ext2_MkNod(tVFS_Node *Parent, const char *Name, Uint Flags)
 * \brief Create a new node
 */
int Ext2_MkNod(tVFS_Node *Parent, const char *Name, Uint Flags)
{
	#if 0
	tVFS_Node	*child;
	Uint64	inodeNum;
	tExt2_Inode	inode;
	inodeNum = Ext2_int_AllocateInode(Parent->ImplPtr, Parent->Inode);
	
	memset(&inode, 0, sizeof(tExt2_Inode));
	
	// File type
	inode.i_mode = 0664;
	if( Flags & VFS_FFLAG_READONLY )
		inode.i_mode &= ~0222;
	if( Flags & VFS_FFLAG_SYMLINK )
		inode.i_mode |= EXT2_S_IFLNK;
	else if( Flags & VFS_FFLAG_DIRECTORY )
		inode.i_mode |= EXT2_S_IFDIR | 0111;
	
	inode.i_uid = Threads_GetUID();
	inode.i_gid = Threads_GetGID();
	inode.i_ctime =
		inode.i_mtime =
		inode.i_atime = now() / 1000;
	
	child = Ext2_int_CreateNode(Parent->ImplPtr, inodeNum);
	return Ext2_Link(Parent, child, Name);
	#else
	return 1;
	#endif
}

/**
 * \brief Rename a file
 * \param Node	This (directory) node
 * \param OldName	Old name of file
 * \param NewName	New name for file
 * \return Boolean Failure - See ::tVFS_Node.Relink for info
 */
int Ext2_Relink(tVFS_Node *Node, const char *OldName, const char *NewName)
{
	return 1;
}

/**
 * \brief Links an existing node to a new name
 * \param Parent	Parent (directory) node
 * \param Node	Node to link
 * \param Name	New name for the node
 * \return Boolean Failure - See ::tVFS_Node.Link for info
 */
int Ext2_Link(tVFS_Node *Node, tVFS_Node *Child, const char *Name)
{	
	#if 0
	tExt2_Disk	*disk = Node->ImplPtr;
	tExt2_Inode	inode;
	tExt2_DirEnt	dirent;
	tExt2_DirEnt	newEntry;
	Uint64	Base;	// Block's Base Address
	 int	block = 0, ofs = 0;
	Uint	size;
	void	*blockData;
	 int	bestMatch = -1, bestSize, bestBlock, bestOfs;
	 int	nEntries;
	
	blockData = malloc(disk->BlockSize);
	
	// Read child inode (get's the file type)
	Ext2_int_ReadInode(disk, Child->Inode, &inode);
	
	// Create a stub entry
	newEntry.inode = Child->Inode;
	newEntry.name_len = strlen(Name);
	newEntry.rec_len = (newEntry.name_len+3+8)&~3;
	newEntry.type = inode.i_mode >> 12;
	memcpy(newEntry.name, Name, newEntry.name_len);
	
	// Read directory's inode
	Ext2_int_ReadInode(disk, Node->Inode, &inode);
	size = inode.i_size;
	
	// Get a lock on the inode
	Ext2_int_LockInode(disk, Node->Inode);
	
	// Get First Block
	// - Do this ourselves as it is a simple operation
	base = inode.i_block[0] * disk->BlockSize;
	VFS_ReadAt( disk->FD, base, disk->BlockSize, blockData );
	block = 0;
	// Find File
	while(size > 0)
	{
		dirent = blockData + ofs;
		// Sanity Check the entry
		if(ofs + dirent->rec_len > disk->BlockSize) {
			Log_Warning("EXT2",
				"Directory entry %i of inode 0x%x extends over a block boundary",
				nEntries, (Uint)Node->Inode);
		}
		else {
		
			// Free entry
			if(dirent->type == 0) {
				if( dirent->rec_len >= newEntry.rec_len
				 && (bestMatch == -1 || bestSize > dirent->rec_len) )
				{
					bestMatch = nEntries;
					bestSize = dirent->rec_len;
					bestBlock = block;
					bestOfs = ofs;
				}
			}
			// Non free - check name to avoid duplicates
			else {
				if(strncmp(Name, dirent->name, dirent->name_len) == 0) {
					Ext2_int_UnlockInode(disk, Node->Inode);
					return 1;	// ERR_???
				}
			}
		}
		
		// Increment the pointer
		nEntries ++;
		ofs += dirent->rec_len;
		if( ofs >= disk->BlockSize ) {
			// Read the next block if needed
			BLOCK_DIR_OFS(Node->Data, block) = nEntries;
			block ++;
			ofs = 0;
			base = Ext2_int_GetBlockAddr(disk, inode.i_block, block);
			VFS_ReadAt( disk->FD, base, disk->BlockSize, blockData );
		}
	}
	
	// Check if a free slot was found
	if( bestMatch >= 0 ) {
		// Read-Modify-Write
		bestBlock = Ext2_int_GetBlockAddr(disk, inode.i_block, bestBlock);
		if( block > 0 )
			bestMatch = BLOCK_DIR_OFS(Node->Data, bestBlock);
		VFS_ReadAt( disk->FD, base, disk->BlockSize, blockData );
		dirent = blockData + bestOfs;
		memcpy(dirent, newEntry, newEntry.rec_len);
		VFS_WriteAt( disk->FD, base, disk->BlockSize, blockData );
	}
	else {
		// Allocate block, Write
		block = Ext2_int_AllocateBlock(Disk, block);
		Log_Warning("EXT2", "");
	}

	Ext2_int_UnlockInode(disk, Node->Inode);
	return 0;
	#else
	return 1;
	#endif
}

// ---- INTERNAL FUNCTIONS ----
/**
 * \fn vfs_node *Ext2_int_CreateNode(tExt2_Disk *Disk, Uint InodeID)
 * \brief Create a new VFS Node
 */
tVFS_Node *Ext2_int_CreateNode(tExt2_Disk *Disk, Uint InodeID)
{
	tExt2_Inode	inode;
	tVFS_Node	retNode;
	tVFS_Node	*tmpNode;
	
	if( !Ext2_int_ReadInode(Disk, InodeID, &inode) )
		return NULL;
	
	if( (tmpNode = Inode_GetCache(Disk->CacheID, InodeID)) )
		return tmpNode;
	
	
	// Set identifiers
	retNode.Inode = InodeID;
	retNode.ImplPtr = Disk;
	
	// Set file length
	retNode.Size = inode.i_size;
	retNode.Data = NULL;
	
	// Set Access Permissions
	retNode.UID = inode.i_uid;
	retNode.GID = inode.i_gid;
	retNode.NumACLs = 3;
	retNode.ACLs = VFS_UnixToAcessACL(inode.i_mode & 0777, inode.i_uid, inode.i_gid);
	
	//  Set Function Pointers
	retNode.Read = Ext2_Read;
	retNode.Write = Ext2_Write;
	retNode.Close = Ext2_CloseFile;
	
	switch(inode.i_mode & EXT2_S_IFMT)
	{
	// Symbolic Link
	case EXT2_S_IFLNK:
		retNode.Flags = VFS_FFLAG_SYMLINK;
		break;
	// Regular File
	case EXT2_S_IFREG:
		retNode.Flags = 0;
		retNode.Size |= (Uint64)inode.i_dir_acl << 32;
		break;
	// Directory
	case EXT2_S_IFDIR:
		retNode.ReadDir = Ext2_ReadDir;
		retNode.FindDir = Ext2_FindDir;
		retNode.MkNod = Ext2_MkNod;
		retNode.Relink = Ext2_Relink;
		retNode.Link = Ext2_Link;
		retNode.Flags = VFS_FFLAG_DIRECTORY;
		retNode.Data = calloc( sizeof(Uint16), DivUp(retNode.Size, Disk->BlockSize) );
		break;
	// Unknown, Write protect it to be safe 
	default:
		retNode.Flags = VFS_FFLAG_READONLY;
		break;
	}
	
	// Set Timestamps
	retNode.ATime = inode.i_atime * 1000;
	retNode.MTime = inode.i_mtime * 1000;
	retNode.CTime = inode.i_ctime * 1000;
	
	// Save in node cache and return saved node
	return Inode_CacheNode(Disk->CacheID, &retNode);
}
