from typing import List, Tuple, Callable, Any

from .base import ILoaderClass, Loader, CAPTURE_EXCEPTIONS
from .exception import CompositeStructureError
from .types import method
from .utils import raw

MAPPING_ERROR_ITEM = Tuple[str, Exception]
MAPPING_ERRORS = List[MAPPING_ERROR_ITEM]


class MappingError(CompositeStructureError):
    """
    Overview:
        Mapping error.
    Interfaces:
        ``__init__``, ``errors``
    """

    def __init__(self, key_errors: MAPPING_ERRORS, value_errors: MAPPING_ERRORS):
        """
        Overview:
            Initialize the MappingError.
        Arguments:
            - key_errors (:obj:`MAPPING_ERRORS`): The key errors.
            - value_errors (:obj:`MAPPING_ERRORS`): The value errors.
        """

        self.__key_errors = list(key_errors or [])
        self.__value_errors = list(value_errors or [])
        self.__errors = self.__key_errors + self.__value_errors

    def key_errors(self) -> MAPPING_ERRORS:
        """
        Overview:
            Get the key errors.
        """

        return self.__key_errors

    def value_errors(self) -> MAPPING_ERRORS:
        """
        Overview:
            Get the value errors.
        """

        return self.__value_errors

    def errors(self) -> MAPPING_ERRORS:
        """
        Overview:
            Get the errors.
        """

        return self.__errors


def mapping(key_loader, value_loader, type_back: bool = True) -> ILoaderClass:
    """
    Overview:
        Create a mapping loader.
    Arguments:
        - key_loader (:obj:`ILoaderClass`): The key loader.
        - value_loader (:obj:`ILoaderClass`): The value loader.
        - type_back (:obj:`bool`): Whether to convert the type back.
    """

    key_loader = Loader(key_loader)
    value_loader = Loader(value_loader)

    def _load(value):
        _key_errors = []
        _value_errors = []
        _result = {}
        for key_, value_ in value.items():
            key_error, value_error = None, None
            key_result, value_result = None, None

            try:
                key_result = key_loader(key_)
            except CAPTURE_EXCEPTIONS as err:
                key_error = err

            try:
                value_result = value_loader(value_)
            except CAPTURE_EXCEPTIONS as err:
                value_error = err

            if not key_error and not value_error:
                _result[key_result] = value_result
            else:
                if key_error:
                    _key_errors.append((key_, key_error))
                if value_error:
                    _value_errors.append((key_, value_error))

        if not _key_errors and not _value_errors:
            if type_back:
                _result = type(value)(_result)
            return _result
        else:
            raise MappingError(_key_errors, _value_errors)

    return method('items') & Loader(_load)


def mpfilter(check: Callable[[Any, Any], bool], type_back: bool = True) -> ILoaderClass:
    """
    Overview:
        Create a mapping filter loader.
    Arguments:
        - check (:obj:`Callable[[Any, Any], bool]`): The check function.
        - type_back (:obj:`bool`): Whether to convert the type back.
    """

    def _load(value):
        _result = {key_: value_ for key_, value_ in value.items() if check(key_, value_)}

        if type_back:
            _result = type(value)(_result)
        return _result

    return method('items') & Loader(_load)


def mpkeys() -> ILoaderClass:
    """
    Overview:
        Create a mapping keys loader.
    """

    return method('items') & method('keys') & Loader(lambda v: set(v.keys()))


def mpvalues() -> ILoaderClass:
    """
    Overview:
        Create a mapping values loader.
    """

    return method('items') & method('values') & Loader(lambda v: set(v.values()))


def mpitems() -> ILoaderClass:
    """
    Overview:
        Create a mapping items loader.
    """

    return method('items') & Loader(lambda v: set([(key, value) for key, value in v.items()]))


_INDEX_PRECHECK = method('__getitem__')


def item(key) -> ILoaderClass:
    """
    Overview:
        Create a item loader.
    Arguments:
        - key (:obj:`Any`): The key.
    """

    return _INDEX_PRECHECK & Loader(
        (lambda v: key in v.keys(), lambda v: v[key], KeyError('key {key} not found'.format(key=repr(key))))
    )


def item_or(key, default) -> ILoaderClass:
    """
    Overview:
        Create a item or loader.
    Arguments:
        - key (:obj:`Any`): The key.
        - default (:obj:`Any`): The default value.
    """

    return _INDEX_PRECHECK & (item(key) | raw(default))