/*
 * JFFS2 -- Journalling Flash File System, Version 2.
 *
 * Copyright © 2001-2007 Red Hat, Inc.
 * Copyright © 2004-2010 David Woodhouse <dwmw2@infradead.org>
 *
 * Created by David Woodhouse <dwmw2@infradead.org>
 *
 * For licensing information, see the file 'LICENCE' in this directory.
 *
 */
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <common.h>
#include <crc.h>
#include <driver.h>
#include <init.h>
#include <fs.h>
#include <malloc.h>
#include <linux/kernel.h>
#include <linux/sched.h>
#include <linux/list.h>
#include <linux/mtd/mtd.h>
#include <linux/pagemap.h>
#include <linux/stat.h>
#include "compr.h"
#include "nodelist.h"
#include "os-linux.h"

static int jffs2_flash_setup(struct jffs2_sb_info *c);

/* from include/linux/fs.h */
static inline void i_uid_write(struct inode *inode, uid_t uid)
{
	inode->i_uid = uid;
}

static inline void i_gid_write(struct inode *inode, gid_t gid)
{
	inode->i_gid = gid;
}

const struct file_operations jffs2_file_operations;
const struct inode_operations jffs2_file_inode_operations;

static int jffs2_open(struct device_d *dev, FILE *file, const char *filename)
{
	struct inode *inode = file->f_inode;
	struct jffs2_file *jf;

	jf = xzalloc(sizeof(*jf));

	jf->inode = inode;
	jf->buf = xmalloc(JFFS2_BLOCK_SIZE);
	jf->offset = -1;

	file->priv = jf;

	return 0;
}

static int jffs2_close(struct device_d *dev, FILE *f)
{
	struct jffs2_file *jf = f->priv;

	free(jf->buf);
	free(jf);

	return 0;
}

static int jffs2_get_block(struct jffs2_file *jf, unsigned int pos)
{
	struct jffs2_sb_info *c = JFFS2_SB_INFO(jf->inode->i_sb);
	struct jffs2_inode_info *f = JFFS2_INODE_INFO(jf->inode);
	int ret;

	if (pos != jf->offset) {
		ret = jffs2_read_inode_range(c, f, jf->buf, pos,
					     JFFS2_BLOCK_SIZE);
		if (ret && ret != -ENOENT)
			return ret;
		jf->offset = pos;
	}

	return 0;
}

static int jffs2_read(struct device_d *_dev, FILE *f, void *buf,
		      size_t insize)
{
	struct jffs2_file *jf = f->priv;
	unsigned int pos = f->pos;
	unsigned int ofs;
	unsigned int now;
	unsigned int size = insize;
	int ret;

	/* Read till end of current block */
	ofs = f->pos % JFFS2_BLOCK_SIZE;
	if (ofs) {
		ret = jffs2_get_block(jf, pos);
		if (ret)
			return ret;

		now = min(size, JFFS2_BLOCK_SIZE - ofs);

		memcpy(buf, jf->buf + ofs, now);
		size -= now;
		pos += now;
		buf += now;
	}

	/* Do full blocks */
	while (size >= JFFS2_BLOCK_SIZE) {
		ret = jffs2_get_block(jf, pos);
		if (ret)
			return ret;

		memcpy(buf, jf->buf, JFFS2_BLOCK_SIZE);
		size -= JFFS2_BLOCK_SIZE;
		pos += JFFS2_BLOCK_SIZE;
		buf += JFFS2_BLOCK_SIZE;
	}

	/* And the rest */
	if (size) {
		ret = jffs2_get_block(jf, pos);
		if (ret)
			return ret;
		memcpy(buf, jf->buf, size);
	}

	return insize;

}

struct inode *jffs2_iget(struct super_block *sb, unsigned long ino)
{
	struct jffs2_inode_info *f;
	struct jffs2_sb_info *c;
	struct jffs2_raw_inode latest_node;
	struct inode *inode;
	int ret;

	jffs2_dbg(1, "%s(): ino == %lu\n", __func__, ino);

	inode = iget_locked(sb, ino);
	if (!inode)
		return ERR_PTR(-ENOMEM);
	if (!(inode->i_state & I_NEW))
		return inode;

	f = JFFS2_INODE_INFO(inode);
	c = JFFS2_SB_INFO(inode->i_sb);

	jffs2_init_inode_info(f);
	mutex_lock(&f->sem);

	ret = jffs2_do_read_inode(c, f, inode->i_ino, &latest_node);
	if (ret)
		goto error;

	inode->i_mode = jemode_to_cpu(latest_node.mode);
	i_uid_write(inode, je16_to_cpu(latest_node.uid));
	i_gid_write(inode, je16_to_cpu(latest_node.gid));
	inode->i_size = je32_to_cpu(latest_node.isize);
	inode->i_atime = ITIME(je32_to_cpu(latest_node.atime));
	inode->i_mtime = ITIME(je32_to_cpu(latest_node.mtime));
	inode->i_ctime = ITIME(je32_to_cpu(latest_node.ctime));

	set_nlink(inode, f->inocache->pino_nlink);

	inode->i_blocks = (inode->i_size + 511) >> 9;

	switch (inode->i_mode & S_IFMT) {

	case S_IFLNK:
		inode->i_op = &simple_symlink_inode_operations;
		inode->i_link = f->target;
		break;

	case S_IFDIR:
	{
		struct jffs2_full_dirent *fd;
		set_nlink(inode, 2); /* parent and '.' */

		for (fd=f->dents; fd; fd = fd->next) {
			if (fd->type == DT_DIR && fd->ino)
				inc_nlink(inode);
		}
		/* Root dir gets i_nlink 3 for some reason */
		if (inode->i_ino == 1)
			inc_nlink(inode);

		inode->i_op = &jffs2_dir_inode_operations;
		inode->i_fop = &jffs2_dir_operations;
		break;
	}
	case S_IFREG:
		inode->i_op = &jffs2_file_inode_operations;
		inode->i_fop = &jffs2_file_operations;
		break;

	case S_IFBLK:
	case S_IFCHR:
	case S_IFSOCK:
	case S_IFIFO:
		break;

	default:
		pr_warn("%s(): Bogus i_mode %o for ino %lu\n",
			__func__, inode->i_mode, (unsigned long)inode->i_ino);
	}

	mutex_unlock(&f->sem);

	jffs2_dbg(1, "jffs2_read_inode() returning\n");
	return inode;

error:
	mutex_unlock(&f->sem);
	iget_failed(inode);
	return ERR_PTR(ret);
}

static int calculate_inocache_hashsize(uint32_t flash_size)
{
	/*
	 * Pick a inocache hash size based on the size of the medium.
	 * Count how many megabytes we're dealing with, apply a hashsize twice
	 * that size, but rounding down to the usual big powers of 2. And keep
	 * to sensible bounds.
	 */

	int size_mb = flash_size / 1024 / 1024;
	int hashsize = (size_mb * 2) & ~0x3f;

	if (hashsize < INOCACHE_HASHSIZE_MIN)
		return INOCACHE_HASHSIZE_MIN;
	if (hashsize > INOCACHE_HASHSIZE_MAX)
		return INOCACHE_HASHSIZE_MAX;

	return hashsize;
}

static void jffs2_put_super(struct super_block *sb)
{
	if (sb->s_fs_info) {
		struct jffs2_sb_info *ctx = sb->s_fs_info;

		jffs2_free_ino_caches(ctx);
		jffs2_free_raw_node_refs(ctx);
		kfree(ctx->blocks);
		kfree(ctx->inocache_list);
		jffs2_flash_cleanup(ctx);

		kfree(sb->s_fs_info);
	}
}

int jffs2_do_fill_super(struct super_block *sb, int silent)
{
	struct jffs2_sb_info *c;
	struct inode *root_i;
	int ret;
	size_t blocks;

	c = JFFS2_SB_INFO(sb);

#ifndef CONFIG_JFFS2_FS_WRITEBUFFER
	if (c->mtd->type == MTD_NANDFLASH) {
		pr_err("Cannot operate on NAND flash unless jffs2 NAND support is compiled in");
		return -EINVAL;
	}
	if (c->mtd->type == MTD_DATAFLASH) {
		pr_err("Cannot operate on DataFlash unless jffs2 DataFlash support is compiled in");
		return -EINVAL;
	}
#endif

	c->flash_size = c->mtd->size;
	c->sector_size = c->mtd->erasesize;
	blocks = c->flash_size / c->sector_size;

	/*
	 * Size alignment check
	 */
	if ((c->sector_size * blocks) != c->flash_size) {
		c->flash_size = c->sector_size * blocks;
		pr_info("Flash size not aligned to erasesize, reducing to %dKiB",
			c->flash_size / 1024);
	}

	if (c->flash_size < 5*c->sector_size) {
		pr_err("Too few erase blocks (%d)",
		       c->flash_size / c->sector_size);
		return -EINVAL;
	}

	c->cleanmarker_size = sizeof(struct jffs2_unknown_node);

	/* NAND (or other bizarre) flash... do setup accordingly */
	ret = jffs2_flash_setup(c);
	if (ret)
		return ret;

	c->inocache_hashsize = calculate_inocache_hashsize(c->flash_size);
	c->inocache_list = kcalloc(c->inocache_hashsize, sizeof(struct jffs2_inode_cache *), GFP_KERNEL);
	if (!c->inocache_list) {
		ret = -ENOMEM;
		goto out_wbuf;
	}

	jffs2_init_xattr_subsystem(c);

	if ((ret = jffs2_do_mount_fs(c)))
		goto out_inohash;

	jffs2_dbg(1, "%s(): Getting root inode\n", __func__);
	root_i = jffs2_iget(sb, 1);
	if (IS_ERR(root_i)) {
		jffs2_dbg(1, "get root inode failed\n");
		ret = PTR_ERR(root_i);
		goto out_root;
	}

	ret = -ENOMEM;

	jffs2_dbg(1, "%s(): d_make_root()\n", __func__);
	sb->s_root = d_make_root(root_i);
	if (!sb->s_root)
		goto out_root;

	sb->s_maxbytes = 0xFFFFFFFF;
	sb->s_blocksize = PAGE_SIZE;
	sb->s_blocksize_bits = PAGE_SHIFT;
	sb->s_magic = JFFS2_SUPER_MAGIC;

	return 0;

out_root:
	jffs2_free_ino_caches(c);
	jffs2_free_raw_node_refs(c);
out_inohash:
	kfree(c->inocache_list);
out_wbuf:
	jffs2_flash_cleanup(c);
	return ret;
}

static int jffs2_flash_setup(struct jffs2_sb_info *c) {
	int ret = 0;

	if (jffs2_cleanmarker_oob(c)) {
		/* NAND flash... do setup accordingly */
		ret = jffs2_nand_flash_setup(c);
		if (ret)
			return ret;
	}

	/* and Dataflash */
	if (jffs2_dataflash(c)) {
		ret = jffs2_dataflash_setup(c);
		if (ret)
			return ret;
	}

	/* and an UBI volume */
	if (jffs2_ubivol(c)) {
		ret = jffs2_ubivol_setup(c);
		if (ret)
			return ret;
	}

	return ret;
}

void jffs2_flash_cleanup(struct jffs2_sb_info *c) {

	if (jffs2_cleanmarker_oob(c)) {
		jffs2_nand_flash_cleanup(c);
	}

	/* and DataFlash */
	if (jffs2_dataflash(c)) {
		jffs2_dataflash_cleanup(c);
	}

	/* and Intel "Sibley" flash */
	if (jffs2_nor_wbuf_flash(c)) {
		jffs2_nor_wbuf_flash_cleanup(c);
	}

	/* and an UBI volume */
	if (jffs2_ubivol(c)) {
		jffs2_ubivol_cleanup(c);
	}
}

static int jffs2_probe(struct device_d *dev)
{
	struct fs_device_d *fsdev;
	struct super_block *sb;
	struct jffs2_sb_info *ctx;
	int ret;

	fsdev = dev_to_fs_device(dev);
	sb = &fsdev->sb;

	ret = fsdev_open_cdev(fsdev);
	if (ret)
		goto err_out;

	ctx = kzalloc(sizeof(struct jffs2_sb_info), GFP_KERNEL);
	if (!ctx)
		return -ENOMEM;

	ctx->mtd = fsdev->cdev->mtd;

	sb->s_fs_info = ctx;

        ret = jffs2_compressors_init();
        if (ret) {
		pr_err("error: Failed to initialise compressors\n");
		goto err_out;
        }

        ret = jffs2_create_slab_caches();
        if (ret) {
		pr_err("error: Failed to initialise slab caches\n");
		goto err_compressors;
        }

        if (jffs2_fill_super(fsdev, 0)) {
		dev_err(dev, "no valid jffs2 found\n");
		ret = -EINVAL;
		goto err_slab;
	}

	return 0;

err_slab:
        jffs2_destroy_slab_caches();
err_compressors:
	jffs2_compressors_exit();
err_out:

	return ret;
}

static void jffs2_remove(struct device_d *dev)
{
	struct fs_device_d *fsdev;
	struct super_block *sb;

	fsdev = dev_to_fs_device(dev);
	sb = &fsdev->sb;

	jffs2_destroy_slab_caches();
	jffs2_compressors_exit();

	jffs2_put_super(sb);
}


static struct fs_driver_d jffs2_driver = {
	.open = jffs2_open,
	.close = jffs2_close,
	.read = jffs2_read,
	.type = filetype_jffs2,
	.flags     = 0,
	.drv = {
		.probe  = jffs2_probe,
		.remove = jffs2_remove,
		.name = "jffs2",
	}
};

static int jffs2_init(void)
{
	return register_fs_driver(&jffs2_driver);
}
coredevice_initcall(jffs2_init);
