# -*- coding: utf-8 -*-

"""Additional utility functions to use with application detectors."""

from __future__ import annotations

__all__ = [
    'is_32bit', 'is_64bit', 'is_linux', 'is_not_linux', 'is_osx', 'is_not_osx', 'is_win', 'is_not_win',
    'CliVersionChecker', 'FileVersionChecker', 'InjectVersion',
]

import io
import os
import re
import typing

from anaconda_navigator import config as navigator_config
from anaconda_navigator.utils.conda import launch as conda_launch_utils

from .. import detectors
from ..detectors import utilities
from .. import validation


def is_32bit() -> bool:
    """Check if user runs a 32-bit OS."""
    return navigator_config.BITS_32


def is_64bit() -> bool:
    """Check if user runs a 64-bit OS."""
    return navigator_config.BITS_64


def is_linux() -> bool:
    """Check if user runs Linux OS."""
    return navigator_config.LINUX


def is_not_linux() -> bool:
    """Check if user runs anything but Linux OS."""
    return not navigator_config.LINUX


def is_osx() -> bool:
    """Check if user runs OS X."""
    return navigator_config.MAC


def is_not_osx() -> bool:
    """Check if user runs anything but OS X."""
    return not navigator_config.MAC


def is_win() -> bool:
    """Check if user runs Windows OS."""
    return navigator_config.WIN


def is_not_win() -> bool:
    """Check if user runs anything but Windows OS."""
    return not navigator_config.WIN


class CliVersionChecker(detectors.Filter):  # pylint: disable=too-few-public-methods
    """
    Detect version of the application from the terminal output of the application.

    It executes application with a given set of `arguments` and searches for `pattern` in output. The latter one must
    have a single group, value from which will be used as a result version.
    """

    __slots__ = ('__arguments', '__pattern')

    def __init__(
            self,
            pattern: str = r'(\d+\.[\d\.]+)',
            arguments: typing.Union[str, typing.Iterable[str]] = ('--version',),
    ) -> None:
        """Initialize new :class:`~CliVersionChecker` instance."""
        if isinstance(arguments, str):
            arguments = (arguments,)
        elif not isinstance(arguments, tuple):
            arguments = tuple(arguments)

        self.__arguments: typing.Final[typing.Tuple[str, ...]] = arguments
        self.__pattern: typing.Final[typing.Pattern[str]] = re.compile(pattern)

    @classmethod
    def _parse_configuration(cls, *args: typing.Any, **kwargs: typing.Any) -> detectors.Detector:
        """Parse configuration for this particular :class:`~Detector`."""
        validation.OfLength(0)(args, field='args')
        pattern: str = validation.ItemPopper('pattern', default=r'(\d+\.[\d\.]+)').convert(validation.IS_STR)(kwargs)
        arguments: typing.Tuple[str, ...] = validation.ItemPopper('arguments', default=('--version',)).convert(
            validation.sequence_of(str),
        )(kwargs)
        validation.IsEmptyMapping()(kwargs)
        with validation.catch_exception():
            return cls(pattern=pattern, arguments=arguments)

    def __call__(
            self,
            parent: typing.Iterator[detectors.DetectedApplication],
            *,
            context: detectors.DetectorContext,
    ) -> typing.Iterator[detectors.DetectedApplication]:
        """Iterate through detected applications."""
        application: 'detectors.DetectedApplication'
        for application in parent:
            if application.has_executable:
                stdout: str
                try:
                    stdout, _, _ = conda_launch_utils.run_process([application.executable, *self.__arguments])
                    match: typing.Optional[typing.Match[str]] = self.__pattern.search(stdout)
                    if match:
                        application = application.replace(version=match.group(1))
                except (OSError, LookupError, TypeError, ValueError):
                    pass
            yield application


class FileVersionChecker(detectors.Filter):  # pylint: disable=too-few-public-methods
    """Detect version of the application from the file in the application :code:`root`."""

    __slots__ = ('__encoding', '__filename', '__pattern')

    def __init__(self, filename: str, pattern: str = r'(\d+\.[\d\.]+)', encoding: str = 'utf8') -> None:
        """Initialize new :class:`~FileVersionChecker` instance."""
        self.__encoding: typing.Final[str] = encoding
        self.__filename: typing.Final[str] = filename
        self.__pattern: typing.Final[typing.Pattern[str]] = re.compile(pattern)

    @classmethod
    def _parse_configuration(cls, *args: typing.Any, **kwargs: typing.Any) -> detectors.Detector:
        """Parse configuration for this particular :class:`~Detector`."""
        validation.OfLength(0)(args, field='args')
        encoding: str = validation.ItemPopper('encoding', default='utf8').convert(validation.IS_STR)(kwargs)
        filename: str = utilities.parse_and_join(validation.ItemPopper('filename')(kwargs))  # type: ignore
        pattern: str = validation.ItemPopper('pattern', default=r'(\d+\.[\d\.]+)').convert(validation.IS_STR)(kwargs)
        validation.IsEmptyMapping()(kwargs)
        with validation.catch_exception():
            return cls(encoding=encoding, filename=filename, pattern=pattern)

    def __call__(
            self,
            parent: typing.Iterator[detectors.DetectedApplication],
            *,
            context: detectors.DetectorContext,
    ) -> typing.Iterator[detectors.DetectedApplication]:
        """Iterate through detected applications."""
        application: 'detectors.DetectedApplication'
        for application in parent:
            if application.has_root:
                try:
                    stream: io.TextIOWrapper
                    with open(
                            os.path.join(application.root, self.__filename),
                            'rt',
                            encoding=self.__encoding,
                    ) as stream:
                        match: typing.Optional[typing.Match[str]] = self.__pattern.search(stream.read(4_194_304))
                        if match:
                            application = application.replace(version=match.group(1))
                except (OSError, LookupError, TypeError, ValueError):
                    pass
            yield application


class InjectVersion(detectors.Filter):  # pylint: disable=too-few-public-methods
    """Register a predefined version for the application."""

    __slots__ = ('__force', '__value')

    def __init__(self, value: str, *, force: bool = False) -> None:
        """Initialize new :class:`~InjectVersion` instance."""
        self.__force: typing.Final[bool] = force
        self.__value: typing.Final[str] = str(value).strip()

    @classmethod
    def _parse_configuration(cls, *args: typing.Any, **kwargs: typing.Any) -> detectors.Detector:
        """Parse configuration for this particular :class:`~Detector`."""
        validation.OfLength(0)(args, field='args')
        force = validation.ItemPopper('force', default=False).convert(validation.IS_BOOL)(kwargs)
        value = validation.ItemPopper('value').convert(validation.IS_STR)(kwargs)
        validation.IsEmptyMapping()(kwargs)
        with validation.catch_exception():
            return cls(force=force, value=value)

    def __call__(
            self,
            parent: typing.Iterator[detectors.DetectedApplication],
            *,
            context: detectors.DetectorContext,
    ) -> typing.Iterator[detectors.DetectedApplication]:
        """Iterate through detected applications."""
        application: 'detectors.DetectedApplication'
        for application in parent:
            if self.__force or (not application.has_version):
                application = application.replace(version=self.__value)
            yield application
