/* TODO


   - submit attrs on buttons
       - action / method / enctype / replace / target / novalidate
  - after_submit:
        - 204 NO CONTENT : leave doc, apply metadata
        - 205 RESET CONTENT : reset form
        - replace="document" (new page)
        - replace="values" (fill form with new tree)
        - relace element
        - others ?
        - "onreceive" event (response after submit)

  - check prototype.js serialize on multivalues
*/

GvaScript.Form = Class.create();

GvaScript.Form.Methods = {

  to_hash: function(form) {
    form = $(form);

    return form.serialize({hash:true});
  },

  to_tree: function(form) {
    form = $(form);

    return Hash.expand(GvaScript.Form.to_hash(form));
  },

  fill_from_tree : (function() {

    var doc = document; // local variable is faster than global 'document'

    // IMPLEMENTATION NOTE : Form.Element.setValue() is quite similar,
    // but our treatment of arrays is different, so we have to reimplement
    var _fill_from_value = function(form, elem, val, is_init) {

      // force val into an array
      if (!(val instanceof Array)) val = [val];


      var old_value = null; // needed for value:change custom event
      var new_value = null;

      switch (elem.type) {

        case "text" :
        case "textarea" :
        case "hidden" :
          old_value  = elem.value;
          elem.value = new_value = val.join(",");
        break;

        case "checkbox" :
        case "radio":
          var elem_val  = elem.value;
          old_value = elem.checked ? elem_val : null;

          // hand-crafted loop through val array (because val.include() is too slow)
          elem.checked = false;
          for (var count = val.length; count--;) {
            if (val[count] == elem_val) {
                elem.checked = true;
                break;
            }
          }
          new_value = elem.checked ? elem_val : null;
        break;

        case "select-one" :
        case "select-multiple" :
          var options = elem.options;
          var old_values = [],
              new_values = [];
          for (var i=0, len=options.length; i<len; i++) {
            var opt = options[i];
            var opt_value = opt.value || opt.text;
            if (opt.selected) old_values.push(opt_value);
            // hand-crafted loop through val array (because val.include() is too slow
            opt.selected = false;
            for (var count = val.length; count--;) {
              if (val[count] == opt_value) {
                new_values.push(opt_value);
                opt.selected = true;
                break;
              }
            }
          }
          old_value = old_values.join(",");
          new_value = new_values.join(",");
        break;

        default:
          // if no element type, might be a node list
          var elem_length = elem.length;
          if (elem_length !== undefined) {
            for (var i=0; i < elem_length; i++) {
              _fill_from_value(form, elem.item(i), val, is_init);
            }
          }
          else
            throw new Error("unexpected elem type : " + elem.type);
        break;
      } // end switch

      // if initializing form
      //   and form has an init handler registered to its inputs
      //   and elem has a new_value set
      // => fire the custom 'value:init' event
      if (is_init) {
        if (form.has_init_registered)
          if (new_value)
            Element.fire(elem, 'value:init', {newvalue: new_value}); 
      }
      else {
        if (new_value != old_value)
          Element.fire(elem, 'value:change', {oldvalue: old_value, newvalue: new_value});
      }
    }

    var _fill_from_array = function (form, field_prefix, array, is_init) {
      for (var i=0, len=array.length; i < len; i++) {
        var new_prefix = field_prefix + "." + i;

        // if form has a corresponding named element, fill it
        var elem = form[new_prefix];
        if (elem) {
          _fill_from_value(form, elem, array[i], is_init);
          continue;
        }

        // otherwise try to walk down to a repetition block

        // try to find an existing repetition block
        elem = doc.getElementById(new_prefix);  // TODO : check: is elem in form ?

        // no repetition block found, try to instanciate one
        if (!elem) {
          var placeholder = doc.getElementById(field_prefix + ".placeholder");
          if (placeholder && placeholder.repeat) {
            GvaScript.Repeat.add(placeholder, i + 1 - placeholder.repeat.count);
            elem = doc.getElementById(new_prefix);
          }
        }
        // recurse to the repetition block

        // mremlawi: sometimes multi-value fields are filled without
        // passing by the repeat moduleearly
        // -> no id's on repeatable blocks are set but need to recurse anyway
//         if (elem)
        GvaScript.Form.fill_from_tree(form, new_prefix, array[i], is_init);
      }
    }

    function fill_from_tree(form, field_prefix, tree, is_init)  {
      if (Object.isString(form)) form = $(form);

      for (var key in tree) {
        if (!tree.hasOwnProperty(key)) continue;

        var val = tree[key];
        var new_prefix = field_prefix ? field_prefix+'.'+key : key;

        switch (typeof(val)) {
          case "boolean" :
              val = val ? "true" : "";
              // NO break here

          case "string":
          case "number":
              var elem = form[new_prefix];
              if (elem)
                _fill_from_value(form, elem, val, is_init);
              break;

          case "object":
              if (val instanceof Array) {
                var elem = form[new_prefix];
                // value is an array but to be filled
                // in one form element =>
                // join array into one value using multival separator
                if (elem)
                  _fill_from_value(
                    form, elem, val.join(GvaScript.Forms.multival_sep), is_init
                  );
                else
                  _fill_from_array(form, new_prefix, val, is_init);
              }
              else
                this.fill_from_tree(form, new_prefix, val, is_init);
              break;

          case "function":
          case "undefined":
              // do nothing
        }
      }
    }
    return fill_from_tree;
  })(),

  autofocus: function(container) {

    if (Object.isString(container)) 
      container = document.getElementById(container);

    // replace prototype's down selector
    // as it performs slowly on IE6
    var _find_autofocus = function(p_node) {
      var _kids = p_node.childNodes;

      for(var _idx = 0, len = _kids.length; _idx < len; ) {
        _kid = _kids[_idx ++];

        if(_kid.nodeType == 1) {
          if(Element.hasAttribute(_kid, 'autofocus')) {
            return _kid;
          }
          else {
            var _look_in_descendants = _find_autofocus(_kid);
            if(_look_in_descendants) return _look_in_descendants;
          }
        }
      }
    }

    if(container) {
      //slow on IE6
      //var target = container.down('[autofocus]');
      var target = _find_autofocus(container);
      // TODO : check if target is visible
      if (target) try {target.activate()}
                  catch(e){}
    }
  },

  /**
    * wrapper around Element.register method.
    * method wrapped for special handling of form inputs
    * 'change' and 'init' events
    *
    * all handlers will receive 'event' object as a first argument.
    * 'change' handler will also receive input's oldvalue/newvalue as
    * second and third arguments respectively.
    * 'init' handler will also receive input's newvalue as a
    * second argument.
    *
    * @param {string} query : css selector to match elements
    *                         to watch
    * @param {string} eventname : standard event name that can be triggered
    *                             by form inputs + the custom 'init' event
    *                             that is triggerd on form initialization
    * @param {Function} handler : function to execute.
    *
    * @return undefined
    */
  register: function(form, query, eventname, handler) {
      form = $(form);

      switch(eventname) {
        // change event doesnot bubble in IE
        // rely on blur event to check for change
        // and fire value:change event
        case 'change':
          form.register(query, 'focus', function(event) {
              var elt = event._target;
              elt.store('value', elt.getValue());
          });

          form.register(query, 'blur', function(event) {
              var elt      = event._target;
              var oldvalue = elt.retrieve('value');
              var newvalue = elt.getValue();

              if(oldvalue != newvalue) {
                  elt.fire('value:change', {
                      oldvalue : oldvalue,
                      newvalue : newvalue,
                      handler  : handler
                  });
                  elt.store('value', newvalue);
              }
          });
        break;

        // value:init fired by GvaScript.Form.fill_from_tree method
        // used in formElt initialization
        case 'init':
          // set a flag here in order to fire the 
          // value:init custom event while initializing
          // the form 
          form.has_init_registered = true;

          form.register(query, 'value:init', function(event) {
              handler(event, event.memo.newvalue);
          });
        break;

        default:
          form.register(query, eventname, handler);
        break;
      }
  },

  /**
    * wrapper around Element.unregister method.
    * method wrapped for special handling of form inputs
    * 'change' and 'init' events
    *
    * remove handler attached to eventname for inputs that match query
    *
    * @param {string} query : css selector to remove handlers from
    * @param {string} eventname : eventname to stop observing
    * @param {Funtion} handler : handler to stop firing oneventname
    *                            NOTE: should be identical to what was used in
    *                            register method.
    *                            {optional} : if not specified, will remove all
    *                            handlers attached to eventname for indicated selector
    * @return undefined
    */
  unregister: function(form, query, eventname, handler) {
    form = $(form);

    switch(eventname) {
      case 'change' :
        form.unregister(query, 'focus', handler);
        form.unregister(query, 'blur',  handler);
      break;
      default :
        form.unregister(query, eventname, handler);
      break;
    }
  }
}

Object.extend(GvaScript.Form.prototype, function() {
    // private method to initialize and add actions
    // to form's actions bar
    function _addActionButtons(form) {
        var _actionsbar = $H(form.options.actionsbar);
        if(_actions_container = _actionsbar.get('container')) {
            _actions_container = $(_actions_container);
            _actions_list = _actionsbar.get('actions') || [];

            form.actionsbar = new GvaScript.CustomButtons.ActionsBar(_actions_container, {
                selectfirst: _actionsbar.get('selectfirst') ,
                actions: _actions_list
            });
        }
    }

    return {
        formElt: null,
        actionsbar: null,
        initialize: function(formElt, options) {
            this.formElt = $(formElt);

            var defaults = {
                datatree: {},                               // data object to init form with
                dataprefix: '',                             // data prefix used on form elements


                actionsbar: {},                             // form actions
                registry: [],                               // list of [elements_selector, event_name, event_handler]

                skipAutofocus : false,

                onInit           : Prototype.emptyFunction,  // called after form initialization

                onRepeatBlockRemove : Prototype.emptyFunction,  // called when a repeatable block gets removed
                onRepeatBlockAdd    : Prototype.emptyFunction,  // called when a repeatable block gets added

                onChange         : Prototype.emptyFunction,  // called if any input/textarea value change
                onBeforeSubmit   : Prototype.emptyFunction,  // called right after form.submit
                onSubmit         : Prototype.emptyFunction,  // form submit handler
                onBeforeDestroy  : Prototype.emptyFunction   // called right before form.destroy
            }

            this.options = Object.extend(defaults, options || {});

            // attaching submitMethod to form.onsubmit event
            this.formElt.observe('submit', function() {
                // submit method only called if
                // onBeforeSubmit handler doesnot return false
                if ( this.fire('BeforeSubmit') ) return this.fire('Submit');
            }.bind(this));

            // initializing watchers
            $A(this.options.registry).each(function(w) {
                this.register(w[0], w[1], w[2]);
            }, this);

            var that = this;
            // workaround as change event doesnot bubble in IE
            this.formElt.observe('value:change', function(event) {
                if(event.memo.handler) {
                    event.memo.handler(event,
                                       event.memo.newvalue,
                                       event.memo.oldvalue
                    );
                    // fire the onChange event passing the event
                    // object as an arguement
                    that.fire('Change', event);
                }
                else {
                    if(Prototype.Browser.IE) {
                        var evt = document.createEventObject();
                        event.target.fireEvent('onblur', evt)
                    }
                    else {
                        var evt = document.createEvent("HTMLEvents");
                        evt.initEvent('blur', true, true); // event type,bubbling,cancelable
                        event.target.dispatchEvent(evt);
                    }
                }
            });

            // initializing form actions
            _addActionButtons(this);

            // registering change event to support the onChange event
            this.register('input,textarea','change', Prototype.emptyFunction);

            // initializing for with data
            GvaScript.Form.init(this.formElt,
                                this.options.datatree,
                                this.options.dataprefix,
                                this.options.skipAutofocus);

            // declaring form as a widget
            this.formElt.store('widget', this);
            this.formElt.addClassName(CSSPREFIX()+'-widget');

            // register the instance
            GvaScript.Forms.register(this);

            // call onInit handler
            this.fire('Init');
        },

        // returns id of the form
        getId: function() {
            return this.formElt.identify();
        },

        // use to submit the for programatically
        // since the form.submit() doesnot fire the
        // onsubmit event. doh!
        submitForm: function() {
          // submit method only called if
          // onBeforeSubmit handler doesnot return false
          if ( this.fire('BeforeSubmit') ) return this.fire('Submit');
        },

        /**
         * fire the eventName (ex: 'XYZ') on the form instance.
         * basic events supported are: Init, Change, BeforeSubmit, Submit
         *
         * will first dispatch EarlyResponders defined in GvaScript.Form.EarlyResponders,
         * if none returned false, will continue to fire the callback defined on this Form instance.
         * if callback doesnot return false, will continue to dispatch Responders
         * defined in GvaScript.Form.Responders
         *
         * @param {string} eventName : eventName to fire without the 'on' prefix
         * @param {object} arg : argument to carry over to handler.
         *
         * @return boolean indicating whether all responders + instance callback have succeeded (if any)
         */
        fire: function(eventName, arg) {
            var callback_ok = true;

            // -- early responders
            callback_ok = GvaScript.Form.EarlyResponders.dispatch('on'+eventName, this, arg);
            if(callback_ok === false) return false;

            // -- instance callback
            if( Object.isFunction(this.options['on'+eventName]) ) {
              callback_ok = this.options['on'+eventName](this, arg);
              if(callback_ok === false) return false;
            }

            // -- late responders
            callback_ok = GvaScript.Form.Responders.dispatch('on'+eventName, this, arg)

            return (callback_ok !== false);
        },

        // instance destructor
        destroy: function() {
            if( this.fire('BeforeDestroy') ) {
              GvaScript.Forms.unregister(this);

              if(this.actionsbar) this.actionsbar.destroy();

              this.formElt.stopObserving();
              this.formElt.unregister();
            }
        }
    }
}());

/**
 * GvaScript.Forms :
 * - holds references to all GvaScript.Form instances indentified
 *   by the instance.getId() method.
 *   handy to get GvaScript.Form instance based on the form id.
 *
 * - holds general observers to be executed on all GvaScript.Form
 *   instances
 */

GvaScript.Form.EarlyResponders = {
  responders: [],

  _each: function(iterator) {
    this.responders._each(iterator);
  },

  register: function(responder) {
    if (!this.include(responder))
      this.responders.push(responder);
  },

  unregister: function(responder) {
    this.responders = this.responders.without(responder);
  },

  dispatch: function(eventName, form, arg) {
      var falsy_observer = this.any(function(responder) {
          if(Object.isFunction(responder[eventName])) {
              return (responder[eventName](form, arg) === false ? true : false);
          }
      });
      return !falsy_observer;
  }
}
Object.extend(GvaScript.Form.EarlyResponders, Enumerable);

GvaScript.Form.Responders = {
  responders: [],

  _each: function(iterator) {
    this.responders._each(iterator);
  },

  register: function(responder) {
    if (!this.include(responder))
      this.responders.push(responder);
  },

  unregister: function(responder) {
    this.responders = this.responders.without(responder);
  },

  dispatch: function(eventName, form, arg) {
      var falsy_observer = this.any(function(responder) {
          if(Object.isFunction(responder[eventName])) {
              return (responder[eventName](form, arg) === false ? true : false);
          }
      });
      return !falsy_observer;
  }
}
Object.extend(GvaScript.Form.Responders, Enumerable);

GvaScript.Forms = {
    multival_sep: '\n', // separator used to join array into one value
    forms: $A(),

    register: function(form) {
      this.unregister(form);
      this.forms.push(form);
    },

    unregister: function(form) {
      // nothing to unregister
      if(!form) return;

      if(typeof form == 'string') form = this.get(form);

      // nothing to unregister
      if(!form) return false;

      // remove the reference from array
      this.forms = this.forms.reject(function(f) { return f.getId() == form.getId() });

      return true;
    },

    get: function(id) {
      return this.forms.find(function(f) {return f.getId() == id});
    }
}

// GvaScript.Form helpers and methods
Object.extend(GvaScript.Form, GvaScript.Form.Methods);
Object.extend(GvaScript.Form, {
  init: function(form, tree, field_prefix, skipAutofocus) {
    form = $(form);

    GvaScript.Repeat.init(form);
    GvaScript.Form.fill_from_tree(form,
                                  field_prefix || "",
                                  tree || {},
                                  true);

    if (!skipAutofocus)
      GvaScript.Form.autofocus(form);
  },

  add: function(repeat_name, count) {
    var n_blocks = GvaScript.Repeat.add(repeat_name, count);
    var last_block = repeat_name + "." + (n_blocks - 1);
    GvaScript.Form.autofocus(last_block);

    // get form owner of block
    if(_block = $(last_block)) {
      _form = _block.up('form');
      // check if form has a GvaSCript.Form instance
      // wrapped around it
      if(_form) {
        if(_gva_form = GvaScript.Forms.get(_form.identify())) {
          _gva_form.fire('RepeatBlockAdd', [repeat_name.split('.').last(), last_block]);
          _gva_form.fire('Change');
        }
      }
    }
    return n_blocks;
  },

  remove: function(repetition_block, live_update) {
    // default behavior to live update all blocks below
    // the removed block
    if(typeof live_update == 'undefined') live_update = true;

    // find element and repeat info
    var elem = $(repetition_block);
    elem.id.match(/(.*)\.(\d+)$/);
    var repeat_name = RegExp.$1;
    var remove_ix   = RegExp.$2;
    var form        = elem.up('form');
    var tree        = {}; // form deserialized as a tree
                          // only relevant if live_update

    // need to update the data for blocks below
    // as they have been reproduced
    if(live_update) {
      // get form data corresponding to the repeated section (should be an array)
      tree  = GvaScript.Form.to_tree(form);

      var parts = repeat_name.split(/\./);
      for (var i = 0, len=parts.length ; i < len; i++) {
        if (!tree) break;
        tree = tree[parts[i]];
      }

      // remove rows below, and shift rows above
      if (tree && tree instanceof Array) {
        tree.splice(remove_ix, 1);
        for (var i = 0 ; i < remove_ix; i++) {
          delete tree[i];
        }
      }
    }

    // call Repeat.remove() to remove from DOM
    // and if live_update, to remove and reproduce
    // the blocks below with correct renumerations
    GvaScript.Repeat.remove(repetition_block, live_update);

    // after form tree has been updated
    // and dom re-populated
    if(live_update) {
      // re-populate blocks below
      GvaScript.Form.fill_from_tree(form, repeat_name, tree);
    }

    // check if form has a GvaSCript.Form instance
    // wrapped around it
    if(_gva_form = GvaScript.Forms.get(form.identify())) {
      _gva_form.fire('RepeatBlockRemove', [repeat_name.split('.').last(), repeat_name + '.' + remove_ix]);
      _gva_form.fire('Change');
    }
  }
});

// copy GvaScript.Form methods into GvaScript.Form.prototype
// set the first argument of methods to this.formElt
(function() {
  var update = function (array, args) {
    var arrayLength = array.length, length = args.length;
    while (length--) array[arrayLength + length] = args[length];
    return array;
  }

  for(var m_name in GvaScript.Form.Methods) {
    var method = GvaScript.Form.Methods[m_name];
    if (Object.isFunction(method)) {
      GvaScript.Form.prototype[m_name] = (function() {
        var __method = method;
        return function() {
          var a = update([this.formElt], arguments);
          return __method.apply(null, a);
        }
      })();
    }
  }
})();