#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <ctype.h>

#define TRUE 1
#define FALSE 0

#define MAX(a,b) (((a)>(b))?(a):(b))

uint32_t PC = 0, R[8] = {0,0,0,0,0,0,0,0};

/******* EXCEPTIONS *********/

#define EXCEPTION_CALLOC                       0x01
#define EXCEPTION_FREE_NOT_ALLOCATED           0x02
#define EXCEPTION_FREE_ZERO                    0x03
#define EXCEPTION_NO_SUCH_ARRAY                0x04
#define EXCEPTION_OUT_OF_ARRAY                 0x05
#define EXCEPTION_STOP                         0x06
#define EXCEPTION_DIVIDE_BY_ZERO               0x07
#define EXCEPTION_OUT_OF_PROGRAM               0x08
#define EXCEPTION_CHAR_TOO_BIG                 0x09
#define EXCEPTION_NO_SUCH_ARRAY_TO_LOAD        0x0A
#define EXCEPTION_INVALID_INSTRUCTION          0x0B

char *exceptions[] = 
{"CALLOC" ,
"FREE_NOT_ALLOCATED",
"FREE_ZERO",
"NO_SUCH_ARRAY",
"OUT_OF_ARRAY",
"STOP",
"DIVIDE_BY_ZERO",
"OUT_OF_PROGRAM",
"CHAR_TOO_BIG",
"NO_SUCH_ARRAY_TO_LOAD",
"INVALID_INSTRUCTION"};

void show_mem();
void dump();

void exception(unsigned int ex) {
  int i;
  if(ex != EXCEPTION_STOP) {
    show_mem();
    dump();
    printf("\n%u: Exception %s",PC,exceptions[ex-1]);
    for(i = 0;i < 8;i++)
    printf(", R%d = %d",i,R[i]);
    printf(".\n");

    fflush(stdout);
  }
  exit(0);
}


/******* MEMORY **********/

struct array {
  uint32_t *block;
  uint32_t size;
  uint8_t present;
};

struct free_list {
  uint32_t id;
  struct free_list *next;
};

struct free_list *free_memory = NULL;
struct array *memory = NULL;
uint32_t nb_arrays = 0;

void show_mem() {
  int i,j,o;
  FILE *out = fopen("memlog.txt","w");
  for(i = 0;i < nb_arrays;i++) {
    if(memory[i].present) {
      fprintf(out,"BLOCK #%u (size = %u)\n",i,memory[i].size);
      o = 0;
      while(o < memory[i].size) {
        for(j = 0;j < 8 && o < memory[i].size ;j++)
           fprintf(out,"%08X ",memory[i].block[o+j]);
        for(j = 0;j < 8 && o < memory[i].size ;j++) {
           char c = memory[i].block[o+j];
           if(isprint(c))
            fprintf(out,"%c",memory[i].block[o+j]);
           else
            fprintf(out,".");
         }
        fprintf(out,"\n");
        o += 8;
      }
      fprintf(out,"\n");
    }
  }
  fclose(out);
}

#define ALLOC_CHUNK 1024

/* allocates an array of size words and returns its id */
uint32_t alloc_array(uint32_t size) {
  uint32_t *block;
  uint32_t i;
  /* alloc block and array block */
  block = calloc(size,sizeof(uint32_t));
  if(block == NULL)
    exception(EXCEPTION_CALLOC);
  /* search for a non allocated array */
  if (free_memory) {
    struct free_list *old = free_memory;
    i = free_memory->id;
    memory[i].size = size;
    memory[i].block = block;
    memory[i].present = TRUE;
    free_memory = free_memory->next;
    free(old);
#ifdef DEBUG
      fprintf(stderr,"---ALLOC %d %d %lu----\n",i,size,(unsigned long) memory[i].block);
#endif /* DEBUG */
    return i;
  }
  /* not enough space */
  struct array *tmp = memory;
  for(i = 1;i < ALLOC_CHUNK;i++) {
    struct free_list *new = malloc(sizeof(struct free_list));
    if(new == NULL)
      exception(EXCEPTION_CALLOC);
    new->id = nb_arrays + i;
    new->next = free_memory;
    free_memory = new;
  }
  tmp = realloc(memory,sizeof(struct array)*(nb_arrays + ALLOC_CHUNK));
  if(tmp == NULL)
    exception(EXCEPTION_CALLOC);
  memory = tmp;
  /* new array */
  for(i = nb_arrays;i < nb_arrays + ALLOC_CHUNK;i++)
    memory[i].present = FALSE;
  i = nb_arrays;
  memory[nb_arrays].size = size;
  memory[nb_arrays].block = block;
  memory[nb_arrays].present = TRUE;
  nb_arrays += ALLOC_CHUNK;
#ifdef DEBUG
  fprintf(stderr,"---ALLOC %d %d %lu----\n",i,size,(unsigned long) memory[i].block);
#endif /* DEBUG */
  return i;
}

/* frees the specified array */
void free_array(uint32_t id) {
  struct free_list *new;
#ifdef DEBUG
  fprintf(stderr,"---FREE %d %lu ----\n",id,(unsigned long) memory[id].block);
#endif /* DEBUG */
  if(id == 0)
    exception(EXCEPTION_FREE_ZERO);
  if(!memory[id].present)
    exception(EXCEPTION_FREE_NOT_ALLOCATED);
  memory[id].present = FALSE;
  free(memory[id].block);
  new = malloc(sizeof(struct free_list));
  if(new == NULL)
    exception(EXCEPTION_CALLOC);
  new->id = id;
  new->next = free_memory;
  free_memory = new;
}

/* checked data access */
uint32_t get(uint32_t R,uint32_t offset) {
  if(R < 0 || R >= nb_arrays || !memory[R].present)
    exception(EXCEPTION_NO_SUCH_ARRAY);
  if(offset < 0 || offset >= memory[R].size)
    exception(EXCEPTION_OUT_OF_ARRAY);
  return memory[R].block[offset];
}

/* checked data modification */
void set(uint32_t R,uint32_t offset, uint32_t value) {
  if(R < 0 || R >= nb_arrays || !memory[R].present)
    exception(EXCEPTION_NO_SUCH_ARRAY);
  if(offset < 0 || offset >= memory[R].size)
    exception(EXCEPTION_OUT_OF_ARRAY);
  memory[R].block[offset] = value;
}

/******* REGISTERS *******/

void dump(void) {
    char buf[255];
    FILE *fdump;
    static int count = 0;
  
    fprintf(stderr,"%d: Dumping in new_dump.%d\n", PC, count);
    sprintf(buf, "new_dump.%d", count++);
    fdump = fopen(buf, "w");
    fwrite(memory[0].block, memory[0].size, sizeof(uint32_t), fdump);
    fclose(fdump);
}

void load(uint32_t R) {
  if(R < 0 || R >= nb_arrays || !memory[R].present)
    exception(EXCEPTION_NO_SUCH_ARRAY_TO_LOAD);
  /* set program to good size */
  memory[0].block = realloc(memory[0].block,memory[R].size * sizeof(uint32_t));
  if(memory[0].block == NULL)
    exception(EXCEPTION_CALLOC);
  memory[0].size = memory[R].size;
  /* blit program */
  memcpy(memory[0].block, memory[R].block,memory[R].size * sizeof(uint32_t));
#ifdef DUMP
  dump();
#endif /* DUMP */
}

void interprete(uint32_t instr) {
  int i;
  uint8_t A, B, C;
  C = instr & 7;
  B = (instr >> 3) & 7;
  A = (instr >> 6) & 7;
#ifdef DEBUG
/*  printf("PC = %d ",PC);
  for(i = 0;i < 8;i++)
    printf(", R%d = %d",i,R[i]);
    printf("\n");
  if(instr >> 28 == 13)
    printf("-- %d - %d - %u --\n", instr >> 28, (instr >> 25) & 7, instr & ((1 << 25) - 1));
  else
    printf("-- %d - %d(%d) - %d(%d) - %d(%d) ---\n",instr >> 28,A,R[A],B,R[B],C,R[C]);
  fflush(stdout); */
#endif /* DEBUG */
  switch (instr >> 28) {
  case 0:
    if (R[C] != 0) {
      R[A] = R[B];
#ifdef DEBUG
      fprintf(stderr,"%d: R%d <- R%d (%u)\n",PC,A,B,R[B]);
      fflush(stderr);
#endif /* DEBUG */
    }
    PC++;
    break;
  case 1: /******** Get */
#ifdef DEBUG
    fprintf(stderr,"%d: R%d <- R%d[R%d] (%u[%u])\n",PC,A,B,C,R[B],R[C]);
    fflush(stderr);
#endif /* DEBUG */
    R[A] = get(R[B],R[C]);
    PC++;
    break;
  case 2: /******** Set */
#ifdef DEBUG
    fprintf(stderr,"%d: R%d[R%d] <- R%d (%u[%u] <- %u)\n",PC,A,B,C,R[A],R[B],R[C]);
    fflush(stderr);
#endif /* DEBUG */
    set(R[A],R[B],R[C]);
    PC++;
    break;
  case 3: /******** + */
#ifdef DEBUG
    fprintf(stderr,"%d: R%d <- R%d + R%d (%u + %u)\n",PC,A,B,C,R[B],R[C]);
    fflush(stderr);
#endif /* DEBUG */
    R[A] = (uint32_t)R[B] + (uint32_t)R[C];
    PC++;
    break;
  case 4: /******** * */
#ifdef DEBUG
    fprintf(stderr,"%d: R%d <- R%d * R%d (%u * %u)\n",PC,A,B,C,R[B],R[C]);
    fflush(stderr);
#endif /* DEBUG */
    R[A] = (uint32_t)R[B] * (uint32_t)R[C];
    PC++;
    break;
  case 5: /******** / */
#ifdef DEBUG
    fprintf(stderr,"%d: R%d <- R%d / R%d (%u / %u)\n",PC,A,B,C,R[B],R[C]);
    fflush(stderr);
#endif /* DEBUG */
    if(R[C] == 0)
      exception(EXCEPTION_DIVIDE_BY_ZERO);
    R[A] = (uint32_t)R[B] / (uint32_t)R[C];
    PC++;
    break;
  case 6: /******** nand */
#ifdef DEBUG
    fprintf(stderr,"%d: R%d <- R%d nand R%d (%u nand %u)\n",PC,A,B,C,R[B],R[C]);
    fflush(stderr);
#endif /* DEBUG */
    R[A] = ~((uint32_t)R[B] & (uint32_t)R[C]);
    PC++;
    break;
  case 7: /******** Stop */
#ifdef DEBUG
    fprintf(stderr,"%d: Stop\n",PC);
#endif /* DEBUG */
    exception(EXCEPTION_STOP);
    break;
  case 8: /******** Alloc */
#ifdef DEBUG
    fprintf(stderr,"%d: R%d <- calloc(R%d (%u))\n",PC,B,C,R[C]);
#endif /* DEBUG */
    R[B] = alloc_array(R[C]);
    PC++;
    break;
  case 9: /******** Free */
#ifdef DEBUG
    fprintf(stderr,"%d: Free R%d: %u\n",PC,C,R[C]);
#endif /* DEBUG */
    free_array(R[C]);
    PC++;
    break;
  case 10: /******** Output */
    if (R[C] > 255) exception(EXCEPTION_CHAR_TOO_BIG);
#ifdef DEBUG
    fprintf(stderr,"%d: Output(R%d) \"%c\" %X\n",PC,C,R[C],R[C]);
#endif /* DEBUG */
    putchar(R[C]);
    fflush(stdout);
    PC++;
    break;
  case 11: /******** Input */
    if (EOF == (R[C] = getchar()))
      R[C] = 0xFFFFFFFF;
#ifdef DEBUG
    fprintf(stderr,"%d: Read(R%d) <- %X '%c'---\n",PC,C,R[C],R[C]);
#endif /* DEBUG */
    PC++;
    break;
  case 12: /******** Load */
#ifdef DEBUG
    fprintf(stderr,"%d: Load R%d (%u), PC = R%d (%u)\n",PC,B,R[B],C,R[C]);
#endif /* DEBUG */
    PC = R[C];
    if(R[B] != 0)
      load(R[B]);
    break;
  case 13: /******** Imm */
#ifdef DEBUG
    fprintf(stderr,"%d: R%d <- #%u\n", PC, (instr >> 25) & 7, instr & ((1 << 25) - 1));
#endif /* DEBUG */
    R[(instr >> 25) & 7] = instr & ((1 << 25) - 1);
    PC++;
    break;
  default:
    exception(EXCEPTION_INVALID_INSTRUCTION);
  }
}

int is_LE(){
  union{struct{uint8_t a;uint8_t c;uint8_t b;uint8_t d;} _8; uint32_t _32;} x;
  x._32 = 0x1234567;
  return x._8.a == 0x67;
}

int main(int argc, char **argv) {
  int i;
  FILE *fcodex;
  uint32_t size;
  /* check file size */
  fcodex = fopen(argv[1], "r");
  fseek(fcodex,0,SEEK_END);
  size = ftell(fcodex);
  fseek(fcodex,0,SEEK_SET);
  /* alloc first heap chunk */
  memory = calloc(sizeof(struct array),ALLOC_CHUNK);
  nb_arrays = ALLOC_CHUNK;
  if(memory == NULL)
    exception(EXCEPTION_CALLOC);
  /* init first array for codex */
  memory[0].block = malloc(size);
  memory[0].size = size/4;
  memory[0].present = TRUE;
  /* blit codex */
  fread(memory[0].block, 1, size, fcodex);
  fclose(fcodex);
  if(is_LE())
    for(i = 0;i < size/4;i++){
      uint32_t tmp = memory[0].block[i];
      memory[0].block[i] = (tmp & 0xFF) << 24;
      memory[0].block[i] |= (tmp & 0xFF00) << 8;
      memory[0].block[i] |= (tmp & 0xFF0000) >> 8;
      memory[0].block[i] |= (tmp & 0xFF000000) >> 24;
    }
  /* start program */
  while(1) {
    if(PC < 0 || PC >= memory[0].size)
      exception(EXCEPTION_OUT_OF_PROGRAM);
    interprete(memory[0].block[PC]);
  }
}
