"""
Form implementation and high-level renderers.
"""

import weakref

from zope.interface import Interface
from twisted.internet import defer
from twisted.python.components import registerAdapter
from twisted.web import iweb
from twisted.web import resource
from twisted.web import server
from twisted.web import template
from twisted.web.template import tags as T
from zope.interface import implementer

from . import iformal, util, validation, nevowc


FORMS_KEY = b'__nevow_form__'


class Action(object):
    """Tracks an action that has been added to a form.
    """
    def __init__(self, callback, name, validate, label):

        if not util.validIdentifier(name):
            import warnings
            warnings.warn('[0.9] Invalid action name %r. This will become an error in the future.' %
                    name, FutureWarning, stacklevel=3)

        self.callback = callback
        self.name = name
        self.validate = validate
        if label is None:
            self.label = util.titleFromName(name)
        else:
            self.label = label



def itemKey(item):
    """
    Build the form item's key.  This currently always is the item name.
    """
    return item.name

# The original formal code included ancestor names.  We don't
# want this in DaCHS since our parameter names may be important (e.g.
# in VO protocols we're funneling through the formal parsers).




class Field(object):


    itemParent = None


    def __init__(self, name, type, widgetFactory=None, label=None,
            description=None, cssClass=None, form=None, default=None):
        if not util.validIdentifier(name):
            raise ValueError('%r is an invalid field name'%name)
        if label is None:
            label = util.titleFromName(name)
        if widgetFactory is None:
            widgetFactory = iformal.IWidget
        self.name = name
        self.type = type
        self.widgetFactory = widgetFactory
        self.label = label
        self.description = description
        self.cssClass = cssClass
        self.default = default
        # form can already be a weakproxy if we're a child of a group or so
        if form is None:
            # for testing only
            self.form = None
        elif isinstance(form, weakref.ProxyType):
            self.form = form
        else:
            self.form = weakref.proxy(form)


    def setItemParent(self, itemParent):
        self.itemParent = itemParent


    key = property(lambda self: itemKey(self))


    def makeWidget(self):
        return self.widgetFactory(self.type)


    def process(self, request, form, args, errors):

        # If the type is immutable then copy the original value to args in case
        # another validation error causes this field to be re-rendered.
        if self.type.immutable:
            args[self.key] = form.data.get(self.key)
            return

        # Process the input using the widget, storing the data back on the form.
        try:
            if self.default is not None:
                form.data[self.key] = self.makeWidget(
                ).processInput(request, self.key, args, self.default)
            else:
                form.data[self.key] = self.makeWidget(
                ).processInput(request, self.key, args)
        except validation.FieldError as e:
            if e.fieldName is None:
                e.fieldName = self.key
            errors.add(e)



@implementer(iweb.IRenderable)
class FieldFragment(nevowc.CommonRenderers, template.Element):


    loader = template.TagLoader(
        T.div(id=template.slot('fieldId'), class_=template.slot('cls'),
                render='field')[
            T.label(class_='label', for_=template.slot('id'))[template.slot('label')],
            T.div(class_='inputs')[template.slot('inputs')],
            template.slot('description'),
            template.slot('message'),
            ])


    hiddenLoader = template.TagLoader(
            T.transparent(render='field')[template.slot('inputs')])


    def __init__(self, field):
        self.fieldInstance = field
        # Nasty hack to work out if this is a hidden field. Keep the widget
        # for later anyway.
        self.widget = field.makeWidget()
        if getattr(self.widget, 'inputType', None) == 'hidden':
            self.loader = self.hiddenLoader


    @template.renderer
    def field(self, request, tag):

        # The field we're rendering
        field = self.fieldInstance

        formData = self.fieldInstance.form.data
        formErrors = self.fieldInstance.form.errors

        # Find any error
        if formErrors:
            error = formErrors.getFieldError(field.key)
            # field.render wants almost unprocessed requests.args if
            # there was an error.
            formData = util.CaseSemisensitiveDict(
                [(k.decode("utf-8", "ignore"),v)
                for k,v in request.args.items()])
        else:
            error = None

        # Build the error message
        if error is None:
            message = ''
        else:
            message = T.div(class_='message')[error.message]

        # Create the widget (it's created in __init__ as a hack)
        widget = self.widget

        # Build the list of CSS classes
        classes = [
            'field',
            field.type.__class__.__name__.lower(),
            widget.__class__.__name__.lower(),
            ]
        if field.type.required:
            classes.append('required')
        if field.cssClass:
            classes.append(field.cssClass)
        if error:
            classes.append('error')

        # Create the widget and decide the method that should be called
        if field.type.immutable:
            render = widget.renderImmutable
        else:
            render = widget.render

        # Fill the slots
        tag.slotData = {}
        tag.fillSlots(id=util.render_cssid(field.key),
            fieldId=[util.render_cssid(field.key), '-field'],
            cls=' '.join(classes),
            label=field.label,
            inputs=render(request, field.key, formData,
                formErrors),
            message=message,
            description=T.div(class_='description')[field.description or ''])

        return tag(render="mapping")


registerAdapter(FieldFragment, Field, iweb.IRenderable)



class AddHelperMixin(object):
    """
    A mixin that provides methods for common uses of add(...).
    """



    def __getitem__(self, items):
        """
        Overridden to allow stan-style construction of forms.
        """
        # Items may be a list or a scalar so stick a scalar into a list
        # immediately to simplify the code.
        try:
            items = iter(items)
        except TypeError:
            items = [items]
        # Add each item
        for item in items:
            self.add(item)
        # Return myself
        return self


class Group(object):


    itemParent = None


    def __init__(self, name, label=None, description=None, cssClass=None,
            form=None):
        if label is None:
            label = util.titleFromName(name)
        self.name = name
        self.label = label
        self.description = description
        self.cssClass = cssClass
        self.items = FormItems(self)
        # Forward to FormItems methods
        self.add = self.items.add
        self.getItemByName = self.items.getItemByName
        self.form = weakref.proxy(form)


    key = property(lambda self: itemKey(self))


    def addGroup(self, *a, **k):
        return self.add(Group(*a, form=self.form, **k))


    def addField(self, *a, **k):
        return self.add(Field(*a, form=self.form, **k))


    def setItemParent(self, itemParent):
        self.itemParent = itemParent


    def process(self, request, form, args, errors):
        for item in self.items:
            item.process(request, form, args, errors)



class GroupFragment(template.Element):


    loader = template.TagLoader(
            T.fieldset(id=template.slot('id'), class_=template.slot('cssClass'),
                    render='_group')[
                T.legend[template.slot('label')],
                T.div(class_='description')[template.slot('description')],
                template.slot('items'),
                ]
            )


    def __init__(self, group):
        super(GroupFragment, self).__init__()
        self.group = group


    @template.renderer
    def _group(self, request, tag):

        # Get a reference to the group, for simpler code.
        group = self.group

        # Build the CSS class string
        cssClass = ['group']
        if group.cssClass is not None:
            cssClass.append(group.cssClass)
        cssClass = ' '.join(cssClass)

        # Fill the slots
        tag.fillSlots(
            id=util.render_cssid(group.key),
            cssClass=cssClass,
            label=group.label,
            description=group.description or '',
            items=[iweb.IRenderable(item) for item in
                group.items])
        return tag



registerAdapter(GroupFragment, Group, iweb.IRenderable)



@implementer( iformal.IForm )
class Form(AddHelperMixin, object):

    callback = None
    actions = None

    def __init__(self, callback=None):
        if callback is not None:
            self.callback = callback
        self.data = {}
        self.items = FormItems(None)
        self.errors = FormErrors()
        # Forward to FormItems methods
        self.add = self.items.add
        self.getItemByName = self.items.getItemByName
        self.actionMaterial = None


    def addField(self, *a, **k):
        return self.add(Field(*a, form=self, **k))


    def addGroup(self, *a, **k):
        return self.add(Group(*a, form=self, **k))


    def addAction(self, callback, name="submit", validate=True, label=None):
        if self.actions is None:
            self.actions = []
        if name in [action.name for action in self.actions]:
            raise ValueError('Action with name %r already exists.' % name)
        self.actions.append( Action(callback, name, validate, label) )

    def process(self, request):

        charset = 'utf-8'

        # Get the request args and decode the arg names
        args = util.CaseSemisensitiveDict(
            [(k.decode(charset),v) for k,v in request.args.items()])

        # Find the callback to use, defaulting to the form default
        callback, validate = self.callback, True
        if self.actions is not None:
            for action in self.actions:
                if action.name in args:
                    # Remove it from the data
                    args.pop(action.name)
                    # Remember the callback and whether to validate
                    callback, validate = action.callback, action.validate
                    break

        # IE does not send a button name in the POST args for forms containing
        # a single field when the user presses <enter> to submit the form. If
        # we only have one possible action then we can safely assume that's the
        # action to take.
        #
        # If there are 0 or 2+ actions then we can't assume anything because we
        # have no idea what order the buttons are on the page (someone might
        # have altered the DOM using JavaScript for instance). In that case
        # throw an error and make it a problem for the developer.
        if callback is None:
            if self.actions is None or len(self.actions) != 1:
                raise Exception('The form has no callback and no action was found.')
            else:
                callback, validate = self.actions[0].callback, \
                        self.actions[0].validate

        # Remember the args in case validation fails.
        self.errors.data = args

        # Iterate the items and collect the form data and/or errors.
        for item in self.items:
            item.process(request, self, args, self.errors)

        if self.errors and validate:
            return self.errors

        d = defer.maybeDeferred(callback, request, self, self.data)
        d.addErrback(self._cbFormProcessingFailed, request)
        return d

    def _cbFormProcessingFailed(self, failure, request):
        failure.trap(validation.FormError, validation.FieldError)
        self.errors.add(failure.value)
        return self.errors



class FormItems(object):
    """
    A managed collection of form items.
    """


    def __init__(self, itemParent):
        self.items = []
        self.itemParent = itemParent


    def __iter__(self):
        return iter(self.items)


    def add(self, item):
        # Check the item name is unique
        if item.name in [i.name for i in self.items]:
            raise ValueError('Item named %r already added to %r' %
                    (item.name, self))
        # Add to child items and set self the parent
        self.items.append(item)
        item.setItemParent(self.itemParent)
        return item


    def getItemByName(self, name):
        # since we have flat names in DaCHS, we need to look
        # into each subordinate container.  Original formal
        # had hierarchical names for that.
        for item in self.items:
            if item.name==name:
                return item
            try:
                return item.getItemByName(name)
            except (AttributeError, KeyError):
                # child either is no container or doesn't have the item
                pass
        raise KeyError("No item called %r" % name)


@implementer( iformal.IFormErrors )
class FormErrors(object):

    def __init__(self):
        self.errors = []

    def add(self, error):
        self.errors.append(error)

    def getFieldError(self, name):
        fieldErrors = [e for e in self.errors if isinstance(e, validation.FieldError)]
        for error in fieldErrors:
            if error.fieldName == name:
                return error

    def getFormErrors(self):
        return self.errors

    def __bool__(self):
        return len(self.errors) != 0


class FormsResourceBehaviour(object):
    """
    I provide the IResource behaviour needed to process and render a page
    containing a Form.
    """
    def __init__(self, **k):
        parent = k.pop('parent')
        super(FormsResourceBehaviour, self).__init__(**k)
        self.parent = parent
        self.forms = {}

    def runAction(self, request, formName):
        form = self.locateForm(request, formName)
        return self._processForm(form, request)

    @template.renderer
    def form(self, name):
        def render(request, tag):
            form = self.locateForm(request, name)

            # put in the proper defaults from request arguments
            # so people can bookmark forms; we're not interested
            # in validation problems for them, so errors is
            # just something with an add method.
            ignoredErrors = set()
            args = util.CaseSemisensitiveDict((k.decode("utf-8"), v)
                for k,v in request.args.items())

            for key, value in args.items():
                try:
                    form.getItemByName(key
                        ).process(
                            request, form, args, ignoredErrors)
                except Exception:
                    # don't fail on extra or bad input
                    pass

            # Create a keyed tag that will render the form when flattened.
            tag = T.transparent(key=name)[iweb.IRenderable(form)]
            return tag

        return render

    def _processForm(self, form, request):
        d = defer.succeed(request)
        d.addCallback(form.process)
        return d

    def locateForm(self, request, name):
        """Locate a form by name.

        Initially, forms are located by looking for a form_<name>
        attribute in our parent.  Once a form has been found, we cache
        it in request.

        This ensures that the form that is located during form processing
        will be the same instance that is located when a form is rendered
        after validation failure.
        """
        if not hasattr(request, "formal_forms"):
            request.formal_forms = {}
        form = request.formal_forms.get(name)
        if form is not None:
            return form

        factory = self.parent
        form = factory.formFactory(request, name)
        if form is None:
            raise Exception('Form %r not found'%name)
        form.name = name
        request.formal_forms[name] = form
        return form


class ResourceWithForm(nevowc.TemplatedPage):
    """A t.w Resource with a template that has one or more forms.

    To handle serious errors occurring during form processing,
    override the crash(failure, request) method.  More benign
    errors are handled through form errors are and being rendered
    into the normal form.

    By default, GET requests do not run actions.  If your
    actions don't change state, you should be ok with setting
    a class variable processOnGET, though.
    """
# You'll probably want to override the crash(failure, request) method...

    __formsBehaviour = None

    processOnGET = False

    def __behaviour(self):
        if self.__formsBehaviour is None:
            self.__formsBehaviour = FormsResourceBehaviour(parent=self)
        return self.__formsBehaviour

    def render(self, request, customCallback=None):
        def gotResult(result):
            if isinstance(result, resource.Resource):
                res = result.render(request)
                if res==server.NOT_DONE_YET:
                    return res
                else:
                    request.finish()
                    return server.NOT_DONE_YET
            else:
                return super(ResourceWithForm, self).render_POST(request)

        formName = request.args.pop(FORMS_KEY, [b""])[0].decode("utf-8")

        if formName and (request.method==b"POST" or self.processOnGET):
            d = defer.maybeDeferred(
                self.__behaviour().runAction, request, formName)
            d.addCallback(customCallback or gotResult)
            d.addErrback(self.crash, request)
        else:
            return super(ResourceWithForm, self).render_POST(request)
        return server.NOT_DONE_YET

    def crash(self, failure, request):
        # the following is just for simpler trial operation; comment out
        # in production
#        import sys; failure.printTraceback(file=sys.stdout)
        request.setResponseCode(500)
        request.setHeader("content-type", "text/plain")
        request.write(b"Unhandled exception while handling the form:\n\n")
        request.write((failure.getErrorMessage()+"\n\n").encode("utf-8"))
        request.write(b"You will probably want to complain to the operators.\n")
        request.write(b"If you *are* the operator, override"
            b" the page.crash(failure, request) method.")
        request.finish()
        return server.NOT_DONE_YET

    @template.renderer
    def form(self, name):
        return self.__behaviour().form(name)

    def formFactory(self, request, name):
        factory = getattr(self, 'form_%s'%name, None)
        if factory is not None:
            return factory(request)
        s = super(ResourceWithForm, self)
        if hasattr(s,'formFactory'):
            return s.formFactory(request, name)


class IKnownForms(Interface):
    """Marker interface used to locate a dict instance containing the named
    forms we know about during this request.
    """


@implementer( IKnownForms )
class KnownForms(dict):
    pass


@implementer(iweb.IRenderable)
class FormRenderer(nevowc.CommonRenderers):

    loader = template.TagLoader(
            T.form(**{'id': template.slot('formName'), 'action': template.slot('formAction'),
                'class': 'nevow-form', 'method': 'post', 'enctype':
                'multipart/form-data', 'accept-charset': 'utf-8'})[
            T.div[
                T.input(type='hidden', name='_charset_'),
                T.input(type='hidden', name=FORMS_KEY, value=template.slot('formName')),
                template.slot('formErrors'),
                template.slot('formItems'),
                T.div(class_='actions')[
                    template.slot('formActions'),
                    ],
                ],
            ]
        )

    def __init__(self, original, *a, **k):
        super(FormRenderer, self).__init__(*a, **k)
        self.original = original

    def render(self, request):
        data = self.original.data
        tag = T.transparent[self.loader.load()](render="mapping")
        tag.fillSlots(
            formName=self.original.name,
            formAction=request.path,
            formErrors=self._renderErrors(request, data),
            formItems=self._renderItems(request, data),
            formActions=self._renderActions(request, data))
        return tag

    def _renderErrors(self, request, data):

        if not self.original.errors:
            return ''

        errors = self.original.errors.getFormErrors()

        errorList = T.ul()
        for error in errors:
            if isinstance(error, validation.FormError):
                errorList[ T.li[ error.message ] ]
        for error in errors:
            if isinstance(error, validation.FieldError):
                item = self.original.getItemByName(error.fieldName)
                errorList[ T.li[ T.strong[ item.label, ' : ' ], error.message ] ]
        return T.div(class_='errors')[ T.p['Please correct the following error(s):'], errorList ]

    def _renderItems(self, request, data):
        if self.original.items is None:
            yield ''
            return
        for item in self.original.items:
            yield iweb.IRenderable(item)

    def _renderActions(self, request, data):

        if self.original.actions is None:
            yield ''
            return

        for action in self.original.actions:
            yield self._renderAction(request, action)

        if self.original.actionMaterial:
            yield self.original.actionMaterial


    def _renderAction(self, request, data):
        return T.input(type='submit', id='%s-action-%s'%(self.original.name, data.name), name=data.name, value=data.label)


registerAdapter(FormRenderer, Form, iweb.IRenderable)

# vi:et:sw=4:sta
