"""
Helpers for configuring locale settings.

Name `localization` is chosen to avoid overlap with builtin `locale` module.
"""

from __future__ import annotations

from contextlib import contextmanager
import functools
import locale
import platform
import re
import subprocess
from typing import (
    TYPE_CHECKING,
    cast,
)

from pandas._config.config import options

if TYPE_CHECKING:
    from collections.abc import Generator


@contextmanager
def set_locale(
    new_locale: str | tuple[str, str], lc_var: int = locale.LC_ALL
) -> Generator[str | tuple[str, str]]:
    """
    Context manager for temporarily setting a locale.

    Parameters
    ----------
    new_locale : str or tuple
        A string of the form <language_country>.<encoding>. For example to set
        the current locale to US English with a UTF8 encoding, you would pass
        "en_US.UTF-8".
    lc_var : int, default `locale.LC_ALL`
        The category of the locale being set.

    Notes
    -----
    This is useful when you want to run a particular block of code under a
    particular locale, without globally setting the locale. This probably isn't
    thread-safe.
    """
    # getlocale is not always compliant with setlocale, use setlocale. GH#46595
    current_locale = locale.setlocale(lc_var)

    try:
        locale.setlocale(lc_var, new_locale)
        normalized_code, normalized_encoding = locale.getlocale()
        if normalized_code is not None and normalized_encoding is not None:
            yield f"{normalized_code}.{normalized_encoding}"
        else:
            yield new_locale
    finally:
        locale.setlocale(lc_var, current_locale)


def can_set_locale(lc: str, lc_var: int = locale.LC_ALL) -> bool:
    """
    Check to see if we can set a locale, and subsequently get the locale,
    without raising an Exception.

    Parameters
    ----------
    lc : str
        The locale to attempt to set.
    lc_var : int, default `locale.LC_ALL`
        The category of the locale being set.

    Returns
    -------
    bool
        Whether the passed locale can be set
    """
    try:
        with set_locale(lc, lc_var=lc_var):
            pass
    except (ValueError, locale.Error):
        # horrible name for an Exception subclass
        return False
    else:
        return True


def _valid_locales(locales: list[str] | str, normalize: bool) -> list[str]:
    """
    Return a list of normalized locales that do not throw an ``Exception``
    when set.

    Parameters
    ----------
    locales : str
        A string where each locale is separated by a newline.
    normalize : bool
        Whether to call ``locale.normalize`` on each locale.

    Returns
    -------
    valid_locales : list
        A list of valid locales.
    """
    return [
        loc
        for loc in (
            locale.normalize(loc.strip()) if normalize else loc.strip()
            for loc in locales
        )
        if can_set_locale(loc)
    ]


# Windows has no "locale -a" to enumerate with, so we hardcode a few values.
#  GH#46597
_WINDOWS_LOCALES = [
    "de-DE.UTF-8",  # Latin with diacritics
    "el-GR.UTF-8",  # Greek, where capitalize() has to handle final sigma
    "en-US.UTF-8",  # ASCII baseline
    "ja-JP.UTF-8",  # caseless script, capitalize() is a no-op
    "tr-TR.UTF-8",  # dotted and dotless i
]


@functools.cache
def _get_locales(prefix: str | None, normalize: bool) -> tuple[str, ...]:
    if platform.system() in ("Linux", "Darwin"):
        raw_locales = subprocess.check_output(["locale", "-a"])
        # raw_locales is "\n" separated list of locales
        # it may contain non-decodable parts, so split
        # extract what we can and then rejoin.
        out_locales = []
        for raw_locale in raw_locales.split(b"\n"):
            try:
                out_locales.append(
                    str(raw_locale, encoding=cast("str", options.display.encoding))
                )
            except UnicodeError:
                # 'locale -a' is used to populated 'raw_locales' and on
                # Redhat 7 Linux (and maybe others) prints locale names
                # using windows-1252 encoding.  Bug only triggered by
                # a few special characters and when there is an
                # extensive list of installed locales.
                out_locales.append(str(raw_locale, encoding="windows-1252"))
    elif platform.system() == "Windows":
        # Note: is_platform_windows causes circular import here
        out_locales = _WINDOWS_LOCALES
        # These are already spelled the way the CRT wants them; running them
        #  through the POSIX alias table would rename them ("ar-SA" -> "ar_AA").
        normalize = False
    else:
        return ()

    if prefix is None:
        return tuple(_valid_locales(out_locales, normalize))

    pattern = re.compile(f"{prefix}.*")
    found = pattern.findall("\n".join(out_locales))
    return tuple(_valid_locales(found, normalize))


def get_locales(
    prefix: str | None = None,
    normalize: bool = True,
) -> list[str]:
    """
    Get all the locales that are available on the system.

    Parameters
    ----------
    prefix : str
        If not ``None`` then return only those locales with the prefix
        provided. For example to get all English language locales (those that
        start with ``"en"``), pass ``prefix="en"``.
    normalize : bool
        Call ``locale.normalize`` on the resulting list of available locales.
        If ``True``, only locales that can be set without throwing an
        ``Exception`` are returned.

    Returns
    -------
    locales : list of strings
        A list of locale strings that can be set with ``locale.setlocale()``.
        For example::

            locale.setlocale(locale.LC_ALL, locale_string)

    On error will return an empty list (no locale available)

    """
    # Every candidate is probed with setlocale, and several test modules call
    #  this at collection time, so the result is cached.
    return list(_get_locales(prefix, normalize))
