#!/usr/bin/python2
# -*- 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

class Cache( object ):
    """
    This class implements a persistent cache, backed in a file specified in
    the object creation. The file is protected for safe, concurrent access
    by multiple instances using flock.
    This cache uses JSON for speed and storage efficiency, so only simple
    data types are supported.
    Data is stored in a simple format {key:(expiretimestamp, data)}
    """
    def __init__(self, engine=None, *args, **kwargs):
        self._engine = None
        self._data = {}
        self._age = 0
        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):
            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:
            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:
                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)

