import ast
import os
import pathlib
import re
import subprocess
import sys
import tempfile

root = pathlib.Path(__file__).parent.parent

lib = root / "lib"
mpl = lib / "matplotlib"


class Visitor(ast.NodeVisitor):
    def __init__(self, filepath, output, existing_allowed):
        self.filepath = filepath
        self.context = list(filepath.with_suffix("").relative_to(lib).parts)
        self.output = output
        self.existing_allowed = existing_allowed

    def _is_already_allowed(self, parts):
        # Skip outputting a path if it's already allowed before.
        candidates = ['.'.join(parts[:s]) for s in range(1, len(parts))]
        for allow in self.existing_allowed:
            if any(allow.fullmatch(path) for path in candidates):
                return True
        return False

    def visit_FunctionDef(self, node):
        # delete_parameter adds a private sentinel value that leaks
        # we do not want that sentinel value in the type hints but it breaks typing
        # Does not apply to variadic arguments (args/kwargs)
        for dec in node.decorator_list:
            if "delete_parameter" in ast.unparse(dec):
                deprecated_arg = dec.args[1].value
                if (
                    node.args.vararg is not None
                    and node.args.vararg.arg == deprecated_arg
                ):
                    continue
                if (
                    node.args.kwarg is not None
                    and node.args.kwarg.arg == deprecated_arg
                ):
                    continue

                parents = []
                if hasattr(node, "parent"):
                    parent = node.parent
                    while hasattr(parent, "parent") and not isinstance(
                        parent, ast.Module
                    ):
                        parents.insert(0, parent.name)
                        parent = parent.parent
                parts = [*self.context, *parents, node.name]
                if not self._is_already_allowed(parts):
                    self.output.write("\\.".join(parts) + "\n")
                break

    def visit_ClassDef(self, node):
        for dec in node.decorator_list:
            if "define_aliases" in ast.unparse(dec):
                parents = []
                if hasattr(node, "parent"):
                    parent = node.parent
                    while hasattr(parent, "parent") and not isinstance(
                        parent, ast.Module
                    ):
                        parents.insert(0, parent.name)
                        parent = parent.parent
                aliases = ast.literal_eval(dec.args[0])
                # Written as a regex rather than two lines to avoid unused entries
                # for setters on items with only a getter
                for substitutions in aliases.values():
                    parts = self.context + parents + [node.name]
                    for a in substitutions:
                        if not (self._is_already_allowed([*parts, f"get_{a}"]) and
                                self._is_already_allowed([*parts, f"set_{a}"])):
                            self.output.write("\\.".join([*parts, f"[gs]et_{a}\n"]))
        for child in ast.iter_child_nodes(node):
            self.visit(child)


existing_allowed = []
with (root / 'ci/mypy-stubtest-allowlist.txt').open() as f:
    for line in f:
        line, _, _ = line.partition('#')
        line = line.strip()
        if line:
            existing_allowed.append(re.compile(line))


with tempfile.TemporaryDirectory() as d:
    p = pathlib.Path(d) / "allowlist.txt"
    with p.open("wt") as f:
        for path in mpl.glob("**/*.py"):
            v = Visitor(path, f, existing_allowed)
            tree = ast.parse(path.read_text())

            # Assign parents to tree so they can be backtraced
            for node in ast.walk(tree):
                for child in ast.iter_child_nodes(node):
                    child.parent = node

            v.visit(tree)
    proc = subprocess.run(
        [
            "stubtest",
            "--mypy-config-file=pyproject.toml",
            "--ignore-disjoint-bases",
            "--allowlist=ci/mypy-stubtest-allowlist.txt",
            f"--allowlist={p}",
            "matplotlib",
        ],
        cwd=root,
        env=os.environ | {"MPLBACKEND": "agg"},
    )
    try:
        os.unlink(f.name)
    except OSError:
        pass

sys.exit(proc.returncode)
