# -*- coding: utf-8 -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
import ulib.base.i_need_py23

__all__ = ('Task', 'TaskFactory')

import re

from ulib.base.uio import _s, _u, Utf8IO
from ulib.base.dates import Date, DateSpecs, parse_date
from ulib.base.times import Time, parse_time
from ulib.base.lines import Lines
from ulib.base.words import plural
from base import *
from consts import *

class FakeStore:
    _uio = Utf8IO()
    uio = property(lambda self: self._uio)
    _type = DEFAULT_TYPE
    type = property(lambda self: self._type)

    def save(self):
        raise NotImplementedError("This task must be attached to a valid store")
fake_store = FakeStore()

class AbstractStrategy:
    u"""Une stratégie pour la gestion d'une tâche.

    Une classe dérivée doit implémenter au minimum is_doable() et do()
    """
    def _not_doable(self, task, permanent=True):
        msg = u"La tâche "
        id = task.id
        if id: msg += "#%s " % id
        msg += u"ne peut pas être effectuée"
        if not permanent: msg += u" pour le moment"
        return ValueError(_s(msg))

    ###########################################################################
    # informations nécessaire pour déterminer l'état de la tâche

    def get_due_date(self, task, now=None):
        u"""Retourner la date à laquelle la tâche doit ou devait être
        effectuée, ou None si cette information n'est pas disponible.
        - Si urgent, date à laquelle la tâche devait être effectuée.
        - Si normal, date à laquelle la tâche devait être effectuée si la
          tâche est en retard, ou doit être effectuée si la tâche n'est pas
          en retard (en principe la date du jour).
        - Si planned, date à laquelle la tâche doit être effectuée.
        - Si obsolete ou done, retourner None.

        Si l'information n'est pas disponible ou n'a pas de sens, retourner
        None.
        """
        return None
    def get_delay(self, task, now=None):
        u"""Délai entre la date d'effet de la tâche et la date actuelle.
        - Si urgent, nombre de jours de retard.
        - Si normal, nombre de jours de retard si la tâche est en retard, 0 si
          la tâche n'est pas en retard.
        - Si planned, nombre de jours avant que la tâche ne puisse être
          effectuée, en tant que nombre négatif.
        - Si obsolete ou done, retourner None.

        Si l'information n'est pas disponible ou n'a pas de sens, retourner
        None.
        """
        if now is None: now = Date()
        due_date = self.get_due_date(task, now)
        if due_date is None: return None
        else: return now.diff(due_date)

    ###########################################################################
    # configuration de la tâche

    def get_urgent_delay(self):
        u"""Retourner le nombre de jours de retard avant de considérer la tâche
        comme urgente.

        @return: 7, par défaut
        """
        return 7
    def get_showable_planned_delay(self):
        u"""Retourner le nombre de jours avant due_date à partir de quand on
        peut afficher une tâche planifiée.

        @return: 7, par défaut
        """
        return 7
    def get_hidden_obsolete_delay(self):
        u"""Retourner le nombre de jours après due_date à partir de quand on
        n'affiche plus une tâche obsolète.

        @return: 7, par défaut
        """
        return 7
    def get_hidden_done_delay(self):
        u"""Retourner le nombre de jours après ddate à partir de quand on
        n'affiche plus une tâche effectuée.

        @return: 7, par défaut
        """
        return 7

    ###########################################################################
    # état de la tâche: urgent, normal, planned, obsolete, done

    def is_urgent(self, task, now=None):
        u"""Tester si la tâche est urgente, c'est à dire qu'elle est à faire
        et qu'elle est (très) en retard.

        urgent implique showable, doable, not purgeable
        """
        return False
    def is_normal(self, task, now=None):
        u"""Tester si la tâche est d'urgence normale, c'est à dire qu'elle est
        à faire et qu'elle n'est pas (ou peu) en retard.

        normal implique showable, doable, not purgeable
        """
        return False
    def is_planned(self, task, now=None):
        u"""Tester si la tâche est plannifiée, c'est à dire qu'elle est
        à faire dans le futur.

        planned implique not doable, not purgeable

        La tâche est par défaut not showable au moins jusqu'à
        showable_planned_delay jours avant due_date.
        """
        return False
    def is_obsolete(self, task, now=None):
        u"""Tester si la tâche est obsolète, c'est à dire qu'elle n'a pas été
        faite à temps, et qu'elle n'est plus faisable.

        obsolete implique not doable, purgeable.

        La tâche est par défaut showable au moins jusqu'à hidden_obsolete_delay
        jours après due_date.
        """
        return False
    def is_done(self, task, now=None):
        u"""Tester si la tâche est effectuée.

        done implique not doable, purgeable

        La tâche est par défaut showable au moins jusqu'à hidden_done_delay
        jours après ddate.
        """
        return False

    ###########################################################################
    # affichage de la tâche

    def is_cal_future(self, task, days=1, gte=False, now=None):
        """Retourner True si sur un calendrier, cette tâche doit s'afficher
        dans days jours. Si gte==True, retourner True aussi si cette tâche
        s'affiche dans days+N jours, où N est un nombre positif.

        Si days==0, sélectionner les tâches du jour.
        Si days==-1, sélectionner les tâches obsolètes ou effectuées dans
        le passé.
        """
        if now is None: now = Date()
        date = now + days
        if gte:
            if days == -1:
                return True
            elif days == 0:
                if self.is_done(task, now) and task.ddate != now: return False
                elif self.is_obsolete(task, now): return False
                else: return True
            else:
                if self.is_done(task, now): return False
                elif self.is_doable(task, now): return False
                elif self.is_obsolete(task, now): return False
                else: return task.date >= date
        else:
            if days == -1:
                if self.is_done(task, now) and task.ddate != now: return True
                elif self.is_obsolete(task, now): return True
                else: return False
            elif days == 0:
                if self.is_done(task, now) and task.ddate == now: return True
                elif self.is_doable(task, now): return True
                else: return False
            else:
                if self.is_done(task, now): return False
                elif self.is_doable(task, now): return False
                elif self.is_obsolete(task, now): return False
                else: return task.date == date

    def get_urgency(self, task, now=None):
        u"""Retourner une description du degré d'urgence de cette tâche, de
        urgent à obsolète.
        """
        if now is None: now = Date()

        if self.is_done(task, now): return u""

        due_date = self.get_due_date(task, now)
        delay = self.get_delay(task, now)
        if due_date is None: return u""
        if delay == 0: return u""

        if delay < 0:
            if delay == -1: return u"demain"
            elif delay == -2: return u"après-demain"
            else: return u"dans %s, le %s" % (plural(u"%i jour%#", -delay), due_date)
        else:
            msg = plural(u"en RETARD d%<'>e #%i jour%<née>s#", delay)
            maxdate = task.maxdate
            if maxdate is not None:
                msg += u", à faire AVANT le %s" % maxdate
            return msg

    def _is_before_mindate(self, task, now):
        u"""Retourner True si now est avant mindate, False sinon.

        mindate peut valoir None, auquel cas on retourne False.
        now ne doit pas être None.
        """
        mindate = task.mindate
        if mindate is None: return False
        else: return now < mindate

    def is_showable(self, task, now=None):
        u"""Tester si la tâche peut être affichée."""
        if now is None: now = Date()
        if self._is_before_mindate(task, now): return False
        if self.is_urgent(task, now): return True
        elif self.is_normal(task, now): return True
        elif self.is_planned(task, now):
            # par défaut, n'afficher que si la date d'effet de la tâche
            # (due_date) est dans moins de showable_planned_delay
            due_date = self.get_due_date(task, now)
            if due_date is None: return False
            else: return due_date.diff(now) <= self.get_showable_planned_delay()
        elif self.is_obsolete(task, now):
            # par défaut, n'afficher que si la date d'effet de la tâche
            # (due_date) est au plus hidden_obsolete_delay après now
            due_date = self.get_due_date(task, now)
            if due_date is None: return False
            else: return now.diff(due_date) <= self.get_hidden_obsolete_delay()
        elif self.is_done(task, now):
            # par défaut, n'afficher que si la date à laquelle la tâche a été
            # effectuée est au plus hidden_done_delay après now
            return now.diff(task.ddate) <= self.get_hidden_done_delay()
        else: return False

    ###########################################################################
    # gestion de la tâche

    def is_doable(self, task, now=None):
        u"""Tester si la tâche peut être effectuée.

        doable implique not done, not obsolete, not purgeable
        """
        raise NotImplementedError
    def is_purgeable(self, task, now=None):
        u"""Tester si la tâche peut être purgée.

        purgeable implique not doable
        """
        if now is None: now = Date()
        return self.is_done(task, now) or self.is_obsolete(task, now)

    def do(self, task, now=None):
        u"""Effectuer la tâche"""
        raise NotImplementedError

    def nodo(self, task, now=None):
        u"""Ne pas effectuer la tâche"""
        raise NotImplementedError

    ###########################################################################
    # Gestion système de la tâche

    def validate(self, task, now=None):
        u"""Corriger et/ou mettre à jour la tâche après chargement depuis une
        source éditable directement par l'utilisateur.

        Ceci permet de s'assurer que la tâche est dans un état cohérent.
        """
        pass

class SampleStrategy(AbstractStrategy):
    u"""Modèle pour les classes dérivées de AbstractStragegy.

    Les méthodes commentées ont une implémentation par défaut.
    Les méthodes non commentées devrait recevoir une implémentation dans la
    classe dérivée.
    """
    def get_due_date(self, task, now=None): return None
    #def get_delay(self, task, now=None): return None

    #def get_urgent_delay(self): return 3
    #def get_showable_planned_delay(self): return 7
    #def get_hidden_obsolete_delay(self): return 7
    #def get_hidden_done_delay(self): return 7

    def is_urgent(self, task, now=None): return False
    def is_normal(self, task, now=None): return False
    def is_planned(self, task, now=None): return False
    def is_obsolete(self, task, now=None): return False
    def is_done(self, task, now=None): return False

    #def is_cal_future(self, task, days=1, gte=False, now=None): return False
    #def get_urgency(self, task, now=None): return u""
    #def _is_before_mindate(self, mindate, now): return False
    #def is_showable(self, task, now=None): return False

    def is_doable(self, task, now=None): return False
    #def is_purgeable(self, task, now=None): return False

    def do(self, task, now=None): pass
    def nodo(self, task, now=None): pass

    #def validate(self, task, now=None): pass

class TaskStrategy(AbstractStrategy):
    u"""Une tâche à effectuer à une date non précisée.

    - Les seuls états que peut avoir une tâche de ce type sont normal et done.
    - Les attributs dates, date, maxdate sont ignorés.
    - L'attribut mindate est honoré pour savoir si la tâche est showable.
    """
    def is_urgent(self, task, now=None):
        return False
    def is_normal(self, task, now=None):
        return task.ddate is None
    def is_planned(self, task, now=None):
        return False
    def is_obsolete(self, task, now=None):
        return False
    def is_done(self, task, now=None):
        return task.ddate is not None

    def is_doable(self, task, now=None):
        return task.ddate is None

    def do(self, task, now=None):
        if now is None: now = Date()
        if not self.is_doable(task, now):
            raise self._not_doable(task, False)
        task.ddate = now
        return task

    def nodo(self, task, now=None):
        pass

class PlanStrategy(TaskStrategy):
    u"""Une tâche à effectuer à une date précise. Si la date d'effet n'est pas
    précisée, se comporter comme TaskStrategy.
    """

    def get_due_date(self, task, now=None):
        return task.date

    def _is_obsolete(self, maxdate, now):
        u"""Retourner True si now est après maxdate, auquel cas la tâche est
        obsolète, False sinon.

        maxdate peut valoir None, auquel cas on retourner False.
        now ne doit pas être None.
        """
        return maxdate is not None and now > maxdate
    def _is_done(self, date, ddate, now):
        u"""Retourner True si la tâche est effectuée, False sinon.

        ddate peut être None.
        now ne doit pas être None.
        Cette méthode doit être appelée après la méthode _is_obsolete().
        """
        if ddate is None: return False
        return date is None or ddate == now
    def _is_doable(self, date, now):
        u"""Retourner True si la tâche peut être effectuée, False sinon.

        date et now ne doivent pas être None.
        Cette méthode doit être appelée après la méthode _is_done().
        """
        return now >= date

    def is_urgent(self, task, now=None):
        if now is None: now = Date()
        if task.date is None:
            return TaskStrategy.is_urgent(self, task, now)
        else:
            if self._is_obsolete(task.maxdate, now): return False
            elif self._is_done(task.date, task.ddate, now): return False
            else: return self.get_delay(task, now) > self.get_urgent_delay()
    def is_normal(self, task, now=None):
        if now is None: now = Date()
        if task.date is None:
            return TaskStrategy.is_normal(self, task, now)
        else:
            if self._is_obsolete(task.maxdate, now): return False
            elif self._is_done(task.date, task.ddate, now): return False
            delay = self.get_delay(task, now)
            return delay >= 0 and delay <= self.get_urgent_delay()
    def is_planned(self, task, now=None):
        if now is None: now = Date()
        if task.date is None:
            return TaskStrategy.is_planned(self, task, now)
        else:
            if self._is_obsolete(task.maxdate, now): return False
            elif self._is_done(task.date, task.ddate, now): return False
            delay = self.get_delay(task, now)
            return delay < 0
    def is_obsolete(self, task, now=None):
        if now is None: now = Date()
        if task.date is None:
            return TaskStrategy.is_obsolete(self, task, now)
        else:
            return self._is_obsolete(task.maxdate, now)
    def is_done(self, task, now=None):
        if now is None: now = Date()
        if task.date is None:
            return TaskStrategy.is_done(self, task, now)
        else:
            return self._is_done(task.date, task.ddate, now)

    def _is_before_mindate(self, task, now):
        mindate = task.mindate
        if mindate is None:
            return False
        elif self._is_done(task.date, task.ddate, now) and task.date is not None:
            # cas particulier: de même qu'une tâche est done le jour ou elle a
            # été effectuée, de même elle est showable ce jour-là
            return False
        else:
            return now < mindate

    def is_doable(self, task, now=None):
        if now is None: now = Date()
        date = task.date
        if date is None:
            return TaskStrategy.is_doable(self, task, now)
        else:
            if self._is_obsolete(task.maxdate, now): return False
            elif self._is_done(task.date, task.ddate, now): return False
            else: return self._is_doable(date, now)

    def is_purgeable(self, task, now=None):
        if now is None: now = Date()
        if task.date is None:
            return TaskStrategy.is_purgeable(self, task, now)
        else:
            return False

    def _set_next_date(self, task, now, ddate, remove_obsoletes=False):
        u"""Initialiser la prochaine date d'effet.

        @return: True si une date d'effet a été initialisée, False sinon.
        """
        if task.dates is None: return False
        dss = DateSpecs(task.dates)
        next_date = dss.get_next_date(now)
        if next_date is None: return False
        if remove_obsoletes:
            if dss.remove_obsoletes():
                task.dates = str(dss) or None
        task.date = next_date
        if next_date.diff(now) <= 1:
            task.mindate = next_date
        return True

    def __do(self, task, now=None, do=True):
        remove_obsoletes = now is None
        if now is None: now = Date()
        if not self.is_doable(task, now):
            raise self._not_doable(task, False)
        if not do and task.dates is None:
            # pas possible de faire un nodo si dates n'est pas défini.
            return

        if do: task.ddate = now
        if task.dates is not None:
            if not self._is_obsolete(task.maxdate, now):
                # il faut tenir compte du cas ou on arrive à la fin de la
                # liste de dates. si do==True, on doit finir avec date==None,
                # sinon, l'opération doit être un noop
                saved = task.date
                task.date = None
                self._set_next_date(task, now, task.ddate, remove_obsoletes)
                if not do and task.date is None: task.date = saved

        return task

    def do(self, task, now=None):
        return self.__do(task, now, True)

    def nodo(self, task, now=None):
        return self.__do(task, now, False)

    def validate(self, task, now=None):
        if now is None: now = Date()
        ddate = task.ddate
        date = task.date
        if ddate is None:
            if date is None:
                self._set_next_date(task, now, ddate)
        elif date is None:
            self._set_next_date(task, now, ddate)
        elif date <= ddate:
            task.date = None
            self._set_next_date(task, now, ddate)

class EventStrategy(PlanStrategy):
    u"""
    """
    def validate(self, task, now=None):
        if now is None: now = Date()
        PlanStrategy.validate(self, task, now)
        if self.is_doable(task, now):
            delay = self.get_delay(task, now)
            if delay >= 1:
                self.do(task, now)

class IdeaStrategy(AbstractStrategy):
    u"""Une idée à concrétiser dans le futur

    Une idée est toujours showable, et jamais doable.
    Les attributs dates, date, mindate, maxdate, ddate sont ignorés
    """
    def is_showable(self, task, now=None): return True
    def is_doable(self, task, now=None): return False

    def is_cal_future(self, task, days=1, gte=False, now=None): return days == 0

    def do(self, task, now=None):
        raise self._not_doable(task, True)
    def nodo(self, task, now=None):
        raise self._not_doable(task, True)

class ActivityStrategy(AbstractStrategy):
    u"""Un type d'activité général. Cette stratégie sert surtout à suivre avec
    les opérations start() et stop() le travail sur ce type d'activité.

    Une activitée est toujours showable, et jamais doable.
    Les attributs dates, date, mindate, maxdate, ddate sont ignorés
    """
    def is_showable(self, task, now=None): return True
    def is_doable(self, task, now=None): return False

    def is_cal_future(self, task, days=1, gte=False, now=None): return days == 0

    def do(self, task, now=None):
        raise self._not_doable(task, True)
    def nodo(self, task, now=None):
        raise self._not_doable(task, True)

STRATEGY_CLASSES = {TASK_TYPE: TaskStrategy,
                    PLAN_TYPE: PlanStrategy,
                    EVENT_TYPE: EventStrategy,
                    IDEA_TYPE: IdeaStrategy,
                    ACTIVITY_TYPE: ActivityStrategy,
                    }
def new_strategy(type):
    ds_cls = STRATEGY_CLASSES.get(type, None)
    if ds_cls is None: raise ValueError("Invalid type: %s" % _s(type))
    else: return ds_cls()

class Task(PropsMixin, object):
    # Propriétés qui peuvent modifiées sur cet objet
    GENERIC_PROPERTIES = [TID, TYPE, CDATE, DDATE, DATES, MINDATE, MAXDATE, DATE]
    SPECIAL_PROPERTIES = [TITLE, BODY, CONTEXTS, PROJECTS]

    _tid, tid, get_tid, set_tid, del_tid = make_uprop('_tid', None)
    _type, type, get_type, set_type, del_type = make_uprop_defaults('_type', None)
    _title, title, get_title, set_title, del_title = make_uprop('_title', None)
    _body, body, get_body, set_body, del_body = make_uprop('_body', None)
    _cdate, cdate, get_cdate, set_cdate, del_cdate = make_prop('_cdate', None, validator=date_validator)
    _ddate, ddate, get_ddate, set_ddate, del_ddate = make_prop('_ddate', None, validator=date_validator)
    _dates, dates, get_dates, set_dates, del_dates = make_prop('_dates', None)
    _mindate, mindate, get_mindate, set_mindate, del_mindate = make_prop('_mindate', None, validator=date_validator)
    _maxdate, maxdate, get_maxdate, set_maxdate, del_maxdate = make_prop('_maxdate', None, validator=date_validator)
    _date, date, get_date, set_date, del_date = make_prop('_date', None, validator=date_validator)
    _contexts, contexts, get_contexts, set_contexts, del_contexts = \
        make_prop_defaults('_contexts', None,
                           validator=contexts_list_validator,
                           merger=contexts_list_merger)
    _projects, projects, get_projects, set_projects, del_projects = \
        make_prop_defaults('_projects', None,
                           validator=projects_list_validator,
                           merger=projects_list_merger)
    _modified, modified, is_modified, set_modified = make_prop('_modified', False, deleter=False, set_modified=False)

    def __init__(self, store=None, **kw):
        if store is None: self._store = fake_store
        else: self._store = store
        self._contexts = []
        self._projects = []
        self.set_props(**kw)
        self._ds = new_strategy(self.type)
        self._modified = False
        if store is not None:
            self.set_store(store)

    _store = None
    def get_store(self):
        store = self._store
        if isinstance(store, FakeStore): return None
        else: return store
    def set_store(self, store):
        self._store = store
        self._store._task_attached(self)
    store = property(get_store, set_store)

    def save(self):
        self._store.save()

    def get_sid(self):
        store = self.store
        if store is None: return None
        else: return store.get_sid()
    sid = property(get_sid)

    def get_id(self):
        sid = self.get_sid()
        tid = self.get_tid()
        if sid is None: return tid
        else: return u"%s.%s" % (sid, tid)
    id = property(get_id)

    def get_title1(self):
        return self.get_title().replace("\n", " ")
    title1 = property(get_title1)

    def is_task_type(self): return self.get_type() in (TASK_TYPE, PLAN_TYPE)
    task_type = property(is_task_type)

    def is_event_type(self): return self.get_type() == EVENT_TYPE
    event_type = property(is_event_type)

    def is_idea_type(self): return self.get_type() == IDEA_TYPE
    idea_type = property(is_idea_type)

    def is_activity_type(self): return self.get_type() == ACTIVITY_TYPE
    activity_type = property(is_activity_type)

    _ds =  None
    def validate(self, now=None): return self._ds.validate(self, now)
    def get_due_date(self, now=None): return self._ds.get_due_date(self, now)
    due_date = property(get_due_date)
    def get_delay(self, now=None): return self._ds.get_delay(self, now)
    delay = property(get_delay)
    def is_urgent(self, now=None): return self._ds.is_urgent(self, now)
    urgent = property(is_urgent)
    def is_normal(self, now=None): return self._ds.is_normal(self, now)
    normal = property(is_normal)
    def is_planned(self, now=None): return self._ds.is_planned(self, now)
    planned = property(is_planned)
    def is_obsolete(self, now=None): return self._ds.is_obsolete(self, now)
    obsolete = property(is_obsolete)
    def is_done(self, now=None): return self._ds.is_done(self, now)
    done = property(is_done)
    def is_cal_future(self, days=1, gte=False, now=None):
        return self._ds.is_cal_future(self, days, gte, now)
    def is_cal_today(self, now=None): return self.is_cal_future(0, False, now)
    cal_today = property(is_cal_today)
    def is_cal_planned(self): return self.date is not None or self.dates is not None
    cal_planned = property(is_cal_planned)
    def get_urgency(self, now=None): return self._ds.get_urgency(self, now)
    urgency = property(get_urgency)
    def is_showable(self, now=None): return self._ds.is_showable(self, now)
    showable = property(is_showable)
    def is_doable(self, now=None): return self._ds.is_doable(self, now)
    doable = property(is_doable)
    def is_purgeable(self, now=None): return self._ds.is_purgeable(self, now)
    purgeable = property(is_purgeable)
    def do(self, now=None): return self._ds.do(self, now)
    def nodo(self, now=None): return self._ds.nodo(self, now)

    def plan(self, date=None):
        u"""Planifier cette tâche pour la date spécifiée

        si date==None, la tâche devient non planifiée
        """
        self.date = date
        self.validate()

    RE_TIMELINE = re.compile(r'(?i)^timeline\s*:\s*(\d\d/\d\d/\d\d\d\d)\s*:\s*(.*)\s*$')
    RE_COMMA = re.compile(r'\s*,\s*')
    RE_DASH = re.compile(r'\s*-\s*')

    _timeline = None
    def __build_timeline(self):
        if self._timeline is None:
            body = Lines(self.get_body())
            timeline = []
            for mo in filter(None, body.map(lambda l: self.RE_TIMELINE.match(l))):
                date = parse_date(mo.group(1))
                for range in self.RE_COMMA.split(mo.group(2)):
                    times = map(parse_time, self.RE_DASH.split(range))
                    timeline.append((date,) + tuple(times))
            timeline.sort()
            self._timeline = timeline
    def __append_timeline(self, now, time, prefix, modify_only=False):
        body = Lines(self.get_body())
        append = True
        last_line_is_timeline = False
        pos = max = len(body) - 1
        while pos >= 0:
            line = body[pos]
            mo = self.RE_TIMELINE.match(line)
            if mo is not None:
                if pos == max:
                    last_line_is_timeline = True
                date = parse_date(mo.group(1))
                if date == now:
                    body[pos] = line + prefix + time
                    append = False
                break
            if not modify_only:
                # si modify_only, continuer jusqu'à trouver une ligne TimeLine:
                # sinon, on ne considère que la dernière ligne
                break
            pos -= 1
        if append:
            if not last_line_is_timeline:
                # sauter une ligne si la dernière ligne n'est pas un TimeLine
                body.append(u"")
            body.append(u"TimeLine: %s: %s" % (now, time))
        self.set_body(body.join())

    def is_started(self):
        self.__build_timeline()
        return self._timeline[-1:] and len(self._timeline[-1]) == 2 or False
    started = property(is_started)

    def __ensure_stopped(self):
        self.__build_timeline()
        if self.is_started():
            raise ValueError("This task is not stopped yet")

    def __ensure_started(self):
        self.__build_timeline()
        if not self.is_started():
            raise ValueError("This task is not started yet")

    def get_start_date(self):
        self.__ensure_started()
        event = self._timeline[-1]
        return event[0], event[1]
    start_date = property(get_start_date)

    def get_stop_date(self):
        self.__ensure_stopped()
        event = self._timeline[-1]
        return event[0], event[2]
    stop_date = property(get_stop_date)

    def start(self, summary=None, time=None, now=None):
        self.__ensure_stopped()
        if time is None: time = Time()
        if now is None: now = Date()
        self._timeline.append((now, time))
        self.__append_timeline(now, _u(time), u", ")
        if summary is not None: self.__append_body(summary)
        return self.get_start_date()

    def __append_body(self, text):
        body = Lines(self.get_body())
        if body: body.append(u"")
        body.appendtext(text)
        self.set_body(body.join())

    def stop(self, summary=None, time=None, now=None):
        self.__ensure_started()
        if time is None: time = Time()
        if now is None: now = Date()
        self._timeline[-1] = self._timeline[-1] + (time, )
        self.__append_timeline(now, _u(time), u"-", True)
        if summary is not None: self.__append_body(summary)
        return self.get_stop_date()

    def id_matches(self, tid, sid=None):
        if self.tid != tid: return False
        if sid is not None and self.sid != sid: return False
        return True

    def ids_matches(self, ids):
        for tid, sid in ids:
            if self.id_matches(tid, sid): return True
        return False

    def context_matches(self, patterns):
        patterns = contexts_list_validator(patterns)
        contexts = self.contexts
        if u"@*" in patterns:
            # Au moins un context défini
            return contexts and True or False
        elif not patterns or u"@-" in patterns:
            # Aucun context défini
            return not contexts and True or False
        else:
            # Tous les contexts mentionnés doit être dans les contexts de
            # cette tâche
            for context in patterns:
                if context[:2] == u"@-":
                    test_presence = False
                    context = u"@" + context[2:]
                else:
                    test_presence = True #XXX
                if test_presence:
                    if context not in contexts: return False
                else:
                    if context in contexts: return False
            return True

    def project_matches(self, patterns):
        patterns = projects_list_validator(patterns)
        projects = self.projects
        if u"+*" in patterns:
            # Au moins un project défini
            return projects and True or False
        elif not patterns or u"-*" in patterns:
            # Aucun project défini
            return not projects and True or False
        else:
            # Tous les projects mentionnés doit être dans les projects de
            # cette tâche
            for project in patterns:
                prefix = project[:1]
                if prefix == u"+": test_presence = True
                elif prefix == u"-": test_presence = False
                else: test_presence = True #XXX
                project = u"+" + project[1:]
                if test_presence:
                    if project not in projects: return False
                else:
                    if project in projects: return False
            return True

    def __getattr(self, name):
        lname = "_" + name
        if hasattr(self, lname): return getattr(self, lname)
        else: return getattr(self, name)

    def tag_matches(self, name, value):
        lvalue = self.__getattr(name)
        if value == u"*":
            # La valeur doit être définie
            return lvalue is not None and True or False
        elif not value:
            # La valeur ne doit pas être définie
            return lvalue is None and True or False
        else:
            # La valeur doit correspondre à l'expression régulière
            if lvalue is None: return False
            return re.search(value, lvalue) is not None

    def text_matches(self, pattern):
        tb = (self._title or u"") + u"\n\n" + (self._body or u"")
        if not pattern.startswith('(?'):
            # recherche insensible à la casse par défaut
            pattern = '(?i)' + pattern
        return re.search(pattern, tb) is not None

    def output(self, join=True, help=False, modeline=False, lastnl=False):
        lines = Lines()
        if modeline:
            lines.append(u"# -*- mode: text -*- vim:sw=4:sts=4:et:ai:si:sta")
            lines.append(u"")
        lines.appendtext(self._title, True)
        if self._body:
            lines.append(u"")
            lines.appendtext(self._body, True)
        lines.append(u"")
        lines.append(u"## contexts=%s" % u" ".join(self._contexts))
        lines.append(u"## projects=%s" % u" ".join(self._projects))
        for name, value in self.get_props(True).items():
            lines.append(u"## %s=%s" % (name, value))
        if help:
            lines.appendtext(u"""\
# -----------------------------------------------------------------------------
# Pour information, les attributs suivants peuvent être spécifiés:
# tid: identifiant de la tâche
# type: type de tâche, parmi T (tâche simple), t (tâche planifiée),
#       e (événément), i (idée), a (activité)
# cdate: date de création
# ddate: date à laquelle la tâche a été effectuée
# dates: dates auxquelles la tâche est planifiée. Liste de spécification de la
#        forme "!w"W ou "!n"W (contrainte)
#        ou ("+"O|"w"WR|DR)"/"MR"/"YR (spécification)
#        avec W=1..7 (jour de semaine), O=1..+INF (offset en jours),
#        WR=1..7[-1..7], DR=1..31[-1..31], MR=1..12[-1..12], YR=Y[-Y]
#        (intervalles de respectivement jours de semaine, jours, mois, années)
# date: prochaine date à laquelle la tâche est planifiée
# mindate: date à partir de laquelle on peut afficher et/ou effectuer la tâche
# maxdate: date maximum à laquelle la tâche peut être effectuée""")

        if join: return lines.join(lastnl)
        else: return lines

    RE_BLANK_LINE = re.compile(r'\s*$')
    RE_COMMENT_LINE = re.compile(r'#[^#]')
    RE_PROP_LINE = re.compile(r'##\s*(\S+)\s*=\s*(.*)\s*$')
    def __is_not_comment_nor_blank(self, line):
        return \
            self.RE_COMMENT_LINE.match(line) is None and \
            self.RE_BLANK_LINE.match(line) is None
    def __is_comment_or_blank_or_prop(self, line):
        return \
            self.RE_COMMENT_LINE.match(line) is not None or \
            self.RE_BLANK_LINE.match(line) is not None or \
            self.RE_PROP_LINE.match(line) is not None

    def input(self, text):
        lines = Lines(text)
        lines = lines.filter(self.__is_not_comment_nor_blank, boundaries=True)
        plines = lines.grep(self.__is_comment_or_blank_or_prop, boundaries=True)
        lines.filter(self.__is_comment_or_blank_or_prop, inverse=True, boundaries=True)
        # supprimer les attributs actuels
        #XXX à déplacer dans une méthode reset()
        names = self.get_all_props().keys()
        for name in names: delattr(self, name)
        self._contexts = []
        self._projects = []
        # traiter title et body
        if u"" in lines:
            pos = lines.index(u"")
            self.title = lines.nl.join(lines[:pos])
            self.body = lines.nl.join(lines[pos + 1:])
        else:
            self.title = lines.nl.join(lines)
        # traiter les propriétés
        for pline in plines:
            mo = self.RE_PROP_LINE.match(pline)
            if mo is None: continue

            name, value = mo.group(1), mo.group(2)
            if not self.is_valid_prop(name):
                raise ValueError("Invalid property: %s" % name)
            if name == u"contexts": value = value.split()
            elif name == u"projects": value = value.split()
            if value: setattr(self, name, value)
        self._ds = new_strategy(self.type)
        self.validate()
        return self

    def woutput(self):
        eps = [(TITLE, self.title),
               (BODY, self.body or u''),
               (CONTEXTS, u" ".join(self._contexts)),
               (PROJECTS, u" ".join(self._projects)),
               ]
        props = self.get_props(True, True)
        for name in (DATE, DATES, TYPE, DDATE,
                     MINDATE, MAXDATE, TID, CDATE):
            eps.append((name, props[name]))
            del props[name]
        eps.extend(props.items())
        return eps

    def winput(self, props):
        # supprimer les attributs actuels
        #XXX à déplacer dans une méthode reset()
        names = self.get_all_props().keys()
        for name in names: delattr(self, name)
        self._contexts = []
        self._projects = []
        # traiter title et body
        self.title = _u(props.pop(TITLE) or u"")
        body = _u(props.pop(BODY) or u"").strip()
        if body: self.body = body
        # traiter les propriétés
        for name, value in props.items():
            if not self.is_valid_prop(name):
                raise ValueError("Invalid property: %s" % name)
            elif value is None: continue
            if name == u"contexts": value = value.split()
            elif name == u"projects": value = value.split()
            if value: setattr(self, name, value)
        self._ds = new_strategy(self.type)
        self.validate()
        return self

    def __str__(self):
        return self._store.uio.s(self.output())
    def __unicode__(self):
        return self._store.uio.u(self.output())
    def __repr__(self):
        r = u'<Task'
        if self._tid is not None:
            r += u'#'
            r += self.get_id()
        if self._modified: r += u'*'
        if self._title is not None:
            r += u': '
            r += self._title.replace("\n", " ")
        for project in self.get_projects():
            r += u' '
            r += project
        for context in self.get_contexts():
            r += u' '
            r += context
        _u = self._store.uio.u
        for name, value in self.get_props(True).items():
            if name == 'tid': continue
            r += u' '
            r += _u(name)
            r += u'='
            r += _u(value)
        if self.urgent: r += " urgent"
        if self.normal: r += " normal"
        if self.planned: r += " planned"
        if self.obsolete: r += " obsolete"
        if self.done: r += " done"
        if self.showable: r += " showable"
        if self.doable: r += " doable"
        if self.purgeable: r += " purgeable"
        r += u'>'
        return _s(r)

    # ordre relatif des tâches
    # i = idea
    # d = tâches effectuées
    # f = tâches à faire dans le futur
    # t = tâches à faire
    # p = tâche planifiée à faire aujourd'hui
    # p = tâche planifiée à faire urgement
    # x = les tâches inclassable (parce que le cas n'est pas traité dans cette
    #     méthode. à corriger.)
    _TYPES = ['x', 'i', 'd', 'f', 't', 'p', 'u']
    def _get_type(cls, t):
        if t.is_idea_type(): return 'i'
        if t.is_done() or t.is_obsolete(): return 'd'
        if t.is_planned(): return 'f'
        if t.is_normal():
            type = t.get_type()
            if type in (PLAN_TYPE, TASK_TYPE):
                if t.get_date() is not None: return 'p'
                else: return 't'
            elif type == EVENT_TYPE: return 'p'
        if t.is_urgent(): return 'u'
        return 'x'
    _get_type = classmethod(_get_type)
    def _get_order(cls, t):
        type = cls._get_type(t)
        return cls._TYPES.index(type)
    _get_order = classmethod(_get_order)

    def cmp(cls, t1, t2):
        # comparer sur le type de tâche
        v = cmp(cls._get_order(t1), cls._get_order(t2))
        if v != 0: return v
        # comparer sur la date d'effet
        v = cmp(t1.get_delay() or 0, t2.get_delay() or 0)
        if v != 0: return v
        # comparer sur la date de création
        v = cmp(t1.get_cdate(), t2.get_cdate())
        if v != 0: return v
        # enfin, comparer sur l'id
        v = cmp(t1.get_id(), t2.get_id())
        return v
    cmp = classmethod(cmp)

class TaskFactory:
    _store = None

    def __init__(self, store):
        self._store = store

    def build_task(self, **props):
        return Task(self._store, **props)

    def new_task(self, title, body=None, **props):
        props[TITLE] = title
        if body is not None: props[BODY] = body
        if not props.has_key(CDATE):
            props[CDATE] = Date()
        return self.build_task(**props)
