#questionnaire.py - the top level one
import os
from rlextra.forms.qbase import QContainer
from rlextra.forms import qparser
from rlextra.forms.qbase import QSection, QTextField, QIntegerField, \
     QFloatField, QDateField, QBooleanField, QChoice, QRepeatedGroup, QText, \
     QHeader, QSeparator
from rlextra.forms.qwidget import DynamicQWidget

class Questionnaire(QContainer):
    "Representation of top level questionnaire"

    class SectionNotFoundError(ValueError): pass

    class AllSections: pass

    qBaseTypeClassMap = {
        'displaySection': QSection,
        'namedSection': QSection,
        'textField': QTextField,
        'integerField': QIntegerField,
        'floatField': QFloatField,
        'dateField': QDateField,
        'booleanField': QBooleanField,
        'choice': QChoice,
        'repeatedGroup': QRepeatedGroup,
        'text': QText,
        'header': QHeader,
        'separator': QSeparator,
        'dynamicWidget': DynamicQWidget,
        }

    qBaseNameClassMap = {}

    def __init__(self, filename=None,dtdDir=None, prefix='', overrideName=None,eoCB=None):
        self.name = None
        self.prefix = prefix
        self.label = None
        self.children = []
        self.hidden = 0
        self._filename = filename
        self._parent = None
        # the parse routine actually makes a QBase instance, but we'll
        # grab its content and bin it so we can allow a natural
        # constructor
        if filename:
            q2 = qparser.parseQdfFile(
                filename, self,
                dtdDir=dtdDir, prefix=prefix, overrideName=overrideName, eoCB=eoCB)
            self.name = q2.name
            self.children = q2.children

    def getFormCode(self):
        return os.path.splitext(os.path.basename(self._filename))[0]

    def createQbase(self, prefix, tagName, sectionName, defaultClass):
        klass = self.qBaseTypeClassMap.get(tagName, defaultClass)
        klass = self.qBaseNameClassMap.get(sectionName, klass)
        obj = klass()
        obj.prefix = prefix
        klass._parent = self
        return obj

    def getDictionary(self):
        "This will be at the top level"
        d = {}
        self.populateDict(d, 0)
        return d

    def getTextDictionary(self):
        "Return a dict where everything is a string"
        d = {}
        self.populateDict(d, 1)
        return d

    def populateDict(self, dict, asText):
        "Just ask child nodes...."
        for child in self.children:
            if isinstance(child, Questionnaire):
                for grandchild in child.children:
                    grandchild.populateDict(dict, asText)
            else:
                child.populateDict(dict, asText)

    def setTextDictionary(self, dict, topLevel=1):
        """Set all element values from the dictionary.

        This is tricky for repeated groups as it may need
        to create on the fly.  Therefore instead of using
        Python's dynamic evaluation at the top we step down

        .setTextDictionary() is called on QSection objects in the same order as
        the sections appear in the QDF file.

        XXX Sub-questionnaire sections are not sorted.

        """
        if topLevel == 1:
            stuff = self.deFixDict(dict, [])
            myStuff = stuff.get(self.name, None)
        else:
            myStuff = dict
        # the dictionary may have nothing of relevance
        # and no keys matchng my name.  e.g. first
        # submission of a web one.  If so, we're done
        if myStuff is not None:
            stuff2 = self.deFixDict(myStuff)
            items = _itemsSortedByQBaseIndex(stuff2, self.getSectionNames())
            for key, smallerDict in items:
                if key is None:
                    continue
                child = getattr(self, key, None)
                if child is not None:
                    if isinstance(child, Questionnaire):
                        child.setTextDictionary(smallerDict, topLevel = 0)
                    else:
                        child.setTextDictionary(smallerDict)

    def _fixup(self):
        """Hack QDF tree.

        This hook is called on each QBase after entire QDF tree is loaded and
        populated.

        XXX

        This is called from all over the place: needs sorting out so that
        this is always called.  Might also be nice to have a method that gets
        called, after .fixup(), iff the section has already been completed.
        For example, this avoids need for the if statement in
        IfaFormsProductDetailsSection.fixup() to decide whether
        self._appendInvestments should be called.

        """
        self.fixup()
        for child in self.getChildrenRecursively():
            child.fixup()

    def sectionNameFromNameOrNumber(self, section):
        sectionNames = self.getSectionNames()
        try:
            secId = int(section)
        except:
            try:
                secId = sectionNames.index(section)
            except ValueError:
                # XXX replace with .as_html() method on exc., or something
                raise Questionnaire.self.SectionNotFoundError(
                    '<p>section name: %s'
                    '<p>list of sections: %s' %
                    (section, sectionNames))
        else:
            #it was an integer, make it a name
            section = sectionNames[secId]
        return section

    def makeNextHtmlForm(
        self, scriptUrl, finalAction, qdf,
        submittedSection=None, section=None,

        hiddenFields={},
        finalPostText = None,
        finalAskNumberOfCopies=0,
        askNumberOfCopiesBarText = 'Select Number of Copies',
        askNumberOfCopiesEntryLabel = 'How many copies?',
        numberOfCopiesVar = 'numberOfCopies',
        target=None,
        lhbcell='',
        ):
        """
        # XXX currently has to return next section as well as HTML form,
        # because webeforms needs it

        submittedSection: section whose response has just been received
          None means no section has just been received

        section: specific request for a particular section.  Takes
          precedence over submittedSection.
          Questionnaire.AllSections means all sections at once

        """
        hiddenFields = hiddenFields.copy()
        hiddenFields['qdf'] = qdf
        hiddenFields['finalAction'] = finalAction
        self._submittedSection = submittedSection

        self._fixup()
        if section is Questionnaire.AllSections:
            # show the whole thing in one go
            header = 'Questionnaire %s' % qdf
            submitCaption='Next'
            section = None
            hiddenFields['submitting'] = '1'
        else:
            # The HTML returned on submission depends on the current
            # section (which is explicitly passed in as the hidden
            # parameter 'section').
            # The next action is to call is always showQuestionnaire,
            # even on submission of the last section, so that any
            # standard Questionnaire hooks are called.  XXX but a
            # Questionnaire object is created by every action in
            # webeforms, so fixup needs calling on every action
            # anyway!

            if section is not None:
                section = self.sectionNameFromNameOrNumber(section)
            else:
                if submittedSection is not None:
                    section = self.sectionNameFromNameOrNumber(submittedSection)
                section = self.getSectionNameToReturn(section)

            hiddenFields['submittedSection'] = section
            header = 'Questionnaire %s - section %s' % (qdf, section)
            finalSection = self.getVisibleSectionNames()[-1]
            hiddenFields['action'] = 'showQuestionnaire'
            if section == finalSection:
                submitCaption='Submit'
                hiddenFields['submitting'] = '1'
            else:
                submitCaption='Next'

        isFinal = (section == finalSection)
        askNumberOfCopies = (finalAskNumberOfCopies and isFinal)
        if not isFinal:
            postText = None
        else:
            postText = finalPostText

        return section, self.makeHtmlForm(
            scriptUrl,
            submitCaption,
            hiddenFields,
            section,
            postText,
            askNumberOfCopies,
            askNumberOfCopiesBarText,
            askNumberOfCopiesEntryLabel,
            numberOfCopiesVar,
            target,
            lhbcell,
            )

    def makeHtmlForm(self,
                     url,
                     submitCaption='submit',
                     hiddenFields={},
                     section=None,
                     postText = None,
                     askNumberOfCopies=0,
                     askNumberOfCopiesBarText = 'Select Number of Copies',
                     askNumberOfCopiesEntryLabel = 'How many copies?',
                     numberOfCopiesVar = 'numberOfCopies',
                     target=None,
                     lhbcell='',
                     ):
        """Returns an HTML chunk as a form.

        The url is the one to submit the form to. Any hidden fields
        will be added as, er, hidden fields.  section will
        display just a single section; this may either be
        an integer index, or a name.  If not supplied you see
        all sections.
        """
        #find the section they wanted
        if section is None:
            sectionWanted = None
        elif section == -1: #yuk
            sectionWanted = -1 # some constant not likely to be an attribute name
        elif type(section) == type(1):
            sectionWanted = self.children[section]
        elif type(section) == type(''):
            sectionWanted = self.findElementByName(section)

        class Visitor:
            getCodes = 'h','i','l','v','u','s'
            def __init__(self):
                for a in self.getCodes:
                    setattr(self,a+'Text',[])

            def __call__(self,obj):
                null = lambda : ''
                for a in self.getCodes:
                    v = getattr(obj,'get'+a.upper()+'Text',null)()
                    if v: getattr(self,a+'Text').append(v)

            def getHText(self):
                return '\n'.join(self.hText)

            def getIText(self):
                return '\n'.join(self.iText)

            def getLText(self):
                return ';'.join(self.lText+[''])

            def getUText(self):
                return ';'.join(self.uText+[''])

            def getVText(self):
                return '+"\\n"+'.join(self.vText)

            def getSText(self):
                text = []
                for condition in self.sText:
                    text.append('if (!%s) {return false;}' % condition)
                text.append('return true;')
                return '\n'.join(text)

        JSV = self.JSV = Visitor()
        self.visit(JSV,section=section)
        html = []
        a = html.append
        a('<div align="right">')
        target = target and (' target="%s"' % target) or ''
        validate = JSV.vText and ' onsubmit="return _RL_VALIDATE();"' or ''
        a('  <form method="post" name="%s" action="%s"%s%s>' % (self.name, url, target, validate))
        for (name, value) in hiddenFields.items():
            a('    <input type="hidden" name="%s" value="%s">' % (name, value))
        a('    <table border="0" width="100%"><tr><td>')
        a('        <colgroup><col width="30%"/><col width="20%"/><col width="50%"/></colgroup>')
        if sectionWanted is None:
            #show the lot
            for elem in self.children:
                a(elem.makeHtml())
        else:
            for elem in self.children:
                if hasattr(elem, 'makeHtmlForm'):
                    # it's a subform, step in...this needs to be recursive
                    # but won't be today
                    for elem2 in elem.children:
                        if elem2 == sectionWanted:
                            a(elem2.makeHtml())
                        else:
                            a(elem2.makeHiddenField())
                else:
                    if elem == sectionWanted:
                        a(elem.makeHtml())
                    else:
                        a(elem.makeHiddenField())

            if postText: a(postText)
        if askNumberOfCopies:
            last = '''<tr><td colspan="3" height="19" class="TblRow1"><span class="TblHead7">%(askNumberOfCopiesBarText)s</span></td></tr>
<tr><td>%(askNumberOfCopiesEntryLabel)s</td><td><input type="text" name="%(numberOfCopiesVar)s" value="1"
onChange="if(/^(1|0|)\d$|20$|^\s*$/.exec(this.value)==null){alert('Please enter a number between 1 and 20');return false};" /></td>
<td></td></tr>'''
        else:
            last = '<input type="hidden" name="%(numberOfCopiesVar)s" value="1"><tr><td></td><td></td><td></td></tr>'
        a(last % locals())
        a('    </td></tr></table>')
        a('    <hr>')
        submit = JSV.sText and ' onclick="return _RL_SUBMIT();"' or ''
        a('    <table width="100%%"><tr><td align="left">%s</td><td align="right"><input type="submit" name="submit"%s value="%s"></td></tr></table>' % (lhbcell,submit,submitCaption))
        a('  </form>')
        a('</div>')
        #remove any empty lines
        html2 = []
        for line in html:
            if line.strip() <> '':
                html2.append(line)
        return '\n'.join(html2)

    def visit(self,visitor,section=None):
        if getattr(self,'hidden'): return
        #find the section they wanted
        if section is None:
            sectionWanted = None
        elif section == -1: #yuk
            sectionWanted = -1 # some constant not likely to be an attribute name
        elif type(section) == type(1):
            sectionWanted = self.children[section]
        elif type(section) == type(''):
            sectionWanted = self.findElementByName(section)

        if sectionWanted is None:
            for c in self.children:
                c.visit(visitor)
        else:
            for c in self.children:
                if hasattr(c, 'makeHtmlForm'):
                    for c2 in c.children:
                        if c2 == sectionWanted:
                            c2.visit(visitor)
                elif c == sectionWanted:
                    c.visit(visitor)

    def getLeafElementNames(self):
        class Visitor:
            def __init__(self):
                self._names = []

            def __call__(self,obj):
                pfx = obj.prefix
                if pfx and pfx[-1]!='.': pfx += '.'
                self._names.append(pfx+obj.name)
        v = Visitor()
        self.visit(v)
        return v._names

    def getSectionNames(self):
        "Return a list of names of top-level sections"
        out = []
        for child in self.children:
            if hasattr(child, 'getSectionNames'):
                #might be another questionnaire
                for subsection in child.getSectionNames():
                    out.append(child.name + '.' + subsection)
            else:
                out.append(child.name)
        return out

    def findElementByName(self, name):
        "Allows dot notation to step into sub-questionnaires"
        words = name.split('.', 1)
        if len(words) == 1:
            return getattr(self, name)
        else:
            child = getattr(self, words[0])
            return child.findElementByName(words[1])

    def getNextSectionName(self, currentSection=None):
        """What's the name of the next one in line?

        Pass in None to get the first one; get back None
        if on the last one.  Pass in a section not there
        and you get a ValueError."""
        names = self.getSectionNames()
        if currentSection is None:
            return names[0]
        else:
            #we get value error if not a section
            pos = names.index(currentSection)
            if pos == len(names) - 1:
                return None
            else:
                return names[pos+1]

    def getVisibleSectionNames(self):
        "Return a list of non-hidden top-level sections"
        out = []
        for child in self.children:
            if hasattr(child, 'getVisibleSectionNames'):
                for subsection in child.getVisibleSectionNames():
                    out.append(child.name + '.' + subsection)
            elif not child.hidden:
                out.append(child.name)
        return out

    def getNextVisibleSectionName(self, currentSection=None):
        """What's the name of the next one in line?

        Pass in None to get the first one; get back None
        if on the last one.  Pass in a section not there
        and you get a ValueError."""
        visibleNames = self.getVisibleSectionNames()
        names = self.getSectionNames()
        if currentSection is None:
            section = visibleNames[0]
        else:
            #we get value error if not a section
            i = names.index(currentSection)

            # XXX assumes no duplicate names
            for section in names[i+1:]:
                if section in visibleNames:
                    break
            else:
                # end of visible sections
                return None

        return section

    def getSectionNameToReturn(self, currentSection=None):
        """Return next section to be returned to client.

        Same interface as .getNextVisibleSectionName().

        The purpose of this method is to allow redirection, within the
        framework (not over HTTP), from one page in the Questionnaire to
        another.  For example, if page 1 is submitted and contains an error, we
        can return page 1 again, with some indication of the error.

        """
        return self.getNextVisibleSectionName(currentSection)

    def getSubmittedSection(self):
        return self._submittedSection


def _itemsSortedByQBaseIndex(httpParamsAsNestedDict, qBaseNames):
    """Return HTTP parameters as nested dict, sorted by given names.

    XXX Sub-questionnaire sections are not sorted.

    """
    h = httpParamsAsNestedDict.copy()
    qBaseIndices = {None: 0}
    for i in range(len(qBaseNames)):
        name = qBaseNames[i]
        qBaseIndices[name] = i+1

    sorterByIndex = []
    for key, value in h.items():
        if qBaseIndices.has_key(key):
            j = qBaseIndices[key]
        else:
            # not an expected child name -- for example, a sub-questionnaire
            i += 1
            j = i
        sorterByIndex.append((j, key, value))

    sorterByIndex.sort()
    items = [(key, value) for i, key, value in sorterByIndex]
    return items
