from __future__ import annotations

from collections.abc import Iterable

from .decoder import H8536Decoder
from .formatting import label_for
from .model import Instruction
from .tables import FLOW_STOP


def trace(decoder: H8536Decoder, starts: Iterable[int], start_limit: int, end_limit: int) -> dict[int, Instruction]:
    decoded: dict[int, Instruction] = {}
    queue = [(addr, decoder.br) for addr in starts if start_limit <= addr < end_limit]
    seen_starts: set[tuple[int, int | None]] = set()

    while queue:
        pc, br = queue.pop(0)
        if (pc, br) in seen_starts:
            continue
        seen_starts.add((pc, br))

        while start_limit <= pc < end_limit:
            decoder.br = br
            if pc in decoded:
                break
            ins = decoder.decode(pc)
            decoded[pc] = ins
            next_br = ins.br_value if ins.writes_br else br
            for target in ins.targets:
                if start_limit <= target < end_limit and (target, next_br) not in seen_starts:
                    queue.append((target, next_br))
            if ins.kind in FLOW_STOP or not ins.fallthrough:
                break
            pc = (pc + max(ins.size, 1)) & 0xFFFF
            br = next_br

    return decoded


def linear_sweep(decoder: H8536Decoder, start: int, end: int) -> dict[int, Instruction]:
    decoded: dict[int, Instruction] = {}
    pc = start
    br = decoder.br
    while pc < end:
        decoder.br = br
        ins = decoder.decode(pc)
        decoded[pc] = ins
        if ins.writes_br:
            br = ins.br_value
        pc += max(ins.size, 1)
    return decoded


def collect_labels(instructions: Iterable[Instruction], vectors: dict[int, tuple[str, int]]) -> dict[int, str]:
    labels: dict[int, str] = {}
    for _vector_addr, (name, target) in vectors.items():
        labels.setdefault(target, f"vec_{name}_{target:04X}")
    for ins in instructions:
        for target in ins.targets:
            labels.setdefault(target, label_for(target))
    return labels


def collect_function_entries(
    instructions: Iterable[Instruction],
    vectors: dict[int, tuple[str, int]],
) -> set[int]:
    entries = {target for _name, target in vectors.values()}
    for ins in instructions:
        if ins.kind == "call":
            entries.update(ins.targets)
    return entries


def assign_functions(instructions: dict[int, Instruction], entries: set[int]) -> dict[int, int]:
    owners: dict[int, int] = {}
    current: int | None = None
    for address in sorted(instructions):
        if address in entries:
            current = address
        if current is not None:
            owners[address] = current
        if instructions[address].kind in {"return", "rte", "sleep"}:
            current = None
    return owners


def build_functions(
    instructions: dict[int, Instruction],
    vectors: dict[int, tuple[str, int]],
    labels: dict[int, str],
) -> list[dict[str, object]]:
    entries = collect_function_entries(instructions.values(), vectors)
    owners = assign_functions(instructions, entries)
    vector_sources: dict[int, list[str]] = {}
    for _vector_addr, (name, target) in vectors.items():
        vector_sources.setdefault(target, []).append(name)

    functions: dict[int, dict[str, object]] = {}
    for address, owner in owners.items():
        ins = instructions[address]
        function = functions.setdefault(
            owner,
            {
                "start": owner,
                "label": labels.get(owner, label_for(owner)),
                "sources": vector_sources.get(owner, []),
                "instruction_count": 0,
                "end": owner,
                "calls": [],
                "unresolved_calls": 0,
            },
        )
        function["instruction_count"] = int(function["instruction_count"]) + 1
        function["end"] = max(int(function["end"]), ins.address + max(ins.size, 1) - 1)
        if ins.kind == "call":
            if ins.targets:
                calls = function["calls"]
                assert isinstance(calls, list)
                for target in ins.targets:
                    if target not in calls:
                        calls.append(target)
            else:
                function["unresolved_calls"] = int(function["unresolved_calls"]) + 1

    return [functions[start] for start in sorted(functions)]


def build_call_graph(
    instructions: dict[int, Instruction],
    vectors: dict[int, tuple[str, int]],
    labels: dict[int, str],
) -> dict[str, object]:
    entries = collect_function_entries(instructions.values(), vectors)
    owners = assign_functions(instructions, entries)
    nodes = build_functions(instructions, vectors, labels)
    edges: list[dict[str, object]] = []
    seen: set[tuple[int, int]] = set()

    for ins in instructions.values():
        if ins.kind != "call" or not ins.targets:
            continue
        source = owners.get(ins.address)
        if source is None:
            continue
        for target in ins.targets:
            key = (source, target)
            if key in seen:
                continue
            seen.add(key)
            edges.append(
                {
                    "from": source,
                    "from_label": labels.get(source, label_for(source)),
                    "to": target,
                    "to_label": labels.get(target, label_for(target)),
                    "call_site": ins.address,
                },
            )

    return {"nodes": nodes, "edges": sorted(edges, key=lambda edge: (edge["from"], edge["to"]))}
