# -*- coding: utf-8 -*-

# Copyright © 2017, Institut Pasteur
#   Contributor: François Laurent

# This file is part of the TRamWAy software available at
# "https://github.com/DecBayComp/TRamWAy" and is distributed under
# the terms of the CeCILL license as circulated at the following URL
# "http://www.cecill.info/licenses.en.html".

# The fact that you are presently reading this means that you have had
# knowledge of the CeCILL license and that you accept its terms.


from __future__ import print_function
import importlib
import copy
import os
import re

try:
    fullmatch = re.fullmatch
except AttributeError:  # Py2
    fullmatch = re.match
from warnings import warn
import traceback


def list_plugins(dirname, package, lookup={}, force=False, require=None, verbose=False):
    if verbose:
        _pre = "loading: "
        _post = "..."
        _success = "[done]"
        _failure = "[failed]"
    if not require:
        require = ()
    elif isinstance(require, str):
        require = (require,)
    pattern = re.compile(r"[a-zA-Z0-9].*([.]py)?")
    candidate_modules = set(
        [
            os.path.splitext(fn)[0]
            for fn in os.listdir(dirname)
            if fullmatch(pattern, fn) is not None
        ]
    )
    modules = {}
    provided = {}
    for name in candidate_modules:
        path = "{}.{}".format(package, name)
        # load module
        try:
            module = importlib.import_module(path)
        except (KeyboardInterrupt, SystemExit):
            raise
        except:
            if verbose:
                print("{}{}{}\t{}".format(_pre, path, _post, _failure))
                print(traceback.format_exc(), end="")
            continue
        # ensure that all the required attributes are available
        _continue = False
        for required in require:
            if not hasattr(module, required):
                _continue = True
                break
        if _continue:
            continue
        elif verbose:
            print("{}{}{}".format(_pre, path, _post), end="\t")
        # parse setup
        if hasattr(module, "setup"):
            setup = module.setup
            try:
                name = setup["name"]
            except KeyError:
                setup["name"] = name
        else:
            setup = dict(name=name)
        # parse other attributes
        try:
            namespace = module.__all__
        except AttributeError:
            namespace = list(module.__dict__.keys())
        missing = conflicting = None
        warning = []
        for key in lookup:
            if key in setup:
                continue
            ref = lookup[key]
            if isinstance(ref, type):
                matches = []
                for var in namespace:
                    try:
                        ok = issubclass(getattr(module, var), ref)
                    except TypeError:
                        ok = False
                    if ok:
                        matches.append(var)
            else:
                matches = [var for var in namespace if fullmatch(ref, var) is not None]
            if matches:
                if matches[1:]:
                    conflicting = key
                    if not force:
                        break
                setup[key] = matches[0]
            else:
                missing = key
                if not force:
                    break
        if conflicting:
            warning.append(
                (
                    "multiple matches in module '{}' for key '{}'".format(
                        path, conflicting
                    ),
                    ImportWarning,
                )
            )
            if not force:
                continue
        if missing:
            warning.append(
                (
                    "no match in module '{}' for key '{}'".format(path, missing),
                    ImportWarning,
                )
            )
            if verbose:
                if force:
                    print(_success)
                else:
                    print(_failure)
        elif verbose:
            print(_success)
        for w in warning:
            warn(*w)
        if missing and not force:
            continue
        # register plugin
        plugin = (setup, module)
        if isinstance(name, str):
            modules[name] = plugin
        else:
            names = name
            for name in names:
                modules[name] = plugin
        try:
            provides = setup["provides"]
        except KeyError:
            pass
        else:
            if isinstance(provides, str):
                provided[provides] = plugin
            else:
                for _provides in provides:
                    provided[_provides] = plugin
    for provides in provided:
        if provides not in modules:
            modules[provides] = provided[provides]
    return modules


def add_arguments(parser, arguments, name=None):
    translations = []
    for arg, options in arguments.items():
        if not options:
            continue
        long_arg = "--" + arg.replace("_", "-")
        has_options = False
        if isinstance(options, (tuple, list)):
            args = list(options)
            kwargs = args.pop()
            options = {}
        else:
            args = []
            try:
                _parse = options["parse"]
            except KeyError:
                pass
            else:
                if callable(_parse):
                    translations.append((arg, _parse))
                    has_options = True
            try:
                kwargs = options["kwargs"]
            except KeyError:
                if has_options or not options:
                    continue
                kwargs = options
                options = None  # should not be used anymore
            else:
                has_options = True
                try:
                    args = list(options.get("args"))
                except KeyError:
                    pass
        if has_options and options.get("translate", False):
            try:
                _arg = args[1]
            except IndexError:
                _arg = args[0]
            _arg = _arg.lstrip("-").replace("-", "_")

            def _translate(**_kwargs):
                return _kwargs.get(_arg, None)

            translations.append((arg, _translate))
        elif long_arg not in args:
            if args:
                args.insert(1, long_arg)
            else:
                args = (long_arg,)
        try:
            parser.add_argument(*args, **kwargs)
        except (KeyboardInterrupt, SystemExit):
            raise
        except:
            if name:
                print("WARNING: option `{}` from plugin '{}' ignored".format(arg, name))
            else:
                print("WARNING: option `{}` ignored".format(arg))
    return translations


def short_options(arguments):
    _options = []
    for args in arguments.values():
        if args:
            if isinstance(args, (tuple, list)):
                args = args[0]
            elif "args" in args:
                args = args["args"]
            else:
                continue
            if not isinstance(args, (tuple, list)):
                args = (args,)
            for arg in args:
                if arg[0] != arg[1]:
                    _options.append(arg)
    return _options


class Plugins(object):

    __slots__ = (
        "modules",
        "dirname",
        "package",
        "lookup",
        "force",
        "require",
        "verbose",
        "post_load",
    )

    def __init__(
        self, dirname, package, lookup={}, force=False, require=None, verbose=False
    ):
        self.modules = None
        self.dirname = dirname
        self.package = package
        self.lookup = dict(lookup)
        self.force = force
        self.require = require
        self.verbose = verbose
        self.post_load = None

    def __load__(self):
        if self.modules is None:
            self.modules = list_plugins(
                self.dirname,
                self.package,
                self.lookup,
                self.force,
                self.require,
                self.verbose,
            )
            if self.post_load:
                self.post_load(self)

    def __repr__(self):
        return "Plugins{{in '{}'}}{}".format(
            self.package,
            "{(not loaded)}" if self.modules is None else repr(self.modules),
        )

    def __nonzero__(self):
        self.__load__()
        return self.modules.__nonzero__()

    def __len__(self):
        self.__load__()
        return self.modules.__len__()

    def __iter__(self):
        self.__load__()
        return self.modules.__iter__()

    def __getitem__(self, mod):
        self.__load__()
        return self.modules.__getitem__(mod)

    def __setitem__(self, mod, descr):
        self.__load__()
        self.modules.__setitem__(mod, descr)

    def __missing__(self, mod):
        self.__load__()
        return self.modules.__missing__(mod)

    def keys(self):
        self.__load__()
        return self.modules.keys()

    def values(self):
        self.__load__()
        return self.modules.values()

    def items(self):
        self.__load__()
        return self.modules.items()

    def get(self, mod, default):
        self.__load__()
        return self.modules.get(mod, default)

    def pop(self, mod, default):
        self.__load__()
        return self.modules.pop(mod, default)

    def update(self, plugins):
        self.__load__()
        if not isinstance(plugins, dict):
            raise TypeError("not a `dict`")
        self.modules.update(plugins)


__all__ = [
    "list_plugins",
    "add_arguments",
    "short_options",
    "Plugins",
]
