/**
 * Author: Radosław Szalski
 *
 * This plugin replaces default checkboxes, selects and radioboxes
 * with more flexible and robust counterparts.
 *
 * The best feature is the ability to style those elements however you like!
 * Say goodbye to those pesky and inconsistent styling.
 *
 * More then that, it also provides an easy way to check/uncheck all checkboxes,
 * select a range (with shift-click) of options in Select-Multiple element.
 *
 * Each replaced element is thoroughly documented in the README.
 */
(function($) {
	$.fn.formReplacer = function() {
		this.each(function() {
			// Lets not touch irrelevant elements
			if (this.type === 'radio' || this.type === 'checkbox' || this.type === 'select-one' || this.type === 'select-multiple') {
				var $oldElem   = $(this);
				var $elemLabel = $('label[for="' + $oldElem.attr('id') + '"]');

				$oldElem.hide();

				if (this.type === 'radio' || this.type === 'checkbox') {
					var className = this.type.replace(/^./, this.type.match(/^./)[0].toUpperCase());
					var $newElemImg    = $('<span class="fReplaced' + className + 'Img"></span>');
					var $newElem       = $('<span class="fReplaced' + className + '"></span>');

					// Make sure already selected/checked elements have the new, .Selected class
					$newElem.toggleClass('selected', $oldElem.is(':checked'));
					// 'Name' allows us to recognize elements from the same group
					$newElem.data('name', $oldElem.attr('name'));

					/**
					 * We bind different events depending on the element type
					 *
					 * Normal click event for radios - because we need to do more work
					 * Toggle event for checkboxes - we only need to toggle class and attr
					 */
					if (this.type === 'radio') {
						$newElem.click(function(e) {
							// Just in case, see: checkbox click event
							e.preventDefault();

							if (!$oldElem.is(':checked')) {
								$oldElem.attr('checked', false);
								$('.fReplacedRadio').filter(function(i){
									return $(this).data('name') === $oldElem.attr('name');
								}).removeClass('selected');

								$oldElem.attr('checked', true);
								$(this).addClass('selected');
							}
						});
					} else if (this.type === 'checkbox') {
						/**
						 * Check/Uncheck All
						 *
						 * Every element with a class 'checkAll' will toggle selected state
						 * for those checkboxes, which name is also included as a class
						 */
						var $checkAll   = $('.checkAll');

						$checkAll.toggle(function() {
							// Check All
							var names = $(this).attr('class').split(' '); // We store every class in this array

							for (name in names) {
								if (names[name] != 'checkAll') {
									$('.fReplacedCheckbox').filter(function() {
										return $(this).data('name') === names[name];
									}).addClass('selected');

									$(':checkbox[name="' + names[name] + '"]').attr('checked', true);
								}
							}
							return false;   // In case the element with .checkAll class has a default action
						}, function() {
							// Uncheck All
							var names = $(this).attr('class').split(' ');

							for (name in names) {
								if (names[name] != 'checkAll') {
									$('.fReplacedCheckbox').filter(function() {
										return $(this).data('name') === names[name];
									}).removeClass('selected');

									$(':checkbox[name="' + names[name] + '"]').attr('checked', false);
								}
							}
							return false;
						});

						$newElem.click(function(e) {
							/* This is crucial, since labels that have the 'for' attribute WILL respond to the click
							   As if you have clicked 2x on the element (once for $newElem, once for label) */
							e.preventDefault();

							$oldElem.attr('checked', ($oldElem.is(':checked')) ? false : true);
							$(this).toggleClass('selected', $oldElem.is(':checked'));
						});
					}
					$newElemImg
						.insertAfter($oldElem)
						.add($elemLabel)
						.wrapAll($newElem);
				}
				/**
				 * Select-one is the standard select with or w/o opt-groups,
				 * that can have only one option selected
				 */
				else if (this.type === 'select-one') {
					var $newElem        = $('<span class="fReplacedSelect"></span>');
					var options         = $oldElem.children('option');
					var hasOptGroups    = !!$oldElem.has('optgroup').length;
					var optGroups       = (hasOptGroups) ? $oldElem.children('optgroup') : null ;
					var currentOption   = (hasOptGroups) ? $oldElem.find('option:selected').text() : options.filter(':selected').text();

					var $newOptions = $('<ul class="fReplacedSelectOptions"></ul>').hide();

					// For Identification purposes
					$newElem.data('name', $oldElem.attr('name'));
					$newOptions.data('name', $oldElem.attr('name'));

					/**
					 * We append options ( as LI items) to UL, values are stored via .data()
					 *
					 * We need to take into account the possibility, that values will be empty,
					 * if so, we insert &nbsp; to keep the dimensions intact
					 */
					if (hasOptGroups) {
						optGroups.each(function () {
							var optGroup = $('<li class="fReplacedOptGroup">' + $(this).attr('label') + '</li>');
							$newOptions.append(optGroup);

							$(this).children('option').each(function(i, option) {
								var text = ($(option).text() !== '') ? $(option).text() : '&nbsp;';
								$newOptions.append($('<li>' + text + '</li>')
									.data('value', $(option).val()));
							});
						});
					} else {
						options.each(function(i, option) {
							var text = ($(option).text() !== '') ? $(option).text() : '&nbsp;';
							$newOptions.append($('<li>' + text + '</li>')
								.data('value', $(option).val()));
						});
					}

					$newElem.click(function(e) {
						e.stopPropagation();

						$newOptions.css({
							'position'  : 'absolute',
							'top'	    : $newElem.offset().top + $newElem.outerHeight() - 1,
							'left'	    : $newElem.offset().left,
							'zIndex'    : '9999'   // Z-Index is added here, because it's crucial to UL's behavior
						});
						$newOptions.slideToggle();
					});

					/**
					 * Handles clicks outside slided options list.
					 * Just like modal windows do.
					 */
					$(document).click(function(e) {
						if (!$(e.target).parents('.fReplacedSelectOptions').length) {
							$newOptions.slideUp();
						}
					});

					/**
					 * Main click event on options
					 * Remember to suppress Opt-groups
					 */
					$newOptions.delegate('li', 'click', function(e) {
						if (!$(e.target).hasClass('fReplacedOptGroup')) {
							$newOptions.slideUp();
							$newElem.html($(this).html());
							$oldElem.val($(this).data('value'));
						}
					});

					// In case the option text is empty, insert dummy &nbsp;
					var value = (currentOption !== '') ? currentOption : '&nbsp;';
					$newElem.html(value).insertAfter($oldElem).after($newOptions);

					/**
					 * IE6 Specific Code
					 * We need to assign calculated (with units) width to LIs,
					 * otherwise IE6 will only interpret click() on text nodes, not the whole LI
					 * (default width is 'auto')
					 *
					 * Yes, I also don't like browser sniffing.
					 */
					if (jQuery.browser.msie) {
						if (parseInt(jQuery.browser.version) <= 6) {
							$newOptions.children('li').css('width', $newOptions.width());
						}
					}
				} else if (this.type === 'select-multiple') {
					/**
					 * Replaces select-multiple, a type of select
					 * that has all the options shown (no sliding) and
					 * allows for checking many options at the same time.
					 *
					 * By default, you have to press CTRL to select more than 1 option,
					 * I decided to make it toggle-like. Also supports full shift-click functionality
					 */
					var $newElem            = $('<ul class="fReplacedSelectMultiple"></ul>').data('name', $oldElem.attr('name'));
					var $options            = $oldElem.children('option');
					var $selectedOptions    = $oldElem.children('option:selected');
					var $lastClicked        = null;     // Important for shift-click functionality,
															// so we can determine the range

					$oldElem.hide();

					$options.each(function(i, option) {
						var text = ($(option).text() !== '') ? $(option).text() : '&nbsp;';
						var $li = $('<li>' + text + '</li>')
							.data('value', $(option).val());

						$li.toggleClass('selected', $(option).is(':selected'));

						$newElem.append($li);
					});

					/**
					 * Each LI will handle either a normal click or a shift-click.
					 *
					 * Normal click behaves exactly like that of a native select-multiple,
					 * where it unselects previously selected options (or needs a CTRL to select >1)
					 */
					$newElem.delegate('li', 'click', function(e) {
						/**
						 * Shift-click should mimic native behavior, that is
						 * selects a range from last clicked option, to currently clicked one.
						 *
						 * We can select in both directions "forward" and "backward", depending on indexes relation.
						 */
						if (e.shiftKey) {
							/**
							 * This click is the first one, so we select whole range,
							 * from beginning up to the current position.
							 *
							 * .andSelf() ensures we add currently clicked element to the set as well
							 */
							if ($lastClicked === null) {
								$oldElem.children('option[value="' + $(this).data('value') + '"]').prevAll().andSelf().attr('selected', true);
								$(this).prevAll().andSelf().addClass('selected');

								$oldElem.children('option[value="' + $(this).data('value') + '"]').nextAll().attr('selected', false);
								$(this).nextAll().removeClass('selected');
							} else {
								/**
								 * Previous click was a normal (non-shift) click.
								 *
								 * Depending on indexes of current $(this) and last click we determine the range to act on.
								 */
								var start       = ($lastClicked.index() <= $(this).index()) ? $lastClicked.index() : $(this).index();
								var end         = ($lastClicked.index() <= $(this).index()) ? $(this).index() + 1 : $lastClicked.index() + 1;
								var liRange     = $(this).siblings().andSelf().slice(start, end);
								var optionRange = $oldElem.children('option').slice(start, end);

								// We select everything in range
								optionRange.attr('selected', true);
								liRange.addClass('selected');
								// And de-select everything else
								$oldElem.children('option').not(optionRange).attr('selected', false);
								$(this).siblings().not(liRange).removeClass('selected');
							}
						} else { // Normal Click - move along...
							$lastClicked    = $(this);  // Marks the current click, so it can be used when determining ranges in shift-click
							var $oldOption  = $oldElem.children('option[value="' + $(this).data('value') + '"]');

							/**
							 * Holding CTRL allows us to select more than 1 option
							 * If it's not pressed, then we deselect everything other than "current e.target"
 							 */
							if (e.ctrlKey) {
								if ($oldOption.is(':selected')) {
									$oldOption.attr('selected', false);
									$(this).removeClass('selected');
								} else {
									$oldOption.attr('selected', true);
									$(this).addClass('selected');
								}
							} else {
								$oldOption.attr('selected', true);
								$(this).addClass('selected');

								$oldOption.nextAll().each(function() {
									$(this).attr('selected', false);
								});

								$oldOption.prevAll().each(function() {
									$(this).attr('selected', false);
								});

								$(this).prevAll('li').removeClass('selected');
								$(this).nextAll('li').removeClass('selected');
							}
						}
					});
					$newElem.insertAfter($oldElem);
					/**
					 * We are fixing a situation, when in Chrome and IE7 scrollbar is added inside the ul.
					 *
					 * This results in an unwelcome horizontal scrollbar. Since the standard width of a scrollbar is 20px
					 *  we need to add 20px to the actual width, so horizontal bar is not needed
					 */
					$newElem.css('width', $newElem.width() + 20);
				}
			}
		});
		return this;
	};
})(jQuery);
