#!/usr/bin/env python3
"""Generate ALL diagrams for PYTANIE 9: Procesy i wątki (SOI).

Replaces every ASCII diagram with a monochrome A4-printable PNG (300 DPI).
"""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING

import matplotlib as mpl

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

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

_logger = logging.getLogger(__name__)

DPI = 300
BG = "white"
LN = "black"
FS = 8
FS_TITLE = 11
FS_SMALL = 6.5
FS_LABEL = 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"
OCCUPIED_SLOTS = 2


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,
    linestyle: str = "-",
) -> None:
    """Draw box."""
    if rounded:
        rect = FancyBboxPatch(
            (x, y),
            w,
            h,
            boxstyle="round,pad=0.05",
            lw=lw,
            edgecolor=edgecolor,
            facecolor=fill,
            linestyle=linestyle,
        )
    else:
        rect = mpatches.Rectangle(
            (x, y),
            w,
            h,
            lw=lw,
            edgecolor=edgecolor,
            facecolor=fill,
            linestyle=linestyle,
        )
    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 draw_double_arrow(
    ax: Axes,
    x1: float,
    y1: float,
    x2: float,
    y2: float,
    lw: float = 1.2,
    color: str = LN,
) -> None:
    """Draw double arrow."""
    ax.annotate(
        "",
        xy=(x2, y2),
        xytext=(x1, y1),
        arrowprops={"arrowstyle": "<->", "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)


def draw_table(
    ax: Axes,
    headers: list[str],
    rows: list[list[str]],
    x0: float,
    y0: float,
    col_widths: list[float],
    row_h: float = 0.4,
    header_fill: str = GRAY2,
    row_fills: list[str] | None = None,
    fontsize: float = FS,
    header_fontsize: float | None = None,
) -> None:
    """Draw a clean table on axes."""
    if header_fontsize is None:
        header_fontsize = fontsize
    len(headers)
    len(rows)
    sum(col_widths)

    # Header
    cx = x0
    for j, hdr in enumerate(headers):
        draw_box(
            ax,
            cx,
            y0,
            col_widths[j],
            row_h,
            hdr,
            fill=header_fill,
            fontsize=header_fontsize,
            fontweight="bold",
            rounded=False,
        )
        cx += col_widths[j]

    # Rows
    for i, row in enumerate(rows):
        cy = y0 - (i + 1) * row_h
        cx = x0
        fill = GRAY4 if (i % 2 == 0) else "white"
        if row_fills and i < len(row_fills):
            fill = row_fills[i]
        for j, cell in enumerate(row):
            fw = "bold" if j == 0 else "normal"
            draw_box(
                ax,
                cx,
                cy,
                col_widths[j],
                row_h,
                cell,
                fill=fill,
                fontsize=fontsize,
                fontweight=fw,
                rounded=False,
            )
            cx += col_widths[j]


# ============================================================
# 1. Process vs Thread comparison table
# ============================================================
def gen_process_vs_thread() -> None:
    """Gen process vs thread."""
    fig, ax = plt.subplots(figsize=(7.5, 4.5))
    ax.set_xlim(0, 10)
    ax.set_ylim(-4.5, 1.5)
    ax.set_aspect("auto")
    ax.axis("off")
    ax.set_title(
        "Proces vs Wątek — porównanie", fontsize=FS_TITLE, fontweight="bold", pad=10
    )

    headers = ["Cecha", "Proces", "Wątek"]
    col_w = [2.5, 3.5, 3.5]
    rows = [
        ["Pamięć", "Własna, izolowana", "Współdzielona (heap)"],
        ["Tworzenie", "~1-10 ms", "~10-100 μs (100x szybciej)"],
        ["Przełączanie", "~1-5 μs (TLB flush)", "~0.1-0.5 μs (10x)"],
        ["Komunikacja", "IPC (pipe, socket, shm)", "Bezpośrednia (wspólna pam.)"],
        ["Izolacja", "Pełna — awaria izolowana", "Brak — może zabić proces"],
        ["Zastosowanie", "Bezpieczeństwo, izolacja", "Wydajność, współdzielenie"],
    ]
    draw_table(
        ax,
        headers,
        rows,
        x0=0.25,
        y0=0.8,
        col_widths=col_w,
        row_h=0.55,
        fontsize=7.5,
        header_fontsize=FS_LABEL,
    )

    # Analogy at bottom
    ax.text(
        5.0,
        -4.2,
        "Analogia:  Proces = mieszkanie (własny adres)     "
        "Wątek = pokój w mieszkaniu (wspólna kuchnia = heap)",
        ha="center",
        fontsize=FS,
        style="italic",
        bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
    )

    save_fig(fig, "q9_process_vs_thread.png")


# ============================================================
# 2. Memory segments layout
# ============================================================
def gen_memory_layout() -> None:
    """Gen memory layout."""
    fig, ax = plt.subplots(figsize=(6, 5))
    ax.set_xlim(0, 10)
    ax.set_ylim(0, 8)
    ax.set_aspect("auto")
    ax.axis("off")
    ax.set_title(
        "Segmenty pamięci procesu", fontsize=FS_TITLE, fontweight="bold", pad=10
    )

    segments = [
        ("STACK ↓", "zmienne lokalne, adresy\npowrotu (każdy wątek WŁASNY)", GRAY1),
        ("...", "(wolna przestrzeń)", "white"),
        ("HEAP ↑", "malloc/new — dynamiczna\nalokacja (współdzielony)", GRAY4),
        ("BSS", "zmienne globalne\nniezainicjalizowane (zerowane)", GRAY2),
        ("DATA", "zmienne globalne\nzainicjalizowane", GRAY3),
        ("TEXT", "kod maszynowy\n(read-only, współdzielony)", GRAY5),
    ]
    bx, bw = 2.0, 2.5
    seg_h = 0.9
    gap = 0.05
    top_y = 7.0

    for i, (name, desc, color) in enumerate(segments):
        y = top_y - i * (seg_h + gap)
        draw_box(
            ax,
            bx,
            y,
            bw,
            seg_h,
            name,
            fill=color,
            fontsize=FS_LABEL,
            fontweight="bold",
            rounded=False,
        )
        ax.text(bx + bw + 0.3, y + seg_h / 2, desc, fontsize=7.5, va="center")

    # Address labels
    ax.text(
        bx - 0.2,
        top_y + seg_h / 2,
        "wysoki\nadres",
        fontsize=FS_SMALL,
        va="center",
        ha="right",
        style="italic",
    )
    bottom_y = top_y - 5 * (seg_h + gap)
    ax.text(
        bx - 0.2,
        bottom_y + seg_h / 2,
        "niski\nadres",
        fontsize=FS_SMALL,
        va="center",
        ha="right",
        style="italic",
    )

    # Arrows for growth
    ax.annotate(
        "",
        xy=(bx - 0.5, top_y - 0.1),
        xytext=(bx - 0.5, top_y + seg_h + 0.1),
        arrowprops={"arrowstyle": "->", "lw": 1.5, "color": LN},
    )
    ax.text(bx - 0.9, top_y + 0.4, "rośnie\nw dół", fontsize=FS_SMALL, ha="center")

    heap_y = top_y - 2 * (seg_h + gap)
    ax.annotate(
        "",
        xy=(bx - 0.5, heap_y + seg_h + 0.1),
        xytext=(bx - 0.5, heap_y - 0.1),
        arrowprops={"arrowstyle": "->", "lw": 1.5, "color": LN},
    )
    ax.text(bx - 0.9, heap_y + 0.5, "rośnie\nw górę", fontsize=FS_SMALL, ha="center")

    save_fig(fig, "q9_memory_layout.png")


# ============================================================
# 3. Process states diagram
# ============================================================
def gen_process_states() -> None:
    """Gen process states."""
    fig, ax = plt.subplots(figsize=(7, 3.5))
    ax.set_xlim(0, 12)
    ax.set_ylim(0, 5)
    ax.set_aspect("auto")
    ax.axis("off")
    ax.set_title(
        "Stany procesu — diagram przejść", fontsize=FS_TITLE, fontweight="bold", pad=10
    )

    states = {
        "NEW": (1.0, 2.5),
        "READY": (3.5, 2.5),
        "RUNNING": (6.5, 2.5),
        "BLOCKED": (6.5, 0.5),
        "TERMINATED": (10.0, 2.5),
    }
    fills = {
        "NEW": GRAY4,
        "READY": GRAY1,
        "RUNNING": GRAY3,
        "BLOCKED": GRAY2,
        "TERMINATED": GRAY5,
    }
    bw, bh = 1.8, 0.9
    for name, (x, y) in states.items():
        draw_box(
            ax,
            x,
            y,
            bw,
            bh,
            name,
            fill=fills[name],
            fontsize=FS_LABEL,
            fontweight="bold",
        )

    # Transitions
    transitions = [
        ("NEW", "READY", "admit"),
        ("READY", "RUNNING", "dispatch\n(scheduler)"),
        ("RUNNING", "TERMINATED", "exit"),
        ("RUNNING", "BLOCKED", "I/O wait"),
    ]
    for src, dst, label in transitions:
        sx, sy = states[src]
        dx, dy = states[dst]
        if sy == dy:  # horizontal
            draw_arrow(ax, sx + bw, sy + bh / 2, dx, dy + bh / 2, lw=1.5)
            mx = (sx + bw + dx) / 2
            ax.text(
                mx,
                sy + bh / 2 + 0.25,
                label,
                fontsize=FS_SMALL,
                ha="center",
                va="bottom",
            )
        else:  # vertical
            draw_arrow(ax, sx + bw / 2, sy, dx + bw / 2, dy + bh, lw=1.5)
            ax.text(
                sx + bw + 0.2,
                (sy + dy + bh) / 2,
                label,
                fontsize=FS_SMALL,
                ha="left",
                va="center",
            )

    # BLOCKED → READY
    bx, by = states["BLOCKED"]
    rx, ry = states["READY"]
    ax.annotate(
        "",
        xy=(rx + bw / 2, ry),
        xytext=(bx - 0.3, by + bh / 2),
        arrowprops={
            "arrowstyle": "->",
            "lw": 1.5,
            "color": LN,
            "connectionstyle": "arc3,rad=0.3",
        },
    )
    ax.text(3.5, 0.7, "I/O done", fontsize=FS_SMALL, ha="center")

    # RUNNING → READY (preemption)
    rux, ruy = states["RUNNING"]
    draw_arrow(ax, rux, ruy + bh, rx + bw, ry + bh, lw=1.2)
    ax.text(5.0, 3.7, "preempt /\ntimeout", fontsize=FS_SMALL, ha="center")

    save_fig(fig, "q9_process_states.png")


# ============================================================
# 4. Thread structure within process
# ============================================================
def gen_thread_structure() -> None:
    """Gen thread structure."""
    fig, ax = plt.subplots(figsize=(8, 4.5))
    ax.set_xlim(0, 10)
    ax.set_ylim(0, 6)
    ax.set_aspect("auto")
    ax.axis("off")
    ax.set_title(
        "Wątki wewnątrz procesu (PID=42)", fontsize=FS_TITLE, fontweight="bold", pad=10
    )

    # Shared memory region
    draw_box(ax, 0.5, 3.5, 9.0, 1.8, "", fill=GRAY1, rounded=False, lw=2)
    ax.text(5.0, 5.0, "WSPÓŁDZIELONE", fontsize=FS, fontweight="bold", ha="center")

    labels_shared = ["TEXT", "DATA", "BSS", "HEAP", "pliki", "PID"]
    for i, lab in enumerate(labels_shared):
        x = 1.0 + i * 1.4
        draw_box(
            ax,
            x,
            3.8,
            1.1,
            0.6,
            lab,
            fill=GRAY3,
            fontsize=FS,
            fontweight="bold",
            rounded=False,
        )
        ax.text(x + 0.55, 4.6, lab, fontsize=FS_SMALL, ha="center", color="#555555")

    # Per-thread regions
    draw_box(
        ax, 0.5, 0.5, 9.0, 2.7, "", fill="white", rounded=False, lw=2, linestyle="--"
    )
    ax.text(
        5.0, 2.95, "PRYWATNE (każdy wątek)", fontsize=FS, fontweight="bold", ha="center"
    )

    for i in range(3):
        x = 1.0 + i * 3.0
        tid = i + 1
        draw_box(ax, x, 0.7, 2.3, 2.0, "", fill=GRAY4, rounded=False)
        ax.text(
            x + 1.15,
            2.4,
            f"Wątek {tid}",
            fontsize=FS_LABEL,
            fontweight="bold",
            ha="center",
        )
        items = [f"stos_{tid}", f"rejestry_{tid}", f"PC_{tid}", f"TID={40 + tid}"]
        for j, item in enumerate(items):
            ax.text(
                x + 1.15,
                2.0 - j * 0.35,
                item,
                fontsize=FS_SMALL,
                ha="center",
                family="monospace",
            )

    save_fig(fig, "q9_thread_structure.png")


# ============================================================
# 5. PCB structure
# ============================================================
def gen_pcb_structure() -> None:
    """Gen pcb structure."""
    fig, ax = plt.subplots(figsize=(5, 3.5))
    ax.set_xlim(0, 8)
    ax.set_ylim(0, 5.5)
    ax.set_aspect("auto")
    ax.axis("off")
    ax.set_title(
        "PCB (Process Control Block)", fontsize=FS_TITLE, fontweight="bold", pad=10
    )

    fields = [
        ("PID", "42"),
        ("Stan", "READY / RUNNING / BLOCKED"),
        ("Rejestry CPU", "EAX, EBX, ESP, EIP ..."),
        ("Tablice stron", "mapowanie wirtualne → fizyczne"),
        ("Otwarte pliki", "fd[0], fd[1], fd[2] ..."),
        ("Priorytety", "nice value, scheduling class"),
        ("Statystyki", "CPU time, I/O count"),
    ]

    top_y = 4.8
    for i, (field, value) in enumerate(fields):
        y = top_y - i * 0.55
        draw_box(
            ax,
            0.5,
            y,
            2.2,
            0.45,
            field,
            fill=GRAY2,
            fontsize=FS,
            fontweight="bold",
            rounded=False,
        )
        draw_box(ax, 2.7, y, 4.5, 0.45, value, fill=GRAY4, fontsize=FS, rounded=False)

    ax.text(
        4.0,
        0.3,
        "Context switch = zapisz PCB starego → wczytaj PCB nowego",
        fontsize=FS_SMALL,
        ha="center",
        style="italic",
    )

    save_fig(fig, "q9_pcb_structure.png")


# ============================================================
# 6. Speed comparison
# ============================================================
def gen_speed_comparison() -> None:
    """Gen speed comparison."""
    fig, axes = plt.subplots(1, 2, figsize=(9, 3.5))
    fig.suptitle(
        "Szybkość — procesy vs wątki (benchmarki Linux)",
        fontsize=FS_TITLE,
        fontweight="bold",
    )

    # Creation time panel
    ax = axes[0]
    ops = ["fork()\n(nowy proces)", "pthread_create()\n(nowy wątek)"]
    times = [3.0, 0.05]  # ms
    colors = [GRAY3, GRAY1]
    bars = ax.barh(ops, times, color=colors, edgecolor=LN, height=0.5, linewidth=1.2)
    ax.set_xlabel("Czas [ms]", fontsize=FS)
    ax.set_title("Tworzenie", fontsize=FS_LABEL, fontweight="bold")
    ax.set_xlim(0, 4.5)
    for bar, t in zip(bars, times, strict=False):
        ax.text(
            bar.get_width() + 0.1,
            bar.get_y() + bar.get_height() / 2,
            f"{t} ms",
            va="center",
            fontsize=FS,
        )
    ax.text(
        2.5,
        -0.6,
        "~100x szybciej",
        fontsize=FS,
        ha="center",
        fontweight="bold",
        transform=ax.get_xaxis_transform(),
    )
    ax.tick_params(labelsize=FS)

    # Right: context switch
    ax = axes[1]
    ops2 = ["Proces→Proces\n(TLB flush)", "Wątek→Wątek\n(TLB warm)"]
    times2 = [3000, 300]  # ns
    bars2 = ax.barh(ops2, times2, color=colors, edgecolor=LN, height=0.5, linewidth=1.2)
    ax.set_xlabel("Czas [ns]", fontsize=FS)
    ax.set_title("Przełączanie kontekstu", fontsize=FS_LABEL, fontweight="bold")
    ax.set_xlim(0, 4500)
    for bar, t in zip(bars2, times2, strict=False):
        ax.text(
            bar.get_width() + 50,
            bar.get_y() + bar.get_height() / 2,
            f"{t} ns",
            va="center",
            fontsize=FS,
        )
    ax.text(
        2500,
        -0.6,
        "~10x szybciej",
        fontsize=FS,
        ha="center",
        fontweight="bold",
        transform=ax.get_xaxis_transform(),
    )
    ax.tick_params(labelsize=FS)

    fig.tight_layout(rect=[0, 0.05, 1, 0.92])
    save_fig(fig, "q9_speed_comparison.png")


# ============================================================
# 7. Scenario table (when to use process vs thread)
# ============================================================
def gen_scenario_table() -> None:
    """Gen scenario table."""
    fig, ax = plt.subplots(figsize=(8.5, 4.5))
    ax.set_xlim(0, 11)
    ax.set_ylim(-5.5, 1)
    ax.set_aspect("auto")
    ax.axis("off")
    ax.set_title(
        "Kiedy proces, kiedy wątek? — typowe scenariusze",
        fontsize=FS_TITLE,
        fontweight="bold",
        pad=10,
    )

    headers = ["Scenariusz", "Wybór", "Dlaczego?"]
    col_w = [3.5, 2.5, 4.5]
    rows = [
        ["Serwer WWW (Apache)", "Proces", "izolacja klientów"],
        ["Serwer WWW (nginx)", "Wątek / async", "szybkość, cooperacja"],
        ["Przeglądarka (karty)", "Proces", "crash isolation"],
        ["Przeglądarka (JS+render)", "Wątek", "współdzielony DOM"],
        ["Gra (fizyka+rendering)", "Wątek", "współdzielony świat gry"],
        ["Kompilacja (make -j8)", "Proces", "izolacja, prostota"],
        ["Baza danych (zapytania)", "Wątek", "współdzielony cache"],
        ["Microservices", "Proces (kontener)", "izolacja, deployment"],
    ]
    draw_table(
        ax, headers, rows, x0=0.25, y0=0.5, col_widths=col_w, row_h=0.5, fontsize=7
    )

    save_fig(fig, "q9_scenario_table.png")


# ============================================================
# 8. IPC details: pipe, shared memory, socket (3-panel)
# ============================================================
def gen_ipc_details() -> None:
    """Gen ipc details."""
    fig, axes = plt.subplots(1, 3, figsize=(11, 3.5))
    fig.suptitle("Mechanizmy IPC — szczegóły", fontsize=FS_TITLE, fontweight="bold")

    # Panel 1: Pipe
    ax = axes[0]
    ax.set_xlim(0, 8)
    ax.set_ylim(0, 5)
    ax.set_aspect("auto")
    ax.axis("off")
    ax.set_title("Pipe (potok)", fontsize=FS_LABEL, fontweight="bold")

    draw_box(ax, 0.2, 2.0, 1.8, 1.2, "Proces A\n(ls)\nstdout", fill=GRAY1, fontsize=FS)
    draw_box(
        ax,
        3.0,
        2.0,
        1.8,
        1.2,
        "Bufor\njądra\n(4 KB)",
        fill=GRAY2,
        fontsize=FS,
        fontweight="bold",
    )
    draw_box(ax, 5.8, 2.0, 1.8, 1.2, "Proces B\n(grep)\nstdin", fill=GRAY1, fontsize=FS)
    draw_arrow(ax, 2.0, 2.6, 3.0, 2.6, lw=1.5)
    ax.text(2.5, 3.0, "write()\nfd[1]", fontsize=FS_SMALL, ha="center")
    draw_arrow(ax, 4.8, 2.6, 5.8, 2.6, lw=1.5)
    ax.text(5.3, 3.0, "read()\nfd[0]", fontsize=FS_SMALL, ha="center")
    ax.text(
        4.0,
        0.8,
        "Jednokierunkowy\nBufor pełny → write() blokuje",
        fontsize=FS_SMALL,
        ha="center",
        style="italic",
        bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY3},
    )

    # Panel 2: Shared Memory
    ax = axes[1]
    ax.set_xlim(0, 8)
    ax.set_ylim(0, 5)
    ax.set_aspect("auto")
    ax.axis("off")
    ax.set_title("Shared Memory", fontsize=FS_LABEL, fontweight="bold")

    draw_box(ax, 0.3, 3.0, 2.2, 1.2, "Proces A\nstrona 7", fill=GRAY1, fontsize=FS)
    draw_box(ax, 5.5, 3.0, 2.2, 1.2, "Proces B\nstrona 3", fill=GRAY1, fontsize=FS)
    draw_box(
        ax,
        2.8,
        1.0,
        2.4,
        1.2,
        "RAM\nramka 42",
        fill=GRAY3,
        fontsize=FS,
        fontweight="bold",
    )
    draw_arrow(ax, 2.0, 3.0, 3.5, 2.2, lw=1.5)
    draw_arrow(ax, 6.0, 3.0, 4.5, 2.2, lw=1.5)
    ax.text(
        4.0,
        0.3,
        "Zero kopiowania!\nA pisze → B widzi od razu\nWymaga synchronizacji (semafor)",
        fontsize=FS_SMALL,
        ha="center",
        style="italic",
        bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY3},
    )

    # Panel 3: Socket
    ax = axes[2]
    ax.set_xlim(0, 8)
    ax.set_ylim(0, 5)
    ax.set_aspect("auto")
    ax.axis("off")
    ax.set_title("Socket", fontsize=FS_LABEL, fontweight="bold")

    # Network socket
    draw_box(
        ax, 0.3, 3.2, 1.8, 0.9, "Klient", fill=GRAY1, fontsize=FS, fontweight="bold"
    )
    draw_box(
        ax, 5.5, 3.2, 1.8, 0.9, "Serwer", fill=GRAY1, fontsize=FS, fontweight="bold"
    )
    draw_double_arrow(ax, 2.1, 3.65, 5.5, 3.65, lw=1.5)
    ax.text(3.8, 4.3, "TCP/IP (sieciowy)", fontsize=FS, ha="center", fontweight="bold")

    # Unix socket
    draw_box(
        ax, 0.3, 1.3, 1.8, 0.9, "Proces A", fill=GRAY4, fontsize=FS, fontweight="bold"
    )
    draw_box(
        ax, 5.5, 1.3, 1.8, 0.9, "Proces B", fill=GRAY4, fontsize=FS, fontweight="bold"
    )
    draw_double_arrow(ax, 2.1, 1.75, 5.5, 1.75, lw=1.5)
    ax.text(
        3.8,
        2.4,
        "Unix domain socket\n(/tmp/app.sock)",
        fontsize=FS,
        ha="center",
        fontweight="bold",
    )

    ax.text(
        3.8,
        0.5,
        "Dwukierunkowy\nNajbardziej uniwersalny IPC",
        fontsize=FS_SMALL,
        ha="center",
        style="italic",
        bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY3},
    )

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


# ============================================================
# 9. IPC comparison table
# ============================================================
def gen_ipc_table() -> None:
    """Gen ipc table."""
    fig, ax = plt.subplots(figsize=(8.5, 3.5))
    ax.set_xlim(0, 11)
    ax.set_ylim(-4.5, 1)
    ax.set_aspect("auto")
    ax.axis("off")
    ax.set_title(
        "Porównanie mechanizmów IPC", fontsize=FS_TITLE, fontweight="bold", pad=10
    )

    headers = ["Mechanizm", "Kierunek", "Szybkość", "Zastosowanie"]
    col_w = [2.5, 2.0, 2.5, 3.5]
    rows = [
        ["Pipe", "jednokierunkowy", "średnia", "ls | grep"],
        ["Named Pipe", "jednokierunkowy", "średnia", "demon → klient"],
        ["Shared Memory", "dwukierunkowy", "NAJSZYBSZA", "video, bazy danych"],
        ["Message Queue", "dwukierunkowy", "średnia", "wieloproducentowe"],
        ["Socket", "dwukierunkowy", "wolna (sieć)", "klient-serwer"],
        ["Signal", "jednokierunkowy", "natychmiastowa", "powiadomienia (nr)"],
    ]
    draw_table(
        ax, headers, rows, x0=0.25, y0=0.5, col_widths=col_w, row_h=0.5, fontsize=7.5
    )

    save_fig(fig, "q9_ipc_table.png")


# ============================================================
# 10. Race condition (simple x + bank timeline)
# ============================================================
def gen_race_condition() -> None:
    """Gen race condition."""
    fig, axes = plt.subplots(1, 2, figsize=(11, 5))
    fig.suptitle(
        "Wyścig (Race Condition) — przykłady", fontsize=FS_TITLE, fontweight="bold"
    )

    # Panel 1: simple x increment
    ax = axes[0]
    ax.set_xlim(0, 8)
    ax.set_ylim(0, 7)
    ax.set_aspect("auto")
    ax.axis("off")
    ax.set_title("Prosty wyścig: x = x + 1", fontsize=FS_LABEL, fontweight="bold")

    # Timeline
    steps_a = ["czytaj x (=0)", "dodaj 1", "zapisz x (=1)"]
    steps_b = ["czytaj x (=0)", "dodaj 1", "zapisz x (=1)"]
    ax.text(2.0, 6.3, "Wątek A", fontsize=FS_LABEL, ha="center", fontweight="bold")
    ax.text(6.0, 6.3, "Wątek B", fontsize=FS_LABEL, ha="center", fontweight="bold")
    ax.plot([2, 2], [0.8, 6.0], color=LN, lw=1)
    ax.plot([6, 6], [0.8, 6.0], color=LN, lw=1)

    for i, (sa, sb) in enumerate(zip(steps_a, steps_b, strict=False)):
        y = 5.3 - i * 1.2
        draw_box(ax, 0.5, y, 3.0, 0.6, sa, fill=GRAY4, fontsize=FS)
        draw_box(ax, 4.5, y - 0.3, 3.0, 0.6, sb, fill=GRAY1, fontsize=FS)

    ax.text(
        4.0,
        0.4,
        "Wynik: x = 1  (powinno 2!)",
        fontsize=FS,
        ha="center",
        fontweight="bold",
        color="#C62828",
        bbox={"boxstyle": "round", "facecolor": "#F8D7DA", "edgecolor": "#C62828"},
    )

    # Panel 2: bank account
    ax = axes[1]
    ax.set_xlim(0, 10)
    ax.set_ylim(0, 7)
    ax.set_aspect("auto")
    ax.axis("off")
    ax.set_title("Konto bankowe: saldo = 1000 zł", fontsize=FS_LABEL, fontweight="bold")

    ax.text(2.5, 6.3, "Wątek A (+500)", fontsize=FS, ha="center", fontweight="bold")
    ax.text(7.5, 6.3, "Wątek B (-200)", fontsize=FS, ha="center", fontweight="bold")
    ax.plot([2.5, 2.5], [0.8, 6.0], color=LN, lw=1)
    ax.plot([7.5, 7.5], [0.8, 6.0], color=LN, lw=1)

    events = [
        ("t1", "czytaj → 1000", "", 5.3),
        ("t2", "", "czytaj → 1000", 4.6),
        ("t3", "1000+500=1500", "", 3.9),
        ("t4", "", "1000-200=800", 3.2),
        ("t5", "zapisz 1500", "", 2.5),
        ("t6", "", "zapisz 800 ✗", 1.8),
    ]
    for t, a, b, y in events:
        ax.text(0.3, y + 0.15, t, fontsize=FS_SMALL, fontweight="bold", va="center")
        if a:
            draw_box(ax, 1.0, y, 3.0, 0.45, a, fill=GRAY4, fontsize=FS_SMALL)
        if b:
            fill = "#F8D7DA" if "✗" in b else GRAY1
            draw_box(ax, 6.0, y, 3.0, 0.45, b, fill=fill, fontsize=FS_SMALL)

    ax.text(
        5.0,
        0.4,
        "Wynik: 800 zł  (powinno 1300!)",
        fontsize=FS,
        ha="center",
        fontweight="bold",
        color="#C62828",
        bbox={"boxstyle": "round", "facecolor": "#F8D7DA", "edgecolor": "#C62828"},
    )

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


# ============================================================
# 11. Deadlock scenario + cycle
# ============================================================
def gen_deadlock_scenario() -> None:
    """Gen deadlock scenario."""
    fig, axes = plt.subplots(1, 2, figsize=(11, 4.5))
    fig.suptitle("Zakleszczenie (Deadlock)", fontsize=FS_TITLE, fontweight="bold")

    # Panel 1: timeline
    ax = axes[0]
    ax.set_xlim(0, 8)
    ax.set_ylim(0, 6)
    ax.set_aspect("auto")
    ax.axis("off")
    ax.set_title("Scenariusz z 2 mutexami", fontsize=FS_LABEL, fontweight="bold")

    ax.text(2.5, 5.3, "Wątek A", fontsize=FS_LABEL, ha="center", fontweight="bold")
    ax.text(6.0, 5.3, "Wątek B", fontsize=FS_LABEL, ha="center", fontweight="bold")

    steps = [
        ("lock(mutex1) OK", "", "trzyma", False, 4.5),
        ("", "lock(mutex2) OK", "trzyma", False, 3.7),
        ("lock(mutex2) ...WAIT", "", "CZEKA!", True, 2.9),
        ("", "lock(mutex1) ...WAIT", "CZEKA!", True, 2.1),
    ]
    for a_text, b_text, _note, is_wait, y in steps:
        if a_text:
            fill = "#F8D7DA" if is_wait else GRAY4
            draw_box(ax, 0.5, y, 3.3, 0.55, a_text, fill=fill, fontsize=FS_SMALL)
        if b_text:
            fill = "#F8D7DA" if is_wait else GRAY4
            draw_box(ax, 4.3, y, 3.3, 0.55, b_text, fill=fill, fontsize=FS_SMALL)

    ax.text(
        4.0,
        1.2,
        "DEADLOCK!\nŻaden nie odpuści",
        fontsize=FS,
        ha="center",
        fontweight="bold",
        color="#C62828",
        bbox={"boxstyle": "round", "facecolor": "#F8D7DA", "edgecolor": "#C62828"},
    )

    # Panel 2: cycle diagram
    ax = axes[1]
    ax.set_xlim(0, 8)
    ax.set_ylim(0, 6)
    ax.set_aspect("auto")
    ax.axis("off")
    ax.set_title("Cykl oczekiwania", fontsize=FS_LABEL, fontweight="bold")

    # Thread boxes
    draw_box(
        ax,
        0.5,
        3.5,
        2.2,
        1.2,
        "Wątek A\ntrzyma Mutex 1",
        fill=GRAY1,
        fontsize=FS,
        fontweight="bold",
    )
    draw_box(
        ax,
        5.3,
        3.5,
        2.2,
        1.2,
        "Wątek B\ntrzyma Mutex 2",
        fill=GRAY1,
        fontsize=FS,
        fontweight="bold",
    )

    # Mutex boxes
    draw_box(
        ax, 0.5, 1.0, 2.2, 1.0, "Mutex 1", fill=GRAY3, fontsize=FS, fontweight="bold"
    )
    draw_box(
        ax, 5.3, 1.0, 2.2, 1.0, "Mutex 2", fill=GRAY3, fontsize=FS, fontweight="bold"
    )

    # holds arrows (down)
    draw_arrow(ax, 1.6, 3.5, 1.6, 2.0, lw=2)
    ax.text(0.9, 2.7, "trzyma", fontsize=FS_SMALL, rotation=90, va="center")
    draw_arrow(ax, 6.4, 3.5, 6.4, 2.0, lw=2)
    ax.text(7.0, 2.7, "trzyma", fontsize=FS_SMALL, rotation=90, va="center")

    # waits-for arrows (across, red)
    draw_arrow(ax, 2.7, 4.3, 5.3, 4.3, lw=2.5, color="#C62828")
    ax.text(
        4.0,
        4.7,
        "czeka na Mutex 2",
        fontsize=FS_SMALL,
        ha="center",
        fontweight="bold",
        color="#C62828",
    )
    draw_arrow(ax, 5.3, 3.7, 2.7, 3.7, lw=2.5, color="#C62828")
    ax.text(
        4.0,
        3.1,
        "czeka na Mutex 1",
        fontsize=FS_SMALL,
        ha="center",
        fontweight="bold",
        color="#C62828",
    )

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


# ============================================================
# 12. Coffman conditions + prevention strategies
# ============================================================
def gen_coffman_strategies() -> None:
    """Gen coffman strategies."""
    fig, ax = plt.subplots(figsize=(9, 4))
    ax.set_xlim(0, 11.5)
    ax.set_ylim(-3.5, 1)
    ax.set_aspect("auto")
    ax.axis("off")
    ax.set_title(
        "Warunki Coffmana — zapobieganie deadlockowi",
        fontsize=FS_TITLE,
        fontweight="bold",
        pad=10,
    )

    headers = ["Warunek", "Opis", "Jak złamać", "Przykład"]
    col_w = [2.5, 2.5, 3.0, 3.0]
    rows = [
        [
            "1. Mutual Exclusion",
            "zasób wyłączny",
            "współdzielony zasób",
            "Read-write lock",
        ],
        [
            "2. Hold and Wait",
            "trzymaj + czekaj",
            "bierz WSZYSTKIE naraz",
            "lock(m1,m2) atomowo",
        ],
        [
            "3. No Preemption",
            "nie zabierzesz siłą",
            "timeout / trylock",
            "pthread_mutex_trylock()",
        ],
        [
            "4. Circular Wait",
            "cykliczne oczekiw.",
            "porządek liniowy",
            "zawsze m1 przed m2",
        ],
    ]
    draw_table(
        ax, headers, rows, x0=0.25, y0=0.5, col_widths=col_w, row_h=0.6, fontsize=7
    )

    ax.text(
        5.75,
        -3.1,
        "▸ Najczęstsza strategia: PORZĄDEK LINIOWY — "
        "numeruj mutexy, zawsze blokuj rosnąco",
        fontsize=FS,
        ha="center",
        fontweight="bold",
        bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY3},
    )

    save_fig(fig, "q9_coffman_strategies.png")


# ============================================================
# 13. Starvation + Priority Inversion (2-panel)
# ============================================================
def gen_starvation_priority() -> None:
    """Gen starvation priority."""
    fig, axes = plt.subplots(1, 2, figsize=(11, 4.5))
    fig.suptitle(
        "Zagłodzenie i Inwersja priorytetów", fontsize=FS_TITLE, fontweight="bold"
    )

    # Panel 1: Starvation + aging
    ax = axes[0]
    ax.set_xlim(0, 8)
    ax.set_ylim(0, 6)
    ax.set_aspect("auto")
    ax.axis("off")
    ax.set_title("Zagłodzenie (Starvation)", fontsize=FS_LABEL, fontweight="bold")

    threads = [
        ("Wątek HIGH", "prio=10", GRAY5, 3.0),
        ("Wątek HIGH", "prio=9", GRAY3, 2.2),
        ("Wątek MED", "prio=5", GRAY2, 1.4),
        ("Wątek LOW", "prio=1 → głoduje!", "#F8D7DA", 0.6),
    ]
    for name, prio, color, y in threads:
        draw_box(
            ax, 0.5, y, 2.0, 0.6, name, fill=color, fontsize=FS_SMALL, fontweight="bold"
        )
        ax.text(2.8, y + 0.3, prio, fontsize=FS_SMALL, va="center")

    ax.text(
        1.5,
        4.2,
        "CPU zawsze\ndostaje HIGH!",
        fontsize=FS,
        ha="center",
        fontweight="bold",
    )
    draw_arrow(ax, 1.5, 3.9, 1.5, 3.65, lw=1.5)

    # Aging solution
    draw_box(ax, 4.5, 1.5, 3.2, 2.5, "", fill=GRAY4, rounded=True)
    ax.text(6.1, 3.7, "Rozwiązanie: AGING", fontsize=FS, fontweight="bold", ha="center")
    aging = [
        "t=0:    prio=1",
        "t=100ms: prio=2",
        "t=200ms: prio=3",
        "...",
        "w końcu → CPU!",
    ]
    for i, line in enumerate(aging):
        ax.text(
            6.1, 3.2 - i * 0.4, line, fontsize=FS_SMALL, ha="center", family="monospace"
        )

    # Panel 2: Priority Inversion
    ax = axes[1]
    ax.set_xlim(0, 10)
    ax.set_ylim(0, 6)
    ax.set_aspect("auto")
    ax.axis("off")
    ax.set_title("Inwersja priorytetów", fontsize=FS_LABEL, fontweight="bold")

    # Timeline
    labels = ["H (wysoki)", "M (średni)", "L (niski)"]
    ys = [4.2, 2.8, 1.4]
    for label, y in zip(labels, ys, strict=False):
        ax.text(0.3, y + 0.2, label, fontsize=FS, fontweight="bold", va="center")

    # L runs and locks mutex
    draw_box(ax, 2.0, ys[2], 1.2, 0.5, "lock(m)", fill=GRAY1, fontsize=FS_SMALL)

    # M preempts L
    draw_box(ax, 3.5, ys[1], 3.0, 0.5, "M pracuje...", fill=GRAY3, fontsize=FS_SMALL)

    # H waits for mutex
    draw_box(
        ax,
        3.5,
        ys[0],
        3.0,
        0.5,
        "CZEKA na mutex!",
        fill="#F8D7DA",
        fontsize=FS_SMALL,
        fontweight="bold",
    )

    # M finishes, L continues, unlocks
    draw_box(ax, 6.8, ys[2], 1.5, 0.5, "unlock(m)", fill=GRAY1, fontsize=FS_SMALL)
    draw_box(ax, 8.5, ys[0], 1.2, 0.5, "H runs", fill=GRAY4, fontsize=FS_SMALL)

    # Explanation
    ax.text(
        5.0,
        0.5,
        "H czeka na M (mimo H > M)!\n"
        "Rozwiązanie: Priority Inheritance\n"
        "L dziedziczy priorytet H → M nie wypycha L",
        fontsize=FS_SMALL,
        ha="center",
        style="italic",
        bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
    )

    ax.text(
        5.0,
        0.0,
        "Mars Pathfinder (1997) — klasyczny bug!",
        fontsize=FS_SMALL,
        ha="center",
        fontweight="bold",
    )

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


# ============================================================
# 14. Bounded buffer + readers-writers + philosophers
# ============================================================
def _draw_bounded_buffer_panel(ax: Axes) -> None:
    """Draw the bounded-buffer (producer-consumer) panel."""
    ax.set_xlim(0, 8)
    ax.set_ylim(0, 7)
    ax.set_aspect("auto")
    ax.axis("off")
    ax.set_title(
        "Producent-Konsument\n(Bounded Buffer, N=4)", fontsize=FS, fontweight="bold"
    )

    draw_box(
        ax,
        0.2,
        4.0,
        2.0,
        1.2,
        "Producent\nP(empty)\nP(mutex)\nwstaw()\nV(mutex)\nV(full)",
        fill=GRAY1,
        fontsize=5.5,
    )
    items = ["A", "B", "", ""]
    for i, item in enumerate(items):
        x = 2.8 + i * 0.9
        fill = GRAY3 if item else "white"
        draw_box(
            ax,
            x,
            4.3,
            0.9,
            0.7,
            item,
            fill=fill,
            fontsize=FS,
            fontweight="bold",
            rounded=False,
        )
    ax.text(4.6, 5.2, "Bufor (N=4)", fontsize=FS_SMALL, ha="center", fontweight="bold")
    draw_box(
        ax,
        6.0,
        4.0,
        2.0,
        1.2,
        "Konsument\nP(full)\nP(mutex)\npobierz()\nV(mutex)\nV(empty)",
        fill=GRAY4,
        fontsize=5.5,
    )
    draw_arrow(ax, 2.2, 4.6, 2.8, 4.65, lw=1.2)
    draw_arrow(ax, 6.4, 4.65, 6.0, 4.6, lw=1.2)

    sems = [("mutex = 1", GRAY2), ("empty = N", GRAY1), ("full = 0", GRAY3)]
    for i, (s, c) in enumerate(sems):
        draw_box(
            ax,
            2.0,
            2.5 - i * 0.6,
            4.0,
            0.45,
            s,
            fill=c,
            fontsize=FS_SMALL,
            fontweight="bold",
        )

    ax.text(
        4.0,
        0.5,
        "KOLEJNOŚĆ: P(empty/full)\nPRZED P(mutex)!\nOdwrotnie = DEADLOCK",
        fontsize=5.5,
        ha="center",
        fontweight="bold",
        color="#C62828",
        bbox={"boxstyle": "round", "facecolor": "#F8D7DA", "edgecolor": "#C62828"},
    )


def _draw_readers_writers_panel(ax: Axes) -> None:
    """Draw the readers-writers panel."""
    ax.set_xlim(0, 8)
    ax.set_ylim(0, 7)
    ax.set_aspect("auto")
    ax.axis("off")
    ax.set_title(
        "Czytelnicy-Pisarze\n(Readers-Writers)", fontsize=FS, fontweight="bold"
    )

    draw_box(
        ax,
        2.5,
        3.5,
        3.0,
        1.5,
        "Dane\n(współdzielone)",
        fill=GRAY2,
        fontsize=FS,
        fontweight="bold",
    )

    for i in range(3):
        x = 0.3 + i * 1.0
        draw_box(
            ax,
            x,
            5.5,
            0.8,
            0.7,
            f"R{i + 1}",
            fill=GRAY4,
            fontsize=FS,
            fontweight="bold",
        )
        draw_arrow(ax, x + 0.4, 5.5, 3.0 + i * 0.5, 5.0, lw=1)

    ax.text(
        1.5,
        6.5,
        "Czytelnicy (wielu naraz)",
        fontsize=FS_SMALL,
        ha="center",
        fontweight="bold",
    )

    draw_box(
        ax, 5.5, 5.5, 1.5, 0.7, "Pisarz", fill=GRAY5, fontsize=FS, fontweight="bold"
    )
    draw_arrow(ax, 6.25, 5.5, 5.0, 5.0, lw=1.5)
    ax.text(
        6.25,
        6.5,
        "WYŁĄCZNY",
        fontsize=FS_SMALL,
        ha="center",
        fontweight="bold",
        color="#C62828",
    )

    rules = [
        "Wielu czytelników = OK",
        "Jeden pisarz = wyłączny",
        "Czytelnik + Pisarz = ✗",
        "Problem: pisarze głodują",
    ]
    for i, r in enumerate(rules):
        ax.text(4.0, 2.5 - i * 0.45, r, fontsize=FS_SMALL, ha="center")

    ax.text(
        4.0,
        0.5,
        "Rozwiązanie:\nrw_mutex + count_mutex\n+ zmienna readers",
        fontsize=5.5,
        ha="center",
        bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY3},
    )


def _draw_philosophers_panel(ax: Axes) -> None:
    """Draw the dining-philosophers panel."""
    ax.set_xlim(0, 8)
    ax.set_ylim(0, 7)
    ax.set_aspect("auto")
    ax.axis("off")
    ax.set_title(
        "Ucztujący filozofowie\n(Dining Philosophers)", fontsize=FS, fontweight="bold"
    )

    cx, cy, r = 4.0, 3.8, 1.8
    table = plt.Circle((cx, cy), 0.8, fill=True, facecolor=GRAY2, edgecolor=LN, lw=1.5)
    ax.add_patch(table)
    ax.text(cx, cy, "Stół", fontsize=FS, ha="center", fontweight="bold")

    for i in range(5):
        angle = np.pi / 2 + i * 2 * np.pi / 5
        px = cx + r * np.cos(angle)
        py = cy + r * np.sin(angle)
        circle = plt.Circle(
            (px, py), 0.35, fill=True, facecolor=GRAY1, edgecolor=LN, lw=1.2
        )
        ax.add_patch(circle)
        ax.text(
            px, py, f"F{i}", ha="center", va="center", fontsize=FS, fontweight="bold"
        )

        fork_angle = np.pi / 2 + (i + 0.5) * 2 * np.pi / 5
        fx = cx + (r * 0.6) * np.cos(fork_angle)
        fy = cy + (r * 0.6) * np.sin(fork_angle)
        ax.plot(
            [fx - 0.1, fx + 0.1],
            [fy - 0.15, fy + 0.15],
            color=LN,
            lw=2.5,
            solid_capstyle="round",
        )
        ax.text(fx + 0.2, fy + 0.15, f"w{i}", fontsize=5, color="#555555")

    rules = [
        "Jedzenie = 2 widelce",
        "Naiwne → DEADLOCK",
        "Fix: F4 bierze odwrotnie",
        "Alt: semafor(4)",
    ]
    for i, r in enumerate(rules):
        ax.text(4.0, 1.2 - i * 0.35, r, fontsize=FS_SMALL, ha="center")


# ============================================================
# 14. Bounded buffer + readers-writers + philosophers
# ============================================================
def gen_classic_problems() -> None:
    """Gen classic problems."""
    fig, axes = plt.subplots(1, 3, figsize=(12, 5))
    fig.suptitle(
        "Klasyczne problemy synchronizacji", fontsize=FS_TITLE, fontweight="bold"
    )

    _draw_bounded_buffer_panel(axes[0])
    _draw_readers_writers_panel(axes[1])
    _draw_philosophers_panel(axes[2])

    fig.tight_layout(rect=[0, 0, 1, 0.88])
    save_fig(fig, "q9_classic_problems.png")


# ============================================================
# 15. Sync mechanisms comparison + mutex/sem/spinlock
# ============================================================
def gen_sync_comparison() -> None:
    """Gen sync comparison."""
    fig, axes = plt.subplots(2, 1, figsize=(9, 7))

    # Top: comparison table
    ax = axes[0]
    ax.set_xlim(0, 11.5)
    ax.set_ylim(-5, 1)
    ax.set_aspect("auto")
    ax.axis("off")
    ax.set_title(
        "Mechanizmy synchronizacji — porównanie",
        fontsize=FS_TITLE,
        fontweight="bold",
        pad=10,
    )

    headers = ["Mechanizm", "Opis", "Kiedy używać"]
    col_w = [2.5, 4.5, 4.0]
    rows = [
        ["Mutex", "Zamek: 1 wątek w sekcji", "Sekcja krytyczna"],
        ["Semafor(n)", "Licznik: max n wątków", "Ograniczone zasoby (n miejsc)"],
        ["Monitor", "Obiekt z wbudowanym mutex", "Java synchronized"],
        ["Cond. Variable", "wait()/signal() na warunek", "Producent-konsument"],
        ["Spinlock", "Aktywne czekanie (busy-wait)", "Bardzo krótkie sekcje (<1 μs)"],
        ["RW Lock", "Wielu czytelników LUB 1 pisarz", "Bazy danych, cache"],
        ["Barrier", "Czekaj aż wszyscy dotrą", "Obliczenia równoległe"],
    ]
    draw_table(
        ax, headers, rows, x0=0.25, y0=0.5, col_widths=col_w, row_h=0.5, fontsize=7
    )

    # Bottom: mutex vs semafor vs spinlock
    ax = axes[1]
    ax.set_xlim(0, 12)
    ax.set_ylim(0, 5)
    ax.set_aspect("auto")
    ax.axis("off")
    ax.set_title(
        "Mutex vs Semafor vs Spinlock", fontsize=FS_TITLE, fontweight="bold", pad=5
    )

    # Mutex
    draw_box(ax, 0.3, 2.5, 3.5, 2.0, "", fill=GRAY4)
    ax.text(2.05, 4.2, "MUTEX", fontsize=FS_LABEL, fontweight="bold", ha="center")
    ax.text(2.05, 3.6, "= klucz do łazienki\n(1 osoba)", fontsize=FS, ha="center")
    ax.text(
        2.05,
        2.8,
        "Wątek ZASYPIA gdy czeka\nOS go obudzi (~μs)",
        fontsize=FS_SMALL,
        ha="center",
        style="italic",
    )

    # Semafor
    draw_box(ax, 4.3, 2.5, 3.5, 2.0, "", fill=GRAY1)
    ax.text(6.05, 4.2, "SEMAFOR(n)", fontsize=FS_LABEL, fontweight="bold", ha="center")
    ax.text(
        6.05, 3.6, "= parking na n miejsc\n(n wątków naraz)", fontsize=FS, ha="center"
    )
    ax.text(
        6.05,
        2.8,
        "Semafor(1) = mutex\nP() = zmniejsz, V() = zwiększ",
        fontsize=FS_SMALL,
        ha="center",
        style="italic",
    )

    # Spinlock
    draw_box(ax, 8.3, 2.5, 3.5, 2.0, "", fill=GRAY2)
    ax.text(10.05, 4.2, "SPINLOCK", fontsize=FS_LABEL, fontweight="bold", ha="center")
    ax.text(
        10.05, 3.6, "= obrotowe drzwi\n(kręcisz się w kółko)", fontsize=FS, ha="center"
    )
    ax.text(
        10.05,
        2.8,
        "Wątek KRĘCI się w pętli\nLepszy gdy sekcja < 1 μs",
        fontsize=FS_SMALL,
        ha="center",
        style="italic",
    )

    # Dividing rule
    ax.text(
        6.0,
        1.5,
        "Reguła kciuka:  sekcja > 1 μs → MUTEX  |  "
        "sekcja < 1 μs → SPINLOCK  |  n jednocześnie → SEMAFOR(n)",
        fontsize=FS,
        ha="center",
        fontweight="bold",
        bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
    )

    fig.tight_layout()
    save_fig(fig, "q9_sync_comparison.png")


# ============================================================
# 16. Semaphore concept diagram
# ============================================================
def gen_semaphore_concept() -> None:
    """Gen semaphore concept."""
    fig, ax = plt.subplots(figsize=(6, 3))
    ax.set_xlim(0, 10)
    ax.set_ylim(0, 5)
    ax.set_aspect("auto")
    ax.axis("off")
    ax.set_title(
        "Semafor — koncepcja (parking na 3 miejsca)",
        fontsize=FS_TITLE,
        fontweight="bold",
        pad=10,
    )

    # Parking slots
    for i in range(3):
        x = 2.0 + i * 2.0
        occupied = i < OCCUPIED_SLOTS
        fill = GRAY3 if occupied else "white"
        label = f"Wątek {i + 1}" if occupied else "(wolne)"
        draw_box(
            ax,
            x,
            2.5,
            1.5,
            1.2,
            label,
            fill=fill,
            fontsize=FS,
            fontweight="bold" if occupied else "normal",
            rounded=False,
        )

    ax.text(
        5.0,
        4.2,
        "semafor(3): counter = 1 (jedno wolne miejsce)",
        fontsize=FS,
        ha="center",
        fontweight="bold",
    )

    # Waiting thread
    draw_box(
        ax,
        0.2,
        0.5,
        1.5,
        0.8,
        "Wątek 4\nP() → czeka",
        fill="#F8D7DA",
        fontsize=FS_SMALL,
    )
    draw_arrow(ax, 1.7, 0.9, 2.0, 2.5, lw=1.2, color="#C62828")

    ax.text(
        5.0,
        0.6,
        "P() = counter--  (jeśli 0 → czekaj)\nV() = counter++  (obudź czekającego)",
        fontsize=FS,
        ha="center",
        family="monospace",
        bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY3},
    )

    save_fig(fig, "q9_semaphore_concept.png")


# ============================================================
# MAIN — generate all
# ============================================================
if __name__ == "__main__":
    _logger.info("Generating ALL PYTANIE 9 diagrams...")
    gen_process_vs_thread()
    gen_memory_layout()
    gen_process_states()
    gen_thread_structure()
    gen_pcb_structure()
    gen_speed_comparison()
    gen_scenario_table()
    gen_ipc_details()
    gen_ipc_table()
    gen_race_condition()
    gen_deadlock_scenario()
    gen_coffman_strategies()
    gen_starvation_priority()
    gen_classic_problems()
    gen_sync_comparison()
    gen_semaphore_concept()
    _logger.info("All 16 PYTANIE 9 diagrams generated successfully!")
