/*
 *************************************************************************************************
 * Emulation of Samsung NAND Flash 
 *
 * State: Nothing is implemented
 *
 * Copyright 2007 Jochen Karrer. All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without modification, are
 * permitted provided that the following conditions are met:
 * 
 *   1. Redistributions of source code must retain the above copyright notice, this list of
 *       conditions and the following disclaimer.
 * 
 *   2. Redistributions in binary form must reproduce the above copyright notice, this list
 *       of conditions and the following disclaimer in the documentation and/or other materials
 *       provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY Jochen Karrer ``AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 * The views and conclusions contained in the software and documentation are those of the
 * authors and should not be interpreted as representing official policies, either expressed
 * or implied, of Jochen Karrer.
 *
 *************************************************************************************************
 */

#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include "sgstring.h"
#include "nand.h"
#include "configfile.h"
#include "cycletimer.h"

#define CMD_READ1_L		(0x00)	/* First half (256 bytes) */
#define CMD_READ1_H		(0x01)	/* Second half (256 bytes) */
#define CMD_READ2		(0x50)	/* 16 Byte ECC area */
#define CMD_READID		(0x90)
#define CMD_RESET		(0xff)	/** Accept when busy */
#define CMD_PP_SETUP		(0x80)
#define CMD_PP			(0x10)
#define CMD_ERASE_SETUP		(0x60)
#define CMD_ERASE		(0xd0)
#define CMD_BLOCKPROT1		(0x41)
#define CMD_BLOCKPROT2		(0x42)
#define CMD_BLOCKPROT3		(0x43)
#define CMD_READSTATUS		(0x70)	/** Accept when busy */
#define CMD_READPROTSTAT	(0x7a)

typedef struct FlashDescription {
	char *type;
	int addrbits;
	int colbits;
	uint64_t size;
	uint32_t data_pagesize;
	uint32_t spare_pagesize;
	int planes;
	int blocks;
	int pages_per_block;
	uint32_t us_busy_read;
} FlashDescription;

static FlashDescription flash_types[] = {
	{ 
		.type = "K9F1208",
		.addrbits = 26,
		.colbits = 8,
		.size = 528 * 32 * 4096,
		.planes = 4,
		.data_pagesize = 512,
		.spare_pagesize = 16,
		.pages_per_block = 32,
		.blocks = 4096,
		.us_busy_read = 15,
	},
	{
		.type = "K9GAG08UM0M",
		.colbits = 12,
		.addrbits = 26,
		.size = UINT64_C(4224) * 128 * 4096,  
		.planes = 4,	/** ??????? */
		.data_pagesize = 4096,
		.spare_pagesize = 128,
		.pages_per_block = 128,
		.blocks = 4096,
		.us_busy_read = 15, /** ????? */
	}
};

#define ASTATE_IDLE	(0)
#define ASTATE_COL	(1)
#define ASTATE_ROW0	(2)
#define ASTATE_ROW1	(3)
#define ASTATE_ROW2	(4)
#define ASTATE_ROW3	(5)
#define ASTATE_IGNORE	(6)

#define APTR_AREA_A	(0)
#define APTR_AREA_B	(1)
#define APTR_AREA_C	(2)

struct NandFlash {
        char *type;
	CycleCounter_t busy_until;
        DiskImage *diskimage;
	uint8_t *page_cache;
	uint32_t data_pagesize;
	uint32_t spare_pagesize;
	uint32_t pagesize;
	uint32_t pages_per_block;
	uint64_t size; /* The total size of all areas */
        int area_pointer; /* A Area 0-255, B Area 256-511, C-Area 512-527 */
	uint32_t cAreaStart; /* Offset of C-Area in Diskimage */
        uint8_t cmd;
        uint32_t col_addr; 
        uint32_t row_addr; 
	uint8_t addr_state;
	int addr_shift;
	uint32_t data_cnt;
        int readstate;
        int writestate;
        int transfer_count;
        NFPlane *nfplane;
        int nfplanes;
        uint64_t addrmask;
	SigNode *sigAle;
	SigNode *sigCle;
	SigNode *sigCe;
	SigNode *sigWe;
	SigNode *sigRe;
        SigNode *sigBusy;
	SigNode *sigData[8];
	SigTrace *ceTrace;
	SigTrace *weTrace;
	SigTrace *reTrace;
	uint32_t us_busy_read;
};

/*
 * CMD sequences
 *	Read 1 		0x00/0x01	  (A/B Area 0-255,256-511)
 *	Read 2  	0x50		  (C Area 512-527)
 *	Reset		0xff
 *	Page PGM True	0x80	0x10
 * 	Page PGM Dummy	0x80	0x11
 *	Copyback PGM True  0x00 0x8a 0x10 
 *	Copyback PGM Dummy 0x03 0x8a 0x11
 *	Block Erase	0x60 	0xd0
 *	Multi Plane Block Erase 60--60 	0xd0
 *	Read Status		0x70
 *	Read Multi Plane Status	(0x71)
 * 
 */

#define WSTATE_IDLE	(0)

#define RSTATE_IDLE	(0)

__UNUSED__ static void
make_busy(NandFlash *nf,uint32_t useconds)
{
        nf->busy_until = CycleCounter_Get() + MicrosecondsToCycles(useconds);
}

static uint32_t 
sample_data(NandFlash *nf) {
	uint8_t data = 0;
	int i;
	for(i = 0; i < 8; i++) {
		if(SigNode_Val(nf->sigData[i]) == SIG_HIGH) {
			data |= (1 << i);
		}
	}
	return data;
}
/**
 ***************************************************************************************
 * Fill the cache with one complete page. (A + B + C area)
 ***************************************************************************************
 */
static void
fill_cache(NandFlash *nf) 
{
	uint64_t addr;
	addr = ((uint64_t)nf->row_addr * nf->data_pagesize); 
	DiskImage_Read(nf->diskimage,addr,nf->page_cache,nf->data_pagesize);
	addr = nf->cAreaStart + (nf->row_addr * nf->spare_pagesize); 
	DiskImage_Read(nf->diskimage,addr,nf->page_cache + nf->data_pagesize,nf->spare_pagesize);
}

static void
cmd_read2_inc_addr(NandFlash *nf) {
	nf->col_addr = (nf->col_addr + 1) & 0xf;
	if(nf->col_addr == 0) {
		nf->area_pointer = APTR_AREA_C;
		make_busy(nf,nf->us_busy_read);
		nf->row_addr++;
	}
}

static void
cmd_read1_inc_addr(NandFlash *nf) {
	switch(nf->area_pointer) {
		case APTR_AREA_A:
			nf->col_addr = (nf->col_addr + 1) & 0xff;
			if(nf->col_addr == 0) {
				nf->area_pointer = APTR_AREA_B;
			}
			break;

		case APTR_AREA_B:
			nf->col_addr = (nf->col_addr + 1) & 0xff;
			if(nf->col_addr == 0) {
				nf->area_pointer = APTR_AREA_C;
			}
			break;

		case APTR_AREA_C:
			nf->col_addr = (nf->col_addr + 1) & 0xf;
			if(nf->col_addr == 0) {
				nf->row_addr++;
				nf->area_pointer = APTR_AREA_A;
				make_busy(nf,nf->us_busy_read);
				fill_cache(nf);
			}
			break;
	}
}

static uint8_t 
read_current(NandFlash *nf) {
	uint32_t col;
	switch(nf->area_pointer)
	{
		case APTR_AREA_A:
			col = nf->col_addr;
			break;
		case APTR_AREA_B:
			col = nf->col_addr | (nf->data_pagesize >> 1);
			break;
		case APTR_AREA_C:
			col = nf->col_addr + nf->data_pagesize;
			break;
		default:
			return 0;
	}
	if(col < nf->pagesize) {
		return nf->page_cache[col];
	} else {
		fprintf(stderr,"Nand Flash simulator bug: Outsize of Cache line\n");
		return 0;
	}
}

static void
cmd_write_inc_addr(NandFlash *nf) {
	switch(nf->area_pointer) {
		case APTR_AREA_A:
			nf->col_addr = (nf->col_addr + 1) & 0xff;
			if(nf->col_addr == 0) {
				nf->area_pointer = APTR_AREA_B;
			}
			break;

		case APTR_AREA_B:
			nf->col_addr = (nf->col_addr + 1) & 0xff;
			if(nf->col_addr == 0) {
				nf->area_pointer = APTR_AREA_C;
			}
			break;

		case APTR_AREA_C:
			nf->col_addr = (nf->col_addr + 1) & 0xf;
			if(nf->col_addr == 0) {
				nf->area_pointer = APTR_AREA_A;
			}
			break;
	}
}

static void 
write_current(NandFlash *nf,uint8_t data) {
	uint32_t col;
	switch(nf->area_pointer)
	{
		case APTR_AREA_A:
			col = nf->col_addr;
			break;
		case APTR_AREA_B:
			col = nf->col_addr | (nf->data_pagesize >> 1);
			break;
		case APTR_AREA_C:
			col = nf->col_addr + nf->data_pagesize;
			break;
		default:
			return;
	}
	if(col < nf->pagesize) {
		nf->page_cache[col] &= data;
	} else {
		fprintf(stderr,"Nand Flash simulator bug: Outsize of Cache line\n");
		return;
	}
}

static void 
output_data(NandFlash *nf,uint8_t data) {
	int i;
	for(i = 0; i < 8; i++) {
		if(data & (1 << i)) {
			SigNode_Set(nf->sigData[i],SIG_HIGH); 
		} else {
			SigNode_Set(nf->sigData[i],SIG_LOW); 
		}
	}
}

static void
NandFlash_Read(NandFlash *nf) 
{
	switch(nf->readstate) {
		case RSTATE_IDLE:
			switch(nf->cmd) {
				case CMD_READ1_L:
				case CMD_READ1_H:
					output_data(nf,read_current(nf));
					cmd_read1_inc_addr(nf);
					break;
				case CMD_READ2:
					output_data(nf,read_current(nf));
					cmd_read2_inc_addr(nf);
					break;
					
			}
			break;
	}
}

static void
NandFlash_Write(NandFlash *nf) 
{
	uint32_t data = sample_data(nf);
	switch(nf->writestate) {
		case WSTATE_IDLE:
			switch(nf->cmd) {
				case CMD_PP_SETUP:
					write_current(nf,data);	
					cmd_write_inc_addr(nf);
					break;
			}
			break;
			
	}
}

static void
NandFlash_EraseBlock(NandFlash *nf) 
{
	uint64_t data_addr;
	uint64_t spare_addr;
	int i;
	uint32_t row_addr = nf->row_addr & ~(nf->pages_per_block - 1);
	data_addr = ((uint64_t)row_addr * nf->data_pagesize); 
	spare_addr = nf->cAreaStart + ((uint64_t)row_addr * nf->spare_pagesize); 
	/* Misuse page cache */
	memset(nf->page_cache,0xff,nf->pagesize);
	for(i = 0; i < nf->pages_per_block;i++) {
		if(data_addr  + nf->data_pagesize <= nf->size) {
			DiskImage_Write(nf->diskimage,data_addr,nf->page_cache,nf->data_pagesize);
		}
		if(spare_addr  + nf->spare_pagesize <= nf->size) {
			DiskImage_Write(nf->diskimage,spare_addr,nf->page_cache,nf->spare_pagesize);
		}
	}
}

static void
NandFlash_PageProgram(NandFlash *nf) 
{
	uint64_t addr;
	addr = ((uint64_t)nf->row_addr * nf->data_pagesize); 
	DiskImage_Write(nf->diskimage,addr,nf->page_cache,nf->data_pagesize);
	addr = nf->cAreaStart + ((uint64_t)nf->row_addr * nf->spare_pagesize); 
	DiskImage_Write(nf->diskimage,addr,nf->page_cache + nf->data_pagesize,nf->spare_pagesize);
}


/**
 *******************************************************************************
 * \fn static uint32_t release_data(NandFlash *nf); 
 * Release the data lines by making them High Impedance.
 *******************************************************************************
 */
static void 
release_data(NandFlash *nf) {
	int i;
	for(i = 0; i < 8; i++) {
		SigNode_Set(nf->sigData[i],SIG_OPEN); 
	}
	return;
}

/**
 **************************************************************************************
 * \fn static void nf_we_change(SigNode *node,int value,void *clientData)
 **************************************************************************************
 */
static void
nf_we_change(SigNode *node,int value,void *clientData)
{
	NandFlash *nf = clientData;
	if(value == SIG_HIGH) {
		/* Sample on positive edge */
		if(SigNode_Val(nf->sigCle) == SIG_LOW) {
			nf->cmd = sample_data(nf);	
			nf->readstate = RSTATE_IDLE;
			nf->writestate = WSTATE_IDLE;
			nf->row_addr = 0; 
			nf->col_addr = 0; 
			switch(nf->cmd) {
				case CMD_ERASE_SETUP:
					nf->addr_state = ASTATE_ROW0;
					break;

				case CMD_ERASE:
					/* Should verify if prev. cmd was ERASE_SETUP */
					NandFlash_EraseBlock(nf);
					break;
				
				case CMD_PP:
					nf->addr_state = ASTATE_IGNORE;
					NandFlash_PageProgram(nf);
					break;
				default:
					nf->addr_state = ASTATE_COL;
					
			}
			nf->data_cnt = 0;
		} else if(SigNode_Val(nf->sigAle) == SIG_LOW) {
			if(nf->addr_state == ASTATE_COL) {
				nf->col_addr = sample_data(nf);
				nf->addr_state = ASTATE_ROW0;
			} else if(nf->addr_state == ASTATE_ROW0) {
				nf->row_addr = sample_data(nf);
				nf->addr_state = ASTATE_ROW1;
			} else if(nf->addr_state == ASTATE_ROW1) {
				nf->row_addr = nf->row_addr | (sample_data(nf) << 8);
				nf->addr_state = ASTATE_ROW2;
			} else if(nf->addr_state == ASTATE_ROW2) {
				nf->row_addr = nf->row_addr | (sample_data(nf) << 16);
				nf->addr_state = ASTATE_ROW3;
			} else if(nf->addr_state == ASTATE_ROW3) {
				nf->row_addr = nf->row_addr | (sample_data(nf) << 24);
				nf->addr_state = ASTATE_IGNORE;
				switch(nf->cmd) {
					case CMD_READ1_L:
					case CMD_READ1_H:
					case CMD_READ2:
					case CMD_PP_SETUP:
						fill_cache(nf);
						make_busy(nf,nf->us_busy_read);
						break;
				}
			} else {
				/* Feed the write state machine */
				NandFlash_Write(nf);	
			}
		} else {
			//sample_data();
		}
	}
}

/**
 **************************************************************************************
 * \fn static void nf_re_change(SigNode *node,int value,void *clientData)
 **************************************************************************************
 */
static void
nf_re_change(SigNode *node,int value,void *clientData)
{
	NandFlash *nf = clientData;
	if(value == SIG_LOW) {
		NandFlash_Read(nf);
	} else {
		release_data(nf);
	}	
}

/**
 ********************************************************************************
 * \fn static void nf_ce_change(SigNode *node,int value,void *clientData)
 ********************************************************************************
 */
static void
nf_ce_change(SigNode *node,int value,void *clientData)
{
	NandFlash *nf = clientData;
	if(value == SIG_LOW) {
		if(!nf->weTrace) {
			nf->weTrace = SigNode_Trace(nf->sigWe,nf_we_change,nf);
		}
		if(!nf->reTrace) {
			nf->reTrace = SigNode_Trace(nf->sigWe,nf_re_change,nf);
		}
	} else {
		if(nf->weTrace) {
			SigNode_Untrace(nf->sigWe,nf->weTrace);
                        nf->weTrace = NULL;
		}
		if(nf->reTrace) {
			SigNode_Untrace(nf->sigRe,nf->reTrace);
                        nf->reTrace = NULL;
		}
	}
}

NandFlash *
NandFlash_New(const char *name) 
{
	int i,nr_types;
	char *type,*imagedir,*filename;
	FlashDescription *fld;
	NandFlash *nfd;
	type = Config_ReadVar(name,"type");	
	if(!type) {
		fprintf(stderr,"No type for Nand Flash \"%s\". Creating nothing\n",name);
		return NULL;
	}
	nr_types = sizeof(flash_types) / sizeof(FlashDescription);
	for(i=0;i < nr_types;i++) {
		fld = &flash_types[i];
		if(!strcmp(type,fld->type)) {
			break;
		}
	}
	if(i == nr_types) {
		fprintf(stderr,"Flash type \"%s\" not found\n",type);
		exit(1);
	}
	imagedir = Config_ReadVar("global","imagedir");
	if(!imagedir) {
		fprintf(stderr,"No directory for NAND Flash diskimage given\n");
		return NULL;
	}
	filename = alloca(strlen(imagedir) + strlen(name) + 50);
	sprintf(filename,"%s/%s.img",imagedir,name);
	nfd = sg_new(NandFlash);
	nfd->us_busy_read = fld->us_busy_read;
	nfd->nfplanes = fld->planes;
	nfd->addrmask = 1 << (fld->addrbits-1);
	nfd->nfplane = sg_calloc(nfd->nfplanes * sizeof(NFPlane));
	nfd->diskimage = DiskImage_Open(filename,fld->size,DI_RDWR | DI_CREAT_FF | DI_SPARSE); 
	nfd->cAreaStart = fld->data_pagesize * fld->pages_per_block * fld->blocks; 
	nfd->data_pagesize = fld->data_pagesize;
	nfd->spare_pagesize = fld->spare_pagesize;
	nfd->pagesize = fld->data_pagesize + fld->spare_pagesize;
	nfd->page_cache = sg_calloc(nfd->pagesize);
	nfd->pages_per_block = fld->pages_per_block;
	nfd->size = fld->size;

	nfd->type = sg_strdup(type);
	nfd->sigAle = SigNode_New("%s.ale",name);
	nfd->sigCle = SigNode_New("%s.cle",name);
	nfd->sigCe = SigNode_New("%s.ce",name);
	nfd->sigWe = SigNode_New("%s.we",name);
	nfd->sigRe = SigNode_New("%s.re",name);
	nfd->sigBusy = SigNode_New("%s.busy",name);
	if(!nfd->sigAle || !nfd->sigCle || !nfd->sigCe || !nfd->sigCe || !nfd->sigWe
	  || !nfd->sigRe || !nfd->sigBusy) {
		fprintf(stderr,"Can not create signale lines for NAND flash \"%s\"\n",name);
		exit(1);
	}
	for(i = 0; i < 8; i ++) {
		nfd->sigData[i] = SigNode_New("%s.data%d",name,i);
		if(!nfd->sigData[i]) {
			fprintf(stderr,"Can not create data lines for NAND flash \"%s\"\n",name);
			exit(1);
		}
	}
	nfd->ceTrace = SigNode_Trace(nfd->sigCe,nf_ce_change,nfd);
	//nf->cmd = CMD_READ1;
	return nfd;
}
