"""Reduced dual: construction and verification.

Test input is the icosahedron G (the unique 5-regular triangulation, n=12).
Its dual G' is the dodecahedron (a cubic plane graph, 20 vertices). We pick a
degree-5 vertex v of G -- equivalently a pentagonal face F_v of G' -- and apply
the reduced-dual construction of Definition 2.1:

  1. delete the 5 dual vertices on the boundary of F_v (and incident edges),
     leaving 5 degree-2 vertices on a new face F;
  2. order those 5 vertices clockwise around F as A_0..A_4;
  3. add a vertex v_n joined to A_i, A_{i+1}, A_{i+2};
  4. add an edge A_{i+3} A_{i+4}.

We verify the result is again a cubic plane graph, and report the triangulation
it is the dual of (its face count = the primal vertex count), to see how the
vertex count changes relative to n.

The dodecahedron is built directly in its concentric "Schlegel" layout with
F_v the inner pentagon, so the figures (draw_reduced_dual_steps.py) are clean.
"""
import math
import networkx as nx

# ---------------------------------------------------------------------------
# Build G' = dodecahedron with concentric positions; F_v = inner pentagon.
# Vertex families a (inner pentagon), b, c, d (outer pentagon), 5 each.
# Angles increase *clockwise* (90 - 72*i deg) so index order is clockwise.
# ---------------------------------------------------------------------------
def build_dual():
    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)   # offset half a step
            pos[(fam, i)] = (R[fam] * math.cos(th), R[fam] * math.sin(th))

    Gp = nx.Graph()
    Gp.add_nodes_from(pos)
    for i in range(5):
        Gp.add_edge(('a', i), ('a', (i + 1) % 5))        # inner pentagon
        Gp.add_edge(('a', i), ('b', i))                  # spokes a-b
        Gp.add_edge(('b', i), ('c', i))                  # b-c
        Gp.add_edge(('b', i), ('c', (i - 1) % 5))        # b-c (other side)
        Gp.add_edge(('c', i), ('d', i))                  # spokes c-d
        Gp.add_edge(('d', i), ('d', (i + 1) % 5))        # outer pentagon

    Fv_boundary = [('a', i) for i in range(5)]           # inner pentagon
    return Gp, pos, Fv_boundary


# ---------------------------------------------------------------------------
# Face / dual helpers.
# ---------------------------------------------------------------------------
def faces_of(G):
    """Return the list of faces (each a list of vertices) of a plane graph."""
    ok, emb = nx.check_planarity(G)
    assert ok, "graph is not planar"
    seen, faces = set(), []
    for u in emb:
        for v in emb[u]:
            if (u, v) not in seen:
                faces.append(emb.traverse_face(u, v, mark_half_edges=seen))
    return faces


def dual_of(G):
    """Combinatorial dual (all faces, including outer) of a plane graph."""
    faces = faces_of(G)
    edge_faces = {}
    for fi, face in enumerate(faces):
        for j in range(len(face)):
            e = frozenset((face[j], face[(j + 1) % len(face)]))
            edge_faces.setdefault(e, []).append(fi)
    D = nx.MultiGraph()
    D.add_nodes_from(range(len(faces)))
    for e, fs in edge_faces.items():
        if len(fs) == 2:
            D.add_edge(fs[0], fs[1])
        elif len(fs) == 1:                # shouldn't happen for 2-connected G
            pass
    return D, faces


# ---------------------------------------------------------------------------
# The reduced-dual construction.
# ---------------------------------------------------------------------------
def clockwise_order(verts, pos):
    """Order verts clockwise around their centroid, starting from the topmost."""
    cx = sum(pos[v][0] for v in verts) / len(verts)
    cy = sum(pos[v][1] for v in verts) / len(verts)
    ang = {v: math.atan2(pos[v][1] - cy, pos[v][0] - cx) for v in verts}
    ccw = sorted(verts, key=lambda v: ang[v])            # counterclockwise
    cw = list(reversed(ccw))                             # clockwise
    start = max(range(len(cw)), key=lambda k: pos[cw[k]][1])   # topmost first
    return cw[start:] + cw[:start]


def apply_reduction(Gp, pos, Fv_boundary, i=0):
    """Apply Definition 2.1 and return a dict capturing each stage."""
    Ghat = Gp.copy()
    npos = dict(pos)

    # (1) delete the 5 boundary dual vertices of F_v
    Ghat.remove_nodes_from(Fv_boundary)
    deg2 = [v for v in Ghat if Ghat.degree(v) == 2]
    assert len(deg2) == 5, f"expected 5 degree-2 vertices, got {len(deg2)}"

    # (2) order them clockwise around the new face F
    A = clockwise_order(deg2, pos)

    # (3) new vertex v_n joined to A_i, A_{i+1}, A_{i+2}
    apex_nbrs = [A[(i + k) % 5] for k in range(3)]
    ax = sum(npos[v][0] for v in apex_nbrs) / 3
    ay = sum(npos[v][1] for v in apex_nbrs) / 3
    v_n = 'v_n'
    npos[v_n] = (ax * 0.55, ay * 0.55)                   # pull toward the 3 nbrs
    Ghat.add_node(v_n)
    for u in apex_nbrs:
        Ghat.add_edge(v_n, u)

    # (4) chord between the remaining two
    chord = (A[(i + 3) % 5], A[(i + 4) % 5])
    Ghat.add_edge(*chord)

    return {
        'Ghat': Ghat, 'pos': npos, 'A': A, 'v_n': v_n,
        'apex_nbrs': apex_nbrs, 'chord': chord,
        'deleted': list(Fv_boundary),
    }


def main():
    Gp, pos, Fv = build_dual()

    # --- verify G' is the dodecahedron = dual of the icosahedron ---
    assert nx.check_planarity(Gp)[0]
    assert all(d == 3 for _, d in Gp.degree()), "G' not cubic"
    assert nx.is_isomorphic(Gp, nx.dodecahedral_graph()), "G' is not dodecahedron"
    Dico, _ = dual_of(Gp)
    Dico = nx.Graph(Dico)
    print(f"G  (icosahedron) : dual of G' has {Dico.number_of_nodes()} vertices, "
          f"degrees {sorted({d for _, d in Dico.degree()})}")
    print(f"G' (dodecahedron): {Gp.number_of_nodes()} vertices, "
          f"{Gp.number_of_edges()} edges, "
          f"{len(faces_of(Gp))} faces; cubic={all(d==3 for _,d in Gp.degree())}")

    # --- apply the reduced-dual construction ---
    res = apply_reduction(Gp, pos, Fv, i=0)
    Ghat = res['Ghat']
    cubic = all(d == 3 for _, d in Ghat.degree())
    planar = nx.check_planarity(Ghat)[0]
    ghat_simple = (nx.number_of_selfloops(Ghat) == 0)   # Graph: no parallels
    nfaces = len(faces_of(Ghat))
    print()
    print(f"reduced dual  G^_v,i : {Ghat.number_of_nodes()} vertices, "
          f"{Ghat.number_of_edges()} edges, {nfaces} faces")
    print(f"  cubic   : {cubic}")
    print(f"  planar  : {planar}")
    print(f"  simple  : {ghat_simple}")

    # --- the triangulation it is dual to ---
    Dred_multi, _ = dual_of(Ghat)
    Dred = nx.Graph(Dred_multi)
    dred_simple = (Dred.number_of_edges() == Dred_multi.number_of_edges())
    is_tri = all(len(f) == 3 for f in faces_of(Dred)) if planar else None
    print()
    print(f"dual of reduced dual : {Dred.number_of_nodes()} vertices "
          f"(= faces of G^), degree seq "
          f"{sorted((d for _, d in Dred.degree()), reverse=True)}")
    print(f"  is a triangulation : {is_tri}")
    print(f"  simple             : {dred_simple}")

    n = Dico.number_of_nodes()
    print()
    print(f"VERTEX COUNT: G has n = {n}; reduced triangulation has "
          f"{Dred.number_of_nodes()} (change = "
          f"{Dred.number_of_nodes() - n}).")


if __name__ == '__main__':
    main()
