import argparse
import enum
import lark
import os
import struct
import sys

import obj_pb2

_HERE = os.path.dirname(__file__)
GRAMMAR_FILE = os.path.join(_HERE, 'as.ebnf')

opcodes = {
        'nop'  : lambda: '0000',
        'load' : lambda p0, p1, p2: f'1{p0:x}{p1:x}{(p2 >> 1)&0xf:x}',
        'store': lambda p0, p1, p2: f'2{p0:x}{p1:x}{(p2 >> 1)&0xf:x}',
        'add'  : lambda p0, p1, p2: f'3{p0:x}{p1:x}{p2:x}',
        'sub'  : lambda p0, p1, p2: f'4{p0:x}{p1:x}{p2:x}',
        'or'   : lambda p0, p1, p2: f'5{p0:x}{p1:x}{p2:x}',
        'and'  : lambda p0, p1, p2: f'6{p0:x}{p1:x}{p2:x}',
        'not'  : lambda p0, p1: f'7{p0:x}{p1:x}0',
        'xor'  : lambda p0, p1, p2: f'8{p0:x}{p1:x}{p2:x}',
        'seth' : lambda p0, p1: f'9{p0:x}{p1&0xff:02x}',
        'shr'  : lambda p0, p1, p2: f'a{p0:x}{p1:x}{p2:x}',
        'mul'  : lambda p0, p1, p2: f'b{p0:x}{p1:x}{p2:x}',
        'cmp'  : lambda p0, p1: f'c{p0:x}{p1:x}0',
        'beq'  : lambda p0, p1: f'd{(p1 >> 1)&0xfff:03x}',
        'set'  : lambda p0, p1: f'e{p0:x}{p1&0xff:02x}',
        'bneq' : lambda p0, p1: f'f{(p1 >> 1)&0xfff:03x}',
        '.word': lambda p0: f'{p0:04x}',
        }


class AsTransformer(lark.Transformer):
    def __init__(self):
        self.addr = 0

    def statement(self, s):
        opcode, *params = s
        if params[0] is None:
            params = []
        addr = self.addr
        self.addr += 2
        return ('OP', addr, opcode, *params)

    def opcode(self, o):
        (o, ) = o
        return str(o)

    def reg_offset(self, r):
        if len(r) < 2:
            return r[0], 0
        return tuple(r[:])

    LABEL = str
    HEX_LITTERAL = lambda _, x: int(x[2:], 16)

    def label(self, l):
        (l,) = l
        return ('LBL', l, self.addr)

    def label_ref(self, l):
        (l,) = l
        return l

    def raw_word(self, w):
        (w,) = w
        addr = self.addr
        self.addr += 2
        return ('OP', addr, '.word', w)

    def register(self, r):
        (r,) = r
        if r == 'pc':
            return 14
        if r == 'lr':
            return 13
        if r == 'sp':
            return 12
        return int(r[1:])

    def immediate(self, i):
        (i,) = i
        return int(i)

    def symbol_def(self, s):
        (sym,) = s
        return ('SYM', s[0])

    start = list


def generate_ops(ops, labels, relocs):
    def filter_label(params, pc):
        for p in params:
            if isinstance(p, str):  # label ref
                if len(params) == 1:  # branch
                    yield 14 # pc
                    yield labels[p] - pc - 4
                else:  # set, allow relocs here
                    relocs.append((pc, p))
                    yield 0xff
            elif isinstance(p, tuple):  # reg offset
                yield p[0]
                yield p[1]
            else:
                yield p

    for op in ops:
        addr = op[0]
        fmt = opcodes[op[1]]
        params = filter_label(op[2:], addr)
        yield fmt(*params)


def larkparse(f, debug=False):
    with open(GRAMMAR_FILE) as g:
        asparser = lark.Lark(g.read())
    tree = asparser.parse(f.read())
    if debug:
        print(tree.pretty())
    lines = AsTransformer().transform(tree)
    labels = {l[1]: l[2] for l in lines if l[0] == 'LBL'}
    ops = [l[1:] for l in lines if l[0] == 'OP']
    syms = [(l[1], labels[l[1]]) for l in lines if l[0] == 'SYM']
    relocs = []
    return [int(x, 16) for x in generate_ops(ops, labels, relocs)], syms, relocs


# any record: >BHs, type, length, data
# header: >cccc, "pol0"
# text record: >BHs, name, text_len, text
# reloc record: >pHip, txt_rec_name, offset, target

def write_obj(fout, ops, syms, relocs):
    obj = obj_pb2.ObjFile(header=obj_pb2.Header(magic='pol0'))

    ends = [s[1] for s in syms[1:]] + [2 * len(ops)]
    data = b''.join(struct.pack('>H', op) for op in ops)

    for (name, begin), end in zip(syms, ends):
        section = obj_pb2.Section(name=name, text=data[begin:end])
        obj.sections.append(section)
        for offs, target in relocs:
            if offs >= begin and offs < end:
                section_off = offs - begin
                reloc = obj_pb2.Reloc(section=name, offset=section_off, target=target)
                obj.relocs.append(reloc)

    fout.write(obj.SerializeToString())



def parse_args():
    parser = argparse.ArgumentParser(description='Assemble.')
    group = parser.add_mutually_exclusive_group()
    group.add_argument('--vhdl', action='store_true',
                        help='output VHDL code')
    group.add_argument('--coe', action='store_true',
                        help='output a Vivado COE file')
    parser.add_argument('--debug', action='store_true',
                        help='print the AST')
    parser.add_argument('--input', '-c', type=argparse.FileType('r'),
                        default=sys.stdin, help='input file (default: stdin)')
    parser.add_argument('--output', '-o', type=argparse.FileType('wb'),
                        default=sys.stdout.buffer, help='output file')
    return parser.parse_args()


def main():
    args = parse_args()

    ops, syms, relocs = larkparse(args.input, debug=args.debug)

    if args.vhdl:
        print(',\n'.join(f'x"{x:04x}"' for x in ops))
        for addr, target in relocs:
            print(f'-- reloc: {target} @{addr}')
    elif args.coe:
        assert not relocs, "relocs not supported with coe output"
        print('memory_initialization_radix=16;')
        print('memory_initialization_vector=')
        code = ',\n'.join(f'{x:04x}' for x in ops)
        print(f'{code};')
    else:
        write_obj(args.output, ops, syms, relocs)


if __name__ == "__main__":
    main()
