"""
Base classes and framework for questionnaire objects

"""
import time, copy, re
from reportlab.lib import normalDate

_helpImgHTTPPath=None
def setHelpImgHTTPPath(httpPath):
    global _helpImgHTTPPath
    _helpImgHTTPPath = httpPath

class FieldValidationError(Exception):
    pass

def _deFixDict(dict):
    "Remove first prefix and return branched one"
    newDicts = {}
    newDicts[None] = {}
    for key, value in dict.items():
        chunks = key.split('.', 1)
        if len(chunks) == 1:
            newDicts[None][key] = value
        else:
            head, rest = chunks
            if not newDicts.has_key(head):
                newDicts[head] = {}
            newDicts[head][rest] = value
    return newDicts

def _getHeader(caption):
    return """<tr><td colspan="3" height="19" class="TblRow1"><span class="TblHead7">%s</span></td></tr>\n""" % caption

class QBase:
    def __init__(self):
        self.prefix = ''

    def __getattr__(self, name):
        """Find an appropriate named child.

        This gets called if a lookup fails.  Thus, whatever is asked
        for is not an attribute.  See if it is the name of a child."""
        if self.__dict__.has_key('children'):
            for child in self.children:
                if child.name == name:
                    return child
        #still here?
        raise AttributeError("%s has no attribute or child named '%s'" % (self.__class__.__name__, name))

    def childNames(self):
        qBaseNames = []
        try:
            self.children
        except AttributeError:
            pass
        else:
            for child in self.children:
                try:
                    child.name
                except AttributeError:
                    pass
                else:
                    qBaseNames.append(child.name)
        return qBaseNames

    def deFixDict(self, params, qBaseNames=None):
        """As for _deFixDict(), but:

        Keys are returned for all qBases in the QDF tree, not only those in the
        HTTP dictionary.  For keys not in the HTTP dictionary, empty dictionary
        keys are returned.

        """
        if qBaseNames is None:
            qBaseNames = self.childNames()
        newParams = _deFixDict(params)
        for name in qBaseNames:
            newParams.setdefault(name, {})
        return newParams

    def getChildrenRecursively(self):
        """Return a list of all child QBase instances.

        Includes children, children of children, etc.

        """
        if not hasattr(self, 'children'):
            return []

        children = []
        for child in self.children:
            try:
                child.getChildrenRecursively
            except AttributeError:
                # some joker stuck a non-QBase in the tree
                continue
            children.append(child)
            children.extend([ch for ch in child.getChildrenRecursively()])
        return children

    def getParent(self):
        """Return immediate parent of this object in QDF tree."""
        return self._parent

    def getRoot(self):
        """Return root of the QDF tree (usually a Questionnaire instance)."""
        lastParent = self
        while 1:
            parent = lastParent._parent
            if parent is None:
                break
            lastParent = parent
        return lastParent

    def makeSample(self):
        "Initialize an object with sample data"
        if isinstance(self, QField):
            self.setTextValue(self.sampleData)
        if hasattr(self, 'children'):
            for child in self.children:
                if isinstance(child, QBase):
                    child.makeSample()

    def populateDict(self, dict, asText):
        "Do not modify dict in base class"
        pass

    def setTextDictionary(self, dict):
        """Unserialize QDF tree from HTTP parameters.

        This is called for every object in the QDF tree, regardless of whether
        HTTP parameters for this particular object were supplied.  XXX this may
        not always be true yet, due to use of self.deFixDict(D)[None].items():
        .deFixDict() only .setdefault()s the top-level keys in the dictionary
        it returns

        """
        pass

    def clear(self):
        "Reset all values to just-loaded state"
        pass
    def makeHtml(self):
        template = """
        <tr>
            <td colspan="3">%s</td>
        </tr>
        """
        return template % (self.prefix + '.' + self.name)
    def makeHiddenField(self):
        return '' #fields do stuff, base objects don't

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

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

        """
        pass

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

        This hook is called (by the root Questionnaire) on each QBase after
        entire QDF tree is loaded and populated.

        XXX currently only called for QSections

        """
        pass

    def visit(self,visitor):
        visitor(self)

    def findElementByName(self,name):
        return getattr(self,name,None)

class QSeparator(QBase):
    def __init__(self):
        QBase.__init__(self)
        self.name = ''
        self.htmlClass = ''
        self.html = ''

    def makeHtml(self):
        html = self.html or "<hr>"
        htmlClass= self.htmlClass
        if htmlClass and 'class=' not in html: html.replace('>',' class="%s">'%htmlClass)
        name = self.name
        if name: html = '<span name="%s">%s</span>' % (name,html)
        return '<tr><td colspan="3">%s</td></tr>' % html

class QContainer(QBase):
    "has children - used for Questionnaire and Section"
    def __init__(self):
        QBase.__init__(self)
        self.label = None
        self.name = None
        self.children = []
        self.hidden = 0

    def populateDict(self, dict, asText):
        "Just ask child nodes...."
        for child in self.children:
            child.populateDict(dict, asText)

    def clear(self):
        for child in self.children:
            child.clear()

    def setTextDictionary(self, dict):
        """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"""
        stuff = self.deFixDict(dict)
        # this is things without a prefix like 'age'
        myStuff = stuff[None]
        for key, value in myStuff.items():
            child = getattr(self, key, None)
            if child:
                child.setTextValue(value)

        # these go onto a child node like 'personalDetails.
        del stuff[None]
        for key, smallerDict in stuff.items():
            child = getattr(self, key, None)
            if child:
                child.setTextDictionary(smallerDict)

    def makeHtml(self):
        "Returns an HTML chunk as a form"
        if self.hidden: return ''
        prefix = self.prefix
        html = []
        if not int(getattr(self,'noCaption','0')):
            if self.label is not None:
                caption = self.label
            else:
                caption = prefix
            html.append(_getHeader(caption))
        for elem in self.children:
            html.append(elem.makeHtml())
        return '\n'.join(html)

    def makeHiddenField(self):
        html = []
        for elem in self.children:
            html.append(elem.makeHiddenField())
        return '\n'.join(html)

    def visit(self,visitor):
        for c in self.children:
            c.visit(visitor)

    def findElementByName(self, name):
        words = name.split('.', 1)
        if len(words) == 1:
            return getattr(self, name, None)
        else:
            child = getattr(self, words[0], None)
            if child: child = child.findElementByName(words[1])
            return child

class QSection(QContainer):
    "Represents a display section in a form"
    def __init__(self):
        QContainer.__init__(self)
        self.numbered = 1
        self.alias = None

    def postInit(self):
        #might be set a string value
        self.hidden = int(self.hidden)
        self.numbered = int(self.numbered)

    def getLabel(self):
        "Display to left of field"
        if self.label is None:
            return self.name
        else:
            return self.label

class QField(QBase):
    "Base class for a field"
    def __init__(self):
        QBase.__init__(self)
        self.name = None
        self.editable = 1
        self.value = None
        self.label = None
        self.shortHelp = ''
        self.longHelp = ''
        self.maxlength = ''
        self.regex = None
        self.errorMessage = ''
        self.sampleData = ''
        self.onSubmit = ''
        self.onLoad = ''
        self.onUnload = ''
        self.jsEvents = ''
        self.jsHeader = ''

    def __str__(self):
        "String representation should be field value"
        return self.getTextValue()

    def checkPattern(self, text):
        "Return true or raise error"
        if self.regex is None:
            return 1
        else:
            pat = re.compile(self.regex)
            match = pat.match(text)
            if match:
                # want it to match the whole string,
                # not just the front...
                if match.end() == len(text):
                    return 1
        #still here?  bomb out
        msg = self.getInputErrorMessage()
        raise FieldValidationError(msg)

    def getVText(self):
        self.vText = self.onSubmit
        return self.vText

    def getLText(self):
        self.lText = self.onLoad
        return self.lText

    def getUText(self):
        self.uText = self.onUnload
        return self.uText

    def getIText(self):
        self.iText = self.jsHeader
        return self.iText

    def getInputErrorMessage(self):
        "Return the best error message possible"
        if self.errorMessage:
            msg = self.errorMessage
        else:
            msg = 'invalid input'
        return msg

    def setTextValue(self, newValue):
        "Accept string and do any needed conversions"
        if newValue in ('',None):
            self.value = None
        else:
            self.checkPattern(newValue)
            self.value = newValue

    def clear(self):
        self.value = None

    def setTextDictionary(self, dict):
        if dict.has_key(self.name):
            self.setTextValue(dict[self.name])

    def getTextValue(self):
        if self.value is None:
            v = ''
        else:
            v = str(self.value)
        return v

    def getLabel(self):
        "Display to left of field"
        if self.label is None:
            return self.name
        else:
            return self.label

    def getLongHelp(self):
        "Long Help as tooltip"
        if self.longHelp is not None:
            return self.longHelp
        else:
            return ''

    def getMaxLength(self):
        "use for max no. of chars"
        if self.maxlength is not None:
            return self.maxlength
        else:
            return ''

    def getValidationEventSource(self):
        "Returns the regex handler if needed"
        return (self.regex and '''onChange="if(/%s/.exec(this.value)==null){alert('%s');return false};" ''' % (
                self.regex, self.getInputErrorMessage()) or '') + self.jsEvents

    def _getDotHelpEx(self,tt,sh):
        if tt:
            if _helpImgHTTPPath:
                tt = """<img src="%s" tooltip="%s" ttclass="tooltip">""" % (_helpImgHTTPPath,tt)
            else:
                tt = """<b><span tooltip="%s" ttclass="tooltip">Help</span></b>""" % tt
            sh = sh and ('%s&nbsp;&nbsp;%s' % (tt,sh)) or tt
        return sh

    def _getDotHelp(self):
        return self._getDotHelpEx(self.getLongHelp(), self.shortHelp)

    def makeHtml(self):
        html = []
        h = html.append
        h('<tr><td width="30%%">%s</td>' % self.getLabel())
        h('<td width="40%%">')
        if self.editable:
            h('<input type="text" name="%s.%s" maxlength="%s" value="%s" %s/>' %
              (self.prefix,
               self.name,
               self.getMaxLength(),
               self.getTextValue(),
               self.getValidationEventSource(),
               )
              )
            h('</td>')
            h('<td width="30%%">%s</td>' % self._getDotHelp())
        else:
            h('%s</td>' % self.getTextValue())
            h(self.makeHiddenField())
            h('<td width="40%%"></td>')
        h('</tr>')
        return '\n'.join(html)

    def makeHiddenField(self):
        if self.getTextValue() == '':
            template = ''
        else:
            template = """        <input type="hidden" name="%s" value="%s">""" % (
                self.prefix+'.'+self.name,
                self.getTextValue().replace('&','&amp;')   #AR hack
                )
        return template

    def populateDict(self, dict, asText):
        "Just ask child nodes...."
        if self.value in [None, '']:
            pass
        key = self.prefix+'.'+self.name
        if asText:
            dict[key] = str(self.value)
        else:
            dict[key] = self.value

class QTextField(QField):
    pass



class QIntegerField(QField):
    # This class is untested, so probably broken.
    def __init__(self):
        QField.__init__(self)
        self.regex = r"-?\d+"
        self.errorMessage = 'Please supply a whole number (no decimal point)'
    def setTextValue(self, newValue):
        "Accept string and do any needed conversions"
        self.value = int(newValue)

class QFloatField(QField):
    def getVText(self):
        from xml.sax.saxutils import unescape
        #from htmlentitydefs import entitydefs  # keys are not quite right for our purpose
        entitydefs = {'&pound;': '\xa3'}
        label = unescape(self.label, entitydefs)
        prefix = '%s.%s' % (self.prefix, self.name)
        self.vText = "(isNaN(parseFloat(document.forms[0].elements['%s'].value))?'Please supply an amount for %s':'')" % (
            prefix, label)
        return self.vText

    def setTextValue(self, newValue):
        "Accept string and do any needed conversions"
        self.value = float(newValue)

class QDateField(QField):
    """For now accept UK dates only - any of 31/12/2003,
    31.12.2003, 31-12-2003, 31-12-03"""
    def __init__(self):
        QField.__init__(self)
        #dd/mm/yyyy or dd-mm-yyyy
        self.regex = r"\d\d?(-|\/)\d\d?(-|\/)\d\d\d\d"
        self.errorMessage = 'Please use format dd/mm/yyyy e.g. 31/12/2002'

    def parseDate(self, dateStr):
        dd = mmm = yyyy = None
        for delim in '-/.':
            if len(dateStr.split(delim))==3:
                dd,mmm,yyyy = dateStr.split(delim)
        if dd == None:
            raise ValueError('bad date value "%s", use e.g. "23/01/2003"' % dateStr)
        yyyy = int(yyyy)
        if yyyy < 1000:
            if yyyy < 25:  # pivot date
                yyyy = yyyy + 2000
            else:
                yyyy = yyyy + 1900

        nd = normalDate.NormalDate((yyyy,int(mmm),int(dd)))
        return nd

    def setTextValue(self, newValue):
        if newValue == '':
            self.value = None
        else:
            self.value = self.parseDate(newValue)

    def getTextValue(self):
        if self.value is None:
            return ''
        else:
            return self.value.formatMS('{dd}-{mm}-{yyyy}')

class QBooleanField(QField):
    def __init__(self):
        QField.__init__(self)
        self.spanAllCols = 'no'

    def setTextValue(self, newValue):
        "Accept string and do any needed conversions"
        if newValue in ['on', #web forms do this,
                        '1',
                        'yes']:
            self.value = 1
        else:
            self.value = 0

    def makeHtml(self):
        if self.spanAllCols == 'yes':
            colspan = ' colspan="3"'
            labelTd = ''
            inputLabel = self.getLabel()+'&nbsp;&nbsp;&nbsp;'
        else:
            colspan = ''
            labelTd = '<td>%s</td>' % self.getLabel()
            inputLabel = ''
        xtras = (self.value and ['checked'] or [])+(self.jsEvents and [self.jsEvents] or [])
        return '<tr>%s<td%s>%s<input type="checkbox" name="%s.%s"%s/></td><td>%s</td></tr>' % (
                            labelTd,
                            colspan,
                            inputLabel,
                            self.prefix,
                            self.name,
                            xtras and ''.join(['']+xtras) or '',
                            self._getDotHelp()
                            )

class QText(QField):
    def __init__(self):
        QField.__init__(self)
        self.name = None
        self.label = None
        self.value = None

    def makeHtml(self):
        label = self.getLabel()
        return '<tr><td colspan=3>' + (label or '&nbsp;') + '</td></tr>'

class QHeader(QText):
    def makeHtml(self):
        return _getHeader(self.getLabel() or '&nbsp;')

class QChoice(QField):
    def __init__(self):
        QField.__init__(self)
        self._initialized = 0
        self.name = None
        self.value = None
        self.label = None
        self.spanAllCols = 'no'
        self.shortHelp = ''
        self.options = []
        self.labels = []
        self.style = 'dropdown' #only one for now, also have 'radio'

    def postInit(self):
        #called AFTER options have been set
        self._initialized = 1

    def addChild(self, child):
        label = child.label
        value = child.value
        self.options.append(value)
        if label is None:
            self.labels.append(value)
        else:
            self.labels.append(label)

    def setTextValue(self, newValue):
        #ignore validation if options not set yet...
        if not self._initialized:
            self.value = newValue
        else:
            if newValue in self.options:
                self.value = newValue
            elif newValue == '':  # must allow empty string initializing
                self.value = None
            else:
                raise KeyError('%s not in allowed values %s' % (newValue, self.options))

    def makeHtml(self):
        if not self.editable:
            return self.makeStaticText()
        elif self.style == 'dropdown':
            return self.makeHtmlCombo()
        elif self.style == 'radiobutton':
            return self.makeHtmlRadios()
        else:
            return ''

    def makeStaticText(self):
        html = []
        h = html.append
        h('<tr><td>%s</td>' % self.getLabel())
        h('<td>%s</td>' % self.getTextValue())
        h(self.makeHiddenField())
        h('<td></td></tr>')
        return '\n'.join(html)

    def makeHtmlCombo(self):
        if self.spanAllCols == 'yes':
            colspan = ' colspan="3"'
            labelTd = ''
        else:
            colspan = ''
            labelTd = '<td>%s</td>' % self.getLabel()
        html = ["""<tr>%s<td%s><select name="%s.%s"%s>""" \
                    % (labelTd, colspan, self.prefix, self.name, self.jsEvents and (' '+self.jsEvents) or '')]
        v = self.value
        options = self.options
        j = v not in options and -1 or options.index(v)
        for i in xrange(len(options)):
            html.append('    <option value="%s"%s>%s</option>' %
                        (options[i], j==i and " selected" or "", self.labels[i])
                        )
        html.append("""</select></td><td>%s</td></tr>\n"""%self._getDotHelp())
        return '\n'.join(html)

    def makeHtmlRadios(self):
        helpText = self._getDotHelp()
        maxRadiosPerRow = 2
        if not helpText:
            colspan = ' colspan="2"'
        else:
            colspan = ''
        if self.spanAllCols == 'yes':
            colspan = ' colspan="3"'
            maxRadiosPerRow = 3
            labelTd = ''
        else:
            labelTd = '<td>%s</td>' % self.getLabel()
        html = ['<tr>%s<td%s><p>' % (labelTd, colspan)]
        v = self.value
        options = self.options
        if v not in options:
            j = 0
        else:
            j = options.index(v)
        jsEvents = self.jsEvents and (' '+self.jsEvents) or ''
        for i in xrange(len(options)):
            selected = j==i and " checked" or ""
            if len(options) <= maxRadiosPerRow:
                html.append('%s<input type="radio" name="%s.%s" value="%s"%s%s>' %
                        (self.labels[i],self.prefix, self.name, options[i], selected, jsEvents)
                        )
            else:
                html.append('<input type="radio" name="%s.%s" value="%s"%s%s>%s<br>' %
                        (self.prefix, self.name, options[i], selected, jsEvents, self.labels[i])
                        )
        html.append("""</p></td><td>%s</td></tr>\n""" % helpText)
        return '\n'.join(html)


class QRepeatedGroup(QContainer):
    """This can create multiple rows based on a 'pattern'.

    example:  holds between 1 and 7 groupings of
      (fundCode, fundName, amount)
    i.e. 2 text fields and a floatField.

    The self.substructure field is populated at parse time
    with one example each of the type of fields allowed.
    The things in the group MUST exhibit the same structure
    in each row.  At runtime it generates a list of children,
    each of which is a QSection with named children, such
    as the 3 above.  To append a row to the children list
    (i.e. create a new fundCode/name.amount triple), call
    addRow().
    """
    def __init__(self):
        QContainer.__init__(self)
        self.minCount = 0 # zero allowed
        self.maxCount = None  # i.e. no maximum
        self.sampleData = 0
        self.substructure = []
        self._autoCreateRows = 0

    def postInit(self):
        "called after last child added"
        if self.maxCount:
            self.maxCount = int(self.maxCount)
        self.sampleData = int(self.sampleData)

    def makeSample(self):
        "Initialize an object with sample data"
        for i in range(self.sampleData):
            self.addRow()
        for child in self.children:
            child.makeSample()

    def clear(self):
        self.children = []

    def addChild(self, child):
        self.substructure.append(child)

    def addRow(self):
        sec = QSection()
        sec.noCaption = 1
        sec.name = '%s.row%d' % (self.name,len(self.children))
        for thing in self.substructure:
            # Andy's algorithm is horribly broken for the case
            # when the substructure items are complex and have
            # dictionaries/lists as attributes. A step too far
            # in greedy optimization
            # this copy algorithm makes one from a prototype
            # and is 20x faster than using copy.copy or
            # copy.deepcopy  !?!??
            #klass = thing.__class__
            #thang = klass()
            #thang.__dict__.update(thing.__dict__)
            #thang.prefix = self.prefix + '.' + sec.name
    
            # use deepcopy, it ensures that we get guaranteed different
            # objects
            # we should really do the thing in three stages.
            # 1) create 2) apply attributes 3) do the postInit
            # but we have no proper way to get the initial attributes
            # from the template objects
            thang = copy.deepcopy(thing)
            thang.prefix = self.prefix + '.' + sec.name
            thang.postInit()
            sec.children.append(thang)
        self.children.append(sec)
        return sec

    def deleteRow(self):
        "Deletes last row"
        self.children = self.children[:-1]

    def setTextDictionary(self, dict):
        self._autoCreateRows = 1
        QContainer.setTextDictionary(self, dict)
        self._autoCreateRows = 0

    def __getattr__(self, name):
        "If they ask for row4, make sure 4 rows exist..."

        if name[0:3] == 'row':
            rowNo = int(name[3:])
            if rowNo < len(self.children):
                return self.children[rowNo]
            elif self._autoCreateRows:
                while len(self.children) < rowNo+1:
                    self.addRow()
                return self.children[rowNo]
            else:
                raise IndexError('QRepeatedGroup does not have enough rows')
        else:
            raise AttributeError('Attribute %s not found in QRepeatedGroup' % name)

    def __getitem__(self, idx):
        return self.children[idx]

    def getLabel(self):
        "Display to left of field"
        if self.label is None:
            return self.name
        else:
            return self.label

    def makeHtml(self):
        """Must return as many rows as asked for..."""
        html = []
        rowLabel = self.getLabel()
        if rowLabel:
            rowLabelFmt = """%s %%d<!--%%s-->\n""" % rowLabel
        # we might have 3 rows but allow up to 7.  Generate more
        # rows if needed so stuff can be input.
        childCount = len(self.children)
        if self.maxCount:
            extraNeeded = self.maxCount - childCount
            for i in range(extraNeeded):
                self.addRow()

        html.append('<!-- QRepeatedGroup name="%s" prefix="%s" -->'%(self.name, self.prefix))
        for elem in self.children:
            rowNum = self.children.index(elem)
            rowNum1 = rowNum+1
            h = elem.makeHtml()
            html.append(h)

        if self.maxCount:
            for i in range(extraNeeded):
                self.deleteRow()

        return '\n'.join(html)
