from bitstring import Bits #pip install bitstring if you have not already
'''
Library for compiling single instructions
'''

#converts string to int
#string can by binary, decimal or hex
def getNumber(word, allowNeg=True, returnABS=False):
    value = 0
    #check for binary number
    if len(word) > 2 and word[:2] == '0b':
        try:
            value = int(word, 2)
        except ValueError:
            raise ValueError(str(word) + " is not a valid binary number")

    #check for hex number
    elif len(word) > 2 and word[:2] == '0x':
        try:
            value = int(word, 16)
        except ValueError:
            raise ValueError(str(word) + " is not a valid hex number")

    #check for decimal number
    else:
        try:
            value = int(word, 10)
        except ValueError:
            raise ValueError(str(word) + " is not a valid decimal number")

    #check for negative numbers
    if value < 0 and not allowNeg:
        raise ValueError(str(word) + " is a negative number (which are not allowed)")

    if allowNeg and returnABS:

        if value < 0:
            return abs(value), True
        else:
            return value, False
    return value


#converts string to int, representing the register
#string must be in format: rx
#where x is a number between 0 and 15 (including 0 and 15)
def getReg(word):
    value = 0
    
    #check if first char starts with an r
    if word[0].lower() == 'r':
        #check for rbp and rsp (rbp == r14, rsp == r15)
        if word.lower() == "rbp":
            return 14
        if word.lower() == "rsp":
            return 15

        #parse number after r
        try:
            value = int(word[1:], 10)
        except ValueError:
            raise ValueError("Register" + str(word) + " is not a valid register")
    else:
        raise ValueError("Register " + str(word) + " does not start with 'r'")

    if value < 0 or value > 15:
        raise ValueError("Register " + str(word) + " is not a valid register")

    return value

#checks if the given signed value fits in the given number of bits
def CheckFitsInBits(value, bits, signed=True):
    if signed: # signed numbers have one bit less
        bits -= 1
    if not (value.bit_length() <= bits):
        raise ValueError("Value " + str(value) + " does not fit in " + str(bits) + " bits")

"""
------------------LINE COMPILING FUNCTIONS---------------------
"""

#compiles halt instruction
#should have 0 arguments
def compileHalt(line):
    if len(line) != 1:
        raise Exception("Incorrect number of arguments. Expected 0, but got " + str(len(line)-1))

    return "11111111111111111111111111111111 //Halt"


#compiles READ instruction
#should have 3 arguments
#arg1 should be a signed number that fits in 16 bits
#arg2 should be a valid register
#arg3 should be a valid register
def compileRead(line):
    if len(line) != 4:
        raise Exception("Incorrect number of arguments. Expected 3, but got " + str(len(line)-1))

    const16 = ""

    #convert arg1 to number
    arg1Int = getNumber(line[1])

    #convert arg1 to binary
    CheckFitsInBits(arg1Int, 16)
    const16 = format(arg1Int & 0xffff, '016b')

    #convert arg2 to number
    arg2Int = getReg(line[2])

    #convert arg2 to binary
    areg = format(arg2Int, '04b')

    #convert arg3 to number
    arg3Int = getReg(line[3])

    #convert arg3 to binary
    dreg = format(arg3Int, '04b')

    #create instruction
    instruction = "1110" + const16 + areg + "0000" + dreg + " //Read at address in " + line[2] + " with offset " + line[1] + " to " + line[3]

    return instruction


#compiles WRITE instruction
#should have 3 arguments
#arg1 should be a signed number that fits in 16 bits
#arg2 should be a valid register
#arg3 should be a valid register
def compileWrite(line):
    if len(line) != 4:
        raise Exception("Incorrect number of arguments. Expected 3, but got " + str(len(line)-1))

    const16 = ""

    #convert arg1 to number
    arg1Int = getNumber(line[1])

    #convert arg1 to binary
    CheckFitsInBits(arg1Int, 16)
    const16 = format(arg1Int & 0xffff, '016b')

    #convert arg2 to number
    arg2Int = getReg(line[2])

    #convert arg2 to binary
    areg = format(arg2Int, '04b')

    #convert arg3 to number
    arg3Int = getReg(line[3])

    #convert arg3 to binary
    breg = format(arg3Int, '04b')

    #create instruction
    instruction = "1101" + const16 + areg + breg + "0000" + " //Write value in " + line[3] + " to address in " + line[2] + " with offset " + line[1]

    return instruction


#compiles INTID instruction
#should have 1 argument
def compileIntID(line):
    if len(line) != 2:
        raise Exception("Incorrect number of arguments. Expected 1, but got " + str(len(line)-1))

    #convert arg1 to number
    arg1Int = getReg(line[1])

    #convert arg1 to binary
    dreg = format(arg1Int, '04b')

    #create instruction
    instruction = "1100000000000000000000000000" + dreg + " //Save Interrupt ID to " + line[1]

    return instruction


#compiles push instruction
#should have 1 argument
def compilePush(line):
    if len(line) != 2:
        raise Exception("Incorrect number of arguments. Expected 1, but got " + str(len(line)-1))

    #convert arg1 to number
    arg1Int = getReg(line[1])

    #convert arg1 to binary
    breg = format(arg1Int, '04b')

    #create instruction
    instruction = "101100000000000000000000" + breg + "0000" + " //Push " + line[1] + " to stack"

    return instruction


#compiles push instruction
#should have 1 argument
def compilePop(line):
    if len(line) != 2:
        raise Exception("Incorrect number of arguments. Expected 1, but got " + str(len(line)-1))

    #convert arg1 to number
    arg1Int = getReg(line[1])

    #convert arg1 to binary
    dreg = format(arg1Int, '04b')

    #create instruction
    instruction = "1010000000000000000000000000" + dreg + " //Pop from stack to " + line[1]

    return instruction


#compiles jump instruction
#should have 1 argument or a label
#arg1 should be a positive number that is within 27 bits unsigned
def compileJump(line):
    if len(line) != 2:
        raise Exception("Incorrect number of arguments. Expected 1, but got " + str(len(line)-1))

    instruction = ""
    ARG1isAlabel = False


    try:
        getNumber(line[1], False)
        ARG1isAlabel = False
    except:
        ARG1isAlabel = True


    #if no label is given
    if not ARG1isAlabel:

        #convert arg1 to number
        arg1Int = getNumber(line[1], False)

        #convert arg1 to binary
        CheckFitsInBits(arg1Int, 27, False)
        const27 = format(arg1Int, '027b')

        #create instruction
        instruction = "1001" + const27 + "0" + " //Jump to constant address " + line[1]

    #if a label is given, process it later
    else:
        instruction = " ".join(line)

    return instruction


#compiles jump instruction to offset
#should have 1 argument
#arg1 should be a positive number that is within 27 bits unsigned
def compileJumpo(line):
    if len(line) != 2:
        raise Exception("Incorrect number of arguments. Expected 1, but got " + str(len(line)-1))

    instruction = ""

    #convert arg1 to number
    arg1Int = getNumber(line[1], False)

    #convert arg1 to binary
    CheckFitsInBits(arg1Int, 27, False)
    const27 = format(arg1Int, '027b')

    #create instruction
    instruction = "1001" + const27 + "1" + " //Jump to offset address " + line[1]

    return instruction


#compiles jump instruction to reg with offset
#should have 2 arguments
#arg1 should be a positive number that is within 16 bits signed
#arg2 should be a reg
def compileJumpr(line):
    if len(line) != 3:
        raise Exception("Incorrect number of arguments. Expected 2, but got " + str(len(line)-1))

    instruction = ""
    
    #convert arg1 to number
    arg1Int= getNumber(line[1])

    #convert arg1 to binary
    CheckFitsInBits(arg1Int, 16)
    const16 = format(arg1Int & 0xfff, '016b')

    #convert arg2 to number
    arg2Int = getReg(line[2])

    #convert arg2 to binary
    breg = format(arg2Int, '04b')

    #create instruction
    instruction = "1000" + const16 + "0000" + breg + "000" + "0" + " //Jump to reg " + line[2] + " with offset " + line[1]

    return instruction


#compiles jump instruction to offset in reg with offset
#should have 2 arguments
#arg1 should be a positive number that is within 16 bits signed
#arg2 should be a reg
def compileJumpro(line):
    if len(line) != 3:
        raise Exception("Incorrect number of arguments. Expected 2, but got " + str(len(line)-1))

    instruction = ""
    
    #convert arg1 to number
    arg1Int= getNumber(line[1])

    #convert arg1 to binary
    CheckFitsInBits(arg1Int, 16)
    const16 = format(arg1Int & 0xffff, '016b')

    #convert arg2 to number
    arg2Int = getReg(line[2])

    #convert arg2 to binary
    breg = format(arg2Int, '04b')

    #create instruction
    instruction = "1000" + const16 + "0000" + breg + "000" + "1" + " //Jump to offset in reg " + line[2] + " with offset " + line[1]

    return instruction


#compiles branch instructions
#should have 3 arguments
#arg1 should be a valid register
#arg2 should be a valid register
#arg3 should be a positive number that is within 16 bits signed
def compileBranch(line, opcode, signed):
    if len(line) != 4:
        raise Exception("Incorrect number of arguments. Expected 3, but got " + str(len(line)-1))

    const16 = ""

    #convert arg1 to number
    arg1Int = getReg(line[1])

    #convert arg1 to binary
    areg = format(arg1Int, '04b')

    #convert arg2 to number
    arg2Int = getReg(line[2])

    #convert arg2 to binary
    breg = format(arg2Int, '04b')

    arg3Int = 0

    #convert arg3 to number
    arg3Int= getNumber(line[3])

    #convert arg3 to binary
    CheckFitsInBits(arg3Int, 16)
    const16 = format(arg3Int & 0xffff, '016b')

    #signed bit
    signedBit = "0"
    signedString = "(unsigned)"
    if signed:
        signedBit = "1"
        signedString = "(signed)"

    #opcode to operation map
    branchOpcodeDict = {
        "000" : " == ",
        "001" : " > ",
        "010" : " >= ",
        "011" : " ?? ",
        "100" : " != ",
        "101" : " < ",
        "110" : " <= ",
        "111" : " ?? "
    }


    #create instruction
    instruction = "0110" + const16 + areg + breg + opcode + signedBit + " //" + signedString + " If " + line[1] + branchOpcodeDict[opcode] + line[2] + ", then jump to offset " + line[3]

    return instruction


# compiles BEQ instruction
def compileBEQ(line):
    return compileBranch(line, "000", False)

# compiles BGT instruction
def compileBGT(line):
    return compileBranch(line, "001", False)

# compiles BGTS instruction
def compileBGTS(line):
    return compileBranch(line, "001", True)

# compiles BGE instruction
def compileBGE(line):
    return compileBranch(line, "010", False)

# compiles BGES instruction
def compileBGES(line):
    return compileBranch(line, "010", True)

# compiles BNE instruction
def compileBNE(line):
    return compileBranch(line, "100", False)

# compiles BLT instruction
def compileBLT(line):
    return compileBranch(line, "101", False)

# compiles BLTS instruction
def compileBLTS(line):
    return compileBranch(line, "101", True)

# compiles BLE instruction
def compileBLE(line):
    return compileBranch(line, "110", False)

# compiles BLES instruction
def compileBLES(line):
    return compileBranch(line, "110", True)


#compiles SAVPC instruction
#should have 1 argument
def compileSavPC(line):
    if len(line) != 2:
        raise Exception("Incorrect number of arguments. Expected 1, but got " + str(len(line)-1))

    #convert arg1 to number
    arg1Int = getReg(line[1])

    #convert arg1 to binary
    dreg = format(arg1Int, '04b')

    #create instruction
    instruction = "0101000000000000000000000000" + dreg + " //Save PC to " + line[1]

    return instruction


#compiles RETI instruction
#should have 0 arguments
def compileReti(line):
    if len(line) != 1:
        raise Exception("Incorrect number of arguments. Expected 0, but got " + str(len(line)-1))

    return "01000000000000000000000000000000 //Return from interrupt"


#compiles CCACHE instruction
#should have 0 arguments
def compileCcache(line):
    if len(line) != 1:
        raise Exception("Incorrect number of arguments. Expected 0, but got " + str(len(line)-1))

    return "01110000000000000000000000000000 //Clear L1 Cache"

#compiles ARITH/ARITHC instructions, except LOAD/LOADHI
#should have 3 arguments
#arg1 should be a valid register
#arg2 should either be a number that is within 16 bits signed, 
# or a valid register
#arg3 should be a valid register
def compileARITH(line, opcode):
    if len(line) != 4:
        raise Exception("Incorrect number of arguments. Expected 3, but got " + str(len(line)-1))

    const16 = ""
    arg2Int = 0
    constantEnable = False
    breg = ""
    instruction = ""

    #convert arg1 to number
    arg1Int = getReg(line[1])

    #convert arg1 to binary
    areg = format(arg1Int, '04b')

    #convert arg2 to number
    if line[2][0].lower() == 'r':   #if arg2 is a register argument
        constantEnable = False
        arg2Int = getReg(line[2])
    else:                           #arg2 is a constant
        constantEnable = True
        arg2Int= getNumber(line[2])

    #convert arg2 to binary
    if constantEnable:
        CheckFitsInBits(arg2Int, 16)
        const16 = format(arg2Int & 0xffff, '016b')
    else:
        breg = format(arg2Int, '04b')
    
    #convert arg3 to number
    arg3Int = getReg(line[3])

    #convert arg3 to binary
    dreg = format(arg3Int, '04b')


    #opcode to operation map
    branchOpcodeDict = {
        "0000" : " OR ",
        "0001" : " AND ",
        "0010" : " XOR ",
        "0011" : " + ",
        "0100" : " - ",
        "0101" : " << ",
        "0110" : " >> ",
        "0111" : " ~A ",
        "1000" : " * (signed) ",
        "1001" : " * (unsigned) ",
        "1010" : " SLT ",
        "1011" : " SLTU ",
        "1110" : " >> (signed) "
    }

    #create instruction
    if constantEnable:
        instruction = "0001" + opcode + const16 + areg + dreg + " //Compute " +  line[1] + branchOpcodeDict[opcode] + line[2] + " and write result to " + line[3]
    else:
        instruction = "0000" + opcode + "000000000000" + areg + breg + dreg + " //Compute " +  line[1] + branchOpcodeDict[opcode] + line[2] + " and write result to " + line[3]

    return instruction


# compiles OR instruction
def compileOR(line):
    return compileARITH(line, "0000")

# compiles AND instruction
def compileAND(line):
    return compileARITH(line, "0001")

# compiles XOR instruction
def compileXOR(line):
    return compileARITH(line, "0010")

# compiles ADD instruction
def compileADD(line):
    return compileARITH(line, "0011")

# compiles SUB instruction
def compileSUB(line):
    return compileARITH(line, "0100")

# compiles SHIFTL instruction
def compileSHIFTL(line):
    return compileARITH(line, "0101")

# compiles SHIFTR instruction
def compileSHIFTR(line):
    return compileARITH(line, "0110")

# compiles SHIFTRS instruction
def compileSHIFTRS(line):
    return compileARITH(line, "1110")

# compiles MULTS instruction
def compileMULTS(line):
    return compileARITH(line, "1000")

# compiles MULTU instruction
def compileMULTU(line):
    return compileARITH(line, "1001")

# compiles SLT instruction
def compileSLT(line):
    return compileARITH(line, "1010")

# compiles SLTU instruction
def compileSLTU(line):
    return compileARITH(line, "1011")

# compiles NOT instruction
#should have 2 arguments
#arg1 should be a valid register
#arg2 should be a valid register
def compileNOT(line):
    if len(line) != 3:
        raise Exception("Incorrect number of arguments. Expected 2, but got " + str(len(line)-1))


    instruction = ""

    #convert arg1 to number
    arg1Int = getReg(line[1])

    #convert arg2 to binary
    areg = format(arg1Int, '04b')
       

    #convert arg3 to number
    arg2Int = getReg(line[2])

    #convert arg3 to binary
    dreg = format(arg2Int, '04b')

    instruction = "00000111000000000000" + areg + "0000" + dreg + " //Compute NOT " +  line[1] + " and write result to " + line[2]

    return instruction


#compiles load instruction
#should have 2 arguments
#arg1 should be a positive number that is within 16 bits unsigned
#arg2 should be a valid register
def compileLoad(line):
    if len(line) != 3:
        raise Exception("Incorrect number of arguments. Expected 2, but got " + str(len(line)-1))

    const16 = ""

    #convert arg1 to number
    arg1Int = getNumber(line[1], False)

    #convert arg1 to binary
    CheckFitsInBits(arg1Int, 16, False)
    const16 = format(arg1Int, '016b')

    #convert arg2 to number
    arg2Int = getReg(line[2])

    #convert arg2 to binary
    dreg = format(arg2Int, '04b')

    #create instruction
    instruction = "00011100" + const16 + dreg + dreg + " //Set " + line[2] + " to " + line[1]

    return instruction


#compiles loadhi instruction
#should have 2 arguments
#arg1 should be a positive number that is within 16 bits unsigned
#arg2 should be a valid register
def compileLoadHi(line):
    if len(line) != 3:
        raise Exception("Incorrect number of arguments. Expected 2, but got " + str(len(line)-1))

    const16 = ""

    #convert arg1 to number
    arg1Int = getNumber(line[1], False)

    #convert arg1 to binary
    CheckFitsInBits(arg1Int, 16, False)
    const16 = format(arg1Int, '016b')

    #convert arg2 to number
    arg2Int = getReg(line[2])

    #convert arg2 to binary
    dreg = format(arg2Int, '04b')

    #create instruction
    instruction = "00011101" + const16 + dreg + dreg + " //Set highest 16 bits of " + line[2] + " to " + line[1]

    return instruction


#compiles addr2reg instruction
#should have 2 arguments
#arg 1 should be a valid label
#arg 2 should be a reg
def compileAddr2reg(line):
    if len(line) != 3:
        raise Exception("Incorrect number of arguments. Expected 2, but got " + str(len(line)-1))

    instruction = "loadLabelHigh " + line[1] + " " + line[2]

    #check if register is valid
    getReg(line[2])

    return instruction


#compiles load32 instruction
#should have 2 arguments
#arg 1 should be a number within 32 bits
#arg 2 should be a reg
def compileLoad32(line):
    if len(line) != 3:
        raise Exception("Incorrect number of arguments. Expected 2, but got " + str(len(line)-1))

    const32 = ""

    #convert arg1 to number
    arg1Int, neg= getNumber(line[1], True, True)
    if neg:
        arg1Int = -arg1Int # because getNumber does ABS on it


    #check if it fits in 16 bits, so we can skip the loadhi
    if not neg:
        try:
            CheckFitsInBits(arg1Int, 16, False)
            return compileLoad(line)
        except:
            pass #continue

    #check if it fits in 32 bits
    if not neg:
        CheckFitsInBits(arg1Int, 32, False)
    else:
        if arg1Int < -2147483648:
            raise ValueError("Negative value " + str(arg1Int) + " does not fit in 32 bits (signed)")

    #convert to 32 bit
    const32 = ""
    if not neg:
        const32 = format(arg1Int, '032b')
    else:
        b = Bits(int=arg1Int, length=32) #signed binary notation
        const32 = b.bin

    constLow = int(const32[0:16], 2)
    constHigh = int(const32[16:32], 2)

    #create instruction
    instruction = "loadBoth " + str(constLow) + " " + str(constHigh) + " " + line[2]

    return instruction


#compiles compileLoadLabelLow instruction
def compileLoadLabelLow(line):
    if len(line) != 3:
        raise Exception("Incorrect number of arguments. Expected 2, but got " + str(len(line)-1))

    const16 = ""

    #convert arg1 to number
    arg1Int = getNumber(line[1], False)

    x = '{0:027b}'.format(arg1Int)

    #x[:11] highest 11 bits
    #x[11:] lowest 16 bits

    arg1Int = getNumber("0b" + x[11:], False)

    #convert arg1 to binary
    CheckFitsInBits(arg1Int, 16, False) #should not be needed
    const16 = format(arg1Int, '016b')

    #convert arg2 to number
    arg2Int = getReg(line[2])

    #convert arg2 to binary
    dreg = format(arg2Int, '04b')

    #create instruction
    instruction = "00011100" + const16 + dreg + dreg + " //Set " + line[2] + " to " + str(arg1Int)

    return instruction


#compiles compileLoadLabelHigh instruction
def compileLoadLabelHigh(line):
    if len(line) != 3:
        raise Exception("Incorrect number of arguments. Expected 2, but got " + str(len(line)-1))

    const16 = ""

    #convert arg1 to number
    arg1Int = getNumber(line[1], False)

    x = '{0:027b}'.format(arg1Int)

    #x[:11] highest 4 bits
    #x[11:] lowest 16 bits

    arg1Int = getNumber("0b" + x[:11], False)

    #convert arg1 to binary
    CheckFitsInBits(arg1Int, 16, False)
    const16 = format(arg1Int, '016b')

    #convert arg2 to number
    arg2Int = getReg(line[2])

    #convert arg2 to binary
    dreg = format(arg2Int, '04b')

    #create instruction
    instruction = "00011101" + const16 + dreg + dreg + " //Set highest 16 bits of " + line[2] + " to " + str(arg1Int)

    return instruction


#compiles nop instruction
#should have 0 arguments
def compileNop(line):
    if len(line) != 1:
        raise Exception("Incorrect number of arguments. Expected 0, but got " + str(len(line)-1))

    return "00000000000000000000000000000000 //NOP"


#compiles .dw instruction
def compileDw(line):
    if len(line) < 2:
        raise Exception("Incorrect number of arguments. Expected 1 or more, but got " + str(len(line)-1))
    
    instruction = "data"

    for i in line:
        if i != ".dw":
            number = getNumber(i, True)
            if i[0] == '-' and int(i, 10) < 0:
                CheckFitsInBits(number, 33, True)
            else:
                CheckFitsInBits(number, 32, False)
            instruction = instruction + " " + format(number & 0xffffffff, '032b')

    return instruction


#compiles .dd instruction
def compileDd(line):
    if len(line) < 2:
        raise Exception("Incorrect number of arguments. Expected 1 or more, but got " + str(len(line)-1))
    
    instruction = "data"

    counter = 1
    for i in line:
        if i != ".dd":
            number = getNumber(i, False)
            CheckFitsInBits(number, 16, False)
            if counter < 1:
                counter = counter + 1
                instruction = instruction + "" + '{0:016b}'.format(number)
            else:
                instruction = instruction + " " + '{0:016b}'.format(number)
                counter = 0

    if counter == 0:
        instruction = instruction + "0000000000000000"

    return instruction


#compiles .db instruction
def compileDb(line):
    if len(line) < 2:
        raise Exception("Incorrect number of arguments. Expected 1 or more, but got " + str(len(line)-1))
    
    instruction = "data"

    counter = 3
    for i in line:
        if i != ".db":
            number = getNumber(i, False)
            CheckFitsInBits(number, 8, False)
            if counter < 3:
                counter = counter + 1
                instruction = instruction + "" + '{0:08b}'.format(number)
            else:
                instruction = instruction + " " + '{0:08b}'.format(number)
                counter = 0

    if counter == 0:
        instruction = instruction + "000000000000000000000000"
    if counter == 1:
        instruction = instruction + "0000000000000000"
    if counter == 2:
        instruction = instruction + "00000000"

    return instruction


#compiles .ds instruction by converting it to a .db instruction
def compileDs(line):
    if len(line) != 2:
        raise Exception("Incorrect number of arguments. Expected 1, but got " + str(len(line)-1))

    #.db instruction
    dbList = []

    dbList.append(".db")

    # check for " "
    if (line[1][0] == "\"" and line[1][-1] == "\""):
        dbList = dbList + [str(ord(char)) for char in line[1][1:-1]]
        #TODO convert string to ascii list using [ord(char) for char in string]
    else:
        raise Exception("Invalid string: " + line[1])
    
    return compileDb(dbList)


#compiles .dl
#should have 1 argument or a label
#arg1 should be a positive number that is within 27 bits unsigned
def compileDl(line):
    if len(line) != 2:
        raise Exception("Incorrect number of arguments. Expected 1, but got " + str(len(line)-1))

    instruction = ""
    ARG1isAlabel = False


    try:
        getNumber(line[1], False)
        ARG1isAlabel = False
    except:
        ARG1isAlabel = True


    #if no label is given
    if not ARG1isAlabel:

        #convert arg1 to number
        arg1Int = getNumber(line[1], False)

        #convert arg1 to binary
        CheckFitsInBits(arg1Int, 27, False)
        const27 = format(arg1Int, '027b')

        #create data (only the label address)
        instruction = "00000" + const27 + " //Label data " + line[1]

    #if a label is given, process it later
    else:
        instruction = " ".join(line)

    return instruction


#compile nothing
def compileNothing(line):
    return "ignore"