#!/usr/bin/env python3
"""Generate diagrams for PYTANIE 9 and PYTANIE 12.

  PYTANIE 9:  Processes & Threads
    (IPC mechanisms, deadlock, producer-consumer)
  PYTANIE 12: Network optimization models
    (Ford-Fulkerson, Hungarian, CPM, Kruskal, TSP, Min-cost).

All: A4-compatible, B&W, 300 DPI, laser-printer-friendly.
"""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING

import matplotlib as mpl

if TYPE_CHECKING:
    from matplotlib.axes import Axes
    from matplotlib.figure import Figure

mpl.use("Agg")
from pathlib import Path

import matplotlib.patches as mpatches
from matplotlib.patches import FancyBboxPatch
import matplotlib.pyplot as plt
import numpy as np

_logger = logging.getLogger(__name__)

DPI = 300
BG = "white"
LN = "black"
FS = 8
FS_TITLE = 11
FS_SMALL = 6.5
FS_EDGE = 9
OUTPUT_DIR = str(Path(__file__).resolve().parent / "img")
Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)

GRAY1 = "#E8E8E8"
GRAY2 = "#D0D0D0"
GRAY3 = "#B8B8B8"
GRAY4 = "#F5F5F5"
GRAY5 = "#C0C0C0"
LIGHT_GREEN = "#D5E8D4"
LIGHT_RED = "#F8D7DA"
LIGHT_BLUE = "#D6EAF8"
LIGHT_YELLOW = "#FFF9C4"
LIGHT_ORANGE = "#FFE0B2"
_LAST_CONDITION_INDEX = 3
_CENTER_Y = 2.5


def draw_box(
    ax: Axes,
    x: float,
    y: float,
    w: float,
    h: float,
    text: str,
    fill: str = "white",
    lw: float = 1.2,
    fontsize: float = FS,
    fontweight: str = "normal",
    ha: str = "center",
    va: str = "center",
    *,
    rounded: bool = True,
    edgecolor: str = LN,
) -> None:
    """Draw box."""
    if rounded:
        rect = FancyBboxPatch(
            (x, y),
            w,
            h,
            boxstyle="round,pad=0.05",
            lw=lw,
            edgecolor=edgecolor,
            facecolor=fill,
        )
    else:
        rect = mpatches.Rectangle(
            (x, y), w, h, lw=lw, edgecolor=edgecolor, facecolor=fill
        )
    ax.add_patch(rect)
    ax.text(
        x + w / 2,
        y + h / 2,
        text,
        ha=ha,
        va=va,
        fontsize=fontsize,
        fontweight=fontweight,
        wrap=True,
    )


def draw_arrow(
    ax: Axes,
    x1: float,
    y1: float,
    x2: float,
    y2: float,
    lw: float = 1.2,
    style: str = "->",
    color: str = LN,
) -> None:
    """Draw arrow."""
    ax.annotate(
        "",
        xy=(x2, y2),
        xytext=(x1, y1),
        arrowprops={"arrowstyle": style, "color": color, "lw": lw},
    )


def save_fig(fig: Figure, name: str) -> None:
    """Save fig."""
    path = str(Path(OUTPUT_DIR) / name)
    fig.savefig(path, dpi=DPI, bbox_inches="tight", facecolor=BG, pad_inches=0.15)
    plt.close(fig)
    _logger.info("  Saved: %s", path)


# ============================================================
# PYTANIE 9 DIAGRAMS
# ============================================================


def gen_ipc_mechanisms() -> None:
    """IPC mechanisms comparison diagram."""
    fig, ax = plt.subplots(1, 1, figsize=(8, 5))
    ax.set_xlim(0, 10)
    ax.set_ylim(0, 7)
    ax.set_aspect("equal")
    ax.axis("off")
    ax.set_title(
        "Mechanizmy IPC — porównanie", fontsize=FS_TITLE, fontweight="bold", pad=10
    )

    mechanisms = [
        (
            "Pipe",
            "→ jednokierunkowy\n→ bufor w jądrze\n→ spokrewnione procesy",
            "ls | grep txt",
            GRAY1,
        ),
        (
            "Shared\nMemory",
            "→ wspólna ramka RAM\n→ zero kopiowania\n→ wymaga synchronizacji",
            "mmap() / shm_open()",
            LIGHT_GREEN,
        ),
        (
            "Message\nQueue",
            "→ strukturalne wiad.\n→ asynchroniczna\n→ filtrowanie typów",
            "msgsnd() / msgrcv()",
            LIGHT_BLUE,
        ),
        (
            "Socket",
            "→ dwukierunkowy\n→ lokalny lub sieciowy\n→ TCP/UDP",
            "connect() / accept()",
            LIGHT_YELLOW,
        ),
    ]

    for i, (name, desc, example, color) in enumerate(mechanisms):
        x = 0.3
        y = 5.5 - i * 1.5
        # Box for mechanism name
        draw_box(ax, x, y, 1.5, 1.0, name, fill=color, fontsize=9, fontweight="bold")
        # Description
        ax.text(
            x + 2.0,
            y + 0.5,
            desc,
            fontsize=FS,
            va="center",
            ha="left",
            family="monospace",
        )
        # Example
        draw_box(ax, 6.5, y + 0.15, 3.0, 0.7, example, fill=GRAY4, fontsize=FS_SMALL)

    # Draw process boxes for pipe illustration at top
    y_top = 6.3
    ax.text(
        5.0,
        y_top,
        "Proces A  ──bufor jądra──▶  Proces B",
        fontsize=FS,
        ha="center",
        va="center",
        family="monospace",
        bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY1, "edgecolor": GRAY3},
    )

    # Legend
    ax.text(
        0.3,
        0.3,
        "Szybkość: Shared Memory > Pipe ≈ MsgQueue > Socket (sieciowy)",
        fontsize=FS,
        va="center",
        style="italic",
    )

    save_fig(fig, "ipc_mechanisms.png")


def gen_deadlock_illustration() -> None:
    """Deadlock circular wait diagram."""
    fig, ax = plt.subplots(1, 1, figsize=(6, 5))
    ax.set_xlim(0, 8)
    ax.set_ylim(0, 6.5)
    ax.set_aspect("equal")
    ax.axis("off")
    ax.set_title(
        "Zakleszczenie (Deadlock) — cykliczne oczekiwanie",
        fontsize=FS_TITLE,
        fontweight="bold",
        pad=10,
    )

    # Thread boxes
    draw_box(
        ax,
        0.5,
        3.5,
        2.0,
        1.2,
        "Wątek A\n(trzyma Mutex 1)",
        fill=LIGHT_BLUE,
        fontsize=9,
        fontweight="bold",
    )
    draw_box(
        ax,
        5.5,
        3.5,
        2.0,
        1.2,
        "Wątek B\n(trzyma Mutex 2)",
        fill=LIGHT_ORANGE,
        fontsize=9,
        fontweight="bold",
    )

    # Resource boxes
    draw_box(
        ax,
        0.5,
        0.8,
        2.0,
        1.0,
        "Mutex 1\nzablokowany",
        fill=GRAY2,
        fontsize=8,
        fontweight="bold",
    )
    draw_box(
        ax,
        5.5,
        0.8,
        2.0,
        1.0,
        "Mutex 2\nzablokowany",
        fill=GRAY2,
        fontsize=8,
        fontweight="bold",
    )

    # Hold arrows (downward)
    draw_arrow(ax, 1.5, 3.5, 1.5, 1.8, lw=2.0, color="#333333")
    ax.text(0.3, 2.65, "trzyma", fontsize=FS, ha="center", rotation=90, color="#333333")

    draw_arrow(ax, 6.5, 3.5, 6.5, 1.8, lw=2.0, color="#333333")
    ax.text(7.7, 2.65, "trzyma", fontsize=FS, ha="center", rotation=90, color="#333333")

    # Arrows: "waits for" (across, with red)
    draw_arrow(ax, 2.5, 4.3, 5.5, 4.3, lw=2.5, color="#C62828")
    ax.text(
        4.0,
        4.6,
        "czeka na Mutex 2",
        fontsize=FS,
        ha="center",
        color="#C62828",
        fontweight="bold",
    )

    draw_arrow(ax, 5.5, 3.7, 2.5, 3.7, lw=2.5, color="#C62828")
    ax.text(
        4.0,
        3.2,
        "czeka na Mutex 1",
        fontsize=FS,
        ha="center",
        color="#C62828",
        fontweight="bold",
    )

    # Coffman conditions
    conditions = [
        "1. Mutual Exclusion — zasoby wyłączne",
        "2. Hold and Wait — trzymaj + czekaj",
        "3. No Preemption — nie można zabrać siłą",
        "4. Circular Wait — cykl oczekiwania ← złam ten!",
    ]
    for i, cond in enumerate(conditions):
        color_c = "#C62828" if i == _LAST_CONDITION_INDEX else LN
        fw = "bold" if i == _LAST_CONDITION_INDEX else "normal"
        ax.text(
            0.5,
            0.5 - i * 0.25 + 0.2,
            cond,
            fontsize=FS_SMALL,
            color=color_c,
            fontweight=fw,
            va="center",
        )

    save_fig(fig, "deadlock_illustration.png")


def gen_producer_consumer() -> None:
    """Producer-consumer with bounded buffer diagram."""
    fig, ax = plt.subplots(1, 1, figsize=(8, 4.5))
    ax.set_xlim(0, 10)
    ax.set_ylim(0, 5.5)
    ax.set_aspect("equal")
    ax.axis("off")
    ax.set_title(
        "Producent-Konsument z buforem cyklicznym (N=4)",
        fontsize=FS_TITLE,
        fontweight="bold",
        pad=10,
    )

    # Producer
    draw_box(
        ax,
        0.3,
        2.0,
        1.8,
        1.5,
        "Producent\n\nwstaw(elem)\nV(full)\nV(mutex)",
        fill=LIGHT_GREEN,
        fontsize=FS,
        fontweight="bold",
    )

    # Buffer slots
    buf_x = 3.0
    buf_y = 2.5
    slot_w = 1.0
    slot_h = 0.8
    items = ["A", "B", "", ""]
    fills = [LIGHT_BLUE, LIGHT_BLUE, "white", "white"]
    for i, (item, fc) in enumerate(zip(items, fills, strict=False)):
        x = buf_x + i * slot_w
        draw_box(
            ax,
            x,
            buf_y,
            slot_w,
            slot_h,
            item,
            fill=fc,
            fontsize=10,
            fontweight="bold",
            rounded=False,
        )

    ax.text(
        buf_x + 2.0,
        buf_y + slot_h + 0.3,
        "Bufor (N=4)",
        fontsize=9,
        ha="center",
        fontweight="bold",
    )
    ax.text(
        buf_x + 2.0,
        buf_y - 0.3,
        "full=2, empty=2",
        fontsize=FS,
        ha="center",
        family="monospace",
    )

    # Consumer
    draw_box(
        ax,
        7.8,
        2.0,
        1.8,
        1.5,
        "Konsument\n\npobierz()\nV(empty)\nV(mutex)",
        fill=LIGHT_YELLOW,
        fontsize=FS,
        fontweight="bold",
    )

    # Arrows
    draw_arrow(ax, 2.1, 2.75, 3.0, 2.9, lw=1.5)
    draw_arrow(ax, 7.0, 2.9, 7.8, 2.75, lw=1.5)

    # Semaphores
    sems = [
        ("mutex = 1", "wyłączny dostęp do bufora", GRAY2),
        ("empty = 2", "wolne sloty (P = czekaj, V = +1)", LIGHT_GREEN),
        ("full = 2", "pełne sloty (P = czekaj, V = +1)", LIGHT_BLUE),
    ]
    for i, (name, desc, color) in enumerate(sems):
        y = 1.2 - i * 0.45
        draw_box(
            ax,
            3.0,
            y,
            1.5,
            0.35,
            name,
            fill=color,
            fontsize=FS_SMALL,
            fontweight="bold",
        )
        ax.text(4.7, y + 0.17, desc, fontsize=FS_SMALL, va="center")

    # Warning
    ax.text(
        0.3,
        4.8,
        "KOLEJNOŚĆ: P(empty/full) PRZED P(mutex)!  Odwrotnie = DEADLOCK",
        fontsize=FS,
        fontweight="bold",
        color="#C62828",
        bbox={
            "boxstyle": "round,pad=0.2",
            "facecolor": LIGHT_RED,
            "edgecolor": "#C62828",
        },
    )

    save_fig(fig, "producer_consumer.png")


# ============================================================
# PYTANIE 12 DIAGRAMS
# ============================================================


def draw_network_node(
    ax: Axes,
    name: str,
    pos: tuple[float, float],
    color: str = "white",
    fontsize: float = 10,
    r: float = 0.3,
) -> None:
    """Draw a network node (circle)."""
    x, y = pos
    circle = plt.Circle(
        (x, y), r, fill=True, facecolor=color, edgecolor=LN, linewidth=1.5, zorder=5
    )
    ax.add_patch(circle)
    ax.text(
        x,
        y,
        name,
        ha="center",
        va="center",
        fontsize=fontsize,
        fontweight="bold",
        zorder=6,
    )


def draw_network_edge(
    ax: Axes,
    pos1: tuple[float, float],
    pos2: tuple[float, float],
    label: str = "",
    color: str = LN,
    lw: float = 1.5,
    offset: float = 0.0,
    *,
    directed: bool = True,
    r: float = 0.33,
    label_bg: str = "white",
) -> None:
    """Draw a directed edge with label."""
    x1, y1 = pos1
    x2, y2 = pos2
    dx, dy = x2 - x1, y2 - y1
    length = np.sqrt(dx**2 + dy**2)
    if length == 0:
        return
    sx = x1 + r * dx / length
    sy = y1 + r * dy / length
    ex = x2 - r * dx / length
    ey = y2 - r * dy / length

    if directed:
        ax.annotate(
            "",
            xy=(ex, ey),
            xytext=(sx, sy),
            arrowprops={"arrowstyle": "->", "color": color, "lw": lw},
        )
    else:
        ax.plot([sx, ex], [sy, ey], color=color, linewidth=lw, zorder=2)

    if label:
        mx = (x1 + x2) / 2
        my = (y1 + y2) / 2
        perp_x = -dy / length * (0.2 + offset)
        perp_y = dx / length * (0.2 + offset)
        ax.text(
            mx + perp_x,
            my + perp_y,
            str(label),
            ha="center",
            va="center",
            fontsize=FS_EDGE,
            fontweight="bold",
            bbox={
                "boxstyle": "round,pad=0.1",
                "facecolor": label_bg,
                "edgecolor": GRAY3,
                "alpha": 0.95,
            },
            zorder=4,
        )


def gen_ford_fulkerson() -> None:
    """Ford-Fulkerson max flow step-by-step."""
    fig, axes = plt.subplots(2, 2, figsize=(10, 8))
    fig.suptitle(
        "Ford-Fulkerson — Maksymalny przepływ (krok po kroku)",
        fontsize=FS_TITLE,
        fontweight="bold",
    )

    pos = {"s": (0.5, 1.5), "A": (2.5, 2.5), "B": (2.5, 0.5), "t": (4.5, 1.5)}

    steps = [
        {
            "title": "Krok 0: Sieć wejściowa\n(przepustowości)",
            "edges": [
                ("s", "A", "10"),
                ("s", "B", "8"),
                ("A", "t", "6"),
                ("B", "t", "10"),
                ("B", "A", "2"),
            ],
            "flows": {},
            "path": [],
            "note": "Szukamy ścieżki s→...→t",
        },
        {
            "title": "Krok 1: Ścieżka s→A→t\nPrzepływ: +6 (min(10,6))",
            "edges": [
                ("s", "A", "4/10"),
                ("s", "B", "0/8"),
                ("A", "t", "6/6"),
                ("B", "t", "0/10"),
                ("B", "A", "0/2"),
            ],
            "flows": {},
            "path": [("s", "A"), ("A", "t")],
            "note": "Łączny przepływ: 6",
        },
        {
            "title": "Krok 2: Ścieżka s→B→t\nPrzepływ: +8 (min(8,10))",
            "edges": [
                ("s", "A", "4/10"),
                ("s", "B", "8/8"),
                ("A", "t", "6/6"),
                ("B", "t", "8/10"),
                ("B", "A", "0/2"),
            ],
            "flows": {},
            "path": [("s", "B"), ("B", "t")],
            "note": "Łączny przepływ: 14",
        },
        {
            "title": "Krok 3: Brak ścieżki powiększającej\nMAX FLOW = 14",
            "edges": [
                ("s", "A", "4/10"),
                ("s", "B", "8/8"),
                ("A", "t", "6/6"),
                ("B", "t", "8/10"),
                ("B", "A", "0/2"),
            ],
            "flows": {},
            "path": [],
            "note": "Min-cut: {s,A,B}|{t}\nA→t(6)+B→t(10)=16? Nie!\ns→B(8)+A→t(6)=14 ✓",
        },
    ]

    for _idx, (ax, step) in enumerate(zip(axes.flat, steps, strict=False)):
        ax.set_xlim(-0.3, 5.3)
        ax.set_ylim(-0.3, 3.3)
        ax.set_aspect("equal")
        ax.axis("off")
        ax.set_title(step["title"], fontsize=FS, fontweight="bold", pad=5)

        path_set = set(step["path"])

        for e in step["edges"]:
            u, v, label = e
            is_path = (u, v) in path_set
            c = "#C62828" if is_path else LN
            w = 2.5 if is_path else 1.5
            draw_network_edge(ax, pos[u], pos[v], label=label, color=c, lw=w)

        for name, p in pos.items():
            if name == "s":
                c = LIGHT_GREEN
            elif name == "t":
                c = LIGHT_RED
            else:
                c = "white"
            draw_network_node(ax, name, p, color=c)

        ax.text(
            2.5,
            -0.15,
            step["note"],
            fontsize=FS_SMALL,
            ha="center",
            va="center",
            style="italic",
            bbox={"boxstyle": "round,pad=0.15", "facecolor": GRAY4, "edgecolor": GRAY3},
        )

    fig.tight_layout(rect=[0, 0, 1, 0.93])
    save_fig(fig, "ford_fulkerson_example.png")


def gen_hungarian() -> None:
    """Hungarian algorithm step-by-step."""
    fig, axes = plt.subplots(2, 2, figsize=(9, 7))
    fig.suptitle(
        "Algorytm węgierski — Problem przydziału (krok po kroku)",
        fontsize=FS_TITLE,
        fontweight="bold",
    )

    matrices = [
        {
            "title": "Macierz kosztów (wejściowa)",
            "data": [[8, 4, 7], [5, 2, 3], [9, 4, 8]],
            "highlight": [],
            "note": "Minimalizuj łączny koszt przydziału",
        },
        {
            "title": "Krok 1: Redukcja wierszy\n(odejmij min z wiersza)",
            "data": [[4, 0, 3], [3, 0, 1], [5, 0, 4]],
            "highlight": [(0, 1), (1, 1), (2, 1)],
            "note": "min: A=4, B=2, C=4",
        },
        {
            "title": "Krok 2: Redukcja kolumn\n(odejmij min z kolumny)",
            "data": [[1, 0, 2], [0, 0, 0], [2, 0, 3]],
            "highlight": [(1, 0), (0, 1), (1, 1), (2, 1), (1, 2)],
            "note": "min: Z1=3, Z2=0, Z3=1",
        },
        {
            "title": "Krok 3: Optymalne przypisanie\nA→Z2(4), B→Z1(5), C=?",
            "data": [[0, 0, 1], [0, 1, 0], [1, 0, 2]],
            "highlight": [(0, 1), (1, 0), (2, 1)],
            "note": "Optymalne: A→Z1(8) + B→Z3(3) + C→Z2(4) = 15",
        },
    ]

    rows = ["A", "B", "C"]
    cols = ["Z1", "Z2", "Z3"]

    for ax, m in zip(axes.flat, matrices, strict=False):
        ax.set_xlim(-0.5, 4.5)
        ax.set_ylim(-1, 4.5)
        ax.set_aspect("equal")
        ax.axis("off")
        ax.set_title(m["title"], fontsize=FS, fontweight="bold", pad=5)

        # Column headers
        for j, col in enumerate(cols):
            ax.text(
                j + 1.5,
                3.8,
                col,
                ha="center",
                va="center",
                fontsize=9,
                fontweight="bold",
            )

        # Row headers and data
        for i, row in enumerate(rows):
            y = 2.8 - i
            ax.text(
                0.3, y, row, ha="center", va="center", fontsize=9, fontweight="bold"
            )
            for j in range(3):
                val = m["data"][i][j]
                is_zero = val == 0
                is_hl = (i, j) in m["highlight"]
                fc = (
                    LIGHT_GREEN if is_hl else ("white" if not is_zero else LIGHT_YELLOW)
                )
                rect = FancyBboxPatch(
                    (j + 1.0, y - 0.35),
                    1.0,
                    0.7,
                    boxstyle="round,pad=0.05",
                    lw=1.2,
                    edgecolor=LN if not is_hl else "#1B5E20",
                    facecolor=fc,
                )
                ax.add_patch(rect)
                ax.text(
                    j + 1.5,
                    y,
                    str(val),
                    ha="center",
                    va="center",
                    fontsize=10,
                    fontweight="bold" if is_hl else "normal",
                )

        ax.text(
            2.0,
            -0.6,
            m["note"],
            fontsize=FS_SMALL,
            ha="center",
            va="center",
            style="italic",
            bbox={"boxstyle": "round,pad=0.15", "facecolor": GRAY4, "edgecolor": GRAY3},
        )

    fig.tight_layout(rect=[0, 0, 1, 0.93])
    save_fig(fig, "hungarian_example.png")


def gen_cpm() -> None:
    """CPM critical path diagram."""
    fig, ax = plt.subplots(1, 1, figsize=(10, 5))
    ax.set_xlim(-0.5, 12)
    ax.set_ylim(-0.5, 5)
    ax.set_aspect("equal")
    ax.axis("off")
    ax.set_title(
        "CPM — Ścieżka krytyczna projektu IT",
        fontsize=FS_TITLE,
        fontweight="bold",
        pad=10,
    )

    # Task positions: (x, y)
    tasks = {
        "START": (0.5, 2.5),
        "A\n3 tyg": (2.5, 2.5),
        "B\n4 tyg": (5.0, 3.8),
        "C\n5 tyg": (5.0, 1.2),
        "D\n6 tyg": (7.5, 3.8),
        "E\n2 tyg": (9.5, 2.5),
        "F\n1 tyg": (11.5, 2.5),
    }

    # Critical path: START→A→B→D→E→F
    critical = {"START", "A\n3 tyg", "B\n4 tyg", "D\n6 tyg", "E\n2 tyg", "F\n1 tyg"}
    critical_edges = {
        ("START", "A\n3 tyg"),
        ("A\n3 tyg", "B\n4 tyg"),
        ("B\n4 tyg", "D\n6 tyg"),
        ("D\n6 tyg", "E\n2 tyg"),
        ("E\n2 tyg", "F\n1 tyg"),
    }

    edges = [
        ("START", "A\n3 tyg"),
        ("A\n3 tyg", "B\n4 tyg"),
        ("A\n3 tyg", "C\n5 tyg"),
        ("B\n4 tyg", "D\n6 tyg"),
        ("C\n5 tyg", "E\n2 tyg"),
        ("D\n6 tyg", "E\n2 tyg"),
        ("E\n2 tyg", "F\n1 tyg"),
    ]

    # Draw edges
    for u, v in edges:
        is_crit = (u, v) in critical_edges
        c = "#C62828" if is_crit else GRAY3
        w = 2.5 if is_crit else 1.2
        draw_network_edge(ax, tasks[u], tasks[v], color=c, lw=w, r=0.5)

    # Draw nodes
    for name, p in tasks.items():
        is_crit = name in critical
        c = LIGHT_RED if is_crit else LIGHT_BLUE
        r = 0.45
        circle = plt.Circle(
            p,
            r,
            fill=True,
            facecolor=c,
            edgecolor="#C62828" if is_crit else LN,
            linewidth=2.0 if is_crit else 1.2,
            zorder=5,
        )
        ax.add_patch(circle)
        ax.text(
            p[0],
            p[1],
            name,
            ha="center",
            va="center",
            fontsize=7 if "\n" in name else 8,
            fontweight="bold",
            zorder=6,
        )

    # ES/EF labels
    es_ef = [
        ("A\n3 tyg", "ES=0, EF=3"),
        ("B\n4 tyg", "ES=3, EF=7"),
        ("C\n5 tyg", "ES=3, EF=8\nzapas=5"),
        ("D\n6 tyg", "ES=7, EF=13"),
        ("E\n2 tyg", "ES=13, EF=15"),
        ("F\n1 tyg", "ES=15, EF=16"),
    ]
    for name, label in es_ef:
        x, y = tasks[name]
        offset_y = 0.7 if y > _CENTER_Y else -0.7
        ax.text(
            x,
            y + offset_y,
            label,
            ha="center",
            va="center",
            fontsize=FS_SMALL,
            bbox={
                "boxstyle": "round,pad=0.1",
                "facecolor": "white",
                "edgecolor": GRAY3,
                "alpha": 0.95,
            },
        )

    # Legend
    ax.text(
        0.5,
        -0.2,
        "Ścieżka krytyczna: A→B→D→E→F (16 tyg)",
        fontsize=9,
        fontweight="bold",
        color="#C62828",
    )
    ax.text(
        0.5,
        -0.6,
        "C ma 5 tyg zapasu — może się opóźnić bez wpływu na projekt",
        fontsize=FS,
        style="italic",
    )

    save_fig(fig, "cpm_example.png")


def gen_kruskal() -> None:
    """Kruskal MST construction step-by-step."""
    fig, axes = plt.subplots(2, 2, figsize=(9, 8))
    fig.suptitle(
        "Kruskal — budowa MST krok po kroku", fontsize=FS_TITLE, fontweight="bold"
    )

    pos = {"A": (0.5, 2.5), "B": (3.0, 2.5), "C": (3.0, 0.5), "D": (0.5, 0.5)}

    all_edges = [
        ("C", "D", 1),
        ("A", "C", 2),
        ("A", "B", 4),
        ("B", "C", 6),
        ("B", "D", 7),
        ("A", "D", 8),
    ]

    steps = [
        {
            "title": "Graf wejściowy\n(6 krawędzi)",
            "mst": [],
            "consider": None,
            "note": "Posortowane: CD(1), AC(2), AB(4), BC(6), BD(7), AD(8)",
        },
        {
            "title": "Krok 1: Dodaj C-D (waga 1)\nNajlżejsza krawędź",
            "mst": [("C", "D", 1)],
            "consider": ("C", "D"),
            "note": "MST = {C-D}, koszt = 1",
        },
        {
            "title": "Krok 2: Dodaj A-C (waga 2)\nA nie w {C,D}",
            "mst": [("C", "D", 1), ("A", "C", 2)],
            "consider": ("A", "C"),
            "note": "MST = {C-D, A-C}, koszt = 3",
        },
        {
            "title": "Krok 3: Dodaj A-B (waga 4)\nB nie w {A,C,D} → KONIEC",
            "mst": [("C", "D", 1), ("A", "C", 2), ("A", "B", 4)],
            "consider": ("A", "B"),
            "note": "MST = {C-D, A-C, A-B}, koszt = 7 ✓",
        },
    ]

    for ax, step in zip(axes.flat, steps, strict=False):
        ax.set_xlim(-0.5, 4.0)
        ax.set_ylim(-0.5, 3.5)
        ax.set_aspect("equal")
        ax.axis("off")
        ax.set_title(step["title"], fontsize=FS, fontweight="bold", pad=5)

        mst_set = {(u, v) for u, v, _ in step["mst"]}

        for u, v, w in all_edges:
            in_mst = (u, v) in mst_set or (v, u) in mst_set
            is_cur = step["consider"] and (
                (u, v) == step["consider"] or (v, u) == step["consider"]
            )
            if is_cur:
                c, lw = "#C62828", 3.0
            elif in_mst:
                c, lw = "#1B5E20", 2.5
            else:
                c, lw = GRAY3, 1.0
            draw_network_edge(
                ax,
                pos[u],
                pos[v],
                label=str(w),
                color=c,
                lw=lw,
                directed=False,
                label_bg=LIGHT_GREEN if in_mst else "white",
            )

        for name, p in pos.items():
            # Check if in current MST component
            in_mst = any(name in (u, v) for u, v, _ in step["mst"])
            c = LIGHT_GREEN if in_mst else "white"
            draw_network_node(ax, name, p, color=c, r=0.3)

        ax.text(
            1.75,
            -0.3,
            step["note"],
            fontsize=FS_SMALL,
            ha="center",
            va="center",
            style="italic",
            bbox={"boxstyle": "round,pad=0.15", "facecolor": GRAY4, "edgecolor": GRAY3},
        )

    fig.tight_layout(rect=[0, 0, 1, 0.93])
    save_fig(fig, "kruskal_example.png")


def gen_tsp() -> None:
    """TSP nearest neighbor heuristic."""
    fig, axes = plt.subplots(1, 2, figsize=(10, 4.5))
    fig.suptitle(
        "TSP — heurystyka Nearest Neighbor (5 miast)",
        fontsize=FS_TITLE,
        fontweight="bold",
    )

    pos = {
        "A": (0.5, 3.0),
        "B": (2.0, 4.0),
        "C": (4.0, 3.5),
        "D": (3.5, 1.0),
        "E": (1.5, 1.5),
    }

    dist = {
        ("A", "B"): 20,
        ("A", "C"): 42,
        ("A", "D"): 35,
        ("A", "E"): 12,
        ("B", "C"): 30,
        ("B", "D"): 34,
        ("B", "E"): 10,
        ("C", "D"): 12,
        ("C", "E"): 40,
        ("D", "E"): 25,
    }

    # Left: full graph with all distances
    ax = axes[0]
    ax.set_xlim(-0.5, 5.0)
    ax.set_ylim(0, 5.0)
    ax.set_aspect("equal")
    ax.axis("off")
    ax.set_title("Graf pełny (odległości)", fontsize=FS, fontweight="bold")

    for (u, v), d in dist.items():
        draw_network_edge(
            ax, pos[u], pos[v], label=str(d), color=GRAY3, lw=0.8, directed=False, r=0.3
        )

    for name, p in pos.items():
        draw_network_node(ax, name, p, color=LIGHT_BLUE, r=0.3)

    # Right: NN solution
    ax = axes[1]
    ax.set_xlim(-0.5, 5.0)
    ax.set_ylim(0, 5.0)
    ax.set_aspect("equal")
    ax.axis("off")
    ax.set_title(
        "Nearest Neighbor (start A)\nTrasa: A→E→B→C→D→A = 99",
        fontsize=FS,
        fontweight="bold",
    )

    nn_path = [
        ("A", "E", 12),
        ("E", "B", 10),
        ("B", "C", 30),
        ("C", "D", 12),
        ("D", "A", 35),
    ]
    colors = ["#C62828", "#1B5E20", "#1565C0", "#E65100", "#4A148C"]

    for i, (u, v, d) in enumerate(nn_path):
        draw_network_edge(
            ax,
            pos[u],
            pos[v],
            label=f"{d}",
            color=colors[i],
            lw=2.0,
            directed=True,
            r=0.3,
        )
        # Step number
        mx = (pos[u][0] + pos[v][0]) / 2
        my = (pos[u][1] + pos[v][1]) / 2
        dx = pos[v][0] - pos[u][0]
        dy = pos[v][1] - pos[u][1]
        length = np.sqrt(dx**2 + dy**2)
        ox = dy / length * 0.45
        oy = -dx / length * 0.45
        ax.text(
            mx + ox,
            my + oy,
            f"#{i + 1}",
            fontsize=FS_SMALL,
            ha="center",
            color=colors[i],
            fontweight="bold",
        )

    for name, p in pos.items():
        c = LIGHT_GREEN if name == "A" else LIGHT_BLUE
        draw_network_node(ax, name, p, color=c, r=0.3)

    fig.tight_layout(rect=[0, 0, 1, 0.9])
    save_fig(fig, "tsp_nearest_neighbor.png")


def gen_min_cost_flow() -> None:
    """Min-cost flow example."""
    fig, axes = plt.subplots(1, 2, figsize=(10, 4))
    fig.suptitle(
        "Minimalny koszt przepływu — transport 10 ton",
        fontsize=FS_TITLE,
        fontweight="bold",
    )

    pos = {"s": (0.5, 1.5), "A": (2.5, 2.5), "B": (2.5, 0.5), "t": (4.5, 1.5)}

    # Left: network with capacities and costs
    ax = axes[0]
    ax.set_xlim(-0.3, 5.3)
    ax.set_ylim(-0.3, 3.3)
    ax.set_aspect("equal")
    ax.axis("off")
    ax.set_title("Sieć (przepustowość, koszt/t)", fontsize=FS, fontweight="bold")

    edges_info = [
        ("s", "A", "(8, 2zł)"),
        ("s", "B", "(5, 4zł)"),
        ("A", "t", "(6, 3zł)"),
        ("B", "t", "(5, 1zł)"),
    ]
    for u, v, label in edges_info:
        draw_network_edge(ax, pos[u], pos[v], label=label, r=0.33)

    for name, p in pos.items():
        c = LIGHT_GREEN if name == "s" else (LIGHT_RED if name == "t" else "white")
        draw_network_node(ax, name, p, color=c)

    # Right: optimal flow
    ax = axes[1]
    ax.set_xlim(-0.3, 5.3)
    ax.set_ylim(-0.3, 3.3)
    ax.set_aspect("equal")
    ax.axis("off")
    ax.set_title("Optymalny przepływ (koszt = 50 zł)", fontsize=FS, fontweight="bold")

    opt_edges = [
        ("s", "A", "5/8", "#1B5E20"),
        ("s", "B", "5/5", "#C62828"),
        ("A", "t", "5/6", "#1B5E20"),
        ("B", "t", "5/5", "#C62828"),
    ]
    for u, v, label, color in opt_edges:
        draw_network_edge(ax, pos[u], pos[v], label=label, color=color, lw=2.0, r=0.33)

    for name, p in pos.items():
        c = LIGHT_GREEN if name == "s" else (LIGHT_RED if name == "t" else "white")
        draw_network_node(ax, name, p, color=c)

    ax.text(
        2.5,
        -0.15,
        "5tx(2+3)=25zł + 5tx(4+1)=25zł = 50zł",
        fontsize=FS,
        ha="center",
        style="italic",
        bbox={"boxstyle": "round,pad=0.15", "facecolor": GRAY4, "edgecolor": GRAY3},
    )

    fig.tight_layout(rect=[0, 0, 1, 0.9])
    save_fig(fig, "min_cost_flow_example.png")


# ============================================================
# MAIN
# ============================================================

if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    _logger.info("Generating PYTANIE 9 diagrams...")
    gen_ipc_mechanisms()
    gen_deadlock_illustration()
    gen_producer_consumer()

    _logger.info("Generating PYTANIE 12 diagrams...")
    gen_ford_fulkerson()
    gen_hungarian()
    gen_cpm()
    gen_kruskal()
    gen_tsp()
    gen_min_cost_flow()

    _logger.info("All diagrams generated successfully!")
