#!/usr/bin/env python
#
# cache.py - The PropCache class.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module provides the :class:`PropCache` class, a convenience class
for caching the values of the properties of a :class:`.HasProperties` instance.
"""


import weakref

import fsl.utils.weakfuncref as weakfuncref


class CacheError(Exception):
    """Error raised by the :class:`PropCache` when a reference to the target
    object being cached, or the key function, is lost.
    """
    pass


class PropCache:
    """The ``PropCache`` A little convenience class which can be used to
    track and cache the property values of a :class:`.HasProperties` instance.

    Whenever a *trigger* property changes, the ``PropCache`` will take a copy
    of all specified propertyies on the *target* :class:`.HasProperties`
    instance, and store them alongside a *key*, which is generated by a
    function that is supplied.


    The ``PropCache`` maintains weak references to the target object, the
    key function, and the trigger objects. If the target or key function
    are garbage-collected, the ``PropCache`` will effectively disable itself,
    and subsequent calls to :meth:`get` will raise a :exc:`CacheError`.
    """


    def __init__(self, target, propNames, keyFunc, triggers):
        """Create a ``PropCache``.

        :arg target:    Target :class:`.HasProperties` instance whose
                        property values are to be cached.

        :arg propNames: Names of properties on the ``target`` to be cached

        :arg keyFunc:   Function which will be called whenever a trigger
                        property changes, to generate a suitable key to
                        use for cached property values.

        :arg triggers:  Sequence of ``(hasProps, propName)`` pairs, specifying
                        which property changes should trigger caching of
                        the ``target`` property values.
        """

        self.__name      = '{}_{}'.format(type(self).__name__, id(self))
        self.__cache     = {}
        self.__target    = weakref.ref(target)
        self.__propNames = propNames
        self.__keyFunc   = weakfuncref.WeakFunctionRef(keyFunc)
        self.__triggers  = [(weakref.ref(tobj), tprop)
                            for (tobj, tprop) in triggers]

        for tobj, tprop in triggers:
            tobj.addListener(tprop,
                             self.__name,
                             self.__doCache,
                             immediate=True)


    def __deregister(self):
        """Called if the target or key function are garbage-collected.
        De-registers trigger listeners, and removes all references and
        cached property values.
        """

        if self.__target is None:
            return

        for tobj, tprop in self.__triggers:
            tobj = tobj()
            if tobj is not None:
                tobj.removeListener(tprop, self.__name)

        self.__target   = None
        self.__keyFunc  = None
        self.__cache    = None
        self.__triggers = None


    def alive(self):
        """Returns ``True`` if this ``PropCache`` is active, ``False``
        otherwise. A ``PropCache`` will become inactive if its target,
        or the provided key generation function is deleted.
        """
        return self.__cache is not None and self.__target() is not None


    def get(self, key, propName, *args):
        """Returns the cached property value for the specified ``key``.
        If there is no cached property value for the overlay, the specified
        ``default`` value is returned. If a ``default`` value is not
        provided, the current property value is returned.

        :arg overlay:  Key to retrieve the property value for

        :arg propName: Name of the property to return a value for

        :arg default:  Value to return if a value for the
                       key/property is not cached. Must be passed as a
                       positional argument.
        """
        if len(args) not in (0, 1):
            raise ValueError('Invalid arguments passed to PropCache.get')

        haveDefault = len(args) == 1
        target      = self.__target() if self.__target else None

        if target is None:
            self.__deregister()
            raise CacheError('Target has been gc\'d')

        val = self.__cache.get((key, propName), None)

        if val is None:
            if haveDefault: val = args[0]
            else:           val = getattr(target, propName)

        return val


    def __doCache(self, *a):
        """Called when any trigger property changes. Generates a key, and
        then caches property values on the target instance.
        """

        target  = self.__target()  if self.__target  else None
        keyFunc = self.__keyFunc() if self.__keyFunc else None

        if keyFunc is None or target is None:
            self.__deregister()
            return

        key = keyFunc()

        for propName in self.__propNames:
            self.__cache[key, propName] = getattr(target, propName)
