#include <stdio.h>
#include <stdlib.h>

#include "defs.h"
#include "disk.h"
#include "interrupts_private.h"
#include "random.h"

HANDLE disk_mutex = NULL;

double crash_rate = 0.0;
double failure_rate = 0.0;
double reordering_rate = 0.0;


void start_disk_poll(disk_t* disk); /* forward declaration */

int disk_send_request(disk_t* disk, int blocknum, char* buffer,
		      disk_request_type_t type){
  disk_queue_elem_t* disk_request 
    = (disk_queue_elem_t*) malloc(sizeof(disk_queue_elem_t));

  if (disk_request == NULL)
    return -1;

  /* put data in the request */
  disk_request->request.blocknum = blocknum;
  disk_request->request.buffer = buffer;
  disk_request->request.type = type;

  /* queue the request */
  WaitOnObject(disk_mutex);
  
  if (type == DISK_SHUTDOWN){
    /* put the request on top of the queue */
    disk_request->next=disk->queue;
    disk->queue=disk_request;
  } else {
    /* put it at the end */
    disk_request->next=NULL;
    if (disk->last != NULL){
      disk->last->next=disk_request;
    } else {
      disk->queue=disk_request;
    }
    disk->last=disk_request;
  }
 
  ReleaseMutex(disk_mutex);
 
  /* signal the task that simulates the disk */
  if (!ReleaseSemaphore(disk->semaphore, 1, NULL)){
    kprintf("You have exceeded the maximum number of requests pending.\n");
    return -2; 
  }

  return 0;
}


int
disk_create(disk_t* disk, char* name, int size, int flags) {
  if ((disk->file = fopen(name, "w+b")) == NULL)
    return -1;
  disk->layout.size = size;
  disk->layout.flags = flags;

  /* write the disk layout to the "disk" */
  if (fwrite(&disk->layout, 1, sizeof(disk_layout_t), disk->file)
      != sizeof(disk_layout_t)) {
    fclose(disk->file);
    return -1;
  }

  start_disk_poll((void*)disk);

  return 0;
}

int
disk_startup(disk_t* disk, char* name) {
  if ((disk->file = fopen(name, "r+b")) == NULL)
    return -1;
  
  if (fread(&disk->layout, 1, sizeof(disk_layout_t), disk->file) != 
      sizeof(disk_layout_t)) {
    fclose(disk->file);
    return -1;
  }
  
  start_disk_poll((void*)disk);

  return 0;
}

static void
disk_write_layout(disk_t* disk) {
  if (fseek(disk->file, 0, SEEK_SET) != 0)
    fclose(disk->file);
  else if (fwrite(&disk->layout, 1, sizeof(disk_layout_t), disk->file) != 
	   sizeof(disk_layout_t))
    fclose(disk->file);
  else
    fflush(disk->file);
}

int
disk_shutdown(disk_t* disk) {
  disk_send_request(disk, 0, NULL, DISK_SHUTDOWN);
  return 0;
}
   
void
disk_set_flags(disk_t* disk, int flags) {
  WaitOnObject(disk_mutex);

  disk->layout.flags |= flags;
  disk_write_layout(disk);

  ReleaseMutex(disk_mutex);
}

void
disk_unset_flags(disk_t* disk, int flags) {
  WaitOnObject(disk_mutex);

  disk->layout.flags ^= disk->layout.flags | flags;
  disk_write_layout(disk);

  ReleaseMutex(disk_mutex);
}

int
disk_read_block(disk_t* disk, int blocknum, char* buffer) {
  return 
    disk_send_request(disk,blocknum,buffer,DISK_READ);
}

int
disk_write_block(disk_t* disk, int blocknum, char* buffer) {
  return 
    disk_send_request(disk,blocknum,buffer,DISK_WRITE);
}


/* handle read and write to disk. Watch the job queue and 
   submit the requests to the operating system one by one.
   On completion, the user suplied function with the user
   suplied parameter is called

   The interrupt mechanism is used much like in the network
   case. One such process per disk.

   The argument is a disk_t with the disk description.
*/
DWORD WINAPI disk_poll(LPVOID arg) {
  enum { DISK_OK, DISK_CRASHED } disk_state;

  disk_t* disk = (disk_t*) arg;
  disk_layout_t layout;

  disk_interrupt_arg_t* disk_interrupt;
  disk_queue_elem_t* disk_request;
  
  disk_state=DISK_OK;

  for (;;){
    int blocknum;
    char* buffer;
    disk_request_type_t type;
    int offset;

    /* We use mutex to protect queue handling, as should
       the code that inserts requests in the queue
    */
    
    /* wait for somebody to put something in the queue */
    WaitOnObject(disk->semaphore);
    
    if (DEBUG)
      kprintf("Disk Controler: got a request.\n");

    /* get exclusive access to queue handling  and dequeue a request */
    WaitOnObject(disk_mutex);

    layout = disk->layout; /* this is the layout used until the request is fulfilled */
    /* this is safe since a disk can only grow */

    if (disk->queue != NULL){
      disk_interrupt = (disk_interrupt_arg_t*)
	malloc(sizeof(disk_interrupt_arg_t));
      assert( disk_interrupt != NULL);

      disk_interrupt->disk = disk;
      /* we look first at the first request in the queue
	 to see if it is special.
      */
      disk_interrupt->request = 
	disk->queue->request;

      /* check if we shut down the disk */
      if (disk->queue->request.type == DISK_SHUTDOWN){
	if (DEBUG)
	  kprintf("Disk: Shutting down.\n");

	disk_interrupt->reply=DISK_REPLY_OK;
	fclose(disk->file);
	ReleaseMutex(disk_mutex);
	CloseHandle(disk->semaphore);
	send_interrupt(DISK_INTERRUPT_TYPE, (void*)disk_interrupt);
	break; /* end the disk task */
      }

      /* check if we got to reset the disk */
      if (disk->queue->request.type == DISK_RESET){
	disk_queue_elem_t* curr;
	disk_queue_elem_t *next;

	if (DEBUG)
	  kprintf("Disk: Resetting.\n");

	disk_interrupt->reply=DISK_REPLY_OK;
	/* empty the queue */
	curr=disk->queue;
	while (curr!=NULL){
	  next=curr->next;
	  free(curr);
	  curr=next;
	}
	disk->queue = disk->last = NULL;

	disk_state = DISK_OK;
	ReleaseMutex(disk_mutex);
	goto sendinterrupt;
      }

      /* permute the first two elements in the queue
	 probabilistically if queue has two elements 
      */
      if (disk->queue->next !=NULL && 
	  (genrand() < reordering_rate)){
	disk_queue_elem_t* first = disk->queue;
	disk_queue_elem_t* second = first->next;
	first->next = second->next;
	second->next = first;
	disk->queue = second;
	if (disk->last == second)
	  disk->last = first;
      }

      /* dequeue the first request */
      disk_request = disk->queue;
      disk->queue = disk_request->next;
      if (disk->queue == NULL)
	disk->last = NULL;
    } else {
      /* empty queue, release the lock and leave */
      ReleaseMutex(disk_mutex);
      break;
    }
  
    ReleaseMutex(disk_mutex);

    disk_interrupt->request = disk_request->request;

    /* crash the disk ocasionally */
    if (genrand() < crash_rate){
      disk_state = DISK_CRASHED;

      /*      if (DEBUG) */
      kprintf("Disk: Crashing disk.\n");

    }

    /* check if disk crashed */
    if (disk_state == DISK_CRASHED){  
      disk_interrupt->reply=DISK_REPLY_CRASHED;
      goto sendinterrupt;
    }

    if ( genrand() < failure_rate ) {
      /* Trash the request */
      disk_interrupt->reply = DISK_REPLY_FAILED;

      if (DEBUG)
	kprintf("Disk: Request failed.\n");

      goto sendinterrupt;
    }
     
    /* Check validity of request */
      
    disk_interrupt->reply = DISK_REPLY_OK;
    
    blocknum = disk_request->request.blocknum;
    buffer = disk_request->request.buffer;
    type = disk_request->request.type;

    if (DEBUG)
      kprintf("Disk Controler: got a request for block %d type %d .\n",
	      blocknum, type);

    /* If we got here is a read or a write request */

    offset = DISK_BLOCK_SIZE*(blocknum + 1);

    if ( (blocknum >= layout.size) ||
	 (fseek(disk->file, offset, SEEK_SET) != 0) ) {
      disk_interrupt->reply = DISK_REPLY_ERROR;
      
      if (DEBUG)
	kprintf("Disk Controler: Block too big or failed fseek, block=%d,  offset=%d, disk_size=%d.\n",
		blocknum, offset, layout.size);

      goto sendinterrupt;
    }
     
    switch (type) {
    case DISK_READ:
      if (fread(buffer, 1, DISK_BLOCK_SIZE, disk->file) 
	  < DISK_BLOCK_SIZE)
	disk_interrupt->reply = DISK_REPLY_ERROR;
      if (DEBUG)
	kprintf("Disk: Read request.\n");

      break;
    case DISK_WRITE:
      if (fwrite(buffer, 1, DISK_BLOCK_SIZE, disk->file) 
	  < DISK_BLOCK_SIZE)
	disk_interrupt->reply = DISK_REPLY_ERROR;
      fflush(disk->file);
      if (DEBUG)
	kprintf("Disk: Write request.\n");

      break;
    default:
      break;
    }
    
  sendinterrupt:
    if (DEBUG)
      kprintf("Disk Controler: sending an interrupt for block %d, request type %d, with reply %d.\n",
	      disk_interrupt->request.blocknum, 
	      disk_interrupt->request.type,
	      disk_interrupt->reply);

    send_interrupt(DISK_INTERRUPT_TYPE, (void*)disk_interrupt);
  }

  /* never reached */
  return 0;
}

void start_disk_poll(disk_t* disk){
  DWORD id;  
  HANDLE disk_thread;

  /* reset the request queue */
  disk->queue = disk->last = NULL;
  
  /* create request semaphore */
  disk->semaphore = CreateSemaphore(NULL, 0, MAX_PENDING_DISK_REQUESTS, NULL); 

  disk_thread = 
    CreateThread(NULL, 0, disk_poll, (void*)disk, 0, &id);
}

void install_disk_handler(interrupt_handler_t disk_handler){
  kprintf("Starting disk interrupt.\n");
  register_interrupt(DISK_INTERRUPT_TYPE, disk_handler, INTERRUPT_DEFER);
  
  /* create mutex used to protect disk datastructures */
  disk_mutex = CreateMutex(NULL, FALSE, NULL);
}
