"""Empirical investigation: for each chord-apex+Kempe colouring (NOT
restricted to bad ones), correlate |S| = |V \\ (V(K_b) ∪ V(K_c))|
with the number of pentagonal F_k adjacent to F_v in the parent G'.

Hypothesis (from the |S|=8 hit=8 finding): there's a coupling where
larger |S| forces fewer pentagonal F_k.

Concretely: for each colouring, record:
  - |S|
  - # pentagonal F_k (= # of v's 5 neighbours in G with degree 5)
  - # G'-pentagons hit by S (when applicable)

Aggregate the joint distribution.

Run with: sage experiments/check_S_vs_pent_Fk.py
"""
import os
import sys
import time

from sage.all import Graph
from sage.graphs.graph_generators import graphs

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)

from check_conj_final_scaled import (
    apply_reduction,
    proper_3_edge_colorings,
    matches_chord_apex_kempe,
    kempe_cycle_set,
    edge_idx,
)
from check_heawood_on_kempe import dual_of, vertices_of_kempe


def cyclic_neighbour_degrees(G, v):
    G.is_planar(set_embedding=True)
    emb = G.get_embedding()
    return [G.degree(u) for u in emb[v]]


def test_one(D, G_parent):
    D.is_planar(set_embedding=True)
    G_parent.is_planar(set_embedding=True)
    joint_dist = {}   # (|S|, # pent F_k) -> count
    for v_parent in G_parent.vertex_iterator():
        if G_parent.degree(v_parent) != 5: continue
        cyc_degs = cyclic_neighbour_degrees(G_parent, v_parent)
        n_pent_Fk_all = sum(1 for d in cyc_degs if d == 5)
        # For each reduction index i (0 to 4), determine the specific
        # pentagonal F_k among (F_0, ..., F_4) (= all 5).
        # We need to map v_parent + i_red to a specific reduction.
        # Use a different approach: enumerate reductions and check.
        # For each pentagonal face F_v of D (= matches v_parent),
        # iterate i_red.
        # Find D-faces matching v_parent's incident G'-faces:
        # (this is harder without explicit dual labelling)
        pass

    # Alternative approach: iterate over D's faces and reductions
    for face in D.faces():
        if len(face) != 5: continue
        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)]
            v_n = 9999
            # Compute n_k from face lengths in reduced dual
            n_pent_Fk = 0
            for f in H.faces():
                fset = {frozenset(e) for e in f}
                # F_flank_lower = len n_i - 1; if = 4, n_i = 5 (pentagonal).
                if named['side_0'] in fset and named['spike'] in fset:
                    if len(f) == 4:
                        n_pent_Fk += 1
                if named['spike'] in fset and named['side_1'] in fset:
                    if len(f) == 4:
                        n_pent_Fk += 1
                if (named['side_0'] in fset and named['side_1'] in fset
                    and named['merged'] in fset):
                    # F_outer = n_{i+2} + n_{i+4} - 3
                    # For F_outer = 7 = 5+5-3, both n_{i+2}, n_{i+4} = 5.
                    # For F_outer = 8 = 5+6-3 or 6+5-3, one = 5.
                    # Hmm we can't tell which side from outer length alone.
                    pass
                if (named['merged'] in fset and named['side_0'] not in fset
                    and named['side_1'] not in fset
                    and named['spike'] not in fset):
                    # F_merged = n_{i+3} - 2
                    if len(f) == 3:
                        n_pent_Fk += 1
            # (Note: this counts pent F_k in n_i, n_{i+1}, n_{i+3} but
            # not in n_{i+2} or n_{i+4} which are mixed by F_outer.)
            # For better counting, use the parent graph.
            for col in cand:
                merged_idx = edge_idx(edges, named['merged'])
                a = col[merged_idx]
                bs = [c for c in range(3) if c != a]
                kc_b = kempe_cycle_set(edges, col, merged_idx, (a, bs[0]))
                kc_c = kempe_cycle_set(edges, col, merged_idx, (a, bs[1]))
                V_b = vertices_of_kempe(edges, kc_b)
                V_c = vertices_of_kempe(edges, kc_c)
                S_size = H.order() - len(V_b | V_c)
                key = (S_size, n_pent_Fk)
                joint_dist[key] = joint_dist.get(key, 0) + 1
    return joint_dist


def main(max_n=20, time_budget_per_n=1800):
    print("Joint distribution of (|S|, # pentagonal F_k via flank/merged)\n"
          "across all chord-apex+Kempe colourings (NOT restricted to bad).\n"
          "\nNote: this counts pent F_k via flank-lower (= n_i = 5),\n"
          "flank-upper (= n_{i+1} = 5), and merged (= n_{i+3} = 5);\n"
          "it does NOT count pent F_k among n_{i+2}, n_{i+4} (= outer-side)\n"
          "which can't be distinguished from F_outer length alone.\n")
    grand_dist = {}
    for n in range(12, max_n + 1):
        start = time.time()
        try:
            triangulations = list(graphs.triangulations(n, minimum_degree=5))
        except Exception as ex:
            print(f"n={n}: cannot enumerate ({ex})")
            continue
        n_col = 0
        for tri_idx, G in enumerate(triangulations):
            if time.time() - start > time_budget_per_n:
                print(f"  n={n}: timeout at tri {tri_idx}")
                break
            G.is_planar(set_embedding=True)
            D = dual_of(G)
            jd = test_one(D, G)
            for k, v in jd.items():
                grand_dist[k] = grand_dist.get(k, 0) + v
                n_col += v
        elapsed = time.time() - start
        print(f"n={n}: {n_col} colourings counted [{elapsed:.0f}s]")
        sys.stdout.flush()
    print()
    print("=" * 70)
    print("Joint distribution (|S|, # pent F_k visible):\n")
    # Print as a table
    all_S = sorted(set(k[0] for k in grand_dist))
    all_F = sorted(set(k[1] for k in grand_dist))
    print(f"   |S|\\#pent_Fk_visible", end=' ')
    for f in all_F:
        print(f"{f:>7}", end='')
    print(f"{'total':>9}")
    for s in all_S:
        print(f"   |S| = {s:>3}              ", end=' ')
        total = 0
        for f in all_F:
            c = grand_dist.get((s, f), 0)
            total += c
            print(f"{c:>7}", end='')
        print(f"{total:>9}")
    print(f"   total                  ", end=' ')
    for f in all_F:
        col_total = sum(grand_dist.get((s, f), 0) for s in all_S)
        print(f"{col_total:>7}", end='')
    grand_total = sum(grand_dist.values())
    print(f"{grand_total:>9}")


if __name__ == '__main__':
    main()
