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

"""Components to expand/modify initial application descriptions."""

from __future__ import annotations

__all__ = ['StepIntoRoot', 'AppendExecutable', 'AppendRoot']

import os
import re
import typing

from .. import exceptions
from .. import validation
from . import core
from . import utilities


class StepIntoRoot(core.Filter, alias='step_into_root'):  # pylint: disable=too-few-public-methods
    """
    Iterate through children of application root.

    Each child might be checked that it `starts_with`, `ends_with` or `equals` to one or multiple values.

    Iteration results are sorted. If you want descending order of the results - set `reverse` to :code:`True`.
    """

    __slots__ = ('__ends_with', '__equals', '__matches', '__reverse', '__starts_with')

    def __init__(  # pylint: disable=too-many-arguments
            self,
            *,
            equals: typing.Union[str, typing.Iterable[str]] = (),
            starts_with: typing.Union[str, typing.Iterable[str]] = (),
            ends_with: typing.Union[str, typing.Iterable[str]] = (),
            matches: typing.Union[str, typing.Iterable[str]] = (),
            reverse: bool = False,
    ) -> None:
        """Initialize new :class:`~StepIntoRoot` instance."""
        if isinstance(equals, str):
            equals = frozenset({equals})
        else:
            equals = frozenset(equals)

        if isinstance(starts_with, str):
            starts_with = (starts_with,)
        else:
            starts_with = tuple(starts_with)

        if isinstance(ends_with, str):
            ends_with = (ends_with,)
        else:
            ends_with = tuple(ends_with)

        if isinstance(matches, str):
            matches = (matches,)
        else:
            matches = tuple(matches)

        self.__ends_with: typing.Final[tuple[str, ...]] = ends_with
        self.__equals: typing.Final[frozenset[str]] = equals
        self.__matches: typing.Final[tuple[str, ...]] = matches
        self.__reverse: typing.Final[bool] = reverse
        self.__starts_with: typing.Final[tuple[str, ...]] = starts_with

    @classmethod
    def _parse_configuration(cls, *args: typing.Any, **kwargs: typing.Any) -> core.Detector:
        """Parse configuration for this particular :class:`~Detector`."""
        validation.OfLength(0)(args, field='args')

        query: typing.Dict[str, typing.Any] = {
            key: validation.ItemPopper(key, default=()).convert(validation.sequence_of(str))(kwargs)
            for key in ('equals', 'starts_with', 'ends_with', 'matches')
        }
        query['reverse'] = validation.ItemPopper('reverse', default=False).convert(validation.IS_BOOL)(kwargs)

        validation.IsEmptyMapping()(kwargs)

        with validation.catch_exception():
            return cls(**query)

    def __call__(
            self,
            parent: typing.Iterator[core.DetectedApplication],
            *,
            context: core.DetectorContext,
    ) -> typing.Iterator[core.DetectedApplication]:
        """Iterate through detected applications."""
        application: core.DetectedApplication
        for application in parent:
            try:
                root: str = application.root
                children: typing.List[str] = sorted(os.listdir(root), reverse=self.__reverse)
            except (AttributeError, OSError):
                continue

            child: str
            for child in children:
                if self.__equals and (child not in self.__equals):
                    continue
                if self.__starts_with and not any(child.startswith(item) for item in self.__starts_with):
                    continue
                if self.__ends_with and not any(child.endswith(item) for item in self.__ends_with):
                    continue
                if self.__matches and not any(re.fullmatch(item, child) for item in self.__matches):
                    continue
                yield application.replace(root=os.path.join(root, child))


class AppendExecutable(core.Filter, alias='append_executable'):  # pylint: disable=too-few-public-methods
    """
    Check multiple options of executables and iterate through existing ones.

    Each option of the executable must be a path relative to the application root.
    """

    __slots__ = ('__content',)

    def __init__(self, *args: typing.Union[None, str, typing.Iterable[typing.Optional[str]]]) -> None:
        """Initialize new :class:`~AppendExecutable` instance."""
        self.__content: typing.Final[typing.Tuple[str, ...]] = tuple(utilities.collect_str(args))

    @classmethod
    def _parse_configuration(cls, *args: typing.Any, **kwargs: typing.Any) -> core.Detector:
        """Parse configuration for this particular :class:`~Detector`."""
        new_args: typing.List[str] = []
        with exceptions.ValidationError.with_field('args'):
            validation.OfLength(at_least=1)(args)
            new_args.extend(map(utilities.parse_and_join, validation.iterable_items(args)))  # type: ignore

        validation.IsEmptyMapping()(kwargs)

        with validation.catch_exception():
            return cls(*new_args)

    def __call__(
            self,
            parent: typing.Iterator[core.DetectedApplication],
            *,
            context: core.DetectorContext,
    ) -> typing.Iterator[core.DetectedApplication]:
        """Iterate through detected applications."""
        application: core.DetectedApplication
        for application in parent:
            try:
                root: str = application.root
            except AttributeError:
                continue

            executable: str
            for executable in self.__content:
                executable = os.path.abspath(os.path.join(root, executable))
                if os.path.isfile(executable):
                    yield application.replace(executable=executable)


class AppendRoot(core.Filter, alias='append_root'):  # pylint: disable=too-few-public-methods
    """
    Append parent directory of an `executable` as a root.

    It is possibly to select the `level` of the parent folder, which should be used as a root directory.

    If you have  application with :code:`executable = '/a/b/c/d/e/application'`, then:

    - :code:`level=0` would result in :code:`root = '/a/b/c/d/e'`
    - :code:`level=1` would result in :code:`root = '/a/b/c/d'`
    - :code:`level=2` would result in :code:`root = '/a/b/c'`
    - and so on
    """

    __slots__ = ('__level',)

    def __init__(self, *, level: int = 0) -> None:
        """Initialize new :class:`~AppendRoot` instance."""
        self.__level: typing.Final[int] = level + 1

    @classmethod
    def _parse_configuration(cls, *args: typing.Any, **kwargs: typing.Any) -> core.Detector:
        """Parse configuration for this particular :class:`~Detector`."""
        validation.OfLength(0)(args, field='args')

        level: int = validation.ItemPopper('level', default=0).convert(validation.IS_INT)(kwargs)

        validation.IsEmptyMapping()(kwargs)

        with validation.catch_exception():
            return cls(level=level)

    def __call__(
            self,
            parent: typing.Iterator[core.DetectedApplication],
            *,
            context: core.DetectorContext,
    ) -> typing.Iterator[core.DetectedApplication]:
        """Iterate through detected applications."""
        application: core.DetectedApplication
        for application in parent:
            try:
                root: str = application.executable
            except AttributeError:
                continue
            for _ in range(self.__level):
                root = os.path.dirname(root)
            yield application.replace(root=root)
