"""Optimizes and analyzes CFG-IR."""

from collections import defaultdict
import modelverse_jit.cfg_ir as cfg_ir
import modelverse_jit.cfg_dominators as cfg_dominators
import modelverse_jit.cfg_ssa_construction as cfg_ssa_construction
import modelverse_jit.cfg_data_structures as cfg_data_structures
import modelverse_kernel.primitives as primitive_functions

def is_empty_block(block):
    """Tests if the given block contains no parameters or definitions."""
    return len(block.parameters) == 0 and len(block.definitions) == 0

def optimize_flow(block):
    """Optimizes the given block's flow instruction."""
    changed = True
    while changed:
        changed = False

        # Select flow with a literal condition can be optimized to a direct jump.
        if (isinstance(block.flow, cfg_ir.SelectFlow)
                and cfg_ir.is_literal_def(block.flow.condition)):
            literal = cfg_ir.get_literal_def_value(block.flow.condition)
            block.flow = cfg_ir.JumpFlow(
                block.flow.if_branch if literal else block.flow.else_branch)
            changed = True

        # Jumps to blocks which contain no parameters or definitions can be replaced
        # by the target block's flow.
        if (isinstance(block.flow, cfg_ir.JumpFlow)
                and is_empty_block(block.flow.branch.block)
                and block.flow.branch.block is not block):
            block.flow = block.flow.branch.block.flow
            changed = True

        # Branches to blocks which contain nothing but a jump can be replaced by branches
        # to the jump's target.
        for branch in block.flow.branches():
            if (is_empty_block(branch.block)
                    and branch.block is not block
                    and isinstance(branch.block.flow, cfg_ir.JumpFlow)):
                new_branch = branch.block.flow.branch
                branch.block = new_branch.block
                branch.arguments = new_branch.arguments
                changed = True

def optimize_graph_flow(entry_point):
    """Optimizes all flow instructions in the graph defined by the given entry point."""
    for block in cfg_ir.get_all_blocks(entry_point):
        optimize_flow(block)

def merge_blocks(entry_point):
    """Merges blocks which have exactly one predecessor with said predecessor, if the
       predecessor has a jump flow instruction."""
    predecessor_map = cfg_ir.get_all_predecessor_blocks(entry_point)
    queue = set(predecessor_map.keys())
    queue.add(entry_point)
    def __do_merge(source, target):
        target_params = list(target.parameters)
        branch_args = list(source.flow.branch.arguments)
        for target_param, branch_arg in zip(target_params, branch_args):
            target.remove_parameter(target_param)
            target_param.redefine(branch_arg)
            source.append_definition(target_param)

        target_defs = list(target.definitions)
        for target_def in target_defs:
            target.remove_definition(target_def)
            source.append_definition(target_def)

        source.flow = target.flow
        for preds in predecessor_map.values():
            if target in preds:
                preds[preds.index(target)] = source
                # preds.remove(target)
                # preds.add(source)

    while len(queue) > 0:
        block = queue.pop()
        if isinstance(block.flow, cfg_ir.JumpFlow):
            next_block = block.flow.branch.block
            preds = predecessor_map[next_block]
            if (len(preds) == 1
                    and next(iter(preds)) == block
                    and block != next_block
                    and next_block != entry_point):
                __do_merge(block, next_block)
                del predecessor_map[next_block]
                queue.add(block)
                if next_block in queue:
                    queue.remove(next_block)

def elide_local_checks(entry_point):
    """Tries to elide redundant checks on local variables."""
    # The plan here is to replace all check-local-exists defs by literals if
    # they are either dominated by an appropriate declare-local or not reachable
    # from a declare-local.
    local_checks = []
    local_defs = defaultdict(list)
    for block in cfg_ir.get_all_blocks(entry_point):
        for definition in block.definitions:
            def_value = definition.value
            if isinstance(def_value, cfg_ir.CheckLocalExists):
                local_checks.append((def_value.variable.node_id, definition))
            elif isinstance(def_value, cfg_ir.DeclareLocal):
                local_defs[def_value.variable.node_id].append(definition)

    dominator_tree = cfg_dominators.get_dominator_tree(entry_point)
    reachable_blocks = cfg_ir.get_all_reachable_blocks(entry_point)
    for (variable, check) in local_checks:
        is_reachable = False
        for local_def in local_defs[variable]:
            if dominator_tree.dominates_instruction(local_def, check):
                # Check is dominated by a definition. Replace it by a 'True' literal.
                check.redefine(cfg_ir.Literal(True))
                is_reachable = True
                break
            elif check.block in reachable_blocks[local_def.block]:
                is_reachable = True

        if not is_reachable:
            # Check cannot be reached from any definition. Replace it by a 'False' literal.
            check.redefine(cfg_ir.Literal(False))

def eliminate_unused_definitions(entry_point):
    """Tries to eliminate unused definitions in the control-flow graphb defined by the
       given entry point."""
    def_dependencies = defaultdict(set)
    root_defs = set()
    # Gather dependencies.
    for block in cfg_ir.get_all_blocks(entry_point):
        for definition in block.parameters + block.definitions:
            all_dependencies = list(definition.get_all_dependencies())
            def_dependencies[definition].update(
                [dep for dep in all_dependencies
                 if isinstance(dep, cfg_ir.Definition)])

            if len(all_dependencies) > 0 and definition.has_bidirectional_dependencies():
                for dep in all_dependencies:
                    def_dependencies[dep].add(definition)

            if definition.has_side_effects():
                root_defs.add(definition)

        for dep in block.flow.get_dependencies():
            if isinstance(dep, cfg_ir.Definition):
                root_defs.add(dep)
            else:
                assert isinstance(dep, cfg_ir.Branch)
                for param, arg in zip(dep.block.parameters, dep.arguments):
                    def_dependencies[param].add(arg)

    # Figure out which definitions are live.
    live_defs = set()
    def __mark_live(definition):
        if definition in live_defs:
            return

        live_defs.add(definition)
        if definition in def_dependencies:
            for dep in def_dependencies[definition]:
                __mark_live(dep)

    for root in root_defs:
        __mark_live(root)

    # Remove all dead definitions.
    dead_defs = set.difference(set(def_dependencies.keys()), live_defs)
    dead_phis = set()
    for dead_def in dead_defs:
        if isinstance(dead_def.value, cfg_ir.BlockParameter):
            dead_phis.add(dead_def)
        else:
            dead_def.block.remove_definition(dead_def)

    erase_parameters(entry_point, dead_phis)

def eliminate_trivial_phis(entry_point):
    """Eliminates trivial block parameters, i.e., block parameters which are really
       aliases."""
    phi_values = defaultdict(set)
    all_blocks = list(cfg_ir.get_all_blocks(entry_point))
    for block in all_blocks:
        for branch in block.flow.branches():
            for phi, arg in zip(branch.block.parameters, branch.arguments):
                phi_values[phi].add(arg)

    replacements = []
    for block in all_blocks:
        block_parameters = list(block.parameters)
        for parameter_def in block_parameters:
            trivial_phi_val = cfg_ir.get_trivial_phi_value(
                parameter_def, phi_values[parameter_def])
            if trivial_phi_val is not None:
                replacements.append((parameter_def, trivial_phi_val))

    erase_parameters(entry_point, set([parameter_def for parameter_def, _ in replacements]))

    for parameter_def, trivial_phi_val in replacements:
        block = parameter_def.block
        parameter_def.redefine(trivial_phi_val)
        block.prepend_definition(parameter_def)

def erase_parameters(entry_point, parameters_to_erase):
    """Erases all arguments for the given set of parameters, and then takes out the
       parameters themselves."""
    for block in cfg_ir.get_all_blocks(entry_point):
        for branch in block.flow.branches():
            new_arg_list = []
            for parameter, arg in zip(branch.block.parameters, branch.arguments):
                if parameter not in parameters_to_erase:
                    new_arg_list.append(arg)
            branch.arguments = new_arg_list

    for parameter_def in parameters_to_erase:
        parameter_def.block.remove_parameter(parameter_def)

def apply_cfg_intrinsic(intrinsic_function, original_definition, named_args):
    """Applies the given intrinsic to the given sequence of named arguments."""
    kwargs = dict(named_args)
    kwargs['original_def'] = original_definition
    return intrinsic_function(**kwargs)

def try_redefine_as_direct_call(definition, jit, called_globals):
    """Tries to redefine the given indirect call definition as a direct call."""
    call = cfg_ir.get_def_value(definition)
    if not isinstance(call, cfg_ir.IndirectFunctionCall):
        return

    target = cfg_ir.get_def_value(call.target)
    if isinstance(target, cfg_ir.LoadPointer):
        loaded_ptr = cfg_ir.get_def_value(target.pointer)
        if isinstance(loaded_ptr, cfg_ir.ResolveGlobal):
            resolved_var_name = loaded_ptr.variable.name
            called_globals.add(loaded_ptr)

            # Try to resolve the callee as an intrinsic.
            intrinsic = jit.get_cfg_intrinsic(resolved_var_name)
            if intrinsic is not None:
                definition.redefine(
                    cfg_ir.DirectFunctionCall(
                        resolved_var_name, call.argument_list,
                        cfg_ir.JIT_CFG_INTRINSIC_CALLING_CONVENTION))
            else:
                # Otherwise, build a thunk.
                thunk_name = jit.jit_thunk_global(resolved_var_name)
                calling_convention = (
                    cfg_ir.JIT_NO_GC_CALLING_CONVENTION
                    if jit.get_intrinsic(thunk_name) is not None
                    else cfg_ir.JIT_CALLING_CONVENTION)
                definition.redefine(
                    cfg_ir.DirectFunctionCall(
                        thunk_name, call.argument_list, calling_convention))
                called_globals.add(loaded_ptr)
    elif isinstance(target, cfg_ir.Literal):
        node_id = target.literal
        thunk_name = jit.jit_thunk_constant_function(node_id)
        definition.redefine(
            cfg_ir.DirectFunctionCall(
                thunk_name, call.argument_list, cfg_ir.JIT_CALLING_CONVENTION))

def get_checked_global(definition):
    """If the definition is a check that tests if a global does not exist, then
       the instruction that resolves the global is returned; otherwise None."""
    def_value = cfg_ir.get_def_value(definition)
    if not isinstance(def_value, cfg_ir.Binary):
        return None

    if def_value.operator != 'is':
        return None

    def __get_checked_global_single_dir(lhs, rhs):
        if (isinstance(lhs, cfg_ir.ResolveGlobal)
                and isinstance(rhs, cfg_ir.Literal)
                and rhs.literal is None):
            return lhs
        else:
            return None

    bin_lhs = cfg_ir.get_def_value(def_value.lhs)
    bin_rhs = cfg_ir.get_def_value(def_value.rhs)
    result = __get_checked_global_single_dir(bin_lhs, bin_rhs)
    if result is None:
        result = __get_checked_global_single_dir(bin_rhs, bin_lhs)

    return result

def optimize_calls(entry_point, jit):
    """Converts indirect calls to direct calls in the control-flow graph defined by the
       given entry point."""
    called_globals = set()
    global_exists_defs = defaultdict(list)
    for block in cfg_ir.get_all_blocks(entry_point):
        for definition in block.definitions:
            checked_global = get_checked_global(definition)
            if checked_global is not None:
                global_exists_defs[checked_global].append(definition)
            else:
                try_redefine_as_direct_call(definition, jit, called_globals)

    for resolve_global in called_globals:
        for exists_def in global_exists_defs[resolve_global]:
            exists_def.redefine(cfg_ir.Literal(False))

def expand_cfg_intrinsics(entry_point, jit):
    """Expands CFG JIT intrinsics in the control-flow graph defined by the given entry point."""
    for block in cfg_ir.get_all_blocks(entry_point):
        for definition in block.definitions:
            def_value = definition.value
            if (isinstance(def_value, cfg_ir.DirectFunctionCall)
                    and def_value.calling_convention ==
                    cfg_ir.JIT_CFG_INTRINSIC_CALLING_CONVENTION):
                intrinsic = jit.get_cfg_intrinsic(def_value.target_name)
                apply_cfg_intrinsic(intrinsic, definition, def_value.argument_list)

def simplify_values(entry_point):
    """Simplifies values in the control-flow graph defined by the given entry point."""
    for block in cfg_ir.get_all_blocks(entry_point):
        for definition in block.definitions:
            def_val = cfg_ir.get_def_value(definition)
            if isinstance(def_val, cfg_ir.Read):
                read_node = cfg_ir.get_def_value(def_val.node)
                if isinstance(read_node, cfg_ir.CreateNode):
                    definition.redefine(read_node.value)
            elif isinstance(def_val, cfg_ir.Binary):
                lhs = cfg_ir.get_def_value(def_val.lhs)
                rhs = cfg_ir.get_def_value(def_val.rhs)
                if isinstance(lhs, cfg_ir.Literal) and isinstance(rhs, cfg_ir.Literal):
                    definition.redefine(
                        cfg_ir.Literal(
                            eval('%r %s %r' % (lhs.literal, def_val.operator, rhs.literal))))
            elif isinstance(def_val, cfg_ir.Unary):
                operand = cfg_ir.get_def_value(def_val.operand)
                if isinstance(operand, cfg_ir.Literal):
                    definition.redefine(
                        cfg_ir.Literal(
                            eval('%s %r' % (def_val.operator, operand.literal))))

def inline_constants(entry_point):
    """Replaces reads of constant nodes by the literals they contain."""
    for block in cfg_ir.get_all_blocks(entry_point):
        for definition in block.definitions:
            def_val = cfg_ir.get_def_value(definition)
            if isinstance(def_val, cfg_ir.Read):
                read_node = cfg_ir.get_def_value(def_val.node)
                if isinstance(read_node, cfg_ir.Literal):
                    val, = yield [("RV", [read_node.literal])]
                    definition.redefine(cfg_ir.Literal(val))

def expand_indirect_definitions(entry_point):
    """Replaces indirect definitions by the values referred to by those definitions."""
    def __expand_indirect_defs(value):
        dependencies = value.get_dependencies()
        if len(dependencies) == 0:
            return value
        else:
            new_dependencies = []
            for dep in dependencies:
                new_dep = dep
                if isinstance(new_dep, cfg_ir.Definition):
                    while isinstance(new_dep.value, cfg_ir.Definition):
                        new_dep = new_dep.value
                else:
                    new_dep = __expand_indirect_defs(new_dep)

                new_dependencies.append(new_dep)
            return value.create(new_dependencies)

    for block in cfg_ir.get_all_blocks(entry_point):
        block_definitions = list(block.definitions)
        for definition in block_definitions:
            if isinstance(definition.value, cfg_ir.Definition):
                block.remove_definition(definition)
            else:
                definition.redefine(
                    __expand_indirect_defs(definition.value))

        block.flow = __expand_indirect_defs(block.flow)

def optimize_reads(entry_point):
    """Tries to replace repeated reads by a single read."""
    cfg_ir.match_and_rewrite(
        entry_point,
        lambda _: True,
        lambda use_def, _: cfg_ir.is_value_def(use_def, cfg_ir.Read),
        lambda def_def:
        def_def.redefine(
            cfg_ir.Read(def_def.insert_before(def_def.value))),
        lambda use_def, def_def: use_def.redefine(def_def))

def protect_from_gc(entry_point):
    """Protects locals in the control-flow graph defined by the given
       entry point from the GC."""
    root_node = entry_point.prepend_definition(cfg_ir.AllocateRootNode())
    def protect_def_from_gc(definition):
        """Protects the given definition from the GC."""
        definition.insert_after(cfg_ir.create_gc_protect(definition, root_node))

    def maybe_protect_def_from_gc(definition):
        """Protects the given definition from the GC, if its result is not None."""
        definition.insert_after(cfg_ir.create_conditional_gc_protect(definition, root_node))

    for block in cfg_ir.get_all_blocks(entry_point):
        for definition in block.definitions:
            def_value = cfg_ir.get_def_value(definition)
            if isinstance(def_value, cfg_ir.CreateNode):
                protect_def_from_gc(definition)
            elif (isinstance(def_value, cfg_ir.IndirectFunctionCall)
                  or (isinstance(def_value, cfg_ir.DirectFunctionCall)
                      and (def_value.calling_convention == cfg_ir.JIT_CALLING_CONVENTION
                           or def_value.calling_convention == cfg_ir.JIT_NO_GC_CALLING_CONVENTION
                           or def_value.calling_convention == cfg_ir.MACRO_IO_CALLING_CONVENTION)
                      and def_value.has_value())):
                maybe_protect_def_from_gc(definition)

        if isinstance(block.flow, (cfg_ir.ReturnFlow, cfg_ir.ThrowFlow)):
            block.append_definition(cfg_ir.DeallocateRootNode(root_node))

def elide_gc_protects(entry_point):
    """Tries to elide GC protection values."""
    # We don't need to protect a value from the GC if it is used for the
    # last time _before_ the GC has an opportunity to kick in. To simplify
    # things, we'll do a quick block-based analysis.
    def __may_cause_gc(definition):
        def_value = cfg_ir.get_def_value(definition)
        if isinstance(def_value, cfg_ir.IndirectFunctionCall):
            return True
        elif (isinstance(def_value, cfg_ir.DirectFunctionCall)
              and (def_value.calling_convention == cfg_ir.JIT_CALLING_CONVENTION
                   or def_value.calling_convention == cfg_ir.MACRO_IO_CALLING_CONVENTION)):
            return True
        else:
            return False

    def __get_protected_def(def_or_value):
        value = cfg_ir.get_def_value(def_or_value)
        if cfg_ir.is_call(
                value, target_name=cfg_ir.GC_PROTECT_MACRO_NAME,
                calling_convention=cfg_ir.MACRO_POSITIONAL_CALLING_CONVENTION):
            _, protected_def = value.argument_list[0]
            return protected_def
        elif cfg_ir.is_call(
                value, target_name=cfg_ir.MAYBE_GC_PROTECT_MACRO_NAME,
                calling_convention=cfg_ir.MACRO_POSITIONAL_CALLING_CONVENTION):
            _, protected_def = value.argument_list[1]
            return protected_def
        else:
            return None

    def_blocks = {}
    def __register_def_or_use(definition, block):
        if definition in def_blocks and def_blocks[definition] != block:
            # Definition seems to be used across basic blocks.
            ineligible_defs.add(definition)

        def_blocks[definition] = block

    ineligible_defs = set()
    def_protections = defaultdict(list)
    for block in cfg_ir.get_all_blocks(entry_point):
        no_gc_defs = set()
        block_defs = set()
        first_gc = {}
        last_def_uses = {}
        for i, definition in enumerate(block.definitions):
            if isinstance(definition.value, cfg_ir.Definition):
                # Handling definitions of definitions is complicated and they should already have
                # been expanded at this point. Just mark them as ineligible.
                ineligible_defs.add(definition)
                ineligible_defs.add(definition.value)
                continue

            protected_def = __get_protected_def(definition)
            if protected_def is not None:
                # We just ran into a gc_protect/maybe_gc_protect.
                def_protections[protected_def].append(definition)
                continue

            block_defs.add(definition)
            __register_def_or_use(definition, block)

            for dependency in definition.get_all_dependencies():
                __register_def_or_use(dependency, block)
                last_def_uses[dependency] = i

            if __may_cause_gc(definition):
                for gc_def in no_gc_defs:
                    first_gc[gc_def] = i
                no_gc_defs = set()

            no_gc_defs.add(definition)

        # Mark all branch arguments as ineligible.
        for branch in block.flow.branches():
            ineligible_defs.update(branch.arguments)

        for dependency in block.flow.get_dependencies():
            last_def_uses[dependency] = None

        for definition in block_defs:
            if definition in ineligible_defs:
                # Definition was already ineligible.
                continue

            # Mark `definition` as ineligible if there is a GC definition in the range of
            # definitions (definition, last_def_uses[definition]].
            if definition in first_gc:
                if definition in last_def_uses:
                    last_use = last_def_uses[definition]
                    if last_use is None or first_gc[definition] <= last_use:
                        ineligible_defs.add(definition)

    # Elide all GC protections for definitions which are not in the `ineligible_defs` set.
    for protected, protections in def_protections.items():
        if protected not in ineligible_defs:
            for protect_def in protections:
                protect_def.redefine(cfg_ir.Literal(None))

def optimize(entry_point, jit):
    """Optimizes the control-flow graph defined by the given entry point.
       A potentially altered entry point is returned."""
    optimize_graph_flow(entry_point)
    elide_local_checks(entry_point)
    optimize_graph_flow(entry_point)
    merge_blocks(entry_point)
    eliminate_trivial_phis(entry_point)
    entry_point = cfg_ssa_construction.construct_ssa_form(entry_point)
    if jit.direct_calls_allowed:
        optimize_calls(entry_point, jit)
    cfg_data_structures.optimize_data_structures(entry_point)
    expand_cfg_intrinsics(entry_point, jit)
    yield [("CALL_ARGS", [inline_constants, (entry_point,)])]
    optimize_reads(entry_point)
    simplify_values(entry_point)
    eliminate_unused_definitions(entry_point)
    optimize_graph_flow(entry_point)
    expand_indirect_definitions(entry_point)
    eliminate_unused_definitions(entry_point)
    merge_blocks(entry_point)
    expand_indirect_definitions(entry_point)
    protect_from_gc(entry_point)
    elide_gc_protects(entry_point)
    eliminate_unused_definitions(entry_point)
    raise primitive_functions.PrimitiveFinished(entry_point)
