from collections import OrderedDict
from uuid import UUID
from future.moves.urllib.parse import urlparse
from six import string_types, callable
from datetime import datetime
from inspect import isfunction
from dateutil import parser
from importlib import import_module

from .types import TypedSequence, TypedMapping, TypedSet
from .functions import to_model

CHILD_ERROR_MSG = "Failed to convert value ({}) to child object class ({}). " \
                  + "... [Original error message: {}]"


def to_child_field(cls):
    """
    Returns an callable instance that will convert a value to a Child object.

    :param cls: Valid class type of the Child.
    :return: instance of ChildConverter.
    """

    class ChildConverter(object):

        def __init__(self, cls):
            self._cls = cls

        @property
        def cls(self):
            return resolve_class(self._cls)

        def __call__(self, value):
            try:
                # Issue #33: if value is the class and callable, then invoke
                if value == self._cls and callable(value):
                    value = value()

                return to_model(self.cls, value)
            except ValueError as e:
                error_msg = CHILD_ERROR_MSG.format(value, self.cls, str(e))
                raise ValueError(error_msg)

    return ChildConverter(cls)


def to_sequence_field(cls):
    """
    Returns a callable instance that will convert a value to a Sequence.

    :param cls: Valid class type of the items in the Sequence.
    :return: instance of the SequenceConverter.
    """
    class SequenceConverter(object):

        def __init__(self, cls):
            self._cls = cls

        @property
        def cls(self):
            return resolve_class(self._cls)

        def __call__(self, values):
            values = values or []
            args = [to_model(self.cls, value) for value in values]
            return TypedSequence(cls=self.cls, args=args)

    return SequenceConverter(cls)


def to_set_field(cls):
    """
    Returns a callable instance that will convert a value to a Sequence.

    :param cls: Valid class type of the items in the Sequence.
    :return: instance of the SequenceConverter.
    """
    class SetConverter(object):

        def __init__(self, cls):
            self._cls = cls

        @property
        def cls(self):
            return resolve_class(self._cls)

        def __call__(self, values):
            values = values or set()
            args = {to_model(self.cls, value) for value in values}
            return TypedSet(cls=self.cls, args=args)

    return SetConverter(cls)


def to_mapping_field(cls, key):  # pragma: no mccabe
    """
    Returns a callable instance that will convert a value to a Mapping.

    :param cls: Valid class type of the items in the Sequence.
    :param key: Attribute name of the key value in each item of cls instance.
    :return: instance of the MappingConverter.
    """
    class MappingConverter(object):

        def __init__(self, cls, key):
            self._cls = cls
            self.key = key

        @property
        def cls(self):
            return resolve_class(self._cls)

        def __call__(self, values):
            kwargs = OrderedDict()

            if isinstance(values, TypedMapping):
                return values

            if not isinstance(values, (type({}), type(None))):
                raise TypeError("Invalid type : {}".format(type(values)))

            if values:
                for key_value, item in values.items():
                    if isinstance(item, dict):
                        item[self.key] = key_value
                        item = to_model(self.cls, item)
                    kwargs[key_value] = item

            return TypedMapping(cls=self.cls, kwargs=kwargs, key=self.key)

    return MappingConverter(cls, key)


def str_if_not_none(value):
    """
    Returns an str(value) if the value is not None.

    :param value: None or a value that can be converted to a str.
    :return: None or str(value)
    """
    if not(value is None or isinstance(value, string_types)):
        value = str(value)

    return value


def int_if_not_none(value):
    """
    Returns an int(value) if the value is not None.

    :param value: None or a value that can be converted to an int.
    :return: None or int(value)
    """
    return None if value is None else int(value)


def float_if_not_none(value):
    """
    Returns an float(value) if the value is not None.

    :param value: None or a value that can be converted to an float.
    :return: None or float(value)
    """
    return None if value is None else float(value)


def str_to_url(value):
    """
    Returns a UUID(value) if the value provided is a str.

    :param value: str or UUID object
    :return: UUID object
    """
    return urlparse(value) if isinstance(value, string_types) else value


def str_to_uuid(value):
    """
    Returns a UUID(value) if the value provided is a str.

    :param value: str or UUID object
    :return: UUID object
    """
    if isfunction(value):
        value = value()

    return UUID(value) if isinstance(value, string_types) else value


def to_date_field(formatter):
    """
    Returns a callable instance that will convert a string to a Date.

    :param formatter: String that represents data format for parsing.
    :return: instance of the DateConverter.
    """
    class DateConverter(object):

        def __init__(self, formatter):
            self.formatter = formatter

        def __call__(self, value):
            if isinstance(value, string_types):
                value = datetime.strptime(value, self.formatter).date()

            if isinstance(value, datetime):
                value = value.date()

            return value

    return DateConverter(formatter)


def to_datetime_field(formatter):
    """
    Returns a callable instance that will convert a string to a DateTime.

    :param formatter: String that represents data format for parsing.
    :return: instance of the DateTimeConverter.
    """
    class DateTimeConverter(object):

        def __init__(self, formatter):
            self.formatter = formatter

        def __call__(self, value):
            if isinstance(value, string_types):
                value = parser.parse(value)

            return value

    return DateTimeConverter(formatter)


def to_time_field(formatter):
    """
    Returns a callable instance that will convert a string to a Time.

    :param formatter: String that represents data format for parsing.
    :return: instance of the TimeConverter.
    """
    class TimeConverter(object):

        def __init__(self, formatter):
            self.formatter = formatter

        def __call__(self, value):
            if isinstance(value, string_types):
                value = datetime.strptime(value, self.formatter).time()

            return value

    return TimeConverter(formatter)


def resolve_class(cls):
    if isinstance(cls, str):
        module_name, model_name = cls.rsplit(".", 1)
        module = import_module(module_name)
        cls = getattr(module, model_name)
    return cls