#!/usr/bin/env python
# -*- coding: utf-8 -*-
#-----------------------
# Name: cache.py
# Python Library
# Author: Raymond Wagner
# Purpose: Caching framework to store TMDb API results
#-----------------------

import time
import os

from tmdb_exceptions import *
from cache_engine import Engines

import cache_null
import cache_file

DEBUG = False

class Cache(object):
    """
    This class implements a cache framework, allowing selecting of a
    pluggable engine. The framework stores data in a key/value manner,
    along with a lifetime, after which data will be expired and
    pulled fresh next time it is requested from the cache.

    This class defines a wrapper to be used with query functions. The
    wrapper will automatically cache the inputs and outputs of the
    wrapped function, pulling the output from local storage for
    subsequent calls with those inputs.
    """
    def __init__(self, engine=None, *args, **kwargs):
        self._engine = None
        self._data = {}
        self._age = 0
        self._rate_limiter = []
        self.configure(engine, *args, **kwargs)

    def _import(self, data=None):
        if data is None:
            data = self._engine.get(self._age)
        for obj in sorted(data, key=lambda x: x.creation):
            self._rate_limiter.append(obj.creation)
            if len(self._rate_limiter) > 30:
                self._rate_limiter.pop(0)
            if not obj.expired:
                self._data[obj.key] = obj
                self._age = max(self._age, obj.creation)

    def _expire(self):
        for k, v in self._data.items():
            if v.expired:
                del self._data[k]

    def configure(self, engine, *args, **kwargs):
        if engine is None:
            engine = 'file'
        elif engine not in Engines:
            raise TMDBCacheError("Invalid cache engine specified: "+engine)
        self._engine = Engines[engine](self)
        self._engine.configure(*args, **kwargs)

    def put(self, key, data, lifetime=60*60*12):
        # pull existing data, so cache will be fresh when written back out
        if self._engine is None:
            raise TMDBCacheError("No cache engine configured")
        self._expire()
        self._import(self._engine.put(key, data, lifetime))

    def get(self, key):
        if self._engine is None:
            raise TMDBCacheError("No cache engine configured")
        self._expire()
        if key not in self._data:
            self._import()
        try:
            return self._data[key].data
        except:
            # no cache data, so we're going to query
            # wait to ensure proper rate limiting
            if len(self._rate_limiter) == 30:
                w = 10 - (time.time() - self._rate_limiter.pop(0))
                if (w > 0):
                    if DEBUG:
                        print "rate limiting - waiting {0} seconds".format(w)
                    time.sleep(w)
            return None

    def cached(self, callback):
        """
        Returns a decorator that uses a callback to specify the key to use
        for caching the responses from the decorated function.
        """
        return self.Cached(self, callback)

    class Cached( object ):
        def __init__(self, cache, callback, func=None, inst=None):
            self.cache = cache
            self.callback = callback
            self.func = func
            self.inst = inst

            if func:
                self.__module__ = func.__module__
                self.__name__ = func.__name__
                self.__doc__ = func.__doc__

        def __call__(self, *args, **kwargs):
            if self.func is None:
                # decorator is waiting to be given a function
                if len(kwargs) or (len(args) != 1):
                    raise TMDBCacheError(
                        'Cache.Cached decorator must be called a single ' +
                        'callable argument before it be used.')
                elif args[0] is None:
                    raise TMDBCacheError(
                        'Cache.Cached decorator called before being given ' +
                        'a function to wrap.')
                elif not callable(args[0]):
                    raise TMDBCacheError(
                        'Cache.Cached must be provided a callable object.')
                return self.__class__(self.cache, self.callback, args[0])
            elif self.inst.lifetime == 0:
                # lifetime of zero means never cache
                return self.func(*args, **kwargs)
            else:
                key = self.callback()
                data = self.cache.get(key)
                if data is None:
                    data = self.func(*args, **kwargs)
                    if hasattr(self.inst, 'lifetime'):
                        self.cache.put(key, data, self.inst.lifetime)
                    else:
                        self.cache.put(key, data)
                return data

        def __get__(self, inst, owner):
            if inst is None:
                return self
            func = self.func.__get__(inst, owner)
            callback = self.callback.__get__(inst, owner)
            return self.__class__(self.cache, callback, func, inst)
