"""
Widgets are small components that render form fields for inputing data in a
certain format.

Caution: the args argument in the widgets' render methods is a disaster
in python3 -- it maps from bytes when there are errors (it's a t.w
Request.args), from string otherwise (it's a formal-internal thing).
And don't get me started on its values.
"""

# TODO: Fix this -- have a common sort of args for both cases in
# some way.

import itertools

from pkg_resources import resource_filename
from zope.interface import implementer
from twisted.web import template
from twisted.web.template import tags as T

from . import iformal, validation, nevowc
from .util import render_cssid


# Marker object for args that are not supplied
_UNSET = object()


def getStringFromArgs(args, key, default=''):
    """returns args[key][0] suitably decoded.

    This is used to insert erroneous inputs into fields from request.args.

    Default must already be a string, not bytes.

    Unfortunately, depending on the context, formal will pass in args
    with either string keys or bytes keys.  While we can't rebuild
    the thing to be halfway sane, we simply adapt key based on some
    key we pick from args.  Yeah, madness.
    """
    if not args:
        return default
    if isinstance(next(iter(args)), str):
        if isinstance(key, bytes):
            key = key.decode("ascii")
    else:
        if isinstance(key, str):
            key = key.encode("ascii")

    if key in args and args[key]:
        return args[key][0].decode("utf-8", "replace")
    else:
        return default


def perhapsDecode(val):
    """returns val utf-8-decoded if it's bytes, unchanged otherwise.
    """
    if isinstance(val, bytes):
        return val.decode("utf-8")
    return val


@implementer( iformal.IWidget )
class TextInput(object):
    """
    A text input field.

    <input type="text" ... />
    """

    inputType = 'text'
    showValueOnFailure = True
    placeholder = None

    def __init__(self, original):
        self.original = original

    def _renderTag(self, request, key, value, readonly):
        tag=T.input(type=self.inputType, name=key, id=render_cssid(key))
        if value is not None:
            tag(value=str(value))
        if readonly:
            tag(class_='readonly', readonly='readonly')
        if self.placeholder is not None:
            tag(placeholder=self.placeholder)
        return tag

    def render(self, request, key, args, errors):
        if errors:
            value = getStringFromArgs(args, key)
        else:
            value = iformal.IStringConvertible(self.original).fromType(
                args.get(key))
        if not self.showValueOnFailure:
            value = None
        return self._renderTag(request, key, value, False)

    def renderImmutable(self, request, key, args, errors):
        value = iformal.IStringConvertible(self.original).fromType(
            args.get(key))
        return self._renderTag(request, key, value, True)

    def processInput(self, request, key, args, default=''):
        value = getStringFromArgs(args, key, default)
        value = iformal.IStringConvertible(self.original).toType(value)
        return self.original.validate(value)


@implementer( iformal.IWidget )
class Checkbox(object):
    """
    A checkbox input field.

    <input type="checkbox" ... />
    """

    def __init__(self, original):
        self.original = original

    def _renderTag(self, request, key, value, disabled):
        tag = T.input(type='checkbox', name=key, id=render_cssid(key), value='True')
        if value == 'True':
            tag(checked='checked')
        if disabled:
            tag(class_='disabled', disabled='disabled')
        return tag

    def render(self, request, key, args, errors):
        if errors:
            value = getStringFromArgs(args, key)
        else:
            value = iformal.IBooleanConvertible(self.original).fromType(
                args.get(key))
        return self._renderTag(request, key, value, False)

    def renderImmutable(self, request, key, args, errors):
        value = iformal.IBooleanConvertible(self.original).fromType(
            args.get(key))
        return self._renderTag(request, key, value, True)

    def processInput(self, request, key, args, default=''):
        value = getStringFromArgs(args, key, default)
        if not value:
            value = 'False'
        value = iformal.IBooleanConvertible(self.original).toType(value)
        return self.original.validate(value)


class Password(TextInput):
    """
    A text input field that hides the text.

    <input type="password" ... />
    """
    inputType = 'password'
    showValueOnFailure = False


@implementer( iformal.IWidget )
class TextArea(object):
    """
    A large text entry area that accepts newline characters.

    <textarea>...</textarea>
    """

    cols = 48
    rows = 6

    def __init__(self, original, cols=None, rows=None):
        self.original = original
        if cols is not None:
            self.cols = cols
        if rows is not None:
            self.rows = rows

    def _renderTag(self, request, key, value, readonly):
        tag=T.textarea(name=key,
            id=render_cssid(key),
            cols=str(self.cols),
            rows=str(self.rows))[str(value or '')]
        if readonly:
            tag(class_='readonly', readonly='readonly')
        return tag

    def render(self, request, key, args, errors):
        if errors:
            value = getStringFromArgs(args, key)
        else:
            value = iformal.IStringConvertible(self.original).fromType(
                args.get(key))
        return self._renderTag(request, key, value or "", False)

    def renderImmutable(self, request, key, args, errors):
        value = iformal.IStringConvertible(self.original).fromType(
            args.get(key))
        return self._renderTag(request, key, value, True)

    def processInput(self, request, key, args, default=''):
        value = getStringFromArgs(args, key, default)
        value = iformal.IStringConvertible(self.original).fromType(value)
        return self.original.validate(value)


@implementer( iformal.IWidget )
class TextAreaList(object):
    """
    A text area that allows a list of values to be entered, one per line. Any
    empty lines are discarded.
    """

    cols = 48
    rows = 6

    def __init__(self, original, cols=None, rows=None):
        self.original = original
        if cols is not None:
            self.cols = cols
        if rows is not None:
            self.rows = rows

    def _renderTag(self, request, key, values, readonly):
        value = '\n'.join(values)
        tag=T.textarea(name=key,
            id=render_cssid(key),
            cols=str(self.cols),
            rows=str(self.rows))[str(value or '')]
        if readonly:
            tag(class_='readonly', readonly='readonly')
        return tag

    def render(self, request, key, args, errors):
        converter = iformal.IStringConvertible(self.original.type)
        if errors:
            values = args.get(key, [])
        else:
            values = args.get(key)
            if values is not None:
                values = [converter.fromType(v) for v in values]
            else:
                values = []
        return self._renderTag(request, key, values, False)

    def renderImmutable(self, request, key, args, errors):
        converter = iformal.IStringConvertible(self.original.type)
        values = args.get(key)
        if values is not None:
            values = [converter.fromType(v) for v in values]
        else:
            values = []
        return self._renderTag(request, key, values, True)

    def processInput(self, request, key, args, default=''):
        # Get the whole string
        value = getStringFromArgs(args, key, default)
        # Split into lines
        values = value.splitlines()
        # Strip each line
        values = [v.strip() for v in values]
        # Discard empty lines
        values = [v for v in values if v]
        # Convert values to correct type
        converter = iformal.IStringConvertible(self.original.type)
        values = [converter.toType(v) for v in values]
        # Validate and return
        return self.original.validate(values)


@implementer( iformal.IWidget )
class CheckedPassword(object):
    """
    Two password entry fields that must contain the same value to validate.
    """

    def __init__(self, original):
        self.original = original

    def render(self, request, key, args, errors):
        if errors and not errors.getFieldError(key):
            values = args.get(key)
        else:
            values = ('', '')
        return [
            T.input(type='password', name=key, id=render_cssid(key),
                value=str(values[0])),
            T.br,
            T.label(for_=render_cssid(key, 'confirm'))[' Confirm '],
            T.input(type='password', name=key, id=render_cssid(key, 'confirm'), value=str(values[1])),
            ]

    def renderImmutable(self, request, key, args, errors):
        values = ('', '')
        return [
            T.input(type='password', name=key, id=render_cssid(key),
                value=str(values[0]), class_='readonly', readonly='readonly'),
            T.br,
            T.label(for_=render_cssid(key, 'confirm'))[' Confirm '],
            T.input(type='password', name=key, id=render_cssid(key, 'confirm'),
                    value=str(values[1]), class_='readonly',
                    readonly='readonly')
        ]

    def processInput(self, request, key, args, default=''):
        pwds = [perhapsDecode(pwd) for pwd in args.get(key, [])]
        if len(pwds) == 0:
            pwds = ["", ""]
        elif len(pwds) == 1:
            raise validation.FieldValidationError('Please enter the password twice for confirmation.')
        else:
            if pwds[0] != pwds[1]:
                raise validation.FieldValidationError('Passwords do not match.')
        return self.original.validate(pwds[0])


class ChoiceBase(object):
    """
    A base class for widgets that provide the UI to select one or more items
    from a list.

    options:
        A sequence of objects adaptable to IKey and ILabel. IKey is used as the
        <option>'s value attribute; ILabel is used as the <option>'s child.
        IKey and ILabel adapters for tuple are provided.
    noneOption:
        An object adaptable to IKey and ILabel that is used to identify when
        nothing has been selected.
    """

    options = None
    noneOption = None

    def __init__(self, original, options=None, noneOption=_UNSET):
        self.original = original
        if options is not None:
            self.options = options
        if noneOption is not _UNSET:
            self.noneOption = noneOption

    def processInput(self, request, key, args, default=''):
        value = getStringFromArgs(args, key, default)
        value = iformal.IStringConvertible(self.original).toType(value)
        if self.noneOption is not None and \
                value == iformal.IKey(self.noneOption).key():
            value = None
        return self.original.validate(value)


@implementer( iformal.IWidget )
class SelectChoice(ChoiceBase):
    """
    A drop-down list of options.
    """

    noneOption = ('', '')

    def _renderTag(self, request, key, value, converter, disabled):

        def renderOptions():
            data = self.options
            if self.noneOption is not None:
                noneVal = iformal.IKey(self.noneOption).key()
                option = T.option(value=str(noneVal))[
                    iformal.ILabel(self.noneOption).label()]
                if value is None or value==str(noneVal):
                    option = option(selected='selected')
                yield option
            if data is None:
                return
            for item in data:
                optValue = iformal.IKey(item).key()
                optLabel = iformal.ILabel(item).label()
                optValue = converter.fromType(optValue)
                option = T.option(value=stringifyOptionValue(optValue))[
                    iformal.ILabel(optLabel).label()]
                if optValue == value:
                    option = option(selected='selected')
                yield option

        tag=T.select(name=key, id=render_cssid(key))[list(renderOptions())]
        if disabled:
            tag(class_='disabled', disabled='disabled')
        return tag

    def render(self, request, key, args, errors):
        converter = iformal.IStringConvertible(self.original)
        if errors:
            value = getStringFromArgs(args, key)
        else:
            value = converter.fromType(args.get(key))
        return self._renderTag(request, key, value, converter, False)

    def renderImmutable(self, request, key, args, errors):
        converter = iformal.IStringConvertible(self.original)
        value = converter.fromType(args.get(key))
        return self._renderTag(request, key, value, converter, True)


@implementer(iformal.IWidget)
class SelectOtherChoice(object):
    """
    A <select> widget that includes an "Other ..." option. When the other
    option is selected an <input> field is enabled to allow free text entry.

    Unlike SelectChoice, the options items are not a (value,label) tuple
    because that makes no sense with the free text entry facility.

    TODO:
      * Make the Other option configurable in the JS
      * Refactor, refactor, refactor
    """

    options = None
    noneOption = ('', '')
    otherOption = ('...', 'Other ...')

    template = None

    def __init__(self, original, options=None, otherOption=None):
        self.original = original
        if options is not None:
            self.options = options
        if otherOption is not None:
            self.otherOption = otherOption
        if self.template is None:
            self.template = nevowc.XMLFile(resource_filename('formal',
                'html/SelectOtherChoice.html'))


    def _valueFromRequestArgs(self, charset, key, args, default=''):
        value = getStringFromArgs(args, key, default)
        if value == self.otherOption[0]:
            value = getStringFromArgs(args, key+'-other')
        return value

    def render(self, request, key, args, errors):
        return self._render(request, key, args, errors, False)

    def renderImmutable(self, request, key, args, errors):
        return self._render(request, key, args, errors, True)

    def _render(self, request, key, args, errors, immutable):

        charset = "utf-8"
        converter = iformal.IStringConvertible(self.original)

        if errors:
            value = self._valueFromRequestArgs(
                charset, key, args)
        else:
            value = converter.fromType(args.get(key))

        if value is None:
            value = iformal.IKey(self.noneOption).key()

        if immutable:
            template = nevowc.locatePattern(self.template, ['immutable'])
        else:
            template = nevowc.locatePattern(self.template, ['editable']
                )["editable"]
        optionGen = nevowc.locatePattern(template, ['option']
            )["option"]
        selectedOptionGen = nevowc.locatePattern(template,
            ['selectedOption'])["selectedOption"]
        optionTags = []
        selectOther = True

        if self.noneOption is not None:
            noneValue = iformal.IKey(self.noneOption).key()
            if value == noneValue:
                tag = selectedOptionGen.clone()
                selectOther = False
            else:
                tag = optionGen.clone()
            tag.fillSlots('value', noneValue)
            tag.fillSlots('label', iformal.ILabel(self.noneOption).label())
            optionTags.append(tag)

        if self.options is not None:
            for item in self.options:
                if value == item:
                    tag = selectedOptionGen.clone()
                    selectOther = False
                else:
                    tag = optionGen.clone()
                tag.fillSlots('value', item)
                tag.fillSlots('label', item)
                optionTags.append(tag)

        if selectOther:
            tag = selectedOptionGen.clone()
            otherValue = value
        else:
            tag = optionGen.clone()
            otherValue = ''
        tag.fillSlots('value', self.otherOption[0])
        tag.fillSlots('label', self.otherOption[1])
        optionTags.append(tag)

        tag = template
        tag.fillSlots('key', key)
        tag.fillSlots('id', render_cssid(key))
        tag.fillSlots('options', optionTags)
        tag.fillSlots('otherValue', otherValue)
        return tag

    def processInput(self, request, key, args, default=''):
        charset = "utf-8"
        value = self._valueFromRequestArgs(charset, key, args, default)
        value = iformal.IStringConvertible(self.original).toType(value)
        if self.noneOption is not None and value == iformal.IKey(self.noneOption).key():
            value = None
        return self.original.validate(value)


@implementer( iformal.IWidget )
class RadioChoice(ChoiceBase):
    """
    A list of options in the form of radio buttons.

    <div class="radiobutton"><input type="radio" ... value="..."/><label>...</label></div>
    """

    def _renderTag(self, request, key, value, converter, disabled):

        def renderOption(request, itemKey, itemLabel, num, selected):
            cssid = render_cssid(key, num)
            tag = T.input(name=key, type='radio', id=cssid,
                value=stringifyOptionValue(itemKey))
            if selected:
                tag = tag(checked='checked')
            if disabled:
                tag = tag(disabled='disabled')
            return T.div(class_='radiobutton')[ tag, T.label(for_=cssid)[itemLabel] ]

        def renderOptions():
            # A counter to assign unique ids to each input
            data = self.options
            idCounter = itertools.count()
            if self.noneOption is not None:
                itemKey = iformal.IKey(self.noneOption).key()
                itemLabel = iformal.ILabel(self.noneOption).label()
                yield renderOption(request, itemKey, itemLabel, next(idCounter), value is None)
            if not data:
                return
            for item in data:
                itemKey = iformal.IKey(item).key()
                itemLabel = iformal.ILabel(item).label()
                itemKey = converter.fromType(itemKey)
                yield renderOption(request, itemKey, itemLabel, next(idCounter), itemKey==value)

        return T.transparent[list(renderOptions())]

    def render(self, request, key, args, errors):
        converter = iformal.IStringConvertible(self.original)
        if errors:
            value = getStringFromArgs(args, key)
        else:
            value = converter.fromType(args.get(key))
        return self._renderTag(request, key, value, converter, False)

    def renderImmutable(self, request, key, args, errors):
        converter = iformal.IStringConvertible(self.original)
        value = converter.fromType(args.get(key))
        return self._renderTag(request, key, value, converter, True)


@implementer( iformal.IWidget )
class DatePartsSelect(object):
    """
    A date entry widget that uses three <input> elements for the day, month and
    year parts.

    The default entry format is the US (month, day, year) but can be switched to
    the more common (day, month, year) by setting the dayFirst attribute to
    True.

    The start and end year can be passed through but default to 1970 and 2070.

    The months default to non-zero prefixed numerics but can be passed as a list
    of label, value pairs

    default can be whitespace-separated parts here.
    """

    dayFirst = False
    days = [ (d,d) for d in range(1,32) ]
    months = [ (m,m) for m in range(1,13) ]
    yearFrom = 1970
    yearTo = 2070
    noneOption = ('', '')

    def __init__(self, original, dayFirst=None, yearFrom=None, yearTo=None, months=None, noneOption=_UNSET):
        self.original = original
        if dayFirst is not None:
            self.dayFirst = dayFirst
        if yearFrom is not None:
            self.yearFrom = yearFrom
        if yearTo is not None:
            self.yearTo = yearTo
        if months is not None:
            self.months = months
        if noneOption is not _UNSET:
            self.noneOption = noneOption

    def _namer(self, prefix):
        def _(part):
            return '%s__%s' % (prefix,part)
        return _

    def _renderTag(self, request, year, month, day, namer, readonly):
        years = [(v,v) for v in range(self.yearFrom,self.yearTo)]
        months = self.months
        days = self.days

        options = []
        if self.noneOption is not None:
            options.append( T.option(value=str(self.noneOption[0]))[
                iformal.ILabel(self.noneOption[1]).label()] )
        for value in years:
            if str(value[0]) == str(year):
                options.append( T.option(value=str(value[0]),
                        selected='selected')[
                    iformal.ILabel(value[1]).label()] )
            else:
                options.append( T.option(value=str(value)[0])[
                    iformal.ILabel(value[1]).label()] )
        yearTag = T.select(name=namer('year'))[ options ]

        options = []
        if self.noneOption is not None:
            options.append( T.option(value=str(self.noneOption[0]))[
                iformal.ILabel(self.noneOption[1]).label()] )
        for value in months:
            if str(value[0]) == str(month):
                options.append( T.option(value=str(value[0]),
                    selected='selected')[
                    iformal.ILabel(value[1]).label()] )
            else:
                options.append( T.option(value=str(value[0]))[
                    iformal.ILabel(value[1]).label()] )
        monthTag = T.select(name=namer('month'))[ options ]

        options = []
        if self.noneOption is not None:
            options.append( T.option(value=str(self.noneOption[0]))[
                iformal.ILabel(self.noneOption[1]).label()] )
        for value in days:
            if str(value[0]) == str(day):
                options.append( T.option(value=str(value[0]),
                        selected='selected')[
                    iformal.ILabel(value[1]).label()] )
            else:
                options.append( T.option(value=str(value[0]))[
                    iformal.ILabel(value[1]).label()] )
        dayTag = T.select(name=namer('day'))[ options ]

        if readonly:
            tags = (yearTag, monthTag, dayTag)
            for tag in tags:
                tag(class_='readonly', readonly='readonly')

        if self.dayFirst:
            return dayTag, ' / ', monthTag, ' / ', yearTag, ' ', ('(day/month/year)')
        else:
            return monthTag, ' / ', dayTag, ' / ', yearTag, ' ', ('(month/day/year)')

    def render(self, request, key, args, errors):
        converter = iformal.IDateTupleConvertible(self.original)
        namer = self._namer(key)
        if errors:
            year = getStringFromArgs(args, namer('year').encode("ascii"))
            month = getStringFromArgs(args, namer('month').encode("ascii"))
            day = getStringFromArgs(args, namer('day').encode("ascii"))
        else:
            year, month, day = converter.fromType(args.get(key))

        return self._renderTag(request, year, month, day, namer, False)

    def renderImmutable(self, request, key, args, errors):
        converter = iformal.IDateTupleConvertible(self.original)
        namer = self._namer(key)
        year, month, day = converter.fromType(args.get(key))
        return self._renderTag(request, year, month, day, namer, True)

    def processInput(self, request, key, args, default=None):
        namer = self._namer(key)
        # Get the form field values as a (y,m,d) tuple
        ymd = [getStringFromArgs(args, namer(part)).strip()
            for part in ('year', 'month', 'day')]
        # Remove parts that were not entered.
        ymd = [p for p in ymd if p]
        # Nothing entered means None otherwise we need all three.
        if not ymd:
            ymd = default and default.split()
        elif len(ymd) != 3:
            raise validation.FieldValidationError("Invalid date")
        # So, we have what looks like a good attempt to enter a date.
        if ymd is not None:
            # Map to integers
            try:
                ymd = [int(p) for p in ymd]
            except ValueError:
                raise validation.FieldValidationError("Invalid date")
        ymd = iformal.IDateTupleConvertible(self.original).toType(ymd)
        return self.original.validate(ymd)


@implementer( iformal.IWidget )
class DatePartsInput(object):
    """
    A date entry widget that uses three <input> elements for the day, month and
    year parts.

    The default entry format is the US (month, day, year) but can be switched to
    the more common (day, month, year) by setting the dayFirst attribute to
    True.

    By default the widget is designed to only accept unambiguous years, i.e.
    the user must enter 4 character dates.

    Many people find it convenient or even necessary to allow a 2 character
    year. This can be allowed by setting the twoCharCutoffYear attribute to an
    integer value between 0 and 99. Anything greater than or equal to the cutoff
    year will be considered part of the 20th century (1900 + year); anything
    less the cutoff year is a 21st century (2000 + year) date.

    A typical twoCharCutoffYear value is 70 (i.e. 1970). However, that value is
    somewhat arbitrary. It's the year that time began according to the PC, but
    it doesn't mean much to your non-techie user.

    dayFirst:
        Make the day the first input field, i.e. day, month, year
    twoCharCutoffYear:
        Allow 2 char years and set the year where the century flips between
        20th and 21st century.

    default can be whitespace-separated parts.
    """

    dayFirst = False
    twoCharCutoffYear = None

    def __init__(self, original, dayFirst=None, twoCharCutoffYear=None):
        self.original = original
        if dayFirst is not None:
            self.dayFirst = dayFirst
        if twoCharCutoffYear is not None:
            self.twoCharCutoffYear = twoCharCutoffYear

    def _namer(self, prefix):
        def _(part):
            return '%s__%s' % (prefix,part)
        return _

    def _renderTag(self, request, year, month, day, namer, readonly):
        yearTag = T.input(type="text", name=namer('year'),
            value=str(year or ""), size="4")
        monthTag = T.input(type="text", name=namer('month'),
            value=str(month or ""), size="2")
        dayTag = T.input(type="text", name=namer('day'),
            value=str(day or ""), size="2")
        if readonly:
            tags = (yearTag, monthTag, dayTag)
            for tag in tags:
                tag(class_='readonly', readonly='readonly')

        if self.dayFirst:
            return dayTag, ' / ', monthTag, ' / ', yearTag, ' ', ('(day/month/year)')
        else:
            return monthTag, ' / ', dayTag, ' / ', yearTag, ' ', ('(month/day/year)')

    def render(self, request, key, args, errors):
        converter = iformal.IDateTupleConvertible(self.original)
        namer = self._namer(key)
        if errors:
            year = getStringFromArgs(args, namer('year').encode("ascii"))
            month = getStringFromArgs(args, namer('month').encode("ascii"))
            day = getStringFromArgs(args, namer('day').encode("ascii"))
        else:
            year, month, day = converter.fromType(args.get(key))

        return self._renderTag(request, year, month, day, namer, False)

    def renderImmutable(self, request, key, args, errors):
        converter = iformal.IDateTupleConvertible(self.original)
        namer = self._namer(key)
        year, month, day = converter.fromType(args.get(key))
        return self._renderTag(request, year, month, day, namer, True)

    def processInput(self, request, key, args, default=None):
        namer = self._namer(key)
        # Get the form field values as a (y,m,d) tuple
        ymd = [getStringFromArgs(args, namer(part)).strip()
            for part in ('year', 'month', 'day')]
        # Remove parts that were not entered.
        ymd = [p for p in ymd if p]
        # Nothing entered means None otherwise we need all three.
        if not ymd:
            ymd = default and default.split()
        elif len(ymd) != 3:
            raise validation.FieldValidationError("Invalid date")
        # So, we have what looks like a good attempt to enter a date.
        if ymd is not None:
            # If a 2-char year is allowed then prepend the century.
            if self.twoCharCutoffYear is not None and len(ymd[0]) == 2:
                try:
                    if int(ymd[0]) >= self.twoCharCutoffYear:
                        century = '19'
                    else:
                        century = '20'
                    ymd[0] = century + ymd[0]
                except ValueError:
                    pass
            # By now, we should have a year of at least 4 characters.
            if len(ymd[0]) < 4:
                if self.twoCharCutoffYear is not None:
                    msg = "Please enter a 2 or 4 digit year"
                else:
                    msg = "Please enter a 4 digit year"
                raise validation.FieldValidationError(msg)
            # Map to integers
            try:
                ymd = [int(p) for p in ymd]
            except ValueError:
                raise validation.FieldValidationError("Invalid date")
        ymd = iformal.IDateTupleConvertible(self.original).toType(ymd)
        return self.original.validate(ymd)


@implementer( iformal.IWidget )
class MMYYDatePartsInput(object):
    """
    Two input fields for entering the month and year.

    default can be a year and month separated by whitespace.
    """

    cutoffYear = 70

    def __init__(self, original, cutoffYear=None):
        self.original = original
        if cutoffYear is not None:
            self.cutoffYear = cutoffYear

    def _namer(self, prefix):
        def _(part):
            return '%s__%s' % (prefix,part)
        return _

    def _renderTag(self, request, year, month, namer, readonly):
        yearTag = T.input(type="text", name=namer('year'),
            value=str(year), size="2")
        monthTag = T.input(type="text", name=namer('month'),
            value=str(month), size="2")
        if readonly:
            tags=(yearTag, monthTag)
            for tag in tags:
                tag(class_='readonly', readonly='readonly')
        return monthTag, ' / ', yearTag, ' (mm/yy)'

    def render(self, request, key, args, errors):
        converter = iformal.IDateTupleConvertible(self.original)
        namer = self._namer(key)
        if errors:
            year = getStringFromArgs(args, namer('year').encode("ascii"))
            month = getStringFromArgs(args, namer('month').encode("ascii"))
            # return a blank for the day
            day = ''
        else:
            year, month, day = converter.fromType(args.get(key))
            # if we have a year as default data, stringify it and only use last two digits
            if year is not None:
                year = str(year)[2:]

        return self._renderTag(request, year, month, namer, False)

    def renderImmutable(self, request, key, args, errors):
        converter = iformal.IDateTupleConvertible(self.original)
        year, month, day = converter.fromType(args.get(key))
        namer = self._namer(key)
        # if we have a year as default data, stringify it and only use last two digits
        if year is not None:
            year = str(year)[2:]
        return self._renderTag(request, year, month, namer, True)

    def processInput(self, request, key, args, default=None):
        namer = self._namer(key)
        value = [getStringFromArgs(args, namer(part)).strip()
            for part in ('year', 'month')]
        value = [p for p in value if p]
        if not value:
            value = default and default.split()
        elif len(value) != 2:
            raise validation.FieldValidationError("Invalid date")
        if value is not None:
            try:
                value = [int(p) for p in value]
            except ValueError:
                raise validation.FieldValidationError("Invalid date")
            if value[1] < 0 or value[1] > 99:
                raise validation.FieldValidationError("Invalid year. Please enter a two-digit year.")
            if value[0] > self.cutoffYear:
                value[0] = 1900 + value[0]
            else:
                value[0] = 2000 + value[0]
            value.append(1)
        value = iformal.IDateTupleConvertible(self.original).toType( value )
        return self.original.validate(value)


def stringifyOptionValue(s):
    """returns s safe for inclusion into selection boxes.

    That's a str with + replaced by %2b; I introduced this during a time
    of confusion before I understood that plus-quoting was for urlencoded
    values only.  But doing this helps in case there are broken http
    components in the way, so I'll keep replacing where I easily can.
    """
    return str(s).replace("+", "%2b")


@implementer( iformal.IWidget )
class CheckboxMultiChoice(template.Element):
    """
    Multiple choice list, rendered as a list of checkbox fields.

    lots enters blanks between the items (which allows a bit more
    flexibility in styling the stuff).

    emphasized can be a set of item titles that should additionally
    get an emphasized css class.

    Default is a whitespace-spearated enumeration for now.
    """

    options = None

    def __init__(self, original, options=None, lots=False,
            emphasized=frozenset()):
        self.original = original
        self.lots = lots
        self.emphasized = emphasized
        if options is not None:
            self.options = options


    def _renderTag(self, request, key, values, converter, disabled):
        def _():
           options = self.options
           # loops through checkbox options and renders
           for n,item in enumerate(options):
               optValue = iformal.IKey(item).key()
               optLabel = iformal.ILabel(item).label()
               optValue = converter.fromType(optValue)
               optid = render_cssid(key, n)
               checkbox = T.input(type='checkbox', name=key,
                    value=stringifyOptionValue(optValue),
                    id=optid+["-box"], class_="multichoice",
                    onchange="Forms.Util.updateLabel(this)")
               if disabled:
                   checkbox = checkbox(class_='disabled', disabled='disabled')
 	            # Label for is abominable on most browsers for this purpose.
               # So, let's use a span (sigh)
               if optValue in self.emphasized:
                   cssCls = "multichoice emphasized"
               else:
                   cssCls = "multichoice"
               label = T.span(class_=cssCls, id=optid+["-label"],
                 onclick="Forms.Util.labelClick(this)")[optLabel]
               if optValue in values:
                   checkbox = checkbox(checked='checked')
                   label = label(class_=cssCls+" selected")
               yield T.span(class_="nobreak")[checkbox, label]
               if self.lots:
                 yield " "
               else:
                 yield T.br()

        return T.transparent[list(_())]

    def render(self, request, key, args, errors):

        converter = iformal.IStringConvertible(self.original)

        if errors:
            values = args.get(key, [])
        else:
            values = args.get(key)
            if values is not None:
                values = [converter.fromType(v) for v in values]
            else:
                values = []

        return self._renderTag(request, key, values, converter, False)

    def renderImmutable(self, request, key, args, errors):

        converter = iformal.IStringConvertible(self.original)

        values = args.get(key)
        if values is not None:
            values = [converter.fromType(v) for v in values]
        else:
            values = []

        return self._renderTag(request, key, values, converter, True)

    def processInput(self, request, key, args, default=''):
        values = [perhapsDecode(v) for v in args.get(key, default.split())]
        converter = iformal.IStringConvertible(self.original)
        values = [converter.toType(v) for v in values]
        return self.original.validate(values)


@implementer( iformal.IWidget )
class FileUpload(object):
    """NOTE: You have use something like twistedpatch.MPRequest as
    your site's requestFactory to make this work.
    """
    def __init__(self, original):
        self.original = original

    def _renderTag(self, request, key, disabled):
        tag=T.input(name=key, id=render_cssid(key),type='file')
        if disabled:
            tag(class_='disabled', disabled='disabled')
        return tag

    def render(self, request, key, args, errors):
        return self._renderTag(request, key, False)

    def renderImmutable(self, request, key, args, errors):
        iformal.IFileConvertible(self.original).fromType(args.get(key))
        return self._renderTag(request, key, True)

    def processInput(self, request, key, args, default=None):
       # default is ignored here.

        if key not in request.files:
            return self.original.validate(None)
        fileitem = request.files[key][0]
        name = fileitem.file_name
        if isinstance(name, bytes):
            name = name.decode("utf-8", "replace")
        value = (name, fileitem.file_object)

        value = iformal.IFileConvertible(self.original).fromType(value)
        return self.original.validate(value)


class Hidden(object):
    """
    A hidden form field.
    """
    __implements__ = iformal.IWidget,

    inputType = 'hidden'

    def __init__(self, original):
        self.original = original

    def render(self, request, key, args, errors):
        if errors:
            value = getStringFromArgs(args, key)
        else:
            value = iformal.IStringConvertible(self.original).fromType(
                args.get(key))
        if value is None:
            value = ""

        return T.input(type=self.inputType, name=key, id=render_cssid(key),
            value=stringifyOptionValue(value))

    def renderImmutable(self, request, key, args, errors):
        return self.render(request, key, args, errors)

    def processInput(self, request, key, args, default=''):
        value = getStringFromArgs(args, key, default)
        value = iformal.IStringConvertible(self.original).toType(value)
        return self.original.validate(value)


__all__ = [
    'Checkbox', 'CheckboxMultiChoice', 'CheckedPassword',
    'Password', 'SelectChoice', 'TextArea', 'TextInput', 'DatePartsInput',
    'DatePartsSelect', 'MMYYDatePartsInput', 'Hidden', 'RadioChoice',
    'SelectOtherChoice', 'FileUpload', 'TextAreaList',
    ]

