/**
 * Olympus Corporation of the Americas (OCA) Suggested Search plugin for jQuery
 *
 * @package		com.oca.jquery.searchsuggest
 * @author		Stephen Holsinger <stephen.holsinger@olympus.com>
 * @note		Very loosely based on the work of Marco Kuiper (http://marcofolio.net/)
 * @copyright	2010 Olympus Corporation of the Americas (OCA)
 */
(function($) {
	
	$.fn.log = function (msg) {
		if(window.console && window.console.log) {
			console.log("%s: %o", msg, this);
		}
		return this;
	};

	function log (message, element) {
		if($.browser.msie){/*alert(message+": "+element);*/ return false;}
		if(window.console && window.console.log) {
			console.log("%s: %o", message, element);
		}
	}
	
	/**
	 *	Instance options get loaded here every time a function call is made so 
	 *	that all subsequent program branches have access to them.
	 */
	var o = {};
	
	/**
	 *	Commonly used string keys
	 */
	var strings = {
		optKey: "searchsuggest-options",
		hoverClass: "hover"
	};
	
	// private search string storage
	var searchString = "";
	// private current item storage
	var currentItem = -1;
	
	/**
	 *	A timer for delaying result queries which prevents excessive back & 
	 *	forth traffic.
	 */
	var timer = null;
	
	/**
	 *	Planned for use with positioning the search results near the input which
	 *	was the target of a typing event.
	 */
	var x = 0;
	var y = 0;

	/**
	 *	Default values for settings
	 */
	var defaults = {
		dataServiceURL: "/dataService/searchSuggest.asp",
		transition: "fade",
		transitionLengthIn: 150,
		transitionLengthOut: 100,
		delay: 350,
		format: "json",
		resultsContainerId: "suggested-search-results",
		resultsLoadingContainerId: "suggested-search-results-loading",
		requestMethod: "GET",
		selectedItemClass: "selected",
		limitCategory: "",
		limitSection: "",
		x: 0,
		y: 0,
		xyRelativeTo: "#searchbox",
		xRelativeSide: "right",
		autoAlign: false
	};

	$.fn.searchSuggest = function(options) {
		var opts;
		
		// allow just a string to be passed for the url of the data service
		if (typeof options == "string"){ options = {dataServiceURL: options}; }
		
		// merge instance options with defaults
		opts = $.extend({}, defaults, options);
		
		// iterate over the selection
		return this.each(function(){
			var $this = $(this), $container, xyRelativeOffsetEl;
			
			// prevent bad behavior by ignoring non-inputs.
			if($this.is(":not(:input)")){ return false; }
			
			// get instance options while allowing the use of the meta-data plugin.
			o = $.meta ?  $.extend({}, opts, $this.data()) : opts;
			
			$container = getContainer();
			
			// if relative positioning is enabled, do it!
			if (o.xyRelativeTo != "" && o.xyRelativeTo != null) {
				xyRelativeOffsetEl = $(o.xyRelativeTo);
				log("xyRelativeOffsetEl.offset()", xyRelativeOffsetEl.offset());
				o.y = xyRelativeOffsetEl.offset().top + xyRelativeOffsetEl.height();
				o.x = xyRelativeOffsetEl.offset().left;
				if (o.xRelativeSide == "right") {
					o.x += xyRelativeOffsetEl.outerWidth(true);
					o.x -= $container.outerWidth(true);
				}
				log("Current Settings", o);
			}
			
			// automagically align the container so it is within the window
			if (o.autoAlign){
				// set initial x & y coords for the results container's top left pixel.
				o.y = $this.offset().top + $this.outerHeight();
				o.x = $this.offset().left;
			}
			
			if (o.autoAlign || (o.xyRelativeTo != "" && o.xyRelativeTo != null)) {
				// make sure the results container won't extend off the window edge
				if( (o.x + $container.outerWidth(true) + 10) > $(window).width()){
					o.x = $(window).width() - $container.outerWidth(true) - 10;
				}
			}
			
			$container.hide(0);
			
			// create the loading icon before it's needed so the image will be pre-loaded.
			createLoadingNotification();
			
			// store instance options as data attached to the element.
			$this.data(strings.optKey, o);
			// initialize the currentItem local variable.
			currentItem = -1;
			// initialize the plugin on the element
			init($this);
		});
	};
	
	/**
	 *	Remove bindings and temporary data from the targeted elements.
	 *	@return jQuery
	 */
	$.fn.searchSuggestDestroy = function() {
		return this.each(function() {
			if( $(this).is(":input") ) {
				unInit($(this));
			}
		});
	}
	
	/**
	 *	Attach necessary event handlers to the target element and any other required initialization
	 *	@param [HTML Element] element	The target element
	 *	@return null
	 */
	function init($element) {
		$element
			.blur(handleBlur)
			.focus(handleFocus)
			.keydown(handlePress)
			.keyup(handleUp)
			//.hover(handleMouseIn, handleMouseOut); // removed because it isn't necessary.
			// next line disables in-browser autocomplete for the whole form.
			.attr('autocomplete','off')
			.parent('form').attr('autocomplete','off');
	}
	
	/**
	 *	Remove the event handlers which were attached to the element by this plugin
	 *	@param [HTML Element] element	The target element
	 *	@return null
	 */
	function unInit($element) {
		$element
			.unbind({
				'blur':handleBlur,
				'focus':handleFocus,
				'keydown': handlePress,
				'keyup':handleUp
			})
			.removeAttr('autocomplete')
			.parent('form').removeAttr('autocomplete');
			o = {}
	}
	
	/**
	 *	Appends the search results (formatted as HTML) to the search results container.
	 *	@param [Object JSON] results	AJAX response object to display
	 *	@return	null
	 */
	function displaySuggestions(results) {
		$container = getContainer();
		// insert formatted results
		$container.find('ol').replaceWith(formatSuggestions(results));
		// hide the loading notification
		hideLoadingNotification();
		// show results container
		showResultsContainer();
	}
	
	/**
	 *	Un-hides the search results container element with the set transition.
	 *	@return null
	 */
	function showResultsContainer() {
		$container = getContainer();
		$container.css({left: o.x, top: o.y});
		switch (o.transition) {
			case "slide":
				$container.slideDown(o.transitionLengthIn);
			default:
				$container.fadeIn(o.transitionLengthIn);
		}
	}
	
	/**
	 *	Hides the search results container element with the set transition.
	 *	@return null
	 */
	function hideResultsContainer() {
		$container = getContainer();
		switch (o.transition) {
			case "slide":
				$container.slideUp(o.transitionLengthOut);
			default:
				$container.fadeOut(o.transitionLengthOut);
		}
	}
	
	function showLoadingNotification() {
		var $container = getContainer();
		$container.find('#'+o.resultsLoadingContainerId).css({display:'block'});
		showResultsContainer();
	}
	
	function createLoadingNotification() {
		var $container = getContainer();
		$container.prepend($("<div/>").attr("id", o.resultsLoadingContainerId));
	}
	
	function hideLoadingNotification() {
		$('#'+o.resultsLoadingContainerId).css({display:'none'});
	}
	
	/**
	 *	Formats the collection of suggestions as HTML elements.
	 *	@param	[Object JSON] suggestions	The collection of search suggestions to format
	 *	@return	[Object DOMDocumentFragment]	The search suggestions formatted in a DOMDocumentFragment
	 */
	function formatSuggestions(suggestions) {
		$list = $('<ol/>');
		$.each(suggestions, function(){
			log('formatting section', this);
			$list.append( $("<div/>").addClass("category").append( $("<span/>").addClass(this.title.toLowerCase().replace(" ","_")).text(this.title) ) );
			$.each(this.items, function() {
				log('formatting item', this.name);
				$list.append(
					$("<li/>").append(
						$("<a/>").attr('href',this.url).append(
							$("<span/>").addClass('image').append(
								$("<img>").attr({"alt":"","src":this.image_path})
							)
						).append(
							$("<span/>").addClass('heading').text(this.name)
						).append(
							$("<span/>").addClass('description').text(this.description)
						)
					)
				);
			});
		});
		return $list.eq(0);
	}
	
	/**
	 *	Issues the request for search suggestions.
	 *	@param	[String] searchString	The user's search string.
	 *	@return	null
	 */
	function getSuggestions(searchString) {
		// initialize the request data
		var rdata = {
			'q':searchString,
			's':o.limitSection,
			'c':o.limitCategory,
			'f':o.format
		};
		
		// make sure there was something to search
		if(searchString.length > 0) {
		
			// show loading notification
			showLoadingNotification();
			
			// use the set request method (GET or POST)
			if(o.requestMethod == 'GET') {
				$.get(o.dataServiceURL, rdata, handleResponse, o.format);
			} else {
				$.post(o.dataServiceURL, rdata, handleResponse, o.format);
			}
		} else { // if there wasn't, then hide the results container
			hideResultsContainer();
		}
	}
	
	/**
	 *	Returns a jQuery collection containing the container for the search results.
	 *	@return jQuery
	 */
	function getContainer() {
		if(!getContainer.container){
			getContainer.container = $('#'+o.resultsContainerId);
		}
		return getContainer.container;
	}
	
	/**
	 *	Returns a jquery object containing the currently selected result (by keyboard navigation)
	 *	@return jQuery
	 */
	function getSelected() {
		log("attempting to get the selected item", getContainer().find('.'+o.selectedItemClass));
		return getContainer().find('.'+o.selectedItemClass);
	}
	
	
	function goToSelected() {
		if(currentItem < 0){return true;}
		log("attempting to navigate to: " + getSelected().children('a').attr('href'));
		window.location = getSelected().children('a').attr('href');
		return false;
	}
	
	function handleBlur() {
		timer = setTimeout(hideResultsContainer, o.delay);
	}
	
	function handleDelay() {
		getSuggestions( searchString );
	}
	
	function handleFocus() {
		clearTimeout(timer);
	}
	
	/**
	 *	Handles the response from the request for search suggestions.
	 *	@param	[Object JSON] response	The response from the server.
	 *	@param	[String] status	Status string from the jQuery AJAX request.
	 *	@return null
	 */
	function handleResponse(response, status) {
		$.fn.log('Got response');
		// fast fail on error
		if (status != 'success') {
			alert('Error communicating with server. Status message: '+ status);
			return false;
		}
		displaySuggestions(response);
	}
	
	/**
	 *	Handles keyUp events in the target element
	 *	@param	event [Object jQuery.Event]	the jQuery.Event object for the action being handled.
	 *  @return null
	 */
	function handleUp(event) {
		var $target = $(event.target);
		o = $target.data(strings.optKey);
		if($(event.target).val() == "") {
			hideResultsContainer();
			return true;
		}
		if(event.keyCode > 48 || event.keyCode == 8) {
			clearTimeout(timer);
			// reset selected item when new data is requested.
			currentItem = -1;
			setSelected(-1);
			searchString = $target.val();
			timer = setTimeout( handleDelay , o.delay);
		}
	}
	
	/**
	 *	Handles keypresses in the target element
	 *	@param	event [Object jQuery.Event]	the jQuery.Event object for the action being handled.
	 *  @return boolean
	 */
	function handlePress(event) {
		// refresh options
		o = $(event.target).data(strings.optKey);
		switch(event.keyCode) {
			case 27: // esc
				// close the results window when escape is pressed.
				hideResultsContainer();
				return false;
			case 38: // up
				return kbdNavigate(-1, event.target);
				break;
			case 40: // down
				return kbdNavigate(1, event.target);
				break;
			case 13: // enter
				return goToSelected();
				break;
			default: // anything else
				return true;
		}
		return false;
	}
	
	function handleMouseIn(event) {
		if($.browser.msie && $.browser.version <= 7) {
			$(event.target).addClass(strings.hoverClass);
		}
	}
	
	function handleMouseOut(event) {
		if($.browser.msie && $.browser.version <= 7) {
			$(event.target).removeClass(strings.hoverClass);
		}
	}
	
	/**
	 *	Handles keyboard navigation
	 *	@param	dir [Int]	The direction to move the "cursor" (-1==up 1==down)
	 *	@param	target [HTML Element]	The target of the keyboard event.
	 *  @return	null
	 */
	function kbdNavigate(dir, target) {
		var $target = $(target);
		log('kbdNavigate ' + dir, target);
		// default the selected item to "none selected" (-1)
		if (typeof currentItem != "number") {
			currentItem = -1;
		}
		// the "&& currentItem > 0" prevents selecting past the currently available set of items
		if(dir < 1 && currentItem > 0) {
			currentItem--;
		// the "&& (currentItem + 1 <= $(...)" prevents selecting past the currently available set of items
		} else if (dir ==1 && (currentItem + 1 <= $("#"+o.resultsContainerId+" li").size() - 1)) {
			currentItem++;
		}
		// tell the system what item is currently selected
		setSelected(currentItem);
		return false;
	}
	
	/**
	*	Applies a class to the index given.
	*	@param	item [Integer]	the index to apply the class to
	*	@return	null
	*/
	function setSelected(item) {
		$selection = getContainer().find('li');
		$selection.removeClass(o.selectedItemClass);
		if(item > -1) {
			$selection.eq(item).addClass(o.selectedItemClass);
		}
	}
	
})(jQuery);
