/*
 * Copyright (c) 2012 Paulo Alcantara <pcacjr@zytor.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it would be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write the Free Software Foundation,
 * Inc.,  51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include <dprintf.h>
#include <stdio.h>
#include <string.h>
#include <sys/dirent.h>
#include <cache.h>
#include <core.h>
#include <disk.h>
#include <fs.h>
#include <ilog2.h>
#include <klibc/compiler.h>
#include <ctype.h>

#include "codepage.h"
#include "xfs_types.h"
#include "xfs_sb.h"
#include "xfs_ag.h"
#include "misc.h"
#include "xfs.h"
#include "xfs_dinode.h"
#include "xfs_dir2.h"
#include "xfs_readdir.h"

static inline int xfs_fmt_local_readdir(struct file *file,
					struct dirent *dirent, xfs_dinode_t *core)
{
    return xfs_readdir_dir2_block(file, dirent, core);
}

static inline int xfs_fmt_extents_readdir(struct file *file,
					  struct dirent *dirent,
					  xfs_dinode_t *core)
{
    int retval;

    if (be32_to_cpu(core->di_nextents) <= 1) {
	/* Single-block Directories */
	retval = xfs_readdir_dir2_block(file, dirent, core);
    } else if (xfs_dir2_isleaf(file->fs, core)) {
	/* Leaf Directory */
	retval = xfs_readdir_dir2_leaf(file, dirent, core);
    } else {
	/* Node Directory */
	retval = xfs_readdir_dir2_node(file, dirent, core);
    }

    return retval;
}

static int xfs_readdir(struct file *file, struct dirent *dirent)
{
    struct fs_info *fs = file->fs;
    xfs_dinode_t *core;
    struct inode *inode = file->inode;
    int retval = -1;

    core = xfs_dinode_get_core(fs, inode->ino);
    if (!core) {
	xfs_error("Failed to get dinode from disk (ino %llx)", inode->ino);
	return -1;
    }

    if (core->di_format == XFS_DINODE_FMT_LOCAL)
	retval = xfs_fmt_local_readdir(file, dirent, core);
    else if (core->di_format == XFS_DINODE_FMT_EXTENTS)
	retval = xfs_fmt_extents_readdir(file, dirent, core);

    return retval;
}

static uint32_t xfs_getfssec(struct file *file, char *buf, int sectors,
			     bool *have_more)
{
    return generic_getfssec(file, buf, sectors, have_more);
}

static int xfs_next_extent(struct inode *inode, uint32_t lstart)
{
    struct fs_info *fs = inode->fs;
    xfs_dinode_t *core = NULL;
    xfs_bmbt_irec_t rec;
    block_t bno;
    xfs_bmdr_block_t *rblock;
    int fsize;
    xfs_bmbt_ptr_t *pp;
    xfs_btree_block_t *blk;
    uint16_t nextents;
    block_t nextbno;
    uint32_t index;

    (void)lstart;

    core = xfs_dinode_get_core(fs, inode->ino);
    if (!core) {
	xfs_error("Failed to get dinode from disk (ino %llx)", inode->ino);
	goto out;
    }

    /* The data fork contains the file's data extents */
    if (XFS_PVT(inode)->i_cur_extent == be32_to_cpu(core->di_nextents))
        goto out;

    if (core->di_format == XFS_DINODE_FMT_EXTENTS) {
	bmbt_irec_get(&rec, (xfs_bmbt_rec_t *)&core->di_literal_area[0] +
						XFS_PVT(inode)->i_cur_extent++);

	bno = fsblock_to_bytes(fs, rec.br_startblock) >> BLOCK_SHIFT(fs);

	XFS_PVT(inode)->i_offset = rec.br_startoff;

	inode->next_extent.pstart = bno << BLOCK_SHIFT(fs) >> SECTOR_SHIFT(fs);
	inode->next_extent.len = ((rec.br_blockcount << BLOCK_SHIFT(fs)) +
				  SECTOR_SIZE(fs) - 1) >> SECTOR_SHIFT(fs);
    } else if (core->di_format == XFS_DINODE_FMT_BTREE) {
        xfs_debug("XFS_DINODE_FMT_BTREE");
        index = XFS_PVT(inode)->i_cur_extent++;
        rblock = (xfs_bmdr_block_t *)&core->di_literal_area[0];
        fsize = XFS_DFORK_SIZE(core, fs, XFS_DATA_FORK);
        pp = XFS_BMDR_PTR_ADDR(rblock, 1, xfs_bmdr_maxrecs(fsize, 0));
        bno = fsblock_to_bytes(fs, be64_to_cpu(pp[0])) >> BLOCK_SHIFT(fs);

        /* Find the leaf */
        for (;;) {
            blk = (xfs_btree_block_t *)get_cache(fs->fs_dev, bno);
            if (be16_to_cpu(blk->bb_level) == 0)
                break;

            pp = XFS_BMBT_PTR_ADDR(fs, blk, 1,
                    xfs_bmdr_maxrecs(XFS_INFO(fs)->blocksize, 0));
            bno = fsblock_to_bytes(fs, be64_to_cpu(pp[0])) >> BLOCK_SHIFT(fs);
        }

        /* Find the right extent among threaded leaves */
        for (;;) {
            nextbno = be64_to_cpu(blk->bb_u.l.bb_rightsib);
            nextents = be16_to_cpu(blk->bb_numrecs);
            if (nextents - index > 0) {
                bmbt_irec_get(&rec, XFS_BMDR_REC_ADDR(blk, index + 1));

                bno = fsblock_to_bytes(fs, rec.br_startblock)
						>> BLOCK_SHIFT(fs);

                XFS_PVT(inode)->i_offset = rec.br_startoff;

                inode->next_extent.pstart = bno << BLOCK_SHIFT(fs)
                                                >> SECTOR_SHIFT(fs);
                inode->next_extent.len = ((rec.br_blockcount
                                            << BLOCK_SHIFT(fs))
                                            + SECTOR_SIZE(fs) - 1)
                                            >> SECTOR_SHIFT(fs);
                break;
            }

            index -= nextents;
            bno = fsblock_to_bytes(fs, nextbno) >> BLOCK_SHIFT(fs);
            blk = (xfs_btree_block_t *)get_cache(fs->fs_dev, bno);
        }
    }

    return 0;

out:
    return -1;
}

static inline struct inode *xfs_fmt_local_find_entry(const char *dname,
						     struct inode *parent,
						     xfs_dinode_t *core)
{
    return xfs_dir2_local_find_entry(dname, parent, core);
}

static inline struct inode *xfs_fmt_extents_find_entry(const char *dname,
						       struct inode *parent,
						       xfs_dinode_t *core)
{
    struct inode *inode;

    if (be32_to_cpu(core->di_nextents) <= 1) {
        /* Single-block Directories */
        inode = xfs_dir2_block_find_entry(dname, parent, core);
    } else if (xfs_dir2_isleaf(parent->fs, core)) {
        /* Leaf Directory */
	inode = xfs_dir2_leaf_find_entry(dname, parent, core);
    } else {
        /* Node Directory */
        inode = xfs_dir2_node_find_entry(dname, parent, core);
    }

    return inode;
}

static inline struct inode *xfs_fmt_btree_find_entry(const char *dname,
                                                     struct inode *parent,
                                                     xfs_dinode_t *core)
{
    return xfs_dir2_node_find_entry(dname, parent, core);
}

static struct inode *xfs_iget(const char *dname, struct inode *parent)
{
    struct fs_info *fs = parent->fs;
    xfs_dinode_t *core = NULL;
    struct inode *inode = NULL;

    xfs_debug("dname %s parent %p parent ino %lu", dname, parent, parent->ino);

    core = xfs_dinode_get_core(fs, parent->ino);
    if (!core) {
        xfs_error("Failed to get dinode from disk (ino 0x%llx)", parent->ino);
        goto out;
    }

    if (core->di_format == XFS_DINODE_FMT_LOCAL) {
	inode = xfs_fmt_local_find_entry(dname, parent, core);
    } else if (core->di_format == XFS_DINODE_FMT_EXTENTS) {
        inode = xfs_fmt_extents_find_entry(dname, parent, core);
    } else if (core->di_format == XFS_DINODE_FMT_BTREE) {
        inode = xfs_fmt_btree_find_entry(dname, parent, core);
    } else {
	xfs_debug("format %hhu", core->di_format);
	xfs_debug("TODO: format \"local\" and \"extents\" are the only "
		  "supported ATM");
	goto out;
    }

    if (!inode) {
	xfs_debug("Entry not found!");
	goto out;
    }

    if (inode->mode == DT_REG) {
	XFS_PVT(inode)->i_offset = 0;
	XFS_PVT(inode)->i_cur_extent = 0;
    } else if (inode->mode == DT_DIR) {
	XFS_PVT(inode)->i_btree_offset = 0;
	XFS_PVT(inode)->i_leaf_ent_offset = 0;
    }

    return inode;

out:
    return NULL;
}

static int xfs_readlink(struct inode *inode, char *buf)
{
    struct fs_info *fs = inode->fs;
    xfs_dinode_t *core;
    int pathlen = -1;
    xfs_bmbt_irec_t rec;
    block_t db;
    char *dir_buf;

    core = xfs_dinode_get_core(fs, inode->ino);
    if (!core) {
	xfs_error("Failed to get dinode from disk (ino 0x%llx)", inode->ino);
	goto out;
    }

    pathlen = be64_to_cpu(core->di_size);
    if (!pathlen)
	goto out;

    if (pathlen < 0 || pathlen > MAXPATHLEN) {
	xfs_error("inode (%llu) bad symlink length (%d)",
		  inode->ino, pathlen);
	goto out;
    }

    if (core->di_format == XFS_DINODE_FMT_LOCAL) {
	memcpy(buf, (char *)&core->di_literal_area[0], pathlen);
    } else if (core->di_format == XFS_DINODE_FMT_EXTENTS) {
	bmbt_irec_get(&rec, (xfs_bmbt_rec_t *)&core->di_literal_area[0]);
	db = fsblock_to_bytes(fs, rec.br_startblock) >> BLOCK_SHIFT(fs);
	dir_buf = xfs_dir2_get_dirblks(fs, db, rec.br_blockcount);

        /*
         * Syslinux only supports filesystem block size larger than or equal to
	 * 4 KiB. Thus, one directory block is far enough to hold the maximum
	 * symbolic link file content, which is only 1024 bytes long.
         */
	memcpy(buf, dir_buf, pathlen);
	free(dir_buf);
    }

out:
    return pathlen;
}

static struct inode *xfs_iget_root(struct fs_info *fs)
{
    xfs_dinode_t *core = NULL;
    struct inode *inode = xfs_new_inode(fs);

    xfs_debug("Looking for the root inode...");

    core = xfs_dinode_get_core(fs, XFS_INFO(fs)->rootino);
    if (!core) {
	xfs_error("Inode core's magic number does not match!");
	xfs_debug("magic number 0x%04x", be16_to_cpu(core->di_magic));
	goto out;
    }

    fill_xfs_inode_pvt(fs, inode, XFS_INFO(fs)->rootino);

    xfs_debug("Root inode has been found!");

    if ((be16_to_cpu(core->di_mode) & S_IFMT) != S_IFDIR) {
	xfs_error("root inode is not a directory ?! No makes sense...");
	goto out;
    }

    inode->ino			= XFS_INFO(fs)->rootino;
    inode->mode 		= DT_DIR;
    inode->size 		= be64_to_cpu(core->di_size);

    return inode;

out:
    free(inode);

    return NULL;
}

static inline int xfs_read_superblock(struct fs_info *fs, xfs_sb_t *sb)
{
    struct disk *disk = fs->fs_dev->disk;

    if (!disk->rdwr_sectors(disk, sb, XFS_SB_DADDR, 1, false))
	return -1;

    return 0;
}

static struct xfs_fs_info *xfs_new_sb_info(xfs_sb_t *sb)
{
    struct xfs_fs_info *info;

    info = malloc(sizeof *info);
    if (!info)
	malloc_error("xfs_fs_info structure");

    info->blocksize		= be32_to_cpu(sb->sb_blocksize);
    info->block_shift		= sb->sb_blocklog;
    info->dirblksize		= 1 << (sb->sb_blocklog + sb->sb_dirblklog);
    info->dirblklog		= sb->sb_dirblklog;
    info->inopb_shift 		= sb->sb_inopblog;
    info->agblk_shift 		= sb->sb_agblklog;
    info->rootino 		= be64_to_cpu(sb->sb_rootino);
    info->agblocks 		= be32_to_cpu(sb->sb_agblocks);
    info->agblocks_shift 	= sb->sb_agblklog;
    info->agcount 		= be32_to_cpu(sb->sb_agcount);
    info->inodesize 		= be16_to_cpu(sb->sb_inodesize);
    info->inode_shift 		= sb->sb_inodelog;

    return info;
}

static int xfs_fs_init(struct fs_info *fs)
{
    struct disk *disk = fs->fs_dev->disk;
    xfs_sb_t sb;
    struct xfs_fs_info *info;

    xfs_debug("fs %p", fs);

    SECTOR_SHIFT(fs) = disk->sector_shift;
    SECTOR_SIZE(fs) = 1 << SECTOR_SHIFT(fs);

    if (xfs_read_superblock(fs, &sb)) {
	xfs_error("Superblock read failed");
	goto out;
    }

    if (!xfs_is_valid_magicnum(&sb)) {
	xfs_error("Invalid superblock");
	goto out;
    }

    xfs_debug("magicnum 0x%lX", be32_to_cpu(sb.sb_magicnum));

    info = xfs_new_sb_info(&sb);
    if (!info) {
	xfs_error("Failed to fill in filesystem-specific info structure");
	goto out;
    }

    fs->fs_info = info;

    xfs_debug("block_shift %u blocksize 0x%lX (%lu)", info->block_shift,
	      info->blocksize, info->blocksize);

    xfs_debug("rootino 0x%llX (%llu)", info->rootino, info->rootino);

    BLOCK_SHIFT(fs) = info->block_shift;
    BLOCK_SIZE(fs) = info->blocksize;

    cache_init(fs->fs_dev, BLOCK_SHIFT(fs));

    XFS_INFO(fs)->dirleafblk = xfs_dir2_db_to_da(fs, XFS_DIR2_LEAF_FIRSTDB(fs));

    return BLOCK_SHIFT(fs);

out:
    return -1;
}

const struct fs_ops xfs_fs_ops = {
    .fs_name		= "xfs",
    .fs_flags		= FS_USEMEM | FS_THISIND,
    .fs_init		= xfs_fs_init,
    .iget_root		= xfs_iget_root,
    .searchdir		= NULL,
    .getfssec		= xfs_getfssec,
    .open_config	= generic_open_config,
    .close_file         = generic_close_file,
    .mangle_name	= generic_mangle_name,
    .readdir		= xfs_readdir,
    .iget		= xfs_iget,
    .next_extent	= xfs_next_extent,
    .readlink		= xfs_readlink,
};
