from zope.interface import implements

from twisted.protocols.basic import LineReceiver
LineReceiver.MAX_LENGTH = 1024*1024*64

from twisted.internet import defer, reactor, protocol
from twisted.python import log
import logging

from distutils.version import StrictVersion

import time

# MD_ resources
from riakasaurus.metadata import *

from riakasaurus.riak_index_entry import RiakIndexEntry
from riakasaurus.mapreduce import RiakLink

# protobuf
from riakasaurus.transport import transport, pbc

LOGLEVEL_DEBUG = 1
LOGLEVEL_TRANSPORT = 2
LOGLEVEL_TRANSPORT_VERBOSE = 4


class StatefulTransport(object):

    def __init__(self,transport):
        self.__transport = transport
        self.__state = 'idle'
        self.__created = time.time()
        self.__used = time.time()

    def __repr__(self):
        return '<StatefulTransport idle=%.2fs state=\'%s\' transport=%s>' % (time.time() - self.__used, self.__state, self.__transport)

    def isActive(self):
        return self.__state == 'active'

    def setActive(self):
        self.__state = 'active'
        self.__used = time.time()

    def isIdle(self):
        return self.__state == 'idle'

    def setIdle(self):
        self.__state = 'idle'
        self.__used = time.time()

    def getTransport(self):
        return self.__transport

    def age(self):
        return time.time() - self.__used


class PBCTransport(transport.FeatureDetection):
    """ Protocoll buffer transport for Riak """

    implements(transport.ITransport)

    debug = 0
    logToLevel = logging.INFO
    MAX_TRANSPORTS = 50
    MAX_IDLETIME   = 5*60     # in seconds
    GC_TIME        = 120        # how often (in seconds) the garbage collection should run
    timeout        = None

    def __init__(self, client):
        self._prefix = client._prefix
        self.host = client._host
        self.port = client._port
        self.client = client
        self._client_id = None
        self._transports = []    # list of transports, empty on start
        self._gc = reactor.callLater(self.GC_TIME, self._garbageCollect)

    def setTimeout(self,t):
        self.timeout = t

    @defer.inlineCallbacks
    def _getFreeTransport(self):
        foundOne = False
        for stp in self._transports:
            if stp.isIdle():
                stp.setActive()
                foundOne = True
                if self.debug & LOGLEVEL_TRANSPORT_VERBOSE:
                    log.msg("[%s] aquired idle transport[%d]: %s" % (self.__class__.__name__, len(self._transports),stp), logLevel = self.logToLevel)
                defer.returnValue(stp)
        if not foundOne:
            if len(self._transports) > self.MAX_TRANSPORTS:
                raise Exception("to many transports, aborting")

            # nothin free, create a new protocol instance, append
            # it to self._transports and return it
            transport = yield pbc.RiakPBCClient().connect(self.host, self.port)
            if self.timeout:
                transport.setTimeout(self.timeout)
            stp = StatefulTransport(transport)
            idx = len(self._transports)
            self._transports.append(stp)
            stp.setActive()
            if self.debug & LOGLEVEL_TRANSPORT:
                log.msg("[%s] allocate new transport[%d]: %s" % (self.__class__.__name__, len(self._transports),stp), logLevel = self.logToLevel)
            defer.returnValue(stp)

    @defer.inlineCallbacks
    def _garbageCollect(self):
        self._gc = reactor.callLater(self.GC_TIME, self._garbageCollect)
        for idx, stp in enumerate(self._transports):
            if (stp.isIdle() and stp.age() > self.MAX_IDLETIME):
                yield stp.getTransport().quit()
                if self.debug & LOGLEVEL_TRANSPORT:
                    log.msg("[%s] expire idle transport[%d] %s" % (self.__class__.__name__, idx,stp), logLevel = self.logToLevel)
                    log.msg("[%s] %s" % (self.__class__.__name__, self._transports), logLevel = self.logToLevel)
                self._transports.remove(stp)
            elif self.timeout and stp.isActive() and stp.age() > self.timeout:
                yield stp.getTransport().quit()
                if self.debug & LOGLEVEL_TRANSPORT:
                    log.msg("[%s] expire timeouted transport[%d] %s" % (self.__class__.__name__, idx,stp), logLevel = self.logToLevel)
                    log.msg("[%s] %s" % (self.__class__.__name__, self._transports), logLevel = self.logToLevel)
                self._transports.remove(stp)


    @defer.inlineCallbacks
    def quit(self):
        self._gc.cancel()      # cancel the garbage collector

        for stp in self._transports:
            if self.debug & LOGLEVEL_DEBUG:
                log.msg("[%s] transport[%d].quit() %s" % (self.__class__.__name__, len(self._transports),stp), logLevel = self.logToLevel)
            yield stp.getTransport().quit()

    def __del__(self):
        """on shutdown, close all transports"""
        self.quit()

    def put(self, robj, w = None, dw = None, pw = None, return_body = True, if_none_match=False):
        ret = self.__put(robj, w, dw, pw, return_body = return_body, if_none_match = if_none_match)
        if return_body:
            return ret
        else:
            return None

    def put_new(self, robj, w=None, dw=None, pw=None, return_body=True, if_none_match=False):
        ret = self.__put(robj, w, dw, pw, return_body = return_body, if_none_match = if_none_match)
        if return_body:
            return ret
        else:
            return (ret[0],None,None)


    @defer.inlineCallbacks
    def __put(self, robj, w = None, dw = None, pw = None, return_body=True, if_none_match=False):
        # std kwargs
        kwargs = {'w'             : w,
                  'dw'            : dw,
                  'pw'            : pw,
                  'return_body'   : return_body,
                  'if_none_match' : if_none_match
                  }
        # vclock
        vclock = robj.vclock() or None

        payload = {
            'value' : robj.get_encoded_data(),
            'content_type' : robj.get_content_type(),
            }

        # links
        links = robj.get_links()
        if links:
            payload['links'] = []
            for l in links:
                payload['links'].append((l.get_bucket(), l.get_key(), l.get_tag()))

        # usermeta
        if robj.get_usermeta():
            payload['usermeta'] = []
            for key, value in robj.get_usermeta().iteritems():
                payload['usermeta'].append((key, value))

        # indexes
        if robj.get_indexes():
            payload['indexes'] = []
            for index in robj.get_indexes():
                payload['indexes'].append((index.get_field(), index.get_value()))


        # aquire transport, fire, release
        stp = yield self._getFreeTransport()
        transport = stp.getTransport()
        ret = yield transport.put(robj.get_bucket().get_name(),
                                  robj.get_key(),
                                  payload,
                                  vclock,
                                  **kwargs
                                  )
        stp.setIdle()
        defer.returnValue(self.parseRpbGetResp(ret))


    @defer.inlineCallbacks
    def get(self, robj, r = None, pr = None, vtag = None):

        # ***FIXME*** whats vtag for? ignored for now

        stp = yield self._getFreeTransport()
        transport = stp.getTransport()
        ret = yield transport.get(robj.get_bucket().get_name(),
                                  robj.get_key(),
                                  r = r,
                                  pr = pr)

        stp.setIdle()
        defer.returnValue(self.parseRpbGetResp(ret))

    @defer.inlineCallbacks
    def head(self, robj, r = None, pr = None, vtag = None):
        stp = yield self._getFreeTransport()
        transport = stp.getTransport()
        ret = yield transport.get(robj.get_bucket().get_name(),
                                  robj.get_key(),
                                  r = r,
                                  pr = pr,
                                  head = True)

        stp.setIdle()
        defer.returnValue(self.parseRpbGetResp(ret))


    @defer.inlineCallbacks
    def delete(self, robj, rw=None, r = None, w = None, dw = None, pr = None, pw = None):
        """
        Delete an object.
        """
        # We could detect quorum_controls here but HTTP ignores
        # unknown flags/params.
        kwargs = {'rw' : rw, 'r': r, 'w': w, 'dw': dw, 'pr': pr, 'pw': pw}
        headers = {}

        ts = yield self.tombstone_vclocks()
        if ts and robj.vclock() is not None:
            kwargs['vclock'] = robj.vclock()

        stp = yield self._getFreeTransport()
        transport = stp.getTransport()
        ret = yield transport.delete(robj.get_bucket().get_name(),
                                     robj.get_key(),
                                     **kwargs
                                     )

        stp.setIdle()
        defer.returnValue(ret)


    @defer.inlineCallbacks
    def get_buckets(self):
        stp = yield self._getFreeTransport()
        transport = stp.getTransport()
        ret = yield transport.getBuckets()
        stp.setIdle()
        defer.returnValue([x for x in ret.buckets])


    @defer.inlineCallbacks
    def server_version(self):
        if not self._s_version:
            self._s_version = yield self._server_version()

        defer.returnValue(StrictVersion(self._s_version))

    @defer.inlineCallbacks
    def _server_version(self):
        stp = yield self._getFreeTransport()
        transport = stp.getTransport()

        stats = yield transport.getServerInfo()
        stp.setIdle()


        if stats is not None:
            if self.debug % LOGLEVEL_DEBUG:
                log.msg("[%s] fetched server version: %s" % (
                    self.__class__.__name__, stats.server_version
                ), logLevel = self.logToLevel)
            defer.returnValue(stats.server_version)
        else:
            defer.returnValue("0.14.0")

    @defer.inlineCallbacks
    def ping(self):
        """
        Check server is alive
        """
        stp = yield self._getFreeTransport()
        transport = stp.getTransport()
        ret = yield transport.ping()
        stp.setIdle()
        defer.returnValue(ret == True)


    @defer.inlineCallbacks
    def set_bucket_props(self, bucket, props):
        """
        Set bucket properties
        """
        stp = yield self._getFreeTransport()
        transport = stp.getTransport()
        ret = yield transport.setBucketProperties(bucket.get_name(), **props)
        stp.setIdle()
        defer.returnValue(ret == True)


    @defer.inlineCallbacks
    def get_bucket_props(self, bucket):
        """
        get bucket properties
        """
        stp = yield self._getFreeTransport()
        transport = stp.getTransport()
        ret = yield transport.getBucketProperties(bucket.get_name())
        stp.setIdle()
        defer.returnValue({'n_val'      : ret.props.n_val,
                           'allow_mult' : ret.props.allow_mult})

    @defer.inlineCallbacks
    def get_keys(self, bucket):
        stp = yield self._getFreeTransport()
        transport = stp.getTransport()
        ret = yield transport.getKeys(bucket.get_name())
        stp.setIdle()
        defer.returnValue(ret)

    @defer.inlineCallbacks
    def get_index(self, bucket, index, startkey, endkey=None):
        stp = yield self._getFreeTransport()
        transport = stp.getTransport()
        ret = yield transport.get_index(bucket, index, startkey, endkey=endkey)
        stp.setIdle()
        defer.returnValue(ret)

    def parseRpbGetResp(self,res):
        """
        adaptor for a RpbGetResp message
        message RpbGetResp {
           repeated RpbContent content = 1;
           optional bytes vclock = 2;
           optional bool unchanged = 3;
        }
        """
        if res == True:         # empty response
            return None
        vclock = res.vclock
        resList = []
        for content in res.content: # iterate over RpbContent field
            metadata = {MD_USERMETA: {}, MD_INDEX: []}
            data = content.value
            if content.HasField('content_type'): 
                metadata[MD_CTYPE] = content.content_type

            if content.HasField('charset'): 
                metadata[MD_CHARSET] = content.charset

            if content.HasField('content_encoding'): 
                metadata[MD_ENCODING] = content.content_encoding

            if content.HasField('vtag'): 
                metadata[MD_VTAG] = content.vtag

            if content.HasField('last_mod'): 
                metadata[MD_LASTMOD] = content.last_mod

            if content.HasField('deleted'): 
                metadata[MD_DELETED] = content.deleted

            if len(content.links):
                metadata[MD_LINKS] = []
                for l in content.links:
                    metadata[MD_LINKS].append(RiakLink(l.bucket,l.key,l.tag))

            if len(content.usermeta):
                metadata[MD_USERMETA] = {}
                for md in content.usermeta:
                    metadata[MD_USERMETA][md.key] = md.value

            if len(content.indexes):
                metadata[MD_INDEX] = []
                for ie in content.indexes:
                    metadata[MD_INDEX].append(RiakIndexEntry(ie.key, ie.value))
            resList.append((metadata, data))
        return vclock, resList


    def decodeJson(self, s):
        return self.client.get_decoder('application/json')(s)

    def encodeJson(self, s):
        return self.client.get_encoder('application/json')(s)

    # def deferred_sleep(self,secs):
    #     """
    #     fake deferred sleep

    #     @param secs: time to sleep
    #     @type secs: float
    #     """
    #     d = defer.Deferred()
    #     reactor.callLater(secs, d.callback, None)
    #     return d
