"""
Level cycle computation for maximal planar graphs.

Definitions
-----------
- Level source: either (a) a face of G [all face vertices at level 0]
                       or (b) a degree-3 vertex of G [singleton at level 0]
- Levels: BFS distance from the level source.
- Level cycle: simple face of the level subgraph L_k (subgraph induced by
  level-k vertices) in the embedding inherited from G's planar embedding.
- Level resolution: G' obtained from G by flipping
    - exactly one edge per ODD level cycle (mandatory)
    - at most one edge per EVEN level cycle (optional)
"""
import networkx as nx
from collections import deque, defaultdict
from itertools import product as iproduct


def compute_levels(G, source_set):
    """BFS levels from any iterable of source vertices."""
    levels, queue = {}, deque()
    for v in source_set:
        levels[v] = 0
        queue.append(v)
    while queue:
        v = queue.popleft()
        for w in G.neighbors(v):
            if w not in levels:
                levels[w] = levels[v] + 1
                queue.append(w)
    return levels


def get_all_faces(emb):
    seen, faces = set(), []
    for v in emb.nodes():
        for w in emb[v]:
            if (v, w) not in seen:
                face = emb.traverse_face(v, w)
                for i in range(len(face)):
                    seen.add((face[i], face[(i+1) % len(face)]))
                faces.append(tuple(face))
    return faces


def inherited_embedding(emb_G, sub_nodes):
    """PlanarEmbedding of the induced subgraph with inherited cyclic order."""
    sub_set = set(sub_nodes)
    emb = nx.PlanarEmbedding()
    for v in sub_nodes:
        emb.add_node(v)
    for v in sub_nodes:
        cw = [w for w in emb_G.neighbors_cw_order(v) if w in sub_set]
        prev = None
        for w in cw:
            if prev is None:
                emb.add_half_edge_first(v, w)
            else:
                emb.add_half_edge_cw(v, w, prev)
            prev = w
    return emb


def _is_simple(face):
    return len(set(face)) == len(face)


def get_level_cycles_by_parity(G, emb_G, levels):
    """Simple faces of each L_k, split by length parity."""
    by_level = defaultdict(list)
    for v, lv in levels.items():
        by_level[lv].append(v)
    odd, even, seen = [], [], set()
    for k, nodes_k in by_level.items():
        if len(nodes_k) < 3:
            continue
        L_k = G.subgraph(nodes_k)
        if L_k.number_of_edges() < 3:
            continue
        try:
            emb_L = inherited_embedding(emb_G, nodes_k)
            faces = get_all_faces(emb_L)
        except Exception:
            ip, emb_L = nx.check_planarity(L_k)
            if not ip:
                continue
            faces = get_all_faces(emb_L)
        for face in faces:
            if not _is_simple(face):
                continue
            rots = [tuple(face[i:] + face[:i]) for i in range(len(face))] \
                 + [tuple(face[::-1][i:] + face[::-1][:i])
                    for i in range(len(face))]
            canon = min(rots)
            if canon in seen:
                continue
            seen.add(canon)
            (odd if len(face) % 2 else even).append(face)
    return odd, even


def get_flip_candidates(G, emb_G, cycle):
    """Valid edge flips along a level cycle."""
    cands = []
    n = len(cycle)
    for i in range(n):
        u, v = cycle[i], cycle[(i+1) % n]
        if not G.has_edge(u, v):
            continue
        f1 = emb_G.traverse_face(u, v)
        f2 = emb_G.traverse_face(v, u)
        if len(f1) != 3 or len(f2) != 3:
            continue
        w = next(x for x in f1 if x != u and x != v)
        x = next(y for y in f2 if y != u and y != v)
        if w == x or G.has_edge(w, x):
            continue
        cands.append((u, v, w, x))
    return cands


def level_sources(G, emb_G):
    """Yield every valid level source as (kind, label, vertex_set)."""
    for face in get_all_faces(emb_G):
        yield ('face', tuple(face), set(face))
    for v in G.nodes():
        if G.degree(v) == 3:
            yield ('vertex', v, {v})


def all_level_resolutions(G):
    """Yield (G', source_kind, source_label, choices) for every resolution."""
    is_planar, emb = nx.check_planarity(G)
    if not is_planar:
        return
    for kind, label, source_set in level_sources(G, emb):
        levels = compute_levels(G, source_set)
        odd_cycles, even_cycles = get_level_cycles_by_parity(G, emb, levels)
        if not odd_cycles and not even_cycles:
            yield (G.copy(), kind, label, [])
            continue
        cand_lists, ok = [], True
        for cycle in odd_cycles:
            cands = get_flip_candidates(G, emb, cycle)
            if not cands:
                ok = False
                break
            cand_lists.append(cands)
        if not ok:
            continue
        for cycle in even_cycles:
            cands = get_flip_candidates(G, emb, cycle)
            cand_lists.append(cands + [None])
        for choices in iproduct(*cand_lists):
            Gp = G.copy()
            applied, ok2 = set(), True
            for choice in choices:
                if choice is None:
                    continue
                u, v, w, x = choice
                if frozenset([u, v]) in applied:
                    ok2 = False; break
                if not Gp.has_edge(u, v) or Gp.has_edge(w, x):
                    ok2 = False; break
                Gp.remove_edge(u, v)
                Gp.add_edge(w, x)
                applied.add(frozenset([u, v]))
            if not ok2:
                continue
            ip2, _ = nx.check_planarity(Gp)
            if ip2:
                yield (Gp, kind, label, [c for c in choices if c is not None])
