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

__all__ = ('Server',)

import re, cgi, traceback

from ulib.base.base import make_prop, make_delegate
from ulib.base.uio import _s
from ulib.base.output import enote
from ulib.base.args import split_args, join_args, get_args
from ulib.base.dates import Date, parse_date, FR_DATEF
from ulib import json
from ulib.web import web, Application, Page, defaults, nocache
from ulib.tasks import *

from ulib.base.words import plural
TEMPLATE_GLOBALS = {'plural': plural,
                    }

class TaskView(object):
    _t = None
    def __init__(self, t): self._t = t
    def __getattr__(self, name): return getattr(self._t, name)

    # pour l'affichage
    def _htmlize(self, s):
        s = cgi.escape(s)
        s = s.replace(u'\n', u'<br />\n')
        return s

    def get_htbody(self, truncate=None):
        body = self._t.get_body() or u''
        if truncate is not None and len(body) > truncate:
            body = u'%s...' % body[:truncate]
        return self._htmlize(body)
    htbody = property(get_htbody)

    TASK_TYPES = {TASK_TYPE: u'task-type',
                  PLAN_TYPE: u'task-type',
                  EVENT_TYPE: u'event-type',
                  IDEA_TYPE: u'idea-type',
                  ACTIVITY_TYPE: u'activity-type',
                  }

    def get_css_classes(self):
        css_classes = []
        t = self._t
        # type
        type = t.get_type()
        css_classes.append(self.TASK_TYPES.get(type, u'%s-type' % type))
        # urgence
        if t.is_urgent(): css_classes.append(u'urgent')
        if t.is_normal(): css_classes.append(u'normal')
        if t.is_planned(): css_classes.append(u'planned')
        if t.is_obsolete(): css_classes.append(u'obsolete')
        if t.is_done(): css_classes.append(u'done')
        return u' '.join(css_classes)
    css_classes = property(get_css_classes)

    # pour l'édition: champs, valeurs, et classes des champs
    ep_items = property(lambda self: self._t.woutput())
    def ep_class(self, name):
        return {'title': 'l', 'body': 'textarea',
                'contexts': 'l', 'projects': 'l'}.get(name, 's')
    def ep_style(self, name):
        if self.ep_class(name) == 'textarea':
            nblines = (self._t.body or u'').count("\n") + 1
            if nblines >= 7: return u"height: %iem;" % nblines
        return u""

def ensure_task(t):
    if isinstance(t, TaskView): return t._t
    elif isinstance(t, Task): return t
    else: return None

def ensure_taskview(t):
    if isinstance(t, Task): return TaskView(t)
    elif isinstance(t, TaskView): return t
    else: return None

class Server(Application):
    PORT = 1974

    def __init__(self, basedir=None, templatedir=None, host=None, port=None, debug=None,
                 tsrc=None, ctl=None):
        self.template_globals = TEMPLATE_GLOBALS

        Application.__init__(self, basedir, templatedir, host, port, debug)

        if ctl is None: ctl = TasksCtl(tsrc)
        self._ctl = ctl
        self.OPTIONS = ctl.OPTIONS + self.FILTER_OPTIONS
        self.LONG_OPTIONS = ctl.LONG_OPTIONS + self.FILTER_LONG_OPTIONS
        self._ioptions = []

        self.reset_hts()
        self.reset_ts()

    _ctl, ctl = make_prop('_ctl', setter=False)[:2] # instance de TasksCtl

    FILTER_OPTIONS = 'aqdln'
    FILTER_LONG_OPTIONS = ['show-all', 'show-showable', 'show-doable', 'show-body', 'no-stats']
    _ioptions = None # options initiales
    _ifilter = None # filtre initial sous forme de chaine

    def is_option(self, option, value):
        if option in ('-a', '--show-all'): pass
        elif option in ('-q', '--show-showable'): pass
        elif option in ('-d', '--show-doable'): pass
        elif option in ('-l', '--show-body'): pass
        elif option in ('-n', '--no-stats'): pass
        elif self.ctl.is_option(option, value): return True
        else: return False
        if option.startswith('--'):
            self._ioptions.append('%s=%s' % (option, value))
        else:
            self._ioptions.append('%s%s' % (option, value))
        return True

    RE_PIPE = re.compile(ur'\s*\|\s*')
    def process_args(self, args):
        ioptions = self._ioptions
        if ioptions and args[0:1] and args[0].startswith('-'):
            ioptions.append('--')
        ioptions.extend(args)
        self._cfilters = cfs = filter(None, self.RE_PIPE.split(join_args(ioptions)))
        self._ifilter = cfs and cfs[0] or u''

    def before_start(self):
        enote(u"Lancement du serveur de tâches sur http://%s:%i" % (self.HOST, self.PORT))

    SHOW_ALL, SHOW_SHOWABLE, SHOW_DOABLE = 0, 1, 2

    def list_tasks(self, cfilter=None, ifilter=None):
        if ifilter is None: ifilter = self._ifilter
        if cfilter is None: cfilter = ifilter

        fargs = split_args(cfilter)
        options, fargs = self.ctl.get_args(fargs, self.FILTER_OPTIONS, self.FILTER_LONG_OPTIONS)
        showwhat = None
        showbody = False
        showstats = True
        for option, value in options:
            if option in ('-a', '--show-all'): showwhat = self.SHOW_ALL
            elif option in ('-q', '--show-showable'): showwhat = self.SHOW_SHOWABLE
            elif option in ('-d', '--show-doable'): showwhat = self.SHOW_DOABLE
            elif option in ('-l', '--show-body'): showbody = True
            elif option in ('-n', '--no-stats'): showstats = False
            elif self.ctl.is_option(option, value): pass

        store = self.ctl.get_stores()
        ids, text, props, projects, contexts, filtered = self.ctl.get_filters(fargs, store=store)
        if showwhat is None:
            if filtered: showwhat = self.SHOW_ALL
            else: showwhat = self.SHOW_SHOWABLE

        ts = [TaskView(t) for t in store.find_tasks(ids, text, props, projects, contexts)]
        nb_total = len(ts)
        if showwhat == self.SHOW_ALL:
            pass
        elif showwhat == self.SHOW_SHOWABLE:
            ts = [t for t in ts if t.is_showable()]
        elif showwhat == self.SHOW_DOABLE:
            ts = [t for t in ts if t.is_doable()]
        ts.sort(Task.cmp, reverse=True)
        nb_showable = len(ts)

        tasks = [t for t in ts if t.is_task_type()]
        nb_tasks = len(tasks)
        nb_done = len([t for t in tasks if t.is_done()])

        stats = web.storage(showwhat=showwhat,
                     showbody=showbody, showstats=showstats,
                     nb_showable=nb_showable, nb_total=nb_total,
                     nb_tasks=nb_tasks, nb_done=nb_done)
        return cfilter, stats, ts

    def ht(self, t):
        return self.render.task(t)

    # cache du rendu des tâches
    _hts, hts = make_prop('_hts', setter=False)[:2]

    def reset_hts(self):
        self._hts = {}

    def update_hts(self, *ts, **kw):
        force = kw.get('force', False)
        hts = self._hts
        for t in [ensure_taskview(t) for t in ts]:
            tid = t.id
            if force or not hts.has_key(tid):
                hts[tid] = self.ht(t)

    def remove_from_hts(self, *ts):
        if self._hts is None: return
        hts = self._hts
        for t in ts:
            tid = t.id
            if hts.has_key(tid):
                del hts[tid]

    # requêtes précédentes
    _cfilters, cfilters = make_prop('_cfilters')[:2]

    def new_cfilter(self, cfilter):
        if cfilter is not None and cfilter not in self._cfilters:
            self._cfilters.append(cfilter)

    # cache du résultat de la recherche
    _cfilter, cfilter = make_prop('_cfilter')[:2]
    _stats, stats = make_prop('_stats')[:2]
    _ts, ts = make_prop('_ts')[:2]

    def reset_ts(self, reset_cfilter=False):
        if reset_cfilter: self._cfilter = None
        self._stats = None
        self._ts = None

    def refresh(self):
        self.reset_hts()
        self.reset_ts()

    def set_query(self, cfilter=None, ifilter=None):
        # vérifier s'il faut utiliser le cache de requête
        if cfilter is None: cfilter = self._cfilter
        if cfilter != self._cfilter or ifilter is not None:
            self._cfilter = cfilter
            self.reset_ts()
        self.new_cfilter(cfilter)
        return cfilter

    def set_default_query(self):
        self.reset_ts(True)

    def reload_maybe(self):
        stores = self.ctl.get_stores()
        if stores.should_reload():
            stores.load()
            self.refresh()

    def get_hts(self, cfilter=None, ifilter=None):
        cfilter = self.set_query(cfilter, ifilter)
        if self._ts is None:
            # reconstruire le cache de requête
            self._ts = [] # pour être sûr d'avoir une liste en cas d'exception
            self._cfilter, self._stats, self._ts = self.list_tasks(cfilter, ifilter)
            self.update_hts(*self._ts)
        return self._cfilter, self._stats, self._ts, self._hts

    def encoder(self, obj):
        if isinstance(obj, Date): return obj.format(FR_DATEF)
        raise TypeError(repr(obj) + " is not JSON serializable")
    def dumps(self, obj):
        return json.dumps(obj, default=self.encoder)

    def dt(self, t, all_values=True):
        """Transformer une tâche en un dictionnaire dont les clés sont les
        attributs de la tâche.

        Si all_values==True, inclure les attributs pour lesquels il n'y a pas
        de valeur définie, avec la valeur None.

        Les attributs du dictionnaire sont: tid, type, cdate, ddate, dates,
        mindate, maxdate, date, title, body, contexts, projects, sid, id,
        title1, task_type, event_type, idea_type, due_date, delay, urgent,
        normal, planned, obsolete, done, urgency, showable, doable, purgeable
        """
        td = t.get_all_props(all_values=all_values)
        for name in ('sid', 'id', 'title1',
                     'task_type', 'event_type', 'idea_type',
                     'due_date', 'delay',
                     'urgent', 'normal', 'planned', 'obsolete', 'done',
                     'urgency',
                     'showable', 'doable', 'purgeable',
                     ):
            value = getattr(t, name)
            if all_values or value is not None:
                td[name] = value
        return td

    def jsencode_tasks(self, cfilter, stats, ts):
        dts = [self.dt(t) for t in ts]
        return self.dumps(cfilter), self.dumps(dts), self.dumps(stats)

    def add(self, args):
        t = self.ctl.get_wstore().new_task_from_clargs(args)
        t.save()
        self.update_hts(t, force=True)
        self.reset_ts()
        return t

    def get_tasks(self, tids=None, query=None, required=True, unique=False):
        stores = self.ctl.get_stores()
        if tids: ts = filter(None, [stores.get_by_id(tid) for tid in tids])
        elif query: ts = self.list_tasks(query)[2]
        else: ts = []
        if (required or unique) and not ts:
            raise ValueError("No tasks found")
        elif unique and len(ts) != 1:
            raise ValueError("Too many tasks found")
        return ts

    def edit(self, tids=None, query=None, **props):
        t = self.get_tasks(tids, query, unique=True)[0]
        t.winput(props)
        t.save()
        self.update_hts(t, force=True)
        return t

    DO = 'do'
    NODO = 'nodo'

    def do_or_nodo(self, action, tids=None, query=None):
        stores = self.ctl.get_stores()
        ts = self.get_tasks(tids, query)
        try:
            for t in ts:
                if action == self.DO: t.do()
                elif action == self.NODO: t.nodo()
                else: raise ValueError("Invalid action: %s" % action)
        except:
            stores.revert()
            self.refresh()
            raise
        stores.save()
        self.update_hts(force=True, *ts)
        return ts

    START = 'start'
    STOP = 'stop'
    TOGGLE = 'toggle'

    def start_stop_or_toggle(self, action, tids=None, query=None):
        stores = self.ctl.get_stores()
        ts = self.get_tasks(tids, query)
        try:
            for t in ts:
                if action == self.START: t.start()
                elif action == self.STOP: t.stop()
                elif action == self.TOGGLE:
                    if t.is_started(): t.stop()
                    else: t.start()
                else: raise ValueError("Invalid action: %s" % action)
        except:
            stores.revert()
            self.refresh()
            raise
        stores.save()
        self.update_hts(force=True, *ts)
        return ts

    def plan(self, args=None, tids=None):
        date = Date()
        options, args = get_args(args or [], 'd:', ['date='])
        for option, value in options:
            if option in ('-d', '--date'):
                if value: date = parse_date(value)
                else: date = None
        query = self.cfilter = join_args(args)

        stores = self.ctl.get_stores()
        ts = self.get_tasks(tids, query)
        try:
            for t in ts: t.plan(date)
        except:
            stores.revert()
            self.refresh()
            raise
        stores.save()
        self.update_hts(force=True, *ts)
        return ts

    def remove(self, tids=None, query=None):
        stores = self.ctl.get_stores()
        ts = self.get_tasks(tids, query)
        self.remove_from_hts(*ts)
        try:
            for t in ts:
                t.get_store().remove(ensure_task(t))
        except:
            stores.revert()
            self.refresh()
            raise
        stores.save()
        self.reset_ts()
        return ts

class page(Page):
    __abstract_page__ = True

    reload_timeout = 5

    ctl = make_delegate('app.ctl')
    cfilter = make_delegate('app.cfilter')
    stats = make_delegate('app.stats')
    ts = make_delegate('app.ts')
    hts = make_delegate('app.hts')

    cfilters = make_delegate('app.cfilters')

    @defaults(query=None)
    def index(self):
        try:
            self.app.get_hts(self.query)
            s = self.stats
            msg = u"%i / %s " % (s.nb_tasks - s.nb_done, plural(u"%i tâche%#", s.nb_tasks))
            if s.nb_total != s.nb_tasks:
                msg += plural(u", %i ligne%# au total", s.nb_total)
            if s.nb_total != s.nb_showable:
                msg += plural(u", %i ligne%# masquée%#", s.nb_total - s.nb_showable)
            self.add_msg(msg, level=0)
        except:
            traceback.print_exc()
            self.add_last_error_msg()
        return self.r('index', self)

class action(page):
    PATH = r'/action'

    reload_timeout = None # pas de rechargement automatique

    MSG_FADE = (None, u'msg_fade') # classes css pour le message

    @defaults(query=None)
    def doSearch(self):
        self.app.set_query(self.query)
        return self.redirect('/')

    def doAdd(self, args):
        t = self.app.add(args)
        self.add_msg(u"La tâche #%s a été créée avec succès" % t.id, self.MSG_FADE, level=1)
        return self.redirect('/')

    @defaults(tids=[], query=None)
    def doEditForm(self):
        t = self.app.get_tasks(self.tids, self.query, unique=True)[0]
        return self.redirect('/edit', tid=t.id)

    @defaults(tids=[], query=None, savebtn=None, cancelbtn=None)
    def doEdit(self):
        if self.cancelbtn is None:
            input = web.input()
            props = {}
            for name in Task.all_properties():
                props[name] = input.get(name, None)
            t = self.app.edit(self.tids, self.query, **props)
            self.add_msg(u"La tâche #%s a été modifiée avec succès" % t.id, self.MSG_FADE, level=1)
        return self.redirect('/')

    @defaults(tids=[], query=None)
    def doDoOrNodo(self, action):
        self.app.do_or_nodo(action, self.tids, self.query)
        return self.redirect('/')

    @defaults(tids=[], query=None)
    def doStartStopOrToggle(self, action):
        self.app.start_stop_or_toggle(action, self.tids, self.query)
        return self.redirect('/')

    @defaults(tids=[], query=None)
    def doPlan(self, args=None):
        if args is None: args = split_args(self.query)
        self.app.plan(args, self.tids)
        return self.redirect('/')

    @defaults(tids=[], query=None)
    def doRemove(self):
        ts = self.app.remove(self.tids, self.query)
        msg_value = u"Suppression de %i tâche(s): %s" % (len(ts),
                                                         u", ".join(["#" + t.id for t in ts]))
        self.add_msg(msg_value, self.MSG_FADE, level=1)
        return self.redirect('/')

    def doRefresh(self):
        self.app.refresh()
        return self.redirect('/')

    def doSetDefaultQuery(self):
        self.app.set_default_query()
        return self.redirect('/')

    @defaults(store=None)
    def doSetWStore(self):
        self.app.ctl.get_stores().set_wstore(self.store)
        return self.redirect('/')

    SEARCH = 'search'
    ADD = 'add'
    EDIT_FORM = 'edit'
    EDIT = '__edit__'
    DO = Server.DO
    NODO = Server.NODO
    START = Server.START
    STOP = Server.STOP
    TOGGLE = Server.TOGGLE
    PLAN = 'plan'
    REMOVE = 'remove'
    REFRESH = 'refresh'
    DEFAULTS = 'defaults'
    SET_WSTORE = '__sws__'

    ACTIONS = {'S': SEARCH, 'L': SEARCH, 'l': SEARCH, 'ls': SEARCH, 'list': SEARCH,
               'A': ADD, 'a': ADD, 'N': ADD, 'n': ADD, 'new': ADD,
               'E': EDIT_FORM, 'e': EDIT_FORM, 'ed': EDIT_FORM,
               'D': DO, 'd': DO,
               'nd': NODO,
               's': START,
               'k': STOP,
               'T': TOGGLE, 't': TOGGLE,
               'P': PLAN, 'p': PLAN, 'pl': PLAN,
               'rm': REMOVE, 'del': REMOVE, 'delete': REMOVE,
               'R': REFRESH,
               'D': DEFAULTS, 'default': DEFAULTS,
               }

    @nocache
    @defaults(name=None, query=None, tids=[], searchbtn=None, dobtn=None, nodobtn=None)
    def action(self):
        # Calculer l'action à exécuter
        args = split_args(self.query)
        action = self.name
        if action is None:
            # tester s'il s'agit d'une commande
            if args is not None and args[0:1] and args[0][:1] == u'/':
                action = _s(args[0][1:])
                args = args[1:]
                self.query = join_args(args)
            else:
                if self.searchbtn is not None: action = self.SEARCH
                elif self.dobtn is not None: action = self.DO
                elif self.nodobtn is not None: action = self.NODO
                else: action = self.SEARCH
        action = self.ACTIONS.get(action, action)
        # toujours vérifier si les fichiers ont été modifiés sur disque
        self.app.reload_maybe()
        try:
            self.clear_msgs(level=1)
            if action == self.SEARCH: return self.doSearch()
            elif action == self.ADD: return self.doAdd(args)
            elif action == self.EDIT_FORM: return self.doEditForm()
            elif action == self.EDIT: return self.doEdit()
            elif action in (self.DO, self.NODO):
                return self.doDoOrNodo(action)
            elif action in (self.START, self.STOP, self.TOGGLE):
                return self.doStartStopOrToggle(action)
            elif action == self.PLAN: return self.doPlan(args)
            elif action == self.REMOVE: return self.doRemove()
            elif action == self.REFRESH: return self.doRefresh()
            elif action == self.DEFAULTS: return self.doSetDefaultQuery()
            elif action == self.SET_WSTORE: return self.doSetWStore()
            else: raise ValueError("Invalid action: %s" % action)
        except:
            traceback.print_exc()
            self.add_last_error_msg()
            return self.index()

class index(page):
    PREFIX = r'/'

    @nocache
    def index(self):
        self.app.reload_maybe()
        self.clear_msgs(0)
        return page.index(self)

    @nocache
    @defaults(tid=None)
    def edit(self):
        self.app.reload_maybe()
        t = ensure_taskview(self.ctl.get_stores().get_by_id(self.tid))
        return self.r('edit', t)
