"""A self-contained ``.gitignore`` matcher.

Rules are layered per directory: the stack holds one layer per ``.gitignore``
found on the way down, and the last rule that matches decides, shallowest layer
first, which is what git does.
"""

from __future__ import annotations

import re

ALWAYS_SKIP = frozenset(
    {".git", "node_modules", "dist", "build", "__pycache__", ".venv", "venv", ".zyte"}
)


class _Rule:
    __slots__ = ("regex", "negated", "dir_only")

    def __init__(self, regex: re.Pattern[str], negated: bool, dir_only: bool) -> None:
        self.regex = regex
        self.negated = negated
        self.dir_only = dir_only


def _translate(pattern: str) -> str:
    out: list[str] = []
    index = 0
    size = len(pattern)
    while index < size:
        char = pattern[index]
        if char == "*":
            if pattern.startswith("**/", index):
                out.append("(?:.*/)?")
                index += 3
                continue
            if pattern.startswith("**", index):
                out.append(".*")
                index += 2
                continue
            out.append("[^/]*")
            index += 1
            continue
        if char == "?":
            out.append("[^/]")
            index += 1
            continue
        if char == "[":
            end = index + 1
            if end < size and pattern[end] in "!^":
                end += 1
            if end < size and pattern[end] == "]":
                end += 1
            while end < size and pattern[end] != "]":
                end += 1
            if end >= size:
                out.append(re.escape(char))
                index += 1
                continue
            body = pattern[index + 1 : end]
            if body.startswith("!"):
                body = "^" + body[1:]
            out.append("[" + body + "]")
            index = end + 1
            continue
        out.append(re.escape(char))
        index += 1
    return "".join(out)


def _compile(line: str) -> _Rule | None:
    pattern = line.rstrip("\n").rstrip("\r")
    if not pattern.strip() or pattern.lstrip().startswith("#"):
        return None
    pattern = pattern.replace("\\ ", "\x00")
    pattern = pattern.rstrip()
    pattern = pattern.replace("\x00", " ")
    negated = pattern.startswith("!")
    if negated:
        pattern = pattern[1:]
    dir_only = pattern.endswith("/")
    pattern = pattern.rstrip("/")
    if not pattern:
        return None
    anchored = "/" in pattern
    if pattern.startswith("/"):
        pattern = pattern[1:]
    prefix = "^" if anchored else "(?:^|.*/)"
    try:
        regex = re.compile(prefix + _translate(pattern) + r"(?:/.*)?$")
    except re.error:
        return None
    return _Rule(regex, negated, dir_only)


class IgnoreStack:
    """Layered gitignore rules, pushed as the walk descends and popped on the way out."""

    def __init__(self) -> None:
        self._layers: list[tuple[str, list[_Rule]]] = []

    def push(self, base: str, text: str) -> None:
        rules = [rule for rule in (_compile(line) for line in text.splitlines()) if rule is not None]
        self._layers.append((base, rules))

    def pop(self) -> None:
        if self._layers:
            self._layers.pop()

    def ignored(self, rel_path: str, is_dir: bool) -> bool:
        verdict = False
        for base, rules in self._layers:
            if base:
                if not rel_path.startswith(base + "/"):
                    continue
                candidate = rel_path[len(base) + 1 :]
            else:
                candidate = rel_path
            ancestors = _ancestors(candidate) if not is_dir else ()
            for rule in rules:
                if rule.dir_only and not is_dir:
                    if any(rule.regex.match(parent) for parent in ancestors):
                        verdict = not rule.negated
                    continue
                if rule.regex.match(candidate):
                    verdict = not rule.negated
        return verdict


def _ancestors(rel_path: str) -> tuple[str, ...]:
    parts = rel_path.split("/")[:-1]
    return tuple("/".join(parts[: index + 1]) for index in range(len(parts)))
