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

__all__ = ('Model', 'ModelC',
           'AbstractSessionFactory', 'create_mappings',
           )

import re, logging
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm import mapper as mapperfunc
from ulib.base import odict, isseq, make_prop
from ulib.web import web

class Model(object):
    u"""Un modèle à mapper avec une table.

    Les classes dérivées doit définir une methode de classe
    __map(cls, mapper, tables), qui retourne le mapper et la table, e.g:

        class MyModel(Model):
            @classmethod
            def __map(cls, mapper, tables):
                mymodels = tables.mymodels
                return mapper(MyModel, mymodels), mymodels
    """
    __table__ = None
    __mapper__ = None

    def __init__(self, **kw):
        mapper = self.__mapper__
        for name, value in kw.items():
            if mapper.has_property(name):
                setattr(self, name, value)

    def __repr__(self):
        ss = ['<', self.__class__.__name__, '(']
        first = True
        for column in self.__table__.columns:
            if first: first = False
            else: ss.append(',')
            ss.append(column.name)
            ss.append('=')
            ss.append(repr(getattr(self, column.name)))
        ss.append(')>')
        return ''.join(ss)

class ModelC(object):
    u"""Un squelette pour un gestionnaire d'instances de modèle.
    """
    # session d'accès à la base de données
    _db, db = None, property(lambda self: self._db)

    def __init__(self, db):
        super(ModelC, self).__init__()
        self._db = db

    def commit(self, _c=True):
        if _c: self._db.commit()
    def addc(self, m, _c):
        self._db.add(m)
        self.commit(_c)
        return m
    def deletec(self, m, _c):
        self._db.delete(m)
        self.commit(_c)

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

    # validateurs
    _validators, validators = make_prop('_validators')[:2]

    def validate(self, value, value_class, required=False):
        vcname = value_class.__name__
        validators = self.validators
        if validators is not None:
            validator = validators.get(value_class, None)
            if validator is not None:
                try:
                    value = validator(value)
                except:
                    msg = "Exception while parsing %s instance" % vcname
                    logging.exception(msg)
                    raise ValueError(msg)
        if value is not None and not isinstance(value, value_class):
            raise ValueError("Expected a %s instance, got %s" % (vcname, repr(value)))
        elif value is None and required:
            raise ValueError("The %s instance is required" % vcname)
        else:
            return value

    def validator(self, value_class, required=False):
        """Retourner une fonction validate(value) qui quand on l'appelle, retourne le
        résultat de self.validate(value, value_class, required)
        """
        def wrapper(value):
            return self.validate(value, value_class, required)
        return wrapper

    def update_model(self, model, fields):
        """Mettre à jour model avec les champs de fields qui ne sont pas None
        """
        modified = False
        for attr in fields.ATTRS:
            value = getattr(fields, attr)
            if value is not None:
                setattr(model, attr, value)
                modified = True
        return modified

    # classe du modèle géré par cette classe
    __model__ = None

    def get(self, id):
        """Obtenir une instance de modèle par son id
        """
        raise NotImplementedError

    def parse(self, m):
        """Retourner une instance de modèle.

        Si m est None ou une instance du modèle, retourner la valeur.
        Sinon, utiliser la méthode get pour obtenir l'instance.
        """
        if m is None: return None
        elif self.__model__ is None: return m
        elif isinstance(m, self.__model__): return m
        else: return self.get(m)

class AbstractSessionFactory(object):
    _engine = None
    _metadata = None
    _sessionmaker = None

    def __init__(self, url=None, echo=None):
        if url is None: url = "sqlite:///:memory:"
        if echo is None: echo = web.config.debug
        self._engine = create_engine(url, echo=echo)
        self._metadata.create_all(self._engine)
        self._sessionmaker = sessionmaker(bind=self._engine)

    def __call__(self):
        return self._sessionmaker()

def create_mappings(metadata, *models):
    # liste des tables
    tables = odict()
    for table in metadata.sorted_tables:
        tables[table.name] = table

    # créer les mappings
    for model in models:
        funcname = '_%s__map' % model.__name__
        func = getattr(model, funcname, None)
        if func is not None:
            r = func(mapperfunc, tables)
            mapper = None
            table = None
            if isseq(r):
                mapper = r[0:1] and r[0] or None
                table = r[1:2] and r[1] or None
            elif r is not None:
                mapper = r
            if mapper is not None: model.__mapper__ = mapper
            if table is not None: model.__table__ = table

    return metadata
