// ethan@waveriderdesign.com 4.15.2008 - PROJ-JSCRIPT
// ethan@waveriderdesign.com 6.26.2008 - PROJ-REVIEWS
// TODO: make this into a class
// TODO: add an associative array mapping CSS classes to validation functions (for formatted-fields)
// TODO: decent error-checking if assumptions are violated
// TODO: redo checkboxes/radio buttons with :checked

String.prototype.trim = function() {
	return this.replace(/^\s+|\s+$/g, '');
}
// thanks to http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/
	if(!Array.indexOf){
	    Array.prototype.indexOf = function(obj){
	        for(var i=0; i<this.length; i++){
	            if(this[i]==obj){
	                return i;
	            }
	        }
	        return -1;
	    }
	}

String.prototype.reverse = function() {
    var s = "";
    var i = this.length;
    while (i>0) {
        s += this.substring(i-1,i);
        i--;
    }
    return s;
}

function validateForm(form_name, failure_callback) {
	var success = true;
	// pre-process require-if-visible fields to have the required-field class if they're visible
	$("#" + form_name + " input.require-if-visible").add("#" + form_name + " select.require-if-visible").each(function() {
	  if (isVisible($(this))) $(this).addClass('required-field');
	  else $(this).removeClass('required-field');
	});
	
	// pull radio buttons & checkbox fields into this array for separate processing after the easy fields have been taken care of
	var radio_array = [];
	
	// verify that all required-field elements have input
	$("#"+form_name+" input.required-field,#"+form_name+" select.required-field,#"+form_name+" textarea.required-field", document).each(function() {
	  if ($(this).attr('type') == 'radio' || $(this).attr('type') == 'checkbox') {
		  if (radio_array.indexOf($(this).attr('name')) == -1) radio_array.push($(this).attr('name'));
	  } else if (!validateText($(this).val(), getMinLength($(this).attr('class')))) {
	    highlightField(this);
		success = false;
	  }
	});
	
	// now we can deal with required radio/checkbox fields, stepping through all elements with the same name to make sure one is selected
	for (var i = 0; i < radio_array.length; i++) {
		var num_checked = 0;
		$('input[name=' + radio_array[i] + ']').each(function() {
		  if ($(this).attr('checked') !== undefined) num_checked++;
		});
		if (num_checked == 0) {
			showRadioFieldInstructions(radio_array[i]);
			success = false;
		}
	}
	
	// this new ugly array holds element "formatting groups" that need to be formatted according to some inter-relation between elements.
	// so far, all this is good for is CC expiration dates. Groups are identified by none other than their shared "role" attribute, thus
	// perpetuating my systemic & wholesale abuse of this humble accessibility feature. Take THAT, blind people!
	var role_groups = [];
	
	// verify that all formatted-field elements respect their formatting (there may be overlap between required-field and
	// formatted-field elements, e.g. in the case of a required email address)
	$("#" + form_name + " input.formatted-field", document).add("#" + form_name + " select.formatted-field").each(function() {
	  var valid_field = true;
	  
	  // don't care about formatted-field elements if they don't have anything in them
	  if($(this).val().length == 0) return;
	  
	  if($(this).hasClass('email')) valid_field = validateEmail($(this).val());
	  else if ($(this).hasClass('phone')) valid_field = validatePhoneNumber($(this).val());
	  else if ($(this).hasClass('email-confirmation') || $(this).hasClass('password-confirmation'))  {
	    valid_field = confirmEmail($('#' + $(this).attr('id').replace(/-confirm/, '')).val(), $(this).val());
	  } else if ($(this).hasClass('number')) valid_field = validateNumber($(this).val());
	  else if ($(this).hasClass('currency')) valid_field = validateCurrency($(this).val());
	  else if ($(this).hasClass('cc-number')) valid_field = validateCreditCard($(this).val());
	  else if ($(this).hasClass('expiry') && role_groups.indexOf($(this).attr('role')) == -1) role_groups.push($(this).attr('role'));

	if (!valid_field) {
		highlightField(this);
		success = false;
	  }
	});
	
	// go through the role groups that were collected and harvest elements within them that match a possible role
	for (var i = 0; i < role_groups.length; i++) {
		var expiry_group = $('.expiry[role="' + role_groups[i] + '"]');
		if (expiry_group.length > 0 && !validateExpiryGroup(expiry_group)) {
			highlightGroup(expiry_group);
			success = false;
		}
	}
	  
	if (success) {
	  $("#" + form_name + " input[type=submit]").attr('disabled', 'true');
	  $("#" + form_name + "").trigger('submit');
	} else {
		window.location.hash = 'form-top';
		$('#form-instructions').css('color', 'red').css('font-weight', 'bold');
		if (typeof failure_callback != 'undefined') failure_callback();
	}
}

function validateExpiryGroup(expiry_group) {
	// assumptions are made that there will be only 1 element with class year, month, etc.
	var month = parseInt(expiry_group.filter('.month').eq(0).val(), 10);
	var year = parseInt(expiry_group.filter('.year').eq(0).val(), 10);
	if (year < 100) year += 2000;
	
	var current_month = new Date().getMonth() + 1;
	var current_year = new Date().getFullYear();
	
	if (current_year > year) return false;
	if (current_year < year) return true;
	
	if (current_month > month) return false;
	if (current_month < month) return true;
	
	// this would be where we validate the day
	return true;
}

function getMinLength(class_string) {
  // parse the field's class list for instructions like min-length-2
  // I suppose it's proper to take the LAST declaration if multiple min-lengths are specified
  // although I really don't endorse that type of behavior
  var min_length = 1;
  if (class_string.indexOf('min-length') != -1) {
	  var ugly_array = class_string.split('min-length-');
	  min_length = parseInt(ugly_array[ugly_array.length - 1], 10);
  }
  return min_length;
}

function highlightGroup(element_group) {
	element_group.each(function() {
		highlightField($(this));
		$(this).focus(function() { unhighlightGroup(element_group); });
	});
}

function unhighlightGroup(element_group) {
	element_group.each(function() { unhighlightField($(this)); });
}

function unhighlightField(field) {
//	$(field).css('background-color', 'white').css('color', 'black');
//	$('label[for="' + $(field).attr('id') + '"]').css('color', 'black');
  	$(field).removeClass('highlighted');
  	$('label[for="' + $(field).attr('id') + '"]').removeClass('highlighted');
	$('span[role="' + $(field).attr('id') + '"].formatting-instructions').css('visibility', 'hidden');
}

function highlightField(field) {
//  $(field).css('background-color', '#FFCCCC').css('color', 'red').focus(function() { unhighlightField($(this)); });
//  $('label[for="' + $(field).attr('id') + '"]').css('color', 'red');
  $(field).addClass('highlighted').focus(function() { unhighlightField($(this)); });
  $('label[for="' + $(field).attr('id') + '"]').addClass('highlighted');
  $('span[role="' + $(field).attr('id') + '"].formatting-instructions').css('visibility', 'visible');
}

function showRadioFieldInstructions(field_name) {
	$('span[role=' + field_name + '].formatting-instructions').css('visibility', 'visible');
	$('input[name=' + field_name + ']').each(function() {
	  $(this).focus(function() { $('span[role=' + field_name + '].formatting-instructions').css('visibility', 'hidden'); });
	});
}

function validateText(input_string, min_length) {
	return (input_string.trim().length >= min_length);
}

function validatePhoneNumber(number) {
	var regex = /^((\+\d{1,3}(-| )?\(?\d\)?(-| )?\d{1,5})|(\(?\d{2,6}\)?))(-| )?(\d{3,4})(-| )?(\d{4})(( x| ext)\d{1,5}){0,1}$/;
	return regex.test(number);
}

function confirmEmail(email1, email2) {
	return email1 == email2;
}

// ethan@waveriderdesign.com 8.26.2008 - replaced validateEmail with much smarter version in jscript_email_validate.js
//function validateEmail(email) {
//	var regex = /^[A-Za-z0-9_.]+@([A-Za-z0-9_.]+)*[A-Za-z0-9_]+\.[A-Za-z0-9_]+$/;
//	return regex.test(email);
//}

function validateNumber(number) {
	var regex = /^[0-9,.]+$/;
	return regex.test(number);
}

function validateCurrency(amount) {
	var regex = /\$/;
	return validateNumber(amount.replace(regex, ''));
}

function validateCreditCard(n) {
  var cc_number = n.replace(/[^0-9]/, '');

  if (!cc_number.match(/^4[0-9]{12}([0-9]{3})?$/) && !cc_number.match(/^5[1-5][0-9]{14}$/) && !cc_number.match(/^3[47][0-9]{13}$/) && !cc_number.match(/^6011[0-9]{12}$/)) return false;

    var reversed = cc_number.reverse();
    var numSum = 0;

    for (i=0; i< reversed.length; i++) {
      currentNum = reversed.substring(i, i + 1);

      // Double every second digit
      if (i % 2 == 1) {
        currentNum *= 2;
      }

      // Add digits of 2-digit numbers together
      if (currentNum > 9) {
        firstNum = currentNum % 10;
        secondNum = (currentNum - firstNum) / 10;
        currentNum = firstNum + secondNum;
      }
      numSum += currentNum * 1;
    }

    // If the total has no remainder it's OK
    return ((numSum % 10) == 0);
}
  
// Recurses up the DOM tree to figure out if the element is *actually* visible (are any of its parents hidden?)
function isVisible(current_element) {
	// the only way an element can REALLY be visible is if we've reached the document root without finding a hidden element
	if($(current_element).parent().length == 0) return true;
	if ($(current_element).css('display') == 'none' || $(current_element).css('visibility') == 'hidden') return false;
	return isVisible($(current_element).parent());
}

