"""Iterated reduced-dual reduction with protected edges (Sage).

Implements Algorithm 3.1 from paper.tex:

  0. G' = dual of G (a triangulation conjectured to be a minimal
     counterexample).
  1. Apply Definition 2.1 to G' at a pentagonal face to get H_1; find a
     proper 3-edge-colouring phi_1 of H_1.
  2. Initialise protected edge set E := {spike, side-0, side-1, merged} from
     step 1.
  3. Iterate: find a pentagonal face of H_{t-1} whose 10 incident edges are
     disjoint from E, pick a valid index i_t (Lemma 2.4), apply
     Definition 2.1, extend phi_{t-1} to phi_t, and add the four new named
     edges to E.
  4. Terminate when no safe pentagonal face exists.

We run on the icosahedron-dodecahedron pair as a concrete trace; the
icosahedron is 4-colourable, so the dodecahedron is 3-edge-colourable and
the algorithm terminates combinatorially.

Run with:
    sage experiments/iterated_reduction.py
"""
from sage.all import Graph
from sage.graphs.graph_coloring import edge_coloring


# ---------------------------------------------------------------------------
# Build G' = dodecahedron with the same naming as experiments/reduced_dual.py:
# vertex families a (inner pentagon, will play the role of partial F_v's
# boundary vertices for the first reduction), b, c, d.
# ---------------------------------------------------------------------------
def build_dodecahedron():
    edges = []
    for i in range(5):
        edges.append((('a', i), ('a', (i + 1) % 5)))   # inner pentagon
        edges.append((('a', i), ('b', i)))             # spoke a-b
        edges.append((('b', i), ('c', i)))             # b-c
        edges.append((('b', i), ('c', (i - 1) % 5)))   # b-c (other side)
        edges.append((('c', i), ('d', i)))             # spoke c-d
        edges.append((('d', i), ('d', (i + 1) % 5)))   # outer pentagon
    G = Graph(edges, multiedges=False, loops=False)
    assert G.is_planar(set_embedding=True), "dodecahedron not planar?"
    return G


# ---------------------------------------------------------------------------
# 3-edge-colour a cubic plane graph; return None if not 3-edge-colourable.
# ---------------------------------------------------------------------------
def proper_3_coloring(G):
    classes = edge_coloring(G, value_only=False)
    if len(classes) > 3:
        return None
    out = {}
    for k, edge_list in enumerate(classes):
        for u, v in edge_list:
            out[frozenset([u, v])] = k
    return out


# ---------------------------------------------------------------------------
# Face search: pentagonal face whose 10 incident edges are outside `protected`.
# Returns (boundary, externals, A) or None.
# ---------------------------------------------------------------------------
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]
        # the external edge at each boundary vertex (the one not on the 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:
                # In a cubic graph each face-boundary vertex has exactly one
                # external edge; otherwise something is off.
                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


# ---------------------------------------------------------------------------
# Lemma 2.4: any proper 3-edge-colouring forces the external vector at a
# pentagonal face to have shape (a, b, c, c, c) up to cyclic rotation. The
# valid index i for the reduction is one where positions i+3, i+4 host the
# doubled colour and positions i, i+1, i+2 host three distinct colours.
# ---------------------------------------------------------------------------
def valid_indices(f_vec):
    out = []
    for i in range(5):
        if f_vec[(i + 3) % 5] != f_vec[(i + 4) % 5]:
            continue
        triple = {f_vec[i], f_vec[(i + 1) % 5], f_vec[(i + 2) % 5]}
        if len(triple) == 3:
            out.append(i)
    return out


# ---------------------------------------------------------------------------
# Apply Definition 2.1 at (F, i): delete the 5 boundary vertices, add v_n
# connected to A[i], A[i+1], A[i+2], add the chord A[i+3]-A[i+4]. Extend the
# colouring: every surviving edge keeps its colour; each new edge takes the
# colour of the deleted external at the same A_k (the unique third colour at
# A_k under the surviving edges).
# ---------------------------------------------------------------------------
def apply_reduction(G, boundary, externals, A, coloring, i, v_n_label):
    G_new = G.copy()
    for v in boundary:
        G_new.delete_vertex(v)
    G_new.add_vertex(v_n_label)
    side_0 = (v_n_label, A[i])
    spike = (v_n_label, A[(i + 1) % 5])
    side_1 = (v_n_label, A[(i + 2) % 5])
    merged = (A[(i + 3) % 5], A[(i + 4) % 5])
    G_new.add_edges([side_0, spike, side_1, merged])
    assert G_new.is_planar(set_embedding=True), "reduction broke planarity"

    coloring_new = {
        e: c for e, c in coloring.items() if not any(u in boundary for u in e)
    }
    coloring_new[frozenset(side_0)] = coloring[externals[i]]
    coloring_new[frozenset(spike)] = coloring[externals[(i + 1) % 5]]
    coloring_new[frozenset(side_1)] = coloring[externals[(i + 2) % 5]]
    coloring_new[frozenset(merged)] = coloring[externals[(i + 3) % 5]]

    named = {
        'spike': frozenset(spike),
        'side_0': frozenset(side_0),
        'side_1': frozenset(side_1),
        'merged': frozenset(merged),
    }
    return G_new, coloring_new, named


# ---------------------------------------------------------------------------
# Sanity check: verify that a coloring is proper (each vertex sees 3 colours).
# ---------------------------------------------------------------------------
def is_proper_3_coloring(G, coloring):
    for v in G.vertex_iterator():
        seen = set()
        for u in G.neighbor_iterator(v):
            seen.add(coloring[frozenset([u, v])])
        if len(seen) != G.degree(v):
            return False
    return True


# ---------------------------------------------------------------------------
# Main driver.
# ---------------------------------------------------------------------------
def run_algorithm(max_iterations=50, verbose=True):
    if verbose:
        print("STEP 0: G' = dodecahedron (dual of the icosahedron)")
    G_prime = build_dodecahedron()
    if verbose:
        print(f"  |V(G')| = {G_prime.order()}, |E(G')| = {G_prime.size()}")

    # ---- STEP 1: apply Definition 2.1 to G' at the inner pentagon, i_1 = 0
    face_data = find_safe_pentagonal_face(G_prime, set())
    if face_data is None:
        print("  No pentagonal face in G'.")
        return
    boundary, externals, A = face_data

    H = G_prime.copy()
    for v in boundary:
        H.delete_vertex(v)
    v_n_label = ('v_n', 1)
    H.add_vertex(v_n_label)
    side_0 = (v_n_label, A[0])
    spike  = (v_n_label, A[1])
    side_1 = (v_n_label, A[2])
    merged = (A[3], A[4])
    H.add_edges([side_0, spike, side_1, merged])
    assert H.is_planar(set_embedding=True)

    coloring = proper_3_coloring(H)
    if coloring is None:
        print("  H_1 is not 3-edge-colourable; algorithm halts.")
        return
    assert is_proper_3_coloring(H, coloring)
    if verbose:
        print(f"STEP 1: H_1 = reduced dual at the first face, i_1 = 0")
        print(f"  |V(H_1)| = {H.order()}, |E(H_1)| = {H.size()}; "
              f"3-edge-coloured.")

    # ---- STEP 2: initialise the protected set
    E = {
        frozenset(spike),
        frozenset(side_0),
        frozenset(side_1),
        frozenset(merged),
    }
    if verbose:
        print(f"STEP 2: |E_protected| = {len(E)}")

    # ---- STEP 3-4: iterate
    for t in range(2, max_iterations + 1):
        face_data = find_safe_pentagonal_face(H, E)
        if face_data is None:
            if verbose:
                print(f"\nTerminated at iteration t = {t}: "
                      f"no pentagonal face avoids E.")
            break
        boundary, externals, A = face_data
        f_vec = [coloring[e] for e in externals]
        choices = valid_indices(f_vec)
        if not choices:
            print(f"  iter t = {t}: f-vector {f_vec} has no valid index "
                  f"(Lemma 2.4 should preclude this --- bug!)")
            break
        i_t = choices[0]
        v_n_label = ('v_n', t)
        H, coloring, named = apply_reduction(
            H, boundary, externals, A, coloring, i_t, v_n_label)
        assert is_proper_3_coloring(H, coloring)
        E |= set(named.values())
        if verbose:
            print(f"  iter t = {t:>2}: f = {f_vec}, chose i_t = {i_t}; "
                  f"|V| = {H.order():>2}, |E_graph| = {H.size():>2}, "
                  f"|E_protected| = {len(E):>2}")
    else:
        if verbose:
            print(f"\nReached max_iterations = {max_iterations}.")

    if verbose:
        print(f"\nFinal H: |V| = {H.order()}, |E| = {H.size()}, "
              f"|E_protected| = {len(E)}")


if __name__ == '__main__':
    run_algorithm()
