import fnmatch
import inspect
import os
from importlib.util import spec_from_file_location

import logging
import sys

__dirname, __init_python_script = os.path.split(os.path.abspath(__file__))

logger = logging.getLogger("SimplePlugins")

formatter = logging.Formatter('%(asctime)s %(name)-12s %(levelname)-8s %(message)s')
console_log_handler = logging.StreamHandler(stream=sys.stdout)
console_log_handler.setFormatter(formatter)
console_log_handler.setLevel(logging.DEBUG)

logger.addHandler(console_log_handler)
logger.setLevel(logging.DEBUG)


def get_files_recursive(path, match='*.py'):
    """
    Perform a recursive search to find all the files matching the
    specified search criteria.
    :param path: Path to begin the recursive search.
    :param match: String/Regex used to match files with a pattern.
    :return: Full path of all the files found.
    :rtype: list
    """
    matches = []
    for root, dirnames, filenames in os.walk(path):
        for filename in fnmatch.filter(filenames, match):
            matches.append(os.path.join(root, filename))

    return matches


def get_filename(file):
    """
    Safe method to retrieve only the name of the file.
    :param file: Path of the file to retrieve the name from.
    :return: None if the file is non-existant, otherwise the filename (extension included)
    :rtype: None, str
    """
    if not os.path.exists(file):
        return None
    return "%s%s" % os.path.splitext(file)


def import_module_from_file(full_path_to_module):
    """
    Import a module given the full path/filename of the .py file
    Python 3.4
    """
    if inspect.ismodule(full_path_to_module):
        return full_path_to_module

    module = None

    try:

        # Get module name and path from full path
        module_dir, module_file = os.path.split(full_path_to_module)
        module_name, module_ext = os.path.splitext(module_file)

        # Get module "spec" from filename
        spec = spec_from_file_location(module_name, full_path_to_module)

        module = spec.loader.load_module()

    except Exception as ec:
        # Simple error printing
        # Insert "sophisticated" stuff here
        print(ec)

    finally:
        return module


class PluginException(Exception):
    pass


class Plugin(object):
    """
    Base class that all plugins derive from.
    """

    def __init__(self, **kwargs):
        self.name = kwargs.pop('name', self.__class__.__name__)
        self.version = kwargs.pop('version', "1.0.0")
        self.description = kwargs.pop('description', "No Description Available")
        self.active = False

    def activate(self):
        """
        Operations to perform whenever the plugin is activated.
        :return:
        """
        raise NotImplementedError("Activation for %s is not implemented" % self.name)

    def deactivate(self):
        """
        Operations to perform whenever the plugin is deactivated.
        :return:
        """
        raise NotImplementedError("Deactivation for %s is not implemented" % self.name)

    def perform(self, **kwargs):
        """
        Operations that will be performed when invoked.
        This method is where the actual "use" logic of plugins will be defined.
        :param kwargs:
        :return:
        """
        raise NotImplementedError("Perform for %s is not implemented" % self.name)


class PluginManager(object):
    """
    Holds instances, and information for each plugin.
    Provides functionality to interact, activate, deactivate, perform, and operate with or on plugins.
    """

    def __init__(self):
        self.plugins = {}

    def register(self, plugin=None, plugin_file=None, directory=None, skip_types=None, override=False, activate=True):
        """
        Register a plugin, or plugins to be managed and recognized by the plugin manager.
        Will take a plugin instance, file where a plugin / plugin(s) reside, parent directory
        that holds plugin(s), or sub-folders with plugin(s).

        Will optionally "activate" the plugins, and perform any operations defined in their "activate" method.

        :param plugin: Plugin Instance to register.
        :param plugin_file: str: File (full path) to scan for Plugins.
        :param directory: str: Directory to perform a recursive scan on for Plugins.
        :param skip_types: list: Types of plugins to skip when found, during a scan / search.
        :param override: bool: Whether or not to override registered plugin when it's being registered again.
        :param activate: bool: Whether or not to activate the plugins upon registration.
        :return: Does not Return.
        """
        # Double verify that there's types to skip. We don't want to register "Base" types (Plugin)

        if not isinstance(skip_types, list):
            skip_types = [skip_types]
            logger.debug("Skip Types must be a list. Created list with values passed.")

        if skip_types is None:
            skip_types = [Plugin]
        else:
            skip_types.append(Plugin)

        # Check if they've passed a method of registration!
        if plugin is None and plugin_file is None and directory is None:
            raise PluginException("Unable to perform registration without a plugin, module, or directory.")

        # First we'll check if they're registering via directory (Scanning)
        # as it might be best for bigger applications / apps with many plugins to register them via
        # a folder, where plugins are expected!
        if directory is not None:
            plugins_in_dir = PluginManager.scan_for_plugins(directory)
            # Loop through all the plugins in the directory, associated by file -> list[] (or none)
            for file, plugins in plugins_in_dir.items():
                # If there's no plugins in that file then just continue.
                if plugins is None:
                    continue

                for plugin in plugins:
                    # If there's a duplicate plugin and we're not overriding, then we'll skip it.
                    if plugin.name in self.plugins:
                        if not override:
                            logger.warn("Failed to register %s: Duplicate plugin found!" % plugin.name)
                            continue

                    # Now verify if we're supposed to skip the type of the plugin that's being attempted to register.
                    # Useful when plugins classes extend a base-class (Plugin, for example)
                    # but you don't want to register the base class.
                    if type(plugin) in skip_types:
                        logger.warn(
                            "Skipping registration of %s, as it's not to be registered." % plugin.__class__.__name__)
                        continue

                    # Assign the plugin (via name) to the dictionary of registered plugins
                    self.plugins[plugin.name] = plugin
                    # Give a little output of the plugin!
                    logger.debug("Registered plugin %s from %s in %s" % (plugin.name, file, directory))

                    # Then if we're going to activate the plugin, do so!
                    if activate:
                        self.plugins[plugin.name].activate()

        # Now we're going to check if they're registering the plugins
        # either by file, or module
        if plugin_file is not None:
            # If the plugin_file is not a module, then we're going to verify the file actually exists!
            if not inspect.ismodule(plugin_file):
                # Verify if there's a ~ (Home dir call) inside the path, and if so then expand it.
                plugin_file = os.path.expanduser(plugin_file)
                # Then verify if the path of the plugin exists, raising an exception if not!
                if not os.path.exists(plugin_file):
                    raise FileNotFoundError("Unable to locate file %s" % plugin_file)

            # Next after verifying, we get all the plugins inside the file or module.`
            plugins_in_file = PluginManager.get_plugins_in_module(plugin_file)
            # If there's no plugins inside, then we're going to throw an exception. There's nothing to register in here.
            if plugins_in_file is None or len(plugins_in_file) == 0:
                raise PluginException("Unable to locate plugins inside %s" % plugin_file)

            # Loop through every plugin inside the file/module and attempt to register it.
            for fplugin in plugins_in_file: