"""
$URL: svn+ssh://sting.mems-exchange.org/repos/trunk/DurusWorks/qp/fill/form.qpy $
$Id: form.qpy 31671 2009-06-03 18:17:55Z rmasse $
"""
from qp.fill.html import htmltag
from qp.fill.widget import CheckboxWidget, SingleSelectWidget, IntWidget
from qp.fill.widget import HiddenWidget, StringWidget, TextWidget
from qp.fill.widget import PasswordWidget, subname, FileWidget
from qp.fill.widget import RadiobuttonsWidget, MultipleSelectWidget
from qp.fill.widget import SubmitWidget, FloatWidget, ResetWidget
from qp.pub.common import get_request, get_hit, get_user
from qpy import xml

def get_javascript_code():
    if 'javascript_code' not in get_hit().get_info():
        get_hit().get_info()['javascript_code'] = {}
    return get_hit().get_info().get('javascript_code')

def add_javascript_code(name, javascript):
    if 'javascript_code' not in get_hit().get_info():
        get_hit().get_info()['javascript_code'] = {}
    get_hit().get_info()['javascript_code'][name] = javascript


class FormTokenWidget(HiddenWidget):

    def _parse(self, request):
        token = request.get_field(self.name)
        tokens = get_user().get_tokens()
        if token not in tokens:
            self.error = 'invalid'
        else:
            tokens.remove(token)

    def render_error(self, error):
        return ''

    def render(self):
        self.value = get_user().get_tokens().new_token()
        return HiddenWidget.render(self)


class Form(object):
    """
    Provides a high-level mechanism for collecting and processing user
    input that is based on HTML forms.

    Instance attributes:
      widgets : [Widget]
        widgets that are not subclasses of SubmitWidget or HiddenWidget
      submit_widgets : [SubmitWidget]
        subclasses of SubmitWidget, normally rendered at the end of the
        form
      hidden_widgets : [HiddenWidget]
        subclasses of HiddenWidget, normally rendered at the beginning
        of the form
      _names : { name:string : Widget }
        names used in the form and the widgets associated with them
    """

    TOKEN_NAME = "_form_id" # name of hidden token widget

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

    TOKEN_NOTICE = xml(
        '<div class="errornotice">'
        'The form you have submitted has already been submitted once. '
        'Please review the form data and submit the form again.'
        '</div>')

    ERROR_NOTICE = xml(
        '<div class="errornotice">'
        'There were errors processing your form.<br />'
        'See details below.'
        '</div>')

    def __init__(self,
                 method="post",
                 action=None,
                 enctype=None,
                 use_tokens=True,
                 prefix=None,
                 **attrs):

        if method not in ("post", "get"):
            raise ValueError("Form method must be 'post' or 'get', "
                             "not %r" % method)
        self.method = method
        self.action = action or self._get_default_action()
        if 'class' not in attrs:
            attrs['class'] = 'qp'
        self.prefix = prefix
        self.attrs = attrs
        self.widgets = []
        self.submit_widgets = []
        self.hidden_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

        if use_tokens and self.method == "post":
            # unique token for each form, this prevents many cross-site
            # attacks and prevents a form from being submitted twice
            self.add(FormTokenWidget, self.TOKEN_NAME, value=None)

    def _get_default_action(self):
        return get_request().get_path_query()

    def __getitem__(self, name):
        """(name:string) -> any
        Return a widget's value.  Raises KeyError if widget named 'name'
        does not exist.
        """
        try:
            return self._names[name].parse()
        except KeyError:
            raise KeyError('no widget named %r' % name)

    def has_key(self, name):
        """Return true if the widget named 'name' is in the form."""
        return name in self._names

    def get(self, name, default=None):
        """(name:string, default=None) -> any
        Return a widget's value.  Returns 'default' if widget named 'name'
        does not exist.
        """
        widget = self._names.get(name)
        if widget is not None:
            return widget.parse()
        else:
            return default

    def get_widget(self, name):
        """(name:string) -> Widget | None
        Return the widget named 'name'.  Returns None if the widget does
        not exist.
        """
        return self._names.get(name)

    def get_submit_widgets(self):
        """() -> [SubmitWidget]
        """
        return self.submit_widgets

    def get_all_widgets(self):
        """() -> [Widget]
        Return all the widgets that have been added to the form.  Note that
        this while this list includes submit widgets and hidden widgets, it
        does not include sub-widgets (e.g. widgets that are part of
        CompositeWidgets)
        """
        return list(self._names.values())

    def __iter__(self):
        for widget in self._names.values():
            for w in widget:
                yield w

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

        Has this form been submitted?  This is true if any widget
        name appears in the fields of the request.
        If the form has been submitted, this *modifies* the
        request fields as necessary to make sure that every widget
        of this form has a key in the fields dictionary.
        """
        request = get_request()
        for widget in self:
            if widget.is_submitted(request=request):
                fields = request.get_fields()
                for widget in self:
                    fields.setdefault(widget.get_name(), '')
                return True
        return False

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

        Ensure that all components of the form have parsed themselves.
        Return true if any of them have errors.
        """
        request = get_request()
        has_errors = False
        if self.is_submitted():
            for widget in self:
                widget.parse(request)
            for widget in self:
                if widget.has_error(request=request):
                    has_errors =  True
        return has_errors

    def clear_errors(self):
        """Ensure that all components of the form have parsed themselves.
        Clear any errors that might have occurred during parsing.
        """
        request = get_request()
        for widget in self:
            widget.clear_error(request)

    def clear(self):
        """Clear submitted values.
        """
        request = get_request()
        for widget in self:
            widget.clear(request)

    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 any known
        form-level SubmitWidget then return True.  Otherwise, return False.
        """
        request = get_request()
        for button in self.submit_widgets:
            if button.parse(request=request):
                return button.name
        return self.is_submitted()

    def set_error(self, name, error):
        """(name : string, error : string)
        Set the error attribute of the widget named 'name'.
        """
        widget = self._names.get(name)
        if not widget:
            raise KeyError("unknown name %r" % name)
        widget.set_error(error)

    def add_widget(self, widget_class, name, *args, **kwargs):
        if name in self._names:
            raise ValueError("form already has '%s' widget" % name)
        if self.prefix is not None:
            widget_name = subname(self.prefix, name)
        else:
            widget_name = name
        widget = widget_class(widget_name, *args, **kwargs)
        self._names[name] = widget

        self.widgets.append(widget)

        # Ensure the new widget appears in the fields of the request
        request = get_request()
        for w in self:
            if w.is_submitted(request=request):
                fields = request.get_fields()
                for sub_w in widget:
                    fields.setdefault(sub_w.get_name(), '')
                break

        return widget

    def add(self, widget_class, name, *args, **kwargs):
        widget = self.add_widget(widget_class, name, *args, **kwargs)
        if isinstance(widget, SubmitWidget):
            self.widgets.remove(widget)
            self.submit_widgets.append(widget)
        elif isinstance(widget, HiddenWidget):
            self.widgets.remove(widget)
            self.hidden_widgets.append(widget)
        if isinstance(widget, FileWidget):
            assert self.enctype == "multipart/form-data"


    def add_submit(self, name, value=None, **kwargs):
        self.add(SubmitWidget, name, value, **kwargs)

    def add_reset(self, name, value=None, **kwargs):
        self.add(ResetWidget, name, value, **kwargs)

    def add_hidden(self, name, value=None, **kwargs):
        self.add(HiddenWidget, name, value, **kwargs)

    def add_string(self, name, value=None, **kwargs):
        self.add(StringWidget, name, value, **kwargs)

    def add_text(self, name, value=None, **kwargs):
        self.add(TextWidget, name, value, **kwargs)

    def add_password(self, name, value=None, **kwargs):
        self.add(PasswordWidget, name, value, **kwargs)

    def add_checkbox(self, name, value=None, **kwargs):
        self.add(CheckboxWidget, name, value, **kwargs)

    def add_single_select(self, name, value=None, **kwargs):
        self.add(SingleSelectWidget, name, value, **kwargs)

    def add_multiple_select(self, name, value=None, **kwargs):
        self.add(MultipleSelectWidget, name, value, **kwargs)

    def add_radiobuttons(self, name, value=None, **kwargs):
        self.add(RadiobuttonsWidget, name, value, **kwargs)

    def add_float(self, name, value=None, **kwargs):
        self.add(FloatWidget, name, value, **kwargs)

    def add_int(self, name, value=None, **kwargs):
        self.add(IntWidget, name, value, **kwargs)

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

    def render:xml(self):
        self._render_start()
        self._render_body()
        self._render_finish()

    def _render_start:xml(self):
        '\n'
        htmltag('form', method=self.method,
                enctype=self.enctype, action=self.action or "",
                **self.attrs)
        '\n<div class="qpform">'
        self._render_hidden_widgets()

    def _render_finish:xml(self):
        '\n<div class="close"></div></div></form>\n'
        code = get_javascript_code()
        if code:
            self._render_javascript(code)
            '\n'

    def _render_widgets:xml(self):
        for widget in self.widgets:
            '\n'
            widget.render()

    def _render_hidden_widgets:xml(self):
        for widget in self.hidden_widgets:
            '\n'
            widget.render()

    def _render_submit_widgets:xml(self):
        if self.submit_widgets:
            '\n<div class="submit">'
            for widget in self.submit_widgets:
                '\n'
                widget.render()
            '\n<div class="close"></div></div>\n'

    def _render_error_notice(self):
        token_widget = self.get_widget(self.TOKEN_NAME)
        if token_widget is not None and token_widget.has_error():
            # form tokens are enabled but the token data in the request
            # does not match anything in the session.  It could be an
            # a cross-site attack but most likely the back button has
            # been used
            return self.TOKEN_NOTICE
        else:
            return self.ERROR_NOTICE

    def _render_javascript(self, javascript_code):
        """Render javacript code for the form.  Insert code lexically
        sorted by code_id.
        """
        form_code = []
        code_ids = sorted(javascript_code.keys())
        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 self.JAVASCRIPT_MARKUP % xml(''.join(form_code))
        else:
            return ''

    def _render_body:xml(self):
        if self.has_errors():
            self._render_error_notice()
        self._render_widgets()
        self._render_submit_widgets()
