import os
import re
from logging import Logger
from os import DirEntry
from typing import AbstractSet, Dict, List, Optional, Set, Tuple, cast

from ._const import (
    IGNORE_DIRS,
    SPHINX_BUILD_TARGET,
    TARGETS,
    Category,
    DeleteEntry,
    EntryType,
    RemoveTarget,
)
from ._manipulator import DirEntryManipulator


class Finder:
    def __init__(
        self,
        logger: Logger,
        manipulator: DirEntryManipulator,
        exclude_pattern: Optional[str],
        include_categories: AbstractSet[str],
    ) -> None:
        self.__logger = logger
        self.__manipulator = manipulator
        self.__include_categories = include_categories

        self.__target_map = self.__make_target_map()
        self.__exclude_pattern = re.compile(exclude_pattern) if exclude_pattern else None
        self.__delete_entries: Set[DeleteEntry] = set()

        logger.debug(f"exclude_pattern: {exclude_pattern}")
        logger.debug(f"include_categories: {include_categories}")

    def is_remove_entry(self, entry: DirEntry) -> Tuple[bool, Optional[RemoveTarget]]:
        for target in self.__target_map.get(self.__manipulator.get_entry_type(entry), []):
            if target.regexp.search(entry.name):
                self.__logger.debug(
                    f"match remove pattern: path={entry.path}, pattern={target.regexp.pattern}"
                )
                return (True, target)

        if Category.BUILD in self.__include_categories:
            if entry.name == "_build" and SPHINX_BUILD_TARGET.regexp.search(entry.path) is not None:
                return (True, target)

        return (False, None)

    def is_skip_entry(self, entry: DirEntry) -> bool:
        if self.__exclude_pattern and self.__exclude_pattern.search(entry.name):
            self.__logger.debug(f"match exclude pattern: {entry.path}")
            return True

        if self.__manipulator.is_dir(entry) and entry.name in IGNORE_DIRS:
            self.__logger.debug(f"match ignore dir: {entry.path}")
            return True

        return False

    def get_delete_entries(self) -> AbstractSet[DeleteEntry]:
        return self.__delete_entries

    def traverse(self, root: str) -> AbstractSet[DeleteEntry]:
        with os.scandir(root) as it:
            for entry in it:
                if self.is_skip_entry(entry):
                    self.__logger.debug(f"skip entry: {entry.path}")
                    continue

                is_remove, remove_target = self.is_remove_entry(entry)
                if is_remove:
                    self.__logger.debug(f"add delete target: {entry.path}")
                    self.__delete_entries.add((entry, cast(RemoveTarget, remove_target)))
                    continue

                if self.__manipulator.is_dir(entry):
                    self.traverse(entry.path)

        return self.__delete_entries

    def __make_target_map(self) -> Dict[EntryType, List[RemoveTarget]]:
        target_map: Dict[EntryType, List[RemoveTarget]] = {}

        for target in TARGETS:
            if target.category not in self.__include_categories:
                continue

            target_map.setdefault(target.target_type, []).append(target)

        return target_map
