#!/usr/bin/python

import getopt
import sys
import os
import re
import time

if sys.platform =='win32':
  import msvcrt
else:
  import tty
  import termios

###################
# INSTRUCTION-SET #
###################

# a very simple instruction set simulator for the simpleCPUv1a, 
# input file is a .dat file generated by the assembler i.e. 
# AAAA DDDDDDDDDDDDDDD, a deciaml address (A) and a 16bit binary (D)
# machine code instruction.

# INSTR   IR15 IR14 IR13 IR12 IR11 IR10 IR09 IR08 IR07 IR06 IR05 IR04 IR03 IR02 IR01 IR00
# MOVE    0    0    0    0    X    X    X    X    K    K    K    K    K    K    K    K
# ADD     0    0    0    1    X    X    X    X    K    K    K    K    K    K    K    K
# SUB     0    0    1    0    X    X    X    X    K    K    K    K    K    K    K    K
# AND     0    0    1    1    X    X    X    X    K    K    K    K    K    K    K    K

# LOAD    0    1    0    0    X    X    X    X    A    A    A    A    A    A    A    A
# STORE   0    1    0    1    X    X    X    X    A    A    A    A    A    A    A    A
# ADDM    0    1    1    0    X    X    X    X    A    A    A    A    A    A    A    A
# SUBM    0    1    1    1    X    X    X    X    A    A    A    A    A    A    A    A

# JUMPU   1    0    0    0    X    X    X    X    A    A    A    A    A    A    A    A
# JUMPZ   1    0    0    1    X    X    X    X    A    A    A    A    A    A    A    A
# JUMPNZ  1    0    1    0    X    X    X    X    A    A    A    A    A    A    A    A
# JUMPC   1    0    1    1    X    X    X    X    A    A    A    A    A    A    A    A

# .data  IMM

#############
# FUNCTIONS #
#############

def get_key():
  if sys.platform == 'win32':
    return msvcrt.getch().decode('utf-8')
  else:
    fd = sys.stdin.fileno()
    old_settings = termios.tcgetattr(fd)
    try:
      tty.setraw(sys.stdin.fileno())
      ch = sys.stdin.read(1)
    finally:
      termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
    return ch

def pad(number):
  if number <10:
    return "00" + str(number)
  elif number >9 and number <100:
    return "0" + str(number)
  else:
    return str(number)

################
# MAIN PROGRAM #
################

def simple_cpu_v1a_simulator(argv):

  if len(sys.argv) <= 1:
    print ("Usage: simple_cpu_v1d_simulator.py -i <input_file.dat>")
    print ("                                   -b <label>") 
    print ("                                   -d <debug level>") 
    return
  
  s_config = 'i:d:b:'
  l_config = ['input', 'debug', 'breakpoint']

  source = []
  source_filename = ""

  memory = []
  for i in range(0,256):
    memory.append("0000000000000000")

  debug = False
  debug_level = 0

  input_file_present = False
  run = False

  number_of_lines = 0

  breakpoint = False
  breakpoint_address = 0

  jump = False
  jump_address = 0

  acc = 0
  pc = 0
  z = 0
  c = 0

  stack = [0,0,0,0]
  stack_pointer = 0

  isa = {"0000":"move",
         "0001":"add", 
         "0010":"sub",
         "0011":"and", 
         "0100":"load",
         "0101":"store", 
         "0110":"addm",
         "0111":"subm", 
         "1000":"jumpu",
         "1001":"jumpz", 
         "1010":"jumpnz",
         "1011":"jumpc" }

  try:
    options, remainder = getopt.getopt(sys.argv[1:], s_config, l_config)
  except getopt.GetoptError as m:
    print("Error: invalid arguments -", m)
    sys.exit(1)

  # process arguments #
  # ----------------- #

  for opt, arg in options:
    if opt in ('-i', '--input'):
      if ".dat" in arg:
        source_filename = arg
      else:
        source_filename = arg + ".dat"

      if os.path.isfile(source_filename):
        input_file_present = True
    elif opt in ('-d', '--debug'):
      if int(arg) == 0:
        debug = False
      elif int(arg) == 1:
        debug = True
        debug_level = 1
      else:
        debug = True
        debug_level = 2
    elif opt in ('-b', '--breakpoint'):
      breakpoint = True
      breakpoint_address = int(arg)

  if debug:	  
    print ("read input parameter : OK")
    if debug_level == 2:
      print( str(source_filename) + " " + str(debug) + "\n")

  # read source file #
  # ---------------- #

  if input_file_present:
    try:
      source_file = open(source_filename, "r")
      source = source_file.readlines()
    except IOError: 
      print("Error: could not open source file")
      sys.exit(1) 

  else:
    print("Error: could not find source file")
    sys.exit(1) 

  if debug:	  
    print ("read code : OK")
    if debug_level == 2:
      print ( source )
      print("")

  # read machine code AAAA : DDDDDDDDDDDDDDDD #
  # ----------------------------------------- #

  number_of_lines = 0
  for line in source:
    line = re.sub(r'\s+', ' ', line.replace('\n','').replace('\r','').lower())	
    if line == '': 
      continue
    else:
      try:
        words = line.split(' ')
        memory[int(words[0])] = words[1]
        number_of_lines = number_of_lines + 1
      except IndexError:
        print("Error: invalid address: " + str(words[0])) 
        sys.exit(1)

  if debug:	  
    print ("code processed : OK")
    print ("instructions : " + str(number_of_lines))
    if debug_level == 2:
      for i in range(number_of_lines):
        print( memory[i] )

  # run program #
  # ----------- #

  print("WARNING : this simulator is not guaranteed to be functionally accurate when compared to the HW")

  while True:
    instruction = memory[pc]

    opcode = instruction[0:4]
    imm = int(instruction[8:], 2)
    absolute = int(instruction[4:], 2)

    disassembled_instruction = ""
    jump = False

    if debug:
      print( str(instruction) + " " + str(opcode_high) )
        
    # MOVE #
    # ---- #

    if opcode == "0000": 
      acc = imm & 255
      disassembled_instruction = str(pad(pc)) + ": " + isa[opcode] + " " + str(imm) + " -> acc:" + str(acc)

    # ADD #
    # --- #

    elif opcode == "0001": 
      acc = (acc + imm) & 255
      z = int(acc == 0)
      disassembled_instruction = str(pad(pc)) + ": " + isa[opcode] + " " + str(imm) + " -> acc:" + str(acc) + " z:" + str(z) 

    # SUB #
    # --- #

    elif opcode == "0010": 
      acc = (acc - imm) & 255
      z = int(acc == 0)
      disassembled_instruction = str(pad(pc)) + ": " + isa[opcode] + " " + str(imm) + " -> acc:" + str(acc) + " z:" + str(z) 

    # AND #
    # --- #

    elif opcode == "0011": 
      acc = (acc & imm) & 255
      z = int(acc == 0)
      disassembled_instruction = str(pad(pc)) + ": " + isa[opcode] + " " + str(imm) + " -> acc:" + str(acc) + " z:" + str(z) 

    # LOAD #
    # ---- #

    elif opcode == "0100":
      if absolute <= 255:
        acc = int( memory[absolute], 2 ) & 255
      else:
        print("Error: load - invalid abs: " + str(absolute)) 
        sys.exit(1)

      if debug:	  
        if debug_level == 2:
          print( memory[absolute] )
      disassembled_instruction = str(pad(pc)) + ": " + isa[opcode] + str(absolute) + " -> acc:" + str(acc)  

    # STORE #
    # ----- #

    elif opcode == "0101":
      if absolute <= 255:
        if acc > 0 and acc < 256:
          tmp = bin(acc).split('b')[1]
          for i in range(16 - len(tmp)):
            tmp ="0" + tmp
          memory[absolute] = tmp
        else:
          print("Error: store - invalid acc: " + str(acc)) 
          sys.exit(1)
      else:
        print("Error: store - invalid abs: " + str(absolute)) 
        sys.exit(1)

      if debug:	  
        if debug_level == 2:
          print( memory[absolute] )
      disassembled_instruction = str(pad(pc)) + ": " + isa[opcode] + " " + str(absolute) 

    # ADDM #
    # ---- #

    elif opcode == "0110":
      if absolute <= 255:
        acc = (acc + int( memory[absolute], 2 )) & 255
      else:
        print("Error: addm - invalid abs: " + str(absolute)) 
        sys.exit(1)
      z = int(acc == 0)
      disassembled_instruction = str(pad(pc)) + ": " + isa[opcode] + " " + str(absolute) + " -> acc:" + str(acc) + " z:" + str(z)

    # SUBM #
    # ---- #

    elif opcode == "0111":
      disassembled_instruction = isa[opcode] + " ra " + str(absolute)
      if absolute <= 255:
        acc = (acc - int( memory[absolute], 2 )) & 255
      else:
        print("Error: subm - invalid abs: " + str(absolute)) 
        sys.exit(1)
      z = int(acc == 0)
      disassembled_instruction = str(pad(pc)) + ": " + isa[opcode] + " " + str(absolute) + " -> acc:" + str(acc) + " z:" + str(z)

    # JUMPU #
    # ----- #

    elif opcode == "1000":
      if absolute <= 255:
        jump = True
        jump_address = absolute
      else:
        print("Error: jumpu invalid abs: " + str(absolute)) 
        sys.exit(1)
      disassembled_instruction = str(pad(pc)) + ": " + isa[opcode] + " " + str(absolute) + " -> " + str(jump) 

    # JUMPZ #
    # ----- #

    elif opcode == "1001":
      if absolute <= 255:
        if z == 1:
          jump = True
          jump_address = absolute
      else:
        print("Error: jumpz invalid abs: " + str(absolute)) 
        sys.exit(1)
      disassembled_instruction = str(pad(pc)) + ": " + isa[opcode] + " " + str(absolute) + " -> " + str(jump) + " z:" + str(z) 

    # JUMPNZ #
    # ------ #

    elif opcode == "1010":
      if absolute <= 255:
        if z == 0:
          jump = True
          jump_address = absolute
      else:
        print("Error: jumpnz invalid abs: " + str(absolute)) 
        sys.exit(1)
      disassembled_instruction = str(pad(pc)) + ": " + isa[opcode] + " " + str(absolute) + " -> " + str(jump) + " z:" + str(z) 

    else:
      print("Error: invalid opcode: " + str(opcode_high) + " " + str(opcode_low) ) 
      sys.exit(1)
   
    # update line counter #
    # ------------------- #

    print( disassembled_instruction )
    if jump:
      pc = jump_address
    else:
      pc = pc + 1

    if not run: 
      print("   : r=run, s=step, v=registers, m=memory, q=quit")
      while True:
        key = get_key()
        if key == 'q':
          return
        elif key == 'r':
          run = True
          break
        elif key == 's':
          break
        elif key == 'v':
          print( "   : acc:" + str(acc) + " z:" + str(z) )
        elif key == 'm':
          addr = int(input("   : Enter address "))
          if addr >=0 and addr < 255:
            print( "   : " + str(memory[int(addr)]) + " - " + str( int(memory[int(addr)], 2) ) )
          else:
            print( "   : Invalid address" )
    else:
      time.sleep(0.25)
      if pc == breakpoint_address:
        run = False

   
if __name__ == '__main__':
  simple_cpu_v1a_simulator(sys.argv)


















