package swen90006.machine;

import java.util.Arrays;
import java.util.List;

public class Machine
{
  /** arithmetic instructions each take three registers as arguments with the
   *  destination register appearing first
   */

  /** add rd rs1 rs2   =~   rd = rs1 + rs2 */
  public static final String INSTRUCTION_ADD = "add";

  /** sub rd rs1 rs2   =~   rd = rs1 - rs2 */
  public static final String INSTRUCTION_SUBTRACT = "sub";

  /** mul rd rs1 rs2   =~   rd = rs1 * rs2 */
  public static final String INSTRUCTION_MULT = "mul";

  /** div rd rs1 rs2   =~   rd = rs1 / rs2 */
  public static final String INSTRUCTION_DIVIDE = "div";

  /** ret rs           =~   return rs */
  public static final String INSTRUCTION_RETURN = "ret";

  /** ldr rd rs offs  =~    rd = rs[offs] */
  public static final String INSTRUCTION_LOAD = "ldr";

  /** str ra offs rb   =~    ra[offs] = rb */
  public static final String INSTRUCTION_STORE = "str";

  /** mov rd val       =~    rd = val */
  public static final String INSTRUCTION_MOVE = "mov";

  /** jmp offs         =~    pc = pc + offs */
  public static final String INSTRUCTION_JUMP = "jmp";

  /** jz ra offs       =~   if (ra == 0) pc = pc + offs else pc = pc + 1 */
  public static final String INSTRUCTION_JZ = "jz";

  public static final int NUM_REGS = 32;
  public static final int MAX_REG = (NUM_REGS - 1);
  public static final int MEMORY_SIZE = 65536; /* 4 x as much memory as a 64 */
  public static final int MAX_ADDR = MEMORY_SIZE-1;

  private int[] memory;
  private int[] regs;

  private int count = 0; /* counts number of instructions executed so far */

  public Machine()
  {
    memory = new int[MEMORY_SIZE];
    regs = new int[NUM_REGS];
    count = 0;
  }

  private void do_add(int dest, int src1, int src2)
  {
    regs[dest] = regs[src1] + regs[src2];
  }

  private void do_sub(int dest, int src1, int src2)
  {
    regs[dest] = regs[src1] - regs[src2];
  }

  private void do_mult(int dest, int src1, int src2)
  {
    regs[dest] = regs[src1] * regs[src2];
  }

  private void do_div(int dest, int src1, int src2)
  {
    if (regs[src2] == 0){
      /* no op */
    }else{
      regs[dest] = regs[src1] / regs[src2];
    }
  }

  private void do_load(int dest, int src, int offs) {
    if (regs[src] + offs > MAX_ADDR){
      /* no op */
    }else if(regs[src] + offs < 0){
      /* no op */
    }else{
      regs[dest] = memory[regs[src] + offs];
    }
  }

  private void do_store(int a, int offs, int b)  {
    if (regs[a] + offs > MAX_ADDR){
      /* no op */
    }else if(regs[a] + offs < 0){
      /* no op */
    }else{
      memory[regs[a] + offs] = regs[b];
    }
  }

  private void do_move(int rd, int val){
    regs[rd] = val;
  }

  private int parseReg(String s) throws InvalidInstructionException
  {
    if (s.length() < 2){
      throw new InvalidInstructionException();
    }
    if (s.charAt(0) != 'r'){
      throw new InvalidInstructionException();
    }
    String numstr = s.substring(1);
    int num = 0;
    try {
      num = Integer.parseInt(numstr);
    } catch (Exception e){
      throw new InvalidInstructionException();
    }
    validate_reg(num);
    return num;
  }

  private int parseOffset(String s)
    throws InvalidInstructionException
  {
    int num = 0;
    try {
      num = Integer.parseInt(s);
    } catch (Exception e){
      throw new InvalidInstructionException();
    }
    validate_offset(num);
    return num;
  }


  private void validate_reg(int reg)
    throws InvalidInstructionException
  {
    if (reg < 0 || reg > MAX_REG) {
      throw new InvalidInstructionException();
    }
  }

  private void validate_offset(int offset)
    throws InvalidInstructionException
  {
    if (offset < -MAX_ADDR || offset > MAX_ADDR) {
      throw new InvalidInstructionException();
    }
  }

  /** Execute an assembly program.
   *
   * @param prog is the program to execute as an iterable collection of strings,
   *        each of which is a single instruction.
   * @return the program's return value.
   * @throws Exception when program has unrecognised or
   *         invalid instructions, or when it returns no result when it finishes
   */
  int execute(List<String> instructions)
    throws InvalidInstructionException,
	   NoReturnValueException
  {
    int instructionsExecuted = 0;
    int pc = 0;
    final int progLength = instructions.size();
    while(true){
      if (pc < 0 || pc >= progLength){
	/* will cause NoReturnValueException to be thrown
	 * but that is not a bug and and indeed is what the
	 * VM is supposed to do if the pc becomes negative,
	 * since in this case the program's execution
	 * finishes early without a return value having
	 * been produced. */
	break;
      }
      String inst = instructions.get(pc);
      /* strip leading and trailing whitespace */
      inst = inst.toLowerCase().replaceAll("^\\s+","").replaceAll("\\s+$","");
      /* strip out any comments */
      String[] toks = inst.split(";");
      inst = toks[0];

      /* check for blank lines */
      if (inst.equals("")){
      	pc = pc + 1;
      	count++;
      	continue;
      }

      instructionsExecuted++;
      /* now  tokenize by splitting on whitespace */
      toks = inst.split("\\s+");

      /* check minimum number of tokens */
      if (toks.length < 2){
	       throw new InvalidInstructionException();
      }

      if (toks[0].equals(INSTRUCTION_ADD)){
      	if (toks.length != 4){
      	  throw new InvalidInstructionException();
      	}
      	int rd = parseReg(toks[1]);
      	int rs1 = parseReg(toks[2]);
      	int rs2 = parseReg(toks[3]);
      	do_add(rd,rs1,rs2);
      }
  else if (toks[0].equals(INSTRUCTION_SUBTRACT)){
	   if (toks.length != 4){
	      throw new InvalidInstructionException();
	   }
	    int rd = parseReg(toks[1]);
	    int rs1 = parseReg(toks[2]);
	    int rs2 = parseReg(toks[3]);
	     do_sub(rd,rs1,rs2);
  }
  else if (toks[0].equals(INSTRUCTION_MULT)){
	   if (toks.length != 4){
	      throw new InvalidInstructionException();
	   }
	    int rd = parseReg(toks[1]);
	    int rs1 = parseReg(toks[2]);
	    int rs2 = parseReg(toks[3]);
	    do_mult(rd,rs1,rs2);
  }
  else if (toks[0].equals(INSTRUCTION_DIVIDE)){
	   if (toks.length != 4){
	      throw new InvalidInstructionException();
	    }
	    int rd = parseReg(toks[1]);
	    int rs1 = parseReg(toks[2]);
	    int rs2 = parseReg(toks[3]);
	    do_div(rd,rs1,rs2);
  }
  else if (toks[0].equals(INSTRUCTION_RETURN)){
	   int rs = parseReg(toks[1]);
	   count++;
	   return regs[rs];
    }
  else if (toks[0].equals(INSTRUCTION_LOAD)){
	   if (toks.length != 4){
	      throw new InvalidInstructionException();
	   }
	   int rd = parseReg(toks[1]);
	   int rs = parseReg(toks[2]);
	   int offs = parseOffset(toks[3]);
	   do_load(rd,rs,offs);
  }
  else if (toks[0].equals(INSTRUCTION_STORE)){
	   if (toks.length != 4){
	      throw new InvalidInstructionException();
	    }
	    int ra = parseReg(toks[1]);
	    int offs = parseOffset(toks[2]);
	    int rb = parseReg(toks[3]);
	    do_store(ra,offs,rb);
  }
  else if (toks[0].equals(INSTRUCTION_MOVE)){
	   if (toks.length != 3){
	      throw new InvalidInstructionException();
	   }
	   int rd = parseReg(toks[1]);
	   int offs = parseOffset(toks[2]);
	   do_move(rd,offs);
  }
  else if (toks[0].equals(INSTRUCTION_JUMP)){
	   if (toks.length != 2){
	      throw new InvalidInstructionException();
	   }
	   int offs = parseOffset(toks[1]);
	   pc  = pc + offs;
	   count++;
	   continue; /* avoid default increment of pc below */
  }
  else if (toks[0].equals(INSTRUCTION_JZ)){
	   if (toks.length != 3){
	     throw new InvalidInstructionException();
	   }
	   int ra = parseReg(toks[1]);
	   int offs = parseOffset(toks[2]);
	   if (regs[ra] == 0){
	     pc = pc + offs;
	    }
      else{
	       pc = pc + 1;
	    }
	    count++;
	    continue; /* avoid default increment the pc below */
  }
  else {
	   System.err.println("Unrecognised instruction: " + inst);
	   throw new InvalidInstructionException();
  }
  count++;
  pc = pc + 1;
}

    /* got here without returning already... */
throw new NoReturnValueException();
}

  /**
   * get the number of instructions successfully executed by the VM so far
   */
  public int getCount(){
    return count;
  }
}