/*
 * YAFFS: Yet another FFS. A NAND-flash specific file system. 
 *
 * Copyright (C) 2002 Aleph One Ltd.
 *   for Toby Churchill Ltd and Brightstar Engineering
 *
 * Created by Charles Manning <charles@aleph1.co.uk>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 *
 *
 *  This version hacked for emulating 2kpage NAND for YAFFS2 testing.
 */

#include <linux/config.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/version.h>
#include <linux/slab.h>
#include <linux/init.h>
#include <linux/list.h>
#include <linux/fs.h>
#include <linux/proc_fs.h>
#include <linux/pagemap.h>
#include <linux/mtd/mtd.h>
#include <linux/interrupt.h>
#include <linux/string.h>

#include <asm/uaccess.h>
#include <linux/mtd/mtd.h>
#include <linux/mtd/partitions.h>
#include <linux/mtd/nand.h>

#define EM_SIZE_IN_MEG	4
#define PAGE_DATA_SIZE	2048
#define PAGE_SPARE_SIZE	64
#define PAGES_PER_BLOCK	64
#define NAND_SHIFT	11	/* Shifter for 2k */

#define EM_SIZE_IN_BYTES	(EM_SIZE_IN_MEG << 20)
#define PAGE_TOTAL_SIZE		(PAGE_DATA_SIZE + PAGE_SPARE_SIZE)
#define BLOCK_TOTAL_SIZE	(PAGES_PER_BLOCK * PAGE_TOTAL_SIZE)
#define BLOCKS_PER_MEG		((1<<20) / (PAGES_PER_BLOCK * PAGE_DATA_SIZE))

static struct mtd_info nandemul2k_mtd;

typedef struct {
	__u8 data[PAGE_TOTAL_SIZE];	// Data + spare
	int empty;			// is this empty?
} nandemul_Page;

typedef struct {
	nandemul_Page *page[PAGES_PER_BLOCK];
	int damaged;	
} nandemul_Block;

typedef struct {
	nandemul_Block **block;
	int nBlocks;
} nandemul_Device;

static nandemul_Device ned;
static int sizeInMB = EM_SIZE_IN_MEG;


static void nandemul_yield(int n)
{
#ifdef __KERNEL__
	if (n > 0)
		schedule_timeout(n);
#endif
}

static void nandemul2k_Read(void *buffer, int page, int start, int nBytes)
{
	int pg = page % PAGES_PER_BLOCK;
	int blk = page / PAGES_PER_BLOCK;

	if (buffer && nBytes > 0)
		memcpy(buffer,&ned.block[blk]->page[pg]->data[start],nBytes);
}

static void nandemul2k_Program(const void *buffer, int page, int start, int nBytes)
{
	int blk = page / PAGES_PER_BLOCK;
	int pg = page % PAGES_PER_BLOCK;
	__u8 *p;
	__u8 *b = (__u8 *)buffer;

	p = &ned.block[blk]->page[pg]->data[start];

	while (buffer && nBytes > 0) {
		*p = *p & *b;
		p++;
		b++;
		nBytes--;
	}
}

static void nandemul2k_DoErase(int blockNumber)
{
	int i;
	nandemul_Block *blk;
	
	if (blockNumber < 0 || blockNumber >= ned.nBlocks)
		return;
	
	blk = ned.block[blockNumber];
	
	for(i = 0; i < PAGES_PER_BLOCK; i++) {
		memset(blk->page[i],0xff,sizeof(nandemul_Page));
		blk->page[i]->empty = 1;
	}
	nandemul_yield(2);
}

static int nandemul2k_CalcNBlocks(void)
{
	return EM_SIZE_IN_MEG * BLOCKS_PER_MEG;
}

static int CheckInit(void)
{
	static int initialised = 0;
	int i, j;
	int fail = 0;
	int nBlocks; 
	int nAllocated = 0;
	
	if (initialised) 
		return 0;
	
	ned.nBlocks = nBlocks = nandemul2k_CalcNBlocks();
	
	ned.block = kmalloc(sizeof(nandemul_Block*) * nBlocks, GFP_KERNEL);
	
	if (!ned.block)
		return ENOMEM;
		
	for (i=fail=0; i <nBlocks; i++) {
		
		nandemul_Block *blk;
		
		if (!(blk = ned.block[i] = kmalloc(sizeof(nandemul_Block), GFP_KERNEL)))
			fail = 1;
		else {
			for(j = 0; j < PAGES_PER_BLOCK; j++) {
				if((blk->page[j] = kmalloc(sizeof(nandemul_Page), GFP_KERNEL)) == 0)
					fail = 1;
			}
			nandemul2k_DoErase(i);
			ned.block[i]->damaged = 0;
			nAllocated++;
		}
	}
	
	if (fail) {
		//Todo thump pages
		
		for(i = 0; i < nAllocated; i++)
			kfree(ned.block[i]);

		kfree(ned.block);
		
		return ENOMEM;
	}
	
	ned.nBlocks = nBlocks;
	initialised = 1;
	
	return 1;
}

static void nandemul2k_CleanUp(void)
{
	int i,j;
	
	for(i = 0; i < ned.nBlocks; i++) {
		for(j = 0; j < PAGES_PER_BLOCK; j++) {
		   kfree(ned.block[i]->page[j]);
		}
		kfree(ned.block[i]);
	}
	kfree(ned.block);
	ned.block = 0;
}

#ifdef CONFIG_MTD_NAND_ECC
#include <linux/mtd/nand_ecc.h>
#endif

/*
 * NAND low-level MTD interface functions
 */
static int nand_read (struct mtd_info *mtd, loff_t from, size_t len,
			size_t *retlen, u_char *buf);
static int nand_read_ecc (struct mtd_info *mtd, loff_t from, size_t len,
			size_t *retlen, u_char *buf, u_char *oob_buf, struct nand_oobinfo *dummy);
static int nand_read_oob (struct mtd_info *mtd, loff_t from, size_t len,
			size_t *retlen, u_char *buf);
static int nand_write (struct mtd_info *mtd, loff_t to, size_t len,
			size_t *retlen, const u_char *buf);
static int nand_write_ecc (struct mtd_info *mtd, loff_t to, size_t len,
			size_t *retlen, const u_char *buf, const u_char *oob_buf, struct nand_oobinfo *dummy);
static int nand_write_oob (struct mtd_info *mtd, loff_t to, size_t len,
			size_t *retlen, const u_char *buf);
static int nand_writev (struct mtd_info *mtd, const struct iovec *vecs,
			unsigned long count, loff_t to, size_t *retlen);
static int nand_erase (struct mtd_info *mtd, struct erase_info *instr);
static void nand_sync (struct mtd_info *mtd);


/*
 * NAND read
 */
static int nand_read (struct mtd_info *mtd, loff_t from, size_t len,
		size_t *retlen, u_char *buf)
{
	return nand_read_ecc(mtd, from, len, retlen, buf, NULL,NULL);
}


/*
 * NAND read with ECC
 */
static int nand_read_ecc (struct mtd_info *mtd, loff_t from, size_t len,
			size_t *retlen, u_char *buf, u_char *oob_buf,struct nand_oobinfo *oobsel)
{
	int start, page;
	int n = len;
	int nToCopy;

	/* Do not allow reads past end of device */
	if ((from + len) > mtd->size) {
		*retlen = 0;
		return -EINVAL;
	}

	/* Initialize return value */
	*retlen = 0;

	while (n > 0) {
		/* First we calculate the starting page */
		page = from >> NAND_SHIFT;

		/* Get raw starting column */

		start = from & (mtd->oobblock-1);

		// OK now check for the curveball where the start and end are in
		// the same page
		if((start + n) < mtd->oobblock)
			nToCopy = n;
		else
			nToCopy =  mtd->oobblock - start;

		nandemul2k_Read(buf, page, start, nToCopy);
		nandemul2k_Read(oob_buf,page,PAGE_DATA_SIZE,PAGE_SPARE_SIZE);

		n -= nToCopy;
		from += nToCopy;
		buf += nToCopy;
		if(oob_buf) oob_buf += PAGE_SPARE_SIZE;
		*retlen += nToCopy;
	}

	return 0;
}

/*
 * NAND read out-of-band
 */
static int nand_read_oob (struct mtd_info *mtd, loff_t from, size_t len,
				size_t *retlen, u_char *buf)
{
	int col, page;

	printk("nand_read_oob: from = 0x%08x, buf = 0x%08x, len = %i\n",
			(unsigned int) from, (unsigned int) buf, (int) len);

	/* Shift to get page */
	page = ((int) from) >> NAND_SHIFT;

	/* Mask to get column */
	col = from & 0x0f;

	/* Initialize return length value */
	*retlen = 0;

	/* Do not allow reads past end of device */
	if ((from + len) > mtd->size) {
		printk("nand_read_oob: Attempt read beyond end of device\n");
		*retlen = 0;
		return -EINVAL;
	}

	nandemul2k_Read(buf,page,PAGE_DATA_SIZE + col,len);

	/* Return happy */
	*retlen = len;
	return 0;
}

static int nand_write(struct mtd_info *mtd, loff_t to, size_t len,
			size_t *retlen, const u_char *buf)
{
	return nand_write_ecc (mtd, to, len, retlen, buf, NULL,NULL);
}

static int nand_write_ecc(struct mtd_info *mtd, loff_t to, size_t len,
			size_t *retlen, const u_char *buf,
			const u_char *oob_buf, struct nand_oobinfo *dummy)
{
	int start, page;
	int n = len;
	int nToCopy;

	/* Do not allow reads past end of device */
	if ((to + len) > mtd->size) {
		*retlen = 0;
		return -EINVAL;
	}

	/* Initialize return value */
	*retlen = 0;

	while (n > 0) {

		/* First we calculate the starting page */
		page = to >> NAND_SHIFT;

		/* Get raw starting column */

		start = to & (mtd->oobblock - 1);

		// OK now check for the curveball where the start and end are in
		// the same page
		if((start + n) < mtd->oobblock)
			nToCopy = n;
		else
			nToCopy =  mtd->oobblock - start;

		nandemul2k_Program(buf, page, start, nToCopy);
		nandemul2k_Program(oob_buf, page, PAGE_DATA_SIZE, PAGE_SPARE_SIZE);

		n -= nToCopy;
		to += nToCopy;
		buf += nToCopy;
		if(oob_buf) oob_buf += PAGE_SPARE_SIZE;
		*retlen += nToCopy;
	}

	return 0;
}

static int nand_write_oob (struct mtd_info *mtd, loff_t to, size_t len,
				size_t *retlen, const u_char *buf)
{
	int col, page;

	printk("nand_read_oob: to = 0x%08x, len = %i\n", (unsigned int) to, (int)len);

	/* Shift to get page */
	page = ((int) to) >> NAND_SHIFT;

	/* Mask to get column */
	col = to & 0x0f;

	/* Initialize return length value */
	*retlen = 0;

	/* Do not allow reads past end of device */
	if ((to + len) > mtd->size) {
		printk("nand_read_oob: Attempt read beyond end of device\n");
		*retlen = 0;
		return -EINVAL;
	}

	nandemul2k_Program(buf, page, 512 + col, len);

	/* Return happy */
	*retlen = len;
	return 0;
}

/*
 * NAND write with iovec
 */
static int nand_writev (struct mtd_info *mtd, const struct iovec *vecs,
				unsigned long count, loff_t to, size_t *retlen)
{
	return -EINVAL;
}

/*
 * NAND erase a block
 */
static int nand_erase (struct mtd_info *mtd, struct erase_info *instr)
{
	int i, nBlocks,block;

	printk("nand_erase: start = 0x%08x, len = %i\n", (unsigned int) instr->addr, (unsigned int) instr->len);

	/* Start address must align on block boundary */
	if (instr->addr & (mtd->erasesize - 1)) {
		printk("nand_erase: Unaligned address\n");
		return -EINVAL;
	}

	/* Length must align on block boundary */
	if (instr->len & (mtd->erasesize - 1)) {
		printk("nand_erase: Length not block aligned\n");
		return -EINVAL;
	}

	/* Do not allow erase past end of device */
	if ((instr->len + instr->addr) > mtd->size) {
		printk("nand_erase: Erase past end of device\n");
		return -EINVAL;
	}

	nBlocks = instr->len >> (NAND_SHIFT + 5);
	block = instr->addr >> (NAND_SHIFT + 5);

	for (i = 0; i < nBlocks; i++) {
		nandemul2k_DoErase(block);
		block++;
	}

	return 0;
}

int nand_block_isbad(struct mtd_info *mtd,int blockNo)
{
	return 0;
}

int nand_block_markbad(struct mtd_info *mtd, int blockNo)
{
	return 0;
}


/*
 * NAND sync
 */
static void nand_sync (struct mtd_info *mtd)
{
	printk("nand_sync: called\n");
}

/*
 * Scan for the NAND device
 */
static int nand_scan(struct mtd_info *mtd, int nchips)
{
	mtd->oobblock	= PAGE_DATA_SIZE;
	mtd->oobsize	= PAGE_SPARE_SIZE;
	mtd->erasesize	= PAGE_DATA_SIZE * PAGES_PER_BLOCK;
	mtd->size	= sizeInMB << 20;

	/* Fill in remaining MTD driver data */
	mtd->type	= MTD_NANDFLASH;
	mtd->flags	= MTD_CAP_NANDFLASH;
	mtd->owner	= THIS_MODULE;
	mtd->ecctype	= MTD_ECC_NONE;
	mtd->erase	= nand_erase;
	mtd->point	= NULL;
	mtd->unpoint	= NULL;

	mtd->read	= nand_read;
	mtd->write	= nand_write;
	mtd->read_ecc	= nand_read_ecc;
	mtd->write_ecc	= nand_write_ecc;
	mtd->read_oob	= nand_read_oob;
	mtd->write_oob	= nand_write_oob;
	mtd->block_isbad = nand_block_isbad;
	mtd->block_markbad = nand_block_markbad;
	mtd->readv	= NULL;
	mtd->writev	= nand_writev;
	mtd->sync	= nand_sync;
	mtd->lock	= NULL;
	mtd->unlock	= NULL;
	mtd->suspend	= NULL;
	mtd->resume	= NULL;

	/* Return happy */
	return 0;
}

#if 0
#ifdef MODULE
MODULE_PARM(sizeInMB, "i");

__setup("sizeInMB=",sizeInMB);
#endif
#endif

/*
 * Define partitions for flash devices
 */
static struct mtd_partition nandemul2k_partition[] = {
	{
		name: "NANDemul partition 1",
		offset:  0,
		size: 0
	},
};

static int nPartitions = sizeof(nandemul2k_partition)/sizeof(nandemul2k_partition[0]);

static int __init nandemul2k_init (void)
{
	/* Do the nand init */
	CheckInit();

	nand_scan(&nandemul2k_mtd, 1);

	/* Build the partition table */
	nandemul2k_partition[0].size = sizeInMB * 1024 * 1024;

	/* Register the partition */
	add_mtd_partitions(&nandemul2k_mtd, nandemul2k_partition, nPartitions);

	return 0;
}

static void __exit nandemul2k_cleanup(void)
{
	nandemul2k_CleanUp();
	del_mtd_partitions(&nandemul2k_mtd);	/* Unregister partitions */
	del_mtd_device(&nandemul2k_mtd);	/* Unregister the device */
}

module_init(nandemul2k_init);
module_exit(nandemul2k_cleanup);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Charles Manning <manningc@aleph1.co.uk>");
MODULE_AUTHOR("Coywolf Qi Hunt <qiyong@fc-cn.com>");
MODULE_DESCRIPTION("2k Page/128k Block NAND emulated in RAM");
