"""Plane depth sequencing on maximal planar graphs."""
import os, sys
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")))  # repo root for `lib`
import random
from typing import Any, TypedDict
from sage.all import Graph, graphs  # type: ignore[attr-defined]  # pylint: disable=no-name-in-module
from lib.colored_graphs import canonize_and_save_graph


class DeeplyEmbeddedGraph(TypedDict):
    graph: Graph
    outer_cycle: list[Any]
    plane_depth_labelling: dict[Any, int]
    deep_embedding: Graph


class QuadrilateralSequence(TypedDict):
    deep_embedding: Graph
    triangular_faces: list[frozenset[Any]]
    depth_labelling: dict[Any, int]
    outer_cap_vertex: Any
    quadrilaterals: list[frozenset[frozenset[Any]]]
    sequence: list[frozenset[frozenset[Any]]]
    move_codes: list[int]


def get_plane_depth_labelling(g: Graph, outer_cycle: list[Any]) -> dict[Any, int]:
    """Return the plane depth of each vertex relative to the given outer cycle."""
    # equivalent to the commented out naive implementation:
    # return {v: min(g.distance(v, u) for u in outer_cycle) for v in g.vertices()}
    source = max(g.vertices()) + 1
    g_prime = g.copy()
    g_prime.add_vertex(source)
    g_prime.add_edges([(source, v) for v in outer_cycle])
    distances = g_prime.breadth_first_search(source, report_distance=True)
    return {v: d - 1 for v, d in distances if v != source}


def deep_embedding(g: Graph, outer_cycle: list[Any], plane_depth_labelling: dict[Any, int] | None = None) -> Graph:
    """
    Return the deep embedding of g relative to outer_cycle.

    For every interior triangular face whose three vertices all have the same
    plane depth, a new vertex is added adjacent to each vertex of that face.
    """
    if plane_depth_labelling is None:
        plane_depth_labelling = get_plane_depth_labelling(g, outer_cycle)
    outer_vertices = set(outer_cycle)

    embedding = g.get_embedding()
    if embedding is None:
        g.is_planar(set_embedding=True)
        embedding = g.get_embedding()

    g_prime = g.copy()
    new_vertex = max(g.vertices()) + 1

    for face in g.faces(embedding):
        face_vertices = [u for u, v in face]
        if set(face_vertices) == outer_vertices:
            continue
        if plane_depth_labelling[face_vertices[0]] == plane_depth_labelling[face_vertices[1]] == plane_depth_labelling[face_vertices[2]]:
            g_prime.add_vertex(new_vertex)
            g_prime.add_edges([(new_vertex, v) for v in face_vertices])
            new_vertex += 1

    return g_prime


def _triangle_type(face: frozenset[Any], depth_labelling: dict[Any, int]) -> str:
    """Return 'up' or 'down' for a triangular face (up: {d, d+1, d+1}; down: {d, d, d+1})."""
    depths = sorted(depth_labelling[v] for v in face)
    if depths[0] == depths[1]:
        return 'down'
    return 'up'


def _level_edge_of_face(face: frozenset[Any], depth_labelling: dict[Any, int]) -> frozenset[Any]:
    """Return the unique level edge of an up/down triangular face."""
    vs = list(face)
    for i in range(3):
        for j in range(i + 1, 3):
            if depth_labelling[vs[i]] == depth_labelling[vs[j]]:
                return frozenset([vs[i], vs[j]])
    raise ValueError(f"Face {set(face)} has no level edge (not up or down)")


def _quad_vertices(quad: frozenset[frozenset[Any]]) -> frozenset[Any]:
    """Return the 4 vertices of a quadrilateral."""
    f1, f2 = list(quad)
    return f1 | f2


def _quad_perimeter_edges(
    quad: frozenset[frozenset[Any]],
    depth_labelling: dict[Any, int],
) -> list[frozenset[Any]]:
    """Return the 4 perimeter edges (non-level) of a quadrilateral."""
    f1 = next(iter(quad))
    level_edge = _level_edge_of_face(f1, depth_labelling)
    edges: list[frozenset[Any]] = []
    for f in quad:
        vs = list(f)
        for i in range(3):
            edge = frozenset([vs[i], vs[(i + 1) % 3]])
            if edge != level_edge:
                edges.append(edge)
    return edges


def _quad_type(quad: frozenset[frozenset[Any]], depth_labelling: dict[Any, int]) -> str:
    """Return 'shallow_diamond', 'deep_diamond', or 's_quad' for a quadrilateral."""
    types = tuple(sorted(_triangle_type(f, depth_labelling) for f in quad))
    if types == ('up', 'up'):
        return 'shallow_diamond'
    if types == ('down', 'down'):
        return 'deep_diamond'
    return 's_quad'


def extended_deep_embedding(
    g: Graph,
    outer_cycle: list[Any],
    plane_depth_labelling: dict[Any, int] | None = None,
) -> tuple[Graph, list[frozenset[Any]], dict[Any, int], Any, dict[Any, list[Any]]]:
    """
    Return the extended deep embedding of g (including the outer face).

    Subdivides every neutral triangular face --- including the outer face,
    which is always neutral for a maximal planar graph --- by adding a new
    interior vertex. The vertex added inside the outer face is the
    outer-cap vertex x*.

    Returns (g_prime, faces, depth_labelling, outer_cap_vertex, embedding) where:
      - g_prime: the deep embedding graph G'
      - faces: list of triangular faces of G' (each a frozenset of 3 vertices),
        derived from Sage's planar embedding of g_prime
      - depth_labelling: depth of every vertex in G' (extends the original)
      - outer_cap_vertex: the new vertex placed inside the outer face
      - embedding: the CCW rotation system of g_prime (vertex -> ordered
        neighbors) as computed by Sage's planarity routine
    """
    if plane_depth_labelling is None:
        plane_depth_labelling = get_plane_depth_labelling(g, outer_cycle)
    outer_vertices = frozenset(outer_cycle)

    embedding_g = g.get_embedding()
    if embedding_g is None:
        g.is_planar(set_embedding=True)
        embedding_g = g.get_embedding()

    g_prime = g.copy()
    next_vertex = max(g.vertices()) + 1
    depth_labelling = dict(plane_depth_labelling)
    outer_cap_vertex: Any = None

    for face in g.faces(embedding_g):
        face_vertices = [u for u, v in face]
        a, b, c = face_vertices
        if depth_labelling[a] == depth_labelling[b] == depth_labelling[c]:
            x = next_vertex
            next_vertex += 1
            g_prime.add_vertex(x)
            g_prime.add_edges([(x, a), (x, b), (x, c)])
            if frozenset(face_vertices) == outer_vertices:
                outer_cap_vertex = x
                depth_labelling[x] = -1  # Outer-cap vertex has depth -1
            else:
                depth_labelling[x] = depth_labelling[a] + 1

    g_prime.is_planar(set_embedding=True)
    embedding = g_prime.get_embedding()
    assert embedding is not None, "g_prime must be planar after construction"
    faces = [frozenset([u for u, v in face]) for face in g_prime.faces(embedding)]

    return g_prime, faces, depth_labelling, outer_cap_vertex, embedding


def quadrilateral_decomposition(
    faces: list[frozenset[Any]],
    depth_labelling: dict[Any, int],
) -> tuple[list[frozenset[frozenset[Any]]], dict[frozenset[Any], frozenset[frozenset[Any]]]]:
    """
    Pair each face with the face on the other side of its level edge.

    Returns (quads, quad_of_face) where:
      - quads: list of {F1, F2} pairs (each a quadrilateral of the decomposition)
      - quad_of_face: dict mapping each face to its containing quad
    """
    faces_by_edge: dict[frozenset[Any], list[frozenset[Any]]] = {}
    for face in faces:
        vs = list(face)
        for i in range(3):
            edge = frozenset([vs[i], vs[(i + 1) % 3]])
            faces_by_edge.setdefault(edge, []).append(face)

    quads: list[frozenset[frozenset[Any]]] = []
    quad_of_face: dict[frozenset[Any], frozenset[frozenset[Any]]] = {}
    seen: set[frozenset[Any]] = set()
    for face in faces:
        if face in seen:
            continue
        level_edge = _level_edge_of_face(face, depth_labelling)
        adjacent = faces_by_edge[level_edge]
        assert len(adjacent) == 2, f"Level edge {set(level_edge)} has {len(adjacent)} adjacent faces"
        other = adjacent[0] if adjacent[1] == face else adjacent[1]
        quad = frozenset([face, other])
        quads.append(quad)
        quad_of_face[face] = quad
        quad_of_face[other] = quad
        seen.add(face)
        seen.add(other)

    return quads, quad_of_face


def _boundary_edges(
    slice_faces: set[frozenset[Any]],
    faces_by_edge: dict[frozenset[Any], list[frozenset[Any]]],
) -> set[frozenset[Any]]:
    """Return the set of edges that lie on the boundary of the slice."""
    boundary: set[frozenset[Any]] = set()
    for edge, fs in faces_by_edge.items():
        inside = sum(1 for f in fs if f in slice_faces)
        if inside == 1:
            boundary.add(edge)
    return boundary


def _face_of_wedge(
    embedding: dict[Any, list[Any]],
) -> dict[tuple[Any, Any, Any], frozenset[Any]]:
    """For each (v, n_a, n_b) where n_a, n_b are CCW-consecutive in v's rotation,
    return the triangular face at that wedge."""
    fow: dict[tuple[Any, Any, Any], frozenset[Any]] = {}
    for v, rotation in embedding.items():
        k = len(rotation)
        for i in range(k):
            n_a = rotation[i]
            n_b = rotation[(i + 1) % k]
            fow[(v, n_a, n_b)] = frozenset({v, n_a, n_b})
    return fow


def _find_starting_boundary_edge(
    slice_faces: set[frozenset[Any]],
    embedding: dict[Any, list[Any]],
    face_of_wedge: dict[tuple[Any, Any, Any], frozenset[Any]],
) -> tuple[Any, Any]:
    """Find a directed boundary edge (v, n_b) with the slice on its LEFT."""
    for v, rotation in embedding.items():
        k = len(rotation)
        for i in range(k):
            n_a = rotation[i]
            n_b = rotation[(i + 1) % k]
            n_c = rotation[(i + 2) % k]
            left_wedge = face_of_wedge[(v, n_a, n_b)]
            right_wedge = face_of_wedge[(v, n_b, n_c)]
            if left_wedge in slice_faces and right_wedge not in slice_faces:
                return (v, n_b)
    raise RuntimeError("No boundary edge found for slice")


def _boundary_walk(
    slice_faces: set[frozenset[Any]],
    embedding: dict[Any, list[Any]],
    face_of_wedge: dict[tuple[Any, Any, Any], frozenset[Any]],
) -> list[tuple[Any, Any]]:
    """Trace the slice's boundary CCW with the slice on the LEFT.

    Returns an ordered list of directed edges forming the closed boundary walk.
    """
    start = _find_starting_boundary_edge(slice_faces, embedding, face_of_wedge)
    walk: list[tuple[Any, Any]] = [start]
    curr_u, curr_v = start
    while True:
        rotation = embedding[curr_v]
        k = len(rotation)
        i = rotation.index(curr_u)
        next_v = None
        for j in range(1, k + 1):
            n_jm1 = rotation[(i + j - 1) % k]
            n_j = rotation[(i + j) % k]
            if face_of_wedge[(curr_v, n_jm1, n_j)] not in slice_faces:
                next_v = n_jm1
                break
        if next_v is None:
            raise RuntimeError(f"All wedges at {curr_v} are in slice; no boundary edge")
        next_edge = (curr_v, next_v)
        if next_edge == start:
            break
        walk.append(next_edge)
        curr_u, curr_v = next_edge
    return walk


def _canonicalize_walk(
    walk: list[tuple[Any, Any]],
    depth_labelling: dict[Any, int],
) -> list[tuple[Any, Any]]:
    """Rotate walk to start at the (smallest depth, smallest vertex id) position.

    This fixes the canonical 'top' of the slice for the top-to-bottom scan.
    """
    starts = [(depth_labelling[u], u) for u, _ in walk]
    canon_idx = min(range(len(walk)), key=lambda i: starts[i])
    return walk[canon_idx:] + walk[:canon_idx]


def _attachment_position(
    quad: frozenset[frozenset[Any]],
    walk: list[tuple[Any, Any]],
    depth_labelling: dict[Any, int],
) -> int:
    """Return the latest index in walk at which a perimeter edge of quad appears.

    Realizes 'bottommost attachment on the right boundary scanned top-to-bottom'.
    Returns -1 if the quad has no perimeter edge on the boundary walk.
    """
    perimeter = {frozenset(e) for e in _quad_perimeter_edges(quad, depth_labelling)}
    latest = -1
    for i, (u, v) in enumerate(walk):
        if frozenset([u, v]) in perimeter:
            latest = i
    return latest


def _boundary_deep_diamonds(
    quads: list[frozenset[frozenset[Any]]],
    outer_cycle: list[Any],
    outer_cap_vertex: Any,
) -> list[frozenset[frozenset[Any]]]:
    """Return the three boundary deep diamonds (each spans an edge of C)."""
    outer_set = set(outer_cycle)
    diamonds: list[frozenset[frozenset[Any]]] = []
    for q in quads:
        vs = _quad_vertices(q)
        if outer_cap_vertex in vs and len(vs & outer_set) == 2:
            diamonds.append(q)
    assert len(diamonds) == 3, f"Expected 3 boundary deep diamonds, got {len(diamonds)}"
    return diamonds


def _run_sequence(
    initial_quad: frozenset[frozenset[Any]],
    quads: list[frozenset[frozenset[Any]]],
    depth_labelling: dict[Any, int],
    faces_by_edge: dict[frozenset[Any], list[frozenset[Any]]],
    embedding: dict[Any, list[Any]],
    face_of_wedge: dict[tuple[Any, Any, Any], frozenset[Any]],
) -> tuple[list[frozenset[frozenset[Any]]], list[int]]:
    """Run the sequencing loop from initial_quad and return (sequence, move_codes)."""
    sequence: list[frozenset[frozenset[Any]]] = [initial_quad]
    move_codes: list[int] = []
    slice_quads: set[frozenset[frozenset[Any]]] = {initial_quad}
    slice_faces: set[frozenset[Any]] = set(initial_quad)

    while len(slice_quads) < len(quads):
        slice_v: set[Any] = set().union(*slice_faces)
        boundary = _boundary_edges(slice_faces, faces_by_edge)
        walk = _canonicalize_walk(
            _boundary_walk(slice_faces, embedding, face_of_wedge),
            depth_labelling,
        )

        anchor_drop: list[frozenset[frozenset[Any]]] = []
        level_add: list[frozenset[frozenset[Any]]] = []
        join: list[frozenset[frozenset[Any]]] = []
        ring_completion: list[frozenset[frozenset[Any]]] = []

        for q in quads:
            if q in slice_quads:
                continue
            vs = _quad_vertices(q)
            k = len(vs & slice_v)
            edges = _quad_perimeter_edges(q, depth_labelling)
            j = sum(1 for e in edges if e in boundary)
            qt = _quad_type(q, depth_labelling)

            if qt == 's_quad' and k == 2 and j == 1:
                anchor_drop.append(q)
            if k == 3 and j == 2:
                level_add.append(q)
            if qt == 'deep_diamond' and k == 2 and j == 1:
                join.append(q)
            if k == 4:
                ring_completion.append(q)

        def pick(cands: list[frozenset[frozenset[Any]]]) -> frozenset[frozenset[Any]]:
            return max(cands, key=lambda q: _attachment_position(q, walk, depth_labelling))

        if anchor_drop:
            next_quad = pick(anchor_drop)
            next_code = 0
        elif level_add:
            next_quad = pick(level_add)
            next_code = 1
        elif join:
            next_quad = pick(join)
            next_code = 2
        elif ring_completion:
            next_quad = pick(ring_completion)
            next_code = 3
        else:
            raise RuntimeError(
                f"No applicable move at step {len(sequence)}; slice has {len(slice_quads)}/{len(quads)} quads"
            )

        sequence.append(next_quad)
        move_codes.append(next_code)
        slice_quads.add(next_quad)
        slice_faces.update(next_quad)

    return sequence, move_codes


def quadrilateral_sequencing(
    g: Graph,
    outer_cycle: list[Any],
    plane_depth_labelling: dict[Any, int] | None = None,
) -> QuadrilateralSequence:
    """
    Build the quadrilateral sequence of g relative to outer_cycle.

    Constructs the extended deep embedding G' (including the outer-cap vertex
    x* in the outer face), decomposes G' into quadrilaterals via level-edge
    pairing, and produces the deterministic sequence Q_1, ..., Q_N by
    repeatedly applying the move-selection rule:

        anchor drop (0) > level add (1) > join (2) > ring completion (3).

    Within each move's candidate list, the bottommost attachment on the
    canonical boundary walk is selected (the largest index in the walk where
    a perimeter edge of the candidate appears). Among the three boundary deep
    diamonds, Q_1 is the start that produces the lexicographically smallest
    move-code string.

    Simplification: anchor drop / join detection uses (k, j) = (vertices in
    slice, perimeter edges on slice boundary) only; the orientation-specific
    'left edge = right edge' clause is not separately enforced.
    """
    g_prime, faces, depth_labelling, outer_cap_vertex, embedding = extended_deep_embedding(
        g, outer_cycle, plane_depth_labelling
    )
    quads, _ = quadrilateral_decomposition(faces, depth_labelling)

    faces_by_edge: dict[frozenset[Any], list[frozenset[Any]]] = {}
    for face in faces:
        vs = list(face)
        for i in range(3):
            edge = frozenset([vs[i], vs[(i + 1) % 3]])
            faces_by_edge.setdefault(edge, []).append(face)

    face_of_wedge = _face_of_wedge(embedding)
    candidates = _boundary_deep_diamonds(quads, outer_cycle, outer_cap_vertex)

    best_sequence: list[frozenset[frozenset[Any]]] | None = None
    best_codes: list[int] | None = None
    for q1 in candidates:
        seq, codes = _run_sequence(q1, quads, depth_labelling, faces_by_edge, embedding, face_of_wedge)
        if best_codes is None or codes < best_codes:
            best_sequence = seq
            best_codes = codes
    assert best_sequence is not None and best_codes is not None

    return QuadrilateralSequence(
        deep_embedding=g_prime,
        triangular_faces=faces,
        depth_labelling=depth_labelling,
        outer_cap_vertex=outer_cap_vertex,
        quadrilaterals=quads,
        sequence=best_sequence,
        move_codes=best_codes,
    )


def generate_example(n: int) -> DeeplyEmbeddedGraph:
    """Generate a random maximal planar graph of size n and return the triangulation, outer cycle, and deep embedding."""
    g = graphs.RandomTriangulation(n)
    g.is_planar(set_embedding=True)
    embedding = g.get_embedding()
    faces = g.faces(embedding)
    outer_cycle = [u for u, v in random.choice(faces)]
    plane_depth_labelling = get_plane_depth_labelling(g, outer_cycle)
    return DeeplyEmbeddedGraph(graph=g, outer_cycle=outer_cycle, plane_depth_labelling=plane_depth_labelling, deep_embedding=deep_embedding(g, outer_cycle, plane_depth_labelling))


if __name__ == "__main__":
    example = generate_example(10)
    result = quadrilateral_sequencing(example['graph'], example['outer_cycle'])
    canonical, graph_dir = canonize_and_save_graph(example['graph'])
    (graph_dir / "plane_depth_sequence").mkdir(parents=True, exist_ok=True)
    print(canonical)
    print(f"Number of quadrilaterals: {len(result['quadrilaterals'])}")
    print(f"Sequence length: {len(result['sequence'])}")
    print(f"Move codes: {result['move_codes']}")
