"""Move generator registers that survive suspension onto the generator frame."""

from __future__ import annotations

from mypyc.analysis.dataflow import analyze_live_regs_with_exception_edges, cleanup_cfg
from mypyc.common import TEMP_ATTR_NAME, generator_frame_attribute_prefix
from mypyc.ir.class_ir import ClassIR
from mypyc.ir.func_ir import FuncIR
from mypyc.ir.ops import (
    NO_TRACEBACK_LINE_NO,
    Assign,
    AssignMulti,
    BaseAssign,
    BasicBlock,
    Branch,
    GetAttr,
    LoadAddress,
    LoadErrorValue,
    Op,
    Register,
    SetAttr,
    Value,
)
from mypyc.namegen import exported_name


def promote_generator_registers(ir: FuncIR, frame: ClassIR) -> None:
    """Replace registers live across a suspension with frame attributes.

    This runs before uninitialized-value, exception, and refcount transforms.
    """
    cleanup_cfg(ir.blocks)
    registers = registers_live_across_yield(ir)
    if not registers:
        return

    nullable = nullable_registers(ir.blocks)
    slots = allocate_slots(frame, registers)
    rewrite_registers(ir.blocks, ir.arg_regs[0], slots, nullable)

    # Keep this close to the rewrite: it catches a new op kind that is not
    # handled above without restating the liveness analysis.
    for block in ir.blocks:
        for op in block.ops:
            assert not any(source in slots for source in op.sources())
            assert not (isinstance(op, BaseAssign) and op.dest in slots)


def registers_live_across_yield(ir: FuncIR) -> list[Register]:
    """Return promotable registers live at generator-helper entry."""
    live_in = analyze_live_regs_with_exception_edges(ir.blocks)
    excluded = unpromotable_registers(ir.blocks)
    registers = [
        value
        for value in live_in[ir.blocks[0]]
        if isinstance(value, Register) and not value.is_arg and value not in excluded
    ]
    return order_registers(registers, ir.blocks)


def unpromotable_registers(blocks: list[BasicBlock]) -> set[Register]:
    """Find registers whose representation requires a real C local."""
    result: set[Register] = set()
    for block in blocks:
        for op in block.ops:
            if isinstance(op, LoadAddress) and isinstance(op.src, Register):
                result.add(op.src)
            elif isinstance(op, AssignMulti):
                result.add(op.dest)
            elif (
                isinstance(op, Assign)
                and isinstance(op.src, LoadErrorValue)
                and op.src.undefines
                and op.dest.type.error_overlap
            ):
                result.add(op.dest)
    return result


def nullable_registers(blocks: list[BasicBlock]) -> set[Register]:
    """Find registers for which the error value is an ordinary state."""
    result: set[Register] = set()
    for block in blocks:
        for op in block.ops:
            if (
                isinstance(op, Branch)
                and op.op == Branch.IS_ERROR
                and isinstance(op.value, Register)
            ):
                result.add(op.value)
            elif (
                isinstance(op, Assign)
                and isinstance(op.src, LoadErrorValue)
                and not op.src.undefines
            ):
                result.add(op.dest)
    return result


def order_registers(registers: list[Register], blocks: list[BasicBlock]) -> list[Register]:
    """Order registers by first appearance for deterministic frame layouts."""
    positions: dict[Register, int] = {}
    for block in blocks:
        for op in block.ops:
            values = op.sources()
            if isinstance(op, BaseAssign):
                values = [op.dest, *values]
            for value in values:
                if isinstance(value, Register) and value not in positions:
                    positions[value] = len(positions)
    return sorted(registers, key=positions.__getitem__)


def allocate_slots(frame: ClassIR, registers: list[Register]) -> dict[Register, str]:
    slots: dict[Register, str] = {}
    owner = exported_name(frame.fullname)
    source_prefix = generator_frame_attribute_prefix(
        frame.fullname, is_final_class=frame.is_final_class
    )
    for index, register in enumerate(registers):
        if register.name:
            name = available_attr_name(frame, source_prefix + register.name)
        else:
            name = f"{TEMP_ATTR_NAME}3_{owner}_{index}"
            if register.type.error_overlap:
                # Compiler temporaries are assigned before use. Recording a
                # default avoids allocating an undefinedness bitmap for them.
                frame.attrs_with_defaults.add(name)
        frame.attributes[name] = register.type
        slots[register] = name
    return slots


def available_attr_name(frame: ClassIR, base: str) -> str:
    if not frame.has_attr(base):
        return base
    suffix = 2
    while frame.has_attr(f"{base}__{suffix}"):
        suffix += 1
    return f"{base}__{suffix}"


def rewrite_registers(
    blocks: list[BasicBlock],
    frame_reg: Register,
    slots: dict[Register, str],
    nullable: set[Register],
) -> None:
    """Replace promoted register accesses with generator frame attribute accesses."""
    # IRTransform maps each value to one replacement, but mutable registers need a fresh
    # attribute read at each use, so we can't use IRTransform here.
    for block in blocks:
        old_ops = block.ops
        block.ops = []
        for op in old_ops:
            replacements: dict[Register, GetAttr] = {}
            new_sources: list[Value] = []
            for source in op.sources():
                if isinstance(source, Register) and source in slots:
                    read = replacements.get(source)
                    if read is None:
                        read = GetAttr(
                            frame_reg,
                            slots[source],
                            source_line(op, source),
                            allow_error_value=source in nullable,
                        )
                        block.ops.append(read)
                        replacements[source] = read
                    new_sources.append(read)
                else:
                    new_sources.append(source)
            op.set_sources(new_sources)

            if isinstance(op, Assign) and op.dest in slots:
                block.ops.append(
                    SetAttr(frame_reg, slots[op.dest], op.src, source_line(op, op.dest))
                )
            else:
                block.ops.append(op)


def source_line(op: Op, register: Register) -> int:
    if op.line >= 0 or op.line == NO_TRACEBACK_LINE_NO:
        return op.line
    if register.line >= 0:
        return register.line
    return NO_TRACEBACK_LINE_NO
