"""Visualize the cycling case for the (k,k) tricky-everywhere algorithm.

Shows L_1 of triangulation index 6 at n=10, with source face (0,4,2).
Renders 4 steps of the algorithm side by side.
"""
import sys, os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import networkx as nx
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from collections import defaultdict
from triangulation_gen import enumerate_all_triangulations
from level_cycles import compute_levels
from depth_monovariant_check import (
    faces_of_subgraph, canonical_face, edges_of_face, apex_levels_for_edge,
    identify_outer_face, face_depths, is_both_k_everywhere,
    tricky_odd_faces, lowest_depth_neighbor_flip,
)


def layout_outerplanar(nodes_k, outer_face, G_full):
    """Place outer-face vertices on a circle in the cyclic order they appear,
    and interior vertices via a weighted average of their neighbors on the
    circle."""
    outer_list = list(outer_face)
    n = len(outer_list)
    pos = {}
    for i, v in enumerate(outer_list):
        angle = np.pi / 2 - 2 * np.pi * i / n
        pos[v] = np.array([np.cos(angle), np.sin(angle)])
    interior = [v for v in nodes_k if v not in pos]
    if interior:
        Lk = G_full.subgraph(nodes_k)
        for v in interior:
            nbrs = [u for u in Lk.neighbors(v) if u in pos]
            if nbrs:
                pos[v] = np.mean([pos[u] for u in nbrs], axis=0)
            else:
                pos[v] = np.array([0.0, 0.0])
    return pos


def draw_panel(ax, G_full, emb, levels, nodes_k, k, title,
                highlight_edge=None, new_edge=None):
    """Draw L_k with faces shaded by depth and tricky odd face highlighted."""
    faces = faces_of_subgraph(G_full, emb, nodes_k)
    outer = identify_outer_face(faces, G_full, emb, levels, k)
    depths = face_depths(faces, outer)
    tricky = tricky_odd_faces(faces, G_full, emb, levels, k, depths, outer)
    tricky_canons = {cf for cf, _, _ in tricky}

    # Find the outer face actual tuple (for layout)
    outer_face = None
    for f in faces:
        if canonical_face(f) == outer:
            outer_face = f
            break
    pos = layout_outerplanar(nodes_k, outer_face, G_full)

    # Shade faces by depth (skip outer face)
    max_depth = max(d for d in depths.values()) if depths else 1
    for f in faces:
        cf = canonical_face(f)
        if cf == outer:
            continue
        d = depths.get(cf, 0)
        if cf in tricky_canons:
            color = '#ff5555'  # red for tricky
            alpha = 0.7
        else:
            # Gradient from light yellow (depth 1) to orange (deep)
            shade = 0.3 + 0.4 * (d / max(max_depth, 1))
            color = (1.0, 1.0 - shade * 0.3, 0.7 - shade * 0.4)
            alpha = 0.5
        poly = plt.Polygon([pos[v] for v in f], color=color, alpha=alpha,
                            zorder=1)
        ax.add_patch(poly)
        # Label face center with depth
        cx, cy = np.mean([pos[v] for v in f], axis=0)
        face_len = len(f)
        parity_mark = ' (odd)' if face_len % 2 == 1 else ''
        tricky_mark = ' TRICKY' if cf in tricky_canons else ''
        ax.text(cx, cy, f"d={d}\nlen={face_len}{parity_mark}{tricky_mark}",
                fontsize=9, ha='center', va='center', zorder=3,
                fontweight='bold' if cf in tricky_canons else 'normal')

    # Draw edges of L_k
    Lk = G_full.subgraph(nodes_k)
    hl_set = frozenset(highlight_edge) if highlight_edge else None
    new_set = frozenset(new_edge) if new_edge else None
    for u, v in Lk.edges():
        eset = frozenset([u, v])
        if hl_set and eset == hl_set:
            ax.plot([pos[u][0], pos[v][0]], [pos[u][1], pos[v][1]],
                    color='red', linewidth=4.0, zorder=2.5,
                    linestyle='--')
        else:
            ax.plot([pos[u][0], pos[v][0]], [pos[u][1], pos[v][1]],
                    color='black', linewidth=1.6, zorder=2)
    # Sketch where the new edge will land (dotted green)
    if new_set:
        nu, nv = new_edge
        if nu in pos and nv in pos:
            ax.plot([pos[nu][0], pos[nv][0]], [pos[nu][1], pos[nv][1]],
                    color='green', linewidth=2.5, zorder=2.5,
                    linestyle=':')

    # Draw vertices
    for v in nodes_k:
        ax.plot(pos[v][0], pos[v][1], 'o', color='black', markersize=12,
                zorder=4)
        ax.text(pos[v][0] * 1.13, pos[v][1] * 1.13, str(v), fontsize=14,
                ha='center', va='center', color='blue', zorder=5,
                fontweight='bold')

    ax.set_title(title, fontsize=12)
    ax.set_aspect('equal')
    ax.axis('off')
    ax.set_xlim(-1.3, 1.3)
    ax.set_ylim(-1.3, 1.3)
    return tricky


def main():
    tris = enumerate_all_triangulations(10)
    G = tris[6]
    ip, emb = nx.check_planarity(G)
    source = {0, 4, 2}
    levels = compute_levels(G, source)
    by_level = defaultdict(list)
    for v, lv in levels.items():
        by_level[lv].append(v)
    nodes_k = by_level[1]
    k = 1

    fig, axes_grid = plt.subplots(2, 2, figsize=(14, 14))
    axes = axes_grid.flatten()
    fig.suptitle(
        f"Cycling case: n=10, tri[6], source face (0,4,2), $L_{k}$\n"
        f"Red = tricky-everywhere odd face (depth 2). Yellow = depth-1 odd "
        f"faces (have free edges). Algorithm flips a (k,k) edge of the red "
        f"face toward its lowest-depth neighbor each step.",
        fontsize=13
    )

    Gc = G.copy()
    emb_c = emb
    for step in range(4):
        ax = axes[step]
        # Compute flip choice first so we can render highlight before draw
        faces = faces_of_subgraph(Gc, emb_c, nodes_k)
        outer = identify_outer_face(faces, Gc, emb_c, levels, k)
        depths = face_depths(faces, outer)
        tricky_pre = tricky_odd_faces(faces, Gc, emb_c, levels, k, depths,
                                       outer)
        flip = None
        if tricky_pre:
            cf_t, face_t, d_t = max(tricky_pre, key=lambda x: x[2])
            flip = lowest_depth_neighbor_flip(face_t, Gc, emb_c, levels, k,
                                               depths, faces, outer)
        hl = None
        new_e = None
        if flip is not None:
            u, v, w, x, _ = flip
            hl = (u, v)
            new_e = (w, x)
        tricky = draw_panel(ax, Gc, emb_c, levels, nodes_k, k,
                             f"Step {step}",
                             highlight_edge=hl, new_edge=new_e)
        if not tricky:
            ax.set_title(f"Step {step}: no tricky face — done",
                          fontsize=12)
            continue
        if flip is None:
            ax.set_title(f"Step {step}: no flip available", fontsize=12)
            continue
        u, v, w, x, _ = flip
        ax.set_title(
            f"Step {step}: tricky face {face_t}\n"
            f"red-dashed: edge ({u},{v}) being flipped → "
            f"green-dotted: new edge ({w},{x})",
            fontsize=11)
        # Apply flip
        Gc.remove_edge(u, v)
        Gc.add_edge(w, x)
        ip2, emb_c = nx.check_planarity(Gc)

    plt.tight_layout(rect=[0, 0, 1, 0.93])
    out = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                        '..', 'cycling_visualization.png')
    plt.savefig(out, dpi=130, bbox_inches='tight')
    print(f"saved: {out}")


if __name__ == "__main__":
    main()
