#
# Copyright (C) 2018 Satoru SATOH <ssato @ redhat.com>
# License: MIT
#
# pylint: disable=unidiomatic-typecheck
r"""Abstract processor module.

.. versionadded:: 0.9.5

   - Add to abstract processors such like Parsers (loaders and dumpers).
"""
from __future__ import absolute_import

import operator
import pkg_resources

import anyconfig.compat
import anyconfig.ioinfo
import anyconfig.models.processor

from anyconfig.globals import (
    UnknownProcessorTypeError, UnknownFileTypeError, IOInfo
)


def _load_plugins_itr(pgroup, safe=True):
    """
    .. seealso:: the doc of :func:`load_plugins`
    """
    for res in pkg_resources.iter_entry_points(pgroup):
        try:
            yield res.load()
        except ImportError:
            if safe:
                continue
            raise


def load_plugins(pgroup, safe=True):
    """
    :param pgroup: A string represents plugin type, e.g. anyconfig_backends
    :param safe: Do not raise ImportError during load if True
    :raises: ImportError
    """
    return list(_load_plugins_itr(pgroup, safe=safe))


def find_with_pred(predicate, prs):
    """
    :param predicate: any callable to filter results
    :param prs: A list of :class:`anyconfig.models.processor.Processor` classes
    :return: Most appropriate processor class or None

    >>> class A(anyconfig.models.processor.Processor):
    ...    _type = "json"
    ...    _extensions = ['json', 'js']
    >>> class A2(A):
    ...    _priority = 99  # Higher priority than A.
    >>> class B(anyconfig.models.processor.Processor):
    ...    _type = "yaml"
    ...    _extensions = ['yaml', 'yml']
    >>> prs = [A, A2, B]

    >>> find_with_pred(lambda p: 'js' in p.extensions(), prs)
    <class 'anyconfig.processors.A2'>
    >>> find_with_pred(lambda p: 'yml' in p.extensions(), prs)
    <class 'anyconfig.processors.B'>
    >>> x = find_with_pred(lambda p: 'xyz' in p.extensions(), prs)
    >>> assert x is None

    >>> find_with_pred(lambda p: p.type() == "json", prs)
    <class 'anyconfig.processors.A2'>
    >>> find_with_pred(lambda p: p.type() == "yaml", prs)
    <class 'anyconfig.processors.B'>
    >>> x = find_with_pred(lambda p: p.type() == "x", prs)
    >>> assert x is None
    """
    _prs = sorted((p for p in prs if predicate(p)),
                  key=operator.methodcaller("priority"), reverse=True)
    if _prs:
        return _prs[0]  # Found.

    return None


def find_by_type(ptype, prs, cls=anyconfig.models.processor.Processor):
    """
    :param ptype:
        Type of the data to process or
        :class:`anyconfig.models.processor.Processor` class object or its
        instance
    :param prs: A list of :class:`anyconfig.models.processor.Processor` classes
    :param cls: A class object to compare with `ptype`
    :return:
        Most appropriate processor instance to process files of given data type
        `ptype` or None
    :raises: UnknownProcessorTypeError
    """
    if isinstance(ptype, cls):
        return ptype

    if type(ptype) == type(cls) and issubclass(ptype, cls):
        return ptype()

    def pred(pcls):
        """Predicate"""
        return pcls.type() == ptype

    processor = find_with_pred(pred, prs)
    if processor is None:
        raise UnknownProcessorTypeError(ptype)

    return processor()


def find_by_type_or_id(type_or_id, prs,
                       cls=anyconfig.models.processor.Processor):
    """
    :param type_or_id:
        Type of the data to process or ID of the processor class or
        :class:`anyconfig.models.processor.Processor` class object or its
        instance
    :param prs: A list of :class:`anyconfig.models.processor.Processor` classes
    :param cls: A class object to compare with `type_or_id`
    :return:
        Most appropriate processor instance to process files of given data type
        or processor `type_or_id` found by its ID or None
    :raises: UnknownProcessorTypeError
    """
    if isinstance(type_or_id, cls):
        return type_or_id

    if type(type_or_id) == type(cls) and issubclass(type_or_id, cls):
        return type_or_id()

    def pred(pcls):
        """Predicate"""
        return pcls.cid() == type_or_id or pcls.type() == type_or_id

    processor = find_with_pred(pred, prs)
    if processor is None:
        raise UnknownProcessorTypeError(type_or_id)

    return processor()


def find_by_fileext(fileext, prs):
    """
    :param fileext: File extension
    :param prs: A list of :class:`anyconfig.models.processor.Processor` classes
    :return:
        Most appropriate processor class to process files with given
        extentsions or None
    :raises: UnknownFileTypeError
    """
    def pred(pcls):
        """Predicate"""
        return fileext in pcls.extensions()

    return find_with_pred(pred, prs)


def find_by_maybe_file(obj, prs):
    """
    :param obj:
        a file path, file or file-like object, pathlib.Path object or
        `~anyconfig.globals.IOInfo` (namedtuple) object
    :param cps_by_ext: A list of processor classes

    :return:
        An instance of most appropriate processor class to process given data
    :raises: UnknownFileTypeError
    """
    if not isinstance(obj, IOInfo):
        obj = anyconfig.ioinfo.make(obj)

    processor = find_by_fileext(obj.extension, prs)
    if processor is None:
        raise UnknownFileTypeError("file extension={}".format(obj.extension))

    return processor()


def find(obj, prs, forced_type=None, cls=anyconfig.models.processor.Processor):
    """
    :param obj:
        a file path, file or file-like object, pathlib.Path object or
        `~anyconfig.globals.IOInfo` (namedtuple) object
    :param prs: A list of :class:`anyconfig.models.processor.Processor` classes
    :param forced_type:
        Forced processor type of the data to process or ID of the processor
        class or :class:`anyconfig.models.processor.Processor` class object or
        its instance itself
    :param cls: A class object to compare with `forced_type` later

    :return: an instance of processor class to process `obj` data
    :raises: ValueError, UnknownProcessorTypeError, UnknownFileTypeError
    """
    if (obj is None or not obj) and forced_type is None:
        raise ValueError("The first argument 'obj' or the second argument "
                         "'forced_type' must be something other than "
                         "None or False.")

    if forced_type is None:
        processor = find_by_maybe_file(obj, prs)
        if processor is None:
            raise UnknownFileTypeError(obj)

        return processor

    processor = find_by_type_or_id(forced_type, prs, cls=cls)
    if processor is None:
        raise UnknownProcessorTypeError(forced_type)

    return processor


class Processors(object):
    """An abstract class of which instance holding processors.
    """
    _pgroup = None  # processor group name to load plugins

    def __init__(self, processors=None):
        """
        :param processors:
            A list of :class:`Processor` or its children class objects or None
        """
        self._processors = dict()  # {<processor_class_id>: <processor_class>}
        if processors is not None:
            self.register(*processors)

        self.load_plugins()

    def register(self, *pclss):
        """
        :param pclss: A list of :class:`Processor` or its children classes
        """
        for pcls in pclss:
            if pcls.cid() not in self._processors:
                self._processors[pcls.cid()] = pcls

    def load_plugins(self):
        """Load and register pluggable processor classes internally.
        """
        if self._pgroup:
            self.register(*load_plugins(self._pgroup))

    def list(self, sort=True):
        """
        :return: A list of :class:`Processor` or its children classes
        """
        if sort:
            return sorted(self._processors.values(),
                          key=operator.methodcaller("cid"))

        return self._processors.values()

    def find_by_type(self, ptype):
        """
        :param ptype: Processor's type to find
        """
        return find_by_type(ptype, self.list(sort=False))

    def find_by_type_or_id(self, type_or_id):
        """
        :param type_or_id: Processor's type or ID to find
        """
        return find_by_type_or_id(type_or_id, self.list(sort=False))

    def find(self, obj, forced_type=None,
             cls=anyconfig.models.processor.Processor):
        """
        :param obj:
            a file path, file or file-like object, pathlib.Path object or
            `~anyconfig.globals.IOInfo` (namedtuple) object
        :param forced_type: Forced processor type to find
        :param cls: A class object to compare with `ptype`

        :return: an instance of processor class to process `ipath` data later
        :raises: ValueError, UnknownProcessorTypeError, UnknownFileTypeError
        """
        return find(obj, self.list(sort=False), forced_type=forced_type,
                    cls=cls)

# vim:sw=4:ts=4:et:
