import binascii
from typing import Optional, Dict, ItemsView, List, Union, Tuple
import unicodedata

from . import utils
import re


REGS8 = {"A": 7, "B": 0, "C": 1, "D": 2, "E": 3, "H": 4, "L": 5, "[HL]": 6}
REGS16A = {"BC": 0, "DE": 1, "HL": 2, "SP": 3}
REGS16B = {"BC": 0, "DE": 1, "HL": 2, "AF": 3}
FLAGS = {"NZ": 0x00, "Z": 0x08, "NC": 0x10, "C": 0x18}

CONST_MAP: Dict[str, int] = {}


class ExprBase:
    def asReg8(self) -> Optional[int]:
        return None

    def isA(self, kind: str, value: Optional[str] = None) -> bool:
        return False


class Token(ExprBase):
    def __init__(self, kind: str, value: Union[str, int], line_nr: int) -> None:
        self.kind = kind
        self.value = value
        self.line_nr = line_nr

    def isA(self, kind: str, value: Optional[str] = None) -> bool:
        return self.kind == kind and (value is None or value == self.value)

    def __repr__(self) -> str:
        return "[%s:%s:%d]" % (self.kind, self.value, self.line_nr)

    def asReg8(self) -> Optional[int]:
        if self.kind == 'ID':
            return REGS8.get(str(self.value), None)
        return None


class REF(ExprBase):
    def __init__(self, expr: ExprBase) -> None:
        self.expr = expr

    def asReg8(self) -> Optional[int]:
        if self.expr.isA('ID', 'HL'):
            return REGS8['[HL]']
        return None

    def __repr__(self) -> str:
        return "[%s]" % (self.expr)


class OP(ExprBase):
    def __init__(self, op: str, left: ExprBase, right: Optional[ExprBase] = None):
        self.op = op
        self.left = left
        self.right = right

    def __repr__(self) -> str:
        return "%s %s %s" % (self.left, self.op, self.right)

    @staticmethod
    def make(op: str, left: ExprBase, right: Optional[ExprBase] = None) -> ExprBase:
        if left.isA('NUMBER') and right is not None and right.isA('NUMBER'):
            assert isinstance(right, Token) and isinstance(right.value, int)
            assert isinstance(left, Token) and isinstance(left.value, int)
            if op == '+':
                left.value += right.value
                return left
            if op == '-':
                left.value -= right.value
                return left
            if op == '*':
                left.value *= right.value
                return left
            if op == '/':
                left.value //= right.value
                return left
        if left.isA('NUMBER') and right is None:
            assert isinstance(left, Token) and isinstance(left.value, int)
            if op == '+':
                return left
            if op == '-':
                left.value = -left.value
                return left
        return OP(op, left, right)


class Tokenizer:
    TOKEN_REGEX = re.compile('|'.join('(?P<%s>%s)' % pair for pair in [
        ('NUMBER', r'\d+(\.\d*)?'),
        ('HEX', r'\$[0-9A-Fa-f]+'),
        ('ASSIGN', r':='),
        ('COMMENT', r';[^\n]+'),
        ('LABEL', r':'),
        ('DIRECTIVE', r'#[A-Za-z_]+'),
        ('STRING', '[a-zA-Z]?"[^"]*"'),
        ('ID', r'\.?[A-Za-z_][A-Za-z0-9_\.]*'),
        ('OP', r'[+\-*/,\(\)]'),
        ('REFOPEN', r'\['),
        ('REFCLOSE', r'\]'),
        ('NEWLINE', r'\n'),
        ('SKIP', r'[ \t]+'),
        ('MISMATCH', r'.'),
    ]))

    def __init__(self, code: str) -> None:
        self.__tokens: List[Token] = []
        line_num = 1
        for mo in self.TOKEN_REGEX.finditer(code):
            kind = mo.lastgroup
            assert kind is not None
            value: Union[str, int] = mo.group()
            if kind == 'MISMATCH':
                line = code.split("\n")[line_num - 1]
                raise RuntimeError(f"Syntax error on line: {line_num}: {kind}:`{line}`")
            elif kind == 'SKIP':
                pass
            elif kind == 'COMMENT':
                pass
            else:
                if kind == 'NUMBER':
                    value = int(value)
                elif kind == 'HEX':
                    value = int(str(value)[1:], 16)
                    kind = 'NUMBER'
                elif kind == 'ID':
                    value = str(value).upper()
                self.__tokens.append(Token(kind, value, line_num))
                if kind == 'NEWLINE':
                    line_num += 1
        self.__tokens.append(Token('NEWLINE', '\n', line_num))

    def peek(self) -> Token:
        return self.__tokens[0]

    def pop(self) -> Token:
        return self.__tokens.pop(0)

    def expect(self, kind: str, value: Optional[str] = None) -> None:
        pop = self.pop()
        if not pop.isA(kind, value):
            if value is not None:
                raise SyntaxError("%s != %s:%s" % (pop, kind, value))
            raise SyntaxError("%s != %s" % (pop, kind))

    def __bool__(self) -> bool:
        return bool(self.__tokens)


class Assembler:
    SIMPLE_INSTR = {
        'NOP':  0x00,
        'RLCA': 0x07,
        'RRCA': 0x0F,
        'STOP': 0x010,
        'RLA':  0x17,
        'RRA':  0x1F,
        'DAA':  0x27,
        'CPL':  0x2F,
        'SCF':  0x37,
        'CCF':  0x3F,
        'HALT': 0x76,
        'RETI': 0xD9,
        'DI':   0xF3,
        'EI':   0xFB,
    }

    LINK_REL8 = 0
    LINK_ABS8 = 1
    LINK_ABS16 = 2

    def __init__(self, base_address: Optional[int] = None) -> None:
        self.__base_address = base_address or -1
        self.__result = bytearray()
        self.__label: Dict[str, int] = {}
        self.__constant: Dict[str, int] = {}
        self.__link: Dict[int, Tuple[int, ExprBase]] = {}
        self.__scope: Optional[str] = None

        self.__tok = Tokenizer("")

    def process(self, code: str) -> None:
        conditional_stack = [True]
        self.__tok = Tokenizer(code)
        try:
            while self.__tok:
                start = self.__tok.pop()
                if start.kind == 'NEWLINE':
                    pass  # Empty newline
                elif start.kind == 'DIRECTIVE':
                    if start.value == '#IF':
                        t = self.parseExpression()
                        assert isinstance(t, Token)
                        conditional_stack.append(conditional_stack[-1] and t.value != 0)
                        self.__tok.expect('NEWLINE')
                    elif start.value == '#ELSE':
                        conditional_stack[-1] = not conditional_stack[-1] and conditional_stack[-2]
                        self.__tok.expect('NEWLINE')
                    elif start.value == '#ENDIF':
                        conditional_stack.pop()
                        assert conditional_stack
                        self.__tok.expect('NEWLINE')
                    else:
                        raise SyntaxError(start)
                elif not conditional_stack[-1]:
                    while not self.__tok.pop().isA('NEWLINE'):
                        pass
                elif start.kind == 'ID':
                    if start.value == 'DB':
                        self.instrDB()
                        self.__tok.expect('NEWLINE')
                    elif start.value == 'DW':
                        self.instrDW()
                        self.__tok.expect('NEWLINE')
                    elif start.value == 'LD':
                        self.instrLD()
                        self.__tok.expect('NEWLINE')
                    elif start.value == 'LDH':
                        self.instrLDH()
                        self.__tok.expect('NEWLINE')
                    elif start.value == 'LDI':
                        self.instrLDI()
                        self.__tok.expect('NEWLINE')
                    elif start.value == 'LDD':
                        self.instrLDD()
                        self.__tok.expect('NEWLINE')
                    elif start.value == 'INC':
                        self.instrINC()
                        self.__tok.expect('NEWLINE')
                    elif start.value == 'DEC':
                        self.instrDEC()
                        self.__tok.expect('NEWLINE')
                    elif start.value == 'ADD':
                        self.instrADD()
                        self.__tok.expect('NEWLINE')
                    elif start.value == 'ADC':
                        self.instrALU(0x88)
                        self.__tok.expect('NEWLINE')
                    elif start.value == 'SUB':
                        self.instrALU(0x90)
                        self.__tok.expect('NEWLINE')
                    elif start.value == 'SBC':
                        self.instrALU(0x98)
                        self.__tok.expect('NEWLINE')
                    elif start.value == 'AND':
                        self.instrALU(0xA0)
                        self.__tok.expect('NEWLINE')
                    elif start.value == 'XOR':
                        self.instrALU(0xA8)
                        self.__tok.expect('NEWLINE')
                    elif start.value == 'OR':
                        self.instrALU(0xB0)
                        self.__tok.expect('NEWLINE')
                    elif start.value == 'CP':
                        self.instrALU(0xB8)
                        self.__tok.expect('NEWLINE')
                    elif start.value == 'BIT':
                        self.instrBIT(0x40)
                        self.__tok.expect('NEWLINE')
                    elif start.value == 'RES':
                        self.instrBIT(0x80)
                        self.__tok.expect('NEWLINE')
                    elif start.value == 'SET':
                        self.instrBIT(0xC0)
                        self.__tok.expect('NEWLINE')
                    elif start.value == 'RET':
                        self.instrRET()
                        self.__tok.expect('NEWLINE')
                    elif start.value == 'CALL':
                        self.instrCALL()
                        self.__tok.expect('NEWLINE')
                    elif start.value == 'RLC':
                        self.instrCB(0x00)
                        self.__tok.expect('NEWLINE')
                    elif start.value == 'RRC':
                        self.instrCB(0x08)
                        self.__tok.expect('NEWLINE')
                    elif start.value == 'RL':
                        self.instrCB(0x10)
                        self.__tok.expect('NEWLINE')
                    elif start.value == 'RR':
                        self.instrCB(0x18)
                        self.__tok.expect('NEWLINE')
                    elif start.value == 'SLA':
                        self.instrCB(0x20)
                        self.__tok.expect('NEWLINE')
                    elif start.value == 'SRA':
                        self.instrCB(0x28)
                        self.__tok.expect('NEWLINE')
                    elif start.value == 'SWAP':
                        self.instrCB(0x30)
                        self.__tok.expect('NEWLINE')
                    elif start.value == 'SRL':
                        self.instrCB(0x38)
                        self.__tok.expect('NEWLINE')
                    elif start.value == 'RST':
                        self.instrRST()
                        self.__tok.expect('NEWLINE')
                    elif start.value == 'JP':
                        self.instrJP()
                        self.__tok.expect('NEWLINE')
                    elif start.value == 'JR':
                        self.instrJR()
                        self.__tok.expect('NEWLINE')
                    elif start.value == 'PUSH':
                        self.instrPUSHPOP(0xC5)
                        self.__tok.expect('NEWLINE')
                    elif start.value == 'POP':
                        self.instrPUSHPOP(0xC1)
                        self.__tok.expect('NEWLINE')
                    elif start.value in self.SIMPLE_INSTR:
                        self.__result.append(self.SIMPLE_INSTR[str(start.value)])
                        self.__tok.expect('NEWLINE')
                    elif self.__tok.peek().kind == 'LABEL':
                        self.__tok.pop()
                        self.addLabel(str(start.value))
                    elif self.__tok.peek().kind == 'ASSIGN':
                        self.__tok.pop()
                        value = self.__tok.pop()
                        if value.kind != 'NUMBER':
                            raise SyntaxError(start)
                        self.addConstant(str(start.value), int(value.value))
                    else:
                        raise SyntaxError(start)
                else:
                    raise SyntaxError(start)
        except SyntaxError:
            print("Syntax error on line: %s" % code.split("\n")[self.__tok.peek().line_nr-1])
            raise

    def insert8(self, expr: ExprBase) -> None:
        if expr.isA('NUMBER'):
            assert isinstance(expr, Token)
            value = int(expr.value)
        else:
            self.__link[len(self.__result)] = (Assembler.LINK_ABS8, expr)
            value = 0
        assert 0 <= value < 256
        self.__result.append(value)

    def insertRel8(self, expr: ExprBase) -> None:
        if expr.isA('NUMBER'):
            assert isinstance(expr, Token)
            self.__result.append(int(expr.value))
        else:
            self.__link[len(self.__result)] = (Assembler.LINK_REL8, expr)
            self.__result.append(0x00)

    def insert16(self, expr: ExprBase) -> None:
        if expr.isA('NUMBER'):
            assert isinstance(expr, Token)
            value = int(expr.value)
        else:
            self.__link[len(self.__result)] = (Assembler.LINK_ABS16, expr)
            value = 0
        assert 0 <= value <= 0xFFFF
        self.__result.append(value & 0xFF)
        self.__result.append(value >> 8)

    def insertString(self, string: str) -> None:
        if string.startswith('"') and string.endswith('"'):
            string = string[1:-1]
            string = unicodedata.normalize('NFKD', string)
            self.__result += string.encode("latin1", "ignore")
        elif string.startswith("m\"") and string.endswith("\""):
            self.__result += utils.formatText(string[2:-1].replace("|", "\n"))
        else:
            raise SyntaxError

    def instrLD(self) -> None:
        left_param = self.parseParam()
        self.__tok.expect('OP', ',')
        right_param = self.parseParam()
        lr8 = left_param.asReg8()
        rr8 = right_param.asReg8()
        if lr8 is not None and rr8 is not None:
            self.__result.append(0x40 | (lr8 << 3) | rr8)
        elif left_param.isA('ID', 'A') and isinstance(right_param, REF):
            if right_param.expr.isA('ID', 'BC'):
                self.__result.append(0x0A)
            elif right_param.expr.isA('ID', 'DE'):
                self.__result.append(0x1A)
            elif right_param.expr.isA('ID', 'HL+'):  # TODO
                self.__result.append(0x2A)
            elif right_param.expr.isA('ID', 'HL-'):  # TODO
                self.__result.append(0x3A)
            elif right_param.expr.isA('ID', 'C'):
                self.__result.append(0xF2)
            else:
                self.__result.append(0xFA)
                self.insert16(right_param.expr)
        elif right_param.isA('ID', 'A') and isinstance(left_param, REF):
            if left_param.expr.isA('ID', 'BC'):
                self.__result.append(0x02)
            elif left_param.expr.isA('ID', 'DE'):
                self.__result.append(0x12)
            elif left_param.expr.isA('ID', 'HL+'):  # TODO
                self.__result.append(0x22)
            elif left_param.expr.isA('ID', 'HL-'):  # TODO
                self.__result.append(0x32)
            elif left_param.expr.isA('ID', 'C'):
                self.__result.append(0xE2)
            else:
                self.__result.append(0xEA)
                self.insert16(left_param.expr)
        elif left_param.isA('ID', 'BC'):
            self.__result.append(0x01)
            self.insert16(right_param)
        elif left_param.isA('ID', 'DE'):
            self.__result.append(0x11)
            self.insert16(right_param)
        elif left_param.isA('ID', 'HL'):
            self.__result.append(0x21)
            self.insert16(right_param)
        elif left_param.isA('ID', 'SP'):
            if right_param.isA('ID', 'HL'):
                self.__result.append(0xF9)
            else:
                self.__result.append(0x31)
                self.insert16(right_param)
        elif right_param.isA('ID', 'SP') and isinstance(left_param, REF):
            self.__result.append(0x08)
            self.insert16(left_param.expr)
        elif lr8 is not None:
            self.__result.append(0x06 | (lr8 << 3))
            self.insert8(right_param)
        else:
            raise SyntaxError

    def instrLDH(self) -> None:
        left_param = self.parseParam()
        self.__tok.expect('OP', ',')
        right_param = self.parseParam()
        if left_param.isA('ID', 'A') and isinstance(right_param, REF):
            if right_param.expr.isA('ID', 'C'):
                self.__result.append(0xF2)
            else:
                self.__result.append(0xF0)
                self.insert8(right_param.expr)
        elif right_param.isA('ID', 'A') and isinstance(left_param, REF):
            if left_param.expr.isA('ID', 'C'):
                self.__result.append(0xE2)
            else:
                self.__result.append(0xE0)
                self.insert8(left_param.expr)
        else:
            raise SyntaxError

    def instrLDI(self) -> None:
        left_param = self.parseParam()
        self.__tok.expect('OP', ',')
        right_param = self.parseParam()
        if left_param.isA('ID', 'A') and isinstance(right_param, REF) and right_param.expr.isA('ID', 'HL'):
            self.__result.append(0x2A)
        elif right_param.isA('ID', 'A') and isinstance(left_param, REF) and left_param.expr.isA('ID', 'HL'):
            self.__result.append(0x22)
        else:
            raise SyntaxError

    def instrLDD(self) -> None:
        left_param = self.parseParam()
        self.__tok.expect('OP', ',')
        right_param = self.parseParam()
        if left_param.isA('ID', 'A') and isinstance(right_param, REF) and right_param.expr.isA('ID', 'HL'):
            self.__result.append(0x3A)
        elif right_param.isA('ID', 'A') and isinstance(left_param, REF) and left_param.expr.isA('ID', 'HL'):
            self.__result.append(0x32)
        else:
            raise SyntaxError

    def instrINC(self) -> None:
        param = self.parseParam()
        r8 = param.asReg8()
        if r8 is not None:
            self.__result.append(0x04 | (r8 << 3))
        elif param.isA('ID', 'BC'):
            self.__result.append(0x03)
        elif param.isA('ID', 'DE'):
            self.__result.append(0x13)
        elif param.isA('ID', 'HL'):
            self.__result.append(0x23)
        elif param.isA('ID', 'SP'):
            self.__result.append(0x33)
        else:
            raise SyntaxError

    def instrDEC(self) -> None:
        param = self.parseParam()
        r8 = param.asReg8()
        if r8 is not None:
            self.__result.append(0x05 | (r8 << 3))
        elif param.isA('ID', 'BC'):
            self.__result.append(0x0B)
        elif param.isA('ID', 'DE'):
            self.__result.append(0x1B)
        elif param.isA('ID', 'HL'):
            self.__result.append(0x2B)
        elif param.isA('ID', 'SP'):
            self.__result.append(0x3B)
        else:
            raise SyntaxError

    def instrADD(self) -> None:
        left_param = self.parseParam()
        self.__tok.expect('OP', ',')
        right_param = self.parseParam()

        if left_param.isA('ID', 'A'):
            rr8 = right_param.asReg8()
            if rr8 is not None:
                self.__result.append(0x80 | rr8)
            else:
                self.__result.append(0xC6)
                self.insert8(right_param)
        elif left_param.isA('ID', 'HL') and right_param.isA('ID') and isinstance(right_param, Token) and right_param.value in REGS16A:
            self.__result.append(0x09 | REGS16A[str(right_param.value)] << 4)
        elif left_param.isA('ID', 'SP'):
            self.__result.append(0xE8)
            self.insert8(right_param)
        else:
            raise SyntaxError

    def instrALU(self, code_value: int) -> None:
        param = self.parseParam()
        if param.isA('ID', 'A') and self.__tok.peek().isA('OP', ','):
            self.__tok.pop()
            param = self.parseParam()
        r8 = param.asReg8()
        if r8 is not None:
            self.__result.append(code_value | r8)
        else:
            self.__result.append(code_value | 0x46)
            self.insert8(param)

    def instrRST(self) -> None:
        param = self.parseParam()
        if param.isA('NUMBER') and isinstance(param, Token) and (int(param.value) & ~0x38) == 0:
            self.__result.append(0xC7 | int(param.value))
        else:
            raise SyntaxError

    def instrPUSHPOP(self, code_value: int) -> None:
        param = self.parseParam()
        if param.isA('ID') and isinstance(param, Token) and str(param.value) in REGS16B:
            self.__result.append(code_value | (REGS16B[str(param.value)] << 4))
        else:
            raise SyntaxError

    def instrJR(self) -> None:
        param = self.parseParam()
        if self.__tok.peek().isA('OP', ','):
            self.__tok.pop()
            condition = param
            param = self.parseParam()
            if condition.isA('ID') and isinstance(condition, Token) and str(condition.value) in FLAGS:
                self.__result.append(0x20 | FLAGS[str(condition.value)])
            else:
                raise SyntaxError
        else:
            self.__result.append(0x18)
        self.insertRel8(param)

    def instrCB(self, code_value: int) -> None:
        param = self.parseParam()
        r8 = param.asReg8()
        if r8 is not None:
            self.__result.append(0xCB)
            self.__result.append(code_value | r8)
        else:
            raise SyntaxError

    def instrBIT(self, code_value: int) -> None:
        left_param = self.parseParam()
        self.__tok.expect('OP', ',')
        right_param = self.parseParam()
        rr8 = right_param.asReg8()
        if left_param.isA('NUMBER') and isinstance(left_param, Token) and rr8 is not None:
            self.__result.append(0xCB)
            self.__result.append(code_value | (int(left_param.value) << 3) | rr8)
        else:
            raise SyntaxError

    def instrRET(self) -> None:
        if self.__tok.peek().isA('ID'):
            condition = self.__tok.pop()
            if condition.isA('ID') and condition.value in FLAGS:
                self.__result.append(0xC0 | FLAGS[str(condition.value)])
            else:
                raise SyntaxError
        else:
            self.__result.append(0xC9)

    def instrCALL(self) -> None:
        param = self.parseParam()
        if self.__tok.peek().isA('OP', ','):
            self.__tok.pop()
            condition = param
            param = self.parseParam()
            if condition.isA('ID') and isinstance(condition, Token) and condition.value in FLAGS:
                self.__result.append(0xC4 | FLAGS[str(condition.value)])
            else:
                raise SyntaxError
        else:
            self.__result.append(0xCD)
        self.insert16(param)

    def instrJP(self) -> None:
        param = self.parseParam()
        if self.__tok.peek().isA('OP', ','):
            self.__tok.pop()
            condition = param
            param = self.parseParam()
            if condition.isA('ID') and isinstance(condition, Token) and condition.value in FLAGS:
                self.__result.append(0xC2 | FLAGS[str(condition.value)])
            else:
                raise SyntaxError
        elif param.isA('ID', 'HL'):
            self.__result.append(0xE9)
            return
        else:
            self.__result.append(0xC3)
        self.insert16(param)

    def instrDW(self) -> None:
        param = self.parseExpression()
        self.insert16(param)
        while self.__tok.peek().isA('OP', ','):
            self.__tok.pop()
            param = self.parseExpression()
            self.insert16(param)

    def instrDB(self) -> None:
        param = self.parseExpression()
        if param.isA('STRING'):
            assert isinstance(param, Token)
            self.insertString(str(param.value))
        else:
            self.insert8(param)
        while self.__tok.peek().isA('OP', ','):
            self.__tok.pop()
            param = self.parseExpression()
            if param.isA('STRING'):
                assert isinstance(param, Token)
                self.insertString(str(param.value))
            else:
                self.insert8(param)

    def addLabel(self, label: str) -> None:
        if label.startswith("."):
            assert self.__scope is not None
            label = self.__scope + label
        else:
            assert "." not in label, label
            self.__scope = label
        assert label not in self.__label, "Duplicate label: %s" % (label)
        assert label not in self.__constant, "Duplicate label: %s" % (label)
        self.__label[label] = len(self.__result)

    def addConstant(self, name: str, value: int) -> None:
        assert name not in self.__constant, "Duplicate constant: %s" % (name)
        assert name not in self.__label, "Duplicate constant: %s" % (name)
        self.__constant[name] = value

    def parseParam(self) -> ExprBase:
        t = self.__tok.peek()
        if t.kind == 'REFOPEN':
            self.__tok.pop()
            expr = self.parseExpression()
            self.__tok.expect('REFCLOSE')
            return REF(expr)
        return self.parseExpression()

    def parseExpression(self) -> ExprBase:
        t = self.parseAddSub()
        return t

    def parseAddSub(self) -> ExprBase:
        t = self.parseFactor()
        p = self.__tok.peek()
        if p.isA('OP', '+') or p.isA('OP', '-'):
            self.__tok.pop()
            return OP.make(str(p.value), t, self.parseAddSub())
        return t

    def parseFactor(self) -> ExprBase:
        t = self.parseUnary()
        p = self.__tok.peek()
        if p.isA('OP', '*') or p.isA('OP', '/'):
            self.__tok.pop()
            return OP.make(str(p.value), t, self.parseFactor())
        return t

    def parseUnary(self) -> ExprBase:
        t = self.__tok.pop()
        if t.isA('OP', '-') or t.isA('OP', '+'):
            return OP.make(str(t.value), self.parseUnary())
        elif t.isA('OP', '('):
            result = self.parseExpression()
            self.__tok.expect('OP', ')')
            return result
        if t.kind not in ('ID', 'NUMBER', 'STRING'):
            raise SyntaxError
        if t.isA('ID') and t.value in CONST_MAP:
            t.kind = 'NUMBER'
            t.value = CONST_MAP[str(t.value)]
        elif t.isA('ID') and t.value in self.__constant:
            t.kind = 'NUMBER'
            t.value = self.__constant[str(t.value)]
        elif t.isA('ID') and str(t.value).startswith("."):
            assert self.__scope is not None
            t.value = self.__scope + str(t.value)
        return t

    def link(self) -> None:
        for offset, (link_type, link_expr) in self.__link.items():
            expr = self.resolveExpr(link_expr)
            assert expr is not None
            assert expr.isA('NUMBER'), expr
            assert isinstance(expr, Token)
            value = int(expr.value)
            if link_type == Assembler.LINK_REL8:
                byte = (value - self.__base_address) - offset - 1
                assert -128 <= byte <= 127, expr
                self.__result[offset] = byte & 0xFF
            elif link_type == Assembler.LINK_ABS8:
                assert 0 <= value <= 0xFF
                self.__result[offset] = value & 0xFF
            elif link_type == Assembler.LINK_ABS16:
                assert self.__base_address >= 0, "Cannot place absolute values in a relocatable code piece"
                assert 0 <= value <= 0xFFFF
                self.__result[offset] = value & 0xFF
                self.__result[offset + 1] = value >> 8
            else:
                raise RuntimeError

    def resolveExpr(self, expr: Optional[ExprBase]) -> Optional[ExprBase]:
        if expr is None:
            return None
        elif isinstance(expr, OP):
            left = self.resolveExpr(expr.left)
            assert left is not None
            return OP.make(expr.op, left, self.resolveExpr(expr.right))
        elif isinstance(expr, Token) and expr.isA('ID') and isinstance(expr, Token) and expr.value in self.__label:
            return Token('NUMBER', self.__label[str(expr.value)] + self.__base_address, expr.line_nr)
        return expr

    def getResult(self) -> bytearray:
        return self.__result

    def getLabels(self) -> ItemsView[str, int]:
        return self.__label.items()


def const(name: str, value: int) -> None:
    name = name.upper()
    assert name not in CONST_MAP or CONST_MAP[name] == value
    CONST_MAP[name] = value


def resetConsts() -> None:
    CONST_MAP.clear()


def ASM(code: str, base_address: Optional[int] = None, labels_result: Optional[Dict[str, int]] = None) -> bytes:
    asm = Assembler(base_address)
    asm.process(code)
    asm.link()
    if labels_result is not None:
        assert base_address is not None
        for label, offset in asm.getLabels():
            labels_result[label] = base_address + offset
    return binascii.hexlify(asm.getResult())


def allOpcodesTest() -> None:
    import json
    opcodes = json.load(open("Opcodes.json", "rt"))
    for label in (False, True):
        for prefix, codes in opcodes.items():
            for num, op in codes.items():
                if op['mnemonic'].startswith('ILLEGAL_') or op['mnemonic'] == 'PREFIX':
                    continue
                params = []
                postfix = ''
                for o in op['operands']:
                    name = o['name']
                    if name == 'd16' or name == 'a16':
                        if label:
                            name = 'LABEL'
                        else:
                            name = '$0000'
                    if name == 'd8' or name == 'a8':
                        name = '$00'
                    if name == 'r8':
                        if label and num != '0xE8':
                            name = 'LABEL'
                        else:
                            name = '$00'
                    if name[-1] == 'H' and name[0].isnumeric():
                        name = '$' + name[:-1]
                    if o['immediate']:
                        params.append(name)
                    else:
                        params.append("[%s]" % (name))
                    if 'increment' in o and o['increment']:
                        postfix = 'I'
                    if 'decrement' in o and o['decrement']:
                        postfix = 'D'
                code = op["mnemonic"] + postfix + " " + ", ".join(params)
                code = code.strip()
                try:
                    data = ASM("LABEL:\n%s" % (code), 0x0000)
                    if prefix == 'cbprefixed':
                        assert data[0:2] == b'cb'
                        data = data[2:]
                    assert data[0:2] == num[2:].encode('ascii').lower(), data[0:2] + b"!=" + num[2:].encode('ascii').lower()
                except Exception as e:
                    print("%s\t\t|%r|\t%s" % (code, e, num))
                    print(op)


if __name__ == "__main__":
    #allOpcodesTest()
    const("CONST1", 1)
    const("CONST2", 2)
    ASM("""
    ld a, (123)
    ld hl, $1234 + 456
    ld hl, $1234 + CONST1
    ld hl, label
    ld hl, label.end - label
    ld c, label.end - label
label:
    nop
.end:
    """, 0)
    ASM("""
    jr label
label:
    """)
    assert ASM("db 1 + 2 * 3") == b'07'
