"""
.. module:: riak_object.py

"""

import re
import csv
import urllib
from twisted.internet import defer

from riakasaurus import util, mapreduce, riak_link, riak_index_entry
from riakasaurus.metadata import *

class RiakObject(object):
    """
    The RiakObject holds meta information about a Riak object, plus the
    object's data.
    """

    def __init__(self, client, bucket, key=None):
        """
        Construct a new RiakObject.
        :param client: A RiakClient object.
        :param bucket: A RiakBucket object.
        :param key: Optional key.

        If key is not specified, then it is generated by server when
        store(...) is called.
        """
        self._client = client
        self._bucket = bucket
        self._key = key
        self._jsonize = True
        self._headers = {}
        self._links = []
        self._siblings = []
        self._metas = {}
        self._usermeta = {}
        self._indexes = []
        self._exists = False
        self._data = None

    def get_bucket(self):
        """
        Get the bucket of this object.
        @return RiakBucket
        .. todo:: remove accessor
        """
        return self._bucket

    def get_key(self):
        """
        Get the key of this object.
        @return string
        .. todo:: remove accessor
        """
        return self._key

    def get_data(self):
        """
        Get the data stored in this object. Will return a associative
        array, unless the object was constructed with new_binary(...) or
        get_binary(...), in which case this will return a string.
        @return array or string
        .. todo:: remove accessor
        """
        return self._data

    def get_encoded_data(self):
        """
        Get the data encoded for storing
        """
        if self._jsonize == True:
            content_type = self.get_content_type()
            encoder = self._client.get_encoder(content_type)
            if encoder is None:
                if isinstance(self._data, basestring):
                    return self._data.encode()
                else:
                    raise RiakError("No encoder for non-string data "
                                    "with content type ${0}".
                                    format(content_type))
            else:
                return encoder(self._data)
        else:
            return self._data

    def set_data(self, data):
        """
        Set the data stored in this object. This data will be
        JSON encoded unless the object was constructed with
        new_binary(...) or get_binary(...).
        @param mixed data - The data to store.
        @return data
        .. todo:: remove accessor
        """
        self._data = data
        return self

    def set_encoded_data(self, data):
        """
        Set the object data from an encoded string. Make sure
        the metadata has been set correctly first.
        """
        if self._jsonize == True:
            content_type = self.get_content_type()
            decoder = self._client.get_decoder(content_type)
            if decoder is None:
                # if no decoder, just set as string data for application to handle
                self._data = data
            else:
                self._data = decoder(data)
        else:
            self._data = data
        return self

    def status(self):
        """
        Get the HTTP status from the last operation on this object.
        @return integer
        .. todo:: convert to property
        """
        return self._headers['http_code']

    def exists(self):
        """
        Return True if the object exists, False otherwise. Allows you to
        detect a get(...) or get_binary(...) operation where the
        object is missing.
        @return boolean
        .. todo:: remove accessor
        """
        return self._exists

    def get_content_type(self):
        """
        Get the content type of this object. This is either text/json, or
        the provided content type if the object was created via
        new_binary(...).
        @return string
        .. todo:: convert to property
        """
        return self._headers.get('content-type', 'text/json')

    def set_content_type(self, content_type):
        """
        Set the content type of this object.
        @param string content_type - The new content type.
        @return self
        .. todo:: convert to property
        """
        self._headers['content-type'] = content_type
        return self

    def add_index(self, field, value):
        """
        Tag this object with the specified field/value pair for indexing.

        :param field: The index field.
        :type field: string
        :param value: The index value.
        :type value: string or integer
        :rtype: self
        """
        rie = riak_index_entry.RiakIndexEntry(field, value)
        if not rie in self._indexes:
            self._indexes.append(rie)

        return self

    def remove_index(self, field=None, value=None):
        """
        Remove the specified field/value pair as an index on this object.

        :param field: The index field.
        :type field: string
        :param value: The index value.
        :type value: string or integer
        :rtype: self
        """
        if not field and not value:
            ries = self._indexes[:]
        elif field and not value:
            ries = [x for x in self._indexes if x.get_field() == field]
        elif field and value:
            ries = [riak_index_entry.RiakIndexEntry(field, value)]
        else:
            raise Exception("Cannot pass value without a field name while removing index")

        for rie in ries:
            if rie in self._indexes:
                self._indexes.remove(rie)
        return self

    remove_indexes = remove_index

    def set_indexes(self, indexes):
        """
        Replaces all indexes on a Riak object. Currenly supports an iterable of 2 item tuples,
        (field, value)

        :param indexes: iterable of 2 item tuples consisting the field and value.
        :rtype: self
        """
        new_indexes = []
        for field, value in indexes:
            rie = riak_index_entry.RiakIndexEntry(field, value)
            new_indexes.append(rie)
        self._indexes = new_indexes

        return self

    def get_indexes(self, field = None):
        """
        Get a list of the index entries for this object. If a field is provided, returns a list

        :param field: The index field.
        :type field: string or None
        :rtype: (array of RiakIndexEntry) or (array of string or integer)
        """
        if field == None:
            return self._indexes
        else:
            return [x.get_value() for x in self._indexes if x.get_field() == field]

    def add_link(self, obj, tag=None):
        """
        Add a link to a RiakObject.
        :param obj:  RiakObject or a RiakLink object.
        :param tag: Optional link tag.
        :returns: RiakObject

        Default for the tag is the bucket name.
        Tag is ignored if obj is a RiakLink object.
        """
        if isinstance(obj, riak_link.RiakLink):
            newlink = obj
        else:
            newlink = riak_link.RiakLink(obj._bucket._name, obj._key, tag)

        self.remove_link(newlink)
        self._links.append(newlink)
        return self

    def remove_link(self, obj, tag=None):
        """
        Remove a link to a RiakObject.

        :param obj:RiakObject or a RiakLink object.
        :param tag: Optional link tag.
        :returns: self

        Default for the tag is the bucket name.
        Tag is ignored if obj is a RiakLink object.
        """
        if isinstance(obj, riak_link.RiakLink):
            oldlink = obj
        else:
            oldlink = riak_link.RiakLink(obj._bucket._name, obj._key, tag)

        a = []
        for link in self._links:
            if not link.isEqual(oldlink):
                a.append(link)

        self._links = a
        return self

    def get_links(self):
        """
        Return a list of RiakLink objects.

        :returns: list of RiakLink objects
        """
        # Set the clients before returning...
        for link in self._links:
            link._client = self._client
        return self._links

    def add_meta_data(self, key, data):
        """
        Add one meta data to object.
        Store in a dictionary so there's only one copy.

        :param key: string to add to metas dictionary
        :param data: data value for key
        :returns: self
        """
        self._metas[key] = data
        return self

    def remove_meta_data(self, data):
        """
        Remove meta data from object.

        :param data: string to remove from meta data.
        :returns: self
        """

        try:
            del(self._metas[data])
        except KeyError:
            pass

        return self

    def get_all_meta_data(self):
        """
        Return dictionary of meta data.
        """

        return self._metas

    @defer.inlineCallbacks
    def store(self, w=None, dw=None, pw=None, return_body=True, if_none_match=False):
        """
        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 w: Wait for this many partitions to respond
                  before returning to client.
        :param dw: Wait for this many partitions to confirm the
                   write before returning to client.
        :param return_body: If the newly stored object should be retrieved again.

        :returns: deferred

        """

        # Use defaults if not specified...
        w = self._bucket.get_w(w)
        dw = self._bucket.get_dw(dw)
        pw = self._bucket.get_pw(pw)

        # Issue the put over our transport
        t = self._client.transport

        if self._key is None:
            key, vclock, metadata = yield t.put_new(self, w=w, dw=dw, pw=pw, return_body=return_body, if_none_match=if_none_match)
            self._exists = True
            self._key = key
            self._vclock = vclock
            self.set_metadata(metadata)
        else:
            Result = yield t.put(self, w=w, dw=dw, pw=pw, return_body=return_body, if_none_match=if_none_match)
            if Result is not None:
                self.populate(Result)

        defer.returnValue(self)


    @defer.inlineCallbacks
    def reload(self, r=None, pr=None, vtag=None):
        """
        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.

        :param r: Wait for this many partitions to respond before
                  returning to client.
        :returns: self -- via deferred
        """
        # Do the request...
        r = self._bucket.get_r(r)

        r = self._bucket.get_r(r)
        pr = self._bucket.get_pr(pr)

        result = yield self._client.transport.get(self, r=r, pr=pr, vtag=vtag)

        self.clear()
        if result is not None:
            self.populate(result)

        defer.returnValue(self)

    @defer.inlineCallbacks
    def delete(self, w=None, dw=None):
        """
        Delete this object from Riak.

        :param w: wait for this many partitions to respond
            before returning to client.
        :type w: integer
        :param dw: wait for this many partitions to
            confirm the write before returning to client.
        :type dw: integer
        :returns: self -- via deferred
        """
        # Use defaults if not specified...
        rw = self._bucket.get_rw(rw)
        r = self._bucket.get_r(r)
        w = self._bucket.get_w(w)
        dw = self._bucket.get_dw(dw)
        pr = self._bucket.get_pr(pr)
        pw = self._bucket.get_pw(pw)

        # Run the operation...
        response = yield self._client.transport.delete(
                self, rw=rw, r=r, w=w, dw=dw, pr=pr, pw=pw)

        self.clear()
        defer.returnValue(self)

    def clear(self):
        """
        Reset this object.
        This is a local operation that clears the object,
        not the Riak stored version.
        :returns: self
        """
        self._headers = {}
        self._links = []
        self._data = None
        self._metas = {}
        self._usermeta = {}
        self._exists = False
        self._siblings = []
        self._indexes = []
        return self

    def vclock(self):
        """
        Get the vclock of this object.
        @return string
        .. todo:: convert to property
        """
        if ('x-riak-vclock' in self._headers.keys()):
            return self._headers['x-riak-vclock']
        else:
            return None

    def populate(self, Result) :
        """
        Populate the object based on the return from get.

        If None returned, then object is not found
        If a tuple of vclock, contents then one or more
        whole revisions of the key were found
        If a list of vtags is returned there are multiple
        sibling that need to be retrieved with get.
        """
        self.clear()
        if Result is None:
            return self
        elif type(Result) == types.ListType:
            self.set_siblings(Result)
        elif type(Result) == types.TupleType:
            (vclock, contents) = Result
            self._vclock = vclock
            if len(contents) > 0:
                (metadata, data) = contents.pop(0)
                self._exists = True
                if not metadata.has_key(MD_INDEX):
                    metadata[MD_INDEX] = []
                self.set_metadata(metadata)
                self.set_encoded_data(data)
                # Create objects for all siblings
                siblings = [self]
                for (metadata, data) in contents:
                    sibling = copy.copy(self)
                    sibling.set_metadata(metadata)
                    sibling.set_encoded_data(data)
                    siblings.append(sibling)
                for sibling in siblings:
                    sibling.set_siblings(siblings)
        else:
            raise RiakError("do not know how to handle type " + str(type(Result)))

    def set_metadata(self, metadata):
        """
        Set the metadata stored in this object.

        :param metadata: The data to store.
        :type metadata: dict
        :rtype: data
        """
        self._metas = metadata
        return self

    def get_metadata(self):
        """
        Get the metadata stored in this object. Will return an associative
        array

        :rtype: dict
        """
        return self._metas

    def get_usermeta(self):
        return self._usermeta

    def set_usermeta(self, usermeta):
        """
        Sets the custom user metadata on this object. This doesn't include things
        like content type and links, but only user-defined meta attributes stored
        with the Riak object.

        :param userdata: The user metadata to store.
        :type userdata: dict
        :rtype: data
        """
        self._usermeta = usermeta
        return self

    def _populate_metas(self):
        """
        Scan headers looking for x-riak-meta-
        """
        self._metas = {}
        for head in self._headers.keys():
            # Twisted lowercases headers.
            if 'x-riak-meta-' in head:
                data = self._headers[head]
                key = head.replace('x-riak-meta-', '')
                self._metas[key] = data

    def _populate_links(self, link_headers):
        """
        Parse out the links.

        :param link_headers: comma-delimited list of links
        :returns: self
        """
        # Twisted returns this as a list.
        for link_header in link_headers.strip().split(','):
            link_header = link_header.strip()
            matches = re.match(
                 "\<\/([^\/]+)\/([^\/]+)\/([^\/]+)\>; ?riaktag=\"([^\']+)\"",
                 link_header)
            if (matches != None):
                link = riak_link.RiakLink(urllib.unquote_plus(matches.group(2)),
                                          urllib.unquote_plus(matches.group(3)),
                                          urllib.unquote_plus(matches.group(4)))
                self._links.append(link)

        return self

    def has_siblings(self):
        """
        Return True if this object has siblings.

        """
        return(self.get_sibling_count() > 0)

    def get_sibling_count(self):
        """
        Get the number of siblings that this object contains.

        """
        return len(self._siblings)

    @defer.inlineCallbacks
    def get_sibling(self, i, r=None):
        """
        Retrieve a sibling by sibling number.

        :param i: sibling number
        :param r: R-Value. Wait until this many particitions have responded.
        :returns: RiakObject

        """
        # Use defaults if not specified.
        r = self._bucket.get_r(r)

        # Run the request...
        vtag = self._siblings[i]
        params = {'r': r, 'vtag': vtag}

        # Respond with a new object...
        obj = RiakObject(self._client, self._bucket, self._key)
        obj._jsonize = self._jsonize
        obj.populate(response)
        defer.returnValue(obj)

    @defer.inlineCallbacks
    def get_siblings(self, r=None):
        """
        Retrieve an array of siblings.

        :param r: R-Value. Wait until this many particitions have responded.
        :returns: array of RiakObject -- via deferred
        """
        a = []
        for i in range(self.get_sibling_count()):
            result = yield self.get_sibling(i, r)
            a.append(result)
        defer.returnValue(a)

    def add(self, *args):
        """
        Start assembling a Map/Reduce operation.
        see RiakMapReduce.add()

        :returns: RiakMapReduce
        """
        mr = mapreduce.RiakMapReduce(self._client)
        mr.add(self._bucket._name, self._key)
        return apply(mr.add, args)

    def link(self, *args):
        """
        Start assembling a Map/Reduce operation.
        see RiakMapReduce.link()
        :returns: RiakMapReduce
        """
        mr = mapreduce.RiakMapReduce(self._client)
        mr.add(self._bucket._name, self._key)
        return apply(mr.link, args)

    def map(self, *args):
        """
        Start assembling a Map/Reduce operation.
        see RiakMapReduce.map()
        :returns: RiakMapReduce
        """
        mr = mapreduce.RiakMapReduce(self._client)
        mr.add(self._bucket._name, self._key)
        return apply(mr.map, args)

    def reduce(self, *args):
        """
        Start assembling a Map/Reduce operation.
        see RiakMapReduce.reduce()
        :returns: RiakMapReduce
        """
        mr = mapreduce.RiakMapReduce(self._client)
        mr.add(self._bucket._name, self._key)

        return apply(mr.reduce, args)
