# Copyright (c) 2005 Infrae. All rights reserved.
# See also LICENSE.txt
from datetime import datetime
from sets import Set
from xml.sax.saxutils import escape
import re

from zope.interface import implements
from zope.schema.interfaces import ITitledTokenizedTerm
from zope.schema.vocabulary import SimpleTerm

from zope.app import zapi
from zope.app.intid.interfaces import IIntIds
from zope.app.form.interfaces import IDisplayWidget
from zope.app.form.browser import PasswordWidget
from zope.app.form.browser import DisplayWidget
from zope.app.form.browser import TextWidget as TextWidget_
from zope.app.form.browser import TextAreaWidget as TextAreaWidget_
from zope.app.form.browser import MultiCheckBoxWidget as MultiCheckBoxWidget_
from zope.app.form.browser import RadioWidget as RadioWidget_
from zope.app.form.browser import DateWidget as DateWidget_
from zope.app.form.browser import TextWidget
from zope.app.form.browser.itemswidgets import MultiSelectSetWidget
from zope.app.form.interfaces import ConversionError
from zope.app.form.browser.widget import renderElement
from zope.app.component.hooks import getSite
from zope.app.pagetemplate import ViewPageTemplateFile
from zope.app.datetimeutils import DateTimeError, parseDatetimetz

from documentlibrary.core.ipparse import IPRange
from documentlibrary.core.interfaces import ICategoryContained
from documentlibrary.core import timezone

class TextWidget(TextWidget_):

    displayWidth = 60

class TextAreaWidget(TextAreaWidget_):
    
    width = 60
    height = 5

email_pattern = re.compile(
    '^[0-9a-zA-Z_&.%+-]+@([0-9a-zA-Z]([0-9a-zA-Z-]*[0-9a-zA-Z])?\.)+[a-zA-Z]{2,6}$')

class AuthorsWidget(TextAreaWidget):
    
    required = False

    default = []
    extra = 'widget:type="authors"'
    
    def _toFieldValue(self, value):
        lines = value.splitlines()
        # '||' delimited, one (fistname, lastname, emailaddress) per line
        authors = []
        for line in lines:
            if line == '||||': # empty lines can be ignored
                continue
            try:
                firstname, lastname, emailaddress = [
                    part.strip() for part in line.split('||')]
            except ValueError: # not enough items to unpack; this is invalid
                raise ConversionError((
                    'Incomplete or invalid authors information supplied. '
                    'Per author a firstname, lastname and a valid emailaddress '
                    'are required.'
                    ))

            if not firstname or not lastname or not emailaddress:
                raise ConversionError((
                    'Incomplete or invalid authors information supplied. '
                    'Per author a firstname, lastname and a valid emailaddress '
                    'are required.'
                    ))
                
            if email_pattern.search(emailaddress.lower()) is None:
                raise ConversionError(
                    'Authors information contains an invalid email address.')
                    
            authors.append((firstname, lastname, emailaddress))

        if not authors:
            # No authors! That's no good, we need authors!
            raise ConversionError('Required input is missing..')
            
        return authors
    
    def _toFormValue(self, value):
        # delimit with '||'
        return '\n'.join(['||'.join(t) for t in value])
        
class AuthorsDisplayWidget(DisplayWidget):
    implements(IDisplayWidget)

    required = False
    
    __call__ = ViewPageTemplateFile('templates/authors_displaywidget.pt')
    
    def authors(self):
        value = self._data
        result = []
        for firstname, lastname, email in value:
            d = {
                'firstname': firstname,
                'lastname': lastname,
                'email': email
                }
            result.append(d)
        return result

class UnescapedDisplayWidget(DisplayWidget):
    implements(IDisplayWidget)

    def __call__(self):
        if self._renderedValueSet():
            value = self._data
        else:
            value = self.context.default
        if value == self.context.missing_value:
            return ""
        return value

class HandleUrlWidget(DisplayWidget):
    implements(IDisplayWidget)

    def __call__(self):
        url = (zapi.absoluteURL(getSite(), self.request) +
               '/handle/' +
               self._data)
        return renderElement('a',
                             href=url,
                             contents=url)

class IpRangesWidget(TextAreaWidget):
    required = False

    default = ''
    
    def _toFieldValue(self, value):
        lines = value.splitlines()
        result = []
        try:
            for line in lines:
                line = line.strip()
                result.append(IPRange(line))
        except ValueError:
            raise ConversionError(u'Invalid ip ranges were supplied')
        return result

    def _toFormValue(self, value):
        from zope.security.proxy import removeSecurityProxy
        result = []
        for ip_range in value:
            result.append(str(removeSecurityProxy(ip_range)))
        return '\n'.join(result)

class IpRangesDisplayWidget(DisplayWidget):
    implements(IDisplayWidget)

    required = False

    def __call__(self):
        from zope.security.proxy import removeSecurityProxy
        result = []
        for ip_range in self._data:
            result.append(str(removeSecurityProxy(ip_range)))
        return '<br />\n'.join(result)
    
def RadioWidget(field, request):
    vocabulary = field.vocabulary
    return RadioWidget_(field, vocabulary, request)

def MultiCheckBoxWidget(field, request):
    vocabulary = field.vocabulary
    return MultiCheckBoxWidget_(field, vocabulary, request)

def MultiCheckBoxSetWidget(field, request):
    vocabulary = field.value_type.vocabulary
    return MultiCheckBoxWidget_(field, vocabulary, request)

def CategoryDisplayWidget(field, request):
    vocabulary = field.vocabulary
    return ItemDisplayWidget(field, vocabulary, request)

class ItemDisplayWidget(DisplayWidget):
    implements(IDisplayWidget)

    def __init__(self, field, vocabulary, request):
        self.context = field
        self.request = request
        self.vocabulary = vocabulary
        
    _messageNoValue = ""
    def textForValue(self, term):
        if ITitledTokenizedTerm.providedBy(term):
            return term.title
        return term.token
    
    def __call__(self):
        value = self._data
        if not value:
            return ""
        else:
            term = self.vocabulary.getTerm(value)
            return self.textForValue(term)

class UsersEditWidget_(MultiSelectSetWidget):
    def __call__(self):
        # XXX nested tables aren't very pretty..
        result = []
        # XXX theoretical code for now
        #result.append('<table border="0">')
        #result.append('<tr><td>')
        result.append(super(UsersEditWidget_, self).__call__())
        #result.append('</td><td>')
        #result.append('<ul><li>hoi</li><li>another</li></ul>')        
        #result.append('</td></tr></table>')
        return ''.join(result)

def UsersEditWidget(field, request):
    vocabulary = field.value_type.vocabulary
    return UsersEditWidget_(field, vocabulary, request)

# XXX should have Email field too
class EmailWidget(TextWidget):
    """Widget that validates whether an email address is valid.
    """
    pattern = re.compile(
        '^[0-9a-zA-Z_&.%+-]+@([0-9a-zA-Z]([0-9a-zA-Z-]*[0-9a-zA-Z])?\.)+[a-zA-Z]{2,6}$')

    def _toFieldValue(self, input):
        value = super(EmailWidget, self)._toFieldValue(input)
        if value is self.context.missing_value:
            return value
        if self.pattern.search(value.lower()) is None:
            raise ConversionError("Not a valid email address")
        return value

class PasswordConfirmationWidget(PasswordWidget):
    """Password Widget that uses two fields to confirm user input.
    """

    default = ''
    type = 'password'

    def __call__(self):
        return '%s Confirm: %s ' % (
            renderElement(
                self.tag,
                type=self.type,
                name=self.name,
                id=self.name,
                value=self.default,
                cssClass=self.cssClass,
                style=self.style,
                size=self.displayWidth,
                extra=self.extra),
            renderElement(
                self.tag,
                type=self.type,
                name=self.name + '.confirm',
                id=self.name + '.confirm',
                value=self.default,
                cssClass=self.cssClass,
                style=self.style,
                size=self.displayWidth,
                extra=self.extra))

    def _toFieldValue(self, input):
        """Check whether the confirmation field value is identical to
        the password value.
        """
        formdata = self.request.form
        if input != formdata[self.name + '.confirm']:
            raise ConversionError("Supplied passwords are not identical", '')
        return super(PasswordConfirmationWidget, self)._toFieldValue(input)

    def hasInput(self):
        """Check whether the field is represented in the form.
        """
        return self.name + ".confirm" in self.request.form or \
            super(PasswordConfirmationWidget, self).hasInput()
    
    def hidden(self):
        raise NotImplementedError(
            'Cannot get a hidden tag for a password field')

class DateDisplayWidget(DateWidget_):

    format = '%d-%b-%Y'
    
    def __call__(self):
        return timezone.forDisplay(self._data).strftime(self.format)

class DatetimeDisplayWidget(DateDisplayWidget):

    format = '%d-%b-%Y %H:%M'
    
class FancyDateWidget(DateWidget_):

    format = '%d-%b-%Y'
    displayWidth = 14
    
    def _toFieldValue(self, input):
        if input == self._missing:
            return self.context.missing_value
        try:
            return timezone.fromInput(parseDatetimetz(input))
        except (DateTimeError, ValueError, IndexError), v:
            raise ConversionError, "Invalid datetime data: %s" % v

    def _toFormValue(self, value):
        if value == self.context.missing_value:
            return self._missing
        if value and isinstance(value, datetime):
            formvalue = timezone.forDisplay(value).strftime(self.format)
        else:
            formvalue = ''
        return formvalue
            
    def __call__(self):
        value = self._getFormValue()
        input_tag = renderElement(
            self.tag,
            type=self.type,
            name=self.name,
            id=self.name,
            value=value,
            cssClass=self.cssClass,
            style=self.style,
            size=self.displayWidth,
            extra=self.extra)

        resource_url = zapi.absoluteURL(
            getSite(),
            self.request) + '/++resource++'
        image_id = '%s_calendar_img' % self.name
        image_url = resource_url + 'images/calendar.png'
        
        image_tag = renderElement(
            'img',
            src=image_url,
            id=image_id,
            style="cursor: pointer;",
            title="Date selector",
            onmouseover="this.style.background='';",
            onmouseout="this.style.background=''")
        
        calendar_js = calendar_js_template % {
            'input_id': self.name,
            'image_id': image_id,
            'resource_url': resource_url,
            'ifFormat': '%d-%b-%Y',
            }
        return "%s\n%s\n%s" % (input_tag, image_tag, calendar_js)

def CategoriesWidget(field, request):
    vocabulary = field.vocabulary
    return CategoriesWidget_(field, vocabulary, request)

def FOITopicsWidget(field, request):
    vocabulary = field.vocabulary
    return FOITopicsWidget_(field, vocabulary, request)

from zope.app.form.browser import itemswidgets

class ShortenedTitleWidget(itemswidgets.DropdownWidget):
    """Make sure the widget is initialized correctly, after which
    the titles of the vocabulary terms are shortened to prevent
    excesively wide form elements.
    """
    
    _maxlength = 75
    
    def __init__(self, *args, **kwargs):
        super(ShortenedTitleWidget, self).__init__(*args, **kwargs)
        # shorten
        for term in self.vocabulary:
            shorttitle = centertruncate(term.title, self._maxlength)
            term.title = shorttitle

class HierarchicalDropdown(ShortenedTitleWidget):

    _messageSelectAValue = "select..."
                              
    def textForValue(self, term):
        # Override ItemsWidgetBase.textForValue
        prefix = '.'.join([str(i+1) for i in term.sequence]) # XXX sequence hack
        return prefix + ' ' + term.title
    
    def renderItems(self, value):
        # check if we want to select first item
        if (value == self.context.missing_value
            and getattr(self, 'firstItem', False) 
            and len(self.vocabulary) > 0):
            # Grab the first item from the iterator:
            values = [iter(self.vocabulary).next().value]
        elif value != self.context.missing_value:
            values = [value]
        else:
            values = []
        items = self.renderItemsWithValues(values)
        # Here we unconditionally add a first item, that has no value
        option = (
            '<option value="">%s</option>'
            %(self.translate(self._messageSelectAValue)))
        items.insert(0, option)
        return items
    
class CategoriesWidget_(HierarchicalDropdown):

    _messageSelectAValue = "select..."
    
    # For categories we do something special: we add options for the parents 
    # of terms to provide some semantic context. These options are disabled
    # (visually 'greyed out') though, and since there're not in the vocabulary
    # these options can never provide legal input.
    def renderItemsWithValues(self, values):
        cssClass = self.cssClass
        rendered_items = []
        for term in self.vocabulary:
            for disabled in self._getDisabledParentTerms(term):
                item = self.renderDisabledItem(
                    None, self.textForValue(disabled), None, None, cssClass)
                rendered_items.append(item)
                
            if term.value in values:
                rendered_item = self.renderSelectedItem(
                    None, self.textForValue(term), term.token, self.name, cssClass)
            else:
                rendered_item = self.renderItem(
                    None, self.textForValue(term), term.token, self.name, cssClass)
            rendered_items.append(rendered_item)
            
        return rendered_items
    
    def _getDisabledParentTerms(self, child_term):
        intid = zapi.getUtility(IIntIds)
        obj = intid.getObject(child_term.value).__parent__
        sequence = list(child_term.sequence)[:-1]
        
        visited_items = self.request.annotations.setdefault(
            '__visited_items__', []) # temporarily cache rendered items
        
        parent_terms = []
        while ICategoryContained.providedBy(obj):
            id = intid.getId(obj)
            if id in self.vocabulary: # term is actually available
                break
            if id in visited_items: # was this term was rendered already?
                break
            visited_items.append(id)
            term = SimpleTerm(0, 0, obj.name)
            term.sequence = tuple(sequence) # XXX hack for widgets
            parent_terms.append(term)
            sequence.pop()
            obj = obj.__parent__
        parent_terms.reverse()
        return parent_terms
    
    def renderDisabledItem(self, index, text, value, name, cssClass):
        """Render an item for a particular `value` that is selected."""
        return renderElement(
            'option', contents=escape(text), value='', 
            cssClass='disabled-option', extra='disabled="disabled"')
    
class FOITopicsWidget_(HierarchicalDropdown):
    
    _messageSelectAValue = "not applicable"

class SetTextAreaWidget(TextAreaWidget):

    def _toFieldValue(self, value):
        value = super(SetTextAreaWidget, self)._toFieldValue(value)
        if value:
            value = value.split('\n')
            value = [v.strip() for v in value]
        return Set(value)
    
    def _toFormValue(self, value):
        if value:
            l = list(value)
            l.sort()
            value = '\n'.join(l)
        else:
            value = ''
        return super(SetTextAreaWidget, self)._toFormValue(value)

class PreformattedWidget(DisplayWidget):
    implements(IDisplayWidget)
    def __call__(self):
        if self._data:
            return '<pre>%s</pre>' % self._data
        else:
            return ''

def centertruncate(text, max_length):
    if len(text) < max_length:
        return text
    part_length = (max_length - 3) / 2
    return '%s...%s' % (text[:part_length], text[-part_length:])
        
# these need to be in the page's HTML in order to use the FancyCalendar widget
#<style type="text/css">@import url(%(resource_url)scss/calendar.css);</style>
#<script type="text/javascript" src="%(resource_url)sjs/calendar.js"></script>
#<script type="text/javascript" src="%(resource_url)sjs/calendar-en.js"></script>
#<script type="text/javascript" src="%(resource_url)sjs/calendar-setup.js"></script>

calendar_js_template = '''\
<script type="text/javascript">
Calendar.setup({
  inputField: "%(input_id)s",
  ifFormat: "%(ifFormat)s",
  button: "%(image_id)s",
  align: "Tl",
  singleClick: true
  });
</script>'''
