"""For every chord-apex+Kempe colouring of every reduced dual we can
enumerate, record:

  (1) |V(K_b)|, |V(K_c)|, |V(K_b) cap V(K_c)|, |V(K_b) cup V(K_c)|.
      In particular: is |V(K_b) cap V(K_c)| typically 2 (just the
      merged endpoints) or larger?

  (2) "Side-flip count" on each Kempe cycle: walking K_b in cycle
      order, count the number of consecutive K_b-pairs (v_0, v_1)
      where the c-edges at v_0 and v_1 lie on opposite local sides.
      Same for K_c with b-edges.

      Under Lemma A (now empirically confirmed), the flip count
      equals the number of "same-Heawood" consecutive pairs on the
      cycle. So:
        - constant Heawood on K_b => flip count = |V(K_b)| (max)
        - perfectly alternating h on K_b => flip count = 0
      Anything in between corresponds to a Heawood pattern that's
      neither constant nor perfectly alternating.

      We tally the flip counts for K_b and K_c, and whether they
      match or differ across the same colouring.

Run with: sage experiments/check_kempe_intersection_and_alternation.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,
    trace_kempe_cycle,
    edge_idx,
    kempe_cycle_set,
)
from check_heawood_on_kempe import dual_of, heawood_numbers, vertices_of_kempe
from check_heawood_local_side import c_edge_local_side


def cycle_data(H, edges, col, emb, merged_idx, a, b):
    """Return (V_cycle, side_flips) for the {a, b}-Kempe cycle through
    the edge at merged_idx. side_flips counts consecutive pairs whose
    third-colour edges lie on opposite local sides (= #{same-h pairs}
    via Lemma A)."""
    walk = trace_kempe_cycle(edges, col, merged_idx, (a, b))
    if not walk:
        return set(), 0
    L = len(walk)
    third_color = 3 - a - b
    sides = []
    V_cycle = set()
    for k in range(L):
        v = walk[k][1]
        V_cycle.add(v)
        in_e = edges[walk[k][0]]
        out_e = edges[walk[(k + 1) % L][0]]
        u_in = in_e[0] if in_e[1] == v else in_e[1]
        u_out = out_e[0] if out_e[1] == v else out_e[1]
        side = c_edge_local_side(v, third_color, col, edges, emb,
                                 u_in, u_out)
        sides.append(side)
    flips = 0
    for k in range(L):
        s0 = sides[k]; s1 = sides[(k + 1) % L]
        if s0 is not None and s1 is not None and s0 != s1:
            flips += 1
    return V_cycle, flips


def test_one(D):
    D.is_planar(set_embedding=True)
    n_col = 0
    rec = {
        'cap_size': {},        # |V(K_b) cap V(K_c)| histogram
        'cap_eq_2': 0,         # how often the intersection is exactly 2
        'cap_size_minus_2': {},# |V(K_b) cap V(K_c)| - 2 histogram
        'kb_size': {},
        'kc_size': {},
        'union_size': {},
        'flip_kb': {},         # K_b side-flip count
        'flip_kc': {},         # K_c side-flip count
        'flip_match': 0,       # how often flip_Kb == flip_Kc
        'flip_match_pct_bucket': {},  # bucket of |fkb - fkc| / max
    }
    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)
            emb = H.get_embedding()
            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:
                n_col += 1
                merged_idx = edge_idx(edges, named['merged'])
                a = col[merged_idx]
                bs = [c for c in range(3) if c != a]
                Vb, fb = cycle_data(H, edges, col, emb, merged_idx, a, bs[0])
                Vc, fc = cycle_data(H, edges, col, emb, merged_idx, a, bs[1])
                cap = Vb & Vc
                un = Vb | Vc
                rec['cap_size'][len(cap)] = rec['cap_size'].get(len(cap), 0) + 1
                rec['cap_size_minus_2'][len(cap) - 2] = \
                    rec['cap_size_minus_2'].get(len(cap) - 2, 0) + 1
                if len(cap) == 2:
                    rec['cap_eq_2'] += 1
                rec['kb_size'][len(Vb)] = rec['kb_size'].get(len(Vb), 0) + 1
                rec['kc_size'][len(Vc)] = rec['kc_size'].get(len(Vc), 0) + 1
                rec['union_size'][len(un)] = rec['union_size'].get(len(un), 0) + 1
                rec['flip_kb'][fb] = rec['flip_kb'].get(fb, 0) + 1
                rec['flip_kc'][fc] = rec['flip_kc'].get(fc, 0) + 1
                if fb == fc:
                    rec['flip_match'] += 1
    return n_col, rec


def merge_into(grand, n_rec):
    for key in ('cap_size', 'cap_size_minus_2', 'kb_size', 'kc_size',
                'union_size', 'flip_kb', 'flip_kc'):
        for k, v in n_rec[key].items():
            grand[key][k] = grand[key].get(k, 0) + v
    grand['cap_eq_2'] += n_rec['cap_eq_2']
    grand['flip_match'] += n_rec['flip_match']


def main(max_n=18, time_budget_per_n=1800):
    print(f"K_b/K_c intersection sizes and side-flip counts, "
          f"n in [12, {max_n}]\n")
    grand = {
        'cap_size': {}, 'cap_size_minus_2': {}, 'cap_eq_2': 0,
        'kb_size': {}, 'kc_size': {}, 'union_size': {},
        'flip_kb': {}, 'flip_kc': {}, 'flip_match': 0,
    }
    grand_col = 0
    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_n = 0
        n_rec = {
            'cap_size': {}, 'cap_size_minus_2': {}, 'cap_eq_2': 0,
            'kb_size': {}, 'kc_size': {}, 'union_size': {},
            'flip_kb': {}, 'flip_kc': {}, 'flip_match': 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}/{len(triangulations)}")
                break
            G.is_planar(set_embedding=True)
            D = dual_of(G)
            ni, ri = test_one(D)
            n_col_n += ni
            merge_into(n_rec, ri)
        elapsed = time.time() - start
        print(f"n={n}: {n_col_n} col., [{elapsed:.0f}s]")
        print(f"  |V(K_b) cap V(K_c)| dist: {sorted(n_rec['cap_size'].items())}")
        print(f"  cap == 2: {n_rec['cap_eq_2']}/{n_col_n}")
        print(f"  flip_kb dist: {sorted(n_rec['flip_kb'].items())}")
        print(f"  flip_kc dist: {sorted(n_rec['flip_kc'].items())}")
        print(f"  flip_kb == flip_kc: {n_rec['flip_match']}/{n_col_n}")
        sys.stdout.flush()
        grand_col += n_col_n
        merge_into(grand, n_rec)

    print()
    print("=" * 78)
    print(f"Grand totals (n in [12, {max_n}], {grand_col} colourings):")
    print(f"  |V(K_b) cap V(K_c)| distribution: "
          f"{sorted(grand['cap_size'].items())}")
    cap_2 = grand['cap_eq_2']
    print(f"  |V(K_b) cap V(K_c)| == 2: {cap_2}/{grand_col} "
          f"({100*cap_2/max(1,grand_col):.2f}%)")
    print(f"  |V(K_b)| dist: {sorted(grand['kb_size'].items())}")
    print(f"  |V(K_c)| dist: {sorted(grand['kc_size'].items())}")
    print(f"  |V(K_b) cup V(K_c)| dist: {sorted(grand['union_size'].items())}")
    print(f"  flip_kb dist: {sorted(grand['flip_kb'].items())}")
    print(f"  flip_kc dist: {sorted(grand['flip_kc'].items())}")
    fm = grand['flip_match']
    print(f"  flip_kb == flip_kc: {fm}/{grand_col} "
          f"({100*fm/max(1,grand_col):.2f}%)")


if __name__ == '__main__':
    main()
