from __future__ import annotations

from .formatting import h16, label_for
from .model import Instruction


def summarize_timing(
    instructions: dict[int, Instruction],
    labels: dict[int, str] | None = None,
    call_graph: dict[str, object] | None = None,
) -> dict[str, list[dict[str, object]]]:
    labels = labels or {}
    addresses = sorted(instructions)
    if not addresses:
        return {"blocks": [], "loops": []}

    starts = _block_starts(instructions, addresses, labels, call_graph)
    blocks = [_summarize_block(start, instructions, labels, starts) for start in sorted(starts)]
    loops = _summarize_loops(instructions, labels)
    return {
        "blocks": [block for block in blocks if block["instruction_count"]],
        "loops": loops,
    }


def cycle_bounds(cycles: dict[str, object] | None) -> tuple[int, int] | None:
    if not cycles:
        return None
    if "cycles" in cycles:
        value = int(cycles["cycles"])
        return value, value
    if "cycles_min" in cycles and "cycles_max" in cycles:
        return int(cycles["cycles_min"]), int(cycles["cycles_max"])
    if "not_taken" in cycles and "taken" in cycles:
        values = [int(cycles["not_taken"]), int(cycles["taken"])]
        return min(values), max(values)
    if "trap_not_taken" in cycles and "trap_taken" in cycles:
        values = [int(cycles["trap_not_taken"]), int(cycles["trap_taken"])]
        return min(values), max(values)
    if "false" in cycles and "count_minus_1" in cycles and "taken" in cycles:
        values = [int(cycles["false"]), int(cycles["count_minus_1"]), int(cycles["taken"])]
        return min(values), max(values)
    return None


def _block_starts(
    instructions: dict[int, Instruction],
    addresses: list[int],
    labels: dict[int, str],
    call_graph: dict[str, object] | None,
) -> set[int]:
    address_set = set(addresses)
    starts = {addresses[0]}
    starts.update(address for address in labels if address in address_set)

    for node in (call_graph or {}).get("nodes", []):
        start = int(node["start"])
        if start in address_set:
            starts.add(start)

    for ins in instructions.values():
        starts.update(target for target in ins.targets if target in address_set)
        if ins.kind in {"branch", "jump", "return", "rte", "sleep", "invalid"}:
            next_address = ins.address + max(ins.size, 1)
            if next_address in address_set:
                starts.add(next_address)
    return starts


def _summarize_block(
    start: int,
    instructions: dict[int, Instruction],
    labels: dict[int, str],
    starts: set[int],
) -> dict[str, object]:
    addresses = []
    pc = start
    cycles_min = 0
    cycles_max = 0
    unknown_cycles = 0
    terminator: Instruction | None = None

    while pc in instructions:
        if addresses and pc in starts:
            break
        ins = instructions[pc]
        addresses.append(pc)
        bounds = cycle_bounds(ins.cycles)
        if bounds is None:
            unknown_cycles += 1
        else:
            cycles_min += bounds[0]
            cycles_max += bounds[1]
        terminator = ins
        next_pc = pc + max(ins.size, 1)
        if ins.kind in {"branch", "jump", "return", "rte", "sleep", "invalid"}:
            break
        if next_pc in starts:
            break
        pc = next_pc

    end = addresses[-1] if addresses else start
    return {
        "start": start,
        "end": end,
        "label": labels.get(start, label_for(start)),
        "instruction_count": len(addresses),
        "cycles_min": cycles_min,
        "cycles_max": cycles_max,
        "unknown_cycles": unknown_cycles,
        "terminator": terminator.text if terminator else "",
        "targets": list(terminator.targets if terminator else []),
    }


def _summarize_loops(
    instructions: dict[int, Instruction],
    labels: dict[int, str],
) -> list[dict[str, object]]:
    loops: list[dict[str, object]] = []
    for ins in sorted(instructions.values(), key=lambda item: item.address):
        for target in ins.targets:
            if target > ins.address or target not in instructions:
                continue
            body = [
                address
                for address in sorted(instructions)
                if target <= address <= ins.address
            ]
            if not body:
                continue
            cycles_min = 0
            cycles_max = 0
            unknown_cycles = 0
            has_call = False
            for address in body:
                body_ins = instructions[address]
                has_call = has_call or body_ins.kind == "call"
                bounds = cycle_bounds(body_ins.cycles)
                if bounds is None:
                    unknown_cycles += 1
                else:
                    cycles_min += bounds[0]
                    cycles_max += bounds[1]
            loops.append(
                {
                    "start": target,
                    "end": ins.address,
                    "label": labels.get(target, label_for(target)),
                    "back_edge": ins.address,
                    "back_edge_text": ins.text,
                    "instruction_count": len(body),
                    "cycles_min": cycles_min,
                    "cycles_max": cycles_max,
                    "unknown_cycles": unknown_cycles,
                    "has_call": has_call,
                    "kind": _loop_kind(ins, has_call),
                },
            )
    return loops


def _loop_kind(instruction: Instruction, has_call: bool) -> str:
    if instruction.mnemonic.startswith("SCB/"):
        return "counter_delay_loop" if not has_call else "counter_loop"
    if instruction.mnemonic in {"BRA", "BRN"}:
        return "unconditional_loop" if not has_call else "loop_with_call"
    return "delay_loop_candidate" if not has_call else "loop_with_call"


def format_timing_summary(summary: dict[str, list[dict[str, object]]], *, max_items: int = 40) -> list[str]:
    lines: list[str] = []
    blocks = summary.get("blocks", [])
    loops = summary.get("loops", [])
    if not blocks and not loops:
        return lines

    lines.append("; Timing Summary")
    if blocks:
        lines.append("; Straight-line blocks")
        for block in blocks[:max_items]:
            lines.append(
                "; block "
                f"{h16(int(block['start']))}-{h16(int(block['end']))} "
                f"{str(block['label']):<20} "
                f"ins={block['instruction_count']:<3} "
                f"cycles={_cycle_range(block)} "
                f"unknown={block['unknown_cycles']}",
            )
        if len(blocks) > max_items:
            lines.append(f"; ... {len(blocks) - max_items} more blocks")
    if loops:
        lines.append("; Backward-branch loop candidates")
        for loop in loops[:max_items]:
            lines.append(
                "; loop "
                f"{h16(int(loop['start']))}-{h16(int(loop['end']))} "
                f"{str(loop['label']):<20} "
                f"{loop['kind']:<22} "
                f"cycles/iteration={_cycle_range(loop)} "
                f"back_edge={h16(int(loop['back_edge']))}",
            )
        if len(loops) > max_items:
            lines.append(f"; ... {len(loops) - max_items} more loops")
    lines.append("")
    return lines


def _cycle_range(item: dict[str, object]) -> str:
    minimum = int(item["cycles_min"])
    maximum = int(item["cycles_max"])
    if minimum == maximum:
        return str(minimum)
    return f"{minimum}-{maximum}"
