#!/usr/bin/python

import getopt
import signal
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

#############
# VARIABLES #
#############

run = False
gpio = True

RAM_MAX = 251

#############
# 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)

def convertData(data):
  try:
    if '0b' in data:
      return int(data, 2)    
    elif '0x' in data:
      return int(data,16) 
    else:
      return int(data) 
  except:
    print("Error: invalid data can not convert")
    print(data) 
    sys.exit(1)

def signal_handler(sig, frame):
  global run
  print('\nYou pressed Ctrl+C')
  run = False

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

def simple_cpu_v1a_simulator(argv):
  global run

  signal.signal( signal.SIGINT, signal_handler )

  if len(sys.argv) <= 1:
    print ("Usage: simple_cpu_v1a_simulator.py -i <input_file.dat>")
    print ("                                   -b <label>") 
    print ("                                   -d <debug level>") 
    return
  
  version = 2

  s_config = 'i:d:b:'
  l_config = ['input', 'debug', 'breakpoint']

  source = []
  source_filename = ""

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

  gpio_rd = []
  for i in range(0,4):
    gpio_rd.append("1111111111111111")

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

  debug = False
  debug_level = 0

  input_file_present = False
  number_of_lines = 0

  breakpoint = False
  breakpoint_address = []

  jump = False
  jump_address = 0

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

  prev_pc = 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" }
 
  # Note, jumpc is implemented on some versions but not all

  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.append(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 #
  # ----------- #


  if gpio:
    print("MEMORY MAP")
    print("----------")
    print("   ADDR           READ                WRITE     ")
    print("0xFF (255)   Port B DATA OUT    Port B DATA OUT ")
    print("0xFE (254)   Port B DATA IN     Port B DATA OUT ")
    print("0xFD (253)   Port A DATA OUT    Port A DATA OUT ")
    print("0xFC (252)   Port A DATA IN     Port A DATA OUT ")
    print("0xFB (251)   RAM                RAM             ")
    print("...  (...)   ...                ...             ")
    print("0x00 (000)   RAM                RAM             ")
    print("")
  
  print("WARNING : this simulator is not guaranteed to be functionally accurate when compared to the HW")
  print("Press CTRL-C to stop simulation run")

  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 > 0:
      print( str(instruction) + " " + str(opcode) )
        
    # MOVE #
    # ---- #

    if opcode == "0000": 
      acc = imm & 255
      z = int(acc == 0)  # in the simpleCPUv1a any instruction that updates ACC updates Z
      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 < 256:
        if gpio and absolute > RAM_MAX:
          addr = absolute & 3
          if (addr == 3) or (addr == 1):
            acc = int( gpio_wr[addr], 2 ) & 255
          else:
            acc = int( gpio_rd[addr], 2 ) & 255
        else:
          acc = int( memory[absolute], 2 ) & 255
      else:
        print("Error: load - invalid abs - " + str(absolute)) 
        sys.exit(1)
      z = int(acc == 0)  # in the simpleCPUv1a any instruction that updates ACC updates Z

      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 < 256:
        if acc >= 0 and acc < 256:
          tmp = bin(acc).split('b')[1]
          for i in range(16 - len(tmp)):
            tmp ="0" + tmp

          if gpio and absolute > RAM_MAX:
            addr = absolute & 3
            if addr > 1:
              gpio_wr[3] = tmp
              gpio_wr[2] = tmp
            else: 
              gpio_wr[1] = tmp
              gpio_wr[0] = tmp
          else:
            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 < 256:
        if gpio and absolute > RAM_MAX:
          addr = absolute & 3
          if (addr == 3) or (addr == 1):
            tmp = int( gpio_wr[addr], 2 ) & 255
          else:
            tmp = int( gpio_rd[addr], 2 ) & 255
        else:
          tmp = int( memory[absolute], 2 )
        acc = (acc + tmp) & 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 < 256:
        if gpio and absolute > RAM_MAX:
          addr = absolute & 3
          if (addr == 3) or (addr == 1):
            tmp = int( gpio_wr[addr], 2 ) & 255
          else:
            tmp = int( gpio_rd[addr], 2 ) & 255
        else:
          tmp = int( memory[absolute], 2 )

        acc = (acc - tmp) & 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 < 256:
        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 < 256:
        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 < 256:
        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)) 
      sys.exit(1)
   
    # update line counter #
    # ------------------- #

    print( disassembled_instruction )

    prev_pc = pc
    if jump:
      pc = jump_address
    else:
      pc = pc + 1

    if not run: 
      if gpio:
        print("    : r=run, s=step, v=registers, x=read, w=write, i=input, o=output, q=quit")
      else:
        print("    : r=run, s=step, v=registers, x=read, w=write, q=quit")

      while True:
        key = get_key()
        if key == 'q' or prev_pc == pc:
          print( "\nacc:" + str(acc) + " z:" + str(z) )

          for row in range(0, 256, 16):
            block = memory[row:row + 16]
            if all(val == "0000000000000000" for val in block):
              continue

            line = f"{row:02x}:"  # Row label
            for offset in range(16):
              addr = row + offset
              data = int(memory[addr], 2)
              line += f" {data:04x}"
            print(line)
          return
        elif key == 'r':
          run = True
          break
        elif key == 's':
          break
        elif key == 'v':
          print( "    : acc:" + str(acc) + " z:" + str(z) )
        elif key == 'x':
          address = input("    : Enter address - ")
          if address:
            addr = convertData( address )
            if addr >=0 and addr < 256:
              if gpio and addr > RAM_MAX:
                addr = addr & 3
                if (addr == 3) or (addr == 1):
                  tmp = gpio_wr[addr]
                else:
                  tmp = gpio_rd[addr]
              else:
                tmp = memory[addr]
              print( "    : " + str(tmp) + " - " + str(int(tmp, 2)) )
            else:
              print( "    : Invalid address" )
        elif key == 'w':
          address = input("    : Enter address - ")
          data = input("    : Enter data - ")
          if address:
            addr = convertData( address )
            if addr >=0 and addr < 256:
              if data:
                value = convertData( data )
                if value >= 0 and value < 256:
                  tmp = bin(value).split('b')[1]
                  for i in range(16 - len(tmp)):
                    tmp ="0" + tmp

                  if gpio and addr > RAM_MAX:
                    addr = addr & 3
                    if addr > 1:
                      gpio_wr[3] = tmp
                      gpio_wr[2] = tmp
                    else: 
                      gpio_wr[1] = tmp
                      gpio_wr[0] = tmp
                  else:
                    memory[addr] = tmp
        elif key == 'i':
          if gpio:
            data = input("    : Enter input A - ")
            if data:
              value = convertData( data )
              if value >= 0 and value < 256:
                tmp = bin(value).split('b')[1]
                for i in range(16 - len(tmp)):
                  tmp ="0" + tmp
                gpio_rd[0] = tmp
            data = input("    : Enter input B - ")
            if data:
              value = convertData( data )
              if value >= 0 and value < 256:
                tmp = bin(value).split('b')[1]
                for i in range(16 - len(tmp)):
                  tmp ="0" + tmp
                gpio_rd[2] = tmp
        elif key == 'o':
          if gpio:
            tmp = gpio_wr[1]
            print( "    : output A - " + str(tmp) + " - " + str(int(tmp, 2)) )
            tmp = gpio_wr[3]
            print( "    : output B - " + str(tmp) + " - " + str(int(tmp, 2)) )

    else:
      time.sleep(0.25)
      if breakpoint:
        for addr in breakpoint_address:
          if pc == int(addr):
            run = False
      if prev_pc == pc:
        run = False

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




