"""
Faster triangulation enumeration using Weisfeiler-Lehman graph hash as an
isomorphism pre-filter. Iso-check is only run when WL hashes match.
"""
import networkx as nx
from collections import defaultdict


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 insert_vertex_in_face(G, face, new_v):
    Gp = G.copy(); Gp.add_node(new_v)
    for v in face: Gp.add_edge(new_v, v)
    return Gp


def edge_flips(G):
    ip, emb = nx.check_planarity(G)
    if not ip: return
    yielded = set()
    for u, v in 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(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)
        sig = frozenset(frozenset(e) for e in Gp.edges())
        if sig in yielded: continue
        yielded.add(sig)
        yield Gp


def wl_hash(G):
    return nx.weisfeiler_lehman_graph_hash(G)


class IsoBucket:
    """Maintain a set of graphs deduplicated by isomorphism, using WL hash
    as pre-filter."""
    def __init__(self):
        self.by_hash = defaultdict(list)
        self.reps = []

    def add(self, G):
        h = wl_hash(G)
        for R in self.by_hash[h]:
            if nx.is_isomorphic(G, R):
                return False
        self.by_hash[h].append(G)
        self.reps.append(G)
        return True


def enumerate_all_triangulations_fast(n, verbose=False):
    if n < 4: return []
    G = nx.complete_graph(4)
    current = [G]
    for k in range(4, n):
        bucket = IsoBucket()
        for T in current:
            _, emb = nx.check_planarity(T)
            for face in get_all_faces(emb):
                bucket.add(insert_vertex_in_face(T, face, k))
        current = list(bucket.reps)
        if verbose: print(f"  after vertex-insertion to {k+1}: {len(current)}")
    bucket = IsoBucket()
    for T in current: bucket.add(T)
    frontier = list(bucket.reps)
    rounds = 0
    while frontier:
        rounds += 1
        new_frontier = []
        for T in frontier:
            for Tp in edge_flips(T):
                if bucket.add(Tp):
                    new_frontier.append(Tp)
        if verbose:
            print(f"  flip-round {rounds}: total {len(bucket.reps)}")
        frontier = new_frontier
    return bucket.reps


if __name__ == "__main__":
    import time
    for n in [9, 10, 11]:
        t0 = time.time()
        tris = enumerate_all_triangulations_fast(n, verbose=(n >= 11))
        print(f"n={n}: {len(tris)} triangulations ({time.time()-t0:.1f}s)")
