"""Verify the combinatorial winding invariant for simple closed cycles
in a 3-regular planar graph:

  Claim: For a simple closed cycle C in a 3-regular planar graph H
  (with C ⊆ E(H), |C|/2 edges of C at each vertex of C), walked CCW
  around its interior, define for each v ∈ V(C):
    turn-sign(v) = +1 if the third edge (= the one of v's 3 edges NOT
                   on C) is on the OUTSIDE of C, locally at v.
                   (= the walker turns LEFT into the interior).
              -1 if the third edge is on the INSIDE.

  Hypothesis: Σ_v turn-sign(v) = ±6 (= ±(L_C / |V|_C-cycle-curvature)).

This is a combinatorial Gauss-Bonnet for cubic plane graphs:
internal/external turn parity around any face / cycle.

Sanity-check this on:
  - Triangle (length 3) in K_4.
  - Quadrilateral (length 4) in Q_3.
  - Pentagon (length 5) in dodecahedron.
  - Hexagon (length 6) in some triangulation dual.

Also on Kempe cycles in chord-apex+Kempe colourings of reduced duals.

If the invariant holds (always ±6), then combining with Lemma 5.2's
alternation (= turn signs alternate +,-,+,- on any K_b under constancy),
we'd have #(+) = #(-) = L_b / 2, so #(+) - #(-) = 0 ≠ ±6, giving a
direct contradiction.

Run with: sage experiments/check_combinatorial_winding.py
"""
import os
import sys

from sage.all import Graph
from sage.graphs.graph_generators import graphs


def cycle_winding(G, cycle_vertices):
    """Compute Σ_v turn-sign(v) for a simple closed cycle in 3-reg planar G.

    cycle_vertices: list of vertices in walking order around the cycle.
    Walking direction: CCW (= traversal such that interior is on the LEFT).

    For each v in the cycle, the 3 edges at v are split as: 2 cycle
    edges (to v's neighbours in the cycle) and 1 third edge (off-cycle).
    The third edge is at some position in v's CW rotation system.

    turn-sign(v) = +1 if the third edge is OUTSIDE the cycle (= on the
                  right of the CCW walk locally), -1 if INSIDE.
    """
    G.is_planar(set_embedding=True)
    emb = G.get_embedding()
    L = len(cycle_vertices)
    total = 0
    for k in range(L):
        v = cycle_vertices[k]
        prev = cycle_vertices[(k - 1) % L]
        nxt = cycle_vertices[(k + 1) % L]
        # CW rotation at v: emb[v] is the list of neighbours in CW
        # order.
        nbrs = emb[v]
        # We need positions of prev, nxt, and third.
        if prev not in nbrs or nxt not in nbrs:
            return None
        # The third edge endpoint:
        third = [u for u in nbrs if u != prev and u != nxt]
        if len(third) != 1:
            return None
        third = third[0]
        # CW order of (prev, nxt, third) in emb[v]:
        idx_prev = nbrs.index(prev)
        idx_nxt = nbrs.index(nxt)
        idx_third = nbrs.index(third)
        # In CW order, what's the cyclic arrangement?
        # Walking CCW around the cycle means walker is on edge (prev, v),
        # at v, and exits via (v, nxt). The interior of the cycle is
        # on the LEFT of the walker.
        # The third edge is at idx_third in CW rotation.
        # If we go CW from idx_prev to idx_nxt:
        #   - if idx_third is in this CW interval, third is on one side;
        #   - else on the other side.
        # For 3-regular vertex with CW order (positions 0, 1, 2 cyclically),
        # idx_prev, idx_nxt, idx_third are some permutation of {0, 1, 2}.
        # Define: third is on the RIGHT of walker (= going CW from prev
        # to nxt without passing through third) ⟺ third is NOT between
        # prev and nxt in CW direction.
        # The CW order at v: nbrs[0], nbrs[1], nbrs[2].
        # CW direction at v: 0 → 1 → 2 → 0.
        # CW from prev to nxt: starting at idx_prev, going CW (= +1 mod 3),
        # we reach idx_nxt after some steps.
        # If we pass through idx_third in 1 step: third is between prev
        # and nxt in CW direction (i.e., on one specific side).
        # If we don't pass through idx_third (= go directly from prev to
        # nxt in 1 CW step): third is on the OTHER side.
        # 1 CW step from idx_prev: (idx_prev + 1) % 3.
        if (idx_prev + 1) % 3 == idx_nxt:
            # CW step from prev to nxt is direct (1 step).
            # Third is at the "other" position, NOT between in CW.
            # This means third is on the "left" of walker going CCW
            # along the cycle (= interior side).
            # turn-sign = -1 (third inside).
            sign = -1
        elif (idx_prev + 2) % 3 == idx_nxt:
            # CW step from prev to nxt is 2 steps (passing through
            # idx_third).
            # Third is BETWEEN prev and nxt in CW direction.
            # turn-sign = +1 (third outside, walker turns left into
            # interior).
            sign = +1
        else:
            return None
        total += sign
    return total


def face_to_cycle(face):
    """Convert face (= list of edges) to ordered vertex cycle."""
    if not face:
        return None
    verts = [face[0][0]]
    for e in face:
        verts.append(e[1])
    return verts[:-1]   # last vertex = first vertex


def main():
    tests = []
    # K_4 = tetrahedron
    K4 = graphs.CompleteGraph(4)
    K4.is_planar(set_embedding=True)
    tests.append(('K_4', K4))
    # Q_3 = cube
    Q3 = graphs.CubeGraph(3)
    Q3.is_planar(set_embedding=True)
    tests.append(('Q_3', Q3))
    # Dodecahedron
    Dod = graphs.DodecahedralGraph()
    Dod.is_planar(set_embedding=True)
    tests.append(('Dodecahedron', Dod))
    # Triangular prism
    Tprism = graphs.CompleteBipartiteGraph(2, 3)   # not 3-regular
    # Use Q3 plus more
    tests.append(('Triangular prism (3-prism)', graphs.GeneralizedPetersenGraph(3, 1)))

    for name, G in tests:
        if not all(G.degree(v) == 3 for v in G.vertex_iterator()):
            print(f"\n{name}: NOT 3-regular (degree set = "
                  f"{sorted(set(G.degree()))}); skipping")
            continue
        if not G.is_planar(set_embedding=True):
            print(f"\n{name}: NOT planar; skipping")
            continue
        print(f"\n{name}: |V|={G.order()}, |E|={G.size()}, faces:")
        faces = G.faces()
        for f in faces:
            cyc = face_to_cycle(f)
            if cyc is None:
                print(f"  bad face")
                continue
            w = cycle_winding(G, cyc)
            print(f"  face length {len(cyc)}, cycle={cyc}, winding={w}")


if __name__ == '__main__':
    main()
