# -*- coding: utf-8 -*-
# Tabellio -- software suite for deliberative assemblies
#          -- suite logicielle pour assemblées délibératives
#          -- http://www.tabellio.org/
# Copyright (C) 2006 Parlement de la Communauté française de Belgique

# This file is part of Tabellio.

# Tabellio is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.

# Tabellio is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

from types import UnicodeType, StringType
import threading, weakref
from cStringIO import StringIO
import xml.sax
from xml.sax.handler import feature_namespaces, feature_external_ges, property_lexical_handler
import logging

import libxml2
import libxslt

libxml2.lineNumbersDefault(1)
libxml2.substituteEntitiesDefault(1)

__log = None
def log():
    global __log
    if not __log:
        __log = logging.getLogger("lib.xmlutils")
    return __log

class XMLError(Exception):
    pass

__loglibxml2 = None
def _defaultErrorHandler(ctx, str):
    global __loglibxml2
    if not __loglibxml2:
        __loglibxml2 = logging.getLogger("libxml2")
    __loglibxml2.warn(str)

libxslt.registerErrorHandler(_defaultErrorHandler, None)
libxslt.registerAllExtras()

class _ErrorCallback:

    def __init__(self):
        self.__msg = []

    def callback(self, arg, msg, severity, reserved):
        self.__msg.append("- %s: %s (%s)" % (severity, msg, arg))

    def hasError(self):
        return len(self.__msg) > 0

    def getError(self):
        return "".join(self.__msg)

###########################################################################
# helpers for libxml2 tree-oriented API
#

def parseUri(uri, validate):
    """Parse an URI and return a libxml2 document object"""

    log().debug("parseUri %s" % uri)

    ctxt = libxml2.createFileParserCtxt(uri)
    ctxt.validate(validate)
    ec = _ErrorCallback()
    ctxt.setErrorHandler(ec.callback, None)
    ctxt.parseDocument()

    if ec.hasError():
        raise XMLError, "Error parsing %s\n%s" % (uri, ec.getError())

    return ctxt.doc()

def parseString(inputString, validate, baseUri="dummy.xml"):
    """Parse an input string and return a libxml2 document object"""
    if type(inputString) is UnicodeType:
        inputString = inputString.encode("utf8")

    ctxt = libxml2.createPushParser(None, "", 0, baseUri)
    ctxt.validate(validate)
    ec = _ErrorCallback()
    ctxt.setErrorHandler(ec.callback, None)
    ctxt.parseChunk(inputString, len(inputString), 1)

    if ec.hasError():
        file("/tmp/debug.xml", "wb").write(inputString)
        raise XMLError, "Error parsing string: %s" % ec.getError()

    return ctxt.doc()

def parseStream(inputStream, validate, baseUri="dummy.xml"):
    """Parse an input stream and return a libxml2 document object"""
    # TODO: read chunk by chunk
    return parseString(inputStream.read(), validate, baseUri)


def parseHTMLUri(uri):
    """Parse an URI and return a libxml2 document object"""

    log().debug("parseUri %s" % uri)

    ctxt = libxml2.htmlCreateFileParserCtxt(uri, None)
    ec = _ErrorCallback()
    ctxt.setErrorHandler(ec.callback, None)
    ctxt.parseDocument()

    if ec.hasError():
        raise XMLError, "Error parsing %s\n%s" % (uri, ec.getError())

    return ctxt.doc()

def parseHTMLStream(inputStream, baseUri="dummy.html"):
    """Parse an input stream and return a libxml2 document object"""
    # TODO: read chunk by chunk
    return parseHTMLString(inputStream.read(), baseUri)

def parseHTMLString(inputString, baseUri="dummy.html"):
    """Parse an input string and return a libxml2 document object
    """
    if type(inputString) is UnicodeType:
        inputString = inputString.encode("utf8")

    return libxml2.htmlParseDoc(inputString, baseUri)
    # TODO: parse with context to catch errors
    #ctxt = libxml2.htmlCreatePushParser(None, inputString[:4], 4, baseUri)
    #ec = _ErrorCallback()
    #ctxt.setErrorHandler(ec.callback,None)
    #ctxt.parseChunk(inputString[4:],len(inputString)-4,1)
    #if ec.hasError():
    #    raise XMLError, "Error parsing string\n%s" % (ec.getError(),)
    #return ctxt.doc()


###########################################################################
# helpers for libxslt
#

_stylesheet_cache_lock = threading.Lock()
_stylesheet_cache = {}

def compileStylesheet(uri):
    doc = parseUri(uri, 0)
    style = libxslt.parseStylesheetDoc(doc)
    if style is None:
        raise XMLError, "Error compiling stylesheet %s " \
            "(see server log for details)" % uri
    return style

def _getCachedStylesheet(uri):
    _stylesheet_cache_lock.acquire()
    try:
        try:
            return _stylesheet_cache[uri]
        except KeyError:
            style = compileStylesheet(uri)
            if style is None:
                raise XMLError, "Error compiling stylesheet %s " \
                                "(see server log for details)" % \
                                (uri,)
            _stylesheet_cache[uri] = style
            return style
    finally:
        _stylesheet_cache_lock.release()

def makeparam(s):
    """ Convert a string an xpath expression suitable for use as an xslt parameter """
    if type(s) not in (UnicodeType, StringType):
        return s
    if "'" in s:
        l = s.split("'")
        p = ",\"'\",".join(["'%s'" % i for i in l])
        return "concat(%s)" % p
    else:
        return "'%s'" % s

def applyStylesheet(doc, stylesheetUri, outputStream, params={}):
    """Apply a stylesheet to a document object

    The result is send to a stream.
    """
    # prepare parameters
    myParams = {}
    for key,value in params.items():
        myParams[key] = makeparam(value)
    # parse stylesheet
    style = _getCachedStylesheet(stylesheetUri)
    # apply
    result = None
    try:
        result = style.applyStylesheet(doc, myParams)
        if result is None:
            raise XMLError, "Error applying stylesheet " \
                            "(see server log for details)"
        # save result
        s = style.saveResultToString(result)
        if not s:
            raise XMLError, "Error saving transformation result " \
                            "(see server log for details)"
        outputStream.write(s)
    finally:
        if not result is None:
            result.freeDoc()

def applyStylesheet2(doc, stylesheetUri, params={}):
    """Apply a stylesheet to a document object

    The result is send to a stream.
    """
    # prepare parameters
    myParams = {}
    for key, value in params.items():
        myParams[key] = makeparam(value)
    # parse stylesheet
    style = _getCachedStylesheet(stylesheetUri)
    # apply
    return style.applyStylesheet(doc, myParams)


###########################################################################
# helpers for xml.sax
#

def saxMakeParser(ns=0):
    reader = xml.sax.make_parser() #["drv_libxml2"])
    reader.setFeature(feature_namespaces, ns)
    reader.setFeature(feature_external_ges, 0)
    return reader

def saxParse(filename_or_stream,contentHandler,lexicalHandler=None,ns=0):
    reader = saxMakeParser(ns)
    reader.setContentHandler(contentHandler)
    if lexicalHandler is not None:
        reader.setProperty(property_lexical_handler, lexicalHandler)
    return reader.parse(filename_or_stream)

def saxParseString(s, contentHandler, lexicalHandler=None, ns=0):
    return saxParse(StringIO(s), contentHandler, lexicalHandler, ns)


###########################################################################
# find root element name quickly
#

class StopParse(Exception):
    pass

class _FindRootElementNameHandler(xml.sax.handler.ContentHandler):

    def startElement(self,name,atts):
        self.rootElementName = name
        raise StopParse

    def startElementNS(self,name,qname,atts):
        self.rootElementName = name
        raise StopParse

def findRootElementName(filename_or_stream):
    handler = _FindRootElementNameHandler()
    try:
        reader = saxMakeParser()
        reader.setContentHandler(handler)
        reader.parse(filename_or_stream)
        raise RuntimeError, "Root element not found!?"
    except StopParse:
        return handler.rootElementName

def findRootElementNameNS(filename_or_stream):
    handler = _FindRootElementNameHandler()
    try:
        reader = saxMakeParser(1)
        reader.setContentHandler(handler)
        reader.parse(filename_or_stream)
        raise RuntimeError, "Root element not found!?"
    except StopParse:
        return handler.rootElementName


###########################################################################
# quick tests
#

if __name__ == "__main__":
    doc = parseHTMLString(open("sbi.html").read())
    #print repr(doc.serialize())
    doc.freeDoc()
    try:
        doc = parseString("<test><x></test>",0)
    except XMLError, e:
        assert str(e) == "Error parsing string\n" \
                         "Opening and ending tag mismatch: x line 0 and test\n"
    #print repr(doc.serialize())
    doc.freeDoc()
    doc = parseUri("test.xml",0)
    doc.freeDoc()

