import decimal
import os

from datetime import date, timedelta, datetime, time, timezone

from django.conf import settings
from django.core.validators import EMPTY_VALUES
from django.utils.dateparse import (
    parse_duration,
    parse_datetime,
    parse_date,
    parse_time,
)
from django.utils.duration import duration_string
from django.utils.encoding import force_str
from django.utils.timezone import (
    is_aware,
    make_aware,
    make_naive,
    get_default_timezone,
)
from django.db.models.fields.files import FieldFile


class UnsetValue(object):
    pass


UNSET = UnsetValue()


class SerializationError(Exception):
    pass


class BaseSerializer:
    """
    A serializer take a Python variable and returns a string that can be stored safely in database
    """

    exception = SerializationError

    @classmethod
    def serialize(cls, value, **kwargs):
        """
        Return a string from a Python var
        """
        return cls.to_db(value, **kwargs)

    @classmethod
    def deserialize(cls, value, **kwargs):
        """
        Convert a python string to a var
        """
        return cls.to_python(value, **kwargs)

    @classmethod
    def to_python(cls, value, **kwargs):
        raise NotImplementedError

    @classmethod
    def to_db(cls, value, **kwargs):
        return str(cls.clean_to_db_value(value))

    @classmethod
    def clean_to_db_value(cls, value):
        return value


class InstanciatedSerializer(BaseSerializer):
    """
    In some situations, such as with FileSerializer,
    we need the serializer to be an instance and not a class
    """

    def serialize(self, value, **kwargs):
        return self.to_db(value, **kwargs)

    def deserialize(self, value, **kwargs):
        return self.to_python(value, **kwargs)

    def to_python(self, value, **kwargs):
        raise NotImplementedError

    def to_db(self, value, **kwargs):
        return str(self.clean_to_db_value(value))

    def clean_to_db_value(self, value):
        return value


class BooleanSerializer(BaseSerializer):
    true = (
        "True",
        "true",
        "TRUE",
        "1",
        "YES",
        "Yes",
        "yes",
    )

    false = (
        "False",
        "false",
        "FALSE",
        "0",
        "No",
        "no",
        "NO",
    )

    @classmethod
    def clean_to_db_value(cls, value):
        if not isinstance(value, bool):
            raise cls.exception("{0} is not a boolean".format(value))
        return value

    @classmethod
    def to_python(cls, value, **kwargs):

        if value in cls.true:
            return True

        elif value in cls.false:
            return False

        else:
            raise cls.exception(
                "Value {0} can't be deserialized to a Boolean".format(value)
            )


class IntegerSerializer(BaseSerializer):
    @classmethod
    def clean_to_db_value(cls, value):
        if not isinstance(value, int):
            raise cls.exception("IntSerializer can only serialize int values")
        return value

    @classmethod
    def to_python(cls, value, **kwargs):
        try:
            return int(value)
        except:
            raise cls.exception("Value {0} cannot be converted to int".format(value))


IntSerializer = IntegerSerializer


class DecimalSerializer(BaseSerializer):
    @classmethod
    def clean_to_db_value(cls, value):
        if not isinstance(value, decimal.Decimal):
            raise cls.exception(
                "DecimalSerializer can only serialize Decimal instances"
            )
        return value

    @classmethod
    def to_python(cls, value, **kwargs):
        try:
            return decimal.Decimal(value)
        except decimal.InvalidOperation:
            raise cls.exception(
                "Value {0} cannot be converted to decimal".format(value)
            )


class FloatSerializer(BaseSerializer):
    @classmethod
    def clean_to_db_value(cls, value):
        if not isinstance(value, (int, float)):
            raise cls.exception(
                "FloatSerializer can only serialize float or int values"
            )
        return float(value)

    @classmethod
    def to_python(cls, value, **kwargs):
        try:
            return float(value)
        except float.InvalidOperation:
            raise cls.exception("Value {0} cannot be converted to float".format(value))


from django.template import defaultfilters


class StringSerializer(BaseSerializer):
    @classmethod
    def to_db(cls, value, **kwargs):
        if not isinstance(value, str):
            raise cls.exception(
                "Cannot serialize, value {0} is not a string".format(value)
            )

        if kwargs.get("escape_html", False):
            return defaultfilters.force_escape(value)
        else:
            return value

    @classmethod
    def to_python(cls, value, **kwargs):
        """String deserialisation just return the value as a string"""
        if not value:
            return ""
        try:
            return str(value)
        except:
            pass
        try:
            return value.encode("utf-8")
        except:
            pass
        raise cls.exception("Cannot deserialize value {0} tostring".format(value))


class ModelSerializer(InstanciatedSerializer):
    model = None

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

    def to_db(self, value, **kwargs):
        if not value or (value == UNSET):
            return None
        return str(value.pk)

    def to_python(self, value, **kwargs):
        if value is None:
            return
        try:
            pk = self.model._meta.pk.to_python(value)
            return self.model.objects.get(pk=pk)
        except:
            raise self.exception("Value {0} cannot be converted to pk".format(value))


class ModelMultipleSerializer(ModelSerializer):
    separator = ","
    sort = True

    def to_db(self, value, **kwargs):
        if not value:
            return
        if hasattr(value, "pk"):
            # Support single instances in this serializer to allow
            # create_deletion_handler to work for model multiple choice preferences
            value = [value.pk]
        elif hasattr(value, 'values_list'):
            value = list(value.values_list("pk", flat=True))
        elif isinstance(value, list) and len(value) > 0 and isinstance(value[0], self.model):
            # Handle lists of model instances
            value = [i.pk for i in value]
        else:
            raise ValueError(f'Cannot handle value {value} of type {type(value)}')
        if value and self.sort:
            value = sorted(value)

        return self.separator.join(map(str, value))

    def to_python(self, value, **kwargs):
        if value in EMPTY_VALUES:
            return self.model.objects.none()

        try:
            pks = value.split(",")
            pks = [int(i) if str(i).isdigit() else str(i) for i in pks]
            return self.model.objects.filter(pk__in=pks)
        except:
            raise self.exception("Array {0} cannot be converted to int".format(value))


# FieldFile also needs a model instance to save changes.
class FakeInstance(object):
    """
    FieldFile needs a model instance to update when file is persisted
    or deleted
    """

    def save(self):
        return


class FakeField(object):
    """
    FieldFile needs a field object to generate a filename, persist
    and delete files, so we are effectively mocking that.
    """

    name = "noop"
    attname = "noop"
    max_length = 10000


class PreferenceFieldFile(FieldFile):
    """
    In order to have the same API that we have with models.FileField,
    we must return a FieldFile object. However, there are various
    things we have to override, since our files are not bound to a model
    field.
    """

    def __init__(self, preference, storage, name):
        super(FieldFile, self).__init__(None, name)
        self.instance = FakeInstance()
        self.field = FakeField()
        self.field.storage = storage
        self.storage = storage
        self._committed = True
        self.preference = preference


class FileSerializer(InstanciatedSerializer):
    """
    Since this serializer requires additional data from the preference
    especially the upload path, we cannot do it without binding it
    to the preference

    it is therefore designed to be explicitely instanciated by the preference
    object.
    """

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

    def to_db(self, f, **kwargs):
        if not f:
            return
        saved_path = f.name
        if not hasattr(f, "save"):
            path = os.path.join(self.preference.get_upload_path(), f.name)
            saved_path = self.preference.get_file_storage().save(path, f)

        return saved_path

    def to_python(self, value, **kwargs):
        if not value:
            return
        storage = self.preference.get_file_storage()

        return PreferenceFieldFile(
            preference=self.preference, storage=storage, name=value
        )


class DurationSerializer(BaseSerializer):
    @classmethod
    def to_db(cls, value, **kwargs):
        if not isinstance(value, timedelta):
            raise cls.exception(
                "Cannot serialize, value {0} is not a timedelta".format(value)
            )

        return duration_string(value)

    @classmethod
    def to_python(cls, value, **kwargs):
        parsed = parse_duration(force_str(value))
        if parsed is None:
            raise cls.exception(
                "Value {0} cannot be converted to timedelta".format(value)
            )
        return parsed


class DateSerializer(BaseSerializer):
    @classmethod
    def to_db(cls, value, **kwargs):
        if not isinstance(value, date):
            raise cls.exception(
                "Cannot serialize, value {0} is not a date object".format(value)
            )

        return value.isoformat()

    @classmethod
    def to_python(cls, value, **kwargs):
        parsed = parse_date(force_str(value))
        if parsed is None:
            raise cls.exception(
                "Value {0} cannot be converted to a date object".format(value)
            )

        return parsed


class DateTimeSerializer(BaseSerializer):
    @classmethod
    def to_db(cls, value, **kwargs):
        if not isinstance(value, datetime):
            raise cls.exception(
                "Cannot serialize, value {0} is not a datetime object".format(value)
            )

        value = cls.enforce_timezone(value)

        return value.isoformat()

    @classmethod
    def enforce_timezone(cls, value):
        """
        When `self.default_timezone` is `None`, always return naive datetimes.
        When `self.default_timezone` is not `None`, always return aware datetimes.
        """
        field_timezone = cls.default_timezone()

        if (field_timezone is not None) and not is_aware(value):
            return make_aware(value, field_timezone)
        elif (field_timezone is None) and is_aware(value):
            return make_naive(value, timezone.utc)
        return value

    @classmethod
    def default_timezone(cls):
        return get_default_timezone() if settings.USE_TZ else None

    @classmethod
    def to_python(cls, value, **kwargs):
        parsed = parse_datetime(force_str(value))
        if parsed is None:
            raise cls.exception(
                "Value {0} cannot be converted to a datetime object".format(value)
            )
        return parsed


class TimeSerializer(BaseSerializer):
    @classmethod
    def to_db(cls, value, **kwargs):
        if not isinstance(value, time):
            raise cls.exception(
                "Cannot serialize, value {0} is not a time object".format(value)
            )

        return value.isoformat()

    @classmethod
    def to_python(cls, value, **kwargs):
        parsed = parse_time(force_str(value))
        if parsed is None:
            raise cls.exception(
                "Value {0} cannot be converted to a time object".format(value)
            )

        return parsed


class MultipleSerializer(BaseSerializer):
    separator = ","
    sort = True

    @classmethod
    def to_db(cls, value, **kwargs):
        if not value:
            return

        # This makes the use of the separator in choices safe by duplicating
        # it in each value before they are joined later on
        # Contract: choices keys cannot be empty
        value = [str(v).replace(cls.separator, cls.separator * 2) for v in value]
        if "" in value:
            raise cls.exception("Choices must not be empty")

        if cls.sort:
            value = sorted(value)

        return cls.separator.join(value)

    @classmethod
    def to_python(cls, value, **kwargs):
        if value in EMPTY_VALUES:
            return []

        ret = value.split(cls.separator)
        # Duplication of separator is reverted (cf. to_db)
        while "" in ret:
            pos = ret.index("")
            val = ret[pos - 1] + cls.separator + ret[pos + 1]
            ret = ret[0: pos - 1] + [val] + ret[pos + 2:]
        return ret
