#! /usr/bin/env python
# -*- coding: utf-8 -*-

"""
Module that contains functions to handle Python modules
"""

from __future__ import print_function, division, absolute_import

import os
import sys
import uuid
import pkgutil
import inspect
import logging
import importlib
import traceback

from tpDcc.libs.python import python, path as path_utils

if python.is_python3():
    from importlib.machinery import SourceFileLoader
else:
    import imp

LOGGER = logging.getLogger('tpDcc-libs-python')


def is_dotted_module_path(module_path):
    """
    Returns whether given module path is a dotted one (tpDcc.libs.python.modules) or not
    :param module_path: str
    :return: bool
    """

    return len(module_path.split('.')) > 2


def convert_to_dotted_path(path):
    """
    Returns a dotted path relative to the given path
    :param path: str, (eg. randomPath/folder/test.py)
    :return: str, dotted path (eg. folder.test)
    """

    directory, file_path = os.path.split(path)
    directory = path_utils.clean_path(directory)
    file_name = os.path.splitext(file_path)[0]
    package_path = [file_name]
    sys_path = list(set([path_utils.clean_path(p) for p in sys.path]))

    # We ignore current working directory. Useful if we want to execute tools directly inside PyCharm
    current_work_dir = path_utils.clean_path(os.getcwd())
    if current_work_dir in sys_path:
        sys_path.remove(current_work_dir)

    drive_letter = os.path.splitdrive(path)[0] + '\\'
    while directory not in sys_path:
        directory, name = os.path.split(directory)
        directory = path_utils.clean_path(directory)
        if directory == drive_letter or name == '':
            return ''
        package_path.append(name)

    return '.'.join(reversed(package_path))


def import_module(module_path, name=None, skip_warnings=False, skip_errors=False):
    """
    Imports the given module path. If the given module path is a dotted one, import lib will be used. Otherwise, it's
    expected that given module path is the absolute path to the source file. If name argument is not given, then the
    basename without the extension will be used
    :param module_path: str, module path. Can be a dotted path (tpDcc.libs.python.modules) or an absolute one
    :param name: str, name for the imported module which will be used if the module path is an absolute path
    :param skip_warnings: bool, Whether or not warnings should be skipped
    :param skip_errors: bool, Whether or not errors should be skipped
    :return: ModuleObject, imported module object
    """

    if is_dotted_module_path(module_path) and not os.path.exists(module_path):
        try:
            return importlib.import_module(module_path)
        except ImportError:
            if not skip_errors:
                LOGGER.error('Failed to load module: "{}"'.format(module_path), exc_info=True)
            return None

    try:
        if os.path.exists(module_path):
            if not name:
                name = os.path.splitext(os.path.basename(module_path))[0]
            if name in sys.modules:
                return sys.modules[name]
        if not name:
            if not skip_warnings:
                LOGGER.warning(
                    'Impossible to load module because module path: {} was not found!'.format(module_path))
            return None
        if os.path.isdir(module_path):
            module_path = os.path.join(module_path, '__init__.py')
            if not os.path.exists(module_path):
                raise ValueError('Cannot find module path: "{}"'.format(module_path))
        if python.is_python3():
            return SourceFileLoader(name, os.path.realpath(module_path)).load_module()
        else:
            if module_path.endswith('.py'):
                return imp.load_source(name, os.path.realpath(module_path))
            elif module_path.endswith('.pyc'):
                return imp.load_compiled(name, os.path.realpath(module_path))
    except ImportError:
        LOGGER.error('Failed to load module: "{}"'.format(module_path))
        raise


def resolve_module(name, log_error=False):
    """
    Resolves a dotted module name to a global object
    :param name: str
    :return:
    """

    name = name.split('.')
    used = name.pop(0)
    found = __import__(used)
    for n in name:
        used = used + '.' + n
        try:
            found = getattr(found, n)
        except AttributeError:
            try:
                __import__(used)
            except ImportError:
                if log_error:
                    LOGGER.error(traceback.format_exc())
                return None
            found = getattr(found, n)

    return found


def iterate_modules(path, exclude=None, skip_inits=True, recursive=True, return_pyc=False):
    """
    Iterates all the modules of the given path
    :param path: str, folder path to iterate
    :param exclude: list(str), list of files to exclude
    :return: iterator
    """

    exclude = python.force_list(exclude)
    _exclude = ['__init__.py', '__init__.pyc'] if skip_inits else list()

    modules_found = dict()

    extension_to_skip = '.pyc' if not return_pyc else '.py'

    if recursive:
        for root, dirs, files in os.walk(path):
            if '__init__.py' not in files:
                continue
            for f in files:
                base_name = os.path.splitext(f)[0]
                if f not in exclude and base_name not in exclude:
                    module_path = path_utils.clean_path(os.path.join(root, f))
                    if f.endswith('.py') or f.endswith('.pyc') and base_name:
                        if base_name in modules_found:
                            if base_name.endswith(extension_to_skip):
                                continue
                            else:
                                if base_name.endswith(extension_to_skip) and base_name not in modules_found:
                                    modules_found[base_name] = module_path
                        else:
                            modules_found[base_name] = module_path
    else:
        files = os.listdir(path)
        if '__init__.py' not in files:
            return list(modules_found.values())
        for file_name in files:
            base_name = os.path.splitext(file_name)[0]
            if file_name not in exclude and base_name not in exclude:
                module_path = path_utils.clean_path(os.path.join(path, file_name))
                if file_name.endswith('.py') or file_name.endswith('.pyc') and base_name:
                    if base_name in modules_found:
                        if base_name.endswith(extension_to_skip):
                            continue
                        else:
                            if base_name.endswith(extension_to_skip) and base_name not in modules_found:
                                modules_found[base_name] = module_path
                    else:
                        modules_found[base_name] = module_path

    return list(modules_found.values())


def iterate_module_members(module_to_iterate, predicate=None):
    """
    Iterates all the members of the given modules
    :param module_to_iterate: ModuleObject, module object to iterate members of
    :param predicate: inspect.cass, if given members will be restricted to given inspect class
    :return: iterator
    """

    for mod in inspect.getmembers(module_to_iterate, predicate=predicate):
        yield mod


def iterate_module_subclasses(module, class_type):
    """
    Iterates all classes within a module object returning all subclasses of given type
    :param module: ModuleObject, module object to iterate subclasses of
    :param class_type: object, class object to find
    :return: generator(object), generator function returning class objects
    """

    for member in iterate_module_members(module, predicate=inspect.isclass):
        if issubclass(member, class_type):
            yield member


def get_package_children(module_path):
    """
    Returns all package children in given module path
    :param module_path: str
    :return: list(str)
    """

    return [name for _, name, _ in pkgutil.iter_modules([module_path])]


def load_module_from_source(file_path, unique_namespace=False):
    """
    Loads a Python from given source file
    :param file_path:
    :param unique_namespace: bool
    :return:
    """

    file_name = os.path.splitext(os.path.basename(file_path))[0]

    module_name = '{}{}'.format(file_name, str(uuid.uuid4())) if unique_namespace else file_name

    try:
        if python.is_python2():
            if file_path.endswith('.py'):
                return imp.load_source(module_name, file_path)
            elif file_path.endswith('.pyc'):
                return imp.load_compiled(module_name, file_path)
        else:
            return SourceFileLoader(module_name, file_path).load_module()
    except BaseException:
        LOGGER.debug('Failed trying to direct load : {} | {}'.format(file_path, sys.exc_info()))
        return None
