#include <nand.h>
#include <req.h>
#include <nandbus.h>
#include <scsi_layer.h>
#include <chunks.h>
#include <tm.h>
#include <hint_data.h>
#include <hint.h>
#include <time.h>
#include <ngssd.h>
static char filename[10] = "ngssd.c";
#ifdef Debug
extern int maintid;
extern int tids[NAND_CHIP_COUNT];
static char mode[10] = "a+";

static FILE* open_file(char* fname){
	return fopen(fname, mode);
}

static void close_file(FILE* file){
	fclose(file);
}
#endif

extern int fds[NUM_SIM_FILES];
#ifdef PROFILING
extern uint32_t real_writes;
extern uint32_t real_reads;
extern uint32_t reclaim_reads;
extern uint32_t reclaim_writes;

extern uint32_t ch_real_writes;
extern uint32_t ch_real_reads;
extern uint32_t ch_tm_reclaim_reads;
extern uint32_t ch_reclaim_reads;
extern uint32_t ch_reclaim_writes;

extern uint32_t hint_accessed;
extern uint32_t read_accessed;
extern int nand_total_reads;
extern int nand_total_writes;
extern int nand_erases;

extern pthread_t       profiling_thread;
extern pthread_mutex_t profiling_lock;
#endif
extern uint32_t program[NAND_PAGE_COUNT];
/*******
 * TODO:
 *
 * low 1. SSD_MAX_HANDLED not relevant for hints? how to handle them?
 * V 2. hints - later on reject them in chm if we don't have a free cache chunk?
 * V 3. bus lock
 * 4. malloc count and max
 * V (not used) 5. interval between scsi reqs issuing
 * V 6. wrap ngssd.mutex_counter with Fanatic
 *
 */
#define SSD_MAX_HANDLED   NAND_CHIP_COUNT
extern chm_t chm;
extern tm_t tm;
extern ngssd_t ngssd;

/******************************************************
                      bus/chip
 ******************************************************/
/**
 * Assume ngssd_is_chip_ready(bus, chip) == true
 */
bool_t
ngssd_is_chip_done(int bus, int chip){
	return  nandbus[bus].chip[chip].current != 0;
}

bool_t
ngssd_is_chip_ready(int bus, int chip){
	return (nandbus[bus].chip[chip].rb == RB_READY);

}

/**
 * dorment if none is handled and no queue
 */
bool_t
ngssd_is_dorment(){
	if(ngssd.queued == 0){
		LOG("ngssd.queued 0 counter %u handled_counter %u\n", ngssd.counter, ngssd.handled_counter);
	}
	else{
		LOG("ngssd.queued %x lba %u %s magic_num %u, counter %u handled_counter %u\n", ngssd.queued,
				((scsi_specific_t*) ((ngssd.queued)->specific))->cdb->start_lba,
				(((scsi_specific_t*) ((ngssd.queued)->specific))->cdb->cmd == SCSI_READ)?"READ":"WRITE",
						((scsi_specific_t*) ((ngssd.queued)->specific))->cdb->magic_num, ngssd.counter, ngssd.handled_counter);
	}
	return (ngssd.queued == 0 && ngssd.handled_counter == 0);
}

bool_t
ngssd_can_handle_more(){
	LOG("ngssd.handled_counter %u\n", ngssd.handled_counter);
	return (ngssd.handled_counter < SSD_MAX_HANDLED);
}

void
handle_nandbus_defered(req_t* req){
	if (ngssd.nandbusDeferred != 0) {
		pthread_mutex_lock(&(ngssd.nandbus_defer_mutex));
		req->caller_req = ngssd.nandbusDeferred;
		ngssd.nandbusDeferred = ngssd.nandbusDeferred->next;
		ngssd.nandbusDeferred->prev = 0;
		req->next       = 0;
		pthread_mutex_unlock(&(ngssd.nandbus_defer_mutex));

		req_activate(req->caller_req, req);
	} else {
		free(req->specific);
		free(req);
	}
}

void
ngssd_query_chip(int bus, int chip){
	req_t* nandbus_req = 0;
	pthread_mutex_lock(&(nandbus[bus].chip[chip].mutex));

	/* chip READY*/
	LOG("bus %u chip %u rb %s current %u\n", bus, chip, (nandbus[bus].chip[chip].rb == RB_READY)?"READY":"BUSY", nandbus[bus].chip[chip].current);
	if(ngssd_is_chip_ready(bus, chip)){
		/* chip done with current*/
		if(ngssd_is_chip_done(bus, chip)){
			/* lock bus to transfer relevant data (read page, status,...)
			 * to the MCU*/
			pthread_mutex_lock(&(nandbus[bus].mutex));

			/* bus already taken*/
			if(nandbus[bus].current_dma_transfer == 1){
				pthread_mutex_unlock(&(nandbus[bus].mutex));
				pthread_mutex_unlock(&(nandbus[bus].chip[chip].mutex));
				return;
			}

			/* bus is free
			 * do our own "transfer" (currently no-op)*/
			LOG("put new current\n");
			nandbus[bus].current_dma_transfer = 1;

			/* get finished req from chip*/
			nandbus_req = nandbus[bus].chip[chip].current;
			nandbus[bus].chip[chip].current = 0;

			/* transfer data and signal others that bus is free*/
			nandbus[bus].current_dma_transfer = 0;
			pthread_mutex_unlock(&(nandbus[bus].mutex));
			pthread_cond_signal(&(nandbus[bus].cond));
		}

		/* signal for next request in queue (if there is one)
		 * and free mutex*/
		if(nandbus[bus].chip[chip].queued !=0){
			pthread_mutex_unlock(&(nandbus[bus].chip[chip].mutex));
			pthread_cond_signal(&(nandbus[bus].chip[chip].cond));
		}
		else{
			pthread_mutex_unlock(&(nandbus[bus].chip[chip].mutex));
		}

		/* chip finished some req. continue it*/
		if(nandbus_req != 0){
			LOG("activate nandbus_req %x\n", nandbus_req);
			req_activate_caller(nandbus_req, 0);

			/*finally free req*/
			handle_nandbus_defered(nandbus_req);
		}
	}
	/* chip BUSY. continue*/
	else{
		pthread_mutex_unlock(&(nandbus[bus].chip[chip].mutex));
	}
}

void
ngsdd_handle_bus(uint32_t bus){
	int chip;

	for (chip=0; chip<NAND_CHIPS_PER_BUS; chip++) {
		ngssd_query_chip(bus,chip);
	}
}

/******************************************************
                      hints
 ******************************************************/
#ifdef NOHINT
void
ngssd_tentative_cached_hint_notify(){
	return;
}
#else
void
ngssd_tentative_cached_hint_notify(){
	/* no unsent cached chunk*/
	if(chm_get_cache_chunk_hint_accessible_idx() == IDX_EMPTY)
		return;
        LOG("got available hint. issue scsi request\n");
	cdb_t *cdb = (cdb_t*)malloc(sizeof(cdb_t));

	if(!cdb){
		/* malloc error. return*/
		return;
	}

	cdb->cmd = SCSI_READ;

	hint_data_t *hint_data = (hint_data_t*)malloc(sizeof(hint_data_t));

	if(hint_data == 0){
		free(cdb);
		return;
	}

	cdb->buf = hint_data->hint_payload; /*payload*/
	cdb->len = 1;
	cdb->start_lba = IDX_EMPTY;
	cdb->lba_size  = HINT_PAYLOAD_SIZE;
	cdb->private = hint_data;
	req_t* req = scsiRequest(hint_send, cdb, HINT_FLAG_YES);

	if(req == 0){
		free(hint_data);
		free(cdb);
		return;
	}

	req_activate(req, 0);
}
#endif
/******************************************************
                      tm/chm queues
 ******************************************************/
void
ngssd_tm_dequeue(){
	list_t *node, *curr_node;
	req_t *req;
	uint32_t chip_idx;

	/* lba pending*/
	node = tm.lba_pending;
	while(node != 0){//LOG("node->req %x\n", node->req);
		req = node->req;
//		/* max handled reached*/
//		if(ngssd_can_handle_more() == 0){
//			break;
//		}

		node = node->next;
		/* lba not handled */
		if(list_lookup(&(tm.handled_lba), TM_REQ_GET_SPECIFIC(req)->curr_lba) == 0){
			tm_activate_former_lba_pending(req);
			req_activate(req, 0);
		}
	}

	/* open_eu pending*/
	node = tm.chip_pending;
	while(node != 0){//LOG("node->req %x\n", node->req);
//		/* max handled reached*/
//		if(ngssd_can_handle_more() == 0){
//			break;
//		}
		req = node->req;

		/* found open eu not handling a write*/
		chip_idx = tm_get_free_chip_idx(req);
		if(chip_idx != IDX_EMPTY){//LOG("req %x to free chip %u\n", req, chip_idx);
			LOG("send tm req %x to free chip %u\n", req, chip_idx);
			node = node->next;
			tm_activate_former_chip_pending(req, chip_idx);
			req_activate(req, 0);
			continue;
		}

		/* naively - no free chip, so we're done.
		 * however some req may be the one a reclaim req is waiting for*/
		node = node->next;
	}
}

void
ngssd_chm_dequeue(){
	req_t *req;
	list_t *node, *curr_node;
	uint32_t idx;

	/* chm write_buffer pending*/
	/* max handled not reached*/
	LOG("starting\n");

	/* we have write_pending reqs*/
	LOG("chm.write_pending %u\n", chm.write_pending);
	if(chm.write_pending != 0){
		/* see if write_buf is free*/
		//LOG("chm_is_write_buf_available\n");
		if(chm_is_write_buf_available()){
			LOG("write buf available. chm_list_pop_head\n");
			req = chm_list_pop_head(&(chm.write_pending));LOG("dequeue write_pending. req %x\n", req);
			CH_REQ_GET_SPECIFIC(req)->enq_reason = CHM_ENQUEUE_REASON_NONE;
			LOG("req_activate\n");
			req_activate(req, 0);
		}
	}

	/* update relevant data structures (before we go to buffer pending!)*/
	LOG("chm_update_buffer_pending_queue\n");
	chm_update_buffer_pending_queue();/* update relevant BUFFER_TAKEN to NO_BUFFER*/
        if(chm.reserved_used > 0){
	  LOG("chm_update_reserve_cache\n");
	  chm_update_reserve_cache();/* make reserved cache entries that regular if possible*/
        }

	/* cached chunk pending*/
	node = chm.buffer_pending;
	LOG("chm.buffer_pending %x\n", chm.buffer_pending);
	while(node != 0){
//		/* max handled reached*/
//		if(ngssd_can_handle_more() == 0){
//			break;
//		}

		/* found relevant chunk in cache, or a free cache chunk*/
		req = node->req;
		idx = chm_get_cache_chunk(req);
		if(idx != IDX_EMPTY){
			curr_node = node;
			node = node->next;
			assert(req == chm_list_pop_specific_req(&(chm.buffer_pending), req));
			/* for regular enqueued requests decrement unforced coutner*/
			if(CH_REQ_GET_SPECIFIC(req)->enq_reason != CHM_ENQUEUE_REASON_BUFFER_TAKEN){
				chm.unforced_counter--;assert(chm.unforced_counter >= 0);
			}
			CH_REQ_GET_SPECIFIC(req)->enq_reason = CHM_ENQUEUE_REASON_NONE;
			CH_REQ_GET_SPECIFIC(req)->dequeued = 1;

			req_activate(req, 0);
			continue;
		}

		node = node->next;
	}
}

bool_t ngssd_is_idle(){
	return (ngssd.handled_counter == 0);
}

/**********************************************************************
											ngssd main thred and loop
 **********************************************************************/
void* ngssdThread(void* argument) {
	int bus;
	req_t* req;
	list_t *node, *curr_node;
	bool_t bool;
	uint32_t chip_idx;
	struct timespec ts;
	int ret;

#ifdef Debug
	maintid = pthread_self();
#endif
	/*main event loop*/
	while (1) {
		LOG("start loop again\n");
		/* iterate chips. for every chip that's ready
		 * - check if its done with request. if so continue req context
		 * - if queue is not empty, mark it busy and signal*/
			LOG("ngssd_query_chip for RB\n");
		for (bus=0; bus<NAND_BUSES; bus++) {
			ngsdd_handle_bus(bus);
		}

		LOG("ngssd_tm_dequeue\n");
		ngssd_tm_dequeue();
		LOG("ngssd_chm_dequeue\n");
		ngssd_chm_dequeue();

		/* iterate resources queues of pending reqs for:
		 * - lba
		 * - tm open eu
		 * - chunk
		 * - chm write buffer*/

		/* wait for signal if necessary*/
		ngssd_lock();/* lock required, for counter+queued access*/
		LOG("ngssd_is_dorment ?\n");
		if(ngssd_is_dorment()){
			/* read chunk from flash and send back*/
			ngssd_tentative_cached_hint_notify();

			LOG("wait on ngssd.cond\n");
#ifdef Fanatic
			ngssd.mutex_count--;assert(ngssd.mutex_count==0);
#endif
		clock_gettime(CLOCK_REALTIME, &ts);
		if(ts.tv_nsec < 1000*1000*1000-100*1000){
			ts.tv_nsec += 100*1000; // wait 100us
		}
		else{
			ts.tv_sec += 1;
			ts.tv_nsec = 100*1000; // wait 100us (not exact)
		}

		ret = pthread_cond_timedwait(&(ngssd.cond),
																	 &(ngssd.mutex),
									 &ts);


#ifdef Fanatic
		ngssd.mutex_count++;assert(ngssd.mutex_count==1);
#endif
	}

//		chmRequest((req_t*) response,(specific->cdb->cmd == SCSI_READ)?CHM_READ:CHM_WRITE_HINT, specific->cdb->start_lba, IDX_EMPTY, HINT_FLAG_YES);

    /* if a scsi req is pending
     * check if we can insert it without exhausting the resources
     * (lock acquired at this point)*/
		LOG("check for pending Scsi reqs\n");
    while(1){
    	/* max handled reached*/
    	if(ngssd_can_handle_more() == 0){
    		LOG("cant handle more reqs. dont get scsi reqs\n");
    		ngssd_unlock();
    		break;
    	}

    	/* dequeue scsi req*/
    	req = reqDequeue(&(ngssd.queued));

    	if(req!=0){
    		ngssd.counter -= 1; // counter update
    		ngssd.handled_counter += 1; /*handled counter update*/
    		ngssd_unlock();
    		req_activate(req, 0);
    		break;
    	}

     	/* queue empty */
    	if(req==0){
    		pthread_mutex_unlock(&(ngssd.mutex));
    		break;
    	}

    }
  }

  return 0;
}

#ifdef PROFILING
/**********************************************************************
											ngssd main thred and loop
 **********************************************************************/
count_busy(){
	int n=0, bus, chip;
	for (bus=0; bus<NAND_BUSES; bus++) {
	    for (chip=0; chip<NAND_CHIPS_PER_BUS; chip++) {
	    	if(nandbus[bus].chip[chip].rb == RB_BUSY) n++;
	    }
	}

	return n;
}
void* profilingThread(void* argument) {
	/* open log file */
	time_t lt;
	struct tm *ptr;
        char str[80];

        lt = time(NULL);
        ptr = localtime(&lt);

        strftime(str, 100, "%H_%M_%S.", ptr);
	char fname[100];
	sprintf(fname, "%s_%s.log", "test", str);
	FILE* file = fopen(fname, "w+");

	fprintf(file, "%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n",
				          "time",
						  "writes", "reads", "reclamation reads", "reclamation writes",
				          "ch writes", "ch reads", "ch tm reclaim reads", "ch reclamation reads", "ch reclamation writes",
				          "hinted chunk real access", "read chunk real access",
				          "reads (flash)", "writes (flash)", "erases (flash),busy_count,handled_counter");
	fclose(file);
	/*main loop*/
	while (1) {
		sleep(10);
	        lt = time(NULL);
                ptr = localtime(&lt);
                strftime(str, 100, "%H_%M_%S.", ptr);
		file = fopen(fname, "a+");
		/*flush data to log files*/
		fprintf(file, "%s,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u\n",
						str,

						real_writes,
						real_reads,
						reclaim_reads,
						reclaim_writes,

						ch_real_writes,
						ch_real_reads,
						ch_tm_reclaim_reads,
						ch_reclaim_reads,
						ch_reclaim_writes,

						hint_accessed,
						read_accessed,

						nand_total_reads,
						nand_total_writes,
						nand_erases,
						count_busy(),
						ngssd.handled_counter);
		fclose(file);
	}

	return 0;
}
#endif

int
write_struct_to_file(char* fname, void* strct_ptr, uint32_t strct_size){
	int i, irc; // return code
	FILE* fp = fopen(fname,"w+");
	assert(fp != NULL);

	// write the entire struct into the file pointed to by fp
	irc = fwrite(strct_ptr, 1, strct_size, fp);

	return fclose(fp);
}

int
load_struct_from_file(char* fname, void* strct_ptr, uint32_t strct_size){
	int i, irc; // return code
	FILE* fp = fopen(fname,"r");
	assert(fp != NULL);

	// write the entire struct into the file pointed to by fp
	irc = fread(strct_ptr, strct_size, 1, fp);

	return fclose(fp);
}

void mockHandler(req_t* req, void* response){
	free(req);
}

void
ngssdDestroy(){
#ifndef SAVE_STATE
	return;
#endif
	int i, n;
	req_t *chm_req, *req;

	/* stop recieving reqs, finish currently queued*/
	ngssd_lock();
	ngssd.shutdown = 1;
	ngssd_unlock();

	ngssd_lock();
	/*sleep until done with all reqs*/
	while(ngssd.counter > 0){
		ngssd_unlock();
		sleep(1);//PRINT("ngssd.counter %u. sleep(1)\n", ngssd.counter);
		ngssd_lock();
	}

	/*sleep some more until we are done with all tm requests*/
	ngssd_unlock();
	while(1){
		for(i=0; i< NAND_CHIP_COUNT;i++){
			if(tm.open_eu[i].current || tm.open_eu[i].reclamation_in_progress)
				break;
		}
		if(i==NAND_CHIP_COUNT)
			break;

		sleep(1);
	}

	/*sleep some more until we are done with all chm requests*/
	while(chm.open_eu.current || chm.open_eu.reclamation_in_progress){
		sleep(1);
	}

	/*force chunk flush*/
	if(chm_count_dirty_cache_entries()){
		req = (req_t*) malloc(sizeof(req_t));
		req->handler = mockHandler;

		chm_req = (req_t*) malloc(sizeof(req_t));

		  /* start allocating*/
		  chm_specific_t* specific;
		  specific = (chm_specific_t*) malloc(sizeof(chm_specific_t));
		  chm_req->handler    = chmHandler;
		  chm_req->caller_req = req;
		  chm_req->specific   = specific;
		  chm_req->next       = 0; // not in any list
		  chm_req->prev       = 0; // not in any list

		chmRequest(chm_req,
                   CHM_MAGIC_CACHE_FLUSH,
                   IDX_EMPTY,
                   IDX_EMPTY,
                   0);
	}

	/*sleep some more until we are done with all chm requests*/
	while(chm.open_eu.current || chm.open_eu.reclamation_in_progress){
		sleep(1);
	}

	/*save ngssd, tm, chm, nand page program array to relevant files*/
	write_struct_to_file("ngssd_state", &ngssd, sizeof(ngssd_t));
	write_struct_to_file("tm_state", &tm, sizeof(tm_t));
	write_struct_to_file("chm_state", &chm, sizeof(chm_t));
	write_struct_to_file("program_state", program, sizeof(NAND_PAGE_COUNT * sizeof(uint32_t)));

#ifndef PROFILING
	return;
#endif
	/*save profiling TODO: no ch_tm_reclaim_writes*/
	FILE* profiling_file = fopen("profiling_data", "w+");

	fprintf(profiling_file, "%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,\n",
					real_writes,
					real_reads,
					reclaim_reads,
					reclaim_writes,

					ch_real_writes,
					ch_real_reads,
					ch_reclaim_reads,
					ch_reclaim_writes,

					hint_accessed,
					read_accessed,

					nand_total_reads,
					nand_total_writes,
					nand_erases);
	fprintf(profiling_file, "%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n",
						  "real_writes", "real_reads", "reclaim_reads", "reclaim_writes",
						  "ch_real_writes", "ch_real_reads", "ch_reclaim_reads", "ch_reclaim_writes",
						  "hint_accessed", "read_accessed",
						  "nand_total_reads", "nand_total_writes", "nand_erases");
	fclose(profiling_file);
	printf("ngssdDestroy done!!\n");
}

void ngssdInit(void *lu) {
#ifdef Debug
	FILE* file = fopen("log_draft", "w+");
	fclose(file);
	char busfname[20] = "log_chip0";
	int k;
	for(k=0;k<NAND_CHIP_COUNT;k++){
					tids[k] = -1;
					busfname[8] = '0'+k;
					file = fopen(busfname, "w+");
					fclose(file);
	}
	maintid = -1;
#endif

#ifdef PROFILING
	real_writes = 0;
	real_reads = 0;
	reclaim_reads = 0;
	reclaim_writes = 0;

	ch_real_writes = 0;
	ch_real_reads = 0;
	ch_tm_reclaim_reads = 0;
	ch_reclaim_reads = 0;
	ch_reclaim_writes = 0;

	hint_accessed = 0;
	read_accessed = 0;

	nand_total_reads = 0;
	nand_total_writes = 0;
	nand_erases = 0;

#endif
#ifdef LOAD_STATE
  printf("load state\n");
  printf("load program array\n");
  load_struct_from_file("program_state", program, sizeof(NAND_PAGE_COUNT * sizeof(uint32_t)));
  printf("load ngssd state\n");
  load_struct_from_file("ngssd_state", &ngssd, sizeof(ngssd_t));
  printf("load tm state\n");
  load_struct_from_file("tm_state", &tm, sizeof(tm_t));
  printf("load chm state\n");
  load_struct_from_file("chm_state", &chm, sizeof(chm_t));

//  /*load profiling*/
//  printf("load profiling\n");
//  /*TODO: no ch_tm_reclaim_writes*/
//  	FILE* profiling_file = fopen("profiling_data", "r+");
//
//  	char line[128];
//  	fgets(line,
//  	              127,
//  	            profiling_file);
//  	sscanf(line, "%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,",
//  	  					real_writes,
//  	  					real_reads,
//  	  					reclaim_reads,
//  	  					reclaim_writes,
//
//  	  					ch_real_writes,
//  	  					ch_real_reads,
//  	  					ch_reclaim_reads,
//  	  					ch_reclaim_writes,
//
//  	  					hint_accessed,
//  	  					read_accessed,
//
//  	  					nand_total_reads,
//  	  					nand_total_writes,
//  	  					nand_erases);
//  	fclose(profiling_file);
#else
  	printf("dont load state\n");
#endif
	/*init data structs*/
	nandInit();
	nandbusInit();
	chmInit();
	tmInit();

	ngssd.shutdown = 0;
  ngssd.mutex_count = 0;
  ngssd.lu = lu;
  pthread_mutex_init(&(ngssd.mutex), NULL);
	pthread_cond_init (&(ngssd.cond ), NULL);
	pthread_create(&(ngssd.thread), NULL,
								 ngssdThread, 0);
	pthread_mutex_init(&(ngssd.nandbus_defer_mutex), NULL);
	ngssd.nandbusDeferred = 0;

	ngssd.queued  = 0;
	ngssd.counter = 0;
	ngssd.hint_counter = 0;
	ngssd.handled_counter = 0;

	pthread_mutex_init(&(profiling_lock), NULL);

	/*start profiling thread*/
	pthread_create(&(profiling_thread), NULL,
									 profilingThread, 0);
}
