#!/usr/bin/env python3

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 (ISS) for the minimal cpu, 
# program file plain text, single step through program, view variables etc.
# the focus is for students to see / get a feel for what kind of operations
# are performed in low level harware i.e. a first sets away from python :)

#############
# VERSION 1 #
#############

# ADD X Y       ADD X 0
# AND X Y       AND X 0
# JPZ <LABEL>

#############
# VERSION 2 #
#############

# ADD X Y       ADD X 0
# AND X Y       AND X 0
# XOR X Y       XOR X 0
# JPZ <LABEL>

#############
# VERSION 3 #
#############

# ADD X Y       ADD X 0
# AND X Y       AND X 0
# OR  X Y       OR  X 0 
# XOR X Y       XOR X 0
# JPZ <LABEL>

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

run = False
keys = True

#############
# 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 display_variables( variables ):
  line = "    : "
  count = 0
  for v in variables:
    line = line + str(v) + ":" + str(variables[v]) + " "
    count = count + 1
    if count == 14 or count == 26:
      print(line)
      line = "    : "

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


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

def minimal_cpu_simulator(argv):
  global run
  global keys

  signal.signal( signal.SIGINT, signal_handler )

  if len(sys.argv) <= 1:
    print ("Usage: minimal_cpu_simulator.py -i <input_file.asm>")
    print ("                                -v <cpu_version>") 
    print ("                                -b <label>") 
    print ("                                -s <varible:value>") 
    print ("                                -d <0/1/2>") 
    print ("                                -k <0/1>") 
    return
  
  version = 3
  debug = False
  debug_level = 0
  breakpoint = False
  input_file_present = False

  line_number = 0
  max_line_number = 0
  previous_line_number = 0
  jump_line_number = 0
  breakpoint_label = []

  s_config = 'i:v:d:b:s:k:'
  l_config = ['input', 'version', 'debug', 'breakpoint', 'set', 'keys']

  instructions = ['and', 'add', 'jpz', 'xor', 'or']

  variables = { 'a':0, 'b':0, 'c':0, 'd':0, 'e':0, 'f':0, 'g':0, 'h':0, 'i':0, 'j':0,
                'k':0, 'l':0, 'm':0, 'n':0, 'o':0, 'p':0, 'q':0, 'r':0, 's':0, 't':0,
                'u':0, 'v':0, 'w':0, 'x':0, 'y':0, 'z':0 }

  source = []
  source_filename = ""
  code = []
  labels = {}  

  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 ".asm" in arg:
        source_filename = arg
      else:
        source_filename = arg + ".asm"

      if os.path.isfile(source_filename):
        input_file_present = True
        
    elif opt in ('-v', '--version'):
      version = int(arg)
      
    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_label.append(arg)
      
    elif opt in ('-s', '--set'):
      variable= arg.split(":")[0]
      data= arg.split(":")[1]
      variables[variable] = int(data)

    elif opt in ('-k', '--keys'):
      if int(arg) == 0:
        keys = False
      else:
        keys = True

  if debug:	  
    print ("read input parameter : OK")
    if debug_level == 2:
      print( str(source_filename) + " " + str(version) + " " + 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("")

  # seperate labels and instructions #
  # -------------------------------- #

  for line in source:	
    line = re.sub(r'#', '# ', line.lower()) 
    line = re.sub(r'\s+', ' ', line)

    if line == '': 
      break
		
    if len(line) > 1 and line[0] == ' ':
      line = line[1:]
		  
    if line[0] =='#' or line[0] ==' ':
      continue

    if ":" in line:
      if "#" in line:
        print("Error: can not have comments on the same line as labels")
        print(line)
        sys.exit(1)

      key = re.sub(r":.*$", '', line).strip()
      if key in labels:
        print("Error: duplicate labels")
        print(key)
        sys.exit(1)
      else:
        labels[key] = line_number
    else:
      words = re.sub(r'#.*$', '', line).strip().split(' ')
      if words[0] in instructions:	
        if len(words) == 3:
          code.append( (words[0], words[1], words[2]) )
        elif len(words) == 2:
          code.append( (words[0], words[1]) )
        else:
          print("Error: invalid instruction: " + str(words)) 
          sys.exit(1)
        line_number = line_number + 1
      else:
        print("Error: invalid instruction: " + str(words)) 
        sys.exit(1)

  if debug > 0:	  
    print ("code processed : OK")
    print ("instructions : " + str(line_number))
    if debug_level == 2:
      print( code )
      print( labels )
    print("")

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

  max_line_number = line_number
  line_number = 0
  
  zero_flag = False

  print("Press CTRL-C to stop simulation run")

  while True:
    instruction = code[line_number]

    opcode = instruction[0]
    operand_0 = instruction[1]

    absolute = False
    immediate = False
    direct = False
    
    jump_taken = False

    # check breakpoint #
    # ---------------- #

    if breakpoint:
      for name in breakpoint_label:
        if name in labels:
          if line_number == labels[name]:
            print("Breakpoint: " + str(line_number) + " " + str(name))
            display_variables( variables )
            run = False
        else:
          print("Error: breakpoint does not exist")
          print(name + " " + str(breakpoint_label) + " " + str(labels) )
          return          

    # decode instruction #
    # ------------------ #

    if len(instruction) == 3:
      operand_1 = instruction[2]
      if operand_1.isalpha():
        absolute = True
      else:
        immediate = True

    # AND #
    # --- #

    if opcode == "and":
      if immediate: 
        result = (variables[operand_0] & 255) & (int(operand_1) & 255)
        variables[operand_0] = result
      else:
        result = (variables[operand_0] & 255) & (variables[operand_1] & 255)
        variables[operand_0] = result

    # ADD #
    # --- #

    elif opcode == "add":
      if immediate: 
        result = ((variables[operand_0] & 255) + (int(operand_1) & 255)) & 255
        variables[operand_0] = result
      else:
        result = ((variables[operand_0] & 255) + (variables[operand_1] & 255)) & 255
        variables[operand_0] = result
        
    # XOR #
    # --- #

    elif opcode == "xor" and version in range(2,4):
      if immediate: 
        result = (variables[operand_0] & 255) ^ (int(operand_1) & 255)
        variables[operand_0] = result
      else:
        result = (variables[operand_0] & 255) ^ (variables[operand_1] & 255)
        variables[operand_0] = result

    # OR #
    # -- #

    elif opcode == "or" and version in range(3,4):
      if immediate: 
        result = (variables[operand_0] & 255) | (int(operand_1) & 255)
        variables[operand_0] = result
      else:
        result = (variables[operand_0] & 255) | (variables[operand_1] & 255)
        variables[operand_0] = result

    # JPZ #
    # --- #

    elif opcode == "jpz":
      direct = True 
      if zero_flag:
        jump_line_number = labels[operand_0]
        jump_taken = True
      else:
        jump_taken = False

    else:
      print("Error: invalid instruction: " + str(instruction)) 
      print("       check cpu version, check code, cross fingers :)")
      sys.exit(1)

    # update zero flag #
    # ---------------- #

    if result == 0:
      zero_flag = True
    else:
      zero_flag = False

    # update line counter #
    # ------------------- #

    previous_line_number = line_number
    if direct:
      print (pad(line_number) + " : " + opcode + " " + operand_0 + " : " + str(jump_line_number) + ", zf = " + str(zero_flag) + ", taken = " + str(jump_taken) )
    else:
      print (pad(line_number) + " : " + opcode + " " + operand_0 + " " + str(operand_1) + " : " + operand_0 + " = " + str(result) + ", zf = " + str(zero_flag))
      
    if jump_taken:
      line_number = jump_line_number
    else:
      line_number = line_number + 1

    if line_number == max_line_number or line_number == previous_line_number:
      print("")
      display_variables( variables )
      break

    if not run: 
      if keys:
        print("    : r=run, s=step, v=read, w=write, q=quit")

      while True:
        key = get_key()
        if key == 'q':
          print("")
          display_variables( variables )
          return
        elif key == 'r':
          run = True
          break
        elif key == 's':
          break
        elif key == 'v':
          display_variables( variables )
        elif key == 'w':
          variable = input("    : Enter variable - ")
          value = input("    : Enter value - ")

          if variable in variables:
            if int(value) > 0 and int(value) < 256:
              variables[variable] = int(value)
            else:
              print("    : Error: invalid data value, check range ")
          else:
            print("    : Error: can not find variable ")
    else:
      time.sleep(0.25)
   

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





