"""
This module contains multithread-safe cache implementations.

Caches mainly have a

    __getitem__  and  getorbuild() method

where the latter either just return a cached value or
first builds the value.

These are the current cache implementations:

    BuildcostAccessCache  tracks building-time and accesses. Evicts
              by product of num-accesses * build-time.

"""
import py
gettime = py.std.time.time

class WeightedCountingEntry(object):
    def __init__(self, value, oneweight):
        self.num = 1
        self._value = value
        self.oneweight = oneweight

    def weight():
        def fget(self):
            return (self.num * self.oneweight, self.num)
        return property(fget, None, None, "cumulative weight")
    weight = weight()

    def value():
        def fget(self):
            # you need to protect against mt-access at caller side!
            self.num += 1
            return self._value
        return property(fget, None, None)
    value = value()

    def __repr__(self):
        return "<%s weight=%s>" % (self.__class__.__name__, self.weight) 

class BasicCache(object):
    def __init__(self, maxentries=128):
        self.maxentries = maxentries
        self.prunenum = int(maxentries - maxentries/8)
        self._lock = py.std.threading.RLock()
        self._dict = {}

    def getentry(self, key):
        lock = self._lock
        lock.acquire()
        try:
            return self._dict.get(key, None)
        finally:
            lock.release()

    def putentry(self, key, entry):
        self._lock.acquire()
        try:
            self._prunelowestweight()
            self._dict[key] = entry
        finally:
            self._lock.release()

    def delentry(self, key, raising=False):
        self._lock.acquire()
        try:
            try:
                del self._dict[key]
            except KeyError:
                if raising:
                    raise
        finally:
            self._lock.release()

    def getorbuild(self, key, builder, *args, **kwargs):
        entry = self.getentry(key)
        if entry is None:
            entry = self.build(key, builder, *args, **kwargs)
        return entry.value

    def _prunelowestweight(self):
        """ prune out entries with lowest weight. """
        # note: must be called with acquired self._lock!
        numentries = len(self._dict)
        if numentries >= self.maxentries:
            # evict according to entry's weight
            items = [(entry.weight, key) for key, entry in self._dict.iteritems()]
            items.sort()
            index = numentries - self.prunenum
            if index > 0:
                for weight, key in items[:index]:
                    del self._dict[key]

class BuildcostAccessCache(BasicCache):
    """ A BuildTime/Access-counting cache implementation.
        the weight of a value is computed as the product of

            num-accesses-of-a-value * time-to-build-the-value

        The values with the least such weights are evicted
        if the cache maxentries threshold is superceded.
        For implementation flexibility more than one object
        might be evicted at a time.
    """
    # time function to use for measuring build-times
    _time = gettime

    def __init__(self, maxentries=64):
        super(BuildcostAccessCache, self).__init__(maxentries)

    def build(self, key, builder, *args, **kwargs):
        start = self._time()
        val = builder(*args, **kwargs)
        end = self._time()
        entry = WeightedCountingEntry(val, end-start)
        self.putentry(key, entry)
        return entry

class AgingCache(BasicCache):
    """ This cache prunes out cache entries that are too old.
    """
    def __init__(self, maxentries=128, maxseconds=10.0):
        super(AgingCache, self).__init__(maxentries)
        self.maxseconds = maxseconds

    def getentry(self, key):
        self._lock.acquire()
        try:
            try:
                entry = self._dict[key]
            except KeyError:
                entry = None
            else:
                if entry.isexpired():
                    del self._dict[key]
                    entry = None
            return entry
        finally:
            self._lock.release()

    def build(self, key, builder, *args, **kwargs):
        ctime = gettime()
        val = builder(*args, **kwargs)
        entry = AgingEntry(val, ctime + self.maxseconds)
        self.putentry(key, entry)
        return entry

class AgingEntry(object):
    def __init__(self, value, expirationtime):
        self.value = value
        self.weight = expirationtime

    def isexpired(self):
        t = py.std.time.time()
        return t >= self.weight 
