import logging
from typing import Callable, Collection, Dict, Iterable, Tuple, Type, Union

from ..layers import CallableLayer, Layer
from ..utils import MultiDict
from .factory import GraphFactory, SourceFactory, TransformFactory, add_from_mixins, add_quals, items_to_container

logger = logging.getLogger(__name__)
BASES: Dict[Type[Layer], GraphFactory] = {}


class APIMeta(type):
    @classmethod
    def __prepare__(mcs, *args, **kwargs):
        return MultiDict()

    def __new__(mcs, class_name, bases, namespace, **flags):
        if '__factory' in flags:
            factory = flags.pop('__factory')
            scope = namespace.to_dict()
            base = super().__new__(mcs, class_name, bases, scope, **flags)
            BASES[base] = factory  # noqa
            return base

        bases = set(bases)
        intersection = set(BASES) & bases
        if len(intersection) != 1:
            raise TypeError(f'Layers must inherit from one of ' + ', '.join(x.__name__ for x in BASES))

        main, = intersection
        factory = BASES[main]
        bases -= intersection
        base_name = main.__name__
        for base in bases:
            if not issubclass(base, Mixin):
                raise TypeError(f'{base_name}s can only inherit directly from "{base_name}" or other mixins.')

        logger.info('Compiling the layer "%s" of type %s', class_name, base_name)

        if main == Mixin:
            add_from_mixins(namespace, bases)
            scope = add_quals({'__methods__': namespace}, namespace)

        else:
            factory.validate_before_mixins(namespace)
            add_from_mixins(namespace, bases)
            scope = factory.make_scope(class_name, namespace)

        return super().__new__(mcs, class_name, (main,), scope, **flags)


class Source(CallableLayer, metaclass=APIMeta, __factory=SourceFactory):
    """
    Base class for all sources.
    """

    def __init__(self, *args, **kwargs):  # noqa
        raise RuntimeError("\"Source\" can't be directly initialized. You must subclass it first.")


class SourceBase(CallableLayer):
    def __init__(self, items: Union[Iterable[Tuple[str, Callable]], Dict[str, Callable]]):
        super().__init__(*items_to_container(items, type(self), SourceFactory))


class Transform(CallableLayer, metaclass=APIMeta, __factory=TransformFactory):
    """
    Base class for all transforms.

    Can also be used as an inplace factory for transforms.

    Examples
    --------
    # class-based transforms
    >>> from imops import zoom
    >>>
    >>> class Zoom(Transform):
    ...     def image(image):
    ...         return zoom(image, scale_factor=2)
    # inplace transforms
    >>> Transform(image=lambda image: zoom(image, scale_factor=2))
    """
    __inherit__: Union[str, Collection[str], bool] = ()
    __exclude__: Union[str, Collection[str]] = ()

    def __init__(*args, __inherit__: Union[str, Collection[str], bool] = (),
                 __exclude__: Union[str, Collection[str]] = (), **kwargs: Callable):
        assert args
        if len(args) > 1:
            raise TypeError('This constructor accepts only keyword arguments.')
        self, = args
        super(Transform, self).__init__(*items_to_container(
            kwargs, type(self), TransformFactory, __inherit__=__inherit__, __exclude__=__exclude__
        ))

    def __repr__(self):
        return f"{self.__class__.__name__}({', '.join(self._methods.fields())})"


class TransformBase(CallableLayer):
    def __init__(self, items: Union[Iterable[Tuple[str, Callable]], Dict[str, Callable]],
                 inherit: Union[str, Collection[str], bool] = (), exclude: Union[str, Collection[str]] = ()):
        super().__init__(*items_to_container(
            items, type(self), TransformFactory, __inherit__=inherit, __exclude__=exclude
        ))


class Mixin(metaclass=APIMeta, __factory=None):
    """
    Base class for all Mixins.
    """

    def __init__(*args, **kwargs):  # noqa
        raise RuntimeError("Mixins can't be directly initialized.")

    __methods__: dict = {}
