"""Generate the three definition figures for the Level Switching paper.

Uses a stacked 7-vertex triangulation T:
  outer triangle {0,1,2}, inner vertex 3 connected to all three,
  then vertices 4,5,6 inserted into faces (1,2,3),(0,2,3),(0,1,3).
"""
import os
import networkx as nx
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon

OUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir)

# Vertex positions (hand-placed for a clean planar drawing).
POS = {
    0: (-1.5, -0.9),
    1: (1.5, -0.9),
    2: (0.0, 1.6),
    3: (0.0, 0.0),
    4: (0.55, 0.2),      # in face (1,2,3)
    5: (-0.55, 0.2),     # in face (0,2,3)
    6: (0.0, -0.55),     # in face (0,1,3)
}

EDGES = [
    (0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3),   # K_4
    (4, 1), (4, 2), (4, 3),                            # stack in (1,2,3)
    (5, 0), (5, 2), (5, 3),                            # stack in (0,2,3)
    (6, 0), (6, 1), (6, 3),                            # stack in (0,1,3)
]


def make_graph():
    G = nx.Graph()
    G.add_nodes_from(POS.keys())
    G.add_edges_from(EDGES)
    return G


def draw_base(ax, G, node_colors, node_size=520, font_color='white',
              edge_color='#555', edge_width=1.4):
    nx.draw_networkx_edges(G, POS, ax=ax, edge_color=edge_color, width=edge_width)
    nx.draw_networkx_nodes(G, POS, ax=ax, node_color=node_colors,
                           node_size=node_size, edgecolors='black', linewidths=1.0)
    nx.draw_networkx_labels(G, POS, ax=ax, font_color=font_color,
                            font_size=11, font_weight='bold')
    ax.set_aspect('equal')
    ax.axis('off')


# ---------------------------------------------------------------------------
# Figure 1: Level source (face source vs. degree-3 vertex source)
# ---------------------------------------------------------------------------
def fig_level_source():
    G = make_graph()
    fig, axes = plt.subplots(1, 2, figsize=(10, 5))

    # Panel A: face source S = {0,1,2}
    ax = axes[0]
    face_S = {0, 1, 2}
    colors = ['#ef4444' if v in face_S else '#cbd5e1' for v in G.nodes()]
    # Highlight the source face
    tri = Polygon([POS[v] for v in [0, 1, 2]], closed=True,
                  facecolor='#fecaca', edgecolor='#ef4444', linewidth=2.0,
                  alpha=0.45, zorder=0)
    ax.add_patch(tri)
    draw_base(ax, G, colors)
    ax.set_title(r'Face source $S = \{0,1,2\}$', fontsize=12)

    # Panel B: degree-3 vertex source S = {4}
    ax = axes[1]
    vert_S = {4}
    colors = ['#ef4444' if v in vert_S else '#cbd5e1' for v in G.nodes()]
    draw_base(ax, G, colors)
    ax.set_title(r'Degree-3 vertex source $S = \{4\}$', fontsize=12)

    fig.tight_layout()
    out = os.path.join(OUT_DIR, 'fig_level_source.png')
    fig.savefig(out, dpi=200, bbox_inches='tight')
    plt.close(fig)
    print(f'wrote {out}')


# ---------------------------------------------------------------------------
# Figure 2: Levels (BFS distance from a source)
# ---------------------------------------------------------------------------
def fig_levels():
    G = make_graph()
    source = 4  # degree-3 vertex source
    levels = nx.single_source_shortest_path_length(G, source)

    # Color by level
    palette = {0: '#ef4444', 1: '#f59e0b', 2: '#3b82f6'}
    colors = [palette[levels[v]] for v in G.nodes()]

    # Labels = level numbers
    labels = {v: rf'$\ell={levels[v]}$' for v in G.nodes()}

    fig, ax = plt.subplots(figsize=(6.5, 5.5))
    nx.draw_networkx_edges(G, POS, ax=ax, edge_color='#555', width=1.4)
    nx.draw_networkx_nodes(G, POS, ax=ax, node_color=colors,
                           node_size=720, edgecolors='black', linewidths=1.0)
    # Draw vertex id slightly above, level label inside
    for v, (x, y) in POS.items():
        ax.text(x, y, str(v), ha='center', va='center',
                fontsize=10, fontweight='bold', color='white')
        ax.text(x + 0.18, y + 0.18, rf'$\ell={levels[v]}$',
                fontsize=10, color='black',
                bbox=dict(boxstyle='round,pad=0.15',
                          facecolor='white', edgecolor='#999', linewidth=0.6))
    ax.set_aspect('equal')
    ax.axis('off')
    ax.set_title(r'Levels $\ell_G(v)$ from source $S=\{4\}$', fontsize=12)
    fig.tight_layout()
    out = os.path.join(OUT_DIR, 'fig_levels.png')
    fig.savefig(out, dpi=200, bbox_inches='tight')
    plt.close(fig)
    print(f'wrote {out}')


# ---------------------------------------------------------------------------
# Figure 3: Parity subgraph (even and odd induced subgraphs)
# ---------------------------------------------------------------------------
def fig_parity_subgraph():
    G = make_graph()
    source = 4
    levels = nx.single_source_shortest_path_length(G, source)
    parity = {v: levels[v] % 2 for v in G.nodes()}

    even = [v for v in G.nodes() if parity[v] == 0]
    odd = [v for v in G.nodes() if parity[v] == 1]
    even_color = '#3b82f6'   # blue
    odd_color = '#f59e0b'    # orange

    fig, axes = plt.subplots(1, 3, figsize=(15, 5))

    # Panel A: full triangulation, vertices coloured by parity
    ax = axes[0]
    colors = [even_color if parity[v] == 0 else odd_color for v in G.nodes()]
    draw_base(ax, G, colors)
    ax.set_title(r"$G'$ with vertices coloured by $\ell_G$ mod 2", fontsize=12)

    # Panel B: even parity subgraph (induced on even vertices)
    ax = axes[1]
    # Draw all edges faintly, then the induced subgraph in colour
    nx.draw_networkx_edges(G, POS, ax=ax, edge_color='#ddd', width=1.0)
    even_sub = G.subgraph(even)
    nx.draw_networkx_edges(even_sub, POS, ax=ax, edge_color=even_color, width=2.4)
    node_colors = [even_color if v in even else '#e5e7eb' for v in G.nodes()]
    nx.draw_networkx_nodes(G, POS, ax=ax, node_color=node_colors,
                           node_size=520, edgecolors='black', linewidths=1.0)
    nx.draw_networkx_labels(G, POS, ax=ax,
                            font_color='white', font_size=11, font_weight='bold')
    ax.set_aspect('equal')
    ax.axis('off')
    ax.set_title(r"Even parity subgraph $E_{G,S}(G')$", fontsize=12)

    # Panel C: odd parity subgraph
    ax = axes[2]
    nx.draw_networkx_edges(G, POS, ax=ax, edge_color='#ddd', width=1.0)
    odd_sub = G.subgraph(odd)
    nx.draw_networkx_edges(odd_sub, POS, ax=ax, edge_color=odd_color, width=2.4)
    node_colors = [odd_color if v in odd else '#e5e7eb' for v in G.nodes()]
    nx.draw_networkx_nodes(G, POS, ax=ax, node_color=node_colors,
                           node_size=520, edgecolors='black', linewidths=1.0)
    nx.draw_networkx_labels(G, POS, ax=ax,
                            font_color='white', font_size=11, font_weight='bold')
    ax.set_aspect('equal')
    ax.axis('off')
    ax.set_title(r"Odd parity subgraph $O_{G,S}(G')$", fontsize=12)

    fig.tight_layout()
    out = os.path.join(OUT_DIR, 'fig_parity_subgraph.png')
    fig.savefig(out, dpi=200, bbox_inches='tight')
    plt.close(fig)
    print(f'wrote {out}')


# ---------------------------------------------------------------------------
# Figure: Level cycle (simple cycle within a single level)
# ---------------------------------------------------------------------------
def fig_level_cycle():
    G = make_graph()
    source = 4
    levels = nx.single_source_shortest_path_length(G, source)

    palette = {0: '#ef4444', 1: '#f59e0b', 2: '#3b82f6'}
    colors = [palette[levels[v]] for v in G.nodes()]

    # Level cycle: 1-2-3-1 lies entirely in L_1
    cycle_edges = [(1, 2), (2, 3), (1, 3)]

    fig, ax = plt.subplots(figsize=(6.5, 5.5))
    nx.draw_networkx_edges(G, POS, ax=ax, edge_color='#bbb', width=1.2)
    nx.draw_networkx_edges(G, POS, edgelist=cycle_edges, ax=ax,
                           edge_color='#dc2626', width=3.4)
    nx.draw_networkx_nodes(G, POS, ax=ax, node_color=colors,
                           node_size=620, edgecolors='black', linewidths=1.0)
    nx.draw_networkx_labels(G, POS, ax=ax, font_color='white',
                            font_size=11, font_weight='bold')
    # Annotate levels in small floating labels
    for v, (x, y) in POS.items():
        ax.text(x + 0.18, y + 0.18, rf'$\ell={levels[v]}$',
                fontsize=9, color='black',
                bbox=dict(boxstyle='round,pad=0.12',
                          facecolor='white', edgecolor='#999', linewidth=0.5))
    ax.set_aspect('equal')
    ax.axis('off')
    ax.set_title(r'Level cycle in $L_1 = \{1,2,3\}$ (highlighted)', fontsize=12)
    fig.tight_layout()
    out = os.path.join(OUT_DIR, 'fig_level_cycle.png')
    fig.savefig(out, dpi=200, bbox_inches='tight')
    plt.close(fig)
    print(f'wrote {out}')


# ---------------------------------------------------------------------------
# Figure: Edge switch (flip on a level-cycle edge)
# ---------------------------------------------------------------------------
def fig_edge_switch():
    G = make_graph()
    source = 4
    levels = nx.single_source_shortest_path_length(G, source)
    palette = {0: '#ef4444', 1: '#f59e0b', 2: '#3b82f6'}
    colors = [palette[levels[v]] for v in G.nodes()]

    # We switch edge (1,2), which lies in the L_1 cycle 1-2-3-1.
    # Its two adjacent faces in T are (0,1,2) and (1,2,4); the flip
    # removes 1-2 and adds 0-4.
    removed = (1, 2)
    added = (0, 4)

    Gprime = G.copy()
    Gprime.remove_edge(*removed)
    Gprime.add_edge(*added)

    fig, axes = plt.subplots(1, 2, figsize=(12, 5.5))

    # Panel A: before — highlight the level-cycle edge to be switched
    ax = axes[0]
    other_edges = [e for e in G.edges() if set(e) != set(removed)]
    nx.draw_networkx_edges(G, POS, edgelist=other_edges, ax=ax,
                           edge_color='#bbb', width=1.2)
    nx.draw_networkx_edges(G, POS, edgelist=[removed], ax=ax,
                           edge_color='#dc2626', width=3.4)
    nx.draw_networkx_nodes(G, POS, ax=ax, node_color=colors,
                           node_size=560, edgecolors='black', linewidths=1.0)
    nx.draw_networkx_labels(G, POS, ax=ax, font_color='white',
                            font_size=11, font_weight='bold')
    ax.set_aspect('equal'); ax.axis('off')
    ax.set_title(r'Before: edge $1\!-\!2$ lies on the $L_1$ cycle',
                 fontsize=12)

    # Panel B: after — the new edge highlighted in green
    ax = axes[1]
    other_edges = [e for e in Gprime.edges() if set(e) != set(added)]
    nx.draw_networkx_edges(Gprime, POS, edgelist=other_edges, ax=ax,
                           edge_color='#bbb', width=1.2)
    nx.draw_networkx_edges(Gprime, POS, edgelist=[added], ax=ax,
                           edge_color='#16a34a', width=3.4)
    nx.draw_networkx_nodes(Gprime, POS, ax=ax, node_color=colors,
                           node_size=560, edgecolors='black', linewidths=1.0)
    nx.draw_networkx_labels(Gprime, POS, ax=ax, font_color='white',
                            font_size=11, font_weight='bold')
    ax.set_aspect('equal'); ax.axis('off')
    ax.set_title(r'After: $1\!-\!2$ replaced by $0\!-\!4$',
                 fontsize=12)

    fig.tight_layout()
    out = os.path.join(OUT_DIR, 'fig_edge_switch.png')
    fig.savefig(out, dpi=200, bbox_inches='tight')
    plt.close(fig)
    print(f'wrote {out}')


# ---------------------------------------------------------------------------
# Figure: Facial depth (depths in an outerplanar L_k)
# ---------------------------------------------------------------------------
def fig_facial_depth():
    import math
    # 12-vertex maximal outerplanar graph with 3-fold symmetry.
    # Central triangle (0,4,8); three "in-between" triangles (0,2,4),
    # (4,6,8), (0,8,10) sit between the central triangle and the
    # outer "ears" (0,1,2), (2,3,4), (4,5,6), (6,7,8), (8,9,10),
    # (10,11,0).
    n = 12
    pos = {}
    for i in range(n):
        a = math.radians(90 - i * (360 / n))
        pos[i] = (math.cos(a), math.sin(a))

    outer_edges = [(i, (i + 1) % n) for i in range(n)]
    diagonals = [(0, 2), (2, 4), (4, 6), (6, 8), (8, 10), (10, 0),  # "short" chords
                 (0, 4), (4, 8), (0, 8)]                            # central triangle
    L = nx.Graph()
    L.add_nodes_from(pos)
    L.add_edges_from(outer_edges + diagonals)

    inner_faces = [
        (0, 1, 2), (2, 3, 4), (4, 5, 6),
        (6, 7, 8), (8, 9, 10), (10, 11, 0),     # 6 outer "ears"
        (0, 2, 4), (4, 6, 8), (0, 8, 10),        # 3 in-between
        (0, 4, 8),                                # central
    ]

    # Build dual graph on inner faces: edge iff faces share an edge
    def face_edges(f):
        a, b, c = f
        return {frozenset((a, b)), frozenset((b, c)), frozenset((a, c))}

    outer_edge_set = {frozenset(e) for e in outer_edges}
    D = nx.Graph()
    D.add_nodes_from(range(len(inner_faces)))
    for i, fi in enumerate(inner_faces):
        for j, fj in enumerate(inner_faces):
            if i < j and face_edges(fi) & face_edges(fj):
                D.add_edge(i, j)

    # Boundary set B: faces whose bounding level cycle has >= 1 outer-cycle edge
    B = [i for i, f in enumerate(inner_faces)
         if len(face_edges(f) & outer_edge_set) >= 1]

    # Depth = min distance in D to any face in B
    depth = {}
    for i in range(len(inner_faces)):
        dists = [nx.shortest_path_length(D, i, b) for b in B]
        depth[i] = min(dists)

    # Colour by depth
    depth_color = {0: '#86efac', 1: '#fde68a', 2: '#fca5a5'}
    depth_edge = {0: '#16a34a', 1: '#d97706', 2: '#dc2626'}

    fig, ax = plt.subplots(figsize=(7, 7))
    # Fill faces by depth
    for i, f in enumerate(inner_faces):
        poly = Polygon([pos[v] for v in f], closed=True,
                       facecolor=depth_color[depth[i]],
                       edgecolor=depth_edge[depth[i]],
                       linewidth=1.5, alpha=0.7, zorder=0)
        ax.add_patch(poly)
        cx = sum(pos[v][0] for v in f) / 3
        cy = sum(pos[v][1] for v in f) / 3
        ax.text(cx, cy, rf'$\mathrm{{depth}}={depth[i]}$',
                ha='center', va='center', fontsize=10,
                color=depth_edge[depth[i]], fontweight='bold')

    # Draw the graph on top
    nx.draw_networkx_edges(L, pos, ax=ax, edge_color='#333', width=1.4)
    nx.draw_networkx_nodes(L, pos, ax=ax, node_color='#1f2937',
                           node_size=320, edgecolors='black', linewidths=1.0)
    nx.draw_networkx_labels(L, pos, ax=ax, font_color='white',
                            font_size=10, font_weight='bold')

    ax.set_aspect('equal'); ax.axis('off')
    ax.set_xlim(-1.3, 1.3); ax.set_ylim(-1.3, 1.3)
    ax.set_title(r'Facial depth in an outerplanar $L_k$', fontsize=12)
    fig.tight_layout()
    out = os.path.join(OUT_DIR, 'fig_facial_depth.png')
    fig.savefig(out, dpi=200, bbox_inches='tight')
    plt.close(fig)
    print(f'wrote {out}')


if __name__ == '__main__':
    fig_level_source()
    fig_levels()
    fig_level_cycle()
    fig_edge_switch()
    fig_parity_subgraph()
    fig_facial_depth()
