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

__all__ = ('AbstractStore', 'UnsavedModificationError')

import re

from ulib.base.uio import _s, _u, Utf8IO
from ulib.base.lines import Lines
from base import *
from consts import *
from tasks import Task, TaskFactory

class UnsavedModificationError(Exception):
    pass

class AbstractStore(PropsMixin, object):
    _uio, uio, get_uio, set_uio = make_sprop('_uio', Utf8IO(), deleter=False)
    _tf = None
    _valid, valid, is_valid = make_sprop('_valid', False, setter=False, deleter=False)

    GENERIC_PROPERTIES = [SERIAL, TYPE]
    SPECIAL_PROPERTIES = [CONTEXTS, PROJECTS]

    _lsid = None # valeur locale temporaire gérée par MultipleStore, si _sid n'est pas défini
    def set_lsid(self, lsid): self._lsid = self._uio.u(lsid)
    def get_sid(self):
        if self._sid is not None: return self._sid
        else: return self._lsid
    _sid, sid, get_sid, set_sid, del_sid = make_suprop('_sid', None, getter=get_sid)
    _serial, serial, get_serial, set_serial, del_serial = make_suprop('_serial', None)
    _type, type, get_type, set_type, del_type = make_suprop('_type', DEFAULT_TYPE)
    _contexts, contexts, get_contexts, set_contexts, del_contexts = \
        make_sprop('_contexts', None, validator=contexts_list_validator)
    _projects, projects, get_projects, set_projects, del_projects = \
        make_sprop('_projects', None, validator=projects_list_validator)

    def __init__(self):
        self._tf = TaskFactory(self)
        self._valid = True

    def get_name(self):
        u"""Retourner le nom de ce store

        @rtype: unicode
        """
        raise NotImplementedError
    name = property(get_name)

    def get_wstore(self):
        u"""Retourner le store destination, c'est à dire celui dans lequel on
        ajoute les tâches par défaut.

        @return: self
        """
        return self

    def set_wstore(self, name=None):
        u"""Choisir le store destination.

        @param name: None ou le nom de ce store.
        @return: self
        @raise ValueError: si le nom n'est pas celui de ce store.
        """
        if name is not None and name != self.get_name():
            raise ValueError("Invalid store: %s" % name)

    def get_stores(self, force=False):
        u"""Retourner la liste des stores gérés par ce store. Si c'est un store
        simple, la liste de contient qu'un élément.

        @param force: forcer la reconstruction de la liste. par défaut,
            utiliser une valeur mise en cache.
        @rtype: tuple
        """
        return (self,)

    def get_store_by_sid(self, sid=None):
        u"""Obtenir le store dont on donne le sid, ou None si le sid n'existe
        pas. Si sid==None, retourner le store par défaut.

        @rtype: AbstractStore
        """
        if sid is None or _u(self.sid) == _u(sid): return self
        else: return None

    def get_tasks(self):
        u"""Retourner la liste des tâches gérées par cet objet.

        @rtype: list
        """
        raise NotImplementedError
    tasks = property(get_tasks)

    RE_ID = re.compile(r'((?:(\S+)\.)?(\d+))\s*')
    def _parse_id(self, value, split=False):
        u"""Si value est un id de la forme tid ou sid.tid, retourner l'id.
        Sinon retourner None.

        si split==True, retourner (tid, sid)
        """
        mo = self.RE_ID.match(value)
        if split:
            if mo is not None: return mo.group(3), mo.group(2)
            else: return None, None
        else:
            if mo is not None: return mo.group(1)
            else: return None

    def find_tasks(self, ids=None, text=None, props=None, projects=None, contexts=None):
        u"""
        @type ids: list
        @type text: unicode
        @type props: dict
        @type projects: list
        @type contexts: list
        """
        tasks = self.get_tasks()
        if ids is not None:
            tasks = [task for task in tasks if task.ids_matches(ids)]
        if contexts is not None:
            tasks = [task for task in tasks if task.context_matches(contexts)]
        if projects is not None:
            tasks = [task for task in tasks if task.project_matches(projects)]
        if props is not None:
            for name, value in props.items():
                tasks = [task for task in tasks if task.tag_matches(name, value)]
        if text is not None:
            tasks = [task for task in tasks if task.text_matches(text)]
        return tasks

    def get_by_id(self, id):
        """Obtenir une tâche par son id, de la forme tid ou sid.tid

        @return: La tâche, ou None si aucune tâche n'est trouvée.
        @raise ValueError: si plusieurs tâches correspondent ou si l'id est invalide.
        """
        tid, sid = self._parse_id(id, True)
        if tid is None:
            raise ValueError("Invalid id: %s" % _s(id))
        tasks = self.find_tasks(ids=[(tid, sid)])
        if not tasks: return None
        elif len(tasks) == 1: return tasks[0]
        else: raise ValueError("Found several tasks with same id: %s" % _s(id))

    RE_PROP = re.compile(r'([^=\s]+)=(\S+)\s*')
    def _parse_prop(self, value):
        mo = self.RE_PROP.match(value)
        if mo is not None:
            name = _s(mo.group(1))
            if Task.is_valid_prop(name):
                return name, mo.group(2)
        return None, None
    RE_PROJ = re.compile(r'((?:\+|-)\S+)\s*')
    def _parse_project(self, value):
        mo = self.RE_PROJ.match(value)
        if mo is not None: return mo.group(1)
        else: return None
    RE_CTXT = re.compile(r'(@(?:-)?\S+)\s*')
    def _parse_context(self, value):
        mo = self.RE_CTXT.match(value)
        if mo is not None: return mo.group(1)
        else: return None

    def new_task(self, title, body=None, **kw):
        store = self.get_wstore()
        task = store._tf.new_task(title, body, **kw)
        store.tasks.append(task)
        self._task_attached(task, True)
        return task

    def new_task_from_clargs(self, args):
        title = []
        projects = []
        contexts = []
        props = {}
        for arg in [_u(arg) for arg in args]:
            if arg.startswith("~"):
                title.append(arg[1:])
            else:
                name, value = self._parse_prop(arg)
                project = self._parse_project(arg)
                context = self._parse_context(arg)
                if name is not None: props[name] = value
                elif project is not None: projects.append(project)
                elif context is not None: contexts.append(context)
                else: title.append(arg)
        if projects: props[PROJECTS] = projects
        if contexts: props[CONTEXTS] = contexts
        title = u" ".join(title)
        pos = title.find("\n\n")
        if pos != -1:
            props[BODY] = title[pos + 2:]
            title = title[:pos]
        props[TITLE] = title
        return self.new_task(**props)

    def remove(self, task):
        u"""Supprimer la tâche task de ce store

        @return: True si la suppression s'est correctement effectuée, False
        sinon (par exemple si la tâche ne fait pas partie de ce store)
        """
        raise NotImplementedError

    _modified = False
    def is_modified(self):
        u"""Retourner True si l'une des tâches a été modifiée
        """
        if not self._modified:
            # L'état modifié du store est gardé en cache
            # On ne fait le test que si le store n'est pas modifié
            for task in self.get_tasks():
                if task.is_modified():
                    self._modified = True
        return self._modified
    modified = property(is_modified)

    def _task_attached(self, task, validate=False):
        u"""Hook appelé après qu'une tâche soit attaché à ce store
        """
        if validate: task.validate()

    def _props2line(self, o):
        line = u''
        props = o.get_props(True)
        projects = o._projects or ()
        contexts = o._contexts or ()
        if props or projects or contexts:
            prefix = u' {'
            for project in projects:
                line += u'%s%s' % (prefix, project)
                prefix = u' '
            for context in contexts:
                line += u'%s%s' % (prefix, context)
                prefix = u' '
            for name, value in props.items():
                line += u'%s%s=%s' % (prefix, name, value)
                prefix = u' '
            line += u'}'
        return line

    def _unquote_nl(self, text):
        if text is not None:
            text = text.replace(r'\n', '\n')
            text = text.replace(r'\\', '\\')
        return text

    def _split_body(self, text):
        pos = text.find('\n\n')
        if pos == -1: return text, None
        title = text[:pos]
        body = text[pos + 2:]
        return title, body

    RE_TAGS = re.compile(r'\s*\{\s*((?:(?:(?:\+|-)\S+\s*)?(?:@(?:-)?\S+\s*)?(?:\S+=\S+\s*)?)*)\}\s*$')
    def _line2props(self, line, parse_text=True):
        _s, _u = self._uio.s, self._uio.u
        projects = []
        contexts = []
        props = {}
        line = _u(line)
        mo = self.RE_TAGS.search(line)
        if mo is not None:
            tags = mo.group(1)
            line = line[:mo.start(0)]
            while tags:
                propmo = self.RE_PROP.match(tags)
                projmo = self.RE_PROJ.match(tags)
                ctxtmo = self.RE_CTXT.match(tags)
                if propmo is not None:
                    props[_s(propmo.group(1))] = propmo.group(2)
                    tags = tags[propmo.end(0):]
                elif projmo is not None:
                    projects.append(projmo.group(1))
                    tags = tags[projmo.end(0):]
                elif ctxtmo is not None:
                    contexts.append(ctxtmo.group(1))
                    tags = tags[ctxtmo.end(0):]
                else:
                    raise ValueError("Invalid tags: %s" % _s(tags))
            if projects: props[PROJECTS] = projects
            if contexts: props[CONTEXTS] = contexts
        if parse_text:
            title, body = self._split_body(self._unquote_nl(line))
            props[TITLE] = title
            if body is not None:
                props[BODY] = body
        return props

    def load(self, infdn=None, force=False, **ignored):
        u"""Recharger le store depuis le disque.

        @param force: Recharger, même s'il y a eu des modifications locales
        @raise UnsavedModificationError: s'il y a eu des modifications locales,
            sauf si force==True
        """
        raise NotImplementedError

    def should_reload(self):
        u"""Retourner True si ce store doit être rechargé (i.e. son serial est
        différent de celui qui est sur disque, ce qui signifie qu'il y a eu des
        modifications externes)
        """
        raise NotImplementedError

    def revert(self):
        u"""Annuler les modifications en cours.

        Par défaut, recharger les tâches avec load(force=True) uniquement s'il
        y a eu des modifications.

        @return: True si le store a été rechargé, False sinon
        """
        if self.is_modified():
            self.load(force=True)
            return True
        return False

    def _quote_nl(self, text):
        if text is not None:
            text = text.replace('\\', r'\\')
            text = text.replace('\n', r'\n')
        return text

    def _task2line(self, task):
        line = task.title or u''
        if task.body:
            line += u'\n\n'
            line += task.body
        line += self._props2line(task)
        return self._quote_nl(line)

    ENCODING = None
    SPROP = None

    def get_hlines(self, encoding=None, sprop=None):
        if encoding is None: encoding = self.ENCODING
        if sprop is None: sprop = self.SPROP

        lines = Lines(uio=self._uio)
        # écrire les propriétés
        if encoding is not None:
            lines.append(encoding % {'encoding': self._uio.encoding})
        if sprop is not None:
            lines.append(sprop + self._props2line(self))
        return lines

    def get_lines(self, serial=None, encoding=None, sprop=None):
        if serial is not None: self.serial = serial
        lines = self.get_hlines(encoding, sprop)
        # écrire les tâches
        for task in self.tasks:
            lines.append(self._task2line(task))
        return lines

    def save(self, outfdn=None, force=False):
        u"""Enregistrer les tâches.

        @param force: forcer la sauvegarde même si le store n'a pas été modifié
        @return: True si la sauvegarde a été effectuée, False si elle n'a pas
        été nécessaire.
        """
        raise NotImplementedError

    def stop_all(self):
        u"""Arrêter toutes les tâches en cours.
        """
        modified = False
        for t in self.get_tasks():
            if t.is_started():
                t.stop()
                modified = True
        if modified: self.save()
        return modified
