from TinderboxDoc import *
import libxml2,libxslt
import re

listRegexp = re.compile(r'(([ *#])(\2*)) [^ ]')

class _TextRange:
    """This class allows to associate ranges in a text with a markup, so that successive definition of (possibly overlapping) stylistic subranges will be transformed into a hierarchy of markup tags."""
    def __init__(self, start, end, tag=None):
        self.start = start
        self.end = end
        self.subranges = []
        self.dummyTag = False
        if not tag:
            tag = libxml2.newNode('dummy')
            self.dummyTag = True
        self.tag = tag
    def splitHead(self, atPos):
        assert(self.start <= atPos)
        assert(self.end >= atPos)
        start = self.start
        self.start = atPos
        if self.dummyTag:
            return _TextRange(start, atPos)
        else:
            return _TextRange(start, atPos, self.tag.copyNode(True))
    def addSubRange(self, aRange):
        if self.contains(aRange):
            for (i, s) in enumerate(self.subranges):
                if s.contains(aRange):
                    s.addSubRange(aRange)
                    return
                elif s.overlaps(aRange):
                    # Break the range up
                    if (aRange.start < s.start):
                        newRange = aRange.splitHead(s.start)
                        self.subranges.insert(i, newRange)
                        return self.addSubRange(aRange)
                    if aRange.end > s.end:
                        newRange = aRange.splitHead(s.end)
                        s.addSubRange(newRange)
                        return self.addSubRange(aRange)
                if s.after(aRange):
                    self.subranges.insert(i,aRange)
                    return
            self.subranges.append(aRange)
        else:
            raise 'wrongPos: %s, %s'%(self,range)
    def __str__(self):
        try:
            desc= "TextRange (%s: %s-%s)\n"%(self.tag,self.start,self.end)
            for subr in self.subranges:
                desc = desc+" "+str(subr)+"\n"
            return desc
        except:
            return "_TextRange proto"
    def insertSubRange(self, range):
        if self.contains(range):
            newSubranges = []
            inserted = False
            for s in self.subranges:
                if s.before(range):
                    newSubranges.append(s)
                elif range.contains(s):
                    range.addSubRange(s)
                elif s.after(range):
                    if not inserted:
                        newSubranges.append(range)
                        inserted = True
                    newSubranges.append(s)
                elif s.overlaps(range):
                    # Break the range up?
                    raise 'overlapException %d %d %d %d' % (s.start, s.end, range.start, range.end)
            if not inserted:
                newSubranges.append(range)
            self.subranges = newSubranges
        else:
            raise 'wrongPos: %s, %s'%(self,range)
    def contains(self, range):
        return range.start >= self.start and range.end <= self.end
    def overlaps(self, range):
        return (self.start <= range.start < self.end or self.start < range.end <= self.end) and not self.contains(range)
    def before(self, range):
        return self.end <= range.start
    def after(self, range):
        return self.start >= range.end
    def _add(self, item):
        if not isinstance(self.tag, libxml2.xmlNode):
            raise 'invalidTag'+self.tag
        self.tag.addChild(item)
    def compileTag(self, text, encoding="utf-8"):
        if len(self.subranges) == 0:
            self.tag.addContent(text[self.start:self.end].encode(encoding))
        else:
            position = self.start
            for s in self.subranges:
                if position < s.start:
                    self._add(libxml2.newText(text[position:s.start].encode(encoding)))
                self._add(s.compileTag(text, encoding))
                position = s.end
            if position < self.end:
                self._add(libxml2.newText(text[position:self.end].encode(encoding)))
        if self.dummyTag:
            return list(TinderboxDoc.childrenIterator(self.tag))
        else:
            return self.tag



class TinderboxXslExtension(TinderboxDoc):
    """ This class allows to apply a XSL stylesheet to a Tinderbox documents. Some things are done differently, thanks to keys."""
    def __init__(self,document):
        TinderboxDoc.__init__(self,document)
        libxslt.registerExtensionClass("http://eastgate.com/Tinderbox", self)
        self.newLinks = []
        self.linksById = {}
    
    def _calculateIds(self, node):
        pass # We will use XSL keys instead
    
    def _declareNewNoteId(self, node):
        pass # We will use XSL keys instead
    
    def _calculateProto(self, node):
        pass # We will use XSL keys instead
    
    def _registerXPathFunctions(self):
        pass # Will be registered as extension functions
    
    def getNoteById(self, id):
        nodes = self.xpathContext.xpathEval("key('itemId','%s')"%(str(id)))
        return nodes[0]
    
    def getNoteByIdAllowNone(self, id):
        nodes = self.xpathContext.xpathEval("key('itemId','%s')"%(str(id)))
        if not nodes: return None
        return nodes[0]
    
    def getPrototypeById(self, id):
        nodes = self.xpathContext.xpathEval(
            "key('itemId',key('prototypeLink','%s')/@sourceid)"%(str(id)))
        if nodes:
            return nodes[0]
        return None
    
    def ctxtInit(self, ctxt, URI):
        # ctxt is a transformCtxt
        self.xpathContext = ctxt.context()
    
    def styleInit(self, style, ns_uri):
        self.stylesheet = style
        style.addKey('itemId',None,'item','@ID',style)
        style.addKey('prototypeLink',None,"link[@name='prototype']",'@destid',style)
        TinderboxDoc.styleInit(self, style, ns_uri)
    
    def xsltImplementNewLinks(self, tctxt, sourceNode, instruction, compiledInst):
        tctxt = libxslt.transformCtxt(_obj=tctxt)
        sourceNode = libxml2.nodeWrap(sourceNode)
        instruction = libxml2.nodeWrap(instruction)
        insertNode = tctxt.insertNode()
        xpathContext = tctxt.context()
        doc = sourceNode.doc
        docContext = self.getDocContext(doc)
        for link in docContext.newLinks:
            link.getMissingData(xpathContext)
            node = link.toNode()
            insertNode.addChild(node)
    
    def xpathTextAsMarkup(self, ctx, linkType=None, breakPara=True, aNode=None, processLists=True):
        """
        This returns the text of the given node as a XML text object, broken down into paragraphs and text links.
        paragraphs are <p> elements;
        The links are returned as <link> elements within the paragraphs, with attributes ref (desination ref Id) and name (link name)
        linkType - optional - restrict to a link type (other links are ignored and incorporated in the text)
        breakPara - should paragraphs be broen by <p> tags
        aNode - the target item (or .)
        """
        parserContext = libxslt.xpathParserContext(_obj=ctx)
        if aNode:
            node = libxml2.xmlNode(_obj=aNode[0])
        else:
            xpathContext = parserContext.context()
            node = xpathContext.contextNode()
        if node.name != "item":
            raise "Not a tinderbox item: %s" % node
        textNode = None
        c = node.children
        while c:
            if c.name == "text" and not c.isText():
                textNode = c
                break
            c = c.next
        if textNode == None:
            return []
        text = textNode.content.decode('utf-8')
        log.debug("for node %s, text is:\n%s\n\n",node.prop('ID'),text.encode('utf-8'))
        result = _TextRange(0, len(text),[])
        if breakPara:
            position = 0
            para = text.find("\n",position)
            while (para > 0):
                p = libxml2.newNode('p')
                result.addSubRange(_TextRange(position,para+1,p))
                position = para+1
                para = text.find("\n",position)
            if (position < len(text)):
                p = libxml2.newNode('p')
                result.addSubRange(_TextRange(position,len(text),p))
        xpath = "/tinderbox/links/link[@sourceid='"+node.prop('ID')
        if linkType:
            xpath += "' and @name='"+linkType
        xpath += "' and @sstart >= 0]"
        log.debug("evaling %s",xpath)
        val = node.xpathEval(xpath)
        log.debug("yielded %s",str(val))
        if len(val) > 0:
            val.sort(lambda x, y: cmp(int(x.prop('sstart')),int(y.prop('sstart'))))
            position = 0
            for link in val:
                linkStart = int(link.prop('sstart'))
                linkEnd = linkStart+int(link.prop('slen'))
                linkN = libxml2.newNode('link')
                linkN.newProp('name', link.prop('name'))
                linkN.newProp('ref', link.prop('destid'))
                url = link.prop('URL')
                if url:
                    linkN.newProp('url', url)
                linkRange = _TextRange(linkStart, linkEnd, linkN)
                result.addSubRange(linkRange)
        styles = textNode.next
        while styles and styles.isText():
            styles = styles.next
        if styles and styles.name == 'styles':
            currentPos = 0
            boldStart = -1
            italStart = -1
            for child in TinderboxDoc.childrenIterator(styles):
                if child.name == 'tstyle':
                    bold = int(child.prop('bold'))
                    ital = int(child.prop('italic'))
                    pos = int(child.prop('start'))
                    if bold and boldStart < 0:
                        boldStart = pos
                    elif boldStart >= 0 and not bold:
                        result.addSubRange(_TextRange(boldStart, pos, libxml2.newNode('b')))
                        boldStart = -1
                    if ital and italStart < 0:
                        italStart = pos
                    elif italStart >= 0 and not ital:
                        result.addSubRange(_TextRange(italStart, pos, libxml2.newNode('i')))
                        italStart = -1
            if boldStart >= 0:
                result.addSubRange(_TextRange(boldStart, len(text), libxml2.newNode('b')))
            if italStart >= 0:
                result.addSubRange(_TextRange(boldStart, len(text), libxml2.newNode('i')))
        tags = result.compileTag(text)
        for tag in tags:
            tag.setTreeDoc(node.doc)
            #print tag.serialize()
        if processLists:
            tags = self.processBlocks(tags)
        return tags
    
    def _removeFirstChars(self, node, howMany=1):
        assert(node.type == 'element')
        child = node.children
        assert(child.type == 'text')
        child.setContent(child.content[howMany:])
    
    _prefixre = re.compile(r"^([ #*]+) +[^ ]")
    _tagTypes = {'#':'ol','*':'ul',' ':'p'}
    def processBlocks(self, tags):
        newTags = []
        activeTagName = None
        activeTags = [] # stack of ol or ul elements
        activeTagDepths = [] # memorize depths to handle * followed by *** (grrr.)
        depth=0
        lastLi = []
        postProcess = []
        for tag in tags:
            assert(tag.type == 'element')
            content = tag.content
            result = self._prefixre.match(content)
            if result:
                prefix = result.group(1)
                newDepth = len(prefix)
                newTagName = self._tagTypes[prefix[-1]]
                # first, remove tags that are finished
                while  (activeTagDepths 
                   and (newDepth < depth
                      or (newDepth == depth # change of tag type at same level...
                       and (newTagName not in ('p',activeTagName))))):
                    activeTagDepths.pop()
                    activeTags.pop()
                    lastLi.pop()
                    if activeTagDepths:
                        depth = activeTagDepths[-1]
                        activeTagName = activeTags[-1].name
                    else:
                        depth = 0
                        activeTagName = None
                        lastLi = []
                if newTagName == 'p':
                    # Handle p in li. If more spaces, make it preformatted.
                    tag.unlinkNode()
                    if (newDepth > depth+1):
                        tag.setName('pre')
                    if depth > 0:
                        assert(len(lastLi))
                        lastLi[-1].addChild(tag)
                    else:
                        newTags.append(tag)
                    # note depth and activeTags do not change.
                    continue
                if (newDepth > depth):
                    # create new tag 
                    newNode = libxml2.newNode(newTagName)
                    newNode.setTreeDoc(tag.doc)
                    if (depth == 0):
                        newTags.append(newNode)
                    else:
                        assert(len(lastLi))
                        lastLi[-1].addChild(newNode)
                    activeTags.append(newNode)
                    activeTagName = newTagName
                    activeTagDepths.append(newDepth)
                    lastLi.append(None)
                    depth = newDepth
                    #fallthrough
                # create the li element
                assert(activeTags)
                tag.unlinkNode()
                tag.setName('li')
                lastLi[-1] = tag
                self._removeFirstChars(tag, depth+1)
                activeTags[-1].addChild(tag)
            else:
                if activeTags:
                    # clear active tag
                    activeTagName = None
                    activeTags = []
                    activeTagDepths = []
                    depth=0
                    lastLi = []
                newTags.append(tag)
        return newTags
