"""$URL: svn+ssh://svn/repos/trunk/quixote/form2/form.py $
$Id: form.py 23244 2003-12-01 18:39:16Z dbinger $

Provides the Form class and related classes.  Forms are a convenient
way of building HTML forms that are composed of Widget objects.
"""

from types import StringType, ListType
from quixote import get_request, get_session, get_publisher
from quixote.html import url_quote, htmltag, htmltext, nl2br, TemplateIO
from quixote.form2.widget import Widget, HiddenWidget, StringWidget, \
    ButtonWidget, ResetWidget, SubmitWidget, subname


try:
    True, False, bool
except NameError:
    True = 1
    False = 0
    def bool(v):
        return not not v


class FormComponent:
    """Part of a form. Generally contains one or more than one widgets.
    The standard Form class renders components inside a 'table' tag.
    Some sites may wish to provide their own Form.render() and WidgetRow
    in order to get a different look.

    The purpose of this class is to document the FormComponent interface.

    Instance attributes: none
    """

    def __init__(self, name, *args, **kwargs):
        raise NotImplementedError, 'subclass must implement'

    def get_value(self):
        raise NotImplementedError, 'subclass must implement'

    def set_error(self, error):
        raise NotImplementedError, 'subclass must implement'

    def has_errors(self):
        raise NotImplementedError, 'subclass must implement'

    def render(self):
        raise NotImplementedError, 'subclass must implement'


def render_error(error):
    if error:
        return htmltext('<font color="red">%s</font><br />') % nl2br(error)
    else:
        return ''


def render_hint(hint):
    if hint:
        return htmltext('<em>%s</em>') % hint
    else:
        return ''


class WidgetRow(FormComponent):
    """
    Standard wrapper for widgets added to a Form: renders each widget in
    a couple of table rows (one for the widget title, and another for
    the widget itself, along with error and/or hint text).

    Instance attribues:
        widget : Widget
        title : string
        hint : string
        required : bool
    """

    def __init__(self, name, widget_class, value, title=None, hint=None,
                 required=False, **kwargs):
        self.widget = widget_class(name, value, **kwargs)
        self.title = title
        self.hint = hint
        self.required = required
        if required and get_request().form:
            if self.get_value() is None and not self.has_errors():
                self.set_error('value is required')

    def get_value(self):
        return self.widget.get_value()

    def set_error(self, error):
        self.widget.set_error(error)

    def has_errors(self):
        return bool(self.widget.get_error())

    def _render_error(self):
        return render_error(self.widget.get_error())

    def _render_hint(self):
        return render_hint(self.hint)

    def render(self):
        title = self.title or ''
        if title and self.required:
            title = title + htmltext('&nbsp;*')
        r = TemplateIO(html=True)
        r += htmltext('<tr><th colspan="3" align="left">')
        r += title
        r += htmltext('</th></tr>'
                      '<tr><td>&nbsp;&nbsp;</td><td>')
        r += self.widget.render()
        r += htmltext('</td><td>')
        r += self._render_error()
        r += self._render_hint()
        r += htmltext('</td></tr>')
        return r.getvalue()


class WidgetList(FormComponent):
    """A variable length list of widgets.

    Instance attributes:
      value : [any]
      error : string
    """

    def __init__(self,
                 name,
                 element_type=StringWidget,
                 value=None,
                 title=None,
                 hint=None,
                 element_name="row",
                 **args):
        assert value is None or type(value) is ListType, (
            "value '%s' not a list: got %r" % (name, value))
        assert type(element_name) in (StringType, htmltext), (
            "value '%s' element_name not a string: "
            "got %r" % (name, element_name))

        self.element_type = element_type
        self.title = title
        self.hint = hint
        self.error = None

        self.added_elements = HiddenWidget(subname(name, "added_elements"))
        self.add_button = SubmitWidget(subname(name, "add_element"),
                                       value="Add %s" % element_name)
        self.elements = []
        def add_element(value=None):
            self.elements.append(
                element_type(subname(name, "element%d" % len(self.elements)),
                             value=value, **args))
        if value is not None:
            for element_value in value:
                add_element(element_value)
        num_added = int(self.added_elements.get_value() or 1)
        for i in range(num_added):
            add_element()
        if self.add_button.get_value():
            add_element()
            num_added += 1
        self.added_elements.set_value(num_added)
        request = get_request()
        if request.form:
            self.value = []
            for element in self.elements:
                value = element.get_value()
                if value is not None:
                    self.value.append(value)
            self.value = value or None
        else:
            self.value = value

    def get_value(self):
        return self.value

    def set_error(self, error):
        self.error = error

    def has_errors(self):
        if self.error:
            return True
        for element in self.elements:
            if element.get_error():
                return True
        return False

    def _render_element(self, element, hint=None, error=None):
        r = TemplateIO(html=True)
        r += htmltext('<tr><td>&nbsp;&nbsp;</td><td>')
        r += element.render()
        r += htmltext('</td><td>')
        r += render_error(element.get_error() or error)
        r += render_hint(hint)
        r += htmltext('</td></tr>\n')
        return r.getvalue()

    def render(self):
        r = TemplateIO(html=True)
        r += htmltext('\n<tr><th colspan="3" align="left">')
        r += self.title or htmltext('&nbsp;')
        r += htmltext('</th></tr>')
        r += self._render_element(self.elements[0], self.hint, self.error)
        for element in self.elements[1:]:
            r += self._render_element(element)
        r += htmltext('<tr><td>&nbsp;</td><td>')
        r += self.add_button.render()
        r += self.added_elements.render()
        r += htmltext('</td></tr>')
        return r.getvalue()


class FormTokenWidget(HiddenWidget):
    def render(self):
        self.value = get_session().create_form_token()
        return HiddenWidget.render(self)


JAVASCRIPT_MARKUP = htmltext('''\
<script type="text/javascript">
<!--
%s
// -->
</script>
''')


class Form:
    # XXX needs a nice docstring that explains typical usage!
    """
    Instance attributes:
      components : [FormComponent]
      hidden_widgets = [HiddenWidget]
      button_widgets = [ButtonWidget]
      _names : { name:string : Widget|FormComponent }
        names used in the form
    """

    TOKEN_NAME = "_form_id" # name of hidden token widget

    COMPONENT_CLASS = WidgetRow

    def __init__(self,
                 name=None,
                 method="post",
                 action_url=None,
                 enctype=None,
                 use_tokens=True,
                 component_class=None):

        if method not in ("post", "get"):
            raise ValueError("Form method must be 'post' or 'get', "
                             "not %r" % method)
        self.name = name
        self.method = method
        self.action_url = action_url or self._get_default_action_url()
        self.components = []
        self.hidden_widgets = []
        self.button_widgets = []
        self._names = {}

        if enctype is not None and enctype not in (
            "application/x-www-form-urlencoded", "multipart/form-data"):
            raise ValueError, ("Form enctype must be "
                               "'application/x-www-form-urlencoded' or "
                               "'multipart/form-data', not %r" % enctype)
        self.enctype = enctype

        self.use_form_tokens = False
        if use_tokens and self.method == "post":
            config = get_publisher().config
            if config.form_tokens:
                # unique token for each form, this prevents many cross-site
                # attacks and prevents a form from being submitted twice
                self.add_hidden(self.TOKEN_NAME, None, FormTokenWidget)
                self.use_form_tokens = True

        self.component_class = component_class or self.COMPONENT_CLASS

    def _get_default_action_url(self):
        request = get_request()
        action_url = url_quote(request.get_path())
        query = request.get_environ("QUERY_STRING")
        if query:
            action_url += "?" + query
        return action_url

    def __getitem__(self, name):
        """(name:string) -> any
        Return a component's or widget's value.
        """
        try:
            return self._names[name].get_value()
        except KeyError:
            raise KeyError, 'no widget or component named %r' % name

    def get_widget(self, name):
        """(name:string) -> Widget | None
        """
        obj = self._names.get(name)
        if isinstance(obj, WidgetRow):
            return obj.widget
        elif isinstance(obj, Widget):
            return obj
        return None

    def is_submitted(self):
        """() -> bool

        Return true if a form was submitted.
        """
        return bool(get_request().form)

    def set_error(self, name, error):
        widget = self._names.get(name)
        if not widget:
            raise KeyError, "unknown name %r" % name
        widget.set_error(error)

    def has_errors(self):
        """() -> bool

        Return true if form has errors.
        """
        for component in self.components:
            if component.has_errors():
                return True
        return False

    def get_buttons(self):
        return self.button_widgets

    def get_submit(self):
        """() -> string | bool

        Get the name of the submit button that was used to submit the
        current form.  If the form is submitted but not by a button added by
        add_submit() then return True.  Otherwise, return False.

        """
        request = get_request()
        for button in self.button_widgets:
            if request.form.has_key(button.name):
                return button.name
        else:
            if request.form:
                return True
            else:
                return False

    # -- Form population methods ---------------------------------------

    def _add_name(self, name, obj):
        if self._names.has_key(name):
            raise ValueError, "form already has '%s' variable" % name
        self._names[name] = obj

    def add_component(self, klass, name, *args, **kwargs):
        """(FormComponent, name : string, ...)

        Add a form component object to the form.
        """
        if not issubclass(klass, FormComponent):
            raise TypeError, 'FormComponent subclass required (got %r)' % klass
        component = klass(name, *args, **kwargs)
        self._add_name(name, component)
        self.components.append(component)

    def add(self, klass, name, value=None,
             title=None, hint=None, required=False, **kwargs):
        """(Widget,
            name : string,
            value : any = None,
            title : string = None,
            hint : string = None,
            required : boolean = False,
            ...)

        Create a new widget and add it to the form.  The expected type of
        'value' also depends on the widget class.  Any extra keyword args are
        passed to the constructor method.
        """
        if issubclass(klass, HiddenWidget):
            raise TypeError, "use add_hidden() to add hidden widgets"
        self.add_component(self.component_class, name, klass, value,
                           title, hint, required, **kwargs)


    def _add_button_widget(self, klass, name, value, **kwargs):
        widget = klass(name, value, **kwargs)
        self._add_name(name, widget)
        self.button_widgets.append(widget)

    def add_button(self, name, value, **kwargs):
        self._add_button_widget(ButtonWidget, name, value, **kwargs)

    def add_reset(self, name, value, **kwargs):
        self._add_button_widget(ResetWidget, name, value, **kwargs)

    def add_submit(self, name, value, **kwargs):
        self._add_button_widget(SubmitWidget, name, value, **kwargs)

    def add_hidden(self, name, value, klass=HiddenWidget):
        widget = klass(name, value)
        self._add_name(name, widget)
        self.hidden_widgets.append(widget)

    # -- Layout (rendering) methods ------------------------------------

    def render(self):
        """() -> HTML text
        Render a form as HTML.
        """
        r = TemplateIO(html=True)
        r += self._render_start()
        r += self._render_body()
        r += self._render_finish()
        return r.getvalue()

    def _render_start(self):
        r = TemplateIO(html=True)
        r += htmltag('form', name=self.name, method=self.method,
                     enctype=self.enctype, action=self.action_url)
        r += self._render_hidden_widgets()
        return r.getvalue()

    def _render_finish(self):
        r = TemplateIO(html=True)
        r += htmltext('</form>')
        r += self._render_javascript()
        return r.getvalue()

    def _render_sep(self, text, line=True):
        return htmltext('<tr><td colspan="3">%s<strong><big>%s'
                        '</big></strong></td></tr>') % \
                                      (line and htmltext('<hr>') or '', text)

    def _render_hidden_widgets(self):
        r = TemplateIO(html=True)
        for widget in self.hidden_widgets:
            r += widget.render()
        return r.getvalue()

    def _render_button_widgets(self, ncols=3):
        r = TemplateIO(html=True)
        r += htmltext('<tr><td colspan="%d">\n') % ncols
        for button in self.button_widgets:
            r += button.render()
        r += htmltext('</td></tr>')
        return r.getvalue()

    def _render_components(self):
        r = TemplateIO(html=True)
        for component in self.components:
            r += component.render()
        return r.getvalue()

    def _render_error_notice(self):
        if self.has_errors():
            r = htmltext('<tr><td colspan="3">'
                         '<font color="red"><strong>Warning:</strong></font> '
                         'there were errors processing your form.  '
                         'See below for details.'
                         '</td></tr>')
        else:
            r = ''
        return r

    def _render_body(self):
        r = TemplateIO(html=True)
        r += htmltext('<table>')
        r += self._render_error_notice()
        r += self._render_components()
        r += self._render_button_widgets()
        r += htmltext('</table>')
        return r.getvalue()

    def _render_javascript(self):
        """Render javacript code for the form, if any.
           Insert code lexically sorted by code_id
        """
        javascript_code = get_request().response.javascript_code
        if javascript_code:
            form_code = []
            code_ids = javascript_code.keys()
            code_ids.sort()
            for code_id in code_ids:
                code = javascript_code[code_id]
                if code:
                    form_code.append(code)
                    javascript_code[code_id] = ''
            if form_code:
                return JAVASCRIPT_MARKUP % htmltext(''.join(form_code))
        return ''

