# -*- coding: UTF-8 -*-


# Expression
# By: Frederic Peters <fpeters@entrouvert.com>
#     Emmanuel Raviart <eraviart@entrouvert.com>
#
# Copyright (C) 2004 Entr'ouvert, Frederic Peters & Emmanuel Raviart
#
# This program 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.
#
# This program 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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.


"""Descriptions Module

A description contains all informations, methods, etc, needed to properly
layout a XML data holder.
"""


import libxml2
import libxslt

import expression.core.dataholders as dataholders
import expression.core.elements as elements
import expression.core.environs as environs
import expression.core.filesystems as filesystems
import expression.core.html as html
import expression.core.logs as logs
import expression.core.namespaces as namespaces
import expression.core.stations as stations
import expression.modules.parsers.spip as spip
import xforms


_descriptionHolders = {}


class ActionButton(xforms.Control):
    """ An action button submits data, triggers an action and/or redirects to a page.

    Usage:

    <yep:actionButton
        nextStation="next station relative uri (see Page.sendHttpAnswer)"
        saveData="true or false, default true"
        pythonClass="an ActionButton descendant class, that overrides methods isRelevant, onSubmit, checkData, onSave and/or onHttpAnswer"
    >
        <yep:action>an action name, unique within the page</yep:action>
        <xforms:label>a label</xforms:label>
    </yep:actionButton>
    """
    fieldValue = None
    htmlControlClass = "yep-action-button"

    def checkData(self, context):
        """ Hook for submitted specimen data check.
            Called after data was submitted (context.specimen was updated).
            Use self.getModelContext(context).setErrorLabel(...) to trigger alert messages and prevent the specimen from being saved.
            This method is not called when saveData is "false".
        """
        pass

    def generateHtmlContextControlCoreForm(self, context, layout):
        """ Generates a standard HTML button.
        """
        label = self.getLabel()
        action = self.getAction()
        if action is not None and label is not None:
            button = html.button(
                id = action,
                name = "_action",
                type = "submit",
                value = action
            )
            if label.generateHtmlContextMessage(context, button):
                layout.append(button)
                return True
        else:
            logs.debug("Missing action or label for actionButton %s" % self)

    def generateHtmlContextControlCoreStatic(self, context, layout):
        """ Generates an HTML clickable anchor.
        """
        label = self.getLabel()
        action = self.getAction()
        if action is not None and label is not None:
            a = html.a(
                href = context.specimen.getDocument().getActionUri(action)
            )
            if label.generateHtmlContextMessage(context, a):
                layout.append(a)
                return True
        else:
            logs.debug("Missing action or label for actionButton %s" % self)

    def generateHtmlContextControlCoreHidden(self, context, layout):
        return False

    def generateHtmlContextControlLabel(self, context, layout):
        return False

    def getAction(self):
        """ Returns the contents of the action child element.
        """
        for node in self.evaluateXpath("yep:action"):
            return node.content

    def getComputedId(self, context, modelContext):
        return "\\_action_%s" % self.getAction()

    def getNextStation(self):
        """ Returns the value of the nextStation attribute
        """
        return self.node.prop("nextStation")

    def getSaveData(self):
        """ Returns whether the saveData attribute is not "false"
        """
        return self.node.prop("saveData") != "false"

    #~ def isRelevant(self, context, modelContext):
        #~ action = self.action
        #~ if not context.walk([action, 'exists']) or not context.walk([action, 'isAuthorized']):
            #~ return False
        #~ return xforms.Control.isRelevant(self, context, modelContext)

    def onHttpAnswer(self, context):
        """ Hook for actions that should take place just before the http answer is sent out.
            Unless saveData is set to "false", context.specimen was updated and saved to disk.
            You can act upon this HTTP answer by setting context.specimen.nextUri
        """
        pass

    def onSave(self, context):
        """ Hook for actions that should take place after submitted data has been processed and before submitted data is saved.
            This hook is not called when saveData is "false".
        """
        pass

    def onSubmit(self, context):
        """ Hook for actions that should take place before submitted data is processed.
            This hook is not called when saveData is "false".
        """
        pass

    def submitContext(self, context):
        pass


class Component(xforms.Control):
    def generateHtmlContextControl(self, context, layout):
        filled = False
        childNode = self.node.children
        while childNode is not None:
            if childNode.type != "element":
                layout.append(childNode.copyNode(True))
                filled = True
                childNode = childNode.next
                continue
            element = self.newElement(childNode)
            elementContext = element.newContext(context.specimen, previous = context)
            if elementContext.generateXml(layout):
                filled = True
            childNode = childNode.next
        return filled

    def submitContext(self, context):
        childNode = self.node.children
        while childNode is not None:
            if childNode.type != "element":
                childNode = childNode.next
                continue
            element = self.newElement(childNode)
            elementContext = element.newContext(context.specimen, previous = context)
            elementContext.submit()
            childNode = childNode.next


class ComponentWidget(xforms.Control):
    htmlControlClass = "xforms-component"

    def generateHtmlContextControlCore(self, context, layout):
        dataComponent = self.getDataComponent(context)
        if dataComponent:
            return dataComponent.prototype.generateHtmlContextControl(dataComponent, layout)
        return False

    def generateHtmlContextControlCoreForm(self, context, layout):
        return self.generateHtmlContextControlCore(context, layout)

    def generateHtmlContextControlCoreHidden(self, context, layout):
        return self.generateHtmlContextControlCore(context, layout)

    def generateHtmlContextControlCoreReadOnly(self, context, layout):
        return self.generateHtmlContextControlCore(context, layout)

    def generateHtmlContextControlCoreStatic(self, context, layout):
        return self.generateHtmlContextControlCore(context, layout)

    def getDataComponent(self, context):
        modelContext = self.getModelContext(context)
        dataElement = self.getInstanceData(context, modelContext)
        if dataElement is None:
            logs.debug("No data for component %s" % self)
            return
        componentNodes = self.evaluateXpath("@component")
        if componentNodes:
            componentName = componentNodes[0].content
        else:
            componentName = None
        srcNodes = dataElement.evaluateXpath("@src")
        if srcNodes:
            dataElementLocation = srcNodes[0].content
            try:
                dataElementHolder = self.walkToLocation(dataElementLocation)
            except faults.PathNotFound, fault:
                logs.debug(str(fault))
                return
            else:
                dataElement = dataElementHolder.getRootElement()
        dataDescription = dataElement.getDescription()
        if dataDescription is None:
            logs.debug("Missing description for %s" % dataElement)
            return
        dataComponent = dataDescription.getComponent(componentName)
        if dataComponent is None:
            logs.debug("Missing component for %s" % dataElement)
            return
        dataComponent.formCreationNeeded = context.formCreationNeeded
        dataComponent.inForm = context.inForm
        dataComponentPrototype = dataComponent.prototype
        dataComponentModelContext = dataComponentPrototype.getModelContext(dataComponent)
        dataComponentModelContext.embedderContext = context
        dataComponentModelContext.embedder = self
        return dataComponent

    def getHtmlControlGroupingElementClass(self):
        return html.div

    def submitContext(self, context):
        dataComponent = self.getDataComponent(context)
        if dataComponent:
            dataComponent.prototype.submitContext(dataComponent)

    htmlControlGroupingElementClass = property(getHtmlControlGroupingElementClass)


class Custom(xforms.Control):
    htmlControlClass = "yep-custom"

    def generateXmlContext(self, context, layout):
        return getattr(context, self.functionName)(self, layout)

    def getFunctionName(self):
        nodes = self.evaluateXpath('@function')
        if nodes:
            return nodes[0].content
        else:
            return None

    functionName = property(getFunctionName)


class Page(xforms.Control):
    __traceFunctions = []
    def generateXmlContext(self, context, layout):
        oldFormCreationNeeded = context.formCreationNeeded
        context.formCreationNeeded = self.instanceLayoutRequiresForm()
        bodyLayout = html.body()
        filled = super(Page, self).generateXmlContext(context, bodyLayout)
        if filled:
            layout.append(html.html(bodyLayout))
        context.formCreationNeeded = oldFormCreationNeeded
        return filled

    def generateHtmlContextControl(self, context, layout):
        filled = self.generateHtmlContextPageControlBody(context, layout)
        # Layout page buttons bar.
        buttonsBarLayout = html.div(class_ = "buttons-bar")
        actionButtonsBarLayout = html.span(class_ = "action-buttons-bar")
        buttonsBarLayout.append(actionButtonsBarLayout)
        actionButtonsBarNodes = self.evaluateXpath("yep:actionButtonsBar")
        buttonsBarFilled = False
        if actionButtonsBarNodes:
            childNode = actionButtonsBarNodes[0].children
            while childNode is not None:
                if childNode.type != "element":
                    buttonsBarLayout.append(childNode.copyNode(True))
                    buttonsBarFilled = True
                    childNode = childNode.next
                    continue
                element = self.newElement(childNode)
                elementContext = element.newContext(context.specimen, previous = context)
                if elementContext.generateXml(buttonsBarLayout):
                    buttonsBarFilled = True
                childNode = childNode.next
        if buttonsBarFilled:
            layout.append(buttonsBarLayout)
            filled = True
        return filled

    def generateHtmlContextPageControlBody(self, context, layout):
        """Layout page without buttons bar."""

        filled = False
        bodyNodes = self.evaluateXpath("yep:body")
        if bodyNodes:
            childNode = bodyNodes[0].children
            while childNode is not None:
                if childNode.type != "element":
                    layout.append(childNode.copyNode(True))
                    filled = True
                    childNode = childNode.next
                    continue
                element = self.newElement(childNode)
                elementContext = element.newContext(context.specimen, previous = context)
                if elementContext.generateXml(layout):
                    filled = True
                childNode = childNode.next
            if self.generateHtmlContextControlAlert(context, layout):
                filled = True
        return filled

    def getActivatedActionButton(self, context):
        """ Returns the ActionButton that was clicked on. If none, fallback on the first relevant button.
        """
        action = environs.getVar("submission").getField("_action")
        xpath = ".//yep:actionButton" # FIXME: this does not find buttons in components
        if action:
            xpath += "[yep:action='%s']" % action
        for node in self.evaluateXpath(xpath):
            button = self.newElement(node)
            modelContext = self.getModelContext(context)
            if button.isRelevant(context, modelContext): # security check
                return button

    def getComputedId(self, context, modelContext):
        return "\\PAGE"

    def getBind(self, context, modelContext):
        return None

    def getChildInstanceDataXpath(self, context, modelContext):
        return ""

    def getModelId(self):
        return None

    def getName(self):
        nameNodes = self.evaluateXpath("@name")
        if not nameNodes:
            return None
        return nameNodes[0].content

    def instanceLayoutRequiresForm(self):
        pageTypeNodes = self.evaluateXpath("@type")
        if pageTypeNodes:
            pageType = pageTypeNodes[0].content
        else:
            pageType = None
        return pageType == "form"

    def registerTraceFunction(cls, function):
        """ Make the Page class know about a trace function to notify on each modification.
            The trace function should take as single argument the page context.
        """
        cls.__traceFunctions.append(function)
    registerTraceFunction = classmethod(registerTraceFunction)

    def newContext(self, specimen, *attributes, **keywords):
        return PageContext(self, specimen, *attributes, **keywords)

    def saveSubmittedData(self, context):
        """ saves the submitted data in its (possibly new) document

            Calls the specimen's prepareForSaving() method before.
        """
        specimen = context.specimen
        specimen.prepareForSaving()
        specimenDocument = specimen.getDocument()
        if specimenDocument.isTemporary:
            specimenDocument.setPermanentLocalId()
        specimenDocument.save()

    def sendHttpAnswer(self, context):
        """ Sends the next page or redirects to another URL.

            To determine the page URL, this method tries
                - context.specimen.nextUrl, then
                - action button attribute "nextStation", then
                - page attribute "next" (example: <yep:page name="new" type="form" next="edit">)
            When none is set, it returns the specimen's default URI.
        """
        specimen = context.specimen
        nextUrl = "nextUrl" in specimen.__dict__ and specimen.nextUrl\
            or context.button and context.button.getNextStation()\
            or self.node.prop("next")\
            or specimen.getUri()
        try:
            station = specimen.walkToLocation(nextUrl)
        except:
            environs.getVar("httpRequestHandler").outputRedirect(nextUrl, station = specimen)
        else:
            station.callHttpFunction(station.doHttpGet)

    def submitContext(self, context):
        bodyNodes = self.evaluateXpath("yep:body")
        if not bodyNodes:
            logs.info('Missing body description for page named "%s".' % self.localId)
            return
        childNode = bodyNodes[0].children
        while childNode is not None:
            if childNode.type != "element":
                childNode = childNode.next
                continue
            element = self.newElement(childNode)
            elementContext = element.newContext(context.specimen, previous = context)
            elementContext.submit()
            childNode = childNode.next

    def submitValidateAndProcessContext(self, context):
        """ Called on HTTP Post. Handles posted data and sends an HTTP answer.
        """
        button = self.getActivatedActionButton(context)
        context.button = button
        if button:
            if button.getSaveData():
                context.button.onSubmit(context)
                self.submitContext(context)
                button.checkData(context)
                if context.xformsDefaultModel.errors:
                    return context.callHttpFunction(context.doHttpGet)
                button.onSave(context)
                self.saveSubmittedData(context)
            button.onHttpAnswer(context)
            self.sendHttpAnswer(context)
        else:
            self.submitContext(context)
            if context.xformsDefaultModel.errors:
                return context.callHttpFunction(context.doHttpGet)
            self.saveSubmittedData(context)
            self.sendHttpAnswer(context)
        for f in self.__class__.__traceFunctions:
            f(context)

    modelId = property(getModelId)
    name = property(getName)


class PageContext(xforms.WidgetElementContext):
    def __repr__(self):
        return "<%s %s of %s>" % (
            self.__class__.__name__, self.prototype.name, self.specimen.node.name)

    def doHttpPost(self):
        return self.prototype.submitValidateAndProcessContext(self)

    def isAccessAuthorized(self, command):
        return self.specimen.isContentAccessAuthorized(self.prototype.name, command)

##     def outputHttpSource(self):
##         self.specimen.outputHttpSource()


class Description(Page):
    _components = None
    _emailMessages = None
    _pages = None

    def buildXformsContext(self, context):
        modelNodes = self.evaluateXpath("xforms:model")
        for modelNode in modelNodes:
            model = self.newElement(modelNode)
            model.buildXformsContext(context)
        context.xformsDefaultModel.defaultInstance = xforms.DummyInstance(context.specimen)

    def getComponent(self, componentName, componentUriPathFragment = None):
        if self._components is None or componentName not in self._components:
            if componentName:
                componentNodes = self.evaluateXpath(
                    "yep:component[@name = '%s']" % componentName.replace("'", "&apos;"))
                if not componentNodes:
                    logs.debug(
                        "Component named %s not found in description %s. Using default component."
                        % (componentName, self))
            else:
                componentNodes = None
            if not componentNodes:
                componentNodes = self.evaluateXpath("yep:component")
            if not componentNodes:
                logs.debug("No component found in description %s." % self)
                return None
            componentNode = componentNodes[0]
            if self._components is None:
                self._components = {}
            self._components[componentName] = elements.newElement(
                componentNode, previous = self, uriPathFragment = componentUriPathFragment,
                owner = self)
        return self._components[componentName]

    def getEmailMessageNames(self):
        nodes = self.evaluateXpath("mail:message/@name")
        return [node.content for node in nodes]

    def getEmailMessage(self, name):
        if self._emailMessages is None or name not in self._emailMessages:
            nodes = self.evaluateXpath("mail:message[@name = '%s']" % name.replace("'", "&apos;"))
            if not nodes:
                logs.info(
                    """Missing description for email message named "%s"."""
                        % name)
                return None
            emailMessageNode = nodes[0]
            if self._emailMessages is None:
                self._emailMessages = {}
            self._emailMessages[name] = elements.newElement(
                emailMessageNode, previous = self, uriPathFragment = name,
                owner = self)
        return self._emailMessages[name]

    def getPage(self, pageName, pageUriPathFragment = None):
        if self._pages is None or pageName not in self._pages:
            pageNode = self.getPageNode(pageName)
            if pageNode is None:
                return None
            if self._pages is None:
                self._pages = {}
            self._pages[pageName] = elements.newElement(
                pageNode, previous = self,
                uriPathFragment = pageUriPathFragment, owner = self)
        return self._pages[pageName]

    def getPageNames(self):
        pageNameNodes = self.evaluateXpath('yep:page/@name')
        return [pageNameNode.content for pageNameNode in pageNameNodes]

    def getPageNode(self, pageName):
        pageNodes = self.evaluateXpath('yep:page[@name = "%s"]' % pageName.replace('"', "&quot;"))
        if not pageNodes:
            logs.info(
                """Missing description for page named "%s".""" % pageName)
            return None
        return pageNodes[0]

    def newContext(self, specimen, *attributes, **keywords):
        return DescriptionContext(self, specimen, *attributes, **keywords)

    emailMessageNames = property(getEmailMessageNames)
    pageNames = property(getPageNames)


class DescriptionContext(html.HtmlContextMixin, PageContext):
    _components = None
    _emailMessages = None
    _pages = None

    def __init__(self, prototype, specimen, previous = None,
                 uriPathFragment = None):
        super(DescriptionContext, self).__init__(
            prototype, specimen, previous = previous,
            uriPathFragment = uriPathFragment)
        self.prototype.buildXformsContext(self)

    def doHttpGet(self):
        specimenDocument = self.specimen.getDocument()
        if specimenDocument.isTemporary:
            page = self.getPage("new")
            if page is not None:
                return page.checkAccessAndWalk(
                    [], environs.getVar("httpCommand"), environs.getVar("instruction"))
        return super(DescriptionContext, self).doHttpGet()

    def doHttpPost(self):
        specimenDocument = self.specimen.getDocument()
        if specimenDocument.isTemporary:
            page = self.getPage("new")
            if page is not None:
                return page.checkAccessAndWalk(
                    [], environs.getVar("httpCommand"), environs.getVar("instruction"))
        return super(DescriptionContext, self).doHttpPost()

    def getComponent(self, componentName, componentUriPathFragment = None):
        if self._components is None or componentName not in self._components:
            componentPrototype = self.prototype.getComponent(
                componentName, componentUriPathFragment)
            if componentPrototype is None:
                return None
            if self._components is None:
                self._components = {}
            self._components[componentName] = componentPrototype.newContext(
                self.specimen, previous = self, uriPathFragment = componentUriPathFragment)
        return self._components[componentName]

    def getEmailMessage(self, name):
        if self._emailMessages is None or name not in self._emailMessages:
            emailMessagePrototype = self.prototype.getEmailMessage(name)
            if emailMessagePrototype is None:
                return None
            if self._emailMessages is None:
                self._emailMessages = {}
            import expression.modules.emails as emails
            self._emailMessages[name] = emails.MessageContext(
                emailMessagePrototype, None, previous = self,
                uriPathFragment = name)
        return self._emailMessages[name]

    def getPage(self, pageName, pageUriPathFragment = None):
        if self._pages is None or pageName not in self._pages:
            pagePrototype = self.prototype.getPage(pageName, pageUriPathFragment)
            if pagePrototype is None:
                return None
            if self._pages is None:
                self._pages = {}
            self._pages[pageName] = pagePrototype.newContext(
                self.specimen, previous = self, uriPathFragment = pageUriPathFragment)
        return self._pages[pageName]

    def isAccessAuthorized(self, command):
        specimenDocument = self.specimen.getDocument()
        if specimenDocument.isTemporary:
            page = self.getPage("new")
            if page is not None:
                return page.isAccessAuthorized(command)
        return self.specimen.isAccessAuthorized(command)

    def walkToItem(self, uriPathFragments, command = None, instruction = None):
        itemName = uriPathFragments[0]
        emailMessageNames = self.prototype.emailMessageNames
        if itemName in emailMessageNames:
            emailMessage = self.getEmailMessage(itemName)
            return emailMessage, uriPathFragments[1:]
        pageUriPathFragment = itemName
        if itemName in self.prototype.pageNames:
            remainingUriPathFragments = uriPathFragments[1:]
            page = self.getPage(itemName, pageUriPathFragment)
            return page, remainingUriPathFragments
        return self, uriPathFragments


class Spip(xforms.TextArea):
    htmlControlClass = "yep-spip"

    def generateHtmlContextControlCoreStatic(self, context, layout):
        filled = False
        content = self.node.content
        if content:
            htmlText = "<spip>%s</spip>" % spip.makeHtmlFromSpip(content)
            doc = libxml2.readDoc(
                htmlText, None, None, libxml2.XML_PARSE_DTDLOAD | libxml2.XML_PARSE_NONET)
            node = doc.getRootElement().children
            while node is not None:
                layout.append(node)
                node = node.next
            filled = True
        return filled


def getDescriptionHolder(descriptionAbsolutePath):
    """ Caches and returns the description holder at descriptionAbsolutePath.
    """
    if descriptionAbsolutePath not in _descriptionHolders:
        _descriptionHolders[descriptionAbsolutePath] = dataholders.DataHolder(
            pathFragment = descriptionAbsolutePath,
            mimeType = "text/xml",
            isRootElder = True,
            containedFileSystem = filesystems.PartialFileSystem(descriptionAbsolutePath)
        )
    return _descriptionHolders[descriptionAbsolutePath]


elements.registerElement(namespaces.yep.uri, "actionButton", ActionButton)
elements.registerElement(namespaces.yep.uri, "component", Component)
elements.registerElement(namespaces.yep.uri, "componentWidget", ComponentWidget)
elements.registerElement(namespaces.yep.uri, "custom", Custom)
elements.registerElement(namespaces.yep.uri, "description", Description)
elements.registerElement(namespaces.yep.uri, "page", Page)
elements.registerElement(namespaces.yep.uri, "spip", Spip)
