##############################################################################
#
# Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
"""
memcached manager --
  Caches the results of method calls in memcached.

$Id: MemcachedManager.py 14309 2005-11-25 18:03:46Z dreamcatcher $
"""

import time
import memcache
from thread import get_ident
from OFS.Cache import Cache, CacheManager
from OFS.SimpleItem import SimpleItem
from Globals import DTMLFile, InitializeClass
from zLOG import LOG, INFO
from Products.MemcachedManager import O as ObjectCacheEntries

_marker = []  # Create a new marker object.

class Client(memcache.Client):

    def debuglog(self, msg):
        if self.debug:
            LOG('Memcached', INFO, msg)

class Memcached(Cache):
    # Note that objects of this class are not persistent,
    # nor do they make use of acquisition.

    def __init__(self):
        self.cache = None

    def initSettings(self, kw):
        # Note that we lazily allow MemcachedManager
        # to verify the correctness of the internal settings.
        self.__dict__.update(kw)
        servers = kw.get('servers', ('127.0.0.1:11211',))
        debug = kw.get('debug', 1)
        if self.cache is not None:
            self.cache.disconnect_all()
        self.cache = Client(servers, debug=debug)
        self.cache.debuglog(
            '(%s) initialized client '
            'with servers: %s' % (get_ident(), ', '.join(servers)))

    def getObjectCacheEntries(self, ob, create=0):
        """Finds or creates the associated ObjectCacheEntries object.
        """
        cache = self.cache
        h = hash(ob.absolute_url())
        oc = cache.get(h)
        if oc is None:
            if create:
                oc = ObjectCacheEntries(h)
                cache.set(h, oc)
            else:
                return None
        return oc

    def cleanup(self):
        """Remove cache entries.
        """
        self.cache.flush_all()

    def getCacheReport(self):
        """
        Reports on the contents of the cache.
        """
        return self.cache.get_stats()

    def ZCache_invalidate(self, ob):
        """
        Invalidates the cache entries that apply to ob.
        """
        oc = self.getObjectCacheEntries(ob)
        if oc is not None:
            oc.cleanup(self.cache)

    def ZCache_get(self, ob, view_name='', keywords=None,
                   mtime_func=None, default=None):
        """
        Gets a cache entry or returns default.
        """
        oc = self.getObjectCacheEntries(ob)
        if oc is None:
            return default
        index = oc.aggregateIndex(view_name, ob.REQUEST,
                                  self.request_vars, keywords)

        entry = oc.getEntry(self.cache, index)
        if entry is None:
            return default
        return entry

    def ZCache_set(self, ob, data, view_name='', keywords=None,
                   mtime_func=None):
        """
        Sets a cache entry.
        """
        oc = self.getObjectCacheEntries(ob, create=1)
        index = oc.aggregateIndex(view_name, ob.REQUEST,
                                  self.request_vars, keywords)
        oc.setEntry(self.cache, index, data)

caches = {}

class MemcachedManager(CacheManager, SimpleItem):
    """Manage a cache which stores rendered data in memcached.

    This is intended to be used as a low-level cache for
    expensive Python code, not for objects published
    under their own URLs such as web pages.

    MemcachedManager *can* be used to cache complete publishable
    pages, such as DTMLMethods/Documents and Page Templates,
    but this is not advised: such objects typically do not attempt
    to cache important out-of-band data such as 3xx HTTP responses,
    and the client would get an erroneous 200 response.

    Such objects should instead be cached with an
    AcceleratedHTTPCacheManager and/or downstream
    caching.
    """

    __ac_permissions__ = (
        ('View management screens', ('getSettings',
                                     'manage_main',
                                     'manage_stats',
                                     'getCacheReport',
                                     )),
        ('Change cache managers', ('manage_editProps',),
         ('Manager',)),
        )

    manage_options = (
        {'label':'Properties', 'action':'manage_main'},
        {'label':'Statistics', 'action':'manage_stats'},
        ) + CacheManager.manage_options + SimpleItem.manage_options

    meta_type = 'Memcached Manager'

    def __init__(self, ob_id):
        self.id = ob_id
        self.title = ''
        self._settings = {
            'request_vars': ('AUTHENTICATED_USER',),
            'servers': ('127.0.0.1:11211',),
            'debug': 1,
            }
        self.__cacheid = '%s_%f' % (id(self), time.time())

    def getId(self):
        """Get Object Id
        """
        return self.id

    ZCacheManager_getCache__roles__ = ()
    def ZCacheManager_getCache(self):
        key = (get_ident(), self.__cacheid)
        try:
            return caches[key]
        except KeyError:
            cache = Memcached()
            settings = self.getSettings()
            cache.initSettings(settings)
            caches[key] = cache
            return cache

    def getSettings(self):
        """Returns the current cache settings.
        """
        return self._settings.copy()

    manage_main = DTMLFile('dtml/propsMM', globals())

    def manage_editProps(self, title, settings=None, REQUEST=None):
        """Changes the cache settings.
        """
        if settings is None:
            settings = REQUEST
        self.title = str(title)
        request_vars = list(settings['request_vars'])
        request_vars.sort()
        servers = filter(None, list(settings['servers']))
        debug = int(settings['debug'])
        self._settings = {
            'request_vars': tuple(request_vars),
            'servers': tuple(servers),
            'debug': debug,
            }

        settings = self.getSettings()
        for (tid, cid), cache in caches.items():
            if cid == self.__cacheid:
                cache.initSettings(settings)
        if REQUEST is not None:
            return self.manage_main(
                self, REQUEST, manage_tabs_message='Properties changed.')

    manage_stats = DTMLFile('dtml/statsMM', globals())

    def getCacheReport(self):
        """Cache Statistics
        """
        c = self.ZCacheManager_getCache()
        rval = c.getCacheReport()
        return rval

InitializeClass(MemcachedManager)

manage_addMemcachedManagerForm = DTMLFile('dtml/addMM', globals())

def manage_addMemcachedManager(self, id, REQUEST=None):
    """Add a Memcached Manager to the folder.
    """
    self._setObject(id, MemcachedManager(id))
    if REQUEST is not None:
        return self.manage_main(self, REQUEST)
