"""Check whether the dodecahedron's reduced dual admits a proper
3-edge-colouring with:
  (i)   colour(spike) == colour(merged), AND
  (ii)  the {c_spike, c_side0}-Kempe cycle through the spike contains both
        side-0 and the merged edge, AND
  (iii) the {c_spike, c_side1}-Kempe cycle through the spike contains both
        side-1 and the merged edge.

Definitions follow Algorithm 3.1 in paper.tex with G = icosahedron, G' =
dodecahedron, i_1 = 0; the reduced dual has 16 vertices and 24 edges.

Lemmas 2.6 and 2.7 force these properties for ANY proper 3-edge-colouring
of the reduced dual of a *minimal counterexample's* dual. The dodecahedron
is the dual of the icosahedron --- which IS 4-colourable, so the lemmas do
not apply and the question is non-trivial.
"""
from sage.all import Graph


def build_reduced_dodecahedron(i_red=0):
    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)))
    v_n = 'v_n'
    edges.append((v_n, ('b', i_red % 5)))
    edges.append((v_n, ('b', (i_red + 1) % 5)))
    edges.append((v_n, ('b', (i_red + 2) % 5)))
    edges.append((('b', (i_red + 3) % 5), ('b', (i_red + 4) % 5)))
    return Graph(edges, multiedges=False, loops=False)


def enumerate_proper_3_edge_colorings(G):
    """Return (edge_list, list_of_colorings) where each coloring is a list of
    3-edge-colours indexed by edge_list."""
    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(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_cycle_edges(edges, coloring, start_edge_idx, color_pair):
    """Return the set of edge-indices in the Kempe cycle containing
    edges[start_edge_idx] in the subgraph of edges with colour in
    color_pair."""
    a, b = color_pair
    in_sub = [i for i in range(len(edges)) if coloring[i] in (a, b)]
    if start_edge_idx not in in_sub:
        return None
    visited = {start_edge_idx}
    stack = [start_edge_idx]
    while stack:
        cur = stack.pop()
        u, v = edges[cur][0], edges[cur][1]
        for j in in_sub:
            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)
    return visited


def main():
    G = build_reduced_dodecahedron(i_red=0)
    print(f"|V| = {G.order()}, |E| = {G.size()}")

    edges, colorings = enumerate_proper_3_edge_colorings(G)
    print(f"Proper 3-edge-colorings total: {len(colorings)}")

    v_n = 'v_n'
    spike_set  = frozenset({v_n, ('b', 1)})
    side_0_set = frozenset({v_n, ('b', 0)})
    side_1_set = frozenset({v_n, ('b', 2)})
    merged_set = frozenset({('b', 3), ('b', 4)})

    idx = {}
    for i, e in enumerate(edges):
        s = frozenset((e[0], e[1]))
        if s == spike_set:  idx['spike']  = i
        if s == side_0_set: idx['side_0'] = i
        if s == side_1_set: idx['side_1'] = i
        if s == merged_set: idx['merged'] = i

    n_chord_apex   = 0  # spike == merged
    n_kc0_ok       = 0  # condition (ii)
    n_kc1_ok       = 0  # condition (iii)
    n_all_ok       = 0  # all three
    example = None

    for col in colorings:
        c_spike  = col[idx['spike']]
        c_merged = col[idx['merged']]
        c_s0     = col[idx['side_0']]
        c_s1     = col[idx['side_1']]
        if c_spike != c_merged:
            continue
        n_chord_apex += 1

        kc0 = kempe_cycle_edges(edges, col, idx['spike'], (c_spike, c_s0))
        kc1 = kempe_cycle_edges(edges, col, idx['spike'], (c_spike, c_s1))

        kc0_ok = kc0 is not None and idx['side_0'] in kc0 and idx['merged'] in kc0
        kc1_ok = kc1 is not None and idx['side_1'] in kc1 and idx['merged'] in kc1
        n_kc0_ok += int(kc0_ok)
        n_kc1_ok += int(kc1_ok)
        if kc0_ok and kc1_ok:
            n_all_ok += 1
            if example is None:
                example = (col, kc0, kc1)

    print()
    print(f"Colorings with spike == merged (chord-apex condition):     {n_chord_apex}")
    print(f"  ... + {{c, c_0}}-Kempe cycle through spike/side-0/merged: {n_kc0_ok}")
    print(f"  ... + {{c, c_1}}-Kempe cycle through spike/side-1/merged: {n_kc1_ok}")
    print(f"  ... ALL THREE conditions:                                {n_all_ok}")

    if example is not None:
        col, kc0, kc1 = example
        c_spike = col[idx['spike']]
        print()
        print("Example coloring (one of {} matching):".format(n_all_ok))
        for role in ('spike', 'side_0', 'side_1', 'merged'):
            print(f"  {role:7s}: colour {col[idx[role]]}, edge {tuple(edges[idx[role]])}")
        print(f"  spike colour = merged colour = {c_spike}")
        print(f"  Kempe cycle {{c, c_0}} (through spike): {len(kc0)} edges")
        for i in sorted(kc0):
            print(f"    [c={col[i]}] {edges[i]}")
        print(f"  Kempe cycle {{c, c_1}} (through spike): {len(kc1)} edges")
        for i in sorted(kc1):
            print(f"    [c={col[i]}] {edges[i]}")
    else:
        # Dissect a sample chord-apex coloring that fails the Kempe condition
        print()
        print("Sample chord-apex coloring (fails the Kempe-chain condition):")
        for col in colorings:
            if col[idx['spike']] != col[idx['merged']]:
                continue
            c_spike = col[idx['spike']]
            c_s0 = col[idx['side_0']]
            c_s1 = col[idx['side_1']]
            for role in ('spike', 'side_0', 'side_1', 'merged'):
                print(f"  {role:7s}: colour {col[idx[role]]}, edge {tuple(edges[idx[role]])}")
            kc0 = kempe_cycle_edges(edges, col, idx['spike'], (c_spike, c_s0))
            kc1 = kempe_cycle_edges(edges, col, idx['spike'], (c_spike, c_s1))
            kc_m0 = kempe_cycle_edges(edges, col, idx['merged'], (c_spike, c_s0))
            kc_m1 = kempe_cycle_edges(edges, col, idx['merged'], (c_spike, c_s1))
            print(f"  spike's   {{c, c_0}}-Kempe cycle has {len(kc0)} edges; "
                  f"side_0 in it = {idx['side_0'] in kc0}, "
                  f"merged in it = {idx['merged'] in kc0}")
            print(f"  merged's  {{c, c_0}}-Kempe cycle has {len(kc_m0)} edges; "
                  f"disjoint from spike's? {kc0.isdisjoint(kc_m0)}")
            print(f"  spike's   {{c, c_1}}-Kempe cycle has {len(kc1)} edges; "
                  f"side_1 in it = {idx['side_1'] in kc1}, "
                  f"merged in it = {idx['merged'] in kc1}")
            print(f"  merged's  {{c, c_1}}-Kempe cycle has {len(kc_m1)} edges; "
                  f"disjoint from spike's? {kc1.isdisjoint(kc_m1)}")
            break


if __name__ == '__main__':
    main()
