# Copyright 2012, 2013 The Apache Software Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http:#www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# ## t5/core/forms
#
# Defines handlers for HTML forms and HTML field elements, specifically to control input validation.

define ["./events", "./dom", "underscore"],
  (events, dom, _) ->

    # Meta-data name that indicates the next submission should skip validation (typically, because
    # the form was submitted by a "cancel" button).
    SKIP_VALIDATION = "t5:skip-validation"

    clearSubmittingHidden = (form) ->
      hidden = form.findFirst "[name='t:submit']"

      # Clear if found
      hidden and hidden.value null

      form.meta SKIP_VALIDATION, null

      return

    setSubmittingHidden = (form, submitter) ->

      mode = submitter.attr "data-submit-mode"
      isCancel = mode is "cancel"
      if mode and mode isnt "normal"
        form.meta SKIP_VALIDATION, true

      hidden = form.findFirst "[name='t:submit']"

      unless hidden
        firstHidden = form.findFirst "input[type=hidden]"
        hidden = dom.create "input", type: "hidden", name: "t:submit"
        firstHidden.insertBefore hidden

      # TODO: Research why we need id and name and get rid of one if possible.
      name = if isCancel then "cancel" else submitter.element.name
      # Not going to drag in all of json2 just for this one purpose, but even
      # so, I'd like to get rid of this. Prototype includes Object.toJSON(), but jQuery
      # is curiously absent an equivalent.
      hidden.value "[\"#{submitter.element.id}\",\"#{name}\"]"

      return

    # Passed the element wrapper for a form element, returns a map of all the values
    # for all non-disabled fields (including hidden fields, select, textarea). This is primarily
    # used when assembling an Ajax request for a form submission.
    gatherParameters = (form) ->
      result = {}

      fields = form.find "input, select, textarea"

      _.each fields, (field) ->
          return if field.attr "disabled"

          type = field.element.type

          # Ignore types file and submit; file doesn't make sense for Ajax, and submit
          # is handled by keeping a hidden field active with the data Tapestry needs
          # on the server.
          return if type is "file" or type is "submit"

          return if (type is "checkbox" or type is "radio") and field.checked() is false

          value = field.value()

          return if value is null

          name = field.element.name

          # Many modern UIs create name-less elements on the fly (e.g., Backbone); these may be mixed
          # in with normal elements managed by Tapestry but should be ignored (not sent to the server in a POST
          # or Ajax update).
          return if name is ""

          existing = result[name]

          if _.isArray existing
            existing.push value
            return

          if existing
            result[name] = [existing, value]
            return

          result[name] = value

      return result


    defaultValidateAndSubmit = ->

      where = -> "processing form submission"

      try

        if ((@attr "data-validate") is "submit") and
           (not @meta SKIP_VALIDATION)

          @meta SKIP_VALIDATION, null

          hasError = false
          focusField = null

          for field in @find "[data-validation]"
            memo = {}
            where = -> "triggering #{events.field.inputValidation} event on #{field.toString()}"
            field.trigger events.field.inputValidation, memo

            if memo.error
              hasError = true
              focusField = field unless focusField

          # Only do form validation if all individual field validation
          # was successful.
          unless hasError
            memo = {}
            where = -> "trigging cross-form validation event"
            @trigger events.form.validate, memo

            hasError = memo.error

          if hasError
            clearSubmittingHidden this

            # If a specific field has been identified as the source of the validation error, then
            # focus on it.
            focusField.focus() if focusField

            # Trigger an event to inform that form validation results in error
            where = -> "triggering validation in error event"
            @trigger events.form.validateInError

            # Cancel the original submit event when there's an error
            return false

        # Allow certain types of elements to do last-moment set up. Basically, this is for
        # FormFragment, or similar, to make their hidden field enabled or disabled to match
        # their UI's visible/hidden status. This is assumed to work or throw an exception; there
        # is no memo.
        where = -> "triggering #{events.form.prepareForSubmit} event (after validation)"

        @trigger events.form.prepareForSubmit

      catch error
        console.error "Form validiation/submit error `#{error.toString()}', in form #{this.toString()}, #{where()}"
        console.error error
        return false

      # Otherwise, the event is good, there are no validation problems, let the normal processing commence.
      # Possibly, the document event handler provided by the t5/core/zone module will intercept form submission if this
      # is an Ajax submission.
      return

    dom.onDocument "submit", "form", defaultValidateAndSubmit

    # On any click on a submit or image, update the containing form to indicate that the element
    # was responsible for the eventual submit; this is very important to Ajax updates, otherwise the
    # information about which control triggered the submit gets lost.
    dom.onDocument "click", "input[type=submit], input[type=image]", ->
      setSubmittingHidden (dom @element.form), this
      return

    # Support for link submits. `data-submit-mode` will be non-null, possibly "cancel".
    # Update the hidden field, but also cancel the default behavior for the click.
    dom.onDocument "click", "a[data-submit-mode]", ->
      form = @findParent "form"

      unless form
        console.error "Submitting link element not contained inside a form element."
        return false

      setSubmittingHidden form, @closest "a[data-submit-mode]"

      # Now the ugly part; if we just invoke submit() on the form, it does not trigger
      # the form's "submit" event, which we need.

      form.trigger "submit"

      # And cancel the default behavior for the original click event
      return false

    exports =
      gatherParameters: gatherParameters

      setSubmittingElement: setSubmittingHidden

      # Sets a flag on the form to indicate that client-side validation should be bypassed.
      # This is typically associated with submit buttons that "cancel" the form.
      skipValidation: (form) ->
        form.meta SKIP_VALIDATION, true
