import syntax
import colorsys
from cfg.CFG_Node import CFG_DIAMOND


def _expr_used_names(expr) -> set[str]:
    """Collect variable names (syntax.ID) used inside an expression subtree."""
    used: set[str] = set()

    def visit(node):
        if node is None:
            return
        if isinstance(node, syntax.ID):
            used.add(node.name)
            return
        if isinstance(node, syntax.EXPRESSION):
            for _, child in node.children():
                visit(child)

    visit(expr)
    return used


def _show_analysis_on_node(node) -> bool:
    """Return True if analysis annotations should be displayed for this node."""
    ast = node.ast_node
    if isinstance(node, CFG_DIAMOND):
        return False
    if ast is None:
        return False
    return isinstance(
        ast,
        (
            syntax.ASSIGN,
            syntax.CALL,
            syntax.IF,
            syntax.WHILE,
            syntax.DECL,
            syntax.LET,
            syntax.SEQ,
            syntax.COMP,
            syntax.EQOP,
            syntax.LOP,
        ),
    )


def _lv_in_for_display(node, analysis):
    """Display-level IN set for LV."""
    in_set = set(analysis.in_sets.get(node.id, set()))
    ast_node = node.ast_node
    if isinstance(ast_node, syntax.ASSIGN):
        func = analysis._func_scope.get(node.id)
        rhs_vars = {
            analysis._resolve_var(func, name)
            for name in _expr_used_names(ast_node.expr)
        }
        in_set |= rhs_vars
    return in_set


def _node_color(node_id: int) -> tuple[str, str]:
    """Return (edge_color, fill_color) deterministically for a node id."""
    # Golden-angle hue distribution gives stable, distinct colors.
    hue = ((node_id * 0.6180339887498949) % 1.0)
    edge_rgb = colorsys.hsv_to_rgb(hue, 0.70, 0.82)
    fill_rgb = colorsys.hsv_to_rgb(hue, 0.28, 0.97)

    def to_hex(rgb):
        r, g, b = (int(round(c * 255)) for c in rgb)
        return f"#{r:02x}{g:02x}{b:02x}"

    return to_hex(edge_rgb), to_hex(fill_rgb)


def run_all_analyses(cfg):
    """Run Live Variables and Reached Uses on *cfg*.

    Returns ``(analyses, annotations, ru_edges)`` where:
      • *analyses* is a dict with keys ``"lv"`` and ``"ru"``,
      • *annotations* contains only LivingVariables helper-node labels,
      • *ru_edges* maps definition-node ids to reached use-node ids.
    """
    node_by_id = {n.id: n for n in cfg.nodes()}

    from cfa.live_variables import LiveVariablesAnalysis
    from cfa.reached_uses import ReachedUsesAnalysis

    lv = LiveVariablesAnalysis(cfg)
    ru = ReachedUsesAnalysis(cfg)

    all_ids = set(lv.in_sets.keys()) | set(lv.out_sets.keys())
    annotations = {
        nid: (
            "LivingVariables\\n"
            f"In := {sorted(_lv_in_for_display(node_by_id[nid], lv))}\\n"
            f"Out := {sorted(lv.out_sets.get(nid, set()))}"
        )
        for nid in all_ids
        if lv.in_sets.get(nid, set()) or lv.out_sets.get(nid, set())
        if nid in node_by_id and _show_analysis_on_node(node_by_id[nid])
    }

    return {"lv": lv, "ru": ru}, annotations, ru.reached_uses_by_node()


def analysis_to_dot(
    cfg,
    annotations: dict[int, str],
    analysis_name: str,
    ru_edges: dict[int, list[int]] | None = None,
) -> str:
    """Return a DOT string for *cfg* annotated with analysis results."""
    lines = [
        "digraph CFG {",
        f'  // Analysis: {analysis_name}',
        '  graph [splines=ortho, overlap=false, ranksep=0.7, nodesep=0.45];',
        '  node [fontname="Helvetica"];',
    ]
    color_nodes = set(annotations.keys()) | set((ru_edges or {}).keys())
    node_colors = {nid: _node_color(nid) for nid in color_nodes}

    def emit(node):
        base_label = node.dot_label() or ""
        shape = node.dot_shape
        style = node.dot_style
        style_str = f", {style}" if style else ""
        lines.append(f'  n{node.id} [label="{base_label}", shape={shape}{style_str}];')

        if node.id in annotations:
            ann_id = f"a{node.id}"
            ann_label = annotations[node.id].replace('"', '\\"')
            edge_color, fill_color = node_colors.get(node.id, ("#1f77b4", "#d9ecff"))
            lines.append(
                f'  {ann_id} [label="{ann_label}", shape=note, '
                f'style="filled", fillcolor="{fill_color}", color="{edge_color}", '
                f'fontcolor="{edge_color}"];'
            )
            lines.append(
                f'  {ann_id} -> n{node.id} [style=dotted, arrowhead=none, '
                f'color="{edge_color}"];'
            )

        for i, child in enumerate(sorted(node.children, key=lambda n: n.id)):
            edge_label = ""
            if isinstance(node, CFG_DIAMOND):
                if i == 0:
                    edge_label = ' [label="T"]'
                elif i == 1:
                    edge_label = ' [label="F"]'
            lines.append(f"  n{node.id} -> n{child.id}{edge_label};")

    cfg.traverse(emit, start=cfg.START)

    if ru_edges:
        for idx, def_id in enumerate(sorted(ru_edges)):
            use_ids = sorted(set(ru_edges[def_id]))
            if not use_ids:
                continue

            # One routing hub per definition node to mimic UML-like
            # "out to the side, then down/across to targets" connectors.
            side = "e" if idx % 2 == 0 else "w"
            source_port = "se" if side == "e" else "sw"
            hub_id = f"rh{def_id}"
            edge_color, fill_color = node_colors.get(def_id, ("#1f77b4", "#d9ecff"))

            lines.append(
                f'  {hub_id} [shape=point, width=0.05, height=0.05, '
                f'color="{edge_color}", fillcolor="{edge_color}", style=filled];'
            )
            lines.append(f"  {{ rank=same; n{def_id}; {hub_id}; }}")
            lines.append(
                f'  n{def_id}:{source_port} -> {hub_id} [color="{edge_color}", '
                f'style=dashed, penwidth=1.2, arrowhead=none, constraint=false, '
                f'tailclip=true, headclip=true];'
            )

            for use_id in use_ids:
                if side == "e":
                    target_port = "ne" if (use_id % 2 == 0) else "se"
                else:
                    target_port = "nw" if (use_id % 2 == 0) else "sw"
                lines.append(
                    f'  {hub_id} -> n{use_id}:{target_port} [color="{edge_color}", '
                    f'fontcolor="{edge_color}", fontsize=8, style=dashed, '
                    f'penwidth=1.0, arrowsize=0.6, constraint=false, '
                    f'tailclip=true, headclip=true];'
                )

    lines.append("}")
    return "\n".join(lines)
