__all__ = ('AttributeUnitBase', 'ClassAttributeUnit', 'FolderedUnit', 'FunctionUnit', 'InstanceAttributeUnit',
    'MAPPED_OBJECTS', 'ModuleUnit', 'ObjectedUnitBase', 'PropertyUnit', 'TypeUnit', 'UnitBase', 'map_module',
    'search_paths')

import sys, re, warnings
from types import FunctionType, BuiltinFunctionType, BuiltinMethodType, MethodType, GetSetDescriptorType, \
    MemberDescriptorType
from difflib import get_close_matches

from ...backend.utils import cached_property, MethodLike, module_property, basemethod, weakmethod
from ...backend.export import include
from ...discord.bases import IconSlot

from .qualpath import QualPath
from .parser import DocString
from .builder_text import serialize_docs_embed_sized, serialize_docs, serialize_docs_source_text, generate_preview_for
from .builder_html import html_serialize_docs

html_serialize_docs_extended = include('html_serialize_docs_extended')

WrapperDescriptorType = object.__eq__.__class__
MethodDescriptorType = int.bit_length.__class__
SlotWrapperType = object.__lt__.__class__

METHOD_TYPES = {
    GetSetDescriptorType,
    BuiltinMethodType,
    MethodType,
    WrapperDescriptorType,
    MethodDescriptorType,
    MethodLike,
    basemethod,
    weakmethod,
}

FUNCTION_TYPES = {
    *METHOD_TYPES,
    FunctionType,
    BuiltinFunctionType,
    BuiltinMethodType,
}

IGNORED_TYPES = {
    type,
    object,
}

IGNORED_CLASS_ATTRIBUTE_TYPES = {
    *METHOD_TYPES,
}

if (SlotWrapperType is not FunctionType) and (SlotWrapperType is not MethodType):
    IGNORED_CLASS_ATTRIBUTE_TYPES.add(SlotWrapperType)

IGNORED_FUNCTIONS = set()

converted = None
for func in (
        object.__delattr__,
        object.__dir__,
        object.__eq__,
        object.__format__,
        object.__ge__,
        object.__getattribute__,
        object.__gt__,
        object.__hash__,
        object.__init__,
        object.__init_subclass__,
        object.__le__,
        object.__lt__,
        object.__ne__,
        object.__new__,
        object.__reduce__,
        object.__reduce_ex__,
        object.__repr__,
        object.__setattr__,
        getattr(object, '__sizeof__', None), # Not present in Pypy
        object.__str__,
        object.__subclasshook__,
        type.__call__,
        type.__delattr__,
        type.__dir__,
        type.__eq__,
        type.__format__,
        type.__ge__,
        type.__getattribute__,
        type.__gt__,
        type.__hash__,
        type.__init__,
        type.__init_subclass__,
        type.__instancecheck__,
        type.__le__,
        type.__lt__,
        type.__ne__,
        type.__new__,
        type.__prepare__,
        type.__reduce__,
        type.__reduce_ex__,
        type.__repr__,
        type.__setattr__,
        getattr(type, '__sizeof__', None), # Not present in Pypy
        type.__str__,
        type.__subclasscheck__,
        type.__subclasses__,
        type.__subclasshook__,
        type.mro,
            ):
    
    if func is None:
        continue
    
    if (func.__class__ in METHOD_TYPES):
        converted = getattr(func, '__func__', None)
        if (converted is not None):
            func = converted
    
    IGNORED_FUNCTIONS.add(func)

del func, converted

PROPERTY_TYPES = {
    property,
    cached_property,
    module_property,
    IconSlot,
}

ATTRIBUTE_TYPES = {
    MemberDescriptorType,
}

IGNORED_CLASS_ATTRIBUTE_NAMES = {
    '__ne__',
    '__module__',
    '__str__',
    '__format__',
    '__lt__',
    '__gt__',
    '__le__',
    '__ge__',
    '__eq__',
    '__reduce__',
    '__reduce_ex__',
    '__setattr__',
    '__getattr__',
    '__delattr__',
    '__slots__',
    '__getattribute__',
    '__repr__',
    '__doc__',
    '__dir__',
    '__sizeof__',
    '__dict__',
    '__new__',
    '__init__',
    '__hash__',
    '__reversed__',
    '__getitem__',
    '__setitem__',
    '__delitem__',
    '__init_subclass__',
    '__subclasshook__',
    '__call__',
    '__class__',
    '__setstate__',
    '__getstate__',
    '__floor__',
    '__mro__',
    '__dictoffset__',
    '__round__',
    '__trunc__',
    '__len__',
    '__bases__',
    '__basicsize__',
    '__name__',
    '__qualname__',
    '__subclasscheck__',
    '__text_signature__',
    '__Weakrefoffset__',
    '__ceil__',
    '__getnewargs__',
    '__contains__',
    '__enter__',
    '__exit__',
    '__slots__',
    '__abstractmethods__',
    '__aenter__',
    '__aexit__',
    '__weakrefoffset__',
    '__instancecheck__',
    '__itemsize__',
    '__subclasses__',
    '__iter__',
    '__weakref__',
}

def map_types_and_functions(obj, references, path, from_type):
    """
    Maps types and functions of the given `obj` to it's `references`.
    
    Parameters
    ----------
    obj : `type` or `module`
        The object to map.
    references : `dict` of (`str`, ``UnitBase``) items
        References of `obj` to it's contained objects.
    path : ``QualPath``
        Tha path of `obj`.
    from_type : `bool`
        Whether `obj` is a `type` instance.
    """
    for attr_name in dir(obj):
        try:
            attr_value = getattr(obj, attr_name)
        except AttributeError:
            continue
        
        attr_value_type = attr_value.__class__
        
        # Process only hashables
        try:
            hash(attr_value)
        except (TypeError, RuntimeError):
            if not from_type:
                continue
            
            if attr_value_type in IGNORED_CLASS_ATTRIBUTE_TYPES:
                continue
            
            if attr_name in IGNORED_CLASS_ATTRIBUTE_NAMES:
                continue
            
            references[attr_name] = ClassAttributeUnit(attr_name, QualPath(path, attr_name))
            continue
        
        # Translate methods
        if attr_value_type in METHOD_TYPES:
            attr_value_func = getattr(attr_value, '__func__', None)
            # Cpython, check. Let built in methods trough.
            if (attr_value_func is not None):
                attr_value = attr_value_func
                attr_value_type = attr_value.__class__
        
        # Check types.
        if issubclass(attr_value_type, type):
            if attr_value in IGNORED_TYPES:
                continue
            
            attr_value_path = QualPath(attr_value.__module__, attr_value.__qualname__)
            
            if attr_value_path.parent == path:
                attr_name = attr_value.__name__
                references[attr_name] = TypeUnit(attr_name, attr_value_path, attr_value)
            
            continue
        
        # Check functions.
        if attr_value_type in FUNCTION_TYPES:
            if attr_value in IGNORED_FUNCTIONS:
                continue
            
            try:
                if from_type:
                    name = attr_value.__qualname__
                else:
                    name = attr_value.__name__
            except AttributeError:
                name = attr_value_type.__name__
            
            if name == '<lambda>':
                name = attr_name
            
            attr_value_module = getattr(attr_value, '__module__', None)
            if attr_value_module is None:
                # Builtin stuff, skip
                continue
            
            attr_value_path = QualPath(attr_value_module, name)
            
            if from_type or attr_value_path.parent == path:
                if from_type:
                    alternative_path = QualPath(path, attr_name)
                else:
                    alternative_path = None
                
                references[attr_name] = FunctionUnit(name, attr_value_path, attr_value, alternative_path)
            
            continue
        
        if not from_type:
            continue
        
        if attr_value_type in PROPERTY_TYPES:
#            for name in ('fget', 'fset', 'fdel'):
#                func = getattr(attr_value,name, None)
#                if (func is not None):
#                    break
#            else:
#                continue
#
#            try:
#                name = func.__name__
#            except AttributeError:
#                name = func.__class__.__name__
#
#            if name == '<lambda>':
#                name = attr_name
            
            references[attr_name] = PropertyUnit(attr_name, QualPath(path, attr_name), attr_value)
            continue
        
        if attr_value_type in ATTRIBUTE_TYPES:
            name = attr_value.__name__
            references[attr_name] = InstanceAttributeUnit(name, QualPath(path, attr_name))
            continue
        
        if attr_value_type in IGNORED_CLASS_ATTRIBUTE_TYPES:
            continue
        
        if attr_name in IGNORED_CLASS_ATTRIBUTE_NAMES:
            continue
        
        references[attr_name] = ClassAttributeUnit(attr_name, QualPath(path, attr_name))
        continue

MAPPED_OBJECTS = {}

class UnitBase:
    """
    Base class for representative units.
    
    Attributes
    ----------
    _cache : `dict`
        Cache used by cached properties.
    _docs : `None` or ``DocString``
        The processed docstring of the represented unit.
    _docs_parsed : `bool`
        Whether the represented unit's docstring was already processed.
    name : `str`
        The name of the unit.
    path : ``QualPath``
        The path to the unit.
    """
    __slots__ = ('_cache', '_docs', '_docs_parsed', 'name', 'path',)
    
    def __repr__(self):
        """Returns the unit's representation."""
        return f'<{self.__class__.__name__} name={self.name}, path={self.path!s}>'
    
    @property
    def parent(self):
        """
        Returns the parent object of the unit if applicable.
        
        Returns
        -------
        parent : `None` or ``UnitBase`` instance
        """
        parent_path = self.path.parent
        if parent_path:
            parent = MAPPED_OBJECTS.get(parent_path, None)
        else:
            parent = None
        
        return parent
    
    def _get_raw_docstring(self):
        """
        Returns the raw docstring of the represented object.
        
        Returns
        -------
        docstring : `None` or ``DocString``
        """
        return None
    
    @property
    def docs(self):
        """
        Returns the docs of the represented object.
        
        Returns
        -------
        docstring : `None` or ``DocString``
        """
        docs_parsed = self._docs_parsed
        if docs_parsed:
            docstring = self._docs
        else:
            self._docs = docstring = self._get_raw_docstring()
            self._docs_parsed = True
        
        return docstring
    
    @cached_property
    def text(self):
        """
        Renders the docstring of the respective object to one big chunk of string.
        
        Returns
        -------
        docs : `None` or `str`
        """
        docs = self.docs
        if docs is None:
            return None
        
        return serialize_docs(docs)
    
    @cached_property
    def source_text(self):
        """
        Renders the docstring of the respective object to source-like format.
        
        Returns
        -------
        docs : `None` or `str`
        """
        docs = self.docs
        if docs is None:
            return None
        
        return serialize_docs_source_text(docs)
    
    @cached_property
    def embed_sized(self):
        """
        Renders the docstring of the respective object to chunks of string with max size of embed description.
        
        Returns
        -------
        docs : `None` or `list` of `str`
        """
        docs = self.docs
        if docs is None:
            return None
        
        return serialize_docs_embed_sized(docs)
    
    @cached_property
    def html(self):
        """
        Renders the docstring to html.
        
        Returns
        -------
        docs : `None` or `str`
        """
        docs = self.docs
        if docs is None:
            return None
        
        return html_serialize_docs(docs, self)
    
    @cached_property
    def html_extended(self):
        """
        Renders the extended docstring to html.
        
        This html renderer will not only render only the given docstring, but render it with the other related
        docstrings.
        
        Returns
        -------
        html_extended : `None` or `str`
        """
        html_extended, _ = html_serialize_docs_extended(self, True, False)
        return html_extended
    
    @cached_property
    def html_extended_structure(self):
        """
        Creates the extended docstring's html structure.
        
        Returns
        -------
        html_extended_structure : `None` or ``Structure``
        """
        _, html_extended_structure = html_serialize_docs_extended(self, False, True)
        return html_extended_structure
    
    @cached_property
    def html_extended_with_structure(self):
        """
        Renders the extended docstring to html and returns it's structure as well.
        
        Returns
        -------
        html_extended : `None` or `str`
        html_extended_structure : `None` or ``Structure``
        """
        html_extended, html_extended_structure = html_serialize_docs_extended(self, True, True)
        cache = self._cache
        cache['html_extended'] = html_extended
        cache['html_extended_structure'] = html_extended_structure
        return html_extended, html_extended_structure
        
    @cached_property
    def preview(self):
        """
        Generates preview for the docstring.
        
        Returns
        -------
        preview : `None` or `str`
        """
        docs = self.docs
        if docs is None:
            return None
        
        return generate_preview_for(docs)
    
    def lookup_reference(self, reference):
        """
        Returns the closest unit to the given unit.
        
        Returns
        -------
        referred : `None` or ``UnitBase`` instance.
            The referenced unit.
        """
        # Is local reference?
        reference_parts = reference.split('.')
        if not reference_parts:
            return None
        
        reference_parts.reverse()
        
        if not reference_parts[-1]:
            del reference_parts[-1]
            if isinstance(self, FolderedUnit):
                if not reference_parts:
                    return self
                
                object_ = direct_lookup_in(self, reference_parts)
                if (object_ is not None):
                    return object_
            
            parent = MAPPED_OBJECTS.get(self.path.parent, None)
            if parent is None:
                return self
            else:
                return direct_lookup_in(parent, reference_parts)
        
        if isinstance(self, FolderedUnit):
            object_ = lookup_from(self, reference_parts)
            if (object_ is not None):
                return object_
        
        path = self.path.parent
        while path:
            parent = MAPPED_OBJECTS.get(path, None)
            if parent is None:
                break
            
            object_ = lookup_from(parent, reference_parts)
            if (object_ is not None):
                return object_
            
            path = path.parent

def direct_lookup_in(object_, reference_parts):
    """
    Looks up the given reference in a foldered unit directly.
    
    Parameters
    ----------
    object_ : ``UnitBase`` instance
        The folder to lookup up from.
    reference_parts : `list` of `str`
        Reference parts to lookup.
        
        The given `reference_parts` should be reversed from their original state.
    
    Returns
    -------
    object_ : `None` or ``UnitBase``
        The found object if applicable.
    """
    reference_parts = reference_parts.copy()
    
    while reference_parts:
        if not isinstance(object_, FolderedUnit):
            # Cannot move further deep, the reference will not be found, leave.
            object_ = None
            break
        
        reference_part = reference_parts.pop()
        try:
            object_ = object_.references.get(reference_part, None)
        except KeyError:
            # Reference not be found, leave.
            object_ = None
            break
    
    # No more reference part to lookup, return object.
    return object_

def lookup_from(folder, reference_parts):
    """
    Looks up the given reference in the folder and in all of it's sub-folders.
    
    Parameters
    ----------
    folder : ``UnitBase`` instance
        The folder to look up from.
    reference_parts : `list` of `str`
        Reference parts to lookup.
        
        The given `reference_parts` should be reversed from their original state.
    
    Returns
    -------
    object_ : ``UnitBase``
        The unit-base if anything found matching the references.
    """
    if not isinstance(folder, FolderedUnit):
        return None
    
    search_for = reference_parts[-1]
    
    for sub_name, sub_object in folder.references.items():
        if sub_name == search_for:
            if len(reference_parts)  == 1:
                return sub_object
            else:
                object_ = lookup_from(sub_object, reference_parts[:-1])
        else:
            object_ = lookup_from(sub_object, reference_parts)
        
        if object_ is not None:
            return object_
    
    return None

class AttributeUnitBase(UnitBase):
    """
    Base unit class for attributes.
    
    Attributes
    ----------
    _cache : `dict`
        Cache used by cached properties.
    _docs : `None` or ``DocString``
        The processed docstring of the represented unit.
    _docs_parsed : `bool`
        Whether the represented unit's docstring was already processed.
    name : `str`
        The name of the unit.
    path : ``QualPath``
        The path to the unit.
    """
    __slots__ = ()
    
    def _get_raw_docstring(self):
        """
        Returns the raw docstring of the represented object.
        
        Returns
        -------
        docstring : `None` or ``DocString``
        """
        parent = MAPPED_OBJECTS.get(self.path.parent, None)
        if parent is None:
            return None
        
        docstring = parent.docs
        if docstring is None:
            return None
        
        return docstring.attribute_docstring_for(self.name)


class InstanceAttributeUnit(AttributeUnitBase):
    """
    Represents an instance attribute.
    
    Attributes
    ----------
    _cache : `dict`
        Cache used by cached properties.
    _docs : `None` or ``DocString``
        The processed docstring of the represented unit.
    _docs_parsed : `bool`
        Whether the represented unit's docstring was already processed.
    name : `str`
        The name of the attribute.
    path : ``QualPath``
        The path to the attribute.
    """
    __slots__ = ()
    def __new__(cls, name, path):
        """
        Creates a new instance attribute unit from the given parameters.
        
        Parameters
        ----------
        name : `str`
            The name of the attribute
        path : ``QualPath``
            The path to the attribute.
        """
        self = object.__new__(cls)
        self.name = name
        self.path = path
        self._cache = {}
        self._docs = None
        self._docs_parsed = False
        MAPPED_OBJECTS[path] = self
        
        return self


class ClassAttributeUnit(AttributeUnitBase):
    """
    Represents a class attribute
    
    Attributes
    ----------
    name : `str`
        The name of the attribute.
    path : ``QualPath``
        The path to the attribute.
    """
    __slots__ = ()
    def __new__(cls, name, path):
        """
        Creates a new class attribute unit from the given parameters.
        
        Parameters
        ----------
        name : `str`
            The name of the attribute
        path : ``QualPath``
            The path to the attribute.
        """
        self = object.__new__(cls)
        self.name = name
        self.path = path
        self._cache = {}
        self._docs = None
        self._docs_parsed = False
        
        MAPPED_OBJECTS[path] = self
        
        return self


class ObjectedUnitBase(UnitBase):
    """
    Base class for docstrings, which store their represented object.

    Attributes
    ----------
    _cache : `dict`
        Cache used by cached properties.
    _docs : `None` or ``DocString``
        The processed docstring of the represented unit.
    _docs_parsed : `bool`
        Whether the represented unit's docstring was already processed.
    name : `str`
        The name of the unit.
    path : ``QualPath``
        The path to the unit.
    object : `property-like`
        The represented property.
    """
    __slots__ = ('object', )
    
    def _get_raw_docstring(self):
        """
        Returns the raw docstring of the represented object.
        
        Returns
        -------
        docstring : `None` or ``DocString``
        """
        docstring = getattr(self.object, '__doc__', 'None')
        if docstring is None:
            return None
        
        if type(docstring) is not str:
            return None
        
        return DocString(docstring, self.path)


class PropertyUnit(ObjectedUnitBase):
    """
    Represents a property.
    
    Attributes
    ----------
    _cache : `dict`
        Cache used by cached properties.
    _docs : `None` or ``DocString``
        The processed docstring of the represented unit.
    _docs_parsed : `bool`
        Whether the represented unit's docstring was already processed.
    name : `str`
        The name of the unit.
    path : ``QualPath``
        The path to the unit.
    object : `property-like`
        The represented property.
    """
    __slots__ = ()
    def __new__(cls, name, path, obj):
        """
        Creates a new property unit from the given parameters.
        
        Parameters
        ----------
        name : `str`
            The name of the property.
        path : ``QualPath``
            The path to the property.
        obj : `property-like`
            The represented property.
        """
        self = object.__new__(cls)
        self.name = name
        self.path = path
        self.object = obj
        self._cache = {}
        self._docs = None
        self._docs_parsed = False
        
        MAPPED_OBJECTS[path] = self
        
        return self


class FunctionUnit(ObjectedUnitBase):
    """
    Represents a function or method.
    
    Attributes
    ----------
    _cache : `dict`
        Cache used by cached properties.
    _docs : `None` or ``DocString``
        The processed docstring of the represented unit.
    _docs_parsed : `bool`
        Whether the represented unit's docstring was already processed.
    name : `str`
        The name of the function.
    path : ``QualPath``
        The path to the function.
    object : `function-like`
        The represented function.
    """
    __slots__= ()
    def __new__(cls, name, path, obj, alternative_path):
        """
        Creates a new property unit from the given parameters.
        
        Parameters
        ----------
        name : `str`
            The name of the function.
        path : ``QualPath``
            The path to the function.
        obj : `property-like`
            The represented function.
        alternative_path : `None` or ``QualPath``
            Alternative path to the function.
            
            Methods might show up at multiple places, so more access path can be added to them.
        """
        try:
            self = MAPPED_OBJECTS[path]
        except KeyError:
            self = object.__new__(cls)
            self.name = name
            self.path = path
            self.object = obj
            self._cache = {}
            self._docs = None
            self._docs_parsed = False
            
            MAPPED_OBJECTS[path] = self
        
        if (alternative_path is not None) and (alternative_path != path):
            MAPPED_OBJECTS[alternative_path] = self
        
        return self


class FolderedUnit(ObjectedUnitBase):
    """
    Represents a foldered unit.
    
    Attributes
    ----------
    _cache : `dict`
        Cache used by cached properties.
    _docs : `None` or ``DocString``
        The processed docstring of the represented unit.
    _docs_parsed : `bool`
        Whether the represented unit's docstring was already processed.
    name : `str`
        The name of the unit.
    path : ``QualPath``
        The path to the unit.
    references : `dict` of (`str`, `UnitBase`) items
        The references of the object to the objects contained by itself. AKA it's folder.
    object : `property-like`
        The represented unit.
    """
    __slots__ = ('references',)
    
    def __repr__(self):
        """Returns the foldered unit's representation."""
        return (f'<{self.__class__.__name__} name={self.name!r}, path={self.path!s}, reference count='
            f'{len(self.references)!r}>')


class TypeUnit(FolderedUnit):
    """
    Represents a type.
    
    Attributes
    ----------
    _cache : `dict`
        Cache used by cached properties.
    _docs : `None` or ``DocString``
        The processed docstring of the represented unit.
    _docs_parsed : `bool`
        Whether the represented unit's docstring was already processed.
    name : `str`
        The name of the type.
    path : ``QualPath``
        The path to the type.
    references : `dict` of (`str`, `UnitBase`) items
        The references of the type to the objects contained by itself. AKA it's folder.
    object : `type`
        The represented type.
    """
    __slots__ = ()
    def __new__(cls, name, path, obj):
        """
        Creates a new type unit from the given `obj`.
        
        Parameters
        ----------
        name : `str`
            The name of the type.
        path : ``QualPath``
            The path to the type.
        obj : `type`
            The represented type.
        """
        references = {}
        map_types_and_functions(obj, references, path, True)
        
        self = object.__new__(cls)
        self.name = name
        self.path = path
        self.references = references
        self.object = obj
        self._cache = {}
        self._docs = None
        self._docs_parsed = False
        
        MAPPED_OBJECTS[path] = self
        
        return self


class ModuleUnit(FolderedUnit):
    """
    Represents a module.
    
    Attributes
    ----------
    _cache : `dict`
        Cache used by cached properties.
    _docs : `None` or ``DocString``
        The processed docstring of the represented unit.
    _docs_parsed : `bool`
        Whether the represented unit's docstring was already processed.
    name : `str`
        The name of the module.
    path : ``QualPath``
        The path to the module.
    references : `dict` of (`str`, `UnitBase`) items
        The references of the module to the objects contained by itself. AKA it's folder.
    object : `module`
        The represented module.
    """
    __slots__= ()
    def __new__(cls, name, path, obj, modules):
        """
        Creates a new module unit from the given `obj`.
        
        Parameters
        ----------
        name : `str`
            The name of the module.
        path : ``QualPath``
            The path to the module.
        obj : `module`
            The represented module.
        """
        references = {}
        
        base_path_pattern = re.compile(f'{re.escape(str(path))}\.([^\.]+)')
        for module_object in modules:
            parsed = base_path_pattern.fullmatch(module_object.__name__)
            if parsed is None:
                continue
            
            sub_module_name = parsed.group(1)
            references[sub_module_name] = \
                ModuleUnit(sub_module_name, path / sub_module_name, module_object, modules)
        
        map_types_and_functions(obj, references, path, False)
        
        self = object.__new__(cls)
        self.name = name
        self.path = path
        self.references = references
        self.object = obj
        self._cache = {}
        self._docs = None
        self._docs_parsed = False
        
        MAPPED_OBJECTS[path] = self
        
        return self

def map_module(module_name):
    """
    Maps the given module by it's name and returns the root.
    
    Attributes
    ----------
    module_name : `str`
        The module's name.
    
    Returns
    -------
    root : ``ModulePath``
    
    Raises
    ------
    LookupError
        No loaded module is named as `module_name`.
    """
    try:
        return MAPPED_OBJECTS[module_name]
    except KeyError:
        pass
    
    try:
        main_module_object = sys.modules[module_name]
    except KeyError:
        raise LookupError(f'No loaded module is named as {module_name!r}') from None
    
    # Invalidate search cache
    CachedSearcher._cache_valid = False
    
    module_name_pattern = re.compile(f'{re.escape(module_name)}(?:\..*)?')
    modules = []
    
    for name, module_object in sys.modules.items():
        if (module_name_pattern.fullmatch(name) is not None):
            modules.append(module_object)
    
    with warnings.catch_warnings():
        warnings.simplefilter('ignore')
        root = ModuleUnit(module_name, QualPath(module_name), main_module_object, modules)
    
    return root


def qual_path_sort_key(qual_path):
    """
    Sort key to sort ``QualPath`` instances.
    
    Parameters
    ----------
    qual_path : ``QualPath``
        The respective path.
    
    Returns
    -------
    sort_key : `str`
    """
    return str(qual_path)


class CachedSearcher:
    """
    Cached unit path searcher.
    
    Class Attributes
    ----------------
    _cached_relations : `dict` of (``QualPath``, (``QualPath`` or `list` of ``QualPath``)) items
        Path shortening, path relations used when translating found patches back.
    _cached_possibilities : `list` of `str`
        The cached possibilities.
    _cache_valid : `bool`
        Whether the searcher cache is valid.
    """
    _cache_valid = True
    _cached_relations = {}
    _cached_possibilities = []
    
    @classmethod
    def _prepare(cls):
        """
        Prepares the cache of the searcher.
        
        Returns
        -------
        possibilities : `list` of `str`
            The cached possibilities.
        relations : `dict` of (``QualPath``, (``QualPath`` or `list` of ``QualPath``)) items
            Path shortening, path relations used when translating found patches back.
        """
        cached_relations = cls._cached_relations
        cached_possibilities = cls._cached_possibilities
        if not cls._cache_valid:
            cached_relations.clear()
            cached_possibilities.clear()
            for path in MAPPED_OBJECTS:
                path_parts = path.parts
                for starter in range(len(path_parts)):
                    local_path = '.'.join(path_parts[starter:])
                    
                    try:
                        collected_paths = cached_relations[local_path]
                    except KeyError:
                        cached_relations[local_path] = path
                        cached_possibilities.append(local_path)
                    else:
                        if isinstance(collected_paths, QualPath):
                            cached_relations[local_path] = [collected_paths, path]
                        else:
                            collected_paths.append(path)
            
            for value in cached_relations.values():
                if isinstance(value, QualPath):
                    continue
                
                value.sort(key=qual_path_sort_key)
        
        return cached_possibilities, cached_relations
    
    def __call__(self, value, limit=100):
        """
        Returns all the found units with the given value in their path. Case insensitive.
        
        Parameters
        ----------
        value : `str`
            The query string.
        
        Returns
        -------
        paths : `list` of ``QualPath``
        """
        possibilities, relations = self._prepare()
        
        matcheds = get_close_matches(value, possibilities, n=limit, cutoff=0.60)
        
        paths = []
        collected = set()
        
        for matched in matcheds:
            path = relations[matched]
            if isinstance(path, QualPath):
                if path in collected:
                    continue
                
                collected.add(path)
                paths.append(path)
            else:
                for path in path:
                    if path in collected:
                        continue
                    
                    collected.add(path)
                    paths.append(path)
        
        del paths[limit:]
        
        return paths


search_paths = CachedSearcher()
