import logging
import os
from importlib import util

_SUPPORTED_PACKAGES = {}

_FILE_DIRECTORY = os.path.abspath(os.path.dirname(__file__))

__all__ = ["debug", "get_version", "package_available"]


def package_available(name: str):
    """Returns if package is available.

    Args:
        name (str): Name of the package.

    Returns:
        bool: Whether module exists in environment.
    """
    global _SUPPORTED_PACKAGES
    if name not in _SUPPORTED_PACKAGES:
        _SUPPORTED_PACKAGES[name] = util.find_spec(name) is not None
    return _SUPPORTED_PACKAGES[name]


def get_version(package_or_name) -> str:
    """Returns package version.

    Args:
        package_or_name (``module`` or ``str``): Module or name of module.
            This package must have the version accessible through ``<module>.__version__``.

    Returns:
        str: The package version.

    Examples:
        >>> get_version("numpy")
        "1.20.0"
    """
    if isinstance(package_or_name, str):
        if not package_available(package_or_name):
            raise ValueError(f"Package {package_or_name} not available")
        spec = util.find_spec(package_or_name)
        package_or_name = util.module_from_spec(spec)
        spec.loader.exec_module(package_or_name)
    version = package_or_name.__version__
    return version


def sitk_available():
    return package_available("SimpleITK")


def cupy_available():
    if "cupy" not in _SUPPORTED_PACKAGES:
        try:
            import cupy  # noqa
        except ImportError:
            _SUPPORTED_PACKAGES["cupy"] = False
    return package_available("cupy")


def sigpy_available():
    return package_available("sigpy")


def torch_available():
    return package_available("torch")


def output_dir() -> str:
    return os.path.abspath(os.path.join(_FILE_DIRECTORY, "../../.voxel"))


def temp_dir() -> str:
    return os.path.join(output_dir(), "temp")


def log_file_path() -> str:
    return os.path.join(output_dir(), "voxel.log")


def debug(value: bool = None) -> bool:
    """Return (and optionally set) debug mode.

    Args:
        value (bool, optional): If specified, sets the debug status.
            If not specified, debug mode is not set, only returned.

    Returns:
        bool: If ``True``, debug mode is active.

    Raises:
        ValueError: If ``value`` is not a supported value.

    Note:
        Changing the debug state changes the stream handler logging level
        for the default voxel logger. If debug state is turned off, logging
        level is set to ``logging.INFO``. If debug state is turned on,
        logging level is set to ``logging.DEBUG``.

    Examples:
        >>> debug()  # get debug status, defaults to False
        False
        >>> debug(True)  # turn on debug mode
        True
        >>> debug()  # get debug status
        True
    """

    def _is_debug():
        return os.environ.get("VOXEL_DEBUG", "") in ["True", "true"]

    def _toggle_debug(_old_value, _new_value):
        # TODO: Toggle voxel logging to debug mode.
        if _old_value == _new_value:
            return

        _vx_logger = logging.getLogger("voxel")
        if _new_value:
            _vx_logger.setLevel(logging.DEBUG)
            for h in _vx_logger.handlers:
                h.setLevel(logging.DEBUG)
        else:
            _vx_logger.setLevel(logging.DEBUG)  # the root logger should always log at DEBUG level
            for h in _vx_logger.handlers:
                if isinstance(h, logging.StreamHandler):
                    h.setLevel(logging.INFO)

    if value is not None:
        old_value = _is_debug()
        if isinstance(value, bool):
            os.environ["VOXEL_DEBUG"] = str(value)
        elif isinstance(value, str) and value.lower() in ("true", "false", ""):
            os.environ["VOXEL_DEBUG"] = value
        else:
            raise ValueError(f"Unknown value for debug: '{value}'")

        _toggle_debug(old_value, _is_debug())

    return _is_debug()
