"""Brute-force search for (R1)/(R2) violations in small maximal planar
graphs.

For each maximal planar graph G on n vertices (with delta(G) >= 3),
for each single-vertex source v_0 in V(G), compute:
  - BFS depths from v_0.
  - For each face, its dual depth (= min BFS level of vertices).
  - For each depth d, connected components of the dual subgraph G'_d.
  - For each component, check (R1) manifold property and count (R2)
    boundary components.

Report:
  - Per depth d, count of components and count of (R1)-violators,
    (R2)-violators, both.

Run: sage -python experiments/check_r2.py [n_min] [n_max]
"""
import sys
import json
from collections import defaultdict

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


def face_verts(face):
    """face = list of edges [(u, v), (v, w), ...].  Return ordered vertex
    cycle [u, v, w, ...]."""
    if not face:
        return []
    verts = [face[0][0]]
    for e in face:
        verts.append(e[1])
    return verts[:-1]   # last = first


def face_edges_set(face):
    return {frozenset(e) for e in face}


def find_components(face_depths, target_d):
    """face_depths: list of (face, depth, vertex_set).
    Returns list of components; each component = list of (face_idx, face_depth_entry)."""
    cands = [(i, fd) for i, fd in enumerate(face_depths) if fd[1] == target_d]
    n = len(cands)
    if n == 0:
        return []
    edge_to_idxs = defaultdict(list)
    for k, (i, fd) in enumerate(cands):
        for e in face_edges_set(fd[0]):
            edge_to_idxs[e].append(k)
    adj = [[] for _ in range(n)]
    for idxs in edge_to_idxs.values():
        if len(idxs) >= 2:
            for a in idxs:
                for b in idxs:
                    if a != b:
                        adj[a].append(b)
    visited = [False] * n
    comps = []
    for s in range(n):
        if visited[s]:
            continue
        stack = [s]
        comp = []
        while stack:
            x = stack.pop()
            if visited[x]:
                continue
            visited[x] = True
            comp.append(cands[x])
            for y in adj[x]:
                if not visited[y]:
                    stack.append(y)
        comps.append(comp)
    return comps


def check_component(comp, G, emb):
    """For a connected component of same-depth faces, check R1 (manifold)
    and count boundary components (R2)."""
    # boundary edges: edges of G appearing in exactly one face of comp
    edge_count = defaultdict(int)
    for (_, fd) in comp:
        for e in face_edges_set(fd[0]):
            edge_count[e] += 1
    bdry_edges = {e for e, c in edge_count.items() if c == 1}
    if not bdry_edges:
        # No boundary (closed surface).  In a planar graph this shouldn't happen.
        return {'is_manifold': False, 'n_boundary': 0, 'reason': 'no boundary'}

    # R1 check: at each vertex v in V(comp), count how many faces of comp
    # are incident to v, and check whether they form a contiguous arc in
    # v's rotation in emb.
    comp_face_edges = []
    for (_, fd) in comp:
        comp_face_edges.append(face_edges_set(fd[0]))
    vertices = set()
    for fes in comp_face_edges:
        for e in fes:
            vertices.update(e)

    is_manifold = True
    pinch_vertices = []
    for v in vertices:
        rot = emb[v]   # cyclic neighbour order
        # The faces incident to v in G are the triangles {v, rot[i], rot[i+1]}.
        # Check which ones are in comp.
        in_comp = []
        for i in range(len(rot)):
            u = rot[i]
            w = rot[(i + 1) % len(rot)]
            face_e = {frozenset({v, u}), frozenset({u, w}), frozenset({v, w})}
            if any(face_e == fes for fes in comp_face_edges):
                in_comp.append(True)
            else:
                in_comp.append(False)
        # Count "runs" of True in cyclic sequence
        n_true = sum(in_comp)
        if n_true == 0:
            continue
        if n_true == len(in_comp):
            continue  # entire rotation in comp, vertex is interior
        # Count transitions from False to True (cyclic)
        n_runs = 0
        for i in range(len(in_comp)):
            if in_comp[i] and not in_comp[(i - 1) % len(in_comp)]:
                n_runs += 1
        if n_runs > 1:
            is_manifold = False
            pinch_vertices.append(v)

    # R2 check: count boundary components.
    # Build bdry_graph: undirected graph on vertices touching bdry_edges,
    # edges = bdry_edges.
    bdry_graph = defaultdict(set)
    for e in bdry_edges:
        u, w = list(e)
        bdry_graph[u].add(w)
        bdry_graph[w].add(u)
    # Count connected components
    seen = set()
    n_components = 0
    for v in bdry_graph:
        if v in seen:
            continue
        n_components += 1
        stack = [v]
        while stack:
            x = stack.pop()
            if x in seen:
                continue
            seen.add(x)
            for y in bdry_graph[x]:
                if y not in seen:
                    stack.append(y)
    return {'is_manifold': is_manifold,
            'n_boundary': n_components,
            'pinch_vertices': pinch_vertices,
            'n_bdry_edges': len(bdry_edges),
            'n_faces': len(comp)}


def search(n_min, n_max, max_violations=5, verbose=False):
    r1_violations = []
    r2_violations = []
    summary = defaultdict(int)

    for n in range(n_min, n_max + 1):
        ntri = 0
        for G in graphs.triangulations(n):
            ntri += 1
            G.is_planar(set_embedding=True)
            emb = G.get_embedding()
            faces = G.faces(embedding=emb)
            for v_source in G.vertices():
                dist = G.shortest_path_lengths(v_source)
                face_depths = []
                for face in faces:
                    verts = set()
                    for u, w in face:
                        verts.add(u); verts.add(w)
                    d = min(dist[u] for u in verts)
                    face_depths.append((face, d, verts))
                max_d = max(fd[1] for fd in face_depths)
                for target_d in range(max_d + 1):
                    comps = find_components(face_depths, target_d)
                    for comp_idx, comp in enumerate(comps):
                        result = check_component(comp, G, emb)
                        summary[('component', target_d)] += 1
                        if not result['is_manifold']:
                            summary[('r1_violation', target_d)] += 1
                            if len(r1_violations) < max_violations:
                                r1_violations.append({
                                    'n': n, 'source': int(v_source),
                                    'depth': target_d,
                                    'graph_edges': sorted([sorted(e) for e in G.edges(labels=False)]),
                                    'pinch_vertices': result['pinch_vertices'],
                                })
                        if result['is_manifold'] and result['n_boundary'] > 2:
                            summary[('r2_violation_manifold', target_d)] += 1
                            if len(r2_violations) < max_violations:
                                r2_violations.append({
                                    'n': n, 'source': int(v_source),
                                    'depth': target_d,
                                    'n_boundary': result['n_boundary'],
                                    'graph_edges': sorted([sorted(e) for e in G.edges(labels=False)]),
                                })
        if verbose:
            print(f"n={n}: scanned {ntri} triangulations")
            sys.stdout.flush()

    return r1_violations, r2_violations, summary


def main():
    n_min = int(sys.argv[1]) if len(sys.argv) > 1 else 7
    n_max = int(sys.argv[2]) if len(sys.argv) > 2 else 12
    print(f"Searching maximal planar graphs n in [{n_min}, {n_max}]...")
    r1s, r2s, summary = search(n_min, n_max, max_violations=5, verbose=True)
    print()
    print("=== Summary ===")
    by_depth = defaultdict(lambda: {'components': 0, 'r1': 0, 'r2_manifold': 0})
    for (kind, d), c in summary.items():
        if kind == 'component':
            by_depth[d]['components'] += c
        elif kind == 'r1_violation':
            by_depth[d]['r1'] += c
        elif kind == 'r2_violation_manifold':
            by_depth[d]['r2_manifold'] += c
    print(f"{'depth':>6} {'components':>12} {'R1 viol':>10} {'R2 viol (manifold)':>20}")
    for d in sorted(by_depth):
        s = by_depth[d]
        print(f"{d:>6} {s['components']:>12} {s['r1']:>10} {s['r2_manifold']:>20}")

    print()
    if r1s:
        print(f"=== R1 violations (first {len(r1s)}) ===")
        for v in r1s:
            print(f"  n={v['n']}, source={v['source']}, depth={v['depth']}, "
                  f"pinches={v['pinch_vertices']}")
    if r2s:
        print(f"=== R2 violations (manifold, n_bdry > 2) (first {len(r2s)}) ===")
        for v in r2s:
            print(f"  n={v['n']}, source={v['source']}, depth={v['depth']}, "
                  f"n_bdry={v['n_boundary']}")
            print(f"    edges: {v['graph_edges']}")
    if not r1s and not r2s:
        print("No R1 or R2 violations found in this range.")


if __name__ == '__main__':
    main()
