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

__all__ = (
    'load',
    'table', 'tr', 'td',
    'Menu', 'Action', 'litteral', 'p',
    'About',
    'Alert', 'set_alert',
)

import sys, traceback
from types import StringType

from ..base import Undef, isseq, seqof
from ..ext import web

from . import ui
from .api import _ensure_session, u, uval, uval_would_escape
from .model import Row, RowSchema, TableSchema
from .ui import Action, litteral, p

def load(prefix=None, icons=True, charset="utf-8"):
    if prefix is None: prefix = "/static/bootstrap/"
    elif not prefix.endswith("/"): prefix = prefix + "/"
    lines = [
        u"""<meta charset="%s" />""" % charset,
        u"""<meta http-equiv="X-UA-Compatible" content="IE=edge" />""",
        u"""<meta name="viewport" content="width=device-width, initial-scale=1.0" />""",
    ]
    if icons:
        lines.extend([
            u"""<link rel="shortcut" href="%sfavicon.ico" />""" % prefix,
            u"""<link rel="icon apple-touch-icon" href="%sicon.png" />""" % prefix,
        ])
    lines.extend([
        ui.css(u"%sbootstrap/css/bootstrap.min.css" % prefix),
        ui.css(u"%sbslocal.css" % prefix),
        ui.js(u"%sjquery.min.js" % prefix),
        ui.js(u"%sbootstrap/js/bootstrap.min.js" % prefix),
        u"<!--[if lt IE 9]>",
        ui.js(u"%shtml5shiv/html5shiv.js" % prefix),
        ui.js(u"%srespond/respond.min.js" % prefix),
        u"<![endif]-->",
    ])
    return u"\n".join(lines)

################################################################################
# Table

TRD_CLASS_MAP = dict(
    active="active",
    success="success", done="success",
    info="info",
    warning="warning", warn="warning", notice="warning",
    danger="danger", error="danger",
)
def trd_joinc(css, base_css=None):
    if css is Undef: css = None
    if type(css) is StringType: css = css.split(",")
    if type(base_css) is StringType: base_css = base_css.split(",")
    classes = list(seqof(base_css))
    for css in seqof(css):
        classes.append(TRD_CLASS_MAP.get(css, css))
    css = " ".join(classes)
    return css

def td(value, schema=None, css=Undef, tag=Undef, **kw):
    css = Undef.sa(css, kw, 'c', None)
    if schema: value = schema.html(value)
    else: value = uval(value)
    css = trd_joinc(css, schema.css(value) if schema else None)
    if tag is Undef: tag = "td"
    return u"<%s%s>%s</%s>" % (tag, ui.classif(css), value, tag)

def tr(values, schema=None, css=Undef, actions=Undef, celltag=Undef, **kw):
    css = Undef.sa(css, kw, 'c', None)
    actions = Undef.sa(actions, kw, None, None)
    if schema: tdschema = lambda value, index: schema.col_schema(value, index)
    else: tdschema = lambda value, index: None
    css = trd_joinc(css, schema.css(values) if schema else None)
    if actions and actions != True:
        css = ui.addclassif("action-tr", True, css)
    lines = []
    lines.append(u"<tr%s>" % ui.classif(css))
    index = 0
    if isinstance(values, dict) or isinstance(values, Row):
        if schema is not None:
            for col in schema.cols:
                value = values[col.key]
                lines.append(td(value, tdschema(value, index), tag=celltag))
                index += 1
        else:
            for value in values.values():
                lines.append(td(value, tdschema(value, index), tag=celltag))
                index += 1
    else:
        for value in values:
            lines.append(td(value, tdschema(value, index), tag=celltag))
            index += 1
    if actions == True:
        lines.append(td("Action", tag=celltag))
    elif actions:
        cell = []
        cell.append(u"""<div class="action-links invisible">""")
        for action in seqof(actions):
            if schema:
                action = action.update_query(schema.pk_dict(values))
            cell.append(u"""<a href="%s"%s>%s</a>""" % (action.url, ui.classif(action.css), action.text))
        cell.append(u"""</div>""")
        lines.append(td(litteral("".join(cell)), tag=celltag))
        pass
    lines.append(u"</tr>")
    return u"\n".join(lines)

TABLE_CLASS_MAP = dict(
    auto="table-auto", a="table-auto",
    striped="table-striped", strip="table-striped", s="table-striped",
    bordered="table-bordered", border="table-bordered", b="table-bordered",
    hover="table-hover", h="table-hover",
    condensed="table-condensed", c="table-condensed",
)
def table_joinc(css, base_css=None):
    if css is Undef: css = None
    if type(css) is StringType: css = css.split(",")
    if type(base_css) is StringType: base_css = base_css.split(",")
    classes = ["table"]
    classes.extend(seqof(base_css))
    for css in seqof(css):
        classes.append(TABLE_CLASS_MAP.get(css, css))
    css = " ".join(classes)
    return css

def tablejs(schema=None, jquery=False, script=False):
    if schema and not schema.actions: return u""
    if script: jquery = True
    lines = []
    lines.append(u"""<style type="text/css">
.action-links a { margin-right: 0.5em; }
</style>""")
    if script: lines.append(u"""<script type="text/javascript">""")
    if jquery: lines.append(u"""jQuery.noConflict()(function($) {""")
    lines.append(u"""$(".action-tr").hover(
function() {
  $(this).find(".action-links").removeClass("invisible");
}, function() {
  $(this).find(".action-links").addClass("invisible");
});""")
    if jquery: lines.append(u"""});""")
    if script: lines.append(u"""</script>""")
    return u"\n".join(lines)

def table(rows, schema=None, css=Undef, js=False, **kw):
    css = Undef.sa(css, kw, 'c', None)
    if schema is None: schema = TableSchema.parse(rows)
    elif isseq(schema): schema = TableSchema(schema)
    css = table_joinc(css, schema.css())
    lines = []
    if js: lines.append(tablejs(schema, script=True))
    lines.append(u"<table%s>" % ui.classif(css))
    lines.append(u"<thead>")
    lines.append(tr(schema.headers(), schema.header_row_schema(),
                    actions=True if schema.actions else False, celltag="th"))
    lines.append(u"</thead><tbody>")
    index = 0
    for row in rows:
        lines.append(tr(row, schema.body_row_schema(index),
                        actions=schema.actions))
        index += 1
    lines.append(u"</tbody>")
    lines.append(u"</table>")
    return u"\n".join(lines)

################################################################################
# Menu

NAVBAR_TYPE_MAP = {
    'fixed': 'fixed-top',
    'static': 'static-top',
}

class Menu(ui.Menu):
    """Un menu"""

    def __init__(self, text=None, mitems=None, profiles=None,
                 id=None, in_profiles=Undef, css=Undef, title=Undef,
                 navbar_type=Undef, container_fluid=Undef, **kw):
        navbar_type = Undef.sa(navbar_type, kw, 't', None)
        container_fluid = Undef.sa(container_fluid, kw, 'f', None)
        super(Menu, self).__init__(text, mitems, profiles, id, in_profiles, css, title, **kw)
        if navbar_type is None: navbar_type = "static"
        self.navbar_type = navbar_type
        if container_fluid is None: container_fluid = True
        self.container_fluid = container_fluid

    def __mitem(self, mitem, sel_profile):
        # ne pas afficher un menu qui n'est pas dans le bon profil
        if sel_profile is None:
            if mitem.in_profiles is not None:
                return []
        elif mitem.in_profiles is not None:
            if sel_profile not in mitem.in_profiles:
                return []
        li_class = ui.classif("active", mitem.active)
        url = uval(mitem.url)
        a_class = ui.classif(mitem.css)
        accesskey = ui.accesskeyif(mitem.accesskey)
        text = uval(mitem.text)
        return [u"""<li%s><a href="%s"%s%s>%s</a></li>""" % (li_class, url, a_class, accesskey, text)]

    def __dropdown(self, menu, profile):
        children = []
        for mitem in menu.mitems:
            children.extend(self.__mitem(mitem, profile))
        if not children:
            # ne pas afficher un menu vide
            return []
        li_class = ui.addclassif("active", menu.active, "dropdown")
        lines = [u"""<li class="%s">""" % li_class]
        firsturl = uval(menu.mitems[0].url)
        text = uval(menu.text)
        lines.append(u"""<a href="%s" class="dropdown-toggle" data-toggle="dropdown">%s <b class="caret"></b></a>""" % (firsturl, text))
        lines.append(u"""<ul class="dropdown-menu">""")
        lines.extend(children)
        lines.append(u"""</ul>""")
        return lines

    def __is_navbar_fixed(self, navbar_type):
        return navbar_type == 'fixed' or navbar_type == 'fixed-top'
    def __is_navbar_static(self, navbar_type):
        return navbar_type == 'static' or navbar_type == 'static-top'

    def render(self, select=None, profile=None, navbar_type=None, container_fluid=None, **kw):
        if select is None: select = kw.pop('s', None)
        if profile is None: profile = kw.pop('p', None)
        if navbar_type is None: navbar_type = kw.pop('t', None)
        if container_fluid is None: container_fluid = kw.pop('f', None)

        if navbar_type is None: navbar_type = self.navbar_type
        if container_fluid is None: container_fluid = self.container_fluid
        if select is not None: self.select(select, profile)
        selection = self.get_mitem()
        lines = []
        if self.__is_navbar_fixed(navbar_type):
            lines.append(u"""<style type="text/css">body { margin-top: 50px; }</style>""")
        css = u"navbar navbar-default"
        css = ui.addclassif("navbar-fixed-top", self.__is_navbar_fixed(navbar_type), css)
        css = ui.addclassif("navbar-static-top", self.__is_navbar_static(navbar_type), css)
        if self.profiles is not None and self.sel_profile is not None:
            css = ui.addclassif("%s-profile" % self.sel_profile, None, css)
        css = ui.addclassif(self.css, None, css)
        lines.append(u"""<div class="%s" role="navigation">""" % css)
        lines.append(u"""<div class="container%s">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Tooggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>""" % (u'-fluid' if container_fluid else ''))
        text = self.text
        if text is not None:
            if uval_would_escape(text):
                lines.append(u"""<span class="navbar-brand">%s</span>""" % uval(text))
            else:
                lines.append(uval(text))
        lines.append(u"""</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">""")
        if self.profiles is not None:
            # gestion des profils
            lines.append(u"""<li class="navbar-text"><form class="form-inline" method="get">""")
            for profile in self.profiles:
                if selection is not None:
                    if profile == self.sel_profile \
                       and selection.in_profiles is not None \
                       and profile in selection.in_profiles:
                        lines.append(u"""<button class="btn btn-xs btn-primary active" type="submit" formaction="%s" formmethod="%s"><span>%s</span></button>""" % (selection.url, selection.method, profile))
                        continue
                    profile_ref = "%s_ref" % profile
                    profile_mitem = None
                    if profile_ref in selection:
                        profile_mitem = self.get_mitem(selection[profile_ref])
                    if profile_mitem is not None:
                        lines.append(u"""<button class="btn btn-xs btn-default" type="submit" formaction="%s" formmethod="%s"><span>%s</span></button>""" % (profile_mitem.url, profile_mitem.method, profile))
                        continue
                lines.append(u"""<span class="btn btn-xs btn-default disabled">%s</span>""" % profile)
            lines.append(u"""</form></li>""")
        for mitem in self.mitems:
            if isinstance(mitem, ui.Menu):
                lines.extend(self.__dropdown(mitem, self.sel_profile))
            else:
                lines.extend(self.__mitem(mitem, self.sel_profile))
        lines.append(u"""</ul>""")
        if selection is not None:
            title = uval(selection.title)
            lines.append(u"""<p class="navbar-text"><b><em>%s</em></b></p>""" % title)
        lines.append(u"""</div>
</div>
</div>""")
        return u"\n".join(lines)
    def __unicode__(self): return self.render()

def set_menu(menu, **params):
    """Décorateur qui permet d'initialiser une instance de menu dans la session
    """
    def decorator(method):
        def wrapper(self, *args, **kw):
            session = _ensure_session(self)
            session.menu = menu
            return method(self, *args, **kw)
        return wrapper
    return decorator

def menu(id=None, profile=None, **params):
    """Décorateur qui permet de mettre à jour la sélection et le profil dans
    le menu

    par défaut, prendre le profil courant dans la session. le profil courant
    est initialisé par le décorateur @set_profile
    """
    if profile is None: profile = params.pop('p', None)
    def decorator(method):
        def wrapper(self, *args, **kw):
            session = _ensure_session(self)
            if 'menu' not in session:
                raise ValueError("menu est requis")
            sel_profile = profile
            if sel_profile is None and 'profile' in session:
                sel_profile = session.profile
            if id is not None:
                session.menu.select(id, sel_profile)
            return method(self, *args, **kw)
        return wrapper
    return decorator

################################################################################
# about

def About(summary, details=None, onShowDetails=None, js=True):
    lines = []
    if js:
        lines.append(u"""<script type="text/javascript">
jQuery.noConflict()(function($) {
  $(".about .show-details").click(function() {
    var $about = $(this).parents("div.about");
    $about.children(".summary").addClass("hide");
    $about.children(".details").removeClass("hide");""")
        if onShowDetails is not None:
            if callable(onShowDetails): onShowDetails = onShowDetails()
            lines.append(onShowDetails)
        lines.append(u"""\
    return false;
  });
});
</script>""")
    if callable(summary): summary = summary()
    lines.extend([
        u'<div class="about">',
        u'<div class="summary"><p>',
        summary,
    ])
    if details is not None:
        lines.extend([u'<a class="show-details" href="#"><em>En savoir plus...</em></a>'])
    lines.extend([u'</p></div>'])
    if details is not None:
        if callable(details): details = details()
        lines.extend([
            u'<div class="details hide">',
            details,
            u'</div>',
        ])
    lines.extend([u'</div>'])
    return u'\n'.join([u(line) for line in lines])

################################################################################
# Alert

ALERT_TYPE_MAP = dict(
    done="success",
    warn="warning", notice="warning",
    error="danger",
)
class Alert(object):
    msg = None
    exc_info = None
    type = None
    closeable = None
    escape = None
    action = None
    showtb = None

    def __init__(self, msg=None, exc_info=Undef, type=Undef, closeable=Undef, escape=Undef, action=None, showtb=True, **kw):
        exc_info = Undef.sa(exc_info, kw, 'e')
        if exc_info is Undef: exc_info = sys.exc_info()
        if exc_info == (None, None, None): exc_info = None
        type = Undef.sa(type, kw, 't', "error")
        closeable = Undef.sa(closeable, kw, 'c', False)
        escape = Undef.sa(escape, kw, 'x', None)

        self.msg = msg
        self.exc_info = exc_info
        self.type = ALERT_TYPE_MAP.get(type, type)
        self.closeable = closeable
        self.escape = escape
        self.action = action
        self.showtb = showtb

    def __call__(self, msg=Undef, exc_info=Undef, type=Undef, closeable=Undef, escape=Undef, action=Undef, showtb=Undef, **kw):
        exc_info = Undef.sa(exc_info, kw, 'e')
        if msg is Undef:
            # si on ne spécifie pas de message, alors prendre la valeur actuelle
            msg = self.msg
            if exc_info is Undef: exc_info = self.exc_info
        else:
            # si on spécifie un message, alors prendre aussi l'exception courante
            if exc_info is Undef: exc_info = sys.exc_info()
            if exc_info == (None, None, None): exc_info = None
        type = Undef.sa(type, kw, 't', self.type)
        closeable = Undef.sa(closeable, kw, 'c', self.closeable)
        escape = Undef.sa(escape, kw, 'x', self.escape)
        action = Undef.sa(action, kw, None, self.action)
        showtb = Undef.sa(showtb, kw, None, self.showtb)

        self.msg = msg
        self.exc_info = exc_info
        self.type = ALERT_TYPE_MAP.get(type, type)
        self.closeable = closeable
        self.escape = escape
        self.action = action
        self.showtb = showtb
        return self

    def render(self, msg=Undef, exc_info=Undef, type=Undef, closeable=Undef, escape=Undef, action=Undef, showtb=Undef, **kw):
        exc_info = Undef.sa(exc_info, kw, 'e')
        if msg is Undef:
            # si on ne spécifie pas de message, alors prendre la valeur initiale
            msg = self.msg
            if exc_info is Undef: exc_info = self.exc_info
        else:
            # si on spécifie un message, alors prendre aussi l'exception courante
            if exc_info is Undef: exc_info = sys.exc_info()
        type = Undef.sa(type, kw, 't', self.type)
        closeable = Undef.sa(closeable, kw, 'c', self.closeable)
        escape = Undef.sa(escape, kw, 'x', self.escape)
        action = Undef.sa(action, kw, None, self.action)
        showtb = Undef.sa(showtb, kw, None, self.showtb)

        if callable(msg):
            # si msg est callable, par défaut ne pas mettre le résultat en
            # échappement
            if escape is None: escape = False
            msg = msg()
        if escape is None: escape = True

        if msg is None and exc_info is not None:
            msg = u"Une erreur inattendue s'est produite"
        if msg is None: return u""
        if escape: msg = web.websafe(msg)

        lines = []
        css = u"alert alert-%s" % ALERT_TYPE_MAP.get(type, type)
        css = ui.addclassif("alert-dismissible", closeable, css)
        lines.append(u"""<div class="%s" role="alert">""" % css)
        if closeable:
            lines.append(u"""<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>""")
        lines.append(p(msg))
        if action is not None:
            if callable(action): action = action()
            if isinstance(action, Action):
                url = web.websafe(action.url)
                text = web.websafe(action.text or u"Cliquez ici pour continuer")
                action = u"""<p><a href="%s" accesskey="r">%s</a></p>""" % (url, text)
            lines.append(p(action))
        if exc_info is not None:
            lines.append(u"""<div class="small" style="margin-top: 3em; color: gray;">Pour information, le message d'erreur technique est&nbsp;&nbsp;&nbsp;""")
            lines.extend(traceback.format_exception_only(*exc_info[:2]))
            if showtb:
                lines.append(u"<!--")
                lines.append("".join(traceback.format_exception(*exc_info)))
                lines.append(u"-->")
            lines.append(u"""</div>""")
        lines.append(u"""</div>""")
        return u"\n".join([u(line) for line in lines])
    def __unicode__(self): return self.render()

def set_alert(template=None, action=Undef, delay=None, **params):
    """Décorateur qui permet de gérer automatiquement une instance de Alert dans la
    page.

    * Une instance vide self.alert = Alert() est créée dans la page
    * Si la valeur de retour n'est pas une instance de Alert, aucun traitement
      particulier supplémentaire n'est effectué
    * Si une exception se produit et qu'on n'est pas en mode debug, créer une
      nouvelle instance de Alert avec le message de l'exception.
    * Sinon:
      * Si template==None, afficher une page d'erreur générique avec une alerte
        de type Error.
      * Sinon, mettre à jour la variable alert avec la valeur retournée et
        effectuer le rendu du template spécifié.
    """
    def decorator(method):
        def wrapper(self, *args, **kw):
            self.alert = Alert(action=action)
            try:
                alert = method(self, *args, **kw)
            except:
                if web.config.debug: raise
                else: alert = Alert("Une erreur inattendue s'est produite")
            if isinstance(alert, Alert):
                if alert.type == 'success':
                    title = u"Succès"
                elif alert.type == 'warning':
                    title = u"Avertissement"
                else:
                    title = u"Erreur"
                if action is not Undef and alert.action is None:
                    alert.action = action
                if template is not None:
                    self.alert = alert
                    return self.r(seqof(template), self)
                else:
                    lines = []
                    lines.append(u"""\
<!DOCTYPE html><!-- -*- coding: utf-8 mode: html -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
--><html lang="fr">
<head>""")
                    lines.append(load())
                    if alert.action and delay is not None:
                        url = web.websafe(alert.action.url)
                        lines.append(u"""\
<meta http-equiv="Refresh" content="%s; URL=%s"/>""" % (delay, url))
                    lines.append(u"""\
<title>%(title)s</title>
</head>
<body>""" % locals())
                    if web.config.have_session:
                        session = web.config._session
                        if 'menu' in session: lines.append(session.menu.render())
                    lines.append(u"""\
<div class="container">
<h1>%(title)s</h1>""" % locals())
                    lines.append(alert.render())
                    lines.append(u"""\
</div>
</body>
</html>""")
                    return u"\n".join(lines)
            else:
                return alert
        return wrapper
    return decorator
