"""Check empirically whether all proper 3-edge-colorings of a cubic plane
graph form a single Kempe equivalence class.

For each test graph:
  1. enumerate all proper 3-edge-colorings;
  2. build a multigraph K whose vertices are the colorings and whose edges
     connect pairs related by a single Kempe swap (swapping two colors on
     one cycle of the 2-color subgraph);
  3. report the number of connected components of K (= number of Kempe
     classes).

Test inputs:
  - Dodecahedron's reduced dual (16 v, 24 e), built as in
    check_dodecahedron_kempe.py;
  - The n=14 reduced dual found by search_kempe_property.py (20 v, 30 e).
"""
from sage.all import Graph
from sage.graphs.graph_generators import graphs
import sys


def all_proper_3_edge_colorings(G):
    edges = list(G.edges(labels=False))
    n = len(edges)
    adj = [[] for _ in range(n)]
    for i in range(n):
        u, v = edges[i][0], edges[i][1]
        for j in range(i):
            x, y = edges[j][0], edges[j][1]
            if u in (x, y) or v in (x, y):
                adj[i].append(j)
                adj[j].append(i)
    coloring = [-1] * n
    results = []

    def back(k):
        if k == n:
            results.append(tuple(coloring))
            return
        for c in range(3):
            if all(coloring[j] != c for j in adj[k]):
                coloring[k] = c
                back(k + 1)
        coloring[k] = -1
    back(0)
    return edges, results


def kempe_cycles_of(edges, coloring, color_pair):
    """List of connected components (as sorted tuples of edge indices) in
    the subgraph of edges with color in color_pair."""
    a, b = color_pair
    in_sub = [i for i in range(len(edges)) if coloring[i] in (a, b)]
    in_set = set(in_sub)
    visited = set()
    components = []
    for start in in_sub:
        if start in visited:
            continue
        comp = []
        stack = [start]
        visited.add(start)
        while stack:
            cur = stack.pop()
            comp.append(cur)
            u, v = edges[cur][0], edges[cur][1]
            for j in in_set:
                if j in visited:
                    continue
                x, y = edges[j][0], edges[j][1]
                if u in (x, y) or v in (x, y):
                    visited.add(j)
                    stack.append(j)
        components.append(tuple(sorted(comp)))
    return components


def swap_on_cycle(coloring, cycle_edges, color_pair):
    a, b = color_pair
    swap = list(coloring)
    for i in cycle_edges:
        if swap[i] == a:
            swap[i] = b
        elif swap[i] == b:
            swap[i] = a
    return tuple(swap)


def kempe_components(G):
    edges, colorings = all_proper_3_edge_colorings(G)
    print(f"  {len(colorings)} proper 3-edge-colorings")
    idx = {c: i for i, c in enumerate(colorings)}
    parent = list(range(len(colorings)))

    def find(x):
        while parent[x] != x:
            parent[x] = parent[parent[x]]
            x = parent[x]
        return x

    def union(x, y):
        rx, ry = find(x), find(y)
        if rx != ry:
            parent[rx] = ry

    for col in colorings:
        for a, b in [(0, 1), (0, 2), (1, 2)]:
            for cyc in kempe_cycles_of(edges, col, (a, b)):
                new_col = swap_on_cycle(col, cyc, (a, b))
                if new_col == col:
                    continue
                if new_col in idx:
                    union(idx[col], idx[new_col])
    n_classes = len({find(i) for i in range(len(colorings))})
    print(f"  Kempe equivalence classes: {n_classes}")
    return n_classes


def dodecahedron_reduced_dual():
    edges = []
    for k in range(5):
        edges.append((('b', k), ('c', k)))
        edges.append((('b', k), ('c', (k - 1) % 5)))
        edges.append((('c', k), ('d', k)))
        edges.append((('d', k), ('d', (k + 1) % 5)))
    edges.append(('v_n', ('b', 0)))
    edges.append(('v_n', ('b', 1)))
    edges.append(('v_n', ('b', 2)))
    edges.append((('b', 3), ('b', 4)))
    return Graph(edges, multiedges=False, loops=False)


def n14_reduced_dual():
    # First min-degree-5 plantri triangulation on 14 vertices, reduced at the
    # first pentagonal face with i_red = 1 (matches search_kempe_property.py).
    G = next(graphs.triangulations(14, minimum_degree=5))
    G.is_planar(set_embedding=True)
    faces = G.faces()
    edge_to_faces = {}
    for fi, face in enumerate(faces):
        for u, v in face:
            edge_to_faces.setdefault(frozenset((u, v)), []).append(fi)
    dual_edges = [(fs[0], fs[1]) for fs in edge_to_faces.values() if len(fs) == 2]
    D = Graph(dual_edges, multiedges=False, loops=False)
    D.is_planar(set_embedding=True)
    # use first pentagonal face, i_red = 1
    face = next(f for f in D.faces() if len(f) == 5)
    boundary = [u for (u, v) in face]
    A = []
    for B_k in boundary:
        outer = [w for w in D.neighbor_iterator(B_k) if w not in boundary]
        A.append(outer[0])
    H = D.copy()
    for v in boundary:
        H.delete_vertex(v)
    vn = '__v_n__'
    H.add_vertex(vn)
    H.add_edge(vn, A[1])             # spike
    H.add_edge(vn, A[0])             # side_0
    H.add_edge(vn, A[2])             # side_1
    H.add_edge(A[3], A[4])           # merged
    H.is_planar(set_embedding=True)
    return H


def main():
    print("Dodecahedron's reduced dual:")
    G1 = dodecahedron_reduced_dual()
    kempe_components(G1)

    print()
    print("n=14 reduced dual (first plantri triangulation, i_red=1):")
    G2 = n14_reduced_dual()
    kempe_components(G2)

    print()
    print("Small sanity check: K_4")
    K4 = graphs.CompleteGraph(4)
    kempe_components(K4)


if __name__ == '__main__':
    main()
