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

__all__ = (
    'nocache', 'auth', 'defaults', 'redirect',
    'reset_session', 'set_session', 'session', 'check_session',
    'set_profile',
    'Page', 'Application',
)

import os, sys, base64, logging, re, shlex
from os import path

from ..base import isstr, isbool, isnum, isflt, seqof, make_prop
from ..uio import _u
from ..output import set_verbosity, einfo, enote, edebug
from ..args import get_args
from ..dates import rfc2822
from ..functions import apply_args
from ..words import plural
from ..formats import unicodeF, strF, booleanF, integerF, floatF
from ..config import ShConfig
from ..ext import web

from . import ui, blueprint, bootstrap
from .api import _ensure_session

DEFAULT_TEMPLATE_GLOBALS = {
    'checked': ui.checked, 'selected': ui.selected, 'disabled': ui.disabled, 'required': ui.required,
    'accesskeyif': ui.accesskeyif,
    'classif': ui.classif, 'addclassif': ui.addclassif,
    'favicon': ui.favicon, 'css': ui.css, 'js': ui.js, 'jscss': ui.jscss,
    'bp': blueprint, 'blueprintcss': blueprint.load,
    'bs': bootstrap, 'bootstrapcss': bootstrap.load,
    'plural': plural,
}

HANDLER_CLASSES = []
class MetaPage(type):
    """Méta-classe qui suit la définition des classes dérivées de Page, afin
    de construire automatiquement la liste des classes handler.
    """
    def __init__(cls, name, bases, attrs):
        type.__init__(cls, name, bases, attrs)
        abstract = cls.__dict__.get('__abstract_page__', False)
        if not abstract:
            _fix_PATH(cls, name)
            HANDLER_CLASSES.append(cls)

def nocache(method, **params):
    """Décorateur pour s'assurer que le résultat de la méthode web n'est pas mis en cache
    """
    def wrapper(self, *args, **kw):
        web.header("Expires", rfc2822(0))
        web.header("Pragma", "no-cache")
        web.header("Cache-control", "max-age=0, no-cache, must-revalidate")
        return method(self, *args, **kw)
    return wrapper

def auth(authenticator=None, realm='nulib', **params):
    """Décorateur pour s'assurer que la méthode web est authentifiée.

    La fonction authenticator avec la signature (username, password) permet
    d'authentifier l'utilisateur connecté.
    """
    def decorator(method):
        def wrapper(self, *args, **kw):
            env = web.ctx.environ
            if env.has_key('HTTP_AUTHORIZATION'):
                auth = env['HTTP_AUTHORIZATION'].split()
                if auth[0:1] and auth[0].lower() == 'basic':
                    username, password = base64.b64decode(auth[1]).split(':')
                    if authenticator is None or apply_args(authenticator, username, password):
                        return method(self, *args, **kw)
            web.header('WWW-Authenticate', 'Basic realm="%s"' % realm, True)
            return web.HTTPError('401 Unauthorized')
        return wrapper
    return decorator

def redirect(desturl, **params):
    """rediriger vers l'url spécifiée si la méthode retourne None.

    L'url est enregistrée dans l'objet avec le nom desturl au cas où on
    voudrait s'en servir dans la page retournée.
    """
    def decorator(method):
        def wrapper(self, *args, **kw):
            self.desturl = desturl
            self.desturl_params = params
            result = method(self, *args, **kw)
            if result is not None: return result
            else: return web.redirect(web.url(desturl, **params))
        return wrapper
    return decorator

def defaults(*required, **defaults):
    """Initialiser dans l'objet courant des variables à des valeurs par
    défaut, ou en les prenant parmi les paramètres de la requête.

    Si la valeur par défaut est respectivement une chaine, un booléen, un entier,
    une valeur flottante, la valeur de la requête est automatiquement convertie
    et la valeur effective sera toujours respectivement une chaine, un booléen,
    un entier, une valeur flottante.

    Si la valeur par défaut est None, la valeur de la requête sera convertie en
    None si elle est vide.

    Si la valeur est une fonction, elle est appelée pour formatter la valeur de
    la requête reçue.
    """
    def decorator(method):
        def wrapper(self, *args, **kw):
            # defaults doit être purgé des valeurs "callable", elles ne sont
            # utilisées que pour le formattage
            webdefaults = {}
            for key, value in defaults.items():
                if callable(value): webdefaults[key] = None
                else: webdefaults[key] = value
            webinput = self.defaults = web.input(*required, **webdefaults)
            def fix(name, format=None, formatter=None):
                value = webinput.get(name, None)
                if format is None and not value:
                    webinput[name] = None
                elif format is not None:
                    webinput[name] = format.parse(value)
                elif formatter is not None:
                    webinput[name] = formatter(value)
            for name, value in defaults.items():
                if isstr(value): fix(name, unicodeF)
                elif isbool(value): fix(name, booleanF)
                elif isnum(value): fix(name, integerF)
                elif isflt(value): fix(name, floatF)
                elif callable(value): fix(name, None, value)
                elif value is None: fix(name)
            return method(self, *args, **kw)
        return wrapper
    return decorator

def reset_session(**initial):
    """réinitialiser la session avec les valeurs initiales spécifiées

    s'assurer que l'objet courant contient la variable session. si les sessions
    n'ont pas été configurées au niveau de l'application, une exception est
    levée
    """
    def decorator(method):
        def wrapper(self, *args, **kw):
            session = _ensure_session(self)
            # session_id est requis
            initial.update(session_id=session.session_id)
            session._data.clear()
            session.update(initial)
            return method(self, *args, **kw)
        return wrapper
    return decorator

def set_session(**values):
    """initialiser la session avec les valeurs spécifiées

    s'assurer que l'objet courant contient la variable session. si les sessions
    n'ont pas été configurées au niveau de l'application, une exception est
    levée
    """
    def decorator(method):
        def wrapper(self, *args, **kw):
            session = _ensure_session(self)
            session.update(values)
            return method(self, *args, **kw)
        return wrapper
    return decorator

def session(**defaults):
    """s'assurer que les variables spécifiées existent dans la session. Sinon,
    initialiser la session avec les valeurs par défaut spécifiées.

    s'assurer que l'objet courant contient la variable session. si les sessions
    n'ont pas été configurées au niveau de l'application, une exception est
    levée
    """
    def decorator(method):
        def wrapper(self, *args, **kw):
            session = _ensure_session(self)
            for key, value in defaults.items():
                if key not in session:
                    session[key] = value
            return method(self, *args, **kw)
        return wrapper
    return decorator

def check_session(_onerror=None, **validators):
    """vérifier les valeurs des variables spécifiées dans la session.
    pour chaque éléments de validators, si la valeur est scalaire, vérifier que
    le session contient cette valeur exacte. si la valeur est callable, ce doit
    être une fonction qui prend un seul argument, la valeur qui est actuellement
    dans la session. si la fonction retourne True, alors la valeur est vérifiée.

    si la valeur n'est pas vérifiée, retourner un code 401 unauthorized, sauf
    si _onerror est spécifé. dans ce cas, c'est l'url d'une page vers laquelle
    il faut faire une redirection.

    s'assurer que l'objet courant contient la variable session. si les sessions
    n'ont pas été configurées au niveau de l'application, une exception est
    levée
    """
    def decorator(method):
        def wrapper(self, *args, **kw):
            session = _ensure_session(self)
            validated = True
            for key, value in validators.items():
                if key not in session:
                    validated = False
                    break
                elif callable(value):
                    if not value(session[key]):
                        validated = False
                        break
                else:
                    if session[key] != value:
                        validated = False
                        break
            if not validated:
                if _onerror is not None: return web.redirect(_onerror)
                else: raise web.unauthorized("invalid session")
            return method(self, *args, **kw)
        return wrapper
    return decorator

def set_profile(profile=None, **params):
    """mettre à jour la valeur du profil courant

    s'assurer que l'objet courant contient la variable session. si les sessions
    n'ont pas été configurées au niveau de l'application, une exception est
    levée
    """
    if profile is None: profile = params.pop('p', None)
    def decorator(method):
        def wrapper(self, *args, **kw):
            session = _ensure_session(self)
            session.profile = profile
            return method(self, *args, **kw)
        return wrapper
    return decorator

def _fix_PATH(cls, name):
    if cls.PATH is None:
        if cls.PREFIX is None:
            cls.PREFIX = '/%s' % name
        if cls.PREFIX == '/': cls.PATH = '/(.*)'
        else: cls.PATH = '%s(?:/(.*))?' % cls.PREFIX

class Page(object):
    __metaclass__ = MetaPage
    __abstract_page__ = True

    app = None # instance de Application
    _render = None # instance de web.template.render

    TEMPLATEDIR = None # répertoire relatif des templates pour cette classe

    PREFIX = None # url relatif d'accès aux méthodes de cette classe
    # par défaut, PREFIX = '/%s' % le_nom_de_la_classe
    PATH = None # expression régulière qui matche les urls traités par cette classe
    # par défaut, PATH = '%s/(.*)' % PREFIX

    defaults = None
    def __getattr__(self, name):
        defaults = self.defaults
        if defaults is not None:
            if defaults.has_key(name):
                return defaults[name]
        raise AttributeError(name)

    def init(self):
        """Initialiser l'objet
        """

    def get_last_error(self, exc_info=None):
        """Obtenir le message d'erreur de la dernière exception, ainsi que la
        classe CSS associée.

        msg vaut None s'il n'y a pas d'erreur. msgclass peut valoir success,
        warning ou error.

        @return: msg, msgclass
        """
        if exc_info is None: exc_info = sys.exc_info()[:2]
        type, value = exc_info[:2]
        if type is None:
            return None, u"success"
        elif isinstance(value, Warning):
            return u"%s: %s" % (_u(type.__name__), _u(value)), u"warning"
        elif isinstance(value, Exception):
            return u"%s: %s" % (_u(type.__name__), _u(value)), u"error"
    last_error = property(get_last_error)

    def redirect(self, path, **kw):
        return web.redirect(web.url(path, **kw))

    def _utf8plaintext(self, text):
        web.header("Content-Type", "text/plain; charset=utf-8", True)
        return text.encode("utf-8")

    def r(self, name, *args, **kw):
        if name is None or name == 'index': name = self.__class__.__name__
        elif type(name) is tuple: name = '_'.join(name)
        else: name = '%s_%s' % (self.__class__.__name__, name)
        templatedir = getattr(self, 'TEMPLATEDIR', None)
        if templatedir is not None:
            name = path.join(templatedir, name)
        render = getattr(self._render, name)
        return render(*args, **kw)

    def render(self, *args, **kw):
        return self.r(None, *args, **kw)

    NAME = None # nom de la méthode à appeler, ou None s'il n'y a pas de méthode définie
    METHOD = None # GET ou POST
    def __response(self, name, method):
        if name is None and method == "GET":
            # si on essaie d'accéder à /page, rediriger vers /page/
            return web.redirect("%s/%s" % (web.ctx.path, web.ctx.query))
        self.NAME = name
        self.METHOD = method
        m = None
        if m is None and name is not None and hasattr(self, name):
            m = getattr(self, name)
        if m is None and hasattr(self, self.__class__.__name__):
            m = getattr(self, self.__class__.__name__)
        if m is None and not name and hasattr(self, 'index'):
            m = getattr(self, 'index')
        if m is None:
            m = self.error
        # XXX Ajouter un argument en fonction de la valeur du header Accept:, pour text/plain,
        # text/html, application/xml et application/xhtml+xml
        return apply_args(m, method)

    def GET(self, name=None): return self.__response(name, 'GET')
    def POST(self, name=None): return self.__response(name, 'POST')
    def error(self): raise web.notfound()
    def index(self): return self.render()

class Application(object):
    # si ce fichier existe dans basedir, alors forcer le mode développement: la
    # variable HOST est ignorée et DEBUG vaut par défaut True
    DEVEL_SF = '.devel'
    # nom du fichier de configuration à charger automatiquement à partir de
    # basedir. pour désactiver cette fonctionnalité, utiliser la valeur None
    CONFIG_FILE = 'config/server.conf'
    CONFIG_VARS = dict(NAME=strF, HOST=strF, PORT=integerF, DEBUG=booleanF, PROFILE=strF)
    # configuration par défaut
    NAME = None # nom de l'application
    HOST = '0.0.0.0'
    PORT = 7080
    DEBUG = False
    PROFILE = None

    webapp = None # instance de web.application
    session = None # instance de web.session.Session

    basedir = None # répertoire de base pour les fichiers servis

    render = None # instance de web.template.render
    templatedir = None # répertoire des templates
    template_globals = None # dictionnaire global pour render

    args = None # arguments qui restent sur la ligne de commande

    def __init__(self, basedir=None, templatedir=None, host=None, port=None, debug=None, profile=None):
        if self.basedir is None or basedir is not None:
            if basedir is None:
                # par défaut, basedir est le répertoire qui contient la classe
                module_name = self.__class__.__module__
                module = sys.modules.get(module_name, None)
                if module is not None:
                    file = module.__file__
                    if file is not None:
                        basedir = path.abspath(path.split(file)[0])
                if basedir is None:
                    basedir = os.getcwd()
            self.basedir = basedir
        if self.templatedir is None or templatedir is not None:
            if templatedir is None:
                templatedir = path.join(self.basedir, 'templates')
            self.templatedir = templatedir

        devel = path.exists(path.join(self.basedir, self.DEVEL_SF))
        if devel: einfo("Mode developpement activé")

        if self.CONFIG_FILE is not None:
            config = path.join(self.basedir, self.CONFIG_FILE)
            if path.exists(config):
                c = ShConfig(config, self.CONFIG_VARS)
                for name, value in c.items():
                    if devel and name == 'HOST': continue
                    if devel and name == 'DEBUG' and value is None: value = True
                    if value is not None: setattr(self, name, value)

        if self.NAME is None:
            # par défaut, le nom de l'application est le nom du répertoire de
            # base
            self.NAME = path.basename(self.basedir)

        if host is not None: self.HOST = host
        if port is not None: self.PORT = port
        if debug is not None: self.DEBUG = debug
        if profile is not None: self.PROFILE = profile

        tg = self.template_globals
        if tg is None: tg = {}
        tg.update(DEFAULT_TEMPLATE_GLOBALS)
        self.template_globals = tg

    OPTIONS = None # options
    LONG_OPTIONS = None
    def process_option(self, option, value):
        """Traiter une option et retourner True, ou False si l'option n'est
        pas reconnue.

        Cette méthode est prévue pour être dérivée.
        """
        return False

    # process_option() s'appelait auparavant is_option(). Cette méthode est
    # gardée pour compatibilité
    def is_option(self, option, value): return False

    def process_args(self, args):
        """Traiter les arguments après les options.

        Cette méthode est appelée avant que soit initialisé le serveur web, et
        est prévue pour être dérivée.
        """

    def before_start(self):
        """Effectuer un traitement avant de lancer le serveur web standalone.

        Cette méthode est appelée juste avant que soit lancé le serveur web, et
        est prévue pour être dérivée.
        """
        if web.config.debug:
            einfo("Bienvenue sur %s" % self.NAME)
            profile = self.PROFILE
            if profile is None: edebug("Lancement sans profil spécifié")
            else: enote("Lancement dans le profil %s" % profile)
            edebug("basedir: %s" % self.basedir)
            edebug("templatedir: %s" % self.templatedir)
            edebug("Configuration des routes: %s" % str(self.webapp.mapping))
            enote("Lancement du serveur sur http://%s:%i" % (self.HOST, self.PORT))

    def new_render(self, templatedir=None):
        """Retourner une nouvelle instance de web.template.render
        """
        if templatedir is None: templatedir = self.templatedir
        return web.template.render(templatedir,
                                   cache=not self.DEBUG,
                                   globals=self.template_globals)

    def _new_application(self):
        """Créer une nouvelle instance de web.application
        """
        return web.application()

    def _new_session(self):
        """Si les sessions ne sont pas supportées, retourner None.

        Sinon, retourner un tuple (store, initializer) où:
        - store est une instance de web.session.Store, qui sert à stocker les
          sessions.
        - initializer est le dictionnaire initial, et peut être omis

        Exemple:
            return web.session.DiskStore('sessions'), {}
        """
        return None

    def _configure_handler_class(self, handler_class):
        self.webapp.add_mapping(handler_class.PATH, handler_class)

    def _configure_handler(self, handler):
        handler.app = self
        handler._render = self.render
        handler.init()

    def _wsgifunc(self):
        """Obtenir la fonction pour servir les requêtes wsgi
        """
        return self.webapp.wsgifunc()

    def _start_server(self, server_type=None, server_socket=None):
        """Démarrer le serveur web
        """
        func = self._wsgifunc()

        if os.environ.has_key('SERVER_SOFTWARE'): # cgi
            os.environ['FCGI_FORCE_CGI'] = 'Y'

        if (os.environ.has_key('PHP_FCGI_CHILDREN') #lighttpd fastcgi
            or os.environ.has_key('SERVER_SOFTWARE')):
            return web.wsgi.runfcgi(func, None)

        if server_type in ('fcgi', 'fastcgi'):
            return web.wsgi.runfcgi(func, server_socket or (self.HOST, self.PORT))
        elif server_type in ('scgi', ):
            return web.wsgi.runscgi(func, server_socket or (self.HOST, self.PORT))
        else:
            return web.httpserver.runsimple(func, (self.HOST, self.PORT))

    def __configure(self):
        web.config.debug = self.DEBUG
        web.config.BASEDIR = self.basedir

        self.webapp = self._new_application()
        self.webapp.set_handler_configurator(self._configure_handler)
        for handler_class in HANDLER_CLASSES:
            self._configure_handler_class(handler_class)

        session = None
        tmp = seqof(self._new_session())
        store = tmp[0:1] and tmp[0] or None
        initializer = tmp[1:2] and tmp[1] or None
        if store is not None:
            # rendre compatible l'utilisation des sessions avec le mode debub
            session = web.config.get("_session", None)
            if session is None:
                session = web.session.Session(self.webapp, store, initializer=initializer)
                self.session = web.config._session = session

        web.config.have_session = session is not None

        tg = self.template_globals
        # session et application comme variables globales du template
        tg.setdefault("S", session)
        tg.setdefault("A", self)

        self.render = web.template.render(self.templatedir, cache=not self.DEBUG, globals=tg)

    def wsgiapp(self):
        """Retourner l'instance à utiliser pour mod_wsgi.

        mod_wsgi s'attend à trouver une variable nommée application dans le
        module spécifié dans la configuration. Il faut donc utiliser cette
        méthode de cette façon:

            class MyApp(Application):
                ...

            application = MyApp().wsgiapp()
        """
        self.__configure()
        return self._wsgifunc()

    def run(self, args=None, **ignored):
        """Démarrer le serveur, en standalone, fastcgi ou scgi suivant les
        arguments de la ligne de commande.

        Utiliser cette méthode de cette façon:

            class MyApp(Application):
                ...

            if __name__ == '__main__':
                import sys
                MyApp().run(sys.argv[1:])
        """
        if args is None: args = []
        options, args = get_args(map(_u, args),
                                 'S:s:H:P:Dp:' + (self.OPTIONS or ''),
                                 ['server-type=', 'server-socket=', 'host=', 'port=', 'debug', 'no-debug', 'profile='] + list(self.LONG_OPTIONS or ()))
        server_type = None
        server_socket = None
        for option, value in options:
            if self.process_option(option, value): pass
            elif self.is_option(option, value): pass #compatibilité
            elif option in ('-S', '--server-type'): server_type = value
            elif option in ('-s', '--server-socket'): server_socket = value
            elif option in ('-H', '--host'): self.HOST = value
            elif option in ('-P', '--port'): self.PORT = int(value)
            elif option in ('-D', '--debug'):
                self.DEBUG = True
                set_verbosity('--debug')
            elif option in ('--no-debug',):
                self.DEBUG = False
            elif option in ('-p', '--profile'): self.PROFILE = value
        self.args = args
        self.process_args(args)
        self.__configure()
        self.before_start()
        self._start_server(server_type, server_socket)

    def cgirun(self):
        """Retourner un handler CGI. A utiliser avec Google AppEngine

        Utiliser cette méthode de cette façon:

            class MyApp(Application):
                ...

            main = MyApp().cgirun()

            if __name__ == '__main__':
                main()
        """
        self.__configure()
        wsgifunc = self._wsgifunc()
        try:
            from google.appengine.ext.webapp.util import run_wsgi_app
            return run_wsgi_app(wsgifunc)
        except ImportError:
            # we're not running from within Google App Engine
            import wsgiref
            return wsgiref.handlers.CGIHandler().run(wsgifunc)
