"""Investigate fiber distributions when the inner outerplanar graph O
has non-trivial chords.

A chord in O does NOT change the dual annular cycle T'_ann (which is
still C_{m+k}).  What it changes is the inside-O face structure: with
j non-crossing chords, O has j+1 inside-O faces.  Each face f dualizes
to a single vertex u_f in G', whose degree in T'_{f'} equals the
number of B_in edges on the boundary of f.

Concretely: if a face f contains B_in edges e_{a_1}, ..., e_{a_d},
then u_f is incident in T'_{f'} to the d D-triangle positions
p_{a_1}, ..., p_{a_d} on the dual cycle (one per B_in edge).  Proper
edge 3-coloring at u_f requires those d spoke colors to be pairwise
distinct.

Feasibility constraint: at k=3 colors, a face with d > 3 incident
B_in edges has no proper edge 3-coloring of u_f, so the tire admits
no global coloring.  All examples below satisfy d <= 3 per face.
"""
from collections import Counter
from itertools import product

COLORS = (1, 2, 3)


def d_positions_for(m: int, k: int) -> list[int]:
    """Return cycle positions of D-triangles on the dual cycle C_{m+k}
    for a balanced (Bresenham-style) alternating triangulation, in
    the cyclic order matching B_in edges e_0, e_1, ..., e_{k-1}."""
    n = m + k
    return [round(i * n / k) % n for i in range(k)]


def compute_faces_from_chords(k: int, chords: list[tuple[int, int]]) -> list[list[int]]:
    """Compute face partition of B_in = C_k cut by non-crossing chords.

    Each face is returned as a list of B_in edge indices belonging to
    it.  For non-crossing chords: edge e_a belongs to the smallest
    enclosing chord-arc, where chord (i, j) with i < j encloses e_a
    iff i <= a < j.  Edges not enclosed by any chord belong to the
    'outermost' face."""
    chords_norm = [tuple(sorted([a, b])) for (a, b) in chords]

    def enclosing_chord(edge: int):
        candidates = [(j - i, i, j) for (i, j) in chords_norm if i <= edge < j]
        if not candidates:
            return None
        candidates.sort()
        _, i, j = candidates[0]
        return (i, j)

    chord_to_face = {}
    next_face = 0
    edge_to_face = {}
    for a in range(k):
        ch = enclosing_chord(a)
        if ch not in chord_to_face:
            chord_to_face[ch] = next_face
            next_face += 1
        edge_to_face[a] = chord_to_face[ch]

    faces = [[] for _ in range(next_face)]
    for a in range(k):
        faces[edge_to_face[a]].append(a)
    return faces


def fiber_distribution(m: int, k: int, chords: list[tuple[int, int]]):
    """Compute fiber distribution N(T'_{f'}; σ) over σ ∈ {1,2,3}^n for
    the tire with given m, k, and chord set in O.

    Returns (fibers, d_positions, faces_as_cycle_positions).
    """
    n = m + k
    d_positions = d_positions_for(m, k)
    o_faces = compute_faces_from_chords(k, chords)
    d_positions_by_face = [
        [d_positions[a] for a in face_edges]
        for face_edges in o_faces
    ]

    fibers: Counter[tuple[int, ...]] = Counter()
    for ce in product(COLORS, repeat=n):
        if not all(ce[i] != ce[(i + 1) % n] for i in range(n)):
            continue
        sigma = tuple(
            ({1, 2, 3} - {ce[(i - 1) % n], ce[i]}).pop()
            for i in range(n)
        )
        ok = True
        for positions in d_positions_by_face:
            colors = [sigma[p] for p in positions]
            if len(set(colors)) != len(colors):
                ok = False
                break
        if ok:
            fibers[sigma] += 1
    return fibers, d_positions, d_positions_by_face


def projection_support(fibers, positions: list[int]) -> set[tuple[int, ...]]:
    return {tuple(sigma[p] for p in positions) for sigma in fibers}


CASES = [
    # (m, k, chords, label)
    (4, 4, [],            "(4,4) no chord  [baseline]"),
    (4, 4, [(0, 2)],      "(4,4) chord (0,2)  faces 2+2"),
    (5, 5, [],            "(5,5) no chord  [baseline]"),
    (5, 5, [(0, 2)],      "(5,5) chord (0,2)  faces 2+3"),
    (5, 5, [(0, 3)],      "(5,5) chord (0,3)  faces 3+2"),
    (6, 6, [],            "(6,6) no chord  [baseline]"),
    (6, 6, [(0, 3)],      "(6,6) chord (0,3)  antipodal  faces 3+3"),
    (6, 6, [(0, 2), (3, 5)], "(6,6) chords (0,2)(3,5)  faces 2+2+2"),
    # k=4 with smaller outer cycle:
    (3, 4, [],            "(3,4) no chord"),
    (3, 4, [(0, 2)],      "(3,4) chord (0,2)"),
    # larger n for k=4:
    (6, 4, [],            "(6,4) no chord"),
    (6, 4, [(0, 2)],      "(6,4) chord (0,2)"),
]


def u_positions_for(m: int, k: int) -> list[int]:
    """U-positions (outer-spoke cycle positions) = complement of D-positions."""
    n = m + k
    d = set(d_positions_for(m, k))
    return [p for p in range(n) if p not in d]


def spoke_only_fiber_distribution(n: int):
    """Step-1 baseline: G_n = C_n + n independent pendants (Steiner-rich,
    each B_in edge gets its own inside-O sub-triangle). No chord
    constraints; every σ that comes from a proper cycle 3-coloring is
    realisable."""
    fibers: Counter[tuple[int, ...]] = Counter()
    for ce in product(COLORS, repeat=n):
        if not all(ce[i] != ce[(i + 1) % n] for i in range(n)):
            continue
        sigma = tuple(
            ({1, 2, 3} - {ce[(i - 1) % n], ce[i]}).pop()
            for i in range(n)
        )
        fibers[sigma] += 1
    return dict(fibers)


def main():
    print("Two models compared per (m, k, chord-set):")
    print("  STEINER-RICH: G triangulates each O-face using internal Steiner vertices,")
    print("    so each B_in edge dualizes to its own (degree-1) inside-O triangle.")
    print("    Chord set has NO effect on T'_{f'}; equivalent to step-1 baseline.")
    print("  STEINER-POOR: G does NOT further sub-triangulate O-faces beyond O itself.")
    print("    Each O-face becomes one G-face with degree = (B_in-edge count).")
    print("    Edge-3-colorable only if every O-face has ≤ 3 B_in edges.")
    print()
    print(f"{'case':<42s}  {'n':>3s}  "
          f"{'P_e SR':>7s}  {'D SR':>7s}  {'U SR':>7s}  | "
          f"{'P_e SP':>7s}  {'D SP':>7s}  {'U SP':>7s}  | "
          f"faces (cycle-position groups)")
    print("-" * 145)
    for m, k, chords, label in CASES:
        n = m + k
        # Steiner-poor (with chord-induced constraints):
        fibers_sp, d_pos, faces_cp = fiber_distribution(m, k, chords)
        u_pos = u_positions_for(m, k)
        pe_sp = sum(fibers_sp.values())
        d_proj_sp = projection_support(fibers_sp, d_pos)
        u_proj_sp = projection_support(fibers_sp, u_pos)

        # Steiner-rich baseline (chord-set ignored):
        fibers_sr = spoke_only_fiber_distribution(n)
        pe_sr = sum(fibers_sr.values())
        d_proj_sr = projection_support(fibers_sr, d_pos)
        u_proj_sr = projection_support(fibers_sr, u_pos)

        d_uni = 3 ** k
        u_uni = 3 ** m
        faces_str = " | ".join(
            "{" + ",".join(map(str, sorted(f))) + "}" for f in faces_cp
        )
        print(
            f"{label:<42s}  {n:>3d}  "
            f"{pe_sr:>7d}  {len(d_proj_sr):>3d}/{d_uni:<3d}  {len(u_proj_sr):>3d}/{u_uni:<3d}  | "
            f"{pe_sp:>7d}  {len(d_proj_sp):>3d}/{d_uni:<3d}  {len(u_proj_sp):>3d}/{u_uni:<3d}  | "
            f"{faces_str}"
        )


if __name__ == '__main__':
    main()
