#!/usr/bin/python
import getopt
import sys
import re

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

# INSTR  OPCODE  OPERAND0   OPERAND1
# JPZ  : 000     00000      XXXXXXXX
# ADD  : 000     XXXXX      XXXXX000
# ADDI : 001     XXXXX      XXXXXXXX
# AND  : 010     XXXXX      XXXXX000
# ANDI : 011     XXXXX      XXXXXXXX
# XOR  : 100     XXXXX      XXXXX000
# XORI : 101     XXXXX      XXXXXXXX
# OR   : 110     XXXXX      XXXXX000
# ORI  : 111     XXXXX      XXXXXXXX

# VARIABLE CODES
# A = 00001
# B = 00010
# C = 00011
# D = 00100
# E = 00101
# F = 00110
# G = 00111
# H = 01000
# I = 01001
# J = 01010
# K = 01011
# L = 01100
# M = 01101
# N = 01110
# O = 01111
# P = 10000
# Q = 10001
# R = 10010
# S = 10011
# T = 10100
# U = 10101
# V = 10110
# W = 10111
# X = 11000
# Y = 11001
# Z = 11010

# INSTR FORMAT 1 : JPZ

# OPCODE  NU     ADDR
# FED     CBA98  76543210 
# 000     00000  XXXXXXXX

# INSTR FORMAT 2 : ADD, AND, XOR, OR

# OPCODE  OPERAND0  OPERAND1   NU
# FED     CBA98     76543      210 
# XXX     XXXXX     XXXXX      000

# INSTR FORMAT 3 : ADDI, ANDI, XORI, ORI

# OPCODE  OPERAND0  IMM 
# FED     CBA98     76543210  
# XXX     XXXXX     XXXXXXXX

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

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

  if len(sys.argv) <= 1:
    print ("Usage: minimalCPU_as.py -i <input_file.asm>")
    print ("                        -o <output_file>") 
    return

  # init variables #
  version = '3.0'

  tmp_filename = 'tmp.asm'
  source_filename = 'default.asm' 
  data_filename = 'default.dat' 
  ram_filename = 'default.ram' 
  mon_filename = 'default.mon'

  address = 0

  s_config = 'i:o:'
  l_config = ['input', 'output']

  input_file_present = False

  instruction_address = 0
  label_dictionary = {}
  instr_names = ['add', 'and', 'xor', 'or', 'jpz']

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

  # extract options #
  for opt, arg in options:
    if opt in ('-o', '--output'):
      data_filename = arg + ".dat"
      ram_filename = arg + ".ram"
      mon_filename = arg + ".mon"
    elif opt in ('-i', '--input'):
      input_file_present = True
      if ".asm" in arg:
        source_filename = arg
      else:
        source_filename = arg + ".asm"
	  
  # exit if no input file present # 
  if input_file_present:

    # open files #
    try:
      print("Opening: " + source_filename )
      source_file = open(source_filename, "r")
    except IOError: 
      print("Error: Input file does not exist.")
      sys.exit(1)

    try:
      tmp_file = open(tmp_filename, "w")
      data_file = open(data_filename, "w")
      ram_file = open(ram_filename, "w")
      mon_file = open(mon_filename, "w")
    except IOError: 
      print("Error: Could not open output files")
      sys.exit(1)

    # scan through code, count instruction, check opcodes
    # and identify labels and assign addresses.

    instruction_address = address
    
    while True:
      line = source_file.readline() 
      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:
        key = re.sub(r':.$', '', line)
        if key in label_dictionary:
          print("Error: duplicate labels")
          print(key)
          sys.exit(1)
        else:
          label_dictionary[key] = instruction_address
      else:
        words = line.split(' ')
        if words[0] in instr_names:
          instruction_address += 1 		
        else:
          print("Error: invalid instruction -") 
          print(words)
          sys.exit(1)

    # replace lables with addresses, write code to tmp_file
	  
    source_file.seek(0) 
    instruction_address = address

    while True:
      line = source_file.readline() 
      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)
        else:
          continue

      words = line.split(' ')	
      outputString = str.format('{:03}', instruction_address) + " "

      for i in range(0, len(words)):
        if words[i] in label_dictionary:
          key = words[i]
          outputString = outputString + " " + str(label_dictionary[key])
        else:
          if words[i] != '':
            outputString = outputString + " " + words[i]

      outputString = outputString + "\n"
      tmp_file.write( outputString )			
      instruction_address += 1 	
  
    source_file.close() 
    tmp_file.close()  

    # open TMP file #

    try:
      tmp_file = open(tmp_filename, "r")
    except IOError: 
      print("Error: could not output temp file")
      sys.exit(1)

    # vhdl ram text

    ram_file.write( "  signal ram : ram_type := (\n" )

    # generate machine code

    while True:
      line = tmp_file.readline()
      line = re.sub(r'\s+', ' ', line)

      if line == '':
        break 

      words = line.split(' ')
      instr = 0

      # match opcode 

      if words[0].isdigit():
        # ADD X Y, ADD X 1          
        if words[1] == "add":
          if words[2].isalpha() and words[3].isalpha():
            instr = int('0000000000000000', 2)   
            instr = instr | ((ord(words[2]) - 96) << 8)
            instr = instr | ((ord(words[3]) - 96) << 3)
          elif words[2].isalpha() and words[3].isdigit():
            instr = int('0010000000000000', 2) 
            instr = instr | ((ord(words[2]) - 96) << 8)
            instr = instr | (int(words[3]) & 0xFF) 
          else:
            print("Error: invalid operand") 
            print(words) 
            sys.exit(1)
    
        # AND X Y, AND X 1   
        elif words[1] == "and":
          if words[2].isalpha() and words[3].isalpha():
            instr = int('0100000000000000', 2)   
            instr = instr | ((ord(words[2]) - 96) << 8)
            instr = instr | ((ord(words[3]) - 96) << 3)
          elif words[2].isalpha() and words[3].isdigit():
            instr = int('0110000000000000', 2) 
            instr = instr | ((ord(words[2]) - 96) << 8)
            instr = instr | (int(words[3]) & 0xFF) 
          else:
            print("Error: invalid operand") 
            print(words) 
            sys.exit(1)

        # XOR X Y, XOR X 1  
        elif words[1] == "xor":
          if words[2].isalpha() and words[3].isalpha():
            instr = int('1000000000000000', 2)   
            instr = instr | ((ord(words[2]) - 96) << 8)
            instr = instr | ((ord(words[3]) - 96) << 3)
          elif words[2].isalpha() and words[3].isdigit():
            instr = int('1010000000000000', 2) 
            instr = instr | ((ord(words[2]) - 96) << 8)
            instr = instr | (int(words[3]) & 0xFF) 
          else:
            print("Error: invalid operand") 
            print(words) 
            sys.exit(1)

        # OR X Y, OR X 1  
        elif words[1] == "or":
          if words[2].isalpha() and words[3].isalpha():
            instr = int('1100000000000000', 2)   
            instr = instr | ((ord(words[2]) - 96) << 8)
            instr = instr | ((ord(words[3]) - 96) << 3)
          elif words[2].isalpha() and words[3].isdigit():
            instr = int('1110000000000000', 2) 
            instr = instr | ((ord(words[2]) - 96) << 8)
            instr = instr | (int(words[3]) & 0xFF) 
          else:
            print("Error: invalid operand") 
            print(words) 
            sys.exit(1)

        # JPZ
        elif words[1] == "jpz":
          if words[2].isdigit():
            instr = int('0000000000000000', 2)   
            instr = instr | (int(words[2]) & 0xFF) 
          else:
            print("Error: invalid operand") 
            print(words) 
            sys.exit(1)
        else:
          print("Error: invalid opcode") 
          print(words) 
          sys.exit(1)

        print( str.format('{:016b}', instr) )

        instruction_address = int(words[0])
        data_file.write(str.format('{:04}', instruction_address) + ' ')
        bin_value = str.format('{:016b}', instr) 
        data_file.write( bin_value )
        data_file.write("\n")

        ram_file.write(f'    {instruction_address:<6d} => "{bin_value}",\n')
        mon_file.write(f"{instruction_address:04X}:{instr:04X}\n")

    # close files #
    tmp_file.close()
    data_file.close()
    mon_file.close()

    # finish vhdl ram text
    ram_file.write("    others => (others => '0')\n")
    ram_file.write("  );\n")
    ram_file.close()
   
    print("closed")

  else:
    print("Error: Input file not specified")
    sys.exit(1) 

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