#!/usr/bin/env python
# -*- coding: utf-8 -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8

"""%(name)s: Gestion d'un fichier tiddlywiki.html
USAGE
    %(name)s [-f tiddlywiki.html] action [options]
"""

try: True, False
except NameError: True, False = 1, 0

import re, types
from os import path
from time import localtime
from iso8859 import quote_entity, unquote_entity

__all__ = ["ensure_unicode", "Tiddler", "TiddlyWiki", "list_tiddlers", "replace_tiddler", "delete_tiddler"]

# XXX à configurer
ARGS_ENCODING="latin-1"

######################################################################
# Classes et constantes de base

RE_TAG = re.compile(r'<([^ >]+)')
RE_ENDTAG = re.compile(r'</([^ ]+)>')
RE_SOMETHING = re.compile(r'\S')
RE_ATTR1 = re.compile(r'([^= ]+)="([^"]*)"')
RE_ATTR2 = re.compile(r"([^= ]+)='([^']*)'")
def parseDiv(data):
    attrs = {}
    text = ''

    # Parser <div
    mo = RE_TAG.search(data)
    if mo is None or mo.group(1) != 'div': return None, None, data
    data = data[mo.end():]
    # Parser attributs
    while True:
        # Sauter les espaces
        mo = RE_SOMETHING.search(data)
        if not mo: raise ValueError("Fin de flux")
        data = data[mo.start():]
        # Parser l'attribut
        mo = RE_ATTR1.match(data) or RE_ATTR2.match(data) or None
        if not mo: break
        name = mo.group(1)
        value = unquote_entity(mo.group(2))
        attrs[name] = value
        data = data[mo.end():]
    # Fin de tag
    mo = RE_SOMETHING.search(data)
    if not mo: raise ValueError("Fin de flux")
    if data[:1] != '>': raise ValueError("Attendu >")
    data = data[1:]
    # Texte
    pos = data.find("<")
    if pos == -1: raise ValueError("Impossible de trouver le tag de fin")
    text = unquote_entity(data[:pos])
    data = data[pos:]
    # Tag fermant
    if data[:6] != '</div>': raise ValueError("Attendu </div>")
    data = data[6:]

    return attrs, text, data

def ensure_unicode(data):
    if data is None: return None
    elif type(data) is types.UnicodeType: return data
    else: return unicode(data, ARGS_ENCODING)

def quote_nl(text):
    text = text.replace("\n", "\\n")
    return text

def unquote_nl(text):
    text = text.replace("\\n", "\n")
    return text

def get_now():
    """Retourner la date actuelle dans le format attendu par le champ modified
    """
    t = localtime()
    return '%04i%02i%02i%02i%02i' % t[0:5]

class Tiddler:
    def __init__(self, tiddler=None, text=None, tags=None, modifier=None, modified=None, line=None):
        self.tiddler = ensure_unicode(tiddler)
        self.text = ensure_unicode(text)
        self.tags = ensure_unicode(tags)
        self.modifier = ensure_unicode(modifier)
        self.modified = ensure_unicode(modified)
        if line is not None: parse(line)

    def is_valid(self):
        """Retourner True si ce Tiddler est valide
        """
        return self.tiddler is not None

    def parse(self, line):
        """Charger un Tiddler depuis line, et retourner le reste de la chaine qui n'a pas été parsé
        """
        attrs, text, line = parseDiv(ensure_unicode(line))
        if text is not None:
            self.tiddler = attrs["tiddler"]
            self.modified = attrs["modified"]
            self.modifier = attrs["modifier"]
            self.tags = attrs["tags"]
            self.text = unquote_nl(text)
        return line

    def to_line(self):
        """Retourner ce tiddler sous forme de ligne
        """
        if not self.is_valid(): die("Il faut spécifier l'id du tiddler")

        line = u'<div'
        line += u' tiddler="%s"' % quote_entity(self.tiddler or '')
        line += u' modified="%s"' % quote_entity(self.modified or get_now())
        line += u' modifier="%s"' % quote_entity(self.modifier or 'twlib')
        line += u' tags="%s"' % quote_entity(self.tags or '')
        line += u'>'
        line += quote_nl(quote_entity(self.text))
        line += u'</div>'
        return line

    RE_SPACES = re.compile("\s+")
    def tags_contains(self, tag):
        return tag in self.RE_SPACES.split(self.tags)

class TiddlyWiki:
    def __init__(self, twfile=None):
        self.twfile = None
        self.prefix = None
        self.suffix = None
        self.tsmap = None
        self.tslist = None
        if twfile is not None: self.load(twfile)

    RE_MAGIC = re.compile(r'<!--\nTiddlyWiki')
    PREFIX = '<div id="storeArea">'

    def load(self, twfile):
        inf = open(twfile, 'rb')
        data = inf.read()
        inf.close()

        # Vérifier qu'il s'agit bien d'un TiddlyWiki
        if not self.RE_MAGIC.search(data): raise ValueError("Fichier de format incorrect: ce n'est pas un tiddlywiki: %s" % twfile)
        self.twfile = twfile
        self.twpf = path.abspath(twfile)

        # Vérifier la présence du préfixe
        pos = data.find(self.PREFIX)
        if pos == -1: raise ValueError("Fichier de format incorrect: impossible de trouver le préfixe " + PREFIX)
        pos += len(self.PREFIX)

        self.prefix = data[:pos]
        data = unicode(data[pos:], "utf-8")

        self.tsmap = {}
        self.tslist = []
        while True:
            t = Tiddler()
            data = t.parse(data)
            if not t.is_valid(): break
            self.tsmap[t.tiddler] = t
            self.tslist.append(t.tiddler)
        self.suffix = data.encode("utf-8")

    def save(self, twfile=None):
        if twfile is None: twfile = self.twfile
        if twfile is None: die("Il faut spécifier le fichier de sortie")

        outf = open(twfile, 'wb')
        outf.write(self.prefix)
        for tiddler in self.tslist:
            t = self.tsmap[tiddler]
            outf.write("\n")
            outf.write(t.to_line().encode("utf-8"))
        outf.write(self.suffix)
        outf.close()

    def __len__(self):
        return len(self.tslist)
    def __getitem__(self, key):
        if type(key) is int: return self.tslist[key]
        else: return self.tsmap[key]
    def get(self, key, default=None):
        return self.tsmap.get(key, default)
    def __setitem__(self, key, t):
        if type(key) is int: key = self.tslist[key]
        if key in self.tslist:
            self.tsmap[key] = t
        else:
            self.tsmap[key] = t
            self.tslist.append(key)
    def __delitem__(self, key):
        if type(key) is int: key = self.tslist[key]
        if key in self.tslist:
            del self.tsmap[key]
            self.tslist.remove(key)
    remove = __delitem__

    def put(self, t):
        self[t.tiddler] = t

######################################################################
# Lister les tiddlers

def list_tiddlers(twfile, args=None):
    """USAGE: list [tiddlers...]
    """
    tw = TiddlyWiki(twfile)
    args = args or ()

    if args:
        for tiddler in args:
            t = tw.get(tiddler, None)
            if t is not None:
                print "## tiddler=%s" % t.tiddler
                print "## modifier=%s" % t.modifier
                print "## modified=%s" % t.modified
                print "## tags=%s" % t.tags
                print
                print t.text
            else:
                ewarn("Tiddler inconnu: %s" % tiddler)
    else:
        for tiddler in tw:
            print tiddler

######################################################################
# Ajouter ou remplacer un tiddler

def replace_tiddler(twfile, tiddler=None, text=None, tags=None, modifier=None, modified=None, args=None):
    """USAGE: replace [-t tags] [-m modifier] [-d modified] tiddler text
    """
    tw = TiddlyWiki(twfile)
    args = args or ()
    opts, args = get_args(args, [
        ('t:', None, 'Spécifier tags'),
        ('m:', None, 'Spécifier modifier'),
        ('d:', None, 'Spécicier modified'),
        ])
    for opt, value in opts:
        if opt == '-t':
            tags = value
        elif opt == '-m':
            modifier = value
        elif opt == '-d':
            modified = value

    i = 0
    if tiddler is None:
        tiddler = args[i:i + 1] and ensure_unicode(args[i]) or None
        if tiddler is None: die("Il faut spécifier l'id du tiddler")
        i = i + 1
    if text is None:
        text = args[i:i + 1] and ensure_unicode(args[i]) or None
        if text is None: die("Il faut spécifier le texte du tiddler")

    t = tw.get(tiddler, None)
    if t is None:
        t = Tiddler(tiddler, text, tags, modifier, modified)
    else:
        t.text = text
        if tags is not None: t.tags = tags
        if modifier is not None: t.modifier = modifier
        if modified is not None: t.modified = modified
    tw[tiddler] = t

    tw.save()

######################################################################
# Supprimer un tiddler

def delete_tiddler(twfile, tiddler=None, args=None):
    """USAGE: delete tiddler
    """
    tw = TiddlyWiki(twfile)

    args = args or ()
    if tiddler is None:
        tiddler = args[0:1] and ensure_unicode(args[0]) or None
        if tiddler is None: die("Il faut spécifier l'id du tiddler")

    if tiddler in tw:
        del tw[tiddler]
        tw.save()
    else:
        die("Tiddler invalide: %s" % tiddler)

