from aioriak.content import RiakContent
from aioriak.error import ConflictError


def content_property(name, doc=None):
    '''
    Delegates a property to the first sibling in a RiakObject, raising
    an error when the object is in conflict.
    '''
    def _setter(self, value):
        if len(self.siblings) == 0:
            # In this case, assume that what the user wants is to
            # create a new sibling inside an empty object.
            self.siblings = [RiakContent(self)]
        if len(self.siblings) != 1:
            raise ConflictError()
        setattr(self.siblings[0], name, value)

    def _getter(self):
        if len(self.siblings) == 0:
            return
        if len(self.siblings) != 1:
            raise ConflictError()
        return getattr(self.siblings[0], name)

    return property(_getter, _setter, doc=doc)


class RiakObject:
    '''
    The RiakObject holds meta information about a Riak object, plus the
    object's data.
    '''
    siblings = []

    def __init__(self, client, bucket, key=None):
        '''
        Construct a new RiakObject.

        :param client: A RiakClient object.
        :type client: :class:`RiakClient <aioriak.client.RiakClient>`
        :param bucket: A RiakBucket object.
        :type bucket: :class:`RiakBucket <aioriak.bucket.RiakBucket>`
        :param key: An optional key. If not specified, then the key
         is generated by the server when :func:`store` is called.
        :type key: string
        '''

        if key is not None and len(key) == 0:
            raise ValueError('Key name must either be "None"'
                             ' or a non-empty string.')
        self.client = client
        self.bucket = bucket
        self.key = key
        self.vclock = None
        self.siblings = [RiakContent(self)]
        self._resolver = None

    def _exists(self):
        if len(self.siblings) == 0:
            return False
        elif len(self.siblings) > 1:
            # Even if all of the siblings are tombstones, the object
            # essentially exists.
            return True
        else:
            return self.siblings[0].exists

    exists = property(_exists, None, doc='''
        Whether the object exists. This is only ``False`` when there
        are no siblings (the object was not found), or the solitary
        sibling is a tombstone.
        ''')

    async def reload(self):
        '''
        Reload the object from Riak. When this operation completes, the
        object could contain new metadata and a new value, if the object
        was updated in Riak since it was last retrieved.

        .. note:: Even if the key is not found in Riak, this will
           return a :class:`RiakObject`. Check the :attr:`exists`
           property to see if the key was found.

        :rtype: :class:`RiakObject`
        '''

        await self.client.get(self)
        return self

    async def store(self, return_body=True):
        '''
        Store the object in Riak. When this operation completes, the
        object could contain new metadata and possibly new data if Riak
        contains a newer version of the object according to the object's
        vector clock.

        :param return_body: if the newly stored object should be retrieved
        :type return_body: bool
        :rtype: :class:`RiakObject` '''
        if len(self.siblings) != 1:
            raise ConflictError("Attempting to store an invalid object, "
                                "resolve the siblings first")

        await self.client.put(self, return_body)

        return self

    async def delete(self):
        '''
        Delete this object from Riak.

        :rtype: :class:`RiakObject`
        '''
        await self.client.delete(self)
        self.clear()
        return self

    def clear(self):
        '''
        Reset this object.

        :rtype: RiakObject
        '''
        self.siblings = []
        return self

    data = content_property('data', doc='''
        The data stored in this object, as Python objects. For the raw
        data, use the `encoded_data` property. If unset, accessing
        this property will result in decoding the `encoded_data`
        property into Python values. The decoding is dependent on the
        `content_type` property and the bucket's registered decoders.
        ''')

    encoded_data = content_property('encoded_data', doc='''
        The raw data stored in this object, essentially the encoded
        form of the `data` property. If unset, accessing this property
        will result in encoding the `data` property into a string. The
        encoding is dependent on the `content_type` property and the
        bucket's registered encoders.
        ''')

    charset = content_property('charset', doc='''
        The character set of the encoded data as a string
        ''')

    content_encoding = content_property('content_encoding', doc='''
        The encoding (compression) of the encoded data. Valid values
        are identity, deflate, gzip
        ''')

    usermeta = content_property('usermeta', doc='''
        Arbitrary user-defined metadata dict, mapping strings to strings.
        ''')

    links = content_property('links', doc='''
        A set of bucket/key/tag 3-tuples representing links to other
        keys.
        ''')

    content_type = content_property('content_type', doc='''
        The MIME media type of the encoded data as a string
        ''')

    indexes = content_property('indexes', doc='''
        The set of secondary index entries, consisting of
        index-name/value tuples
        ''')

    def _get_resolver(self):
        if callable(self._resolver):
            return self._resolver
        elif self._resolver is None:
            return self.bucket.resolver
        else:
            raise TypeError("resolver is not a function")

    def _set_resolver(self, value):
        if value is None or callable(value):
            self._resolver = value
        else:
            raise TypeError("resolver is not a function")

    resolver = property(_get_resolver, _set_resolver,
                        doc='''The sibling-resolution function for this
                        object. If the resolver is not set, the
                        bucket's resolver will be used.''')
