# -*- coding: utf8 -*-
from collections import namedtuple

from jawa.constants import Constant
from jawa.util.bytecode import (
    Operand,
    OperandTypes,
    Instruction,
    definition_from_mnemonic
)


Label = namedtuple('Label', ['name'])


def assemble(code):
    """
    Assemble the given iterable of mnemonics, operands, and lables.

    A convienience over constructing individual Instruction and Operand
    objects, the output of this function can be directly piped to
    :class:`~jawa.attributes.code.CodeAttribute.assemble()` to produce
    executable bytecode.

    As a simple example, lets produce an infinite loop:

        >>> from jawa.assemble import assemble, Label
        >>> print(list(assemble((
        ...     Label('start'),
        ...     ('goto', Label('start'))
        ... ))))
        [Instruction(mnemonic='goto', opcode=167, operands=[
            Operand(op_type=40, value=0)], pos=0)]

    For a more complex example, see examples/hello_world.py.
    """
    final = []

    # We need to make three passes, because we cannot know the offset for
    # jump labels until after we've figured out the PC for each instructions,
    # which is complicated by the variable-width instructions set and
    # alignment padding.
    for line in code:
        if isinstance(line, Label):
            final.append(line)
            continue

        mnemonic, operands = line[0], line[1:]
        operand_fmts = definition_from_mnemonic(mnemonic)[1]

        # We need to coerce each opcodes operands into their
        # final `Operand` form.
        final_operands = []
        for i, operand in enumerate(operands):
            if isinstance(operand, Operand):
                # Already in Operand form.
                final_operands.append(operand)
            elif isinstance(operand, Constant):
                # Convert constants into CONSTANT_INDEX'es
                final_operands.append(Operand(
                    OperandTypes.CONSTANT_INDEX,
                    operand.index
                ))
            elif isinstance(operand, dict):
                    # lookupswitch's operand is a dict as
                # a special usability case.
                final_operands.append(operand)
            elif isinstance(operand, Label):
                final_operands.append(operand)
            else:
                # For anything else, lookup that opcode's operand
                # type from its definition.
                final_operands.append(Operand(
                    operand_fmts[i][1],
                    operand
                ))

        # Build the final, immutable `Instruction`.
        final.append(Instruction.from_mnemonic(
            mnemonic,
            operands=final_operands
        ))

    label_pcs = {}

    # The second pass, find the absolute PC for each label.
    current_pc = 0
    for ins in final:
        if isinstance(ins, Label):
            label_pcs[ins.name] = current_pc
            continue

        # size_on_disk must know the current pc because of alignment on
        # tableswitch and lookupswitch.
        current_pc += ins.size_on_disk(current_pc)

    # The third pass, now that we know where each label is we can figure
    # out the offset for each jump.
    current_pc = 0
    offset = lambda l: Operand(40, label_pcs[l.name] - current_pc)

    for ins in final:
        if isinstance(ins, Label):
            continue

        for i, operand in enumerate(ins.operands):
            if isinstance(operand, dict):
                # lookupswitch is a special case
                for k, v in operand.items():
                    if isinstance(v, Label):
                        operand[k] = offset(v)
            elif isinstance(operand, Label):
                ins.operands[i] = offset(operand)

        current_pc += ins.size_on_disk(current_pc)

        yield ins
