# this should contain classes for how different object can be rendered,
# e.g. bold, italic, regular
from PIL import ImageFont
from enum import Enum
from typing import Tuple, Optional, Dict
import os
import logging

# Set up logging for font loading
logger = logging.getLogger(__name__)

# Global cache for PIL ImageFont objects to avoid reloading fonts from disk
# Key: (font_path, font_size), Value: PIL ImageFont object
_FONT_CACHE: Dict[Tuple[Optional[str], int], ImageFont.FreeTypeFont] = {}

# Cache for bundled font path to avoid repeated filesystem lookups
_BUNDLED_FONT_PATH: Optional[str] = None

# Cache for bundled fonts directory
_BUNDLED_FONTS_DIR: Optional[str] = None


class FontWeight(Enum):
    NORMAL = "normal"
    BOLD = "bold"


class FontStyle(Enum):
    NORMAL = "normal"
    ITALIC = "italic"


class TextDecoration(Enum):
    NONE = "none"
    UNDERLINE = "underline"
    STRIKETHROUGH = "strikethrough"


class BundledFont(Enum):
    """Bundled font families available in pyWebLayout"""
    SANS = "sans"           # DejaVu Sans - modern sans-serif
    SERIF = "serif"         # DejaVu Serif - classic serif
    MONOSPACE = "monospace" # DejaVu Sans Mono - fixed-width


def get_bundled_fonts_dir():
    """
    Get the directory containing bundled fonts (cached).

    Returns:
        str: Path to the fonts directory, or None if not found
    """
    global _BUNDLED_FONTS_DIR

    # Return cached path if available
    if _BUNDLED_FONTS_DIR is not None:
        return _BUNDLED_FONTS_DIR

    # First time - determine the path and cache it
    current_dir = os.path.dirname(os.path.abspath(__file__))
    fonts_dir = os.path.join(os.path.dirname(current_dir), 'assets', 'fonts')

    if os.path.exists(fonts_dir) and os.path.isdir(fonts_dir):
        _BUNDLED_FONTS_DIR = fonts_dir
        logger.debug(f"Found bundled fonts directory at: {fonts_dir}")
        return fonts_dir
    else:
        logger.warning(f"Bundled fonts directory not found at: {fonts_dir}")
        _BUNDLED_FONTS_DIR = ""  # Empty string to indicate "checked but not found"
        return None


def get_bundled_font_path(
    family: BundledFont = BundledFont.SANS,
    weight: FontWeight = FontWeight.NORMAL,
    style: FontStyle = FontStyle.NORMAL
) -> Optional[str]:
    """
    Get the path to a specific bundled font file.

    Args:
        family: The font family (SANS, SERIF, or MONOSPACE)
        weight: The font weight (NORMAL or BOLD)
        style: The font style (NORMAL or ITALIC)

    Returns:
        str: Full path to the font file, or None if not found

    Example:
        >>> # Get bold italic sans font
        >>> path = get_bundled_font_path(BundledFont.SANS, FontWeight.BOLD, FontStyle.ITALIC)
        >>> font = Font(font_path=path, font_size=16)
    """
    fonts_dir = get_bundled_fonts_dir()
    if not fonts_dir:
        return None

    # Map font parameters to filename
    family_map = {
        BundledFont.SANS: "DejaVuSans",
        BundledFont.SERIF: "DejaVuSerif",
        BundledFont.MONOSPACE: "DejaVuSansMono"
    }

    base_name = family_map.get(family, "DejaVuSans")

    # Build the font file name
    parts = [base_name]

    if weight == FontWeight.BOLD and style == FontStyle.ITALIC:
        # Special case: both bold and italic
        if family == BundledFont.MONOSPACE:
            parts.append("BoldOblique")
        elif family == BundledFont.SERIF:
            parts.append("BoldItalic")
        else:  # SANS
            parts.append("BoldOblique")
    elif weight == FontWeight.BOLD:
        parts.append("Bold")
    elif style == FontStyle.ITALIC:
        # Italic naming differs by family
        if family == BundledFont.MONOSPACE or family == BundledFont.SANS:
            parts.append("Oblique")
        else:  # SERIF
            parts.append("Italic")

    filename = "-".join(parts) + ".ttf"
    font_path = os.path.join(fonts_dir, filename)

    if os.path.exists(font_path):
        logger.debug(f"Found bundled font: {filename}")
        return font_path
    else:
        logger.warning(f"Bundled font not found: {filename}")
        return None


class Font:
    """
    Font class to manage text rendering properties including font face, size, color, and styling.
    This class is used by the text renderer to determine how to render text.
    """

    def __init__(self,
                 font_path: Optional[str] = None,
                 font_size: int = 16,
                 colour: Tuple[int, int, int] = (0, 0, 0),
                 weight: FontWeight = FontWeight.NORMAL,
                 style: FontStyle = FontStyle.NORMAL,
                 decoration: TextDecoration = TextDecoration.NONE,
                 background: Optional[Tuple[int, int, int, int]] = None,
                 language="en_EN",
                 min_hyphenation_width: Optional[int] = None):
        """
        Initialize a Font object with the specified properties.

        Args:
            font_path: Path to the font file (.ttf, .otf). If None, uses default bundled font.
            font_size: Size of the font in points.
            colour: RGB color tuple for the text.
            weight: Font weight (normal or bold).
            style: Font style (normal or italic).
            decoration: Text decoration (none, underline, or strikethrough).
            background: RGBA background color for the text. If None, transparent background.
            language: Language code for hyphenation and text processing.
            min_hyphenation_width: Minimum width in pixels required for hyphenation to be considered.
                                 If None, defaults to 4 times the font size.
        """
        self._font_path = font_path
        self._font_size = font_size
        self._colour = colour
        self._weight = weight
        self._style = style
        self._decoration = decoration
        self._background = background if background else (255, 255, 255, 0)
        self.language = language
        self._min_hyphenation_width = min_hyphenation_width if min_hyphenation_width is not None else font_size * 4
        # Load the font file or use default
        self._load_font()

    @classmethod
    def from_family(cls,
                    family: BundledFont = BundledFont.SANS,
                    font_size: int = 16,
                    colour: Tuple[int, int, int] = (0, 0, 0),
                    weight: FontWeight = FontWeight.NORMAL,
                    style: FontStyle = FontStyle.NORMAL,
                    decoration: TextDecoration = TextDecoration.NONE,
                    background: Optional[Tuple[int, int, int, int]] = None,
                    language: str = "en_EN",
                    min_hyphenation_width: Optional[int] = None) -> 'Font':
        """
        Create a Font using a bundled font family.

        This is a convenient way to use the bundled DejaVu fonts without needing to
        specify paths manually.

        Args:
            family: The font family to use (SANS, SERIF, or MONOSPACE)
            font_size: Size of the font in points.
            colour: RGB color tuple for the text.
            weight: Font weight (normal or bold).
            style: Font style (normal or italic).
            decoration: Text decoration (none, underline, or strikethrough).
            background: RGBA background color for the text. If None, transparent background.
            language: Language code for hyphenation and text processing.
            min_hyphenation_width: Minimum width in pixels required for hyphenation.

        Returns:
            Font object configured with the bundled font

        Example:
            >>> # Create a bold serif font
            >>> font = Font.from_family(BundledFont.SERIF, font_size=18, weight=FontWeight.BOLD)
            >>>
            >>> # Create an italic monospace font
            >>> code_font = Font.from_family(BundledFont.MONOSPACE, style=FontStyle.ITALIC)
        """
        font_path = get_bundled_font_path(family, weight, style)
        return cls(
            font_path=font_path,
            font_size=font_size,
            colour=colour,
            weight=weight,
            style=style,
            decoration=decoration,
            background=background,
            language=language,
            min_hyphenation_width=min_hyphenation_width
        )

    def _get_bundled_font_path(self):
        """Get the path to the bundled font (cached)"""
        global _BUNDLED_FONT_PATH

        # Return cached path if available
        if _BUNDLED_FONT_PATH is not None:
            return _BUNDLED_FONT_PATH

        # First time - determine the path and cache it
        # Get the directory containing this module
        current_dir = os.path.dirname(os.path.abspath(__file__))
        # Navigate to the assets/fonts directory
        assets_dir = os.path.join(os.path.dirname(current_dir), 'assets', 'fonts')
        bundled_font_path = os.path.join(assets_dir, 'DejaVuSans.ttf')

        logger.debug(f"Font loading: current_dir = {current_dir}")
        logger.debug(f"Font loading: assets_dir = {assets_dir}")
        logger.debug(f"Font loading: bundled_font_path = {bundled_font_path}")
        logger.debug(
            f"Font loading: bundled font exists = {os.path.exists(bundled_font_path)}"
        )

        if os.path.exists(bundled_font_path):
            logger.info(f"Found bundled font at: {bundled_font_path}")
            _BUNDLED_FONT_PATH = bundled_font_path
            return bundled_font_path
        else:
            logger.warning(f"Bundled font not found at: {bundled_font_path}")
            # Cache None to indicate bundled font is not available
            _BUNDLED_FONT_PATH = ""  # Use empty string instead of None to differentiate from "not checked yet"
            return None

    def _load_font(self):
        """Load the font using PIL's ImageFont with consistent bundled font and caching"""
        # Determine the actual font path to use
        font_path_to_use = self._font_path
        if not font_path_to_use:
            font_path_to_use = self._get_bundled_font_path()

        # Create cache key
        cache_key = (font_path_to_use, self._font_size)

        # Check if font is already cached
        if cache_key in _FONT_CACHE:
            self._font = _FONT_CACHE[cache_key]
            logger.debug(f"Reusing cached font: {font_path_to_use} at size {self._font_size}")
            return

        # Font not cached, need to load it
        try:
            if self._font_path:
                # Use specified font path
                logger.info(f"Loading font from specified path: {self._font_path}")
                self._font = ImageFont.truetype(
                    self._font_path,
                    self._font_size
                )
                logger.info(f"Successfully loaded font from: {self._font_path}")
            else:
                # Use bundled font for consistency across environments
                bundled_font_path = self._get_bundled_font_path()

                if bundled_font_path:
                    logger.info(f"Loading bundled font from: {bundled_font_path}")
                    self._font = ImageFont.truetype(bundled_font_path, self._font_size)
                    logger.info(
                        f"Successfully loaded bundled font at size {self._font_size}"
                    )
                else:
                    # Only fall back to PIL's default font if bundled font is not
                    # available
                    logger.warning(
                        "Bundled font not available, falling back to PIL default font")
                    self._font = ImageFont.load_default()

            # Cache the loaded font
            _FONT_CACHE[cache_key] = self._font
            logger.debug(f"Cached font: {font_path_to_use} at size {self._font_size}")

        except Exception as e:
            # Ultimate fallback to default font
            logger.error(f"Failed to load font: {e}, falling back to PIL default font")
            self._font = ImageFont.load_default()
            # Don't cache the default font as it doesn't have a path

    @property
    def font(self):
        """Get the PIL ImageFont object"""
        return self._font

    @property
    def font_size(self):
        """Get the font size"""
        return self._font_size

    @property
    def colour(self):
        """Get the text color"""
        return self._colour

    @property
    def color(self):
        """Alias for colour (American spelling)"""
        return self._colour

    @property
    def background(self):
        """Get the background color"""
        return self._background

    @property
    def weight(self):
        """Get the font weight"""
        return self._weight

    @property
    def style(self):
        """Get the font style"""
        return self._style

    @property
    def decoration(self):
        """Get the text decoration"""
        return self._decoration

    @property
    def min_hyphenation_width(self):
        """Get the minimum width required for hyphenation to be considered"""
        return self._min_hyphenation_width

    def _with_modified(self, **kwargs):
        """
        Internal helper to create a new Font with modified parameters.

        This consolidates the duplication across all with_* methods.

        Args:
            **kwargs: Parameters to override (e.g., font_size=20, colour=(255,0,0))

        Returns:
            New Font object with modified parameters
        """
        params = {
            'font_path': self._font_path,
            'font_size': self._font_size,
            'colour': self._colour,
            'weight': self._weight,
            'style': self._style,
            'decoration': self._decoration,
            'background': self._background,
            'language': self.language,
            'min_hyphenation_width': self._min_hyphenation_width
        }
        params.update(kwargs)
        return Font(**params)

    def with_size(self, size: int):
        """Create a new Font object with modified size"""
        return self._with_modified(font_size=size)

    def with_colour(self, colour: Tuple[int, int, int]):
        """Create a new Font object with modified colour"""
        return self._with_modified(colour=colour)

    def with_weight(self, weight: FontWeight):
        """Create a new Font object with modified weight"""
        return self._with_modified(weight=weight)

    def with_style(self, style: FontStyle):
        """Create a new Font object with modified style"""
        return self._with_modified(style=style)

    def with_decoration(self, decoration: TextDecoration):
        """Create a new Font object with modified decoration"""
        return self._with_modified(decoration=decoration)
