import sys
import six

from pike.discovery import filesystem
from pike.discovery import py
from pike.finder import PikeFinder


class PikeManager(object):
    def __init__(self, search_paths=None):
        """The Pike plugin manager

        The manager allows for the dynamic loading of Python packages for any
        location on a user's filesystem.

        :param list search_paths: List of path strings to include during module
            importing. These paths are only in addition to existing Python
            import search paths.


        Using PikeManager as a context manager:

        .. code-block:: python

            from pike.manager import PikeManager

            with PikeManager(['/path/containing/package']) as mgr:
                import module_in_the_package

        Using PikeManager instance:

        .. code-block:: python

            from pike.manager import PikeManager

            mgr = PikeManager(['/path/container/package'])
            import module_in_the_package
            mgr.cleanup()
        """
        self.search_paths = search_paths or []
        self.module_finder = PikeFinder(search_paths)
        self.add_to_meta_path()

    def cleanup(self):
        """Removes Pike's import hooks

        This should be called if an implementer is not using the manager as
        a context manager.
        """
        if sys.meta_path and self.module_finder in sys.meta_path:
            sys.meta_path.remove(self.module_finder)

    def add_to_meta_path(self):
        """Adds Pike's import hooks to Python

        This should be automatically handled by Pike; however, this is method
        is accessible for very rare use-cases.
        """
        if self.module_finder in sys.meta_path:
            return

        if six.PY3:
            sys.meta_path.insert(0, self.module_finder)
        else:
            sys.meta_path.append(self.module_finder)

    def get_classes(self, filter_func=None):
        """Get all classes within modules on the manager's search paths

        :param Function filter_func: Custom filter function(cls_obj).
        :returns: :class:`List` of all found classes
        """
        all_classes = []
        # Top-most Modules
        for module_name in self.get_module_names():
            module = py.get_module_by_name(module_name)
            all_classes.extend(py.classes_in_module(module, filter_func))

        # All packages
        for module_name in self.get_package_names():
            module = py.get_module_by_name(module_name)
            all_classes.extend(py.get_all_classes(module, filter_func))

        return all_classes

    def get_all_inherited_classes(self, base_class):
        """Retrieve all inherited classes from manager's search paths

        :param Class base_class: Base class to filter results by
        :return: :class:`List` of all found classes
        """
        all_classes = []
        # Top-most Modules
        for module_name in self.get_module_names():
            module = py.get_module_by_name(module_name)
            all_classes.extend(py.get_inherited_classes(module, base_class))

        # All packages
        for module_name in self.get_package_names():
            module = py.get_module_by_name(module_name)
            inherited = py.get_all_inherited_classes(module, base_class)
            all_classes.extend(inherited)

        return all_classes

    def get_module_names(self):
        """Get root module names available on the manager's search paths

        :returns: :class:`generator` providing available module names.
        """
        for path in self.search_paths:
            for package_path in filesystem.find_modules(path):
                yield filesystem.get_name(package_path)

    def get_package_names(self):
        """Get root package names available on the manager's search paths

        :returns: :class:`generator` providing available package names.
        """
        for path in self.search_paths:
            for package_path in filesystem.find_packages(path):
                yield filesystem.get_name(package_path)

    def __enter__(self):
        return self

    def __exit__(self, type, value, traceback):
        self.cleanup()

    def __del__(self):
        self.cleanup()
