"""For every chord-apex+Kempe colouring, walk the K_b cycle, and at
each K_b-vertex classify the c-edge as 'local LEFT' or 'local RIGHT'
of the walking direction, using the CW embedding at the vertex.

At v with incoming K-edge to neighbour u_in and outgoing K-edge to
u_out, look at the CW cyclic order of v's three neighbours. Going CW
from u_in:
  - if we hit the c-neighbour before u_out: c-edge is local RIGHT.
  - if we hit u_out before the c-neighbour: c-edge is local LEFT.

Tally over consecutive pairs (v_0, v_1) on K_b: combinations of
(same h_phi?, same local side?).

Predicted biconditional:
  h_phi(v_0) == h_phi(v_1) <==> c-edges on OPPOSITE local sides.
(Equivalently, in global terms, same Heawood at consecutive K-vertices
forces c-edges to alternate inside/outside of K_b.)

Run with: sage experiments/check_heawood_local_side.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 c_edge_local_side(v, c_color, col, edges, embedding, u_in, u_out):
    """At vertex v, walking K_b from u_in into v and out to u_out, find
    the c-coloured neighbour of v and return 'R' (between u_in and u_out
    in CW) or 'L' (between u_out and u_in in CW)."""
    nbrs_cw = embedding[v]   # list of neighbours in clockwise order
    n = len(nbrs_cw)
    pos = {u: i for i, u in enumerate(nbrs_cw)}
    if u_in not in pos or u_out not in pos:
        return None
    p_in = pos[u_in]
    p_out = pos[u_out]
    # find c-neighbour
    c_nbr = None
    for u in nbrs_cw:
        ei = edge_idx(edges, frozenset((v, u)))
        if ei is not None and col[ei] == c_color:
            c_nbr = u
            break
    if c_nbr is None:
        return None
    p_c = pos[c_nbr]
    # Walk CW from p_in (skipping p_in itself):
    for offset in range(1, n):
        idx = (p_in + offset) % n
        if idx == p_c:
            return 'R'
        if idx == p_out:
            return 'L'
    return None


def test_one(D):
    """Tally (same h, same local side) over consecutive K_b pairs."""
    D.is_planar(set_embedding=True)
    n_col = 0
    counts = {(True, True): 0, (True, False): 0,
              (False, True): 0, (False, False): 0}
    skipped = 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)
            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
                try:
                    h = heawood_numbers(H, edges, col)
                except RuntimeError:
                    skipped += 1
                    continue
                merged_idx = edge_idx(edges, named['merged'])
                a = col[merged_idx]
                for b in range(3):
                    if b == a: continue
                    c_color = 3 - a - b
                    walk = trace_kempe_cycle(edges, col, merged_idx, (a, b))
                    # walk[k] = (edge_idx, leave_vertex). Reconstruct vertex
                    # sequence (the vertex we end at after edge k = the
                    # leave_vertex of edge k = the start vertex of edge k+1).
                    L = len(walk)
                    if L == 0: continue
                    # at vertex v = walk[k][1], we arrived via edge walk[k]
                    # and leave via edge walk[(k+1) % L].
                    sides = []
                    h_vals = []
                    for k in range(L):
                        v = walk[k][1]
                        in_edge_idx = walk[k][0]
                        out_edge_idx = walk[(k + 1) % L][0]
                        # find u_in (the other endpoint of in_edge) and u_out.
                        in_e = edges[in_edge_idx]
                        out_e = edges[out_edge_idx]
                        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, c_color, col, edges, emb, u_in, u_out)
                        sides.append(side)
                        h_vals.append(h[v])
                    # Tally consecutive pairs
                    for k in range(L):
                        s0 = sides[k]; s1 = sides[(k + 1) % L]
                        h0 = h_vals[k]; h1 = h_vals[(k + 1) % L]
                        if s0 is None or s1 is None: continue
                        same_h = (h0 == h1)
                        same_side = (s0 == s1)
                        counts[(same_h, same_side)] += 1
    return n_col, counts, skipped


def main(max_n=18, time_budget_per_n=1800):
    print(f"(same h, same local-c-side) on consecutive K_b pairs, "
          f"n in [12, {max_n}]\n")
    grand_col = 0
    grand_counts = {(True, True): 0, (True, False): 0,
                    (False, True): 0, (False, False): 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
        counts_n = {(True, True): 0, (True, False): 0,
                    (False, True): 0, (False, False): 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, c_i, sk_i = test_one(D)
            n_col_n += n_col_i
            for k, v in c_i.items(): counts_n[k] = counts_n.get(k, 0) + v
        elapsed = time.time() - start
        total_pairs = sum(counts_n.values())
        print(f"n={n}: {n_col_n} col., {total_pairs} pairs  [{elapsed:.0f}s]")
        print(f"  {counts_n}")
        sys.stdout.flush()
        grand_col += n_col_n
        for k, v in counts_n.items(): grand_counts[k] = grand_counts.get(k, 0) + v

    print()
    print("=" * 78)
    print(f"Grand totals (n in [12, {max_n}], {grand_col} col., "
          f"{sum(grand_counts.values())} pairs):")
    print(f"  (same h=Y, same side=Y): {grand_counts[(True, True)]}")
    print(f"  (same h=Y, same side=N): {grand_counts[(True, False)]}")
    print(f"  (same h=N, same side=Y): {grand_counts[(False, True)]}")
    print(f"  (same h=N, same side=N): {grand_counts[(False, False)]}")
    total = sum(grand_counts.values())
    if total > 0:
        for k, v in grand_counts.items():
            print(f"    {k}: {100*v/total:.2f}%")
    if (grand_counts[(True, True)] == 0
            and grand_counts[(False, False)] == 0
            and total > 0):
        print(f"  *** PREDICTED BICONDITIONAL HOLDS: "
              f"same h <==> different side ***")
    elif grand_counts[(True, True)] == 0 and total > 0:
        print(f"  empirical implication: same h ==> different side")
    elif grand_counts[(False, False)] == 0 and total > 0:
        print(f"  empirical implication: different h ==> same side")
    else:
        print(f"  no clean biconditional")


if __name__ == '__main__':
    main()
