"""Test Conjecture 3.8 (strengthening of 3.6 with the {b,c}-Kempe / 3-colour
constraint on the new 4-edge face) on all min-degree-5 triangulations up to
n = 18.

For each (G, F_v, i_red, varphi) with varphi a chord-apex + Kempe coloring,
we look for a witness (F, e_1, e_2) of Conjecture 3.6 (clauses 1-3 including
the 4-edge-face criterion). Then we:
  - subdivide e_1, e_2 by X_1, X_2,
  - add the new edge X_1 X_2,
  - recolour the (subdivided) Kempe cycle alternately starting from merged
    so propriety holds and the new edge takes the third colour,
  - identify f_n (the 4-edge face containing the new edge), and
  - test clause 4:
      EITHER partial(f_n) uses all 3 colours,
      OR the {b, c}-Kempe cycle through X_1 X_2 has only X_1 X_2 itself in
         partial(f_n).
Aggregates per-n.

Run with: sage experiments/check_conj_final_scaled.py
"""
from sage.all import Graph
from sage.graphs.graph_generators import graphs
import sys
import time


def dual_of(G):
    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)
    return Graph(
        [(fs[0], fs[1]) for fs in edge_to_faces.values() if len(fs) == 2],
        multiedges=False, loops=False)


def apply_reduction(G, face, i, v_n_label):
    boundary = [u for (u, v) in face]
    if len(set(boundary)) != 5: return None
    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: return None
        A.append(outer[0])
    if len(set(A)) != 5 or A[(i+3) % 5] == A[(i+4) % 5]: return None
    H = G.copy()
    for v in boundary: H.delete_vertex(v)
    H.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])
    H.add_edges([side_0, spike, side_1, merged])
    if H.has_multiple_edges() or H.has_loops(): return None
    if not H.is_planar(set_embedding=True): return None
    if not all(H.degree(v) == 3 for v in H.vertex_iterator()): return None
    return {'H': H, 'named': {'spike': frozenset(spike),
            'side_0': frozenset(side_0), 'side_1': frozenset(side_1),
            'merged': frozenset(merged)}}


def 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_cycle_set(edges, coloring, start_idx, color_pair):
    a, b = color_pair
    if coloring[start_idx] not in (a, b): return set()
    in_sub = set(i for i in range(len(edges)) if coloring[i] in (a, b))
    visited = {start_idx}; stack = [start_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 edge_idx(edges, e_frozen):
    for i, e in enumerate(edges):
        if frozenset((e[0], e[1])) == e_frozen:
            return i
    return None


def trace_kempe_cycle(edges, col_list, start_idx, color_pair):
    cycle_set = kempe_cycle_set(edges, col_list, start_idx, color_pair)
    incident_at = {}
    for ei in cycle_set:
        u, v = edges[ei][0], edges[ei][1]
        incident_at.setdefault(u, []).append(ei)
        incident_at.setdefault(v, []).append(ei)
    start_u, start_v = edges[start_idx][0], edges[start_idx][1]
    walk = [(start_idx, start_v)]
    cur_e = start_idx
    cur_leave = start_v
    while True:
        nbrs = incident_at[cur_leave]
        if len(nbrs) != 2: break
        nxt = nbrs[0] if nbrs[1] == cur_e else nbrs[1]
        u2, v2 = edges[nxt][0], edges[nxt][1]
        leave_next = v2 if u2 == cur_leave else u2
        if nxt == start_idx: break
        walk.append((nxt, leave_next))
        cur_e = nxt
        cur_leave = leave_next
    return walk


def matches_chord_apex_kempe(edges, col, named):
    idx = {role: edge_idx(edges, ns) for role, ns in named.items()}
    if any(v is None for v in idx.values()): return False
    c_spike  = col[idx['spike']]
    c_merged = col[idx['merged']]
    if c_spike != c_merged: return False
    c_s0 = col[idx['side_0']]; c_s1 = col[idx['side_1']]
    kc0 = kempe_cycle_set(edges, col, idx['spike'], (c_spike, c_s0))
    if idx['side_0'] not in kc0 or idx['merged'] not in kc0: return False
    kc1 = kempe_cycle_set(edges, col, idx['spike'], (c_spike, c_s1))
    if idx['side_1'] not in kc1 or idx['merged'] not in kc1: return False
    return True


def find_all_36_witnesses(H, edges, col_list, named):
    """Yield every (F, e_1, e_2) satisfying clauses 1-3 of Conjecture 3.6."""
    merged_idx = edge_idx(edges, named['merged'])
    c_merged = col_list[merged_idx]
    kempe_cycles = []
    for c_prime in range(3):
        if c_prime == c_merged: continue
        kc = kempe_cycle_set(edges, col_list, merged_idx, (c_merged, c_prime))
        kempe_cycles.append((c_prime, kc))
    H.is_planar(set_embedding=True)
    out = []
    for face in H.faces():
        face_edge_indices = []
        for u, v in face:
            ei = edge_idx(edges, frozenset((u, v)))
            if ei is not None:
                face_edge_indices.append(ei)
        n_face = len(face_edge_indices)
        for i in range(n_face):
            for j in range(i + 1, n_face):
                e1, e2 = face_edge_indices[i], face_edge_indices[j]
                if e1 == merged_idx or e2 == merged_idx: continue
                if col_list[e1] != col_list[e2]: continue
                gap_a = (j - i - 1)
                gap_b = (n_face - 2 - gap_a)
                if gap_a != 1 and gap_b != 1: continue
                for c_prime, kc in kempe_cycles:
                    if e1 in kc and e2 in kc:
                        if gap_a == 1:
                            e_F = face_edge_indices[i + 1]
                        else:
                            e_F = face_edge_indices[(j + 1) % n_face]
                        out.append({
                            'face_edges': face_edge_indices,
                            'e1': e1, 'e2': e2, 'e_F': e_F,
                            'kc_color_pair': (c_merged, c_prime),
                        })
    return out


def check_clause_4(H, edges, col_list, named, witness):
    """Construct the modified H + recoloring, identify f_n, check clause 4."""
    e1, e2 = witness['e1'], witness['e2']
    e_F = witness['e_F']
    a = col_list[e1]    # color of e_1 = e_2 (clauses 1)
    merged_idx = edge_idx(edges, named['merged'])
    cyc_a, cyc_b = witness['kc_color_pair']   # = (c_merged, c_other)
    # On the {cyc_a, cyc_b}-Kempe cycle, e_1 carries colour a, which
    # equals either cyc_a or cyc_b. The conjecture's "b" is the OTHER
    # colour on the cycle; we set it accordingly.
    if a == cyc_a:
        b = cyc_b
    elif a == cyc_b:
        b = cyc_a
    else:
        raise RuntimeError(
            f"e_1 colour {a} not in Kempe pair {(cyc_a, cyc_b)}")
    c = ({0, 1, 2} - {a, b}).pop()

    # Subdivided cycle K' alternates blue/green starting from merged = blue
    walk = trace_kempe_cycle(edges, col_list, merged_idx, (cyc_a, cyc_b))
    walk_edges = [w[0] for w in walk]
    leave_at = [w[1] for w in walk]

    # K' position counter. For each old cycle edge e in cyclic order, position
    # increments by 1 normally; for e1/e2, increments by 2 (two halves).
    # For e1, we record (entry_half_color, exit_half_color) where entry_half
    # is the half adjacent to entry_vertex and exit_half adjacent to leaving.
    e1_entry_color = e1_exit_color = None
    e2_entry_color = e2_exit_color = None
    e1_entry_vertex = e1_exit_vertex = None
    e2_entry_vertex = e2_exit_vertex = None
    other_new_colors = {}   # ei -> new color (for cycle edges other than e1/e2)
    pos = 0
    for k, ei in enumerate(walk_edges):
        leaving = leave_at[k]
        u, v = edges[ei][0], edges[ei][1]
        entry = v if leaving == u else u
        if ei == e1:
            c_entry = cyc_b if pos % 2 == 0 else cyc_a
            pos += 1
            c_exit = cyc_b if pos % 2 == 0 else cyc_a
            pos += 1
            e1_entry_color = c_entry; e1_exit_color = c_exit
            e1_entry_vertex = entry; e1_exit_vertex = leaving
        elif ei == e2:
            c_entry = cyc_b if pos % 2 == 0 else cyc_a
            pos += 1
            c_exit = cyc_b if pos % 2 == 0 else cyc_a
            pos += 1
            e2_entry_color = c_entry; e2_exit_color = c_exit
            e2_entry_vertex = entry; e2_exit_vertex = leaving
        else:
            nc = cyc_b if pos % 2 == 0 else cyc_a
            other_new_colors[ei] = nc
            pos += 1

    # Identify which half of e1 is on f_n: it's the half adjacent to e_F.
    # e_F has 2 endpoints; one is shared with e1, one with e2.
    e_F_endpoints = set(edges[e_F])
    e1_endpoints = set(edges[e1])
    e2_endpoints = set(edges[e2])
    shared_e1_eF = (e_F_endpoints & e1_endpoints).pop()
    shared_e2_eF = (e_F_endpoints & e2_endpoints).pop()
    # The half of e1 on f_n connects X_1 to shared_e1_eF. So if shared_e1_eF
    # equals e1_entry_vertex, the half on f_n is the "entry half"; else the
    # "exit half".
    if shared_e1_eF == e1_entry_vertex:
        e1_h_color = e1_entry_color
    else:
        e1_h_color = e1_exit_color
    if shared_e2_eF == e2_entry_vertex:
        e2_h_color = e2_entry_color
    else:
        e2_h_color = e2_exit_color

    # Determine e_F's new colour: if e_F is on the Kempe cycle (other_new_colors)
    # use that, else original.
    if e_F in other_new_colors:
        e_F_color = other_new_colors[e_F]
    else:
        e_F_color = col_list[e_F]

    # f_n's 4 edge colors: [new edge X_1-X_2 = c, e1_h, e_F, e2_h]
    fn_colors = [c, e1_h_color, e_F_color, e2_h_color]
    fn_distinct = set(fn_colors)
    uses_3_colors = (len(fn_distinct) == 3)

    if uses_3_colors:
        return True   # clause 4(i) satisfied

    # Otherwise, check clause 4(ii): the {b,c}-Kempe cycle through the new
    # edge has only X_1-X_2 in f_n's boundary.
    # We need to actually build the modified graph and trace this cycle.
    return check_clause_4_kempe_part(H, edges, col_list, named, witness,
                                     a, b, c, e1_h_color, e2_h_color,
                                     other_new_colors,
                                     e1_entry_vertex, e1_exit_vertex,
                                     e2_entry_vertex, e2_exit_vertex,
                                     e1_entry_color, e1_exit_color,
                                     e2_entry_color, e2_exit_color)


def check_clause_4_kempe_part(H, edges, col_list, named, witness, a, b, c,
                              e1_h_color, e2_h_color, other_new_colors,
                              e1_ev, e1_xv, e2_ev, e2_xv,
                              e1_ec, e1_xc, e2_ec, e2_xc):
    """Build modified H' and check whether the {b,c}-Kempe cycle through X1-X2
    has only that one edge in partial(f_n)."""
    e1 = witness['e1']; e2 = witness['e2']; e_F = witness['e_F']
    H2 = H.copy()
    X1 = max(v for v in H.vertices(sort=False) if isinstance(v, int)) + 1
    X2 = X1 + 1
    H2.add_vertex(X1); H2.add_vertex(X2)
    e1_uv = tuple(edges[e1]); e2_uv = tuple(edges[e2])
    H2.delete_edge(e1_uv); H2.delete_edge(e2_uv)
    H2.add_edges([(e1_uv[0], X1), (X1, e1_uv[1]),
                  (e2_uv[0], X2), (X2, e2_uv[1]),
                  (X1, X2)])
    # Build coloring for H2
    new_coloring = {}
    # Copy non-modified edges: original color (unless in other_new_colors)
    for ei, c0 in enumerate(col_list):
        if ei == e1 or ei == e2: continue
        e_fs = frozenset(edges[ei])
        if ei in other_new_colors:
            new_coloring[e_fs] = other_new_colors[ei]
        else:
            new_coloring[e_fs] = c0
    # Halves of e1, e2
    new_coloring[frozenset((e1_ev, X1))] = e1_ec
    new_coloring[frozenset((X1, e1_xv))] = e1_xc
    new_coloring[frozenset((e2_ev, X2))] = e2_ec
    new_coloring[frozenset((X2, e2_xv))] = e2_xc
    new_coloring[frozenset((X1, X2))] = c

    # Determine f_n's edges (in H2): new edge X1-X2, the half of e1 adjacent
    # to e_F's shared vertex with e1, e_F itself, and the half of e2 adjacent
    # to e_F's shared vertex with e2.
    e_F_endpoints = set(edges[e_F])
    e1_endpoints = set(edges[e1])
    e2_endpoints = set(edges[e2])
    shared_e1_eF = (e_F_endpoints & e1_endpoints).pop()
    shared_e2_eF = (e_F_endpoints & e2_endpoints).pop()
    fn_edges = {
        frozenset((X1, X2)),
        frozenset((X1, shared_e1_eF)),
        frozenset(edges[e_F]),
        frozenset((X2, shared_e2_eF)),
    }

    # Build edge list of H2 + coloring list
    H2_edges = list(H2.edges(labels=False))
    H2_col_list = [new_coloring[frozenset(e)] for e in H2_edges]
    # Sanity: verify phi' is a proper 3-edge-colouring of H2 (the conjecture
    # asserts this; if the construction is wrong, abort).
    for v in H2.vertex_iterator():
        seen = []
        for w in H2.neighbor_iterator(v):
            seen.append(new_coloring[frozenset((v, w))])
        if len(set(seen)) != len(seen):
            raise RuntimeError(
                f"phi' is not proper at vertex {v}: colors {seen}")
    # Trace the {b,c}-Kempe cycle through X1-X2 in H2 using new_coloring
    H2_X1X2_idx = None
    for i, e in enumerate(H2_edges):
        if frozenset(e) == frozenset((X1, X2)):
            H2_X1X2_idx = i; break
    if H2_X1X2_idx is None:
        return False
    kc_bc = kempe_cycle_set(H2_edges, H2_col_list, H2_X1X2_idx, (b, c))
    # Count edges in kc_bc that are in fn_edges
    count = 0
    for ei in kc_bc:
        if frozenset(H2_edges[ei]) in fn_edges:
            count += 1
    return count == 1


def main(max_n=20, time_budget_per_n=7200):
    rows = []
    for n in range(12, max_n + 1):
        start = time.time()
        try:
            triangulations = list(graphs.triangulations(n, minimum_degree=5))
        except Exception as ex:
            rows.append((n, 0, 0, 0, f"cannot enumerate"))
            continue
        n_tri = len(triangulations)
        total_col = 0
        total_pass = 0
        timed_out = False
        for tri_idx, G in enumerate(triangulations, start=1):
            if time.time() - start > time_budget_per_n:
                timed_out = True; break
            G.is_planar(set_embedding=True)
            D = dual_of(G); D.is_planar(set_embedding=True)
            for face in D.faces():
                if len(face) != 5: continue
                if time.time() - start > time_budget_per_n:
                    timed_out = True; break
                for i_red in range(5):
                    res = apply_reduction(D, face, i_red, 9999)
                    if res is None: continue
                    H = res['H']; named = res['named']
                    H.is_planar(set_embedding=True)
                    edges, colorings = proper_3_edge_colorings(H)
                    cand = [c for c in colorings
                            if matches_chord_apex_kempe(edges, c, named)]
                    for col in cand:
                        witnesses = find_all_36_witnesses(H, edges, list(col),
                                                          named)
                        if not witnesses:
                            continue
                        total_col += 1
                        ok = False
                        for w in witnesses:
                            try:
                                if check_clause_4(H, edges, list(col), named,
                                                  w):
                                    ok = True
                                    break
                            except Exception:
                                pass
                        if ok:
                            total_pass += 1
        elapsed = time.time() - start
        status = (f"TIMEOUT ({elapsed:.0f}s)" if timed_out
                  else f"complete ({elapsed:.0f}s)")
        rows.append((n, n_tri, total_col, total_pass, status))
        print(f"n={n}: {n_tri} tri, {total_col} cand witnesses, "
              f"{total_pass} pass clause 4, {status}")
        sys.stdout.flush()

    print()
    print("=" * 70)
    print(f"{'n':>3} {'#tri':>5} {'#witness':>10} {'#pass_cl4':>10} {'status':>25}")
    print("-" * 70)
    for n, n_tri, n_col, n_pass, status in rows:
        print(f"{n:>3} {n_tri:>5} {n_col:>10} {n_pass:>10} {status:>25}")


if __name__ == '__main__':
    main()
