/*--------------------------- LabeledTextField -------------------------------*/

var LabeledTextField = Class.create({

  initialize: function(element, label) {
    this.element = $(element);
    this.form = this.element.form;
    this.label = label;
    this.value = this.element.value;
    this.eventOnFocus = this.removeLabel.bindAsEventListener(this);
    this.eventOnBlur = this.addLabel.bindAsEventListener(this);
    Event.observe(this.element, "focus", this.eventOnFocus, true);
    Event.observe(this.element, "blur", this.eventOnBlur, true);
    LabeledTextField.addFormObserver(this);
    this.addLabel();
  },
  
  destroy: function() {
    Event.stopObserving(this.element, "focus", this.eventOnFocus, true);
    Event.stopObserving(this.element, "blur", this.eventOnBlur, true);
    LabeledTextField.removeFormObserver(this);
    this.removeLabel();
  },

  labeled: function() {
    return Element.hasClassName(this.element, "labeled");
  },

  getValue: function() {
    return this.labeled() ? '' : this.element.value;
  },

  setValue: function(value) {
    this.value = (value || '');
    if (this.value.length > 0)
      this.removeLabel();  
  },

  addLabel: function(event) {
    this.value = this.element.value;
    if (this.value.length == 0) {
      Element.addClassName(this.element, "labeled");
      this.element.value = this.label;
    }
  },
  
  removeLabel: function(event) {
    if (this.element.value == this.label) {
      Element.removeClassName(this.element, "labeled");
      this.element.value = this.value;
    }
  }
        
});

Object.extend(LabeledTextField, {
  
  observedForms: [],
  
  addFormObserver: function(instance) {
    var form = instance.form;
    if (form) {
      if (!LabeledTextField.observedForms.include(form)) {
        Event.observe(form, "submit", LabeledTextField.onSubmit, true);
        form._labeledTextFields = [];
        LabeledTextField.observedForms.push(form);
      }
      form._labeledTextFields.push(instance);
    }
  },

  removeFormObserver: function(instance) {
    var form = instance.form;
    if (form && form._labeledTextFields) {
      form._labeledTextFields = form._labeledTextFields.reject(function(field) {
         return field == instance;
      }.bind(this));
      if (form._labeledTextFields.length == 0) {
        Event.stopObserving(form, "submit", LabeledTextField.eventOnSubmit, true);
        LabeledTextField.observedForms = LabeledTextField.observedForms.reject(function(f) {
           return f == form;
        }.bind(this));      
      }
    }
  },
  
  onSubmit: function(event) {
    LabeledTextField.removeLabels(Event.element(event));
  },
  
  removeLabels: function(form) {
    var fields = form._labeledTextFields;
    if (fields) {
      for (var i=0; i < fields.length; i++) {
        fields[i].removeLabel();
      }
    }
  },
  
  addLabels: function(form) {
    var fields = form._labeledTextFields;
    if (fields) {
      for (var i=0; i < fields.length; i++) {
        fields[i].addLabel();
      }
    }
  },

  formSerialize: Form.serialize,
  
  serialize: function(form) {
    form = $(form);
    var result;
    LabeledTextField.removeLabels(form);
    result = LabeledTextField.formSerialize(form);
    LabeledTextField.addLabels(form);
    return result;
  }
  
});

Form.serialize = LabeledTextField.serialize;

InstanceManager.create(LabeledTextField);


/*--------------------------- LabeledPasswordField -------------------------------*/

var LabeledPasswordField = Class.extend( LabeledTextField, {

  initialize: function(element, label, className) {
    this.element = $(element);
  
    this.labelElement = document.createElement('input');
    this.labelElement.setAttribute('name', '_label_' + this.element.name);
    this.labelElement.setAttribute('type', 'text');
    if (this.element.id) 
      this.labelElement.setAttribute('id', '_label_' + this.element.id);
    this.labelElement.setAttribute('size', this.element.size);
    this.labelElement.setAttribute('maxlength', this.element.maxlength);
    this.labelElement.setAttribute(document.all ? 'className' : 'class', this.element.className);
    Element.addClassName(this.labelElement, className || "labeled");
    this.labelElement.value = label;
    
    this.element.hide();
    this.element.parentNode.insertBefore(this.labelElement, this.element);
    
    this.form = this.element.form;

    this.eventOnFocus = this.removeLabel.bindAsEventListener(this);
    this.eventOnBlur = this.addLabel.bindAsEventListener(this);
    Event.observe(this.labelElement, "focus", this.eventOnFocus);
    Event.observe(this.element, "blur", this.eventOnBlur);
    LabeledTextField.addFormObserver(this);
    this.addLabel();
  },
  
  destroy: function() {
    Event.stopObserving(this.labelElement, "focus", this.eventOnFocus);
    Event.stopObserving(this.element, "blur", this.eventOnBlur);
    LabeledTextField.removeFormObserver(this);
    this.removeLabel();
    this.labelElement.remove();
  },

  addLabel: function(event) {
    if (this.element.value.length == 0) {
      this.labelElement.show();
      this.element.hide();
    }
  },
  
  removeLabel: function(event) {
    this.element.show();
    this.labelElement.hide();
    if (event) {
      this.element.focus();
      this.element.select();
    }
  }
        
});

InstanceManager.create(LabeledPasswordField);


/*-------------------------- MultiplierFormat ------------------------------*/

var MultiplierFormat = Class.create({
  
  initialize: function(factor) {
    this.factor = factor
  },

  format: function(number) {
    return $N(number) * this.factor;
  },

  parse:  function(number) {
    return $N(number) / this.factor;
  }
});

Object.extend(MultiplierFormat, {
  
  instances: {},
  
  instance: function(factor) {
    var instance = this.instances[factor];
    if (!instance) instance = this.instances[factor] = new MultiplierFormat(factor);
    return instance;
  }
});

CentFormat = MultiplierFormat.instance(0.01);

/*--------------------------- CalculatedField ------------------------------*/

var CalculatedField = Class.create({

  initialize: function(field, formula) {
    this.field = $(field);
    this.onChange = this.calculate.bindAsEventListener(this);
    this.formula = formula.gsub(/(:+)([a-zA-Z0-9_]+)/, function(match) {
      var element = $(match[2]);
      if (match[1] == ":") {
        this.target = element.target ? element.target : element;
        Event.observe(this.target, "change", this.onChange, false);
      }
      return "$N($F('" + element.id + "'))";
    }.bind(this));
    this.calculate();
  },
  
  destroy: function() {
    if (this.target) Event.stopObserving(this.target, "change", this.onChange, false);
  },

  calculate: function(event) {
    result = eval(this.formula);
    if (!isNaN(result) && result != Infinity) this.field.value = result;
  }  
});

InstanceManager.create(CalculatedField);

/*--------------------------- FormattedField -------------------------------*/

var FormattedField = Class.create({
  
  initialize: function(source, target, formats) {
    this.source = $(source);
    this.target = $(target);
    this.source.target = this.target;
    this.formatters = [formats].flatten();
    this.parsers = this.formatters.reverse(false);
    this.onChange = this.update.bindAsEventListener(this);
    Event.observe(this.target, "change", this.onChange, true);
    this.sourceObserver = new Form.Element.Observer(this.source, 0.25, this.format.bindAsEventListener(this));
    this.format();
  },
  
  destroy: function() {
    Event.stopObserving(this.target, "change", this.onChange, true);
  },

  format: function() {
    var value = this.source.value;
    this.formatters.each( function(formatter) {
      value = formatter.format(value);
    });
    this.target.value = value;    
  },

  parse: function() {
    var value = this.target.value;
    this.parsers.each( function(parser) {
      value = parser.parse(value);
    });
    this.source.value = isNaN(value) ? "" : value;
  },

  update: function() {
    this.parse();
    this.format();
  }
});

InstanceManager.create(FormattedField);

/*---------------------------- NumberFormat --------------------------------*/

var NumberFormat = Class.create({

  initialize: function(decimalDigits, decimalSeparator, thousandsSeparator) {
    this.decimalDigits = decimalDigits;
    this.decimalSeparator = decimalSeparator || NumberFormat.DefaultDecimalSeparator;
    this.thousandsSeparator = thousandsSeparator || NumberFormat.DefaultThousandsSeparator;
    this._tsRegExp = new RegExp("[" + this.thousandsSeparator + "]", "g");
    this._dsRegExp = new RegExp("[" + this.decimalSeparator + "]");
  },

  format: function(number) {
    number = $N(number);
    if (isNaN(number)) return "";
  
    var result = Math.round(Math.abs(number) * Math.pow(10, this.decimalDigits)).toString();
    while (result.length <= this.decimalDigits) result = "0" + result;
  
    var integerPart = result.substr(0, result.length - this.decimalDigits);
    integerPart = integerPart.gsub(/(\d)(?=(\d\d\d)+(?!\d))/, function(m) {
       return m[0] + this.thousandsSeparator 
    }.bind(this));
  
    result = result.substr(result.length - this.decimalDigits, this.decimalDigits);
    return (number < 0 ? "-" : "") + integerPart + (this.decimalDigits == 0 ? "" : this.decimalSeparator) + result;
  },

  parse: function(number) {
    number = $N(number.replace(this._tsRegExp, "").replace(this._dsRegExp, "."));
    return isNaN(number) ? null : number;
  }
});

Object.extend(NumberFormat, {
  
  DefaultDecimalSeparator: ".",
  DefaultThousandsSeparator: ",",
  
  instances: {},
  
  instance: function(decimalDigits, decimalSeparator, thousandsSeparator) {
    var key = decimalDigits + 
          (decimalSeparator || NumberFormat.DefaultDecimalSeparator) +
          (thousandsSeparator || NumberFormat.DefaultThousandsSeparator);
    var instance = this.instances[key];
    if (!instance) instance = this.instances[key] = new NumberFormat(decimalDigits, decimalSeparator, thousandsSeparator);
    return instance;
  }  
});

/*------------------------------- Dimension ---------------------------------*/

function Dimension(value) {
  value = value.match(/^([0-9.]+)(px|em|ex|pt|pc|in|mm|cm)$/);
  if (value) {
    this.value = Number(value[1]);
    this.unit = value[2];
  } else {
    this.value = NaN;
    this.unit = null;
  }
}

/*---------------------------- DynamicTextarea ------------------------------*/

var DynamicTextarea = Class.create({
   
  initialize: function(textarea) {
    this.textarea = $(textarea);

    var dim = new Dimension(this.textarea.style.minHeight);
    if (dim.value && dim.unit == "px") 
      this.minHeight = dim.value;
    else
      this.minHeight = this.textarea.clientHeight;

    dim = new Dimension(this.textarea.style.maxHeight);
    if (dim.value && dim.unit == "px")
      this.maxHeight = dim.value;
    else
      this.maxHeight = Infinity;
    
    this.onKeyUp = this.resize.bindAsEventListener(this);  
    Event.observe(this.textarea, "keyup", this.onKeyUp);

    this.setHeight(0);
    this.resize();
  },

  destroy: function() {
    Event.stopObserving(this.textarea, "keyup", this.onKeyUp);
  },

  resize: function(event) {
    if (this.contentLength > this.textarea.value.length) {
      this.setHeight(Math.floor(this.textarea.clientHeight * this.textarea.value.length / this.contentLength));
      while (this.textarea.clientHeight > this.textarea.scrollHeight) {
        if (this.textarea.clientHeight > this.minHeight)
          this.setHeight(this.textarea.clientHeight - 1);
        else break;
      }
    }
    this.setHeight(this.textarea.scrollHeight);
    this.contentLength = this.textarea.value.length;
  },
  
  setHeight: function(height) {
    if (height > this.maxHeight) {
      if (this.textarea.style.overflow != "auto") {
        this.textarea.style.overflow = "auto";
        this.textarea.style.height = this.maxHeight + "px";
        this.textarea.scrollTop = this.textarea.scrollHeight - this.textarea.clientHeight;
      } else {
        this.textarea.style.height = this.maxHeight + "px";
      } 
    } else {
      if (this.textarea.style.overflow != "hidden")
        this.textarea.style.overflow = "hidden";
      this.textarea.style.height = Math.max(height, this.minHeight) + "px";      
    }
  }  
});

Object.extend(DynamicTextarea, {
  
  selector: "dynamic",
  
  createInstances: function(form) {
    this.eachTextarea(form, function(textarea) {
      DynamicTextarea.create(textarea);
    });
  },
  
  destroyInstances: function(form) {
    this.eachTextarea(form, function(textarea) {
      DynamicTextarea.destroy(textarea);
    });
  },
  
  eachTextarea: function(form, iterator) {
    var textareas = form.getElementsByTagName('textarea');
    for (var i=0; i < textareas.length; i++) {
      if (Element.hasClassName(textareas[i], DynamicTextarea.selector))
        iterator(textareas[i]);
    }
  }
});

InstanceManager.create(DynamicTextarea);

/*------------------------------ ActiveForm ----------------------------------*/

var ActiveForm = Class.create({

  initialize: function(form, onChange) {
    this.form = $(form);
    this.onChange = onChange;
    this.memento = this.serialize();
    this.capturedReset = this.form.reset;
    this.form.reset = this.reset.bindAsEventListener(this);  
    this.changeListener = this.elementChanged.bindAsEventListener(this);
    this.registerEvents(true);
    DynamicTextarea.createInstances(this.form);
  },

  destroy: function() {
    this.registerEvents(false);
    this.form.reset = this.capturedReset;
    DynamicTextarea.destroyInstances(this.form);
  },
  
  registerEvents: function(register) {
    var elements = Form.getElements(this.form);
    for (var i=0; i < elements.length; i++) {
      this.registerEvent(elements[i], register);
    }
  },

  registerEvent: function(element, register) {
    if (element.type) {
      switch (element.type.toLowerCase()) {
        case 'hidden':
          break;
        case 'checkbox':
        case 'radio':
          (register ? Event.observe : Event.stopObserving)(element, 'click', this.changeListener);
          break;
        default:
          (register ? Event.observe : Event.stopObserving)(element, 'change', this.changeListener);
          break;
      }
    }
  },

  reset: function(event) {
    this.capturedReset(event);
    if (this.onChange) this.onChange(this, this.memento);
    Element.removeClassName(this.form, ActiveForm.hasChangesClass);
  },

  changedElementsCount: function() {
    var count = 0;
    for (var i in this.changedElements) count++;
    return count;
  },

  elementChanged: function() {
    var value = this.serialize();
    if (this.onChange) this.onChange(this, value);
    if (this.memento == value) {
      Element.removeClassName(this.form, ActiveForm.hasChangesClass);
    } else {
      Element.addClassName(this.form, ActiveForm.hasChangesClass);
    }
  },

  serialize: function() {
    return Form.serialize(this.form);
  }
  
});

ActiveForm.hasChangesClass = "hasChanges";

InstanceManager.create(ActiveForm);
