#include "microcode.h"
#include <stdlib.h>
#include <stdio.h>
#include "linkedList.h"
#include <tcl.h>
#include <string.h>
#include "tcl.h"
#include <math.h>

MC_INSTR_UP_DOWN
MC_INSTR_MEM_OP
MC_INSTR_JUMP_COND
MC_INSTR_DEF

#define SET_DIR                                 \
do {                                            \
  switch(currentElement->Direction){            \
    case DIR_DOWN:                              \
      instructions[opIdx].col_up_down = c_DOWN; \
      instructions[opIdx].row_up_down = c_DOWN; \
      break;                                    \
    case DIR_UP:                                \
    case DIR_NONE:                              \
    default:                                    \
      instructions[opIdx].col_up_down = c_UP;   \
      instructions[opIdx].row_up_down = c_UP;   \
      break;                                    \
  }                                             \
} while(0)

t_MC_INSTR* Microcode_Compile(t_Microcode* microcode, int *length){

  //Step 1. Get aproximate size of resulting microcode as number of operations*2
  int opCnt = 1;
  LIST_FOREACH(Element, microcode->MarchElements, {
    LIST_FOREACH(Operation, currentElement->Operations, {
      opCnt++;
    });
  });

  //Step 2. Allocate memory for microcode
  t_MC_INSTR *instructions = malloc(sizeof(t_MC_INSTR)*opCnt*5);
  memset(instructions, 0, sizeof(t_MC_INSTR)*opCnt*5);

  //Step 4. Assemble microcode
  int opIdx = 0;
  LIST_FOREACH_BEGIN(Element, microcode->MarchElements)
    SET_DIR;
    instructions[opIdx].row_clr = instructions[opIdx].col_clr = 1;
    instructions[opIdx].next_adr = opIdx + 1;
    opIdx++;

    int loopStart = opIdx;

    LIST_FOREACH_BEGIN(Operation, currentElement->Operations)
      t_MC_INSTR *newItem = &instructions[opIdx];     
 
      //Set operation type
      switch(currentOperation->Type){
        case OP_READ_CHK: 
        case OP_READ:  newItem->mem_op = c_RD; break;
        case OP_WRITE_CHK:
        case OP_WRITE: newItem->mem_op = c_WR; break;
        case OP_SMARCH_CHK:
        case OP_SMARCH: newItem->mem_op = c_SMARCH;  break;
        case OP_NOP:
        default:
          newItem->mem_op = c_NOP; break;
      }

      //Set checkerboard
      switch(currentOperation->Type){
        case OP_READ_CHK: 
        case OP_WRITE_CHK:
        case OP_SMARCH_CHK:
          newItem->chkbrd_col = newItem->chkbrd_row = 1; break;
        case OP_READ:
        case OP_WRITE:
          newItem->chkbrd_col = newItem->chkbrd_row = 0; break;
        case OP_SMARCH:
        default:
          newItem->chkbrd_col = newItem->chkbrd_row = 0; break;
      }

      newItem->mem_data = currentOperation->TestVector;
      newItem->jump_cond = c_NO_COND;
      newItem->next_adr = opIdx + 1;
      SET_DIR;

      opIdx++;
    LIST_FOREACH_END(Operation);
  
    if(currentElement->Direction != DIR_SINGLE) {
   
      int loopEnd = opIdx-1;
     
      //Inner loop
      switch(currentElement->AddressAccess){
        case ACCESS_COL:
          instructions[loopEnd].col_cnte = true;
          instructions[loopEnd].jump_cond = c_COL_END;
          break;
        case ACCESS_ROW:
        case ACCESS_NORMAL:
        default:
          instructions[loopEnd].row_cnte = true;
          instructions[loopEnd].jump_cond = c_ROW_END;
          break;
      }
      instructions[loopEnd].next_adr = loopStart;
      instructions[loopEnd].jump_adr = opIdx;
  
      //Outer loop
      instructions[opIdx].mem_op = c_NOP;
      switch(currentElement->AddressAccess){
        case ACCESS_COL:
          instructions[opIdx].row_cnte = true;
          instructions[opIdx].col_clr  = true;
          instructions[opIdx].jump_cond = c_ROW_END;
          break;
        case ACCESS_ROW:
        case ACCESS_NORMAL:
        default:
          instructions[opIdx].col_cnte = true;
          instructions[opIdx].row_clr  = true;
          instructions[opIdx].jump_cond = c_COL_END;
          break;
      }
      SET_DIR;
      instructions[opIdx].next_adr = loopStart;
      instructions[opIdx].jump_adr = opIdx+1;
  
      opIdx++;
    }
  //});
  LIST_FOREACH_END(Element);
  
  //How width will microcode address bus be ?
  double width = ceil(log10(opIdx)/log10(2));

  instructions[opIdx-1].jump_adr = (int)pow(2, width)-1;

  *length = opIdx-1;
  
  return instructions;
}

static int TclMicrocode_ObjectCommand(ClientData cd, Tcl_Interp *interp, int objc, Tcl_Obj * CONST *objv){
  t_Microcode *mc = (t_Microcode*) cd;

  if (objc < 2) {
    Tcl_WrongNumArgs(interp, 1, objv, "usage: [object] [export | compile]");
    return TCL_ERROR;
  }

  const char* cmd = Tcl_GetStringFromObj(objv[1], NULL);

  if(!strcmp(cmd, "export")) {
    Tcl_SetStringResult(interp, Microcode_Export(mc));
  }
  else if(!strcmp(cmd, "compile")) {
    int codeLength = 0;
    t_MC_INSTR *compiled = Microcode_Compile(mc, &codeLength);

    Tcl_Obj *tcl_compiled = Tcl_NewListObj(codeLength+1, NULL);

    for(int i = 0; i <= codeLength; i++){
      Tcl_Obj *tcl_instruction = Tcl_NewDictObj();
      if(Struct2Tcl_MC_INSTR(interp, &compiled[i], tcl_instruction) != TCL_OK)
        return TCL_ERROR;
      TCL_CATCH(Tcl_ListObjAppendElement(interp, tcl_compiled, tcl_instruction), "Can't add instruction to TCL object");
    }

    Tcl_SetObjResult(interp, tcl_compiled);
  }
  else {
    Tcl_SetStringResult(interp, "Unknown command ", cmd);
  }

  return TCL_OK;  
}

static void TclMicrocode_ObjectDelete(ClientData cd){
  t_Microcode *mc = (t_Microcode*) cd;
  Microcode_Destroy(mc);
}

static int TclMicrocode(ClientData clientData, Tcl_Interp *interpr, int argc, Tcl_Obj * CONST *objv){
  if(argc != 4){  
    Tcl_WrongNumArgs(interpr, 1, objv, "usage: microcode create [name] [file]");
    return TCL_ERROR;
  }

  if(strcmp(Tcl_GetStringFromObj(objv[1], NULL), "create")){
    Tcl_SetStringResult(interpr, "Unknown subcommand ", Tcl_GetStringFromObj(objv[1], NULL));
    return TCL_ERROR;
  }

  FILE* fp;
  OPEN(fp,Tcl_GetStringFromObj(objv[3],NULL), "r");
  t_Microcode *mc = Microcode_ReadFile(fp);
  fclose(fp);
  Tcl_Command token = Tcl_CreateObjCommand(interpr, Tcl_GetStringFromObj(objv[2], NULL), TclMicrocode_ObjectCommand, (ClientData) mc, TclMicrocode_ObjectDelete);

  if(token ==NULL){
    Tcl_SetStringResult(interpr,"Cant create object");
    return TCL_ERROR;
  }

  Tcl_SetObjResult(interpr, objv[2]);
  return TCL_OK;
}

void Microcode_TclRegister(Tcl_Interp* tcl) {
  if(Tcl_CreateObjCommand(tcl, "microcode",  TclMicrocode, NULL, NULL) == NULL){
    printf("Error creating microcode command\n");
  }
}
