"""Tire graphs: triangulations of the annular region between an outer
cycle and the outer face of an inner outerplanar graph.

Definition.
  A *tire graph* T = (C_out, O, Tann) consists of:
    - an outer cycle C_out of length m, on vertices V_out,
    - an outerplanar graph O whose outer-face boundary is a simple cycle
      C_in of length k, on vertices V_in (V_in disjoint from V_out),
    - a triangulation Tann of the closed annulus with outer boundary
      C_out and inner boundary C_in, using ONLY V_out ∪ V_in (no Steiner
      points).
  As a plane graph: V(T) = V_out ∪ V_in,
                    E(T) = E(C_out) ∪ E(O) ∪ E(Tann).

Random generation.
  Annular triangulations between C_m and C_k (no interior vertices) are
  in bijection with sequences of m "O" moves and k "I" moves, once an
  anchor spoke (V_out[0]–V_in[0]) is fixed.  We sample uniformly over
  the C(m+k, m) such sequences.  Inner-outerplanar-graph chords are
  sampled by greedily adding non-crossing chords until n_chords are
  placed (or no more fit).

Run:
  python3 experiments/tire_graph.py
"""
import math
import os
import random

import matplotlib.pyplot as plt
import matplotlib.patches as patches


def random_tire(m, k, n_chords=0, seed=None):
    """Generate a random tire graph with outer cycle length m and inner
    outerplanar graph whose outer face cycle has length k."""
    rng = random.Random(seed)

    outer = list(range(m))               # outer-cycle vertex labels
    inner = list(range(m, m + k))        # inner-cycle vertex labels

    edges = set()

    # outer cycle
    for i in range(m):
        edges.add(frozenset({outer[i], outer[(i + 1) % m]}))

    # inner cycle (= outer face of inner outerplanar graph)
    for j in range(k):
        edges.add(frozenset({inner[j], inner[(j + 1) % k]}))

    # random non-crossing chords inside the inner cycle
    inner_chords = set()
    candidates = []
    for a in range(k):
        for b in range(a + 2, k):
            if not (a == 0 and b == k - 1):    # skip the cycle edge
                candidates.append((a, b))
    rng.shuffle(candidates)
    for (a, b) in candidates:
        if len(inner_chords) >= n_chords:
            break
        ok = True
        for (a2, b2) in inner_chords:
            # chords (a,b), (a2,b2) cross iff one strictly separates the
            # other on the cycle
            if (a < a2 < b < b2) or (a2 < a < b2 < b):
                ok = False
                break
        if ok:
            inner_chords.add((a, b))
            edges.add(frozenset({inner[a], inner[b]}))

    # anchor spoke
    edges.add(frozenset({outer[0], inner[0]}))

    # annular triangulation via random lattice path
    moves = ['O'] * m + ['I'] * k
    rng.shuffle(moves)

    triangles = []
    i, j = 0, 0
    for move in moves:
        if move == 'O':
            tri = (outer[i % m], inner[j % k], outer[(i + 1) % m])
            triangles.append(tri)
            edges.add(frozenset({inner[j % k], outer[(i + 1) % m]}))
            i += 1
        else:
            tri = (outer[i % m], inner[j % k], inner[(j + 1) % k])
            triangles.append(tri)
            edges.add(frozenset({outer[i % m], inner[(j + 1) % k]}))
            j += 1

    return {
        'm': m, 'k': k, 'n_chords': len(inner_chords),
        'outer': outer, 'inner': inner,
        'edges': [tuple(sorted(e)) for e in edges],
        'triangles': triangles,
        'inner_chords': sorted(inner_chords),
        'lattice_path': ''.join(moves),
        'seed': seed,
    }


def planar_positions(tire, R_out=1.0, R_in=0.45):
    """Compute crossing-free straight-line positions on concentric circles.

    Walking spokes in cyclic order starting from the anchor, each outer
    (resp. inner) vertex appears in a contiguous arc of spokes; we
    place each vertex at the angular centroid of those spoke positions
    on its circle, which preserves the cyclic order implied by the
    planar embedding and so makes all annular edges crossing-free."""
    m, k = tire['m'], tire['k']
    outer, inner = tire['outer'], tire['inner']
    moves = tire['lattice_path']

    # Build spoke list in cyclic order: spoke t connects outer[i_t] to
    # inner[j_t] where (i_t, j_t) is the lattice position after t moves.
    spokes = [(outer[0], inner[0])]   # anchor at t=0
    i, j = 0, 0
    for mv in moves:
        if mv == 'O':
            i += 1
        else:
            j += 1
        spokes.append((outer[i % m], inner[j % k]))
    spokes = spokes[:-1]               # drop wrap-back duplicate
    n = len(spokes)                    # = m + k

    out_t = {v: [] for v in outer}
    in_t = {v: [] for v in inner}
    for t, (ov, iv) in enumerate(spokes):
        out_t[ov].append(t)
        in_t[iv].append(t)

    def circ_mean(ts):
        x = sum(math.cos(2 * math.pi * t / n) for t in ts)
        y = sum(math.sin(2 * math.pi * t / n) for t in ts)
        return math.atan2(y, x)

    pos = {}
    for v in outer:
        theta = circ_mean(out_t[v])
        pos[v] = (R_out * math.cos(theta), R_out * math.sin(theta))
    for v in inner:
        theta = circ_mean(in_t[v])
        pos[v] = (R_in * math.cos(theta), R_in * math.sin(theta))
    return pos


def draw_tire(tire, filename, title=None):
    """Draw the tire on concentric circles with annular triangulation as
    straight-line edges.  Outer cycle = blue, inner cycle = red, inner
    chords = orange, annular spokes/chords = grey."""
    m, k = tire['m'], tire['k']
    outer, inner = tire['outer'], tire['inner']
    edges = tire['edges']

    R_out, R_in = 1.0, 0.45
    pos = planar_positions(tire, R_out=R_out, R_in=R_in)

    fig, ax = plt.subplots(figsize=(5.5, 5.5))

    # faint guide circles
    for r in (R_out + 0.05, R_in - 0.05):
        ax.add_patch(patches.Circle((0, 0), r, fill=False,
                                    edgecolor='lightgray',
                                    linewidth=0.5, linestyle='--'))

    outer_set = set(outer)
    inner_set = set(inner)
    C = {
        'outer_cycle': '#1f77b4',
        'inner_cycle': '#d62728',
        'inner_chord': '#ff7f0e',
        'spoke':       '#7f7f7f',
    }

    for (u, v) in edges:
        if u in outer_set and v in outer_set:
            color, lw = C['outer_cycle'], 2.4
        elif u in inner_set and v in inner_set:
            ia, ib = u - m, v - m
            d = abs(ia - ib)
            d = min(d, k - d)
            if d == 1:
                color, lw = C['inner_cycle'], 2.4
            else:
                color, lw = C['inner_chord'], 1.6
        else:
            color, lw = C['spoke'], 1.0
        x1, y1 = pos[u]; x2, y2 = pos[v]
        ax.plot([x1, x2], [y1, y2], color=color, linewidth=lw, zorder=1)

    for v in outer:
        x, y = pos[v]
        ax.plot(x, y, 'o', color=C['outer_cycle'], markersize=14, zorder=2)
        ax.annotate(str(v), (x, y), color='white', ha='center',
                    va='center', fontsize=8, fontweight='bold', zorder=3)
    for v in inner:
        x, y = pos[v]
        ax.plot(x, y, 'o', color=C['inner_cycle'], markersize=12, zorder=2)
        ax.annotate(str(v), (x, y), color='white', ha='center',
                    va='center', fontsize=7, fontweight='bold', zorder=3)

    ax.set_xlim(-1.18, 1.18)
    ax.set_ylim(-1.18, 1.18)
    ax.set_aspect('equal')
    ax.axis('off')

    if title is None:
        title = (f"tire(m={m}, k={k}, chords={tire['n_chords']}, "
                 f"seed={tire['seed']})")
    ax.set_title(title, fontsize=11)

    plt.savefig(filename, dpi=140, bbox_inches='tight')
    plt.close()


def main():
    out_dir = os.path.dirname(os.path.abspath(__file__))
    # (m, k, n_chords, seed) -- a mix of small/medium, no chords / chords
    examples = [
        (6, 3, 0, 1),
        (8, 5, 0, 7),
        (10, 4, 1, 11),
        (12, 7, 3, 23),
        (9, 9, 2, 42),
        (14, 5, 0, 100),
    ]
    paths = []
    for (m, k, nc, seed) in examples:
        tire = random_tire(m, k, n_chords=nc, seed=seed)
        fn = os.path.join(out_dir, f"tire_m{m}_k{k}_c{nc}_s{seed}.png")
        draw_tire(tire, fn)
        paths.append(fn)
        print(f"  wrote {os.path.basename(fn)}  "
              f"path={tire['lattice_path']}  "
              f"chords={tire['inner_chords']}")
    print(f"\nGenerated {len(paths)} tires.")
    return paths


if __name__ == '__main__':
    main()
