"""Find all objects reachable from a root object."""

from __future__ import annotations

import types
import weakref
from collections.abc import Iterable, Iterator, Mapping
from typing import Final

method_descriptor_type: Final = type(object.__dir__)
method_wrapper_type: Final = type(object().__ne__)
wrapper_descriptor_type: Final = type(object.__ne__)

FUNCTION_TYPES: Final = (
    types.BuiltinFunctionType,
    types.FunctionType,
    types.MethodType,
    method_descriptor_type,
    wrapper_descriptor_type,
    method_wrapper_type,
)

ATTR_BLACKLIST: Final = {"__doc__", "__name__", "__class__", "__dict__"}

# Instances of these types can't have references to other objects
ATOMIC_TYPE_BLACKLIST: Final = {bool, int, float, str, type(None), object}

# Don't look at most attributes of these types
COLLECTION_TYPE_BLACKLIST: Final = {list, set, dict, tuple}

# Don't return these objects
TYPE_BLACKLIST: Final = {weakref.ReferenceType}


def isproperty(o: object, attr: str) -> bool:
    return isinstance(getattr(type(o), attr, None), property)


def get_edge_candidates(o: object) -> Iterator[tuple[object, object]]:
    # use getattr because mypyc expects dict, not mappingproxy
    if "__getattribute__" in getattr(type(o), "__dict__"):  # noqa: B009
        return
    if type(o) not in COLLECTION_TYPE_BLACKLIST:
        for attr in dir(o):
            try:
                if attr not in ATTR_BLACKLIST and hasattr(o, attr) and not isproperty(o, attr):
                    e = getattr(o, attr)
                    if type(e) not in ATOMIC_TYPE_BLACKLIST:
                        yield attr, e
            except AssertionError:
                pass
    if isinstance(o, Mapping):
        yield from o.items()
    elif isinstance(o, Iterable) and not isinstance(o, str):
        for i, e in enumerate(o):
            yield i, e


def get_edges(o: object) -> Iterator[tuple[object, object]]:
    for s, e in get_edge_candidates(o):
        if isinstance(e, FUNCTION_TYPES):
            # We don't want to collect methods, but do want to collect values
            # in closures and self pointers to other objects

            if hasattr(e, "__closure__"):
                yield (s, "__closure__"), e.__closure__
            if hasattr(e, "__self__"):
                se = e.__self__
                if se is not o and se is not type(o) and hasattr(s, "__self__"):
                    yield s.__self__, se
        else:
            if type(e) not in TYPE_BLACKLIST:
                yield s, e


def get_reachable_graph(root: object) -> tuple[dict[int, object], dict[int, tuple[int, object]]]:
    parents = {}
    seen = {id(root): root}
    worklist = [root]
    while worklist:
        o = worklist.pop()
        for s, e in get_edges(o):
            if id(e) in seen:
                continue
            parents[id(e)] = (id(o), s)
            seen[id(e)] = e
            worklist.append(e)

    return seen, parents


def get_path(
    o: object, seen: dict[int, object], parents: dict[int, tuple[int, object]]
) -> list[tuple[object, object]]:
    path = []
    while id(o) in parents:
        pid, attr = parents[id(o)]
        o = seen[pid]
        path.append((attr, o))
    path.reverse()
    return path
