# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

# src/pdftl/utils/destinations.py

"""PDF destination resolution and parsing utilities."""

import logging
from collections.abc import Iterable
from typing import Any, NamedTuple, cast

logger = logging.getLogger(__name__)


class ResolvedDest(NamedTuple):
    page_num: int
    dest_type: str
    args: list[Any]


def get_page_map(pdf_pages) -> dict[tuple[int, int], int]:
    """
    Creates a O(1) lookup table for page objects.
    Maps (obj, gen) -> 1-based page index.
    """
    return {page.objgen: i + 1 for i, page in enumerate(pdf_pages)}


def get_named_destinations(pdf):
    from pikepdf import Dictionary, NameTree

    dests = {}

    # 1. PDF 1.1 Legacy /Root/Dests (Dictionary)
    # ISO 32000-2 §12.3.2.4
    if "/Dests" in pdf.Root and isinstance(pdf.Root.Dests, Dictionary):
        for k, v in pdf.Root.Dests.items():
            dest_str = str(k).lstrip("/")
            dests[dest_str] = v

    # 2. PDF 1.2+ /Root/Names/Dests (NameTree)
    if "/Names" in pdf.Root and "/Dests" in pdf.Root.Names:
        name_tree_dests = {str(k): v for k, v in NameTree(pdf.Root.Names.Dests).items()}
        dests.update(name_tree_dests)

    return dests if dests else None


def _find_page_index(page_obj, page_map) -> int | None:
    """Helper to find 1-based index of a page object."""
    if not hasattr(page_obj, "objgen"):
        return None
    if isinstance(page_map, dict):
        return page_map.get(page_obj.objgen)
    # SLOW PATH (compatibility). FIXME: eliminate this by upgrading calling sites
    for i, page in enumerate(page_map):
        if hasattr(page, "objgen") and page.objgen == page_obj.objgen:
            return i + 1

    return None


def _dest_from_outline_item(dest):
    # OutlineItem.destination handles the primary lookup;
    # fallback to action if None
    orig_item = dest
    dest = orig_item.destination
    if dest is None and hasattr(orig_item, "action"):
        action = orig_item.action
        if action is not None and hasattr(action, "D"):
            dest = action.D
    return dest


def _resolve_named_dest_to_array(dest, named_destinations):
    from pikepdf import Array, Dictionary

    dest_str = str(dest).lstrip("/")
    dest_obj = named_destinations.get(dest_str) if named_destinations else None

    if isinstance(dest_obj, Dictionary) and "/D" in dest_obj:
        return dest_obj["/D"]

    if isinstance(dest_obj, Array):
        return dest_obj

    return None


def resolve_dest_to_page_num(
    dest: Any, pdf_pages: Any, named_destinations: Any
) -> ResolvedDest | None:
    """
    Generalized resolver: Takes an OutlineItem, Array, Name, String, or Dict
    and returns a ResolvedDest(page_num, dest_type, args).
    """
    from pikepdf import Array, Dictionary, Name, OutlineItem, String

    # 1. Extract destination from OutlineItem or Action Dictionary
    if isinstance(dest, OutlineItem):
        dest = _dest_from_outline_item(dest)

    if isinstance(dest, Dictionary) and "/D" in dest:
        dest = dest["/D"]

    # 2. Resolve Named Destinations (String/Name -> Array)
    if isinstance(dest, (String, Name, str)):
        dest = _resolve_named_dest_to_array(dest, named_destinations)

    # 3. Extract Data from [page_obj /Type ...]
    if isinstance(dest, Array) and len(dest) > 0:
        return _resolve_dest_array_to_page_num(dest, pdf_pages)

    return None


def _resolve_dest_array_to_page_num(dest, pdf_pages):
    from pikepdf import Dictionary

    page_obj = dest[0]

    # ISO 32000-2 §12.3.2.3: Structure destinations
    if isinstance(page_obj, Dictionary) and page_obj.get("/Type") == "/StructElem":
        page_obj = _find_page_for_struct_elem(page_obj)
        if not page_obj:
            # Spec: fallback to the first page in the document if no content identified
            return _resolve_struct_elem_fallback(dest)

    if not hasattr(page_obj, "objgen"):
        return None

    # use dictionary if available (fast)
    if isinstance(pdf_pages, dict):
        page_num = pdf_pages.get(page_obj.objgen)
    else:
        # This ensures your tests and other modules still work
        # but warns us that we're using the slow path
        page_num = _find_page_index(page_obj, pdf_pages)

    if page_num:
        dest_type = str(dest[1]).lstrip("/") if len(dest) > 1 else "XYZ"
        dest_args = list(cast(Iterable[Any], dest))[2:] if len(dest) > 2 else []
        return ResolvedDest(page_num, dest_type, dest_args)

    return None


def _resolve_struct_elem_fallback(dest):
    dest_type = str(dest[1]).lstrip("/") if len(dest) > 1 else "XYZ"
    dest_args = list(cast(Iterable[Any], dest))[2:] if len(dest) > 2 else []
    return ResolvedDest(1, dest_type, dest_args)


def _find_page_for_struct_elem(elem):
    """Recursively search a StructElem for a /Pg (Page) reference per §12.3.2.3."""
    visited = set()
    return _walk_struct_from(elem, visited)


def _walk_struct_from(el, visited):
    from pikepdf import Dictionary

    if not isinstance(el, Dictionary):
        return None
    if hasattr(el, "objgen"):
        if el.objgen in visited:
            return None
        visited.add(el.objgen)

    if "/Pg" in el:
        return el["/Pg"]

    if "/K" in el:
        return _walk_struct_kids_for(el, visited)

    return None


def _walk_struct_kids_for(el, visited):
    from pikepdf import Array

    k = el["/K"]
    if isinstance(k, Array):
        for kid in k:
            res = _walk_struct_from(kid, visited)
            if res:
                return res
    else:
        return _walk_struct_from(k, visited)
    return None
