# -*- coding: utf-8 -*-
"""
paml_tester.utils
-----------------
A small collection of beginner-friendly utility helpers
covering strings, simple math, and dates.
"""

from __future__ import annotations

import math
import datetime
from typing import Union


# ──────────────────────────────────────────────
# String helpers
# ──────────────────────────────────────────────

def reverse_string(text: str) -> str:
    """Return the reverse of a string.

    Parameters
    ----------
    text : str
        The input string to reverse.

    Returns
    -------
    str
        The reversed string.

    Examples
    --------
    >>> reverse_string("hello")
    'olleh'
    """
    if not isinstance(text, str):
        raise TypeError(f"Expected str, got {type(text).__name__}")
    return text[::-1]


def is_palindrome(text: str) -> bool:
    """Check whether a string reads the same forwards and backwards.

    Comparison is case-insensitive and ignores spaces.

    Parameters
    ----------
    text : str
        The input string to test.

    Returns
    -------
    bool
        ``True`` if *text* is a palindrome, ``False`` otherwise.

    Examples
    --------
    >>> is_palindrome("racecar")
    True
    >>> is_palindrome("hello")
    False
    """
    cleaned = text.replace(" ", "").lower()
    return cleaned == cleaned[::-1]


def word_count(text: str) -> int:
    """Count the number of words in a string.

    Parameters
    ----------
    text : str
        The input string.

    Returns
    -------
    int
        Number of whitespace-separated words.

    Examples
    --------
    >>> word_count("hello world foo")
    3
    """
    return len(text.split())


# ──────────────────────────────────────────────
# Math helpers
# ──────────────────────────────────────────────

def clamp(value: Union[int, float],
          low: Union[int, float],
          high: Union[int, float]) -> Union[int, float]:
    """Clamp *value* so it stays within [low, high].

    Parameters
    ----------
    value : int | float
        The number to clamp.
    low : int | float
        Lower bound (inclusive).
    high : int | float
        Upper bound (inclusive).

    Returns
    -------
    int | float
        *low* if value < low, *high* if value > high, else *value*.

    Examples
    --------
    >>> clamp(15, 0, 10)
    10
    >>> clamp(-3, 0, 10)
    0
    >>> clamp(5, 0, 10)
    5
    """
    if low > high:
        raise ValueError("low must be <= high")
    return max(low, min(value, high))


def percentage(part: Union[int, float],
               total: Union[int, float],
               decimals: int = 2) -> float:
    """Calculate what percentage *part* is of *total*.

    Parameters
    ----------
    part : int | float
        The partial value.
    total : int | float
        The total value.
    decimals : int, optional
        Number of decimal places to round to (default ``2``).

    Returns
    -------
    float
        Percentage rounded to *decimals* places.

    Examples
    --------
    >>> percentage(25, 200)
    12.5
    """
    if total == 0:
        raise ZeroDivisionError("total cannot be zero")
    return round((part / total) * 100, decimals)


def factorial(n: int) -> int:
    """Return the factorial of a non-negative integer.

    Parameters
    ----------
    n : int
        A non-negative integer.

    Returns
    -------
    int
        n!

    Examples
    --------
    >>> factorial(5)
    120
    """
    if not isinstance(n, int) or n < 0:
        raise ValueError("n must be a non-negative integer")
    return math.factorial(n)


# ──────────────────────────────────────────────
# Date helpers
# ──────────────────────────────────────────────

def days_until(target: datetime.date) -> int:
    """Return the number of days from today until *target*.

    Parameters
    ----------
    target : datetime.date
        The future (or past) date.

    Returns
    -------
    int
        Positive if *target* is in the future, negative if in the past,
        zero if today.

    Examples
    --------
    >>> import datetime
    >>> days_until(datetime.date.today())
    0
    """
    today = datetime.date.today()
    return (target - today).days


def age_in_years(birthdate: datetime.date) -> int:
    """Calculate a person's age in complete years.

    Parameters
    ----------
    birthdate : datetime.date
        The date of birth.

    Returns
    -------
    int
        Age in full years.

    Examples
    --------
    >>> import datetime
    >>> age_in_years(datetime.date(2000, 1, 1))  # doctest: +SKIP
    25
    """
    today = datetime.date.today()
    years = today.year - birthdate.year
    # Subtract 1 if the birthday hasn't occurred yet this year
    if (today.month, today.day) < (birthdate.month, birthdate.day):
        years -= 1
    return years
