/********* $Id ********/

Syn.Oodle = Class.extend(
{
	/**
	 * Constructor. Initializes configuration and sets the proxy request URL.
	 * @member Syn.Oodle
	 * @param {array} Configuration
	 */
	init: function(config)
	{
		// Set the component configuration
		this.Config = config;

		// Set the proxy application request url
		this.Config.request_url = Syn.Config.ProxyHostGeneralUrl + this.Config.request_script_location;

		// Set the form submit handler
		this.set_form_submit_handler();

		// Set the form field focus and blur handlers
		this.set_form_field_focus_blur_events();

		// Initialize the request object
		this.initialize_request();

		var self = this;

		setTimeout(function() {
			// Make the request to the proxy script
			self.make_request(self.Config.request_url);
		}, 2000);
	},

	/**
	 * The following builds the result elements and append it to the results div
	 * @member: Syn.OodleSearch
	 * @param {int} Result number
	 * @param {string} Result title
	 * @param {string} Result price
	 * @param {int} Result creation time in UNIX timestamp format
	 * @param {string} Path to the category of which the result belongs to
	 * @param {string} Name of the category which the result belongs to
	 * @param {string} Result image URL
	 * @param {string} Result image width
	 * @param {string} Result image height
	 * @param {string} Result image alt text
	 * @param {string} Result location
	 */
	add_result: function(num, title, url, price, create_time, category_path, category_name, image_source, image_width, image_height, image_alt, location)
	{
		// Format the price to $## if the price is 10 or greater or $#.## if the price is less than 10
		price = this.format_string_to_us_currency(price);

		// If an image was not returned in the result, use the default image and dimensions
		image_width = (image_width) ? image_width : 112;
		image_height = (image_height) ? image_height : 84;

		// At the time of the integration, Oodle is serving blank images to us and treating them as valid. They're also sending
		// images which display "No image available". Due to this being inconsistant with our "No image available" image, we're
		// going to point to theirs.
		image_source = (image_source && image_source != 'http://i.oodleimg.com') ? image_source : 'http://i.oodleimg.com/a/nothumb.gif';

		// The title needs to fit on one line, so we truncate the title to ~16 characters and append an elipsis.
		// In the future, it would be nice to implement a 'ruler' function which calculates the width of
		// a given string with its CSS applied.
		title = this.truncate_title(title, 16);

		// Results will alternate CSS classes
		var display_type = ((num %2) === 0) ? 'ad_left' : 'ad_right';

		// Unique ID for each result row
		var row_num = Math.floor(num/2);

		// Unique ID for each result
		var div_id = 'oodle_result_' + num;

		// Unique ID for each results image link element
		var image_link_id = 'oodle_result_image_link_' + num;

		// Format time to "days, hours, minutes, seconds ago"
		var time_since_string = this.format_timestamp_to_time_string(create_time);

		/*
		 * Clone the result template elements, insert the result data and append to the results container
		 */

		// If we're on an even result, begin a new row
		if (!(num %2))
		{
			$('#oodle_row_template').clone().appendTo('#oodle_results').attr({id:'oodle_row_' + row_num});
		}

		// Result container element.
		// empty the html contents.
		$('#oodle_' + display_type + '_template').clone().appendTo('#' + 'oodle_row_' + row_num).attr({id:div_id}).html('');

		// Result image anchor element
		$('#oodle_' + display_type + '_image_link_template').clone().appendTo('#' + div_id).attr({id:image_link_id,href:url,target:'_blank'}).html('');

		// Result image element
		$('#oodle_' + display_type + '_image_template').clone().appendTo('#' + image_link_id).attr({src:image_source,width:image_width,height:image_height,alt:image_alt}).removeAttr('id');

		// Result title anchor element
		$('#oodle_' + display_type + '_title_link_template').clone().appendTo('#' + div_id).attr({href:url,target:'_blank'}).html(price + '<br />' + title).removeAttr('id');

		// Result location element
		$('#oodle_' + display_type + '_location_template').clone().appendTo('#' + div_id).removeAttr('id').html(location);

		// Result create time element
		$('#oodle_' + display_type + '_time_template').clone().appendTo('#' + div_id).removeAttr('id').html(time_since_string);

		// Result category element
		$('#oodle_' + display_type + '_category_template').clone().appendTo('#' + div_id).removeAttr('id').attr({href:'http://www.oodle.com/' + category_path,target:'_blank'}).html('More ' + category_name);
	},

	/**
	 * Builds and returns the structure for an Oodle API call in JSON format
	 * @member Syn.Oodle
	 * @param {string} A vendor id for narrowing down results
	 * @param {string} The category id for the call
	 * @param {string} The query
	 * @param {array} An array of vendor ids to exclude
	 * @param {int} The starting offset for the results
	 * @param {int} The ending offset for the results
	 * @param {string} The API key
	 * @param {int} The zip code
	 * @param {int} The search radius 
	 * @return JSON object
	 * @type Object
	 */
	build_call: function(listing_source, category_id, q, exclusions, start, end, partner_id, zip, radius)
	{
		return {
			'partner_id':partner_id,
			'q':q,
			'region':'usa',
			'from':start,
			'to':end,
			'filters':[
			{
				'type':'distance',
				'params':{
					'zip':zip,
					'value':radius,
					'units':'mi',
					'country_code':'USA'
				}
			},
			{
				'type':'source',
				'params':{
					'exclude':exclusions
				}
			}
			],
			'dimensions':[
				listing_source
			],
			'sort':{
				'key':'distance'
			},
			'category':category_id
		};
	},

	/**
	 * This iterates over the results for the Component and calls the add
	 * result function to add the result data to the results container.
	 * @member Syn.Oodle
	 * @param {object} The results object
	 */
	build_results: function(results)
	{
		// Reference to the Syn.Oodle object
		var o = this;

		// Initialize the result counter
		var count = 0;

		// Hide the loading container
		$('#oodle_loading').hide();

		// Initialize total_results to display
		var total_results = 0;

		// The amount of categories the component can pull from may
		// be greater than the amount of listings configured to display
		// per component load...so we need to randomize the result order
		for (var i = results.length - 1; i > -1; i--)
		{

			var rand_num = Math.floor(Math.random() * (i + 1));
			var current_array_index = results[i];

			var randomized_array_index = results[rand_num];

			results[i] = randomized_array_index;
			results[rand_num] = current_array_index;

			// Add the current result total to the total_results if the call returned results

			if (results[i].results_list)
			{
				total_results += results[i].results_list.length;
			}
		}

		// If the amount of results returned is less than the amount configured for the
		// component to display, set the number of results to display to the amount returned.
		var max_results = (total_results < o.Config.num_listings) ? total_results : o.Config.num_listings;

		// Iterate over the result sets and display results. If all categories returned results,
		// there will be one result displayed per category. If a category returned no results,
		//  this will loop back around and pull secondary results from the other categories.
		while (count < max_results)
		{
			// Iterate over the result sets. There will be one result set per each call
			// sent in the request.
			$.each(results, function(index, result_obj)
			{

				// If this result set has results...
				if (result_obj.results_list && result_obj.results_list[0] && count < max_results)
				{
					// Since we're only displaying one result per category, grab the first
					// in the sorted list returned by the proxy. The results are returned
					// sorted by revenue score, then creation time. We're shifting the array
					// as it's possible we might need to come back and grab another result
					// from this call if another call did not return any results.
					var result = result_obj.results_list.shift();

					o.add_result(count, result.title, result.url, result.attributes.price, result.ctime, result.category.path, result.category.name, result.images.src, result.images.width, result.images.height, result.images.alt, result.location.name);
					count++;
				}
			});

		}
	},

	/**
	 * This gets displayed when an error with the API occurs.
	 * @member Syn.Oodle
	 */
	display_api_error: function()
	{
		$('#oodle_results').html('Sorry, this content is currently unavailable, please try again later.');
	},

	/**
	 * Format the currency string to $#.## if less than 10,
	 * $## if 10 or greater.
	 * @member Syn.Oodle
	 * @param {mixed} currency string
	 * @return The formatted currency string
	 * @type String
	 */
	format_string_to_us_currency: function(amount)
	{
		amount = amount.toString();
		var decimal_index = amount.lastIndexOf('.');
		if (decimal_index > 1)
		{
			var delimiter = ',';
			amount = Math.ceil(parseInt(amount, 10)) + '';

			var str_split_array = [];

			while(amount.length > 3)
			{
				var last_three = amount.substr(amount.length - 3);
				str_split_array.unshift(last_three);
				amount = amount.substr(0, amount.length - 3);
			}

			if (amount.length > 0)
			{
				str_split_array.unshift(amount);
			}

			return '$' +  str_split_array.join(delimiter);
		}

		else if (decimal_index == -1)
		{
			return '$' + amount;
		}

		return '$' + parseFloat(amount).toFixed(2);
	},

	/**
	 * Convert a UNIX timestamp to "days, hours, minutes and seconds ago"
	 * @member Syn.Oodle
	 * @param {int} timestamp A Unix Timestamp
	 * @return The formatted time since an item was listed
	 * @type String
	 */
	format_timestamp_to_time_string: function(timestamp)
	{
		var d = Math.floor(new Date().getTime()/1000);
		var time_since = d - timestamp;
		var days = Math.floor(time_since / 86400);
		var hours = Math.floor((time_since - (days * 86400)) / 3600);
		var minutes = Math.floor((time_since - (days * 86400) - (hours * 3600)) / 60);
		var seconds = Math.floor((time_since - (days * 86400) - (hours * 3600)) - (minutes * 60));
		return days + 'd, ' + hours + 'h, ' + minutes + 'm and ' + seconds +'s ago.';
	},

	/**
	 * Retrieve the search form field values , trim any whitespace and return as a JSON object
	 * @member Syn.Oodle
	 * @return JSON object
	 * @type Object
	 */
	get_form_values: function()
	{
		return {'q':this.trim($('#oodle_q').val()),'where':this.trim($('#oodle_where').val()),'category':$('#oodle_category').val()};
	},

	/**
	 * Adds the error class to the element with the given id
	 * to indicate an input error. Binds a function to remove the class
	 * when the field is changed.
	 * @member Syn.Oodle
	 * @param {string} field_id The element id
	 */
	highlight_error_field: function(id)
	{
		$('#' + id).addClass('error').change(function()
		{
			$('#' + id).removeClass('error');
		});
	},

	/**
	 * Sets up the request object
	 * @member Syn.Oodle
	 */
	initialize_request: function()
	{
		// Initial request object setup
		//	'type' tells the proxy how to return the results
		//	'oodle_callback_method' tells the proxy what JavaScript
		//		method to call after it finishes.
		//	'calls' is the placeholder for the Oodle API calls
		//		we will send in the request.
		this.request = {'max_results':100,'type':'oodle_classifieds_component','oodle_callback_method':this.Config.oodle_callback_method,'calls':[]};

		// Reference to Syn.Oodle
		var o = this;

		// The following block of code will build the calls required for
		// the component. Multiple calls are required as Ooodle only
		// allows us to search one category per API call.

		// Iterate over all categories configured and build a call
		// for each.
		$.each(this.Config.categories, function (category_display_name, category_path)
		{
			o.request.calls[o.request.calls.length] = o.build_call('', category_path, '', [], 0, 10, o.Config.partner_id, o.Config.zip, o.Config.radius);
		});
	},

	/**
	 * Make the request to the proxy via getJSON
	 * @member Syn.Oodle
	 */
	make_request: function(request_url)
	{
		// Show the loading message
		this.show_loading($('#oodle_results').height());

		// We need to store a reference so the callback can reference Syn.Oodle
		Syn.Oodle.ajaxInstance = this;

		// Make the getJSON request
		$.getJSON(request_url + '?params=' + json.serialize(this.request) + '&c=' + this.Config.proxy_callback + '&json_callback=?');
	},

	/**
	 * The proxy will call this method and pass in the response object.
	 * @member Syn.Oodle
	 * @param {object} result The proxy response object
	 */
	proxy_callback: function(response)
	{
		// Display error if the request failed
		if (!response.result || !response.total_results)
		{
			this.display_api_error();
		}
		// Otherwise, begin processing the response
		else
		{
			this.build_results(response.data);
		}
	},

	/**
	 * Removes the current results. This is
	 * called when a new request is sent.
	 * @member: Syn.Oodle
	 */
	remove_results: function()
	{
		$('#oodle_results').html('');
	},

	/**
	 * This sets the focus and blur events for the element
	 * with a given id.
	 * @member: Syn.OodleSearch
	 * @param: {int} Input ID
	 */
	set_clear_default_restore_default_input_event: function(id)
	{
		var initial_value = $('#' + id).val();
		$('#' + id).focus(function()
		{
			if ($('#' + id).val() == initial_value)
			{
				$('#' + id).val('').blur(function()
				{
					if (!$('#' + id).val())
					{
						$('#' + id).val(initial_value);
					}
				});
			}
		});

	},

	/**
	 * Sets the onfocus events for the form
	 * fields
	 * @member Syn.Oodle
	 */
	set_form_field_focus_blur_events: function()
	{
		this.set_clear_default_restore_default_input_event('oodle_q');
		this.set_clear_default_restore_default_input_event('oodle_where');
	},

        /**
         * Sets the handler for a form submit
         * @member: Syn.OodleSearch
         */
	set_form_submit_handler: function()
	{
		$('#oodle_serp_search_form').connect('submit', this, (function()
		{
			// Get the form values entered
			var form_values = this.get_form_values();

			// Validate the search form

			this.validate_search_form(form_values.q, form_values.where, form_values.category);
		}));
	},

	/**
	 * Shows the loading container and sets the height
	 * to either the height of the previous resultset
	 * container or the minimum height as defined
	 * in the stylesheet, whichever is greater.
	 * @member Syn.Oodle
	 */
	show_loading: function(height)
	{
		// The loading height as defined in the stylesheet will be the minimum height
		var minimum_height = $('#oodle_loading_template').css('height').substr(0, $('#oodle_loading_template').css('height').length - 2);

		// If the height passed in is under the minimum height, use the minimum height
		height = (height < minimum_height) ? minimum_height : height;

		// Remove the content currently in the results container
		this.remove_results();

		// Clone and display the loading template and set its height
		$('#oodle_loading_template').clone().appendTo('#oodle_results').attr({id:'oodle_loading'}).height(height);
	},

	/**
	 * Remove whitespace at the beginning and end of a string
	 * @member Syn.Oodle
	 * @param {string} text The text to trim
	 * @return The trimmed text
	 * @type String
	 */
	trim: function(text)
	{
		return text.replace(/^\s*/, "").replace(/\s*$/, "");
	},

	/**
	 * Truncate the title and append an ellipsis
	 */
	truncate_title: function(title, num_characters)
	{
		return title.length > num_characters ? title.substr(0, num_characters - 3) + '...' : title;
	},

	/**
	 * Method which calls the individual form validation
	 * methods, displays errors, highlights erroring fields
	 * and returns the result.
	 * @member Syn.Oodle
	 * @param {string} q The users query
	 * @param {string} where The location
	 * @param {string} category The Oodle Category
	 * @return Success or failure
	 * @type boolean
	 */
	validate_search_form: function(q, where, category)
	{

		// Clear the error message container
		$('#oodle_err_msg').html('&nbsp;');

		Syn.Oodle.ajaxInstance = this;

		$.getJSON(Syn.Config.PortalRoot + this.Config.city_state_zip_mapping_path + '?query=' + q + '&where=' + where + '&category=' + category + '&oodle_validation_callback_method=Syn.Oodle.ajaxInstance.validation_callback' + '&json_callback=?');

	},

	/**
	 * The validation script will call this method when validation is complete.
	 * @member Syn.Oodle
	 * @param {object} result The validation result object
	 */
	validation_callback: function(results)
	{

		if (!results.success_value)
		{
			// Error message array
			var msg = [];

			// If the query validation failed
			if (results.query === null)
			{
				this.highlight_error_field('oodle_q');
				msg[msg.length] = 'Keyword';
			}

			// If the category validation failed
			if (results.category === null)
			{
				this.highlight_error_field('oodle_category');
				msg[msg.length] = 'Category';
			}

			// If the location validation failed
			if (results.city === null || results.state === null || results.zip === null)
			{
				this.highlight_error_field('oodle_where');
				msg[msg.length] = '"City, State" combination, where the state is<br />its two-letter abbreviation (e.g. Houston, TX), or a 5-digit zip code';
			}

			// Beginning of error message.
			var err_msg = 'Please enter a valid ';

			// Iterate through the error messages.
			for (var i = 0; i < msg.length; i++)
			{
				// If this is the last entry in the error message array...
				if (i + 1 == msg.length)
				{
					// End the error message sentance with a period.
					err_msg += msg[i] + '.';
				}
				// If this is the second to last entry in the error
				// message array...
				else if (i + 2 == msg.length)
				{
					// Add an 'and' for the final part of the message.
					err_msg += msg[i] + ' and ';
				}
				// Otherwise...
				else
				{
					// Separate the messages with a comma
					err_msg += msg[i] + ', ';
				}
			}

			// Display the error message in the error div.
			$('#oodle_err_msg').html(err_msg);

			return false;

		}
		else
		{

			this.Config.query = results.query;

			this.Config.zip = results.zip;

			this.Config.city_state = results.city + ', ' + results.state;

			this.Config.category = results.category;

			// Call the success function
			this.validation_success();
		}

	},

	/**
	 * This method will get called upon successful validation
	 * @member Syn.Oodle
	 */
	validation_success: function()
	{
		$('#oodle_serp_search_form').unbind('submit');
		$('#oodle_serp_search_form').submit();
	}
});

