import logging
import os
import re
from collections.abc import Sequence
from pathlib import Path
from typing import TYPE_CHECKING

__all__ = 'BaseFilter', 'DefaultFilter', 'PythonFilter'
logger = logging.getLogger('watchfiles.watcher')


if TYPE_CHECKING:
    from .main import Change


class BaseFilter:
    """
    Useful base class for creating filters. `BaseFilter` should be inherited and configured, rather than used
    directly.

    The class supports ignoring files in 3 ways:
    """

    __slots__ = '_ignore_dirs', '_ignore_entity_regexes', '_ignore_paths'
    ignore_dirs: Sequence[str] = ()
    """Full names of directories to ignore, an obvious example would be `.git`."""
    ignore_entity_patterns: Sequence[str] = ()
    """
    Patterns of files or directories to ignore, these are compiled into regexes.

    "entity" here refers to the specific file or directory - basically the result of `path.split(os.sep)[-1]`,
    an obvious example would be `r'\\.py[cod]$'`.
    """
    ignore_paths: Sequence[str | Path] = ()
    """
    Full paths to ignore, e.g. `/home/users/.cache` or `C:\\Users\\user\\.cache`.
    """

    def __init__(self) -> None:
        self._ignore_dirs = set(self.ignore_dirs)
        self._ignore_entity_regexes = tuple(re.compile(r) for r in self.ignore_entity_patterns)
        self._ignore_paths = tuple(map(str, self.ignore_paths))

    def __call__(self, change: 'Change', path: str) -> bool:
        """
        Instances of `BaseFilter` subclasses can be used as callables.
        Args:
            change: The type of change that occurred, see [`Change`][watchfiles.Change].
            path: the raw path of the file or directory that changed.

        Returns:
            True if the file should be included in changes, False if it should be ignored.
        """
        parts = path.lstrip(os.sep).split(os.sep)
        if any(p in self._ignore_dirs for p in parts):
            return False

        entity_name = parts[-1]
        if any(r.search(entity_name) for r in self._ignore_entity_regexes):
            return False
        elif self._ignore_paths and path.startswith(self._ignore_paths):
            return False
        else:
            return True

    def __repr__(self) -> str:
        args = ', '.join(f'{k}={getattr(self, k, None)!r}' for k in self.__slots__)
        return f'{self.__class__.__name__}({args})'


class DefaultFilter(BaseFilter):
    """
    The default filter, which ignores files and directories that you might commonly want to ignore.
    """

    ignore_dirs: Sequence[str] = (
        '__pycache__',
        '.git',
        '.hg',
        '.svn',
        '.tox',
        '.venv',
        '.idea',
        'node_modules',
        '.mypy_cache',
        '.pytest_cache',
        '.hypothesis',
    )
    """Directory names to ignore."""

    ignore_entity_patterns: Sequence[str] = (
        r'\.py[cod]$',
        r'\.___jb_...___$',
        r'\.sw.$',
        '~$',
        r'^\.\#',
        r'^\.DS_Store$',
        r'^flycheck_',
    )
    """File/Directory name patterns to ignore."""

    def __init__(
        self,
        *,
        ignore_dirs: Sequence[str] | None = None,
        ignore_entity_patterns: Sequence[str] | None = None,
        ignore_paths: Sequence[str | Path] | None = None,
    ) -> None:
        """
        Args:
            ignore_dirs: if not `None`, overrides the `ignore_dirs` value set on the class.
            ignore_entity_patterns: if not `None`, overrides the `ignore_entity_patterns` value set on the class.
            ignore_paths: if not `None`, overrides the `ignore_paths` value set on the class.
        """
        if ignore_dirs is not None:
            self.ignore_dirs = ignore_dirs
        if ignore_entity_patterns is not None:
            self.ignore_entity_patterns = ignore_entity_patterns
        if ignore_paths is not None:
            self.ignore_paths = ignore_paths

        super().__init__()


class PythonFilter(DefaultFilter):
    """
    A filter for Python files, since this class inherits from [`DefaultFilter`][watchfiles.DefaultFilter]
    it will ignore files and directories that you might commonly want to ignore as well as filtering out
    all changes except in Python files (files with extensions `('.py', '.pyx', '.pyd')`).
    """

    def __init__(
        self,
        *,
        ignore_paths: Sequence[str | Path] | None = None,
        extra_extensions: Sequence[str] = (),
    ) -> None:
        """
        Args:
            ignore_paths: The paths to ignore, see [`BaseFilter`][watchfiles.BaseFilter].
            extra_extensions: extra extensions to ignore.

        `ignore_paths` and `extra_extensions` can be passed as arguments partly to support [CLI](../cli.md) usage where
        `--ignore-paths` and `--extensions` can be passed as arguments.
        """
        self.extensions = ('.py', '.pyx', '.pyd') + tuple(extra_extensions)
        super().__init__(ignore_paths=ignore_paths)

    def __call__(self, change: 'Change', path: str) -> bool:
        return path.endswith(self.extensions) and super().__call__(change, path)
