# -*- coding: utf-8 -*-
###########################################################################
# Bioconvert is a project to facilitate the interconversion               #
# of life science data from one format to another.                        #
#                                                                         #
# Authors: see CONTRIBUTORS.rst                                           #
# Copyright © 2018  Institut Pasteur, Paris and CNRS.                     #
# See the COPYRIGHT file for details                                      #
#                                                                         #
# bioconvert is free software: you can redistribute it and/or modify      #
# it under the terms of the GNU General Public License as published by    #
# the Free Software Foundation, either version 3 of the License, or       #
# (at your option) any later version.                                     #
#                                                                         #
# bioconvert is distributed in the hope that it will be useful,           #
# but WITHOUT ANY WARRANTY; without even the implied warranty of          #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the           #
# GNU General Public License for more details.                            #
#                                                                         #
# You should have received a copy of the GNU General Public License       #
# along with this program (COPYING file).                                 #
# If not, see <http://www.gnu.org/licenses/>.                             #
###########################################################################
"""Main bioconvert registry that fetches automatically the relevant converter"""
import inspect
import itertools
import pkgutil
import importlib
import colorlog

import bioconvert

_log = colorlog.getLogger(__name__)

__all__ = ['Registry']


class Registry(object):
    """class to centralise information about available conversions

    ::

        from bioconvert.core.registry import Registry
        r = Registry()
        r.conversion_exists("BAM", "BED")
        r.info()  # returns number of available methods for each converter

        conv_class = r[(".bam", ".bed")]
        converter = conv_class(input_file, output_file)
        converter.convert()

    """

    def __init__(self):
        self._ext_registry = {}
        self._fmt_registry = {}
        self._fill_registry(bioconvert.__path__)
        self._build_path_dict()

    def _fill_registry(self, path, target=None, including_not_available_converter=False):
        """
        Explore the directory converters to discover all converter classes
        (a concrete class which inherits from :class:`ConvBase`)
        and fill the register with the input format and output format
        associated to this converter

        :param str path: the path of a directory to explore (not recursive)
        """

        target = self if target is None else target

        def is_converter(item):
            """Check if a module is a converter"""
            obj_name, obj = item
            if not inspect.isclass(obj):
                return False

            # Note that on some Python version, the isabstract is buggy.
            # Therefore, the isabstract does not return False for ConvBase
            # hence the additional check (obj_name in ["ConvBase"])
            return (issubclass(obj, bioconvert.ConvBase)
                    and not inspect.isabstract(obj)
                    and obj_name not in ["ConvBase"])

        modules = pkgutil.iter_modules(path=path)
        for _, module_name, *_ in modules:
            if module_name != '__init__':
                try:
                    module = importlib.import_module("bioconvert." + module_name)
                except (ImportError, TypeError) as err:
                    _log.warning("skip module '{}': {}".format(module_name, err))
                    continue

                converters = inspect.getmembers(module)
                converters = [c for c in converters if is_converter(c)]
                for converter_name, converter in converters:
                    if converter is not None:
                        # the registry is no more based on extension but on format
                        format_pair = (converter.input_fmt, converter.output_fmt)
                        _log.debug("add converter '{}' for {} -> {} in fmt_registry".format(
                            converter_name, *format_pair))
                        target[format_pair] = converter
                        all_ext_pair = itertools.product(converter.input_ext, converter.output_ext)
                        # all_ext_pair = list(all_ext_pair)
                        for ext_pair in all_ext_pair:
                            # format_pair = (converter.input_fmt, converter.output_fmt)
                            if len(converter.available_methods) == 0 and not including_not_available_converter:
                                _log.warning("converter '{}' for {} -> {} was not added as no method is available"
                                             .format(converter_name, *ext_pair))
                            else:
                                _log.debug("add converter '{}' for {} -> {} in ext_registry".format(
                                           converter_name, *ext_pair))
                                self.set_ext(ext_pair, converter)

    def _build_path_dict(self):
        """
        Constructs a dictionary containing shortest paths
        from one format to another.
        """
        from networkx import DiGraph, all_pairs_shortest_path
        self._path_dict = dict(all_pairs_shortest_path(
            DiGraph(self.get_conversions())))
        self._path_dict_ext = dict(all_pairs_shortest_path(
            DiGraph(self.get_conversions_from_ext())))

    # TODO: Should we use a format_pair instead of two strings?
    def conversion_path(self, input_ext, output_ext):
        """
        Returns a list of conversion steps to get from *input_ext* to *output_ext*.

        Each step in the list is a pair of formats.
        """
        try:
            fmt_steps = self._path_dict[input_ext][output_ext]
        except KeyError:
            fmt_steps = []
        return list(zip(fmt_steps, fmt_steps[1:]))

    def __setitem__(self, format_pair, convertor):
        """
        Register new convertor from input format to output format.

        :param format_pair: the input format, the output format
        :type format_pair: tuple of 2 strings
        :param convertor: the convertor which handle the conversion
                          from input_fmt -> output_fmt
        :type convertor: :class:`ConvBase` object
        """
        if format_pair in self._fmt_registry:
            raise KeyError('an other converter already exists for {} -> {}'.format(*format_pair))
        self._fmt_registry[format_pair] = convertor

    def set_ext(self, ext_pair, convertor):
        """
        Register new convertor from input extension and output extension
        in a list. We can have a list of multiple convertors for one
        ext_pair.

        :param ext_pair: the input extension, the output extension
        :type ext_pair: tuple of 2 strings
        :param convertor: the convertor which handle the conversion
                          from input_ext -> output_ext
        :type convertor: list of :class:`ConvBase` object
        """
        if ext_pair in self._ext_registry:
            self._ext_registry[ext_pair].append(convertor)
        else:
            self._ext_registry[ext_pair] = [convertor]

    def __getitem__(self, format_pair):
        """
        :param format_pair: the input format, the output format
        :type format_pair: tuple of 2 strings
        :return: an object of subclass o :class:`ConvBase`
        """
        format_pair = (format_pair[0].upper(), format_pair[1].upper())
        return self._fmt_registry[format_pair]


    def get_ext(self, ext_pair):
        """
        copy the registry into a dict that behaves like a list
        to be able to have multiple values ​​for a single key
        and from a key have all converter able to do the conversion
        from the input extension to the output extension.

        :param ext_pair: the input extension, the output extension
        :type ext_pair: tuple of 2 strings
        :return: list of objects of subclass o :class:`ConvBase`
        """
        return self._ext_registry[ext_pair]

    def __contains__(self, format_pair):
        """
        Can use membership operation on registry to test if a converter
        to go form input format to output format exists.

        :param format_pair: the input format, the output format
        :type format_pair: tuple of 2 strings
        :return: True if format_pair is in registry otherwise False.
        """

        format_pair = (format_pair[0].upper(), format_pair[1].upper())
        return format_pair in self._fmt_registry

    def __iter__(self):
        """
        make registry iterable
        through format_pair (str input format, str output format)
        """
        for format_pair in self._fmt_registry:
            yield format_pair

    def get_conversions(self):
        """
        :return: a generator which allow to iterate on all available conversions
                 a conversion is encoded by a tuple of
                 2 strings (input format, output format)
        :retype: generator
        """
        for conv in self._fmt_registry:
            yield conv

    def get_converters_names(self):
        """
        :return: a generator that allows to get the name of the converter
                 from the subclass (ConvBase object)
        :rtype: generator

        """
        for converter in self._fmt_registry.values():
            yield converter.__name__.lower()

    def get_conversions_from_ext(self):
        """
        :return: a generator which allow to iterate on all available conversions
                 a conversion is encoded by a tuple of
                 2 strings (input format, output format)
        :rtype: generator
        """
        for conv in self._ext_registry:
            yield conv

    def get_all_conversions(self):
        """
        :return: a generator which allow to iterate on all available conversions and their availability
                 a conversion is encoded by a tuple of
                 2 strings (input format, output format)
        :retype: generator (input format, output format, status)
        """
        #all_converter = dict(self._fmt_registry)
        all_converter = {}
        self._fill_registry(bioconvert.__path__, target=all_converter, including_not_available_converter=True)
        for i, o in all_converter:
            yield i, o, (i, o) in self._fmt_registry

    def conversion_exists(self, input_fmt, output_fmt, allow_indirect=False):
        """
        :param str input_fmt: the input format
        :param str output_fmt: the output format
        :param boolean allow_indirect: whether to count indirect conversions
        :return: True if a converter which transform input_fmt into output_fmt exists
        :rtype: boolean
        """
        input_fmt = input_fmt.upper()
        output_fmt = output_fmt.upper()
        # return (input_fmt, output_fmt) in self._fmt_registry
        return ((input_fmt, output_fmt) in self._fmt_registry
                or (allow_indirect
                    and len(self.conversion_path(input_fmt, output_fmt))))


    def get_info(self):
        converters = set([self[this] for this in self._fmt_registry])
        data = {}
        for converter in converters:
            data[converter] = len(converter.available_methods)
        return data

    def iter_converters(self, allow_indirect: bool = False):
        """

        :param bool allow_indirect: also return indirect conversion
        :return: a generator to iterate over (in_fmt, out_fmt, converter class when direct, path when indirect)
        :rtype: a generator
        """
        # if allow_indirect:
        for start, stops in self._path_dict.items():
            for stop, path in stops.items():
                if len(path) == 1:
                    pass
                elif len(path) == 2:
                    yield start, stop, self._fmt_registry[(start, stop)], None
                elif allow_indirect:
                    yield start, stop, None, path
        #     return
        # for conv, converter in self._fmt_registry.items():
        #     in_fmt, out_fmt = conv
        #     yield in_fmt, out_fmt, converter, None

    def __str__(self):
        data = self.info()
        C = data["converters"]
        F = data["formats"]
        M = data["methods"]
        txt = "Number of formats: {}".format(F)
        txt +="\n" + "Number of converters: {}".format(C)
        txt += "\n" + "Number of methods : {}".format(M)
        return txt

    def info(self):
        info = self.get_info()
        _converters = [x for x in info.items()]
        _data = [info[k] for k,v in info.items()]
        C = len(_converters)
        M = sum(_data)

        F = len(set([x for items in self.get_all_conversions() for x in items]))

        return {
            "formats": F, 
            "converters": C, 
            "methods": M,
            "methods_per_converter": round(float(M)/C,2)}



