"""For each chord-apex+Kempe colouring, dissect WHERE the constancy
hypothesis fails:

  (1) Is h_phi constant on V(K_b) alone? On V(K_c) alone? On the
      intersection V(K_b) cap V(K_c)?
  (2) Per colouring, distribution of (#+1, #-1) shared vertices.
  (3) For each colouring, identify the "minority Heawood" shared
      vertices (= those whose h differs from the more frequent value
      on V(K_b) cup V(K_c)).  How many are there?  Are they
      concentrated at specific structural positions (v_n, A_{i+1},
      A_{i+3}, A_{i+4})?
  (4) Is h(v_n) always equal to / always different from the majority?

Under the constancy hypothesis, all of these distributions would be
trivial (everything same Heawood, 0 minority vertices).  Their
non-trivial empirical structure exposes where Path 4's "trap"
mechanism would have to apply.

Run with: sage experiments/check_constancy_obstruction.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,
)


def named_vertices(named, v_n=9999):
    """Recover A_i, A_{i+1}, ..., A_{i+4}."""
    def other(fs, v):
        return next(iter(fs - {v}))
    A_i  = other(named['side_0'], v_n)
    A_i1 = other(named['spike'],  v_n)
    A_i2 = other(named['side_1'], v_n)
    A_i3, A_i4 = sorted(named['merged'])
    return A_i, A_i1, A_i2, A_i3, A_i4


def test_one(D):
    D.is_planar(set_embedding=True)
    n_col = 0
    rec = {
        'const_kb': 0, 'const_kc': 0, 'const_cap': 0,
        # (#+1, #-1) on V(K_b) cup V(K_c) histogram
        'plus_minus_dist': {},
        # # minority Heawood vertices on V(K_b) cup V(K_c)
        'minority_count': {},
        # Is v_n minority? majority?
        'v_n_pos': {'minority': 0, 'majority': 0, 'tie': 0},
        # Position-tallies of minority for each "named" vertex
        'minority_at': {'v_n': 0, 'A_i': 0, 'A_i1': 0, 'A_i2': 0,
                         'A_i3': 0, 'A_i4': 0},
        # Is h(v_n) == h(A_{i+3}) == h(A_{i+4}) always?
        'v_n_eq_merged_endpts': 0,
        # Is h constant on the 6 named vertices {v_n, A_0..A_4}?
        'const_on_named6': 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)]
            v_n = 9999
            A_i, A_i1, A_i2, A_i3, A_i4 = named_vertices(named, v_n)
            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]
                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)
                V_union = V_b | V_c
                V_cap = V_b & V_c
                h_b_vals = {h[v] for v in V_b}
                h_c_vals = {h[v] for v in V_c}
                h_cap_vals = {h[v] for v in V_cap}
                if len(h_b_vals) == 1: rec['const_kb'] += 1
                if len(h_c_vals) == 1: rec['const_kc'] += 1
                if len(h_cap_vals) == 1: rec['const_cap'] += 1
                plus = sum(1 for v in V_union if h[v] == 1)
                minus = sum(1 for v in V_union if h[v] == -1)
                rec['plus_minus_dist'][(plus, minus)] = \
                    rec['plus_minus_dist'].get((plus, minus), 0) + 1
                # Majority sign on union
                if plus > minus:
                    maj = 1
                elif minus > plus:
                    maj = -1
                else:
                    maj = 0
                minority = sum(1 for v in V_union if h[v] != maj and maj != 0)
                if maj == 0:
                    minority = min(plus, minus)
                rec['minority_count'][minority] = \
                    rec['minority_count'].get(minority, 0) + 1
                # v_n's position
                hv = h.get(v_n)
                if hv is not None and maj != 0:
                    if hv == maj:
                        rec['v_n_pos']['majority'] += 1
                    else:
                        rec['v_n_pos']['minority'] += 1
                else:
                    rec['v_n_pos']['tie'] += 1
                # Named vertices: minority count
                for name, vv in [('v_n', v_n), ('A_i', A_i),
                                  ('A_i1', A_i1), ('A_i2', A_i2),
                                  ('A_i3', A_i3), ('A_i4', A_i4)]:
                    if vv in V_union and maj != 0 and h[vv] != maj:
                        rec['minority_at'][name] += 1
                # h(v_n) == h(A_{i+3}) == h(A_{i+4}) ?
                if h.get(v_n) == h.get(A_i3) == h.get(A_i4):
                    rec['v_n_eq_merged_endpts'] += 1
                # h constant on the 6 named vertices?
                named6 = [v_n, A_i, A_i1, A_i2, A_i3, A_i4]
                named6_vals = {h[vv] for vv in named6 if vv in h}
                if len(named6_vals) == 1:
                    rec['const_on_named6'] += 1
    return n_col, rec


def merge_into(g, r):
    for k in ('const_kb', 'const_kc', 'const_cap',
              'v_n_eq_merged_endpts', 'const_on_named6'):
        g[k] += r[k]
    for k in ('plus_minus_dist', 'minority_count'):
        for kk, vv in r[k].items():
            g[k][kk] = g[k].get(kk, 0) + vv
    for sub in ('v_n_pos', 'minority_at'):
        for kk, vv in r[sub].items():
            g[sub][kk] = g[sub].get(kk, 0) + vv


def main(max_n=18, time_budget_per_n=1800):
    print(f"Constancy obstruction analysis, n in [12, {max_n}]\n")
    grand = {
        'const_kb': 0, 'const_kc': 0, 'const_cap': 0,
        'plus_minus_dist': {}, 'minority_count': {},
        'v_n_pos': {'minority': 0, 'majority': 0, 'tie': 0},
        'minority_at': {'v_n': 0, 'A_i': 0, 'A_i1': 0, 'A_i2': 0,
                         'A_i3': 0, 'A_i4': 0},
        'v_n_eq_merged_endpts': 0,
        'const_on_named6': 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
        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(grand, ri)
        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  h constant on V(K_b):             {grand['const_kb']}/{grand_col}")
    print(f"  h constant on V(K_c):             {grand['const_kc']}/{grand_col}")
    print(f"  h constant on V(K_b) cap V(K_c):  {grand['const_cap']}/{grand_col}")
    print(f"  h constant on {{v_n, A_0..A_4}}:    "
          f"{grand['const_on_named6']}/{grand_col}")
    print(f"  h(v_n) == h(A_{{i+3}}) == h(A_{{i+4}}): "
          f"{grand['v_n_eq_merged_endpts']}/{grand_col}")
    print(f"\n  Minority count (= #vertices on V(K_b) cup V(K_c) with "
          f"non-majority h) distribution:")
    for k, v in sorted(grand['minority_count'].items()):
        pct = 100 * v / max(1, grand_col)
        print(f"    {k:>3}: {v} ({pct:.2f}%)")
    print(f"\n  v_n status on V(K_b) cup V(K_c):")
    print(f"    in majority: {grand['v_n_pos']['majority']} "
          f"({100*grand['v_n_pos']['majority']/max(1,grand_col):.2f}%)")
    print(f"    in minority: {grand['v_n_pos']['minority']} "
          f"({100*grand['v_n_pos']['minority']/max(1,grand_col):.2f}%)")
    print(f"    tie:         {grand['v_n_pos']['tie']} "
          f"({100*grand['v_n_pos']['tie']/max(1,grand_col):.2f}%)")
    print(f"\n  How often each named vertex is in the minority:")
    for name, c in grand['minority_at'].items():
        print(f"    {name:>5}: {c}/{grand_col} "
              f"({100*c/max(1,grand_col):.2f}%)")


if __name__ == '__main__':
    main()
