import imp
import importlib
import inspect
import six
import types
import yaml
from copy import deepcopy
from collections import OrderedDict

# Ability to load and dump yaml as OrderedDict
from yaml import resolver

from .config import SWAGGER_SETTINGS

_mapping_tag = yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG


def dict_representer(dumper, data):
    return dumper.represent_dict(six.iteritems(data))


def dict_constructor(loader, node):
    return OrderedDict(loader.construct_pairs(node))


yaml.add_representer(OrderedDict, dict_representer)
yaml.add_constructor(_mapping_tag, dict_constructor)

ALLOWED = 'all'


class YAMLLoaderMixin(object):

    @staticmethod
    def yaml_load(data):
        try:
            return yaml.load(data)
        except (yaml.YAMLError, AttributeError):
            return None


def flatten(seq):
    l = []
    for elt in seq:
        t = type(elt)
        if t is tuple or t is list:
            for elt2 in flatten(elt):
                l.append(elt2)
        else:
            l.append(elt)
    return l


def clean_parameters(parameters, method=ALLOWED):
    # If formData and body parameters presents
    # and file in formData then remove body
    # else remove all formData parameters
    formdata_file = any(p['in'] == 'formData' and p.get('type', None) == 'file' for p in parameters)

    if formdata_file:
        parameters = [p for p in parameters if not p['in'] == 'body']
    else:
        parameters = [p for p in parameters if not p['in'] == 'formData']

    # Remove duplicates and check parameter method
    parameters_set = set()
    cleaned_parameters = []
    for parameter in parameters:
        name = parameter['name']
        if name in parameters_set:
            continue

        p = deepcopy(parameter)
        methods = p.pop('methods', [ALLOWED])

        if method in methods or ALLOWED in methods:
            cleaned_parameters.append(p)
            parameters_set.add(name)

    return cleaned_parameters


def get_mro_list(instance, only_parents=True):
    mro = []
    if inspect.isclass(instance):
        mro = inspect.getmro(instance)
        if only_parents:
            mro = mro[1:]
    return mro


def get_decorators(function):
    # If we have no func_closure, it means we are not wrapping any other functions.
    decorators = []

    try:
        func_closure = six.get_function_closure(function)
    except AttributeError:
        return decorators
    if not func_closure:
        return [function]
    # Otherwise, we want to collect all of the recursive results for every closure we have.
    for closure in func_closure:
        if isinstance(closure.cell_contents, types.FunctionType):
            decorators.extend(get_decorators(closure.cell_contents))
    return [function] + decorators


def _extract_class_path(path):
    module_path = None
    class_name = path

    if '.' in path:
        module_path, class_name = path.rsplit('.', 1)

    return module_path, class_name


def _load_class_obj(module_path, class_name, package=None):
    class_obj = None
    try:
        module = importlib.import_module(module_path, package)
        class_obj = getattr(module, class_name, None)
    except ImportError:
        pass

    return class_obj


def load_class(path):
    if path.startswith('.'):
        # is it relative path?
        # TODO: may be later
        raise ImportError('Relative class path is not supported {}'.format(path))

    module_path, class_name = _extract_class_path(path)
    if module_path is None:
        # is class located in current module?
        # TODO: may be later
        raise ImportError('Absolute module path is required {}'.format(path))

    class_obj = _load_class_obj(module_path, class_name)
    if class_obj is None:
        raise ImportError('Could not find {}'.format(path))

    return class_obj


def update_settings(settings, settings_part):
    for param_name, param_value in settings_part.items():
        if param_name in settings and isinstance(settings[param_name], dict):
            settings[param_name].update(param_value)
        elif param_name in settings and isinstance(settings[param_name], list):
            settings[param_name].extend(param_value)
        else:
            settings[param_name] = param_value

    return settings


def get_settings(local_config_file_path=None):
    swagger_settings = SWAGGER_SETTINGS.copy()
    plugin_settings = {}

    if local_config_file_path:
        custom_config = imp.load_source('config', local_config_file_path)
        swagger_settings.update(getattr(custom_config, 'SWAGGER_SETTINGS', dict()))
        plugin_settings.update(getattr(custom_config, 'PLUGIN_SETTINGS', dict()))

    return swagger_settings, plugin_settings