"""Define and visualise a cut tire: a structure built from a face of
the depth-d subgraph of a cut half G'_i, together with adjacent edges
at depths d-1 and d+1.

Procedure for a chosen d > 0:
  1. Build H_d := subgraph of G'_i on edges of depth d.
  2. Find the faces of H_d in the inherited planar embedding.
  3. Pick one face f.
  4. The cut tire at (d, f) is the subgraph of G'_i consisting of:
     - The boundary edges of f (all depth d in H_d), AND
     - All edges of G'_i with at least one endpoint on the boundary
       walk of f whose depth is d-1 or d+1.
"""
import os
import sys
import math
from collections import deque, defaultdict

import matplotlib.pyplot as plt
import matplotlib.patches as patches

from sage.all import Graph

# Reuse the cut-and-depth-label infrastructure.
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from cut_depth_label import (
    parse_planar_code, HM_FILE, find_six_edge_cut,
    apply_procedure, compute_nice_layout,
)


def faces_of_subgraph(H, pos):
    """Find faces of H using its planar embedding inherited from pos.
    Uses Sage's planar embedding facility.  Returns list of faces, each
    a list of vertices in cyclic order on the face boundary."""
    if not H.is_planar(set_embedding=True):
        return []
    return H.faces()


def cut_tire_at(G_i, edge_depth, d):
    """Build cut tires at depth d for each face of H_d.

    Per the (redefined) Definition, each cut tire is:
      - The face boundary walk of f (edges of depth d in H_d), AND
      - For each vertex v on the boundary walk with degree-2 in H_d:
        a single pendant (fresh vertex n_v + fresh edge {v, n_v})
        labelled 'out spoke' if v has an incident depth-(d-1) edge
        in G'_i, or 'in spoke' if v has an incident depth-(d+1) edge.
    """
    H_d_edges = [e for e, ed in edge_depth.items() if ed == d]
    H_d = Graph([(u, v) for (u, v) in H_d_edges],
                multiedges=False, loops=False)
    if H_d.size() == 0:
        return []
    if not H_d.is_planar(set_embedding=True):
        return []
    faces_h_d = H_d.faces()
    results = []
    # For each vertex in H_d, compute its degree in H_d and find its
    # incident non-H_d edges in G'_i (with their depths).
    h_d_degree = {v: H_d.degree(v) for v in H_d.vertices()}
    incident_other = {v: [] for v in H_d.vertices()}
    for e, ed in edge_depth.items():
        if ed == d:
            continue
        u, w = e
        if u in incident_other:
            incident_other[u].append((w, ed))
        if w in incident_other:
            incident_other[w].append((u, ed))
    for face in faces_h_d:
        vertices_on_face = [u for (u, v) in face]
        face_vertex_list = list(dict.fromkeys(vertices_on_face))  # ordered, unique
        face_edges = [(min(u, v), max(u, v)) for (u, v) in face]
        # For each degree-2 boundary vertex, add a labelled pendant
        out_spokes = []   # list of (boundary_v, pendant_v)
        in_spokes = []    # list of (boundary_v, pendant_v)
        pendant_to_boundary = {}
        for v in face_vertex_list:
            if h_d_degree[v] != 2:
                continue
            # In a cubic G'_i, v has exactly one non-H_d edge.
            others = incident_other.get(v, [])
            if not others:
                continue
            # Look at the depth of the unique non-H_d incident edge.
            for (w, ed) in others:
                if ed == d - 1:
                    pendant_id = ('out', v)
                    pendant_to_boundary[pendant_id] = v
                    out_spokes.append((v, pendant_id))
                elif ed == d + 1:
                    pendant_id = ('in', v)
                    pendant_to_boundary[pendant_id] = v
                    in_spokes.append((v, pendant_id))
        results.append({
            'face_vertices_unique': face_vertex_list,
            'face_edges': face_edges,
            'out_spokes': out_spokes,  # (boundary_v, pendant_id) for d-1
            'in_spokes': in_spokes,    # (boundary_v, pendant_id) for d+1
            'face_length': len(face),
        })
    return results


def draw_cut_tire(ax, G_i, pos, edge_depth, tire, d, title):
    """Draw G_i faded with a single cut tire highlighted.

    The cut tire (under the redefinition) is:
      - face boundary edges (depth d in H_d), and
      - pendant edges from degree-2 boundary vertices: 'out spokes'
        for those incident to a depth-(d-1) edge in G'_i, 'in spokes'
        for those incident to a depth-(d+1) edge.
    """
    face_set = set(tire['face_edges'])
    face_vertex_set = set(tire['face_vertices_unique'])
    out_spokes = tire['out_spokes']
    in_spokes = tire['in_spokes']

    # Fade all G_i edges
    for e in G_i.edges(labels=False):
        u, v = e
        e_norm = (min(u, v), max(u, v))
        if e_norm in face_set:
            continue
        x1, y1 = pos[u]; x2, y2 = pos[v]
        ax.plot([x1, x2], [y1, y2], color='#dddddd', linewidth=0.9,
                zorder=1)

    # Highlight the face boundary
    for e in face_set:
        u, v = e
        x1, y1 = pos[u]; x2, y2 = pos[v]
        ax.plot([x1, x2], [y1, y2], color='#1f77b4', linewidth=3.0,
                zorder=3)

    # Compute pendant positions: each pendant vertex sits offset from
    # its boundary vertex in the direction of its parent-graph
    # incident edge (so the pendant visually replaces that edge).
    incident_other = {v: [] for v in G_i.vertices()}
    for e, ed in edge_depth.items():
        if ed == d:
            continue
        u, w = e
        if u in incident_other:
            incident_other[u].append((w, ed))
        if w in incident_other:
            incident_other[w].append((u, ed))

    pendant_pos = {}
    OFFSET = 0.20
    for (v, pid) in out_spokes + in_spokes:
        # Direction: toward the parent-graph neighbor at depth d-1 or d+1
        target_depth = d - 1 if pid[0] == 'out' else d + 1
        target = None
        for (w, ed) in incident_other.get(v, []):
            if ed == target_depth:
                target = w
                break
        if target is not None and target in pos:
            tx, ty = pos[target]; vx, vy = pos[v]
            dx, dy = tx - vx, ty - vy
            norm = (dx*dx + dy*dy) ** 0.5 or 1.0
            pendant_pos[pid] = (vx + OFFSET * dx / norm,
                                vy + OFFSET * dy / norm)
        else:
            vx, vy = pos[v]
            pendant_pos[pid] = (vx + OFFSET, vy)

    # Draw spokes
    for (v, pid) in out_spokes:
        x1, y1 = pos[v]; x2, y2 = pendant_pos[pid]
        ax.plot([x1, x2], [y1, y2], color='#d62728', linewidth=2.4,
                linestyle='--', zorder=2)
    for (v, pid) in in_spokes:
        x1, y1 = pos[v]; x2, y2 = pendant_pos[pid]
        ax.plot([x1, x2], [y1, y2], color='#2ca02c', linewidth=2.4,
                linestyle='--', zorder=2)

    # Draw vertices: face vertices highlighted, others faded
    for v in G_i.vertices():
        x, y = pos[v]
        if v in face_vertex_set:
            ax.plot(x, y, 'o', color='#1f77b4', markersize=10,
                    zorder=4, markeredgecolor='#0a3b5f',
                    markeredgewidth=1.0)
        else:
            ax.plot(x, y, 'o', color='#bbbbbb', markersize=5, zorder=2)
    # Draw pendant vertices
    for (v, pid) in out_spokes:
        px, py = pendant_pos[pid]
        ax.plot(px, py, 's', color='#ffaa66', markersize=8, zorder=4,
                markeredgecolor='#aa5500')
    for (v, pid) in in_spokes:
        px, py = pendant_pos[pid]
        ax.plot(px, py, 's', color='#a0e0a0', markersize=8, zorder=4,
                markeredgecolor='#2a7a2a')

    legend = [
        plt.Line2D([], [], color='#1f77b4', linewidth=3,
                   label=f'face boundary (depth {d})'),
        plt.Line2D([], [], color='#d62728', linewidth=2.4,
                   linestyle='--',
                   label=f'out spokes (toward depth {d - 1})'),
        plt.Line2D([], [], color='#2ca02c', linewidth=2.4,
                   linestyle='--',
                   label=f'in spokes (toward depth {d + 1})'),
    ]
    ax.legend(handles=legend, loc='upper left',
              bbox_to_anchor=(1.02, 1.0), fontsize=9, frameon=False)
    ax.set_aspect('equal'); ax.axis('off')
    ax.set_title(title, fontsize=10)


def main():
    out_fig = os.path.normpath(os.path.join(HERE, '..', 'notes',
                                            'fig_cut_tire.png'))
    gs = parse_planar_code(HM_FILE)
    G = gs[0]
    S, cut = find_six_edge_cut(G)
    base_pos = compute_nice_layout(G)
    S0 = frozenset(S)
    S1 = frozenset(G.vertices()) - S0
    H0, pos0, ed0, _, _, _ = apply_procedure(
        G, S0, cut, base_pos, '0',
        pendant_start_id=max(G.vertices()) + 1)
    H1, pos1, ed1, _, _, _ = apply_procedure(
        G, S1, cut, base_pos, '1',
        pendant_start_id=max(G.vertices()) + 1 + 6)

    # Try several depths in G'_1 (which has depths 0-7) and pick a
    # decent example.
    print(f"G'_1 depths range: 0 to {max(ed1.values())}")
    for d in range(1, max(ed1.values())):
        tires = cut_tire_at(H1, ed1, d)
        print(f'  depth {d}: {len(tires)} faces in H_d')
        for t in tires:
            print(f'    face length {t["face_length"]}, '
                  f'out spokes: {len(t["out_spokes"])}, '
                  f'in spokes: {len(t["in_spokes"])}')

    # Collect "best" cut tires at multiple depths
    selected = []
    for d in range(1, max(ed1.values())):
        tires = cut_tire_at(H1, ed1, d)
        candidates = [t for t in tires
                      if t['face_length'] >= 4
                      and (t['out_spokes'] or t['in_spokes'])]
        if candidates:
            candidates.sort(key=lambda t: -(len(t['out_spokes']) +
                                            len(t['in_spokes'])))
            selected.append((d, candidates[0]))
    print(f"\nCut tires selected for visualization at depths: "
          f"{[d for d, _ in selected]}")

    # Multi-panel figure
    n = len(selected)
    cols = 2
    rows = (n + cols - 1) // cols
    xs = [p[0] for p in base_pos.values()]
    ys = [p[1] for p in base_pos.values()]
    fig, axes = plt.subplots(rows, cols, figsize=(13, 5 * rows))
    if rows == 1:
        axes = [axes]
    axes_flat = [ax for row in axes for ax in (row if hasattr(row, '__iter__') else [row])]
    for idx, (d, tire) in enumerate(selected):
        ax = axes_flat[idx]
        draw_cut_tire(ax, H1, pos1, ed1, tire, d,
                      title=(f"$d = {d}$:  face length "
                             f"{tire['face_length']}, "
                             f"{len(tire['out_spokes'])} out + "
                             f"{len(tire['in_spokes'])} in spokes"))
        ax.set_xlim(min(xs) - 0.5, max(xs) + 0.5)
        ax.set_ylim(min(ys) - 0.5, max(ys) + 0.5)
    for idx in range(n, len(axes_flat)):
        axes_flat[idx].axis('off')
    fig.suptitle(r"Cut tires on $G'_1$ (Holton-McKay #0, V\S half) at "
                 r"several depths $d$" + "\n"
                 r"Blue = face boundary at depth $d$ (in $H_d$).  "
                 r"Orange-square pendants = out spokes (toward depth $d-1$).  "
                 r"Green-square pendants = in spokes (toward depth $d+1$)." + "\n"
                 r"Each spoke is a labelled pendant added at a "
                 r"degree-$2$ boundary vertex.",
                 fontsize=10, y=1.00)
    plt.tight_layout()
    plt.savefig(out_fig, dpi=160, bbox_inches='tight')
    plt.close()
    print(f'Wrote {out_fig}')


if __name__ == '__main__':
    main()
