'''
Cache holder classes for Cheetah:

Cache regions are defined using the #cache Cheetah directive. Each
cache region can be viewed as a dictionary (keyed by cacheRegionID)
handling at least one cache item (the default one). It's possible to add
cacheItems in a region by using the `varyBy` #cache directive parameter as
in the following example::

   #def getArticle
      this is the article content.
   #end def

   #cache varyBy=$getArticleID()
      $getArticle($getArticleID())
   #end cache

The code above will generate a CacheRegion and add new cacheItem for each value
of $getArticleID().
'''

try:
    from hashlib import md5
except ImportError:
    from md5 import md5

import time
import Cheetah.CacheStore

class CacheItem(object):
    '''
    A CacheItem is a container storing:

        - cacheID (string)
        - refreshTime (timestamp or None) : last time the cache was refreshed
        - data (string) : the content of the cache
    '''

    def __init__(self, cacheItemID, cacheStore):
        self._cacheItemID = cacheItemID
        self._cacheStore = cacheStore
        self._refreshTime = None
        self._expiryTime = 0

    def hasExpired(self):
        return (self._expiryTime and time.time() > self._expiryTime)

    def setExpiryTime(self, time):
        self._expiryTime = time

    def getExpiryTime(self):
        return self._expiryTime

    def setData(self, data):
        self._refreshTime = time.time()
        self._cacheStore.set(self._cacheItemID, data, self._expiryTime)

    def getRefreshTime(self):
        return self._refreshTime

    def getData(self):
        assert self._refreshTime
        return self._cacheStore.get(self._cacheItemID)

    def renderOutput(self):
        """Can be overridden to implement edge-caching"""
        return self.getData() or ""

    def clear(self):
        self._cacheStore.delete(self._cacheItemID)
        self._refreshTime = None

class _CacheDataStoreWrapper(object):
    def __init__(self, dataStore, keyPrefix):
        self._dataStore = dataStore
        self._keyPrefix = keyPrefix

    def get(self, key):
        return self._dataStore.get(self._keyPrefix+key)

    def delete(self, key):
        self._dataStore.delete(self._keyPrefix+key)

    def set(self, key, val, time=0):
        self._dataStore.set(self._keyPrefix+key, val, time=time)

class CacheRegion(object):
    '''
    A `CacheRegion` stores some `CacheItem` instances.

    This implementation stores the data in the memory of the current process.
    If you need a more advanced data store, create a cacheStore class that works
    with Cheetah's CacheStore protocol and provide it as the cacheStore argument
    to __init__.  For example you could use
    Cheetah.CacheStore.MemcachedCacheStore, a wrapper around the Python
    memcached API (http://www.danga.com/memcached).
    '''
    _cacheItemClass = CacheItem

    def __init__(self, regionID, templateCacheIdPrefix='', cacheStore=None):
        self._isNew = True
        self._regionID = regionID
        self._templateCacheIdPrefix = templateCacheIdPrefix
        if not cacheStore:
            cacheStore = Cheetah.CacheStore.MemoryCacheStore()
        self._cacheStore = cacheStore
        self._wrappedCacheDataStore = _CacheDataStoreWrapper(
            cacheStore, keyPrefix=templateCacheIdPrefix+':'+regionID+':')
        self._cacheItems = {}

    def isNew(self):
        return self._isNew

    def clear(self):
        " drop all the caches stored in this cache region "
        for cacheItemId in list(self._cacheItems.keys()):
            cacheItem = self._cacheItems[cacheItemId]
            cacheItem.clear()
            del self._cacheItems[cacheItemId]

    def getCacheItem(self, cacheItemID):
        """ Lazy access to a cacheItem

            Try to find a cache in the stored caches. If it doesn't
            exist, it's created.

            Returns a `CacheItem` instance.
        """
        cacheItemID = md5(str(cacheItemID).encode('ascii')).hexdigest()

        if cacheItemID not in self._cacheItems:
            cacheItem = self._cacheItemClass(
                cacheItemID=cacheItemID, cacheStore=self._wrappedCacheDataStore)
            self._cacheItems[cacheItemID] = cacheItem
            self._isNew = False
        return self._cacheItems[cacheItemID]
