#include <dprintf.h>
#include <stdio.h>
#include <string.h>
#include <sys/dirent.h>
#include <minmax.h>
#include <cache.h>
#include <core.h>
#include <disk.h>
#include <fs.h>
#include "ext2_fs.h"

/*
 * get the group's descriptor of group_num
 */
static const struct ext2_group_desc *
ext2_get_group_desc(struct fs_info *fs, uint32_t group_num)
{
    struct ext2_sb_info *sbi = EXT2_SB(fs);
    uint32_t desc_block, desc_index;
    const struct ext2_group_desc *desc_data_block;

    if (group_num >= sbi->s_groups_count) {
	printf ("ext2_get_group_desc"
		"block_group >= groups_count - "
		"block_group = %d, groups_count = %d",
		group_num, sbi->s_groups_count);

	return NULL;
    }

    desc_block = group_num / sbi->s_desc_per_block;
    desc_index = group_num % sbi->s_desc_per_block;

    desc_block += sbi->s_first_data_block + 1;

    desc_data_block = get_cache(fs->fs_dev, desc_block);
    return &desc_data_block[desc_index];
}

/*
 * Unlike strncmp, ext2_match_entry returns 1 for success, 0 for failure.
 */
static inline bool ext2_match_entry(const char *name, size_t len,
                                    const struct ext2_dir_entry * de)
{
    if (!de->d_inode)
        return false;
    if (len != de->d_name_len)
	return false;
    return !memcmp(name, de->d_name, len);
}


/*
 * p is at least 6 bytes before the end of page
 */
static inline struct ext2_dir_entry *ext2_next_entry(struct ext2_dir_entry *p)
{
    return (struct ext2_dir_entry *)((char*)p + p->d_rec_len);
}

/*
 * Map a logical sector and load it into the cache
 */
static const void *
ext2_get_cache(struct inode *inode, block_t lblock)
{
    block_t pblock = ext2_bmap(inode, lblock, NULL);
    return get_cache(inode->fs->fs_dev, pblock);
}

/*
 * find a dir entry, return it if found, or return NULL.
 */
static const struct ext2_dir_entry *
ext2_find_entry(struct fs_info *fs, struct inode *inode, const char *dname)
{
    block_t index = 0;
    uint32_t i = 0, offset, maxoffset;
    const struct ext2_dir_entry *de;
    const char *data;
    size_t dname_len = strlen(dname);

    while (i < inode->size) {
	data = ext2_get_cache(inode, index++);
	offset = 0;
	maxoffset =  min(BLOCK_SIZE(fs), i-inode->size);

	/* The smallest possible size is 9 bytes */
	while (offset < maxoffset-8) {
	    de = (const struct ext2_dir_entry *)(data + offset);
	    if (de->d_rec_len > maxoffset - offset)
		break;

	    if (ext2_match_entry(dname, dname_len, de))
		return de;

	    offset += de->d_rec_len;
	}
	i += BLOCK_SIZE(fs);
    }

    return NULL;
}

static const struct ext2_inode *
ext2_get_inode(struct fs_info *fs, int inr)
{
    const struct ext2_group_desc *desc;
    const char *data;
    uint32_t inode_group, inode_offset;
    uint32_t block_num, block_off;

    inr--;
    inode_group  = inr / EXT2_INODES_PER_GROUP(fs);
    inode_offset = inr % EXT2_INODES_PER_GROUP(fs);
    desc = ext2_get_group_desc(fs, inode_group);
    if (!desc)
	return NULL;

    block_num = desc->bg_inode_table +
	inode_offset / EXT2_INODES_PER_BLOCK(fs);
    block_off = inode_offset % EXT2_INODES_PER_BLOCK(fs);

    data = get_cache(fs->fs_dev, block_num);

    return (const struct ext2_inode *)
	(data + block_off * EXT2_SB(fs)->s_inode_size);
}

static inline int get_inode_mode(int mode)
{
    mode >>= S_IFSHIFT;
    if (mode == T_IFDIR)
	mode = I_DIR;
    else if (mode == T_IFLNK)
	mode = I_SYMLINK;
    else
	mode = I_FILE; /* we treat others as FILE */
    return mode;
}

static void fill_inode(struct inode *inode, const struct ext2_inode *e_inode)
{
    inode->mode    = get_inode_mode(e_inode->i_mode);
    inode->size    = e_inode->i_size;
    inode->atime   = e_inode->i_atime;
    inode->ctime   = e_inode->i_ctime;
    inode->mtime   = e_inode->i_mtime;
    inode->dtime   = e_inode->i_dtime;
    inode->blocks  = e_inode->i_blocks;
    inode->flags   = e_inode->i_flags;
    inode->file_acl = e_inode->i_file_acl;
    memcpy(PVT(inode)->i_block, e_inode->i_block,
	   EXT2_N_BLOCKS * sizeof(uint32_t *));
}

static struct inode *ext2_iget_by_inr(struct fs_info *fs, uint32_t inr)
{
    const struct ext2_inode *e_inode;
    struct inode *inode;

    e_inode = ext2_get_inode(fs, inr);
    if (!(inode = alloc_inode(fs, inr, sizeof(struct ext2_pvt_inode))))
	return NULL;
    fill_inode(inode, e_inode);

    return inode;
}

static struct inode *ext2_iget_root(struct fs_info *fs)
{
    return ext2_iget_by_inr(fs, EXT2_ROOT_INO);
}

static struct inode *ext2_iget(const char *dname, struct inode *parent)
{
    const struct ext2_dir_entry *de;
    struct fs_info *fs = parent->fs;

    de = ext2_find_entry(fs, parent, dname);
    if (!de)
	return NULL;
    
    return ext2_iget_by_inr(fs, de->d_inode);
}

/*
 * Read the entire contents of an inode into a memory buffer
 */
static int cache_get_file(struct inode *inode, void *buf, size_t bytes)
{
    struct fs_info *fs = inode->fs;
    size_t block_size = BLOCK_SIZE(fs);
    uint32_t index = 0;		/* Logical block number */
    size_t chunk;
    const char *data;
    char *p = buf;

    if (inode->size > bytes)
	bytes = inode->size;

    while (bytes) {
	chunk = min(bytes, block_size);
	data = ext2_get_cache(inode, index++);
	memcpy(p, data, chunk);

	bytes -= chunk;
	p += chunk;
    }

    return 0;
}
	
static int ext2_readlink(struct inode *inode, char *buf)
{
    struct fs_info *fs = inode->fs;
    int sec_per_block = 1 << (fs->block_shift - fs->sector_shift);
    bool fast_symlink;

    if (inode->size > BLOCK_SIZE(fs))
	return -1;		/* Error! */

    fast_symlink = (inode->file_acl ? sec_per_block : 0) == inode->blocks;
    if (fast_symlink)
	memcpy(buf, PVT(inode)->i_block, inode->size);
    else
	cache_get_file(inode, buf, inode->size);

    return inode->size;
}

/*
 * Read one directory entry at a time
 */
static struct dirent *ext2_readdir(struct file *file)
{
    struct fs_info *fs = file->fs;
    struct inode *inode = file->inode;
    struct dirent *dirent;
    const struct ext2_dir_entry *de;
    const char *data;
    block_t index = file->offset >> fs->block_shift;

    if (file->offset >= inode->size)
	return NULL;		/* End of file */

    data = ext2_get_cache(inode, index);
    de = (const struct ext2_dir_entry *)
	(data + (file->offset & (BLOCK_SIZE(fs) - 1)));

    if (!(dirent = malloc(sizeof(*dirent)))) {
	malloc_error("dirent structure in ext2_readdir");
	return NULL;
    }
    dirent->d_ino = de->d_inode;
    dirent->d_off = file->offset;
    dirent->d_reclen = de->d_rec_len;
    dirent->d_type = de->d_file_type;
    memcpy(dirent->d_name, de->d_name, de->d_name_len);
    dirent->d_name[de->d_name_len] = '\0';

    file->offset += de->d_rec_len;  /* Update for next reading */

    return dirent;
}

/*
 * init. the fs meta data, return the block size bits.
 */
static int ext2_fs_init(struct fs_info *fs)
{
    struct disk *disk = fs->fs_dev->disk;
    struct ext2_sb_info *sbi;
    struct ext2_super_block sb;
    struct cache *cs;

    /* read the super block */
    disk->rdwr_sectors(disk, &sb, 2, 2, 0);

    /* check if it is ext2, since we also support btrfs now */
    if (sb.s_magic != EXT2_SUPER_MAGIC)
	return -1;

    sbi = malloc(sizeof(*sbi));
    if (!sbi) {
	malloc_error("ext2_sb_info structure");
	return -1;
    }
    fs->fs_info = sbi;

    if (sb.s_magic != EXT2_SUPER_MAGIC) {
	printf("ext2 mount error: it's not a EXT2/3/4 file system!\n");
	return 0;
    }

    fs->sector_shift = disk->sector_shift;
    fs->block_shift  = sb.s_log_block_size + 10;
    fs->sector_size  = 1 << fs->sector_shift;
    fs->block_size   = 1 << fs->block_shift;

    sbi->s_inodes_per_group = sb.s_inodes_per_group;
    sbi->s_blocks_per_group = sb.s_blocks_per_group;
    sbi->s_inodes_per_block = BLOCK_SIZE(fs) / sb.s_inode_size;
    if (sb.s_desc_size < sizeof(struct ext2_group_desc))
	sb.s_desc_size = sizeof(struct ext2_group_desc);
    sbi->s_desc_per_block   = BLOCK_SIZE(fs) / sb.s_desc_size;
    sbi->s_groups_count     = (sb.s_blocks_count - sb.s_first_data_block
			       + EXT2_BLOCKS_PER_GROUP(fs) - 1)
	                      / EXT2_BLOCKS_PER_GROUP(fs);
    sbi->s_first_data_block = sb.s_first_data_block;
    sbi->s_inode_size = sb.s_inode_size;

    /* Initialize the cache, and force block zero to all zero */
    cache_init(fs->fs_dev, fs->block_shift);
    cs = _get_cache_block(fs->fs_dev, 0);
    memset(cs->data, 0, fs->block_size);
    cache_lock_block(cs);

    return fs->block_shift;
}

const struct fs_ops ext2_fs_ops = {
    .fs_name       = "ext2",
    .fs_flags      = FS_THISIND | FS_USEMEM,
    .fs_init       = ext2_fs_init,
    .searchdir     = NULL,
    .getfssec      = generic_getfssec,
    .close_file    = generic_close_file,
    .mangle_name   = generic_mangle_name,
    .load_config   = generic_load_config,
    .iget_root     = ext2_iget_root,
    .iget          = ext2_iget,
    .readlink      = ext2_readlink,
    .readdir       = ext2_readdir,
    .next_extent   = ext2_next_extent,
};
