"""Two diagnostics:

  (1) Cycle-closure / winding count on K_b and K_c.
      For each cycle, count the c-edges (off-cycle) at K-vertices that
      lie on the local LEFT vs local RIGHT side of the walking direction.
      Under constancy (h constant on V(K_b) U V(K_c)), Lemma A predicts
      sides alternate, so #LEFT == #RIGHT == |V(K)|/2 exactly. We tally
      the empirical (#LEFT, #RIGHT) split for K_b and K_c.

  (2) Ordered sequence of (i_b mod 2, i_c mod 2) parity pairs at shared
      vertices, sorted by i_b position. We look at the sequence of
      transitions in the 4-state space {(0,0), (0,1), (1,0), (1,1)},
      and ask whether the empirical sequences are constrained.

Run with: sage experiments/check_shared_sequence_and_winding.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
from check_heawood_local_side import c_edge_local_side


def walk_positions(walk):
    pos = {}
    for k, (_, leave_v) in enumerate(walk):
        if leave_v not in pos:
            pos[leave_v] = k
    return pos


def collect_cycle_sides(H, walk, edges, col, emb, third_color):
    """For each step k of walk, compute side ('L' or 'R') of the
    third_color edge at the leave_v vertex relative to the walking
    direction (in_edge = walk[k], out_edge = walk[k+1]).
    Returns list of sides indexed by k.
    """
    L = len(walk)
    sides = []
    for k in range(L):
        v = walk[k][1]
        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]
        s = c_edge_local_side(v, third_color, col, edges, emb, u_in, u_out)
        sides.append(s)
    return sides


def test_one(D):
    D.is_planar(set_embedding=True)
    n_col = 0
    # (1) Side-split distribution: (#L, #R, L_cycle).
    kb_split = {}; kc_split = {}
    # (2) Ordered sequence of (i_b mod 2, i_c mod 2) at shared verts in
    #     K_b walk order: sequences (as tuples).
    seq_dist = {}     # signature tuple -> count
    # Aggregate transition counts between the 4 parity states
    transitions = {}  # (prev_state, curr_state) -> count
    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]
                b_color, c_color = bs[0], bs[1]
                walk_b = trace_kempe_cycle(edges, col, merged_idx, (a, b_color))
                walk_c = trace_kempe_cycle(edges, col, merged_idx, (a, c_color))
                pos_b = walk_positions(walk_b)
                pos_c = walk_positions(walk_c)
                shared = set(pos_b.keys()) & set(pos_c.keys())
                # (1)
                sides_b = collect_cycle_sides(H, walk_b, edges, col, emb, c_color)
                sides_c = collect_cycle_sides(H, walk_c, edges, col, emb, b_color)
                Lb_len = len(walk_b); Lc_len = len(walk_c)
                Lb = sum(1 for s in sides_b if s == 'L')
                Rb = sum(1 for s in sides_b if s == 'R')
                Lc = sum(1 for s in sides_c if s == 'L')
                Rc = sum(1 for s in sides_c if s == 'R')
                kb_split[(Lb, Rb)] = kb_split.get((Lb, Rb), 0) + 1
                kc_split[(Lc, Rc)] = kc_split.get((Lc, Rc), 0) + 1
                # (2)
                seq = []
                for k in range(Lb_len):
                    v = walk_b[k][1]
                    if v in shared:
                        seq.append((k % 2, pos_c[v] % 2))
                # Signature: count occurrences of each state in order.
                sig = tuple(seq)
                seq_dist[sig] = seq_dist.get(sig, 0) + 1
                # Transitions:
                for i in range(len(seq)):
                    prev = seq[i - 1]   # wraps to seq[-1]
                    curr = seq[i]
                    transitions[(prev, curr)] = transitions.get(
                        (prev, curr), 0) + 1
    return n_col, kb_split, kc_split, seq_dist, transitions


def main(max_n=18, time_budget_per_n=1800):
    print(f"Cycle-side splits and shared-vertex sequence, "
          f"n in [12, {max_n}]\n")
    grand_col = 0
    grand_kb = {}; grand_kc = {}
    grand_seq = {}
    grand_tr = {}
    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, kb_i, kc_i, seq_i, tr_i = test_one(D)
            n_col_n += ni
            for k, v in kb_i.items(): grand_kb[k] = grand_kb.get(k, 0) + v
            for k, v in kc_i.items(): grand_kc[k] = grand_kc.get(k, 0) + v
            for k, v in seq_i.items(): grand_seq[k] = grand_seq.get(k, 0) + v
            for k, v in tr_i.items(): grand_tr[k] = grand_tr.get(k, 0) + v
        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  K_b (#L, #R) distribution:")
    for k, v in sorted(grand_kb.items()):
        print(f"    {k}: {v}")
    print(f"\n  K_c (#L, #R) distribution:")
    for k, v in sorted(grand_kc.items()):
        print(f"    {k}: {v}")
    # Check: is #L == #R always?
    kb_equal = sum(v for (L, R), v in grand_kb.items() if L == R)
    kc_equal = sum(v for (L, R), v in grand_kc.items() if L == R)
    total_kb = sum(grand_kb.values())
    total_kc = sum(grand_kc.values())
    print(f"\n  K_b: #L == #R in {kb_equal}/{total_kb} "
          f"({100*kb_equal/max(1,total_kb):.2f}%)")
    print(f"  K_c: #L == #R in {kc_equal}/{total_kc} "
          f"({100*kc_equal/max(1,total_kc):.2f}%)")

    print(f"\n  Transition counts in the 4-state parity space "
          f"((prev) -> (curr)):")
    states = [(0,0), (0,1), (1,0), (1,1)]
    for s1 in states:
        for s2 in states:
            c = grand_tr.get((s1, s2), 0)
            print(f"    {s1} -> {s2}: {c}")

    print(f"\n  Top 10 most common shared-sequence signatures (by K_b order):")
    top = sorted(grand_seq.items(), key=lambda kv: -kv[1])[:10]
    for sig, c in top:
        print(f"    {sig}: {c}")


if __name__ == '__main__':
    main()
