"""Test whether the dual of the Tutte graph (46-vertex 3-connected planar
cubic non-Hamiltonian) admits a tree coloring.

The Lederberg-Bosak-Barnette graph (38 vertices) is the smallest known
counterexample to Tait's conjecture but isn't directly available in
networkx; the Tutte graph (1946 original counterexample) is.
"""
import sys
import os
sys.path.insert(0, '/Users/didericis/Code/math-research/papers/'
                'level_resolutions_of_maximal_planar_graphs/experiments')
import networkx as nx
import time


def dual_triangulation(G):
    """Build the planar dual of a 3-connected planar cubic graph.

    Each face of G becomes a vertex of the dual; each edge of G between
    two faces becomes a dual edge. Cubic G ⇒ every dual face is a
    triangle ⇒ dual is a triangulation."""
    ok, emb = nx.check_planarity(G)
    assert ok
    faces = []
    seen = set()
    for u, v in G.edges():
        for src, dst in [(u, v), (v, u)]:
            face = tuple(emb.traverse_face(src, dst))
            key = frozenset(face)
            if key in seen:
                continue
            seen.add(key)
            faces.append(face)

    # The "outer" face is the one with the largest vertex set (heuristic);
    # for connectivity of the dual, we just include all faces.
    face_of_edge = {}  # edge (u, v) -> set of face indices it borders
    for i, face in enumerate(faces):
        for j in range(len(face)):
            a, b = face[j], face[(j + 1) % len(face)]
            key = frozenset((a, b))
            face_of_edge.setdefault(key, []).append(i)

    D = nx.Graph()
    D.add_nodes_from(range(len(faces)))
    for key, face_ids in face_of_edge.items():
        if len(face_ids) == 2:
            D.add_edge(face_ids[0], face_ids[1])
        # If len > 2 or 1, multi-edges or self-loops would appear; for
        # 3-connected G this shouldn't happen.
    return D, faces


def is_tree(subg):
    return nx.is_tree(subg) if subg.number_of_nodes() > 0 else True


def has_tree_property_for_some_pairing(G, coloring):
    pairings = [({0, 1}, {2, 3}), ({0, 2}, {1, 3}), ({0, 3}, {1, 2})]
    for p1, p2 in pairings:
        V1 = [v for v in G.nodes() if coloring[v] in p1]
        V2 = [v for v in G.nodes() if coloring[v] in p2]
        if is_tree(G.subgraph(V1)) and is_tree(G.subgraph(V2)):
            return True, (p1, p2)
    return False, None


def find_tree_coloring(G, time_limit=60.0):
    """Backtracking search for a 4-coloring with the tree property."""
    nodes = list(G.nodes())
    n = len(nodes)
    colors = [None] * n
    adj = {v: set(G.neighbors(v)) for v in nodes}
    idx_of = {v: i for i, v in enumerate(nodes)}
    t0 = time.time()
    visited = [0]

    def bt(i):
        if time.time() - t0 > time_limit:
            return None
        visited[0] += 1
        if i == n:
            coloring = dict(zip(nodes, colors))
            ok, pair = has_tree_property_for_some_pairing(G, coloring)
            return (coloring, pair) if ok else None
        v = nodes[i]
        forbidden = set()
        for w in adj[v]:
            if idx_of[w] < i:
                forbidden.add(colors[idx_of[w]])
        for c in range(4):
            if c in forbidden:
                continue
            colors[i] = c
            r = bt(i + 1)
            if r is not None:
                return r
            colors[i] = None
        return None

    return bt(0), visited[0]


def main():
    G = nx.tutte_graph()
    print(f'Tutte graph: {G.number_of_nodes()} vertices, '
          f'{G.number_of_edges()} edges, planar=True, cubic, 3-connected, '
          f'non-Hamiltonian (Tutte 1946).')

    D, faces = dual_triangulation(G)
    print(f'Dual: {D.number_of_nodes()} vertices, '
          f'{D.number_of_edges()} edges')
    print(f'  is_triangulation (3n-6 edges): '
          f'{D.number_of_edges() == 3 * D.number_of_nodes() - 6}')
    print(f'  degree sequence (sorted desc): '
          f'{sorted([D.degree(v) for v in D.nodes()], reverse=True)}')

    print('Searching for a tree coloring...')
    t0 = time.time()
    result, n_visited = find_tree_coloring(D, time_limit=120.0)
    elapsed = time.time() - t0
    if result is None:
        print(f'  no tree coloring found within time limit '
              f'({elapsed:.1f}s, {n_visited} states visited)')
    else:
        coloring, pair = result
        print(f'  tree coloring FOUND ({elapsed:.1f}s, {n_visited} states).')
        print(f'  pairing: {pair}')
        p1, p2 = pair
        V1 = [v for v in D.nodes() if coloring[v] in p1]
        V2 = [v for v in D.nodes() if coloring[v] in p2]
        sub1 = D.subgraph(V1)
        sub2 = D.subgraph(V2)
        print(f'  D[V1] (colors {p1}): {len(V1)} vertices, '
              f'{sub1.number_of_edges()} edges, tree={nx.is_tree(sub1)}')
        print(f'  D[V2] (colors {p2}): {len(V2)} vertices, '
              f'{sub2.number_of_edges()} edges, tree={nx.is_tree(sub2)}')


if __name__ == '__main__':
    main()
