"""Font management utilities for converting SVG to ReportLab graphics.

This module provides font mapping and registration functionality for converting
SVG fonts to ReportLab-compatible fonts. It handles font discovery, registration,
and mapping between SVG font specifications and ReportLab font names.

The module includes:
- FontMap class for managing font mappings
- Font discovery using system fontconfig
- Support for standard PDF fonts
- Automatic font file detection and registration
"""

import os
import shutil
import subprocess
import sys
from typing import Optional, Union

from reportlab.pdfbase.pdfmetrics import registerFont
from reportlab.pdfbase.ttfonts import TTFError, TTFont

STANDARD_FONT_NAMES = (
    "Times-Roman",
    "Times-Italic",
    "Times-Bold",
    "Times-BoldItalic",
    "Helvetica",
    "Helvetica-Oblique",
    "Helvetica-Bold",
    "Helvetica-BoldOblique",
    "Courier",
    "Courier-Oblique",
    "Courier-Bold",
    "Courier-BoldOblique",
    "Symbol",
    "ZapfDingbats",
)
DEFAULT_FONT_NAME = "Helvetica"
DEFAULT_FONT_WEIGHT = "normal"
DEFAULT_FONT_STYLE = "normal"
DEFAULT_FONT_SIZE = 12  # points (CSS/SVG default: 12 pt = 16 px)


class FontMap:
    """Manages mapping of SVG font names to ReportLab fonts and handles registration.

    This class provides a centralized way to map SVG font specifications (family,
    weight, style) to ReportLab-compatible font names. It supports automatic font
    discovery, registration of custom fonts, and fallback to standard PDF fonts.

    The internal font map uses normalized font names as keys for efficient lookup
    and supports both exact and approximate font matching.
    """

    def __init__(self) -> None:
        """Initialize the FontMap with an empty font registry.

        Creates an empty internal font map and registers all default font mappings
        for standard PDF fonts and common font family aliases.

        The internal font map structure:
            'internal_name': {
                'svg_family': 'family_name',
                'svg_weight': 'font_weight',
                'svg_style': 'font_style',
                'rlgFont': 'reportlab_font_name',
                'exact': True/False
            }

        Internal names are normalized for efficient lookup and follow the pattern:
        'Family-WeightStyle' (e.g., 'Arial-BoldItalic').
        """
        self._map: dict[str, dict[str, Union[str, bool, int]]] = {}
        self._family_index: dict[str, str] = {}  # lowercase family → canonical family

        self.register_default_fonts()

    @staticmethod
    def build_internal_name(
        family: str, weight: str = "normal", style: str = "normal"
    ) -> str:
        """Build normalized internal font name from family, weight, and style.

        Creates a standardized font name for internal mapping by combining the
        font family with capitalized weight and style variants. This follows
        the standard naming convention used by most font systems.

        Args:
            family: Font family name (e.g., "Arial", "Times New Roman").
            weight: Font weight ("normal", "bold", or numeric value).
            style: Font style ("normal" or "italic").

        Returns:
            Normalized font name string (e.g., "Arial-BoldItalic").

        Examples:
            >>> FontMap.build_internal_name("Arial", "bold", "italic")
            'Arial-BoldItalic'
            >>> FontMap.build_internal_name("Times", "normal", "normal")
            'Times'
        """
        result_name = family
        if weight != "normal" or style != "normal":
            result_name += "-"
        if weight != "normal":
            if isinstance(weight, int):
                result_name += f"{weight}"
            else:
                result_name += weight.lower().capitalize()
        if style != "normal":
            result_name += style.lower().capitalize()
        return result_name

    @staticmethod
    def guess_font_filename(
        basename: str,
        weight: str = "normal",
        style: str = "normal",
        extension: str = "ttf",
    ) -> str:
        """Guess font filename based on family, weight, and style parameters.

        Attempts to construct a likely font filename using common naming conventions
        for TrueType fonts. This works well for standard system fonts on Windows and
        many Unix-like systems.

        Args:
            basename: Base font family name (e.g., "arial", "times").
            weight: Font weight ("normal" or "bold").
            style: Font style ("normal" or "italic").
            extension: File extension (default "ttf").

        Returns:
            Guessed filename with appropriate weight/style suffix.

        Examples:
            >>> FontMap.guess_font_filename("arial", "bold", "italic")
            'arialbi.ttf'
            >>> FontMap.guess_font_filename("times", "normal", "normal")
            'times.ttf'
        """
        prefix = ""
        is_bold = weight.lower() == "bold"
        is_italic = style.lower() == "italic"
        if is_bold and not is_italic:
            prefix = "bd"
        elif is_bold and is_italic:
            prefix = "bi"
        elif not is_bold and is_italic:
            prefix = "i"
        filename = f"{basename}{prefix}.{extension}"
        return filename

    def use_fontconfig(
        self, font_name: str, weight: str = "normal", style: str = "normal"
    ) -> tuple[Optional[str], bool]:
        """Find and register a font using system fontconfig.

        Uses the system's fontconfig utility to locate and register fonts that
        match the given specifications. This provides access to system-installed
        fonts that aren't part of the standard PDF font set.

        Args:
            font_name: Name of the font family to search for.
            weight: Font weight specification ("normal" or "bold").
            style: Font style specification ("normal" or "italic").

        Returns:
            Tuple of (font_name, is_exact_match). Returns (None, False) if
            fontconfig is unavailable or no suitable font is found.

        Raises:
            OSError: If fontconfig command is not available on the system.

        Note:
            Fontconfig may return a default fallback font if the exact font
            is not found. The exact_match flag indicates whether the returned
            font is an exact match for the requested font.
        """
        NOT_FOUND = (None, False)
        # Searching with Fontconfig
        font_name_expr = font_name
        if weight != "normal":
            font_name_expr = f"{font_name_expr}:{weight}"
        if style != "normal":
            font_name_expr = f"{font_name_expr}:{style}"
        if font_name_expr != font_name and font_name not in self._map:
            # Ensure the "normal" version of the font is registered first
            self.use_fontconfig(font_name)
        fc_match = shutil.which("fc-match")
        if fc_match is None:
            return NOT_FOUND
        try:
            pipe = subprocess.Popen(
                [fc_match, "-s", "--format=%{file}\\n", "--", font_name_expr],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
            )
            output = pipe.communicate()[0].decode(sys.getfilesystemencoding())
        except OSError:
            return NOT_FOUND
        internal_name = FontMap.build_internal_name(font_name, weight, style)
        font_paths = output.split("\n")
        for font_path in font_paths:
            try:
                registerFont(TTFont(internal_name, font_path))
            except TTFError:
                continue
            else:
                success_font_path = font_path
                break
        else:
            return NOT_FOUND
        # Fontconfig may return a default font totally unrelated with font_name
        exact = font_name.lower() in os.path.basename(success_font_path).lower()
        self._map[internal_name] = {
            "svg_family": font_name,
            "svg_weight": weight,
            "svg_style": style,
            "rlgFont": internal_name,
            "exact": exact,
        }
        self._family_index.setdefault(font_name.lower(), font_name)
        return internal_name, exact

    def register_default_fonts(self) -> None:
        """Register mappings for standard PDF fonts and common font families.

        Sets up the default font mappings that are always available in ReportLab.
        This includes the 14 standard PDF fonts and common font family aliases
        like "serif", "sans-serif", and "monospace" that map to appropriate
        standard fonts.

        This method is called automatically during FontMap initialization and
        establishes the baseline font support for SVG to PDF conversion.
        """
        self.register_font("Times New Roman", rlgFontName="Times-Roman")
        self.register_font("Times New Roman", weight="bold", rlgFontName="Times-Bold")
        self.register_font(
            "Times New Roman", style="italic", rlgFontName="Times-Italic"
        )
        self.register_font(
            "Times New Roman",
            weight="bold",
            style="italic",
            rlgFontName="Times-BoldItalic",
        )

        self.register_font("Helvetica", rlgFontName="Helvetica")
        self.register_font("Helvetica", weight="bold", rlgFontName="Helvetica-Bold")
        self.register_font("Helvetica", style="italic", rlgFontName="Helvetica-Oblique")
        self.register_font(
            "Helvetica",
            weight="bold",
            style="italic",
            rlgFontName="Helvetica-BoldOblique",
        )

        self.register_font("Courier New", rlgFontName="Courier")
        self.register_font("Courier New", weight="bold", rlgFontName="Courier-Bold")
        self.register_font("Courier New", style="italic", rlgFontName="Courier-Oblique")
        self.register_font(
            "Courier New",
            weight="bold",
            style="italic",
            rlgFontName="Courier-BoldOblique",
        )
        self.register_font("Courier", style="italic", rlgFontName="Courier-Oblique")
        self.register_font(
            "Courier", weight="bold", style="italic", rlgFontName="Courier-BoldOblique"
        )

        self.register_font("sans-serif", rlgFontName="Helvetica")
        self.register_font("sans-serif", weight="bold", rlgFontName="Helvetica-Bold")
        self.register_font(
            "sans-serif", style="italic", rlgFontName="Helvetica-Oblique"
        )
        self.register_font(
            "sans-serif",
            weight="bold",
            style="italic",
            rlgFontName="Helvetica-BoldOblique",
        )

        self.register_font("serif", rlgFontName="Times-Roman")
        self.register_font("serif", weight="bold", rlgFontName="Times-Bold")
        self.register_font("serif", style="italic", rlgFontName="Times-Italic")
        self.register_font(
            "serif", weight="bold", style="italic", rlgFontName="Times-BoldItalic"
        )

        self.register_font("times", rlgFontName="Times-Roman")
        self.register_font("times", weight="bold", rlgFontName="Times-Bold")
        self.register_font("times", style="italic", rlgFontName="Times-Italic")
        self.register_font(
            "times", weight="bold", style="italic", rlgFontName="Times-BoldItalic"
        )

        self.register_font("monospace", rlgFontName="Courier")
        self.register_font("monospace", weight="bold", rlgFontName="Courier-Bold")
        self.register_font("monospace", style="italic", rlgFontName="Courier-Oblique")
        self.register_font(
            "monospace",
            weight="bold",
            style="italic",
            rlgFontName="Courier-BoldOblique",
        )

    def register_font_family(
        self,
        family: str,
        normal: str,
        bold: Optional[str] = None,
        italic: Optional[str] = None,
        bolditalic: Optional[str] = None,
    ) -> None:
        """Register a complete font family with all style variants.

        Convenience method to register an entire font family at once by providing
        the font paths for different style combinations. This automatically creates
        mappings for normal, bold, italic, and bold-italic variants.

        Args:
            family: Font family name (e.g., "MyCustomFont").
            normal: Path or name for the normal weight/style variant.
            bold: Optional path or name for bold variant.
            italic: Optional path or name for italic variant.
            bolditalic: Optional path or name for bold-italic variant.

        Example:
            >>> font_map.register_font_family(
            ...     "MyFont",
            ...     "/path/to/myfont-regular.ttf",
            ...     "/path/to/myfont-bold.ttf",
            ...     "/path/to/myfont-italic.ttf",
            ...     "/path/to/myfont-bolditalic.ttf"
            ... )
        """
        self.register_font(family, normal)
        if bold is not None:
            self.register_font(family, bold, weight="bold")
        if italic is not None:
            self.register_font(family, italic, style="italic")
        if bolditalic is not None:
            self.register_font(family, bolditalic, weight="bold", style="italic")

    def register_font(
        self,
        font_family: str,
        font_path: Optional[str] = None,
        weight: str = "normal",
        style: str = "normal",
        rlgFontName: Optional[str] = None,
    ) -> tuple[Optional[str], bool]:
        """Register a font or create a mapping to a ReportLab font name.

        This method handles two scenarios:
        1. Registering a custom TrueType font file with ReportLab
        2. Creating a mapping from SVG font specifications to existing ReportLab fonts

        For standard PDF fonts, only the mapping is created. For custom fonts,
        the font file is registered with ReportLab and then mapped.

        Args:
            font_family: SVG font family name (e.g., "Arial", "Times New Roman").
            font_path: Path to TrueType font file (.ttf). If None, assumes this
                is a mapping to an existing ReportLab font.
            weight: Font weight ("normal" or "bold").
            style: Font style ("normal" or "italic").
            rlgFontName: ReportLab font name to map to. If None, uses the
                normalized internal name.

        Returns:
            Tuple of (internal_font_name, success_flag). Returns (None, False)
            if registration fails.

        Raises:
            TTFError: If the font file cannot be loaded or registered.

        Examples:
            >>> # Map to existing ReportLab font
            >>> font_map.register_font("MyArial", rlgFontName="Helvetica")
            ('MyArial', True)

            >>> # Register custom font file
            >>> font_map.register_font("MyFont", "/path/to/font.ttf")
            ('MyFont', True)
        """
        NOT_FOUND = (None, False)
        internal_name = FontMap.build_internal_name(font_family, weight, style)
        if rlgFontName is None:
            # if no reportlabs font name is given, use the internal fontname to
            # register the reportlab font
            rlgFontName = internal_name

        if rlgFontName in STANDARD_FONT_NAMES:
            # mapping to one of the standard fonts, no need to register
            self._map[internal_name] = {
                "svg_family": font_family,
                "svg_weight": weight,
                "svg_style": style,
                "rlgFont": rlgFontName,
                "exact": True,
            }
            self._family_index.setdefault(font_family.lower(), font_family)
            return internal_name, True

        if internal_name not in STANDARD_FONT_NAMES and font_path is not None:
            try:
                registerFont(TTFont(rlgFontName, font_path))
                self._map[internal_name] = {
                    "svg_family": font_family,
                    "svg_weight": weight,
                    "svg_style": style,
                    "rlgFont": rlgFontName,
                    "exact": True,
                }
                self._family_index.setdefault(font_family.lower(), font_family)
                return internal_name, True
            except TTFError:
                return NOT_FOUND

        # If we reach here, no registration was possible
        return NOT_FOUND

    def find_font(
        self, font_name: str, weight: str = "normal", style: str = "normal"
    ) -> tuple[str, bool]:
        """Find the best matching ReportLab font for given specifications.

        Searches through the font registry to find the most appropriate font match.
        Uses a multi-step fallback strategy: exact match, standard fonts, file-based
        registration, fontconfig discovery, and finally default fallback.

        Args:
            font_name: SVG font family name to search for.
            weight: Font weight ("normal" or "bold").
            style: Font style ("normal" or "italic").

        Returns:
            Tuple of (reportlab_font_name, is_exact_match). The exact_match flag
            indicates whether the returned font is an exact match for the request.

        Note:
            If no suitable font is found, falls back to DEFAULT_FONT_NAME (Helvetica).
            The search prioritizes exact matches over approximate ones.
        """
        font_name = self._family_index.get(font_name.lower(), font_name)
        internal_name = FontMap.build_internal_name(font_name, weight, style)
        # Step 1 check if the font is one of the buildin standard fonts
        if internal_name in STANDARD_FONT_NAMES:
            return internal_name, True
        # Step 2 Check if font is already registered
        if internal_name in self._map:
            font_entry = self._map[internal_name]
            return (
                str(font_entry["rlgFont"]),
                bool(font_entry["exact"]),
            )
        # Step 3 Try to auto register the font
        # Try first to register the font if it exists as ttf
        guessed_filename = FontMap.guess_font_filename(font_name, weight, style)
        reg_name, exact = self.register_font(font_name, guessed_filename)
        if reg_name is not None:
            return reg_name, exact
        fontconfig_result = self.use_fontconfig(font_name, weight, style)
        if fontconfig_result[0] is not None:
            return fontconfig_result[0], fontconfig_result[1]
        # Fallback to default font if nothing found
        return DEFAULT_FONT_NAME, False


_font_map = FontMap()  # the global font map


def register_font(
    font_name: str,
    font_path: Optional[str] = None,
    weight: str = "normal",
    style: str = "normal",
    rlgFontName: Optional[str] = None,
) -> tuple[Optional[str], bool]:
    """Register a font with the global font map.

    Convenience function that delegates to the global FontMap instance.
    Registers a custom font or creates a mapping to an existing ReportLab font.

    Args:
        font_name: SVG font family name (e.g., "Arial", "Times New Roman").
        font_path: Path to TrueType font file (.ttf). Optional for mappings
            to existing fonts.
        weight: Font weight ("normal" or "bold").
        style: Font style ("normal" or "italic").
        rlgFontName: ReportLab font name to map to.

    Returns:
        Tuple of (internal_font_name, success_flag).

    See Also:
        FontMap.register_font: The underlying implementation method.
    """
    return _font_map.register_font(font_name, font_path, weight, style, rlgFontName)


def find_font(
    font_name: str, weight: str = "normal", style: str = "normal"
) -> tuple[str, bool]:
    """Find the best matching font from the global font registry.

    Convenience function that delegates to the global FontMap instance.
    Searches for fonts using a multi-step fallback strategy.

    Args:
        font_name: SVG font family name to search for.
        weight: Font weight ("normal" or "bold").
        style: Font style ("normal" or "italic").

    Returns:
        Tuple of (reportlab_font_name, is_exact_match).

    See Also:
        FontMap.find_font: The underlying implementation method.
    """
    return _font_map.find_font(font_name, weight, style)


def register_font_family(
    family: str,
    normal: str,
    bold: Optional[str] = None,
    italic: Optional[str] = None,
    bolditalic: Optional[str] = None,
) -> None:
    """Register a complete font family with the global font map.

    Convenience function that delegates to the global FontMap instance.
    Registers an entire font family with all style variants at once.

    Args:
        family: Font family name (e.g., "MyCustomFont").
        normal: Path or name for the normal weight/style variant.
        bold: Optional path or name for bold variant.
        italic: Optional path or name for italic variant.
        bolditalic: Optional path or name for bold-italic variant.

    See Also:
        FontMap.register_font_family: The underlying implementation method.
    """
    _font_map.register_font_family(family, normal, bold, italic, bolditalic)


def get_global_font_map() -> FontMap:
    """Get the global FontMap instance used by the module.

    Returns the singleton FontMap instance that manages all font registrations
    and mappings for the svglib module. This is the same instance used by all
    module-level font functions.

    Returns:
        The global FontMap instance.

    Note:
        Direct access to the FontMap allows for advanced font management
        operations not available through the convenience functions.
    """
    return _font_map
