# -*- coding: utf-8 mode: python -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8

__all__ = ('db', 'Cached',
           'ModificationError', 'ModelC', 'getattrs',
           'model_equals', 'model_contains',
           )

import re
from types import FunctionType, MethodType
from google.appengine.ext import db
from google.appengine.api import memcache

from ulib.base.uio import _s
from ulib.base.base import isstr, isseq

class ModificationError(Exception):
    pass

class Cached(object):
    memcache = memcache.Client()

    def _skey(self, model):
        if hasattr(model, 'keyval'): return model.keyval
        key = model.key()
        skey = key.name()
        if skey is None: skey = _s(key)
        return skey

    def _mckey(self, key_or_model):
        if isstr(key_or_model): key = key_or_model
        else: key = self._skey(key_or_model)
        return "%s:%s" % (self.__class__.__name__, _s(key))

    def mcget(self, key, genvalue, time=0):
        u"""Obtenir la valeur gardée en cache pour cette classe avec la clé
        donnée.

        Si aucune valeur n'est gardée en cache pour cette clé, la générer avec
        la fonction genvalue(key)
        """
        mckey = self._mckey(key)
        value = self.memcache.get(mckey)
        if value is None:
            value = genvalue(key)
            self.memcache.set(mckey, value, time)
        return value

    def mcdel(self, key_or_model):
        u"""Supprimer la valeur gardée en cache pour cette classe avec la clé
        donnée.
        """
        self.memcache.delete(self._mckey(key_or_model))

class ModelC(Cached):
    u"""Classe permettant de gérer les instances d'une entité.

    Les arguments des méthodes DOIVENT être dans le type attendu. Le
    comportement n'est pas défini si cette condition n'est pas respectée.
    """

    # gestion des key_name

    RE_SPACES = re.compile(r"[-/'\s]+")
    def _key(self, *names):
        key = ''
        for name in names:
            key += ''.join([name[0:1] for name in self.RE_SPACES.split(name)]).lower()
        return key

    # gestion des requêtes

    def _build_gqlquery(self, filters, fields):
        u"""Bâtir une requête avec les valeurs d'une liste de champs

        - filters est un mapping champ/valeur.
        - fields est la liste des champs valides. Les champs de filters sont
        vérifiés par rapport à cette liste.

        query est une chaine de requête. args est la liste des arguments à
        utiliser pour la requête.

        Si un élément de filters associe une fonction au champ, cette
        correspondance n'est pas inclue dans la requête. Cette association est
        disponible dans la valeur de retour ffilters.

        @return: query, args, ffilters
        @rtype: tuple
        """
        query = None
        args = []
        ffilters = {}
        index = 1
        for field, value in filters.items():
            if type(value) in (FunctionType, MethodType):
                ffilters[field] = value
                continue
            if field not in fields:
                raise ValueError("Invalid field for query: %s" % field)

            if query is None: query = "WHERE "
            else: query += " AND "
            query += field
            query += " = "
            query += ":%i" % index
            args.append(value)
            index += 1
        return query, args, ffilters

    RE_COMMA = re.compile(r'\s*,\s*')
    def _apply_query_order(self, query, order_by):
        u"""Appliquer le tri à l'objet query.

        order_by est une chaine de la forme "attr,-attr,..."

        @type order_by: unicode
        @type query: db.Query
        @return: db.Query
        """
        if order_by is None: return query
        order_by = filter(None, self.RE_COMMA.split(order_by.strip()))
        for key in order_by:
            query.order(key)
        return query

    RE_ORDER = re.compile(r'(\+|-)?(\S+)')
    def _add_gqlquery_order(self, query, order_by):
        u"""Ajouter la clause ORDER BY à la requête GQL query.

        order_by est une chaine de la forme "attr,-attr,..."

        @type order_by: unicode
        @type query: unicode
        @return: unicode
        """
        if order_by is None: return query
        order_by = filter(None, self.RE_COMMA.split(order_by.strip()))
        first = True
        for key in order_by:
            if first: query += " ORDER BY "
            else: query += ", "
            first = False
            mo = self.RE_ORDER.match(key)
            if mo is None:
                raise ValueError("Invalid sort key: %s" % key)
            query += mo.group(2)
            if mo.group(1) == "-": query += " DESC"
            else: query += " ASC"
        return query

    def _results(self, entity_cls, filters, fields, order_by=None):
        u"""Bâtir une requête avec les arguments, puis l'appliquer sur l'entité
        entity_cls.

        order_by est une chaine de la forme "attr,-attr,..." et permet de
        spécifier l'ordre du résultat.

        @return: le résultat de la requête
        @rtype: list
        """
        query, args, ffilters = self._build_gqlquery(filters, fields)
        if query is None:
            results = self._apply_query_order(entity_cls.all(), order_by)
        else:
            query = self._add_gqlquery_order(query, order_by)
            results = entity_cls.gql(query, *args)
        if ffilters:
            def matches(result):
                for field, filter in ffilters.items():
                    if field == '__MODEL__':
                        if not filter(result): return False
                    else:
                        value = getattr(result, field)
                        if not filter(value): return False
                return True
            results = [result for result in results if matches(result)]
        return list(results)

    def _query(self, entity_cls, *terms, **kw):
        gqlquery = ""
        args = []
        i = 1
        first = True
        for name, op, value in terms:
            if value is None: continue
            if not op: op = "="

            if first: gqlquery += "WHERE "
            else: gqlquery += " AND "
            first = False

            gqlquery += "%s %s :%i" % (name, op, i)
            args.append(value)
            i += 1

        order_by = kw.get("order_by", None)
        if not gqlquery:
            results = self._apply_query_order(entity_cls.all(), order_by)
        else:
            gqlquery = self._add_gqlquery_order(gqlquery, order_by)
            results = entity_cls.gql(gqlquery, *args)

        return list(results)

    def get(self, key):
        u"""Obtenir l'entité dont la clé est key, ou lever l'exception
        NotImplementedError s'il n'est pas possible de récupérer une entité de
        cette manière.

        S'il n'existe aucune entité dont la clé est key, retourner None.
        """
        raise NotImplementedError

def getattrs(object, names):
    u"""Soit un objet o, et un chemin de la forme "attr0.attr1....", retourner
    l'objet obtenu avec l'expression o.attr0.attr1....

    Arrêter dès que le résultat de l'expression est None.
    """
    if not isseq(names): names = names.split(".")
    value = object
    for name in names:
        if value is None: break
        value = getattr(value, name)
    return value

def model_equals(model1, model2):
    return _s(model1.key()) == _s(model2.key())

def model_contains(models, model):
    return _s(model.key()) in [_s(model.key()) for model in models]
