#!/usr/bin/env python3
"""
DPI and Scaling Utilities for Windows Tkinter Applications

This module provides utilities to handle Windows DPI scaling properly,
ensuring that Tkinter GUIs look crisp and correctly sized on high-DPI displays.
"""

import sys
import platform
from typing import Tuple, Optional
import tkinter as tk

# Windows-specific constants
if platform.system() == "Windows":
    try:
        import ctypes
        from ctypes import wintypes
        
        # DPI Awareness constants
        PROCESS_DPI_UNAWARE = 0
        PROCESS_SYSTEM_DPI_AWARE = 1
        PROCESS_PER_MONITOR_DPI_AWARE = 2
        
        # Get DPI awareness functions
        shcore = ctypes.windll.shcore if hasattr(ctypes.windll, 'shcore') else None
        user32 = ctypes.windll.user32
    except (ImportError, AttributeError):
        shcore = None
        user32 = None
else:
    shcore = None
    user32 = None


def set_dpi_awareness() -> bool:
    """
    Set DPI awareness for the application on Windows.
    
    This should be called BEFORE creating any Tkinter root windows.
    
    Returns:
        True if DPI awareness was set successfully, False otherwise
    """
    if platform.system() != "Windows":
        return False
    
    if shcore is None or user32 is None:
        return False
    
    try:
        # Try to set Per-Monitor DPI awareness (Windows 8.1+)
        # This allows the app to scale correctly on each monitor independently
        result = shcore.SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE)
        return result == 0  # 0 means success
    except (AttributeError, OSError):
        try:
            # Fallback to System DPI awareness (Windows Vista+)
            # This uses the DPI of the primary monitor
            result = user32.SetProcessDPIAware()
            return result != 0  # Non-zero means success
        except (AttributeError, OSError):
            return False


def get_dpi_scale_factor() -> float:
    """
    Get the DPI scale factor for the current display.
    
    Returns:
        Scale factor (e.g., 1.0 for 96 DPI, 1.25 for 120 DPI, 1.5 for 144 DPI, 2.0 for 192 DPI)
    """
    if platform.system() != "Windows":
        return 1.0
    
    try:
        # Get DPI using GetDpiForSystem (Windows 10 1607+) or GetDeviceCaps
        if hasattr(user32, 'GetDpiForSystem'):
            dpi = user32.GetDpiForSystem()
        else:
            # Fallback: use GetDeviceCaps
            hdc = user32.GetDC(0)
            dpi = user32.GetDeviceCaps(hdc, 88)  # 88 = LOGPIXELSX
            user32.ReleaseDC(0, hdc)
        
        # Standard DPI is 96, so scale factor is dpi / 96
        return dpi / 96.0
    except (AttributeError, OSError):
        return 1.0


def scale_size(size: int, scale_factor: Optional[float] = None) -> int:
    """
    Scale a size value by the DPI scale factor.
    
    Args:
        size: Original size in pixels (assuming 96 DPI)
        scale_factor: Optional scale factor. If None, will be detected automatically.
    
    Returns:
        Scaled size in pixels
    """
    if scale_factor is None:
        scale_factor = get_dpi_scale_factor()
    return int(size * scale_factor)


def scale_font_size(font_size: int, scale_factor: Optional[float] = None) -> int:
    """
    Scale a font size by the DPI scale factor.
    
    Args:
        font_size: Original font size in points (assuming 96 DPI)
        scale_factor: Optional scale factor. If None, will be detected automatically.
    
    Returns:
        Scaled font size in points
    """
    if scale_factor is None:
        scale_factor = get_dpi_scale_factor()
    # Font scaling should be slightly more conservative to avoid oversized text
    # Use 90% of the full scale factor for fonts
    return int(font_size * (scale_factor * 0.9))


def get_scaled_font(family: str, size: int, weight: str = "normal", slant: str = "roman") -> Tuple[str, int]:
    """
    Get a font tuple scaled for current DPI.
    
    Args:
        family: Font family name (e.g., "Arial", "TkDefaultFont")
        size: Base font size (will be scaled)
        weight: Font weight ("normal" or "bold")
        slant: Font slant ("roman" or "italic")
    
    Returns:
        Tuple of (family, scaled_size, weight, slant)
    """
    scaled_size = scale_font_size(size)
    return (family, scaled_size, weight, slant)


def configure_tk_dpi(root: tk.Tk) -> float:
    """
    Configure a Tkinter root window for DPI scaling.
    
    This should be called after creating the root window but before creating widgets.
    
    Args:
        root: The Tkinter root window
    
    Returns:
        The detected DPI scale factor
    """
    scale_factor = get_dpi_scale_factor()
    
    # Tkinter 8.6+ supports tk.call('tk', 'scaling') to set DPI scaling
    try:
        # Get current scaling
        current_scaling = root.tk.call('tk', 'scaling')
        
        # Set scaling based on detected DPI (tk scaling is in DPI/72, not scale factor)
        # Standard is 72 DPI for Tk, so we convert from Windows DPI scale
        new_scaling = (72.0 / 96.0) * scale_factor * 96.0 / 72.0  # This simplifies to scale_factor
        root.tk.call('tk', 'scaling', scale_factor)
    except tk.TclError:
        # Fallback: if tk scaling doesn't work, we'll scale manually
        pass
    
    return scale_factor


def get_screen_dimensions(root: tk.Tk) -> Tuple[int, int]:
    """
    Get the available screen dimensions (accounting for taskbar, etc.).
    
    Args:
        root: Tkinter root window (used to query screen info)
    
    Returns:
        Tuple of (available_width, available_height)
    """
    # Update the window to get accurate screen info
    root.update_idletasks()
    
    # Get screen dimensions
    screen_width = root.winfo_screenwidth()
    screen_height = root.winfo_screenheight()
    
    # Account for taskbar/start menu (typically 40-50 pixels on Windows)
    # We'll be conservative and reserve 100 pixels for taskbar and window decorations
    taskbar_reserve = 100
    
    # Get the actual available work area
    available_width = screen_width
    available_height = screen_height - taskbar_reserve
    
    return (available_width, available_height)


def constrain_window_size(width: int, height: int, root: tk.Tk, 
                         max_width_ratio: float = 0.95, 
                         max_height_ratio: float = 0.90) -> Tuple[int, int]:
    """
    Constrain window dimensions to fit on screen.
    
    Args:
        width: Desired width
        height: Desired height
        root: Tkinter root window
        max_width_ratio: Maximum width as ratio of screen width (default 95%)
        max_height_ratio: Maximum height as ratio of screen height (default 90%)
    
    Returns:
        Tuple of (constrained_width, constrained_height)
    """
    available_width, available_height = get_screen_dimensions(root)
    
    # Calculate maximum allowed dimensions
    max_width = int(available_width * max_width_ratio)
    max_height = int(available_height * max_height_ratio)
    
    # Constrain dimensions
    constrained_width = min(width, max_width)
    constrained_height = min(height, max_height)
    
    return (constrained_width, constrained_height)


def center_window_on_screen(root: tk.Tk, width: int, height: int) -> str:
    """
    Create a geometry string that centers the window on the screen.
    
    Args:
        root: Tkinter root window
        width: Window width
        height: Window height
    
    Returns:
        Geometry string with centered position (e.g., "800x900+560+90")
    """
    # Update the window to get accurate screen info
    root.update_idletasks()
    
    # Get screen dimensions
    screen_width = root.winfo_screenwidth()
    screen_height = root.winfo_screenheight()
    
    # Calculate centered position
    x_pos = (screen_width - width) // 2
    y_pos = (screen_height - height) // 2
    
    # Ensure window doesn't go off-screen (minimum 10 pixels from edges)
    x_pos = max(10, x_pos)
    y_pos = max(10, y_pos)
    
    return f"{width}x{height}+{x_pos}+{y_pos}"


def scale_geometry(geometry: str, scale_factor: Optional[float] = None) -> str:
    """
    Scale a Tkinter geometry string (e.g., "800x900") by the DPI scale factor.
    
    Args:
        geometry: Geometry string in format "WIDTHxHEIGHT" or "WIDTHxHEIGHT+X+Y"
        scale_factor: Optional scale factor. If None, will be detected automatically.
    
    Returns:
        Scaled geometry string
    """
    if scale_factor is None:
        scale_factor = get_dpi_scale_factor()
    
    # Parse geometry string
    if '+' in geometry:
        size_part, position_part = geometry.split('+', 1)
        x_pos, y_pos = position_part.split('+', 1)
    elif '-' in geometry:
        size_part, position_part = geometry.rsplit('-', 1)
        x_pos, y_pos = position_part.split('-', 1)
    else:
        size_part = geometry
        x_pos = None
        y_pos = None
    
    # Parse size
    width, height = map(int, size_part.split('x'))
    
    # Scale dimensions
    width = scale_size(width, scale_factor)
    height = scale_size(height, scale_factor)
    
    # Reconstruct geometry string
    result = f"{width}x{height}"
    if x_pos is not None and y_pos is not None:
        # Position scaling is optional - usually you want windows centered anyway
        # But if provided, scale it too
        x_pos = scale_size(int(x_pos), scale_factor)
        y_pos = scale_size(int(y_pos), scale_factor)
        if '+' in geometry:
            result += f"+{x_pos}+{y_pos}"
        else:
            result += f"-{x_pos}-{y_pos}"
    
    return result


def get_safe_window_geometry(root: tk.Tk, base_width: int, base_height: int,
                             scale_factor: Optional[float] = None,
                             center: bool = True) -> str:
    """
    Get a safe window geometry that fits on screen, accounts for DPI scaling, and optionally centers.
    
    This is the main function to use for setting window geometry on Windows.
    It handles:
    - DPI scaling
    - Screen size constraints
    - Centering
    
    Args:
        root: Tkinter root window
        base_width: Base window width (at 96 DPI)
        base_height: Base window height (at 96 DPI)
        scale_factor: Optional DPI scale factor. If None, will be detected automatically.
        center: Whether to center the window (default True)
    
    Returns:
        Geometry string that's safe to use on the current display
    """
    if scale_factor is None:
        scale_factor = get_dpi_scale_factor()
    
    # Scale dimensions
    width = scale_size(base_width, scale_factor)
    height = scale_size(base_height, scale_factor)
    
    # Constrain to screen size
    width, height = constrain_window_size(width, height, root)
    
    # Create geometry string
    if center:
        return center_window_on_screen(root, width, height)
    else:
        return f"{width}x{height}"


# Initialize DPI awareness when module is imported (if on Windows)
# This must happen before any Tkinter windows are created
if platform.system() == "Windows":
    set_dpi_awareness()

