"""Parses Modelverse bytecode graphs into bytecode ir."""

import modelverse_jit.bytecode_ir as bytecode_ir
import modelverse_jit.runtime as jit_runtime
import modelverse_kernel.primitives as primitive_functions

class BytecodeParser(object):
    """Parses bytecode graphs."""
    def __init__(self):
        self.parsed_nodes = {}

    def parse_instruction(self, node_id):
        """Parses the instruction node with the given identifier."""
        if node_id is None:
            raise primitive_functions.PrimitiveFinished(None)
        elif node_id in self.parsed_nodes:
            # We've already parsed this node, so return it right away.
            raise primitive_functions.PrimitiveFinished(self.parsed_nodes[node_id])

        instruction_val, = yield [("RV", [node_id])]
        instruction_type = instruction_val["value"]
        # Create an instruction and store it in the instruction dictionary.
        instruction = self.create_instruction(instruction_type)
        self.parsed_nodes[node_id] = instruction
        # Initialize the instruction from the node's data.
        yield [("CALL_ARGS", [self.initialize_instruction, (instruction, node_id)])]
        # Retrieve the debug information.
        debug_info, = yield [("RD", [node_id, "__debug"])]
        if debug_info is not None:
            debug_info, = yield [("RV", [debug_info])]
            instruction.debug_information = debug_info

        # Check if the instruction has a 'next' instruction.
        next_instr_id, = yield [("RD", [node_id, "next"])]
        if next_instr_id is not None:
            instruction.next_instruction, = yield [
                ("CALL_ARGS", [self.parse_instruction, (next_instr_id,)])]

        raise primitive_functions.PrimitiveFinished(instruction)

    def parse_variable(self, node_id):
        """Parses the given variable node."""
        var_name, = yield [("RV", [node_id])]
        raise primitive_functions.PrimitiveFinished(
            bytecode_ir.VariableNode(node_id, var_name))

    def __parse_node_unchecked(self, node_id, result_type):
        """Parses the given node as the specified type of object, without
           checking that the result actually conforms to said type."""
        if result_type is bytecode_ir.VariableNode:
            yield [("TAIL_CALL_ARGS", [self.parse_variable, (node_id,)])]
        elif result_type is int:
            raise primitive_functions.PrimitiveFinished(node_id)
        else:
            yield [("TAIL_CALL_ARGS", [self.parse_instruction, (node_id,)])]

    def parse_node(self, node_id, result_type):
        """Parses the given node as the specified type of object."""
        result, = yield [("CALL_ARGS", [self.__parse_node_unchecked, (node_id, result_type)])]
        if result is not None and not isinstance(result, result_type):
            raise jit_runtime.JitCompilationFailedException(
                "Parsed a node as an instance of '%s', expected an instance of '%s'." % (
                    type(result).__name__, result_type.__name__))

        raise primitive_functions.PrimitiveFinished(result)

    def parse_arguments(self, first_argument_id):
        """Parses the parameter-to-argument mapping started by the specified first argument
           node."""
        next_param = first_argument_id
        named_args = []
        while next_param is not None:
            param_name_id, = yield [("RD", [next_param, "name"])]
            param_name, = yield [("RV", [param_name_id])]
            param_val_id, = yield [("RD", [next_param, "value"])]
            param_val, = yield [("CALL_ARGS", [self.parse_instruction, (param_val_id,)])]
            named_args.append((param_name, param_val))

            next_param, = yield [("RD", [next_param, "next_param"])]

        raise primitive_functions.PrimitiveFinished(named_args)

    def create_instruction(self, instruction_type):
        """Creates an instruction of the given type."""
        if instruction_type in bytecode_ir.INSTRUCTION_TYPE_MAPPING:
            return object.__new__(bytecode_ir.INSTRUCTION_TYPE_MAPPING[instruction_type])
        else:
            raise jit_runtime.JitCompilationFailedException(
                "Unknown instruction type: '%s'" % instruction_type)

    def initialize_call(self, call_instruction, node_id):
        """Initializes the given call instruction."""
        func_id, first_arg_id, = yield [
            ("RD", [node_id, "func"]),
            ("RD", [node_id, "params"])]
        func_val, = yield [("CALL_ARGS", [self.parse_instruction, (func_id,)])]
        named_args, = yield [("CALL_ARGS", [self.parse_arguments, (first_arg_id,)])]
        call_instruction.__init__(func_val, named_args)
        raise primitive_functions.PrimitiveFinished(None)

    def initialize_instruction(self, instruction, node_id):
        """Initializes the given instruction with data from the given node."""
        instr_type = type(instruction)
        # print("Initializing '%s' node" % instr_type)
        if instr_type is bytecode_ir.CallInstruction:
            # Call instructions are complicated, so they get special treatment.
            yield [("TAIL_CALL_ARGS", [self.initialize_call, (instruction, node_id)])]
        else:
            # Construct an argument list based on the `constructor_parameters` attribute
            # of the instruction type.
            arg_list = []
            for dict_key, ctor_param_ty in instr_type.constructor_parameters:
                arg_id, = yield [("RD", [node_id, dict_key])]
                arg, = yield [("CALL_ARGS", [self.parse_node, (arg_id, ctor_param_ty)])]
                arg_list.append(arg)
            instruction.__init__(*arg_list)
            raise primitive_functions.PrimitiveFinished(None)
