"""Draw the iterated reduction algorithm's trace on the dodecahedron.

Produces three figures:
  fig_alg_step0.png -- G' (dodecahedron) with F_v (inner pentagon) shaded.
  fig_alg_step1.png -- H_1 (post step 1), 3-edge-coloured; 4 protected edges.
  fig_alg_step2.png -- H_2 (post step 2), 3-edge-coloured; 8 protected edges;
                       algorithm terminates.

Run with: sage experiments/draw_iterated_reduction.py
"""
from sage.all import Graph
from sage.graphs.graph_coloring import edge_coloring
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
import math
import os

OUT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

C = ['#dc2626', '#16a34a', '#2563eb']     # proper-edge-colour palette
GRAY = '#9ca3af'
DARK = '#374151'
HIGHLIGHT = '#fef3c7'


def dodecahedron_positions():
    pos = {}
    R = {'a': 1.0, 'b': 2.2, 'c': 3.6, 'd': 4.8}
    for i in range(5):
        for fam in ('a', 'b'):
            th = math.radians(90 - 72 * i)
            pos[(fam, i)] = (R[fam] * math.cos(th), R[fam] * math.sin(th))
        for fam in ('c', 'd'):
            th = math.radians(90 - 72 * i - 36)
            pos[(fam, i)] = (R[fam] * math.cos(th), R[fam] * math.sin(th))
    return pos


def build_dodecahedron():
    edges = []
    for i in range(5):
        edges.append((('a', i), ('a', (i + 1) % 5)))
        edges.append((('a', i), ('b', i)))
        edges.append((('b', i), ('c', i)))
        edges.append((('b', i), ('c', (i - 1) % 5)))
        edges.append((('c', i), ('d', i)))
        edges.append((('d', i), ('d', (i + 1) % 5)))
    G = Graph(edges, multiedges=False, loops=False)
    G.is_planar(set_embedding=True)
    return G


def find_safe_pentagonal_face(G, protected):
    for face in G.faces():
        if len(face) != 5:
            continue
        boundary = [u for (u, v) in face]
        boundary_edges = [frozenset([u, v]) for (u, v) in face]
        externals = []
        A = []
        for B_k in boundary:
            outer = [w for w in G.neighbor_iterator(B_k) if w not in boundary]
            if len(outer) != 1:
                break
            externals.append(frozenset([B_k, outer[0]]))
            A.append(outer[0])
        else:
            if not any(e in protected for e in boundary_edges + externals):
                return boundary, externals, A
    return None


def valid_indices(f_vec):
    out = []
    for i in range(5):
        if f_vec[(i + 3) % 5] != f_vec[(i + 4) % 5]:
            continue
        if len({f_vec[i], f_vec[(i + 1) % 5], f_vec[(i + 2) % 5]}) == 3:
            out.append(i)
    return out


def draw(ax, G, pos, *, coloring=None, protected=None,
         shade_face=None):
    if shade_face:
        poly = [pos[v] for v in shade_face]
        ax.add_patch(Polygon(poly, closed=True, facecolor=HIGHLIGHT,
                             edgecolor='none', zorder=0))
    protected = protected or set()
    for u, v in G.edges(labels=False):
        e = frozenset([u, v])
        c = C[coloring[e]] if coloring is not None else GRAY
        lw = 3.8 if e in protected else 1.4
        (x0, y0), (x1, y1) = pos[u], pos[v]
        ax.plot([x0, x1], [y0, y1], color=c, lw=lw, zorder=2)
    for v in G.vertices(sort=False):
        x, y = pos[v]
        if isinstance(v, tuple) and v[0] == 'v_n':
            t = v[1]
            ax.scatter(x, y, s=320, color=HIGHLIGHT, marker='s',
                       edgecolors='black', linewidths=1.2, zorder=4)
            ax.annotate(f'$v_n^{{({t})}}$', (x, y),
                        textcoords='offset points', xytext=(16, 16),
                        ha='left', fontsize=14, fontweight='bold',
                        color=DARK, zorder=6,
                        bbox=dict(boxstyle='round,pad=0.2', fc='white',
                                  ec=DARK, lw=0.6))
        else:
            ax.scatter(x, y, s=70, color=DARK, zorder=3)
    ax.set_aspect('equal')
    ax.axis('off')


def main():
    G = build_dodecahedron()
    pos = dodecahedron_positions()
    F_v = [('a', i) for i in range(5)]

    # ----- Step 0: G' with F_v shaded -----
    fig, ax = plt.subplots(figsize=(8, 8))
    draw(ax, G, pos, shade_face=F_v)
    fig.savefig(os.path.join(OUT_DIR, 'fig_alg_step0.png'),
                dpi=170, bbox_inches='tight')
    plt.close(fig)

    # ----- Step 1: Definition 2.1 at F_v with i_1 = 0 -----
    safe = find_safe_pentagonal_face(G, set())
    boundary_1, externals_1, A_1 = safe
    G1 = G.copy()
    for v in boundary_1:
        G1.delete_vertex(v)
    v_n_1 = ('v_n', 1)
    G1.add_vertex(v_n_1)
    G1.add_edge(v_n_1, A_1[0])
    G1.add_edge(v_n_1, A_1[1])
    G1.add_edge(v_n_1, A_1[2])
    G1.add_edge(A_1[3], A_1[4])
    G1.is_planar(set_embedding=True)
    pos1 = {v: p for v, p in pos.items() if v not in boundary_1}
    cx = (pos[A_1[0]][0] + pos[A_1[1]][0] + pos[A_1[2]][0]) / 3
    cy = (pos[A_1[0]][1] + pos[A_1[1]][1] + pos[A_1[2]][1]) / 3
    pos1[v_n_1] = (cx * 0.55, cy * 0.55)

    cols = edge_coloring(G1, value_only=False)
    coloring = {}
    for k, edge_list in enumerate(cols):
        for u, v in edge_list:
            coloring[frozenset([u, v])] = k

    E = {
        frozenset([v_n_1, A_1[1]]),    # spike
        frozenset([v_n_1, A_1[0]]),    # side-0
        frozenset([v_n_1, A_1[2]]),    # side-1
        frozenset([A_1[3], A_1[4]]),   # merged
    }

    fig, ax = plt.subplots(figsize=(8, 8))
    draw(ax, G1, pos1, coloring=coloring, protected=E)
    fig.savefig(os.path.join(OUT_DIR, 'fig_alg_step1.png'),
                dpi=170, bbox_inches='tight')
    plt.close(fig)

    # ----- Step 2: reduce at the only remaining safe face (outer pentagon) -----
    safe = find_safe_pentagonal_face(G1, E)
    if safe is None:
        print("ERROR: expected an outer pentagonal face but none found.")
        return
    boundary_2, externals_2, A_2 = safe
    f_vec = [coloring[e] for e in externals_2]
    choices = valid_indices(f_vec)
    if not choices:
        print(f"ERROR: f-vector {f_vec} has no valid index.")
        return
    i_t = choices[0]

    G2 = G1.copy()
    for v in boundary_2:
        G2.delete_vertex(v)
    v_n_2 = ('v_n', 2)
    G2.add_vertex(v_n_2)
    G2.add_edge(v_n_2, A_2[i_t])
    G2.add_edge(v_n_2, A_2[(i_t + 1) % 5])
    G2.add_edge(v_n_2, A_2[(i_t + 2) % 5])
    G2.add_edge(A_2[(i_t + 3) % 5], A_2[(i_t + 4) % 5])
    G2.is_planar(set_embedding=True)

    coloring2 = {e: c for e, c in coloring.items()
                 if not any(u in boundary_2 for u in e)}
    side_0_2 = frozenset([v_n_2, A_2[i_t]])
    spike_2  = frozenset([v_n_2, A_2[(i_t + 1) % 5]])
    side_1_2 = frozenset([v_n_2, A_2[(i_t + 2) % 5]])
    merged_2 = frozenset([A_2[(i_t + 3) % 5], A_2[(i_t + 4) % 5]])
    coloring2[side_0_2] = coloring[externals_2[i_t]]
    coloring2[spike_2]  = coloring[externals_2[(i_t + 1) % 5]]
    coloring2[side_1_2] = coloring[externals_2[(i_t + 2) % 5]]
    coloring2[merged_2] = coloring[externals_2[(i_t + 3) % 5]]

    pos2 = {v: p for v, p in pos1.items() if v not in boundary_2}
    nbrs = [A_2[i_t], A_2[(i_t + 1) % 5], A_2[(i_t + 2) % 5]]
    cx = sum(pos2[a][0] for a in nbrs) / 3
    cy = sum(pos2[a][1] for a in nbrs) / 3
    r = math.hypot(cx, cy)
    # v_n^{(2)} lies outside the surviving graph (the deleted d's were outermost)
    target_r = 5.0
    pos2[v_n_2] = (cx * target_r / r, cy * target_r / r)

    E |= {side_0_2, spike_2, side_1_2, merged_2}

    fig, ax = plt.subplots(figsize=(8, 8))
    draw(ax, G2, pos2, coloring=coloring2, protected=E)
    fig.savefig(os.path.join(OUT_DIR, 'fig_alg_step2.png'),
                dpi=170, bbox_inches='tight')
    plt.close(fig)

    print(f"Wrote fig_alg_step{{0,1,2}}.png to {OUT_DIR}")


if __name__ == '__main__':
    main()
