# encoding: UTF-8

from sixtypical.ast import (
    Program, Defn, Routine, Block, SingleOp, Reset, Call, GoTo, If, Repeat, For, WithInterruptsOff, Save, PointInto
)
from sixtypical.model import (
    TYPE_BIT, TYPE_BYTE, TYPE_WORD,
    RoutineType, VectorType, TableType, PointerType,
    ConstantRef, IndirectRef, IndexedRef,
)
from sixtypical.scanner import Scanner
from sixtypical.symtab import SymEntry


class ForwardReference(object):
    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return "%s(%r)" % (self.__class__.__name__, self.name)


class Parser(object):
    def __init__(self, symtab, text, filename, include_path):
        self.symtab = symtab
        self.include_path = include_path
        self.scanner = Scanner(text, filename)
        self.current_routine_name = None

    def syntax_error(self, msg):
        self.scanner.syntax_error(msg)

    def lookup(self, name, allow_forward=False, routine_name=None):
        model = self.symtab.fetch_global_ref(name)
        if model is None and routine_name:
            model = self.symtab.fetch_local_ref(routine_name, name)
        if model is None and allow_forward:
            return ForwardReference(name)
        if model is None:
            self.syntax_error('Undefined symbol "{}"'.format(name))
        return model

    def declare(self, name, ast_node, type_):
        if self.symtab.fetch_global_ref(name):
            self.syntax_error('Symbol "%s" already declared' % name)
        self.symtab.symbols[name] = SymEntry(ast_node, type_)

    def declare_local(self, routine_name, name, ast_node, type_):
        if self.symtab.fetch_local_ref(routine_name, name):
            self.syntax_error('Symbol "%s" already declared locally' % name)
        if self.symtab.fetch_global_ref(name):
            self.syntax_error('Symbol "%s" already declared globally' % name)
        self.symtab.locals.setdefault(routine_name, {})[name] = SymEntry(ast_node, type_)

    # ---- symbol resolution

    def resolve_symbols(self, program):
        # This could stand to be better unified.

        def resolve(w):
             return self.lookup(w.name) if isinstance(w, ForwardReference) else w

        def backpatched_type(type_):
            if isinstance(type_, TableType):
                return TableType(backpatched_type(type_.of_type), type_.size)
            elif isinstance(type_, VectorType):
                return VectorType(backpatched_type(type_.of_type))
            elif isinstance(type_, RoutineType):
                return RoutineType(
                    frozenset([resolve(w) for w in type_.inputs]),
                    frozenset([resolve(w) for w in type_.outputs]),
                    frozenset([resolve(w) for w in type_.trashes]),
                )
            else:
                return type_

        for name, symentry in self.symtab.symbols.items():
            symentry.type_ = backpatched_type(symentry.type_)

        def resolve_fwd_reference(obj, field):
            field_value = getattr(obj, field, None)
            if isinstance(field_value, ForwardReference):
                setattr(obj, field, self.lookup(field_value.name))
            elif isinstance(field_value, IndexedRef):
                if isinstance(field_value.ref, ForwardReference):
                    field_value.ref = self.lookup(field_value.ref.name)

        for node in program.all_children():
            if isinstance(node, SingleOp):
                resolve_fwd_reference(node, 'src')
                resolve_fwd_reference(node, 'dest')
            if isinstance(node, (Call, GoTo)):
                resolve_fwd_reference(node, 'location')

    # --- grammar productions

    def program(self):
        defns = []
        routines = []
        includes = []
        while self.scanner.consume('include'):
            filename = self.scanner.token
            self.scanner.scan()
            program = load_program(filename, self.symtab, self.include_path, include_file=True)
            includes.append(program)
        while self.scanner.on('typedef', 'const'):
            if self.scanner.on('typedef'):
                self.typedef()
            if self.scanner.on('const'):
                self.defn_const()
        typenames = ['byte', 'word', 'table', 'vector', 'pointer']  # 'routine',
        typenames.extend(self.symtab.typedefs.keys())
        while self.scanner.on(*typenames):
            type_, defn = self.defn()
            self.declare(defn.name, defn, type_)
            defns.append(defn)
        while self.scanner.consume('define'):
            name = self.scanner.token
            self.scanner.scan()
            self.current_routine_name = name
            preserved = False
            if self.scanner.consume('preserved'):
                preserved = True
            type_, routine = self.routine(name)
            self.declare(name, routine, type_)
            routine.preserved = preserved
            routines.append(routine)
            self.current_routine_name = None
        self.scanner.check_type('EOF')

        program = Program(self.scanner.line_number, defns=defns, routines=routines)
        programs = includes + [program]
        program = merge_programs(programs)

        self.resolve_symbols(program)
        return program

    def typedef(self):
        self.scanner.expect('typedef')
        type_ = self.defn_type()
        name = self.defn_name()
        if name in self.symtab.typedefs:
            self.syntax_error('Type "%s" already declared' % name)
        self.symtab.typedefs[name] = type_
        return type_

    def defn_const(self):
        self.scanner.expect('const')
        name = self.defn_name()
        if name in self.symtab.consts:
            self.syntax_error('Const "%s" already declared' % name)
        loc = self.const()
        self.symtab.consts[name] = loc
        return loc

    def defn(self):
        type_ = self.defn_type()
        name = self.defn_name()

        initial = None
        if self.scanner.consume(':'):
            if isinstance(type_, TableType):
                if self.scanner.on_type('string literal'):
                    initial = self.scanner.token
                    self.scanner.scan()
                else:
                    initial = []
                    initial.append(self.const().value)
                    while self.scanner.consume(','):
                        initial.append(self.const().value)
            else:
                initial = self.const().value

        addr = None
        if self.scanner.consume('@'):
            self.scanner.check_type('integer literal')
            addr = int(self.scanner.token)
            self.scanner.scan()

        if initial is not None and addr is not None:
            self.syntax_error("Definition cannot have both initial value and explicit address")

        return type_, Defn(self.scanner.line_number, name=name, addr=addr, initial=initial)

    def const(self):
        if self.scanner.token in ('on', 'off'):
            loc = ConstantRef(TYPE_BIT, 1 if self.scanner.token == 'on' else 0)
            self.scanner.scan()
            return loc
        elif self.scanner.on_type('integer literal'):
            value = int(self.scanner.token)
            self.scanner.scan()
            type_ = TYPE_WORD if value > 255 else TYPE_BYTE
            loc = ConstantRef(type_, value)
            return loc
        elif self.scanner.consume('word'):
            loc = ConstantRef(TYPE_WORD, int(self.scanner.token))
            self.scanner.scan()
            return loc
        elif self.scanner.token in self.symtab.consts:
            loc = self.symtab.consts[self.scanner.token]
            self.scanner.scan()
            return loc
        else:
            self.syntax_error('bad constant "%s"' % self.scanner.token)

    def defn_size(self):
        self.scanner.expect('[')
        size = self.const().value
        self.scanner.expect(']')
        return size

    def defn_type(self):
        type_ = self.defn_type_term()

        if self.scanner.consume('table'):
            size = self.defn_size()
            if size <= 0 or size > 65536:
                self.syntax_error("Table size must be > 0 and <= 65536")
            type_ = TableType(type_, size)

        return type_

    def defn_type_term(self):
        type_ = None

        if self.scanner.consume('('):
            type_ = self.defn_type()
            self.scanner.expect(')')
            return type_

        if self.scanner.consume('byte'):
            type_ = TYPE_BYTE
        elif self.scanner.consume('word'):
            type_ = TYPE_WORD
        elif self.scanner.consume('vector'):
            type_ = self.defn_type_term()
            if not isinstance(type_, RoutineType):
                self.syntax_error("Vectors can only be of a routine, not %r" % type_)
            type_ = VectorType(type_)
        elif self.scanner.consume('routine'):
            (inputs, outputs, trashes) = self.constraints()
            type_ = RoutineType(frozenset(inputs), frozenset(outputs), frozenset(trashes))
        elif self.scanner.consume('pointer'):
            type_ = PointerType()
        else:
            type_name = self.scanner.token
            self.scanner.scan()
            if type_name not in self.symtab.typedefs:
                self.syntax_error("Undefined type '%s'" % type_name)
            type_ = self.symtab.typedefs[type_name]

        return type_

    def defn_name(self):
        self.scanner.check_type('identifier')
        name = self.scanner.token
        self.scanner.scan()
        return name

    def constraints(self):
        inputs = set()
        outputs = set()
        trashes = set()
        if self.scanner.consume('inputs'):
            inputs = set(self.labels())
        if self.scanner.consume('outputs'):
            outputs = set(self.labels())
        if self.scanner.consume('trashes'):
            trashes = set(self.labels())
        return (
            set([ForwardReference(n) for n in inputs]),
            set([ForwardReference(n) for n in outputs]),
            set([ForwardReference(n) for n in trashes])
        )

    def routine(self, name):
        type_ = self.defn_type()
        if not isinstance(type_, RoutineType):
            self.syntax_error("Can only define a routine, not {}".format(repr(type_)))
        locals_ = []
        if self.scanner.consume('@'):
            self.scanner.check_type('integer literal')
            block = None
            addr = int(self.scanner.token)
            self.scanner.scan()
        else:
            locals_ = self.locals()
            block = self.block()
            addr = None
        return type_, Routine(self.scanner.line_number, name=name, block=block, addr=addr, locals=locals_)

    def labels(self):
        accum = []
        accum.append(self.label())
        while self.scanner.consume(','):
            accum.append(self.label())
        return accum

    def label(self):
        """Like a locexpr, but does not allow literal values, and the labels do not
        need to be defined yet.  They will be resolved at the end of parsing."""
        loc = self.scanner.token
        self.scanner.scan()
        return loc

    def locexprs(self):
        accum = []
        accum.append(self.locexpr())
        while self.scanner.consume(','):
            accum.append(self.locexpr())
        return accum

    def locexpr(self):
        if self.scanner.token in ('on', 'off', 'word') or self.scanner.token in self.symtab.consts or self.scanner.on_type('integer literal'):
            return self.const()
        else:
            name = self.scanner.token
            self.scanner.scan()
            return self.lookup(name, allow_forward=True, routine_name=self.current_routine_name)

    def indlocexpr(self):
        if self.scanner.consume('['):
            loc = self.locexpr()
            self.scanner.expect(']')
            self.scanner.expect('+')
            self.scanner.expect('y')
            return IndirectRef(loc)
        else:
            return self.indexed_locexpr()

    def indexed_locexpr(self):
        loc = self.locexpr()
        if not isinstance(loc, str):
            index = None
            offset = ConstantRef(TYPE_BYTE, 0)
            if self.scanner.consume('+'):
                if self.scanner.token in self.symtab.consts or self.scanner.on_type('integer literal'):
                    offset = self.const()
                    self.scanner.expect('+')
                index = self.locexpr()
                loc = IndexedRef(loc, offset, index)
        return loc

    def locals(self):
        defns = []
        while self.scanner.consume('static'):
            type_, defn = self.defn()
            if defn.initial is None:
                self.syntax_error("Static definition {} must have initial value".format(defn))
            self.declare_local(self.current_routine_name, defn.name, defn, type_)
            defns.append(defn)
        while self.scanner.consume('local'):
            type_, defn = self.defn()
            if defn.initial is not None:
                self.syntax_error("Local definition {} may not have initial value".format(defn))
            self.declare_local(self.current_routine_name, defn.name, defn, type_)
            defns.append(defn)
        return defns

    def block(self):
        instrs = []
        self.scanner.expect('{')
        while not self.scanner.on('}'):
            instrs.append(self.instr())
            if isinstance(instrs[-1], GoTo):
                break
        self.scanner.expect('}')
        return Block(self.scanner.line_number, instrs=instrs)

    def instr(self):
        if self.scanner.consume('if'):
            inverted = False
            if self.scanner.consume('not'):
                inverted = True
            src = self.locexpr()
            block1 = self.block()
            block2 = None
            if self.scanner.consume('else'):
                block2 = self.block()
            return If(self.scanner.line_number, src=src, block1=block1, block2=block2, inverted=inverted)
        elif self.scanner.consume('repeat'):
            inverted = False
            src = None
            block = self.block()
            if self.scanner.consume('until'):
                if self.scanner.consume('not'):
                    inverted = True
                src = self.locexpr()
            else:
                self.scanner.expect('forever')
            return Repeat(self.scanner.line_number, src=src, block=block, inverted=inverted)
        elif self.scanner.consume('for'):
            dest = self.locexpr()
            if self.scanner.consume('down'):
                direction = -1
            elif self.scanner.consume('up'):
                direction = 1
            else:
                self.syntax_error('expected "up" or "down", found "%s"' % self.scanner.token)
            self.scanner.expect('to')
            final = self.const()
            block = self.block()
            return For(self.scanner.line_number, dest=dest, direction=direction, final=final, block=block)
        elif self.scanner.consume('reset'):
            pointer = self.locexpr()
            offset = self.const()
            return Reset(self.scanner.line_number, pointer=pointer, offset=offset)
        elif self.scanner.token in ("ld",):
            # the same as add, sub, cmp etc below, except supports an indlocexpr for the src
            opcode = self.scanner.token
            self.scanner.scan()
            dest = self.locexpr()
            self.scanner.expect(',')
            src = self.indlocexpr()
            return SingleOp(self.scanner.line_number, opcode=opcode, dest=dest, src=src)
        elif self.scanner.token in ("add", "sub", "cmp", "and", "or", "xor"):
            opcode = self.scanner.token
            self.scanner.scan()
            dest = self.locexpr()
            self.scanner.expect(',')
            src = self.indexed_locexpr()
            return SingleOp(self.scanner.line_number, opcode=opcode, dest=dest, src=src)
        elif self.scanner.token in ("st",):
            opcode = self.scanner.token
            self.scanner.scan()
            src = self.locexpr()
            self.scanner.expect(',')
            dest = self.indlocexpr()
            return SingleOp(self.scanner.line_number, opcode=opcode, dest=dest, src=src)
        elif self.scanner.token in ("shl", "shr", "inc", "dec"):
            opcode = self.scanner.token
            self.scanner.scan()
            dest = self.indexed_locexpr()
            return SingleOp(self.scanner.line_number, opcode=opcode, dest=dest, src=None)
        elif self.scanner.token in ("nop",):
            opcode = self.scanner.token
            self.scanner.scan()
            return SingleOp(self.scanner.line_number, opcode=opcode, dest=None, src=None)
        elif self.scanner.consume("call"):
            name = self.scanner.token
            self.scanner.scan()
            instr = Call(self.scanner.line_number, location=ForwardReference(name))
            return instr
        elif self.scanner.consume("goto"):
            name = self.scanner.token
            self.scanner.scan()
            instr = GoTo(self.scanner.line_number, location=ForwardReference(name))
            return instr
        elif self.scanner.token in ("copy",):
            opcode = self.scanner.token
            self.scanner.scan()
            src = self.indlocexpr()
            self.scanner.expect(',')
            dest = self.indlocexpr()
            instr = SingleOp(self.scanner.line_number, opcode=opcode, dest=dest, src=src)
            return instr
        elif self.scanner.consume("with"):
            self.scanner.expect("interrupts")
            self.scanner.expect("off")
            block = self.block()
            return WithInterruptsOff(self.scanner.line_number, block=block)
        elif self.scanner.consume("save"):
            locations = self.locexprs()
            block = self.block()
            return Save(self.scanner.line_number, locations=locations, block=block)
        elif self.scanner.consume("point"):
            pointer = self.locexpr()
            self.scanner.expect("into")
            table = self.locexpr()
            block = self.block()
            return PointInto(self.scanner.line_number, pointer=pointer, table=table, block=block)
        elif self.scanner.consume("trash"):
            dest = self.locexpr()
            return SingleOp(self.scanner.line_number, opcode='trash', src=None, dest=dest)
        else:
            self.syntax_error('bad opcode "%s"' % self.scanner.token)


# - - - -


def load_program(filename, symtab, include_path, include_file=False):
    import os
    if include_file:
        for include_dir in include_path:
            if os.path.exists(os.path.join(include_dir, filename)):
                filename = os.path.join(include_dir, filename)
                break
    text = open(filename).read()
    parser = Parser(symtab, text, filename, include_path)
    program = parser.program()
    return program


def merge_programs(programs):
    """Assumes that the programs do not have any conflicts."""

    full = Program(1, defns=[], routines=[])
    for p in programs:
        full.defns.extend(p.defns)
        full.routines.extend(p.routines)

    return full
