"""
    Based on django-picklefield which is
    Copyright (c) 2009-2010 Gintautas Miliauskas
    but some improvements including not deepcopying values.

    Provides an implementation of a pickled object field.
    Such fields can contain any picklable objects.

    The implementation is taken and adopted from Django snippet #1694
    <http://www.djangosnippets.org/snippets/1694/> by Taavi Taijala,
    which is in turn based on Django snippet #513
    <http://www.djangosnippets.org/snippets/513/> by Oliver Beattie.

"""
from __future__ import absolute_import, unicode_literals

import django

from base64 import b64encode, b64decode
from zlib import compress, decompress

from celery.five import with_metaclass
from celery.utils.serialization import pickle
from kombu.utils.encoding import bytes_to_str, str_to_bytes

from django.db import models

try:
    from django.utils.encoding import force_text
except ImportError:
    from django.utils.encoding import force_unicode as force_text  # noqa

DEFAULT_PROTOCOL = 2

NO_DECOMPRESS_HEADER = b'\x1e\x00r8d9qwwerwhA@'


if django.VERSION >= (1, 8):
    BaseField = models.Field
else:
    @with_metaclass(models.SubfieldBase, skip_attrs=set([
        'db_type',
        'get_db_prep_save'
    ]))
    class BaseField(models.Field):  # noqa
        pass


class PickledObject(str):
    pass


def maybe_compress(value, do_compress=False):
    if do_compress:
        return compress(str_to_bytes(value))
    return value


def maybe_decompress(value, do_decompress=False):
    if do_decompress:
        if str_to_bytes(value[:15]) != NO_DECOMPRESS_HEADER:
            return decompress(str_to_bytes(value))
    return value


def encode(value, compress_object=False, pickle_protocol=DEFAULT_PROTOCOL):
    return bytes_to_str(b64encode(maybe_compress(
        pickle.dumps(value, pickle_protocol), compress_object),
    ))


def decode(value, compress_object=False):
    return pickle.loads(maybe_decompress(b64decode(value), compress_object))


class PickledObjectField(BaseField):

    def __init__(self, compress=False, protocol=DEFAULT_PROTOCOL,
                 *args, **kwargs):
        self.compress = compress
        self.protocol = protocol
        kwargs.setdefault('editable', False)
        super(PickledObjectField, self).__init__(*args, **kwargs)

    def get_default(self):
        if self.has_default():
            return self.default() if callable(self.default) else self.default
        return super(PickledObjectField, self).get_default()

    def to_python(self, value):
        if value is not None:
            try:
                return decode(value, self.compress)
            except Exception:
                if isinstance(value, PickledObject):
                    raise
                return value

    def from_db_value(self, value, expression, connection, context):
        return self.to_python(value)

    def get_db_prep_value(self, value, **kwargs):
        if value is not None and not isinstance(value, PickledObject):
            return force_text(encode(value, self.compress, self.protocol))
        return value

    def value_to_string(self, obj):
        return self.get_db_prep_value(self._get_val_from_obj(obj))

    def get_internal_type(self):
        return 'TextField'

    def get_db_prep_lookup(self, lookup_type, value, *args, **kwargs):
        if lookup_type not in ['exact', 'in', 'isnull']:
            raise TypeError(
                'Lookup type {0} is not supported.'.format(lookup_type))
        return super(PickledObjectField, self) \
            .get_db_prep_lookup(*args, **kwargs)


try:
    from south.modelsinspector import add_introspection_rules
except ImportError:
    pass
else:
    add_introspection_rules(
        [], [r'^djcelery\.picklefield\.PickledObjectField'],
    )
