// SETUP
// 1. Your form must have an ID, which is not included by defult in Rails,
// you can do <% form_for :model, :html => { :id => "model" }, :url...
//
// 2. The first class name for each field must be the name of the ActiveFormField type
// it should be set as. (class = "select" or class = "phone") See below for a list of
// available field types.
//
// 3. Set the requirements for each form. If required, include a class name "required".
// The order doesn't matter as long as it's not first. (class = "text required")
//
// 4. Create an instance of ActiveForm after the page loads. ( var bill = new ActiveForm(form_id); )
// The form id should probably be the same as the instance name.
//
// That's it.
//
// Now in your javascript on the page you can do fun stuff like:
//
// Access and set the field value directly.
// The form ID is removed from the field id to create the attribute/field name.
// user.name.set("Steve") // Sets name as "Steve"
// user.name.get() // => "Steve"
//
// Trigger events
// user.country.after_set = function(value) {
//   if(value = "USA") {
//     $('user_state_div').show();
//   }
// }
//
// REFERENCING
//
// Overview
// VALUE:    form.attribute.get();
// ELEMENT:  form.attribute.element;
//
// NOTES
//
// Choosing to go with the format of form.field.get() instead of
// form.field() and form.fields.field.get() because I'm finding my self using the field functions
// a lot more than I thougth I would.
//
// Changed to extending inputs directly, but realized it creates an issue with value on checkboxes.
// In the old way, the value for checkboxes read from checked or unchecked.
//
// IDEAS
// Aducts tab index for errors, 1 is first error, 2 is second, etc.
// Select text of first error
//
// FUTURE OPTIONS
// Focus first input or not
// Camelize Input IDs
//
// Q&A
// Why didn't I just build everything into the objects themselves?
// I don't know.

// To fix: set form reference to element, not object

var ActiveForm = Class.create({

  initialize: function(id) {
    // Requires an ID on <form>
    this.formElement = $(id);
    this.id = id;

    // Allows this form to be referenced in external functions
    activeForm = this;
    this.fieldNamesArray = new Array();

    // Loops through inputs in the form
    this.formElement.getElements().each(function(input){

      // Make sure the input has an ID
      if(input.id) {

        // First class name should be field type
        fieldType = $w(input.className).first();

        // Make sure the type is known and exists before creating
        if(ActiveFormField[fieldType]) {

          // Strips the form ID from the field ID to make a clean name
          fieldName = input.id.replace(activeForm.id + "_", "");

          // Adding the class "make-hash" allows for grouping fields when there are multiple objects being entered
          // as in home.users[1].first_name, home.users[2].last_name, etc.
          if($(input.id).hasClassName("make-hash")) {

            // Grabs the hash name, index, and hash item
            // Relies on form_name_hash_name_1_hash_item id format
            // An actual ID would look like home_users_1_first_name
            matches = fieldName.match(/([^\d]+)_(\d+)_([^\d]+)/i);
            hashName = matches[1]; hashIndex = matches[2]; hashItem = matches[3];

            // Store hash in list of inputs so we can loop through them later
            if(activeForm.fieldNamesArray.indexOf(hashName) == -1) activeForm.fieldNamesArray.push(hashName);
            // Creates a hash to hold the new set of objects
            if(activeForm[hashName] == undefined) activeForm[hashName] = new Hash;
            // Creates a js object to hold the fields for an object within the set
            if(activeForm[hashName].get(hashIndex) == undefined) activeForm[hashName].set(hashIndex, {});
            // Creates an ActiveFormField object for the field
            // activeField = new ActiveFormField[fieldType](input.id);
            activeForm[hashName].get(hashIndex)[hashItem] = activeForm.extendElement(input, new ActiveFormField[fieldType](input.id));

          } else {
            // Creates an ActiveFormField object for the field
            // activeField = new ActiveFormField[fieldType](input.id);
            // activeForm[fieldName] = input;
            
            activeForm[fieldName] = activeForm.extendElement(input, new ActiveFormField[fieldType](input.id));
            
            // Store field in list of inputs so we can loop through them later
            if(activeForm.fieldNamesArray.indexOf(fieldName) == -1) activeForm.fieldNamesArray.push(fieldName);

          }

          // A radio group allows for radio inputs to be grouped and treated similar to a select
          if(fieldType == "radio") {
            // Use the radio button name instead of ID to create the radio group
            // Since a radio group will share the name attr
            name = input.readAttribute("name");
            if(name) {
              // Create an ActiveFormField form radio
              if(activeForm[name] == undefined) {
                activeForm[name] = new ActiveFormField.radioGroup(name);
              }
              // Add this radio input to the radio group
              activeForm[name].radios.push(activeForm[fieldName]);
            }
          }
        } // end fieldType check
      } // end ID check
    }); // end each

    // Focus on first input
    this.formElement.focusFirstElement();
  }, // end initialize
  
  // Adds all methods and vas to input element itself
  extendElement: function(element, activeFormField) {
    return Object.extend(element, activeFormField);
  }
}); // end ActiveForm

// ActiveForm Field
// Base class for all field classes
// Should not be instantiated itself
var ActiveFormField = {};

ActiveFormField.base = Class.create({

  initialize: function(id) {
    // Store element reference so we can use it before we extend it
    this.element = $(id);
    // Status element holds dynamic errors messages, etc.
    this.statusElement = $(id+"_status");
    // Lable tag for this input
    this.label = $$('label[for="'+id+'"]').first();
    
    this.validations = new Array();
    // Check if required
    this.required = this.element.hasClassName("required");
    this.requiredMessage = "Can't be blank";
    if(this.required) this.addValidation("!this.value", "useRequiredMessage");
    // Message if valid, usually just nothing
    this.defaultValidMessage = "";
    
    
    
    // Store any current event functionality so it doesn't get overwritten
    // Set element to self update onchange
    // If any item besides the last has a return, the rest won't run
    this.onchanges = new Array(function(){ this.selfSet(); }, this.element.onchange);
    this.onfocuses = new Array(this.element.onfocus);
    this.onblurs = new Array(this.element.onblur);
    this.onmouseovers = new Array(this.element.onmouseover);
    this.onmouseouts = new Array(this.element.onmouseout);

    this.errorMessageContainer = "<p class='error message'>MessageHere</p>";
    this.validMessageContainer = "<p class='success message'>MessageHere</p>";
    this.helperMessageContainer = "<p class='helper message'>MessageHere</p>";

    // Add an active class to labels and inputs when their input is in focus
    if(this.label) {
      this.addOnfocus(function(){
        $$('label[for="'+this.id+'"]').first().addClassName("focus");
        this.addClassName("focus");
      });
      this.addOnblur(function(){
        $$('label[for="'+this.id+'"]').first().removeClassName("focus");
        this.removeClassName("focus");
      });
    }
  },

  // Adds all methods and vas to input element itself
  extendElement: function() {
    Object.extend(this.element, this);
  },

  set: function(newValue) {
    this.setElementValue(this.beforeValidate(newValue));
    this.validate();
    this.setElementValue(this.afterValidate(this.get()))
    this.afterSet(this.get());
  },

  get: function() { return this.beforeGet(this.getElementValue()); },

  // For use with onchange
  selfSet: function() { this.set(this.get()); },

  // Allows override for different field value types
  setElementValue: function(value) { this.element.value = value; },
  getElementValue: function() { return this.element.getValue(); },

  // Callbacks
  beforeValidate: function(value) { return value; },
  // beforeSet: function(value) { return value; },
  afterValidate: function(value) { return value; },
  afterSet: function(value) { return value; },

  // Allows for formatting before use elsewhere
  beforeGet: function(value) { return value; },

  // Loop through an array of functions and run them
  // Using eval so the code runs in the scope of the element, so "this" works
  // Not using .each for the same reason
  callFunctionsInArray:  function(functionArray) {
    for (var i = 0; i < functionArray.length; i++){
      if(functionArray[i]) eval(functionArray[i].toString().replace(/\n/g, "").replace(/^function\s*\(\)\s*{\s*(.*)}\s*$/mi,"$1"));
    }
  },

  addToFunctionArray: function(functionArray, functionItem) {
    functionArray[functionArray.length] = functionItem;
  },
  
  // Allows any exisiting functionality to not be overwritten
  onchange:     function() { this.callFunctionsInArray(this.onchanges); },
  onfocus:      function() { this.callFunctionsInArray(this.onfocuses); },
  onblur:       function() { this.callFunctionsInArray(this.onblurs); },
  onmouseover:  function() { this.callFunctionsInArray(this.onmouseovers); },
  onmouseout:   function() { this.callFunctionsInArray(this.onmouseouts); },

  addOnchange:    function(functionItem) { this.addToFunctionArray(this.onchanges, functionItem); },
  addOnfocus:     function(functionItem) { this.addToFunctionArray(this.onfocuses, functionItem); },
  addOnblur:      function(functionItem) { this.addToFunctionArray(this.onblurs, functionItem); },
  addOnmouseover: function(functionItem) { this.addToFunctionArray(this.onmouseovers, functionItem); },
  addOnmouseout:  function(functionItem) { this.addToFunctionArray(this.onmouseouts, functionItem); },

  // Validations
  addValidation: function(validIfTrue, errorMessageIfNot) {
    this.validations.push(new Array(validIfTrue, errorMessageIfNot));
  },

  validate: function() {
    for (var i = 0; i < this.validations.length; i++){
      //alert(this.validations[i][0]);
      test = eval(this.validations[i][0]);
      //alert(test);
      if(test) {
        if(this.validations[i][1] == "useRequiredMessage") return this.error(this.requiredMessage);
        return this.error(this.validations[i][1]);
      }
    }
    return this.valid();
  },

  error: function(message) {
    this.setStatus(false, message);
    return false;
  },

  valid: function(message) {
    this.setStatus(true, message);
    return true;
  },

  // Show error and success messages near the field
  setStatus: function(valid, message) {
    if(this.statusElement != null) {
      if(message == undefined) message = "&nbsp;";
      if(valid) {
        this.statusElement.innerHTML = this.validMessageContainer.replace("MessageHere", message);
      } else {
        this.statusElement.innerHTML = this.errorMessageContainer.replace("MessageHere", message);
      }
    }
  }
}); // end ActiveFormField.base


// Extends ActiveFormField
ActiveFormField.text = Class.create(ActiveFormField.base, {

  initialize: function($super, id) {
    $super(id); // Calls parent's initialize
    this.maxlength = this.element.readAttribute("maxlength");
    this.baseFieldType = "text";
    if(this.maxlength) this.addValidation("this.maxlength && this.value.toString().length > this.maxlength", "Must be less than "+this.maxlength+" characters.");
  }
}); // end ActiveFormField.text

// Extends ActiveTextField
ActiveFormField.number = Class.create(ActiveFormField.text, {

  initialize: function($super, id) {
    $super(id);
    this.requiredMessage = "Must be greater than zero";
    if(this.required) this.addValidation("this.value <= 0", this.requiredMessage);
    this.roundTo = this.getRoundTo();
  },

  // Match round to number. External array ensures an array object is created.
  getRoundTo: function() {
    return Number([this.element.className.match(/round-to-(\d+)/)].flatten()[1]);
  },

  beforeGet: function(value) {
    return this.cleanNumber(value);
  },

  beforeValidate: function(value) {
    return this.cleanNumber(value);
  },

  cleanNumber: function(num) {
    // Check if the number is a string and clean it.
    if(typeof(num) != "number") {
      num = Number(num.toString().replace(/[^0-9\.]+/g, ""));
      num = (isNaN(num)) ? 0 : num;
    }
    
    // Round to a supplied roundTo number of decimal places
    if(!isNaN(this.roundTo)) {
      num = this.roundNumber(num, this.roundTo);
    }

    return num;
  },
  
  roundNumber: function(num, decimal_places) {
    // Multiply 10 times the number of decimal places
    multiplier = Math.pow(10, decimal_places);
    return Math.round(num * multiplier) / multiplier;
  }
  
}); // end ActiveFormField.number

// Extends ActiveNumberField
ActiveFormField.integer = Class.create(ActiveFormField.number, {
  // Could be set on initialize, but this prevents the regular expression
  getRoundTo: function() {
    return 0;
  }
});

// Extends ActiveTextField
ActiveFormField.email = Class.create(ActiveFormField.text, {
  initialize: function($super, id) {
    $super(id);
    this.addValidation('!this.value.match(/^[a-z0-9_\.\-]+@([a-z0-9_\-]+\.)+[a-z0-9_\-]+$/i)', "Must be a valid email address.");
  }
});


// Extends ActiveNumberField
ActiveFormField.amount = Class.create(ActiveFormField.number, {

  // Could be set on initialize, but this prevents the regular expression
  getRoundTo: function() {
    return 2;
  },

  afterValidate: function(value) {
    return this.formatAsMoney(value);
  },

  formatAsMoney: function(amount) {
    amount = this.cleanNumber(amount);
    return (amount == Math.floor(amount)) ? amount + '.00' : ( (amount*10 == Math.floor(amount*10)) ? amount + '0' : amount);
  }
}); // end ActiveFormField.amount

// Extends ActiveNumberField
ActiveFormField.select = Class.create(ActiveFormField.base, {
  initialize: function($super, id) {
    $super(id); // Calls parent's initialize
    this.baseFieldType = "select";
  }
}); // end ActiveFormField.select

ActiveFormField.checkbox = Class.create(ActiveFormField.base, {
  initialize: function($super, id) {
    $super(id);
    this.baseFieldType = "checkbox";
    this.requiredMessage = "Required"
  },

  setElementValue: function(value) {
    if(value) {
      this.element.checked = true;
    } else {
      this.element.checked = false;
    }
  },

  getElementValue: function() {
    return (this.element.checked) ? this.element.value : false;
  }
}); // end ActiveFormField.checkbox

// Extends ActiveTextField
ActiveFormField.password = Class.create(ActiveFormField.text, {
  initialize: function($super, id) {
    $super(id);
    if(id.slice(-12).toLowerCase() == "confirmation") {
      passwordId = id.replace(/_?confirmation$/i, "");
      this.addValidation("this.value != $('"+passwordId+"').value", "Isn't the same");
    }
  }
});

ActiveFormField.submit = Class.create(ActiveFormField.base, {
  initialize: function($super, id) {
    $super(id);
    this.baseFieldType = "submit";
  }
}); // end ActiveFormField.password

ActiveFormField.hidden = Class.create(ActiveFormField.base, {
  initialize: function($super, id) {
    $super(id);
    this.baseFieldType = "hidden";
  }
}); // end ActiveFormField.hidden

ActiveFormField.radio = Class.create(ActiveFormField.checkbox, {});

ActiveFormField.radioGroup = Class.create(ActiveFormField.base, {
  initialize: function(id) {
    this.radios = [];
    this.statusElement = $(id+"_status");
  },

  // Return the selected radio's value. Protects against none selected.
  getElementValue: function() {
    currSelected = this.radios.find(function(radio) { return radio.get(); });
    return (currSelected) ? currSelected.get() : false;
  }
}); // end ActiveFormField.radioGroup





