"""Test whether the 4 open Holton-McKay duals are bridge-derived level
graphs: reachable from an Even Level Graph by bridge switches (E/O
switches whose new same-parity edge is a bridge in its parity subgraph).

Because bridge switches keep parity subgraphs bipartite, the reachable
set is much smaller than the full E/O orbit. We search BACKWARD from the
dual: a bridge-switch predecessor of G is G' such that G' -> G is a
bridge switch. We look for an ELG (matching the orbit labelling) in the
backward bridge-orbit.
"""
import sys
import os
sys.path.insert(0, '/Users/didericis/Code/math-research/papers/'
                'level_resolutions_of_maximal_planar_graphs/experiments')
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import networkx as nx
import time
from load_holton_mckay import parse_planar_code
from tutte_dual_treecolor import dual_triangulation
from test_conjecture import bfs_levels, is_even_level_graph


def sig(G):
    return frozenset(frozenset(e) for e in G.edges())


def parity_subgraph(G, labels, parity):
    nodes = [v for v in G.nodes() if labels[v] % 2 == parity]
    return G.subgraph(nodes)


def edge_is_bridge_in_parity(H, labels, a, b):
    """Given triangulation H that CONTAINS edge ab, with a,b same parity:
    is ab a bridge of its parity subgraph (i.e., not on any cycle)?
    For cross-parity ab, returns True (enters no parity subgraph)."""
    if labels[a] % 2 != labels[b] % 2:
        return True
    sub = parity_subgraph(H, labels, labels[a] % 2).copy()
    sub.remove_edge(a, b)
    return not nx.has_path(sub, a, b)


def forward_bridge_neighbors(G, labels):
    """Forward bridge switches from G."""
    ok, emb = nx.check_planarity(G)
    if not ok:
        return
    for u, v in list(G.edges()):
        if labels[u] % 2 != labels[v] % 2:
            continue  # only flip same-parity edges
        f1 = emb.traverse_face(u, v)
        f2 = emb.traverse_face(v, u)
        if len(f1) != 3 or len(f2) != 3:
            continue
        w = next(a for a in f1 if a != u and a != v)
        x = next(b for b in f2 if b != u and b != v)
        if w == x or G.has_edge(w, x):
            continue
        Gp = G.copy(); Gp.remove_edge(u, v); Gp.add_edge(w, x)
        # bridge condition checked on the post-switch state Gp:
        if not edge_is_bridge_in_parity(Gp, labels, w, x):
            continue
        yield Gp


def backward_bridge_neighbors(G, labels):
    """States G' with G' -> G a bridge switch. G' = G - uv + wx where uv
    is an edge of G, wx its diagonal, wx same-parity (it was the flipped
    edge in G'), and in G' the edge uv (the *new* edge of the switch
    G'->G) must be a bridge in G'-minus-uv's parity subgraph...

    We must reconstruct: G' has wx (same-parity), flipping wx gives uv.
    For G'->G to be a *bridge* switch, the NEW edge uv (added to G when
    going G'->G) must be a bridge in G's parity subgraph if uv is
    same-parity. So check the bridge condition on uv in (G without uv)."""
    ok, emb = nx.check_planarity(G)
    if not ok:
        return
    for u, v in list(G.edges()):
        f1 = emb.traverse_face(u, v)
        f2 = emb.traverse_face(v, u)
        if len(f1) != 3 or len(f2) != 3:
            continue
        w = next(a for a in f1 if a != u and a != v)
        x = next(b for b in f2 if b != u and b != v)
        if w == x or G.has_edge(w, x):
            continue
        if labels[w] % 2 != labels[x] % 2:
            continue  # wx must be same-parity to be flippable in G'
        # Switch G' -> G adds edge uv to the post-state G. Bridge switch
        # requires uv to be a bridge of its parity subgraph in G:
        if not edge_is_bridge_in_parity(G, labels, u, v):
            continue
        Gp = G.copy(); Gp.remove_edge(u, v); Gp.add_edge(w, x)
        yield Gp


def search_bridge_derived(G, labels, max_states=2_000_000, time_limit=600):
    """Backward bridge-orbit BFS; return an ELG (matching labels) if found."""
    t0 = time.time()
    seen = {sig(G)}
    frontier = [G]
    rounds = 0
    while frontier and len(seen) < max_states:
        if time.time() - t0 > time_limit:
            return None, len(seen), rounds, 'timeout'
        new = []
        for H in frontier:
            for s in H.nodes():
                ok, lvls = is_even_level_graph(H, frozenset({s}))
                if not ok:
                    continue
                same = all(lvls[u] % 2 == labels[u] % 2 for u in H.nodes())
                opp = all(lvls[u] % 2 != labels[u] % 2 for u in H.nodes())
                if same or opp:
                    return H, len(seen), rounds, 'found'
            for Hp in backward_bridge_neighbors(H, labels):
                sg = sig(Hp)
                if sg not in seen:
                    seen.add(sg); new.append(Hp)
        frontier = new
        rounds += 1
    return None, len(seen), rounds, ('exhausted' if not frontier else 'capped')


def main():
    graphs = parse_planar_code('experiments/nonham38m4.pc')
    for i in [0, 3, 4, 5]:
        G, _ = dual_triangulation(graphs[i][0])
        print(f'=== dual {i} ===')
        found = False
        for src in list(G.nodes()):
            labels = bfs_levels(G, frozenset({src}))
            result, n_states, rnds, status = search_bridge_derived(
                G, labels, max_states=500000, time_limit=120)
            tag = 'FOUND ELG' if result is not None else 'none'
            print(f'  src={src}: {tag} ({status}, {n_states} states, {rnds} rounds)')
            if result is not None:
                found = True
                break
        print(f'  dual {i}: bridge-derived = {found}')


if __name__ == '__main__':
    main()
