"""For each chord-apex+Kempe colouring, walk K_b and K_c (each in
trace order starting from the merged edge), and for every shared
vertex v in V(K_b) cap V(K_c) record:

  i_b(v) = position of v in the K_b walk (mod 2)
  i_c(v) = position of v in the K_c walk (mod 2)
  h_phi(v)

The proposal: under the constant-Heawood hypothesis, Lemma A forces
each cycle's c-edge / b-edge sides to be determined by i mod 2. The
CW order at a shared vertex v relates these. We tally the joint
distribution of (i_b mod 2, i_c mod 2, h(v)) across all colourings
and shared vertices, looking for a parity constraint.

Run with: sage experiments/check_shared_parity.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,
)
from check_heawood_on_kempe import dual_of, heawood_numbers


def walk_positions(walk):
    """Return dict vertex -> first-position-on-walk."""
    pos = {}
    for k, (_, leave_v) in enumerate(walk):
        if leave_v not in pos:
            pos[leave_v] = k
    return pos


def test_one(D):
    D.is_planar(set_embedding=True)
    n_col = 0
    # Joint distribution: (i_b mod 2, i_c mod 2, h) -> count
    joint = {}
    # Per-colouring: count of shared vertices in each of the 4
    # (i_b, i_c) parity buckets, summarised.
    bucket_dist = {}      # (n00, n01, n10, n11) -> count
    # Per-colouring: is sum of i_b parities over shared vertices ==
    # sum of i_c parities (mod 2)?
    sum_parity_match = 0
    sum_parity_total = 0
    # Per-colouring: is i_b(v) congruent to i_c(v) (mod 2) for ALL
    # shared vertices? Or NEVER? Or mixed?
    all_match = 0
    all_diff = 0
    mixed = 0
    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)]
            for col in cand:
                n_col += 1
                try:
                    h = heawood_numbers(H, edges, col)
                except RuntimeError:
                    continue
                merged_idx = edge_idx(edges, named['merged'])
                a = col[merged_idx]
                bs = [c for c in range(3) if c != a]
                walk_b = trace_kempe_cycle(edges, col, merged_idx, (a, bs[0]))
                walk_c = trace_kempe_cycle(edges, col, merged_idx, (a, bs[1]))
                pos_b = walk_positions(walk_b)
                pos_c = walk_positions(walk_c)
                V_b = set(pos_b.keys())
                V_c = set(pos_c.keys())
                shared = V_b & V_c
                buckets = [0, 0, 0, 0]   # (i_b, i_c) parities
                sum_ib = 0
                sum_ic = 0
                match_count = 0
                diff_count = 0
                for v in shared:
                    pb = pos_b[v] % 2
                    pc = pos_c[v] % 2
                    buckets[2 * pb + pc] += 1
                    sum_ib = (sum_ib + pb) % 2
                    sum_ic = (sum_ic + pc) % 2
                    key = (pb, pc, h[v])
                    joint[key] = joint.get(key, 0) + 1
                    if pb == pc: match_count += 1
                    else:        diff_count += 1
                if shared:
                    sum_parity_total += 1
                    if sum_ib == sum_ic:
                        sum_parity_match += 1
                    if diff_count == 0:
                        all_match += 1
                    elif match_count == 0:
                        all_diff += 1
                    else:
                        mixed += 1
                bd_key = tuple(buckets)
                bucket_dist[bd_key] = bucket_dist.get(bd_key, 0) + 1
    return n_col, joint, bucket_dist, sum_parity_match, sum_parity_total, all_match, all_diff, mixed


def main(max_n=18, time_budget_per_n=1800):
    print(f"Parity check at shared K_b cap K_c vertices, "
          f"n in [12, {max_n}]\n")
    grand_col = 0
    grand_joint = {}
    grand_bucket = {}
    grand_spm = 0; grand_spt = 0
    grand_am = 0; grand_ad = 0; grand_mix = 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
        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)
            (n_col_i, j_i, b_i, spm_i, spt_i,
             am_i, ad_i, mix_i) = test_one(D)
            n_col_n += n_col_i
            for k, v in j_i.items(): grand_joint[k] = grand_joint.get(k, 0) + v
            for k, v in b_i.items(): grand_bucket[k] = grand_bucket.get(k, 0) + v
            grand_spm += spm_i; grand_spt += spt_i
            grand_am += am_i; grand_ad += ad_i; grand_mix += mix_i
        elapsed = time.time() - start
        print(f"n={n}: {n_col_n} col., [{elapsed:.0f}s]")
        sys.stdout.flush()
        grand_col += n_col_n

    print()
    print("=" * 78)
    print(f"Grand totals (n in [12, {max_n}], {grand_col} colourings):")
    print(f"\n  Joint (i_b mod 2, i_c mod 2, h_phi) distribution over "
          f"shared vertices:")
    keys = sorted(grand_joint.keys())
    total_shared = sum(grand_joint.values())
    for k in keys:
        v = grand_joint[k]
        print(f"    {k}: {v} ({100*v/max(1,total_shared):.2f}%)")
    print(f"\n  Per-colouring: i_b(v) == i_c(v) (mod 2) for ALL shared v?")
    print(f"    all match: {grand_am}/{grand_col} "
          f"({100*grand_am/max(1,grand_col):.2f}%)")
    print(f"    all differ: {grand_ad}/{grand_col} "
          f"({100*grand_ad/max(1,grand_col):.2f}%)")
    print(f"    mixed: {grand_mix}/{grand_col} "
          f"({100*grand_mix/max(1,grand_col):.2f}%)")
    print(f"\n  Per-colouring: sum_{{v shared}} i_b(v) ≡ sum_{{v shared}} i_c(v) (mod 2)?")
    print(f"    sum-parity match: {grand_spm}/{grand_spt} "
          f"({100*grand_spm/max(1,grand_spt):.2f}%)")
    print(f"\n  Most common bucket signatures (n00, n01, n10, n11):")
    bs = sorted(grand_bucket.items(), key=lambda kv: -kv[1])[:8]
    for k, v in bs:
        print(f"    {k}: {v} ({100*v/max(1,grand_col):.2f}%)")


if __name__ == '__main__':
    main()
