"""Hunt for a structural invariant of E/O-switch orbits that separates
'derived from an ELG' from 'not derived'.

Strategy: at n=9, T*_9 is NOT a derived level graph (it is intertwining-
only). Compare its E/O orbit (under a fixed labelling) against the orbit
of a graph that IS derived. Look for an invariant constant on orbits
that differs between the two.
"""
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
from triangulation_gen import enumerate_all_triangulations
from test_conjecture import canonical_sig, bfs_levels, is_even_level_graph


def eo_switch_neighbors(G, labels):
    """Yield all triangulations reachable from G by one E/O switch
    (flip a same-parity edge). Directed: this is the FORWARD relation."""
    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
        f1 = emb.traverse_face(u, v)
        f2 = emb.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
        Gp = G.copy()
        Gp.remove_edge(u, v)
        Gp.add_edge(w, x)
        yield Gp


def undirected_orbit(G_start, labels, max_states=200000):
    """Connected component of G_start in the UNDIRECTED switch graph
    (treat each switch as bidirectional by also exploring forward from
    every reached state -- since a forward switch's reverse is itself a
    forward switch from the target when the new edge is same-parity, and
    we explore all forward edges from every node, the BFS closure equals
    the weakly-connected component)."""
    sig0 = frozenset(frozenset(e) for e in G_start.edges())
    seen = {sig0: G_start}
    frontier = [G_start]
    while frontier and len(seen) < max_states:
        new = []
        for H in frontier:
            for Hp in eo_switch_neighbors(H, labels):
                sig = frozenset(frozenset(e) for e in Hp.edges())
                if sig not in seen:
                    seen[sig] = Hp
                    new.append(Hp)
        frontier = new
    return list(seen.values())


def parity_subgraph_edge_counts(G, labels):
    even = [v for v in G.nodes() if labels[v] % 2 == 0]
    odd = [v for v in G.nodes() if labels[v] % 2 == 1]
    e_even = G.subgraph(even).number_of_edges()
    e_odd = G.subgraph(odd).number_of_edges()
    cross = G.number_of_edges() - e_even - e_odd
    return e_even, e_odd, cross


def orbit_report(G, labels, name):
    orbit = undirected_orbit(G, labels)
    has_elg = any(
        any(is_even_level_graph(H, frozenset({s}))[0] for s in H.nodes())
        for H in orbit
    )
    # Invariant candidates over the orbit
    ec = set()
    for H in orbit:
        ec.add(parity_subgraph_edge_counts(H, labels))
    print(f'{name}: orbit size {len(orbit)}, contains ELG: {has_elg}')
    print(f'  (e_even, e_odd, cross) values in orbit: {sorted(ec)}')
    return orbit, has_elg


def main():
    tris = enumerate_all_triangulations(9)
    # T*_9 is the iso class that is NOT derived (intertwining-only).
    # We earlier identified it as iso index 49 via canonical_sig matching
    # the degree sequence (5,5,5,5,5,5,4,4,4).
    Tstar = None
    for G in tris:
        if sorted([G.degree(v) for v in G.nodes()], reverse=True) == \
           [5, 5, 5, 5, 5, 4, 4, 4, 4]:
            pass
    for G in tris:
        ds = sorted([G.degree(v) for v in G.nodes()], reverse=True)
        if ds == [5, 5, 5, 5, 5, 5, 4, 4, 4]:
            Tstar = G
            break
    print(f'T*_9 degree seq: '
          f'{sorted([Tstar.degree(v) for v in Tstar.nodes()], reverse=True)}')

    # Pick a labelling of T*_9: a valid parity partition (bipartite parity
    # subgraphs). Use one we know: V_E={0,1,3,6}? But labels here come from
    # the enumerate ordering. Instead, search labellings = BFS parities.
    print('\n--- T*_9 (NOT derived) orbits under BFS-source labellings ---')
    for s in list(Tstar.nodes()):
        labels = bfs_levels(Tstar, frozenset({s}))
        orbit, has_elg = orbit_report(Tstar, labels, f'T*_9 src={s}')
        if has_elg:
            print('  ^ unexpectedly found ELG')

    # A derived graph: pick one that is an ELG itself.
    print('\n--- A derived graph (an ELG) for comparison ---')
    for G in tris:
        elg_src = next((s for s in G.nodes()
                        if is_even_level_graph(G, frozenset({s}))[0]), None)
        if elg_src is not None:
            labels = bfs_levels(G, frozenset({elg_src}))
            orbit_report(G, labels, f'derived(ELG) src={elg_src}')
            break


if __name__ == '__main__':
    main()
