from __future__ import annotations

import logging
import os
import shutil
import sys
from pathlib import Path
from typing import Any

APP_NAME = "PDFToDOCXOCR"


class CoordinateMapper:
    """Centralized conversion between normalized, view and PDF coordinates."""

    @staticmethod
    def pdf_to_normalized(
        x: float,
        y: float,
        width: float,
        height: float,
        page_width: float,
        page_height: float,
    ) -> tuple[float, float, float, float]:
        if page_width <= 0 or page_height <= 0:
            raise ValueError("Page dimensions must be positive.")

        return (
            x / page_width,
            y / page_height,
            width / page_width,
            height / page_height,
        )

    @staticmethod
    def normalized_to_pdf(
        x: float,
        y: float,
        width: float,
        height: float,
        page_width: float,
        page_height: float,
    ) -> tuple[float, float, float, float]:
        return (
            x * page_width,
            y * page_height,
            width * page_width,
            height * page_height,
        )

    @staticmethod
    def normalized_to_view(
        x: float,
        y: float,
        width: float,
        height: float,
        page_left: float,
        page_top: float,
        page_width: float,
        page_height: float,
    ) -> tuple[float, float, float, float]:
        return (
            page_left + x * page_width,
            page_top + y * page_height,
            width * page_width,
            height * page_height,
        )

    @staticmethod
    def view_to_normalized(
        x: float,
        y: float,
        width: float,
        height: float,
        page_left: float,
        page_top: float,
        page_width: float,
        page_height: float,
    ) -> tuple[float, float, float, float]:
        if page_width <= 0 or page_height <= 0:
            raise ValueError("View page dimensions must be positive.")

        return (
            (x - page_left) / page_width,
            (y - page_top) / page_height,
            width / page_width,
            height / page_height,
        )


def application_data_dir() -> Path:
    if sys.platform.startswith("win"):
        root = Path(os.environ.get("APPDATA", Path.home()))
    elif sys.platform == "darwin":
        root = Path.home() / "Library" / "Application Support"
    else:
        root = Path(
            os.environ.get(
                "XDG_CONFIG_HOME",
                Path.home() / ".config",
            )
        )

    path = root / APP_NAME
    path.mkdir(parents=True, exist_ok=True)
    return path


def log_dir() -> Path:
    path = application_data_dir() / "logs"
    path.mkdir(parents=True, exist_ok=True)
    return path


def configure_logging() -> None:
    path = log_dir() / "application.log"

    logging.basicConfig(
        level=logging.INFO,
        format=(
            "%(asctime)s | %(levelname)s | "
            "%(name)s | %(threadName)s | %(message)s"
        ),
        handlers=[
            logging.FileHandler(
                path,
                encoding="utf-8",
            ),
            logging.StreamHandler(),
        ],
        force=True,
    )


def discover_tesseract() -> str:
    candidates: list[str] = []

    discovered = shutil.which("tesseract")
    if discovered:
        candidates.append(discovered)

    if sys.platform.startswith("win"):
        candidates.extend(
            [
                r"C:\Program Files\Tesseract-OCR\tesseract.exe",
                r"C:\Program Files (x86)\Tesseract-OCR\tesseract.exe",
                str(
                    Path.home()
                    / "AppData"
                    / "Local"
                    / "Programs"
                    / "Tesseract-OCR"
                    / "tesseract.exe"
                ),
            ]
        )

    for candidate in candidates:
        if Path(candidate).is_file():
            return candidate

    return ""

def free_space(path: str | Path) -> int:
    """
    Return available disk space for the filesystem containing `path`.

    The target file does not need to exist yet. If the target's parent
    directory does not exist, walk upward until an existing directory
    is found.
    """
    target = Path(path).expanduser()

    # If the target itself exists, use it.
    if target.exists():
        return shutil.disk_usage(target).free

    # Otherwise find the nearest existing parent.
    parent = target.parent

    while not parent.exists():
        if parent == parent.parent:
            break
        parent = parent.parent

    return shutil.disk_usage(parent).free



def sanitize_filename(value: str) -> str:
    invalid = '<>:"/\\|?*'
    return "".join("_" if char in invalid else char for char in value)


def exception_text(exc: BaseException) -> str:
    return f"{type(exc).__name__}: {exc}"


def as_dict(value: Any) -> dict[str, Any]:
    if isinstance(value, dict):
        return value
    return {}