from dexy.exceptions import InternalDexyProblem
from dexy.exceptions import UserFeedback
from dexy.filter import DexyFilter
import imp
import inspect
import json
import os
import pkgutil
import sys
import sqlite3
from unittest.case import SkipTest

class PythonIntrospection(DexyFilter):
    """
    Base class for classes which use python introspection.
    """
    import_err_msg = "Could not import '%s' received err: %s"
    aliases = []
    _settings = {
            'error-on-import-fail' : (
                "Should an exception be raised if importing a specified module or file fails?",
                False
                ),
            'input-extensions' : ['.txt', '.py'],
            'data-type' : 'keyvalue',
            'output-extensions' : ['.sqlite3', '.json']
            }

    def add_workspace_to_sys_path(self):
        sys.path.append(self.parent_work_dir())
        sys.path.append(self.workspace())

    def handle_fail(self, name, e):
        msg = self.import_err_msg % (name, e)
        if self.setting('error-on-import-fail'):
            raise UserFeedback(msg)
        else:
            self.log_debug(e)

    def load_module(self, name):
        try:
            __import__(name)
            return sys.modules[name]
        except (ImportError, TypeError) as e:
            self.handle_fail(name, e)

    def load_source_file(self):
        self.populate_workspace()
        self.add_workspace_to_sys_path()

        name = self.input_data.name
        target = os.path.join(self.workspace(), name)
        try:
            return imp.load_source("dummy", target)
        except (ImportError, SkipTest) as e:
            self.handle_fail(name, e)

class Pydoc(PythonIntrospection):
    """
    Returns introspected python data in key-value storage format.

    Where input is a .txt file, this is assumed to be the name of an installed
    python module.

    Where input is a .py file, the file itself is loaded and parsed.
    """
    aliases = ["pydoc"]
    _settings = {
            'additional-dirs' : ("Additional source directories to load, relative to package root. Useful for tests/", [])
            }

    def append_item_content(self, key, item):
        self.log_debug("appending content for %s" % key)

        try:
            source = inspect.getsource(item)
            self.output_data.append("%s:source" % key, source)
        except (TypeError, IOError, sqlite3.ProgrammingError):
            pass

        try:
            doc = inspect.getdoc(item)
            self.output_data.append("%s:doc" % key, doc)
        except (TypeError, IOError, sqlite3.ProgrammingError):
            pass

        try:
            comment = inspect.getcomments(item)
            self.output_data.append("%s:comments" % key, comment)
        except (TypeError, IOError, sqlite3.ProgrammingError):
            pass

        try:
            value = json.dumps(item)
            self.output_data.append("%s:value" % key, value)
        except TypeError:
            pass

    def is_defined_in_module(self, mod, mod_name, item):
        if mod_name and hasattr(item, '__module__'):
            return item.__module__.startswith(mod_name)
        else:
            return True

    def process_members(self, mod):
        mod_name = mod.__name__

        if mod_name == 'dummy':
            mod_name = None

        for k, m in inspect.getmembers(mod):
            if mod_name:
                key = "%s.%s" % (mod_name, k)
            else:
                key = k

            is_class = inspect.isclass(m)
            is_def = self.is_defined_in_module(mod, mod_name, m)

            if not is_def:
                # this is something imported, not defined in the module
                # so we don't want to document it here
                self.log_debug("skipping %s for module %s" % (k, mod_name))
                continue

            if not is_class:
                self.append_item_content(key, m)

            else:
                self.append_item_content(key, m)
                for ck, cm in inspect.getmembers(m):
                    self.append_item_content("%s.%s" % (key, ck), cm)

    def process_module(self, package_name, name):
        self.log_debug("processing module %s" % name)
        mod = self.load_module(name)
        self.append_item_content(name, mod)
        if mod:
            self.process_members(mod)
        else:
            self.log_warn("no mod from %s" % name)

    def process_package(self, package):
        """
        Iterates over all modules included in the package and processes them.
        """
        self.log_debug("processing package %s" % package)
        package_name = package.__name__

        # Process top level package
        self.process_module(package_name, package_name)

        # Process sub-packages and modules
        if hasattr(package, '__path__'):
            path = package.__path__
            prefix = "%s." % package_name
            for loader, name, ispkg in pkgutil.walk_packages(path, prefix=prefix):
                self.process_module(package_name, name)

    def process_packages(self):
        package_names = unicode(self.input_data).split()
        packages = [__import__(name) for name in package_names]

        for package in packages:
            self.process_package(package)

    def process_file(self):
        mod = self.load_source_file()
        if mod:
            self.process_members(mod)

    def process(self):
        if self.prev_ext == '.txt':
            self.process_packages()
        elif self.prev_ext == '.py':
            self.process_file()
        else:
            raise InternalDexyProblem("Should not have ext %s" % self.prev_ext)

        self.output_data.save()
