//======================================================================
//	site_setup
//
//	a multi-step series of functions that happens ONCE on page load
//
//	these steps bookend ajax_refresh()
//======================================================================

function site_setup()
{
	site_setup_1();
}

function site_setup_1()
{
	//	hook up events

	jQuery( "#button_dismiss_message_last_active" ).click( condition_for_event( dismiss_message_last_active ) );

	//======================================================================
	//	get an authentication token
	//
	//	if you don't have one, you get one
	//	if you do have one, it is confirmed
	//======================================================================

	fetch_authentication_token()
		.done
		(
			function ( data, text_status, xml_http_request )
			{
				//======================================================================
				//	ajax_refresh
				//======================================================================

				ajax_refresh( site_setup_2 );
			}
		);
}

function site_setup_2()
{
	//======================================================================
	//	set up the site frame template
	//======================================================================

	template_site_frame = WHATAFAG.new_template_site_frame( {} );
	jQuery( "#container_template_site_frame" ).append( template_site_frame.j_template );

	//======================================================================
	//	set up the handler for hash changes
	//
	//	process the url for the first time
	//======================================================================

	jQuery( window ).bind( "hashchange", handle_update_fragment );

	WHATAFAG.url_processor.process_current_fragment();

	//======================================================================
	//	show the site frame template
	//======================================================================

	template_site_frame.set_active();
}

//======================================================================
//	ajax_refresh
//
//	a multi-step series of functions
//
//	clear and re-acquire the big global (account)
//======================================================================

function ajax_refresh( callback )
{
	ajax_refresh_stage_1( callback );
}

//======================================================================
//	get the big globals
//
//	using your current user authentication token (cookie value), get the
//	big data from the server
//======================================================================

function ajax_refresh_stage_1( callback )
{
	//======================================================================
	//	clear the big globals!
	//======================================================================

	resource_account_active = null;

	WHATAFAG
		.resource_library
		.resource( "/rest/multi/starter_pack" )
		.refresh()
		.done
		(
			function ( data )
			{
				resource_account_active = WHATAFAG.resource_library.resource( data.uri_account );
				observer_account_active.set_target( WHATAFAG.resource_library.resource( data.uri_account ) );

				ajax_refresh_stage_2( callback );
			}
		);
}

//======================================================================
//	set the account's location, if necessary
//======================================================================

function ajax_refresh_stage_2( callback )
{
	if ( resource_account_active.get( "location_method" ) === "automatic" && navigator.geolocation )
	{
		navigator.geolocation
			.getCurrentPosition
			(
				function ( position )
				{
					resource_account_active.set( { "location_latitude" : position.coords.latitude, "location_longitude" : position.coords.longitude } );
				},
				function ( error_code )
				{
					if ( error_code === 0 ) console.info( "Browser geolocation error" );
					else if ( error_code === 1 ) console.info( "Browser geolocation denied" );
					else if ( error_code === 2 ) console.info( "Browser geolocation unavailable" );
					else if ( error_code === 3 ) console.info( "Browser geolocation timeout" );
					else console.info( "Browser geolocation unexpected error" );
				},
				{ "enableHighAccuracy" : false, "timeout" : 60000, "maximumAge" : 50000 }
			);
	}

	ajax_refresh_stage_3( callback );
}

//======================================================================
//	skip
//======================================================================

function ajax_refresh_stage_3( callback )
{
	ajax_refresh_stage_4( callback );
}

function ajax_refresh_stage_4( callback )
{
	//======================================================================
	//	callback
	//======================================================================

	if ( typeof callback === "function" ) callback();
}

//======================================================================
//	fetch_authentication_token
//======================================================================

function fetch_authentication_token()
{
	try
	{
		reset_ping_timer();

		return jQuery.ajax
			(
				{
					beforeSend : function ( xhr ) { if ( get_cookie( name_cookie ) ) { xhr.setRequestHeader( key_authorization_header, get_cookie( name_cookie ) ); } },
					cache : true,
					dataType : "jsonp",
					type : "GET",
					url : protocol_domain_port_main + "/resources/pages/fetch_authentication_token.php"
				}
			);
	}
	catch ( err )
	{
		console.info( "Error #2 upgrading authentication token" );
	}
}

//======================================================================
//	queue_messages
//======================================================================

var queue_messages =
	{
		list_properties_messages : [],

		//	all of these properties must be set (timeout_length excepted, if do_timeout is false)
		//
		//	j_message: jQuery object; required, contains the message
		//
		//	id_external : record id of the message, if this message came from the server
		//	do_timeout : boolean
		//	timeout_length : integer; milliseconds
		//	allow_removal : boolean; can this message be removed by later messages
		//	class_container : string; class to assign to the container (practically, this could indicate "severity")

		create :
			function ( hash_properties_message )
			{
				//	create an id for this message

				hash_properties_message[ "uri" ] = make_id();

				//	add the message to the end of the list

				this.list_properties_messages.push( hash_properties_message );

				//	if there are existing messages on the list that can be replaced, remove them

				var list_ids_messages = [];

				for ( var index = 0; index < this.list_properties_messages.length - 1; index++ )
				{
					if ( this.list_properties_messages[ index ][ "allow_removal" ] )
					{
						list_ids_messages.push( this.list_properties_messages[ index ][ "uri" ] );
					}
				}

				this.delete_messages( list_ids_messages );

				//	update the display

				this.update_display();
			},

		delete_messages :
			function ( list_ids_messages )
			{
				//	search for the messages in the list and delete them

				for ( var index1 = 0; index1 < list_ids_messages.length; index1++ )
				{
					for ( var index2 = 0; index2 < this.list_properties_messages.length; index2++ )
					{
						if ( list_ids_messages[ index1 ] === this.list_properties_messages[ index2 ][ "uri" ] )
						{
							//	notify the server if a message with an external id has been deleted

							if ( this.list_properties_messages[ index2 ][ "id_external" ] !== null ) update_message_state( this.list_properties_messages[ index2 ][ "id_external" ], "dismissed by target" );

							//	remove the message

							this.list_properties_messages.splice( index2, 1 );

							index2--;
						}
					}
				}

				//	update the display

				this.update_display();
			},

		update_display :
			function ()
			{
				var j_container_message_pane = jQuery( "#container_message_pane" );

				if ( this.list_properties_messages.length === 0 )
				{
					//	hide the message pane if there are no messages

					j_container_message_pane.fadeOut();

					//	update the last active message
		
					hash_properties_message_last_active = null;
				}
				else
				{
					var hash_properties_message = this.list_properties_messages[ 0 ];

					//	only update the display if there is a new message

					if ( hash_properties_message_last_active === null || hash_properties_message_last_active[ "uri" ] !== hash_properties_message[ "uri" ] )
					{
						//	update the last active message

						hash_properties_message_last_active = hash_properties_message;

						//	set the class for the container
						//	empty the container
						//	append the message

						j_container_message_pane
							.find( ".message_pane" )
							.removeClass()
							.addClass( "message_pane " + hash_properties_message[ "class_container" ] )
							.find( ".container_message" )
							.empty()
							.append( hash_properties_message[ "j_message" ] );

						//	activate the auto-dismiss timer

						if ( hash_properties_message[ "do_timeout" ] )
						{
							window.setTimeout( function () { queue_messages.delete_messages( [ hash_properties_message[ "uri" ] ] ); }, hash_properties_message[ "timeout_length" ] );
						}

						//	show the message pane if there are messages

						//	j_container_message_pane.fadeIn();
						j_container_message_pane.show();
					}
				}
			}
	};

//======================================================================
//	cookie functions
//	courtesy of http://www.elated.com/articles/javascript-and-cookies/
//======================================================================

function get_cookie( cookie_name )
{
	var results = document.cookie.match( '(^|;) ?' + cookie_name + '=([^;]*)(;|$)' );

	if ( results ) return ( unescape( results[ 2 ] ) );
	else return null;
}

function set_cookie( name, value, expY, expM, expD, path, domain, secure )
{
	var cookie_string = name + "=" + escape ( value );

	if ( expY )
	{
		var expires = new Date ( expY, expM, expD );
		cookie_string += "; expires=" + expires.toGMTString();
	}

	if ( path ) cookie_string += "; path=" + escape ( path );

	if ( domain ) cookie_string += "; domain=" + escape ( domain );

	if ( secure ) cookie_string += "; secure";

	document.cookie = cookie_string;
}

function delete_cookie( cookie_name, path, domain )
{
	if ( path === undefined ) path = "/";
	if ( domain === undefined ) domain = window.location[ "hostname" ];

	var cookie_date = new Date();	// current date & time
	cookie_date.setTime( cookie_date.getTime() - 1 );
	document.cookie = cookie_name + "=; expires=" + cookie_date.toGMTString() + "; path=" + path + "; domain=" + domain + ";";
}

//======================================================================
//	select_populate
//======================================================================

function select_populate( j_selects, options )
{
	j_selects
		.each
		(
			function ( index )
			{
				var j_select = jQuery( this );

				//	get the currently selected value

				var selected_option = j_select.val();

				//	clear the select list

				j_select.empty();

				//	populate the select box with at least one option

				if ( options.length == 0 )
				{
					j_select.append( "<option value=\"\"></option>" );	
				}
				else
				{
					jQuery
						.each
						(
							options,
							function ( index, value )
							{
								j_select.append( "<option value=\"" + value[ 0 ] + "\">" + value[ 1 ] + "</option>" );	
							}
						);
				}

				//	you must select at least one option (either the old value or the first option)

				if ( selected_option === null )
				{
					j_select.val( j_select.find( "option:first" ).val() );
				}
				else
				{
					j_select.val( selected_option );

					if ( j_select.val() != selected_option )
					{
						j_select.val( j_select.find( "option:first" ).val() );
					}
				}
			}
		);
}

//======================================================================
//	select_add_if_necessary
//======================================================================

function select_add_if_necessary( j_selects, option, do_select )
{
	j_selects
		.each
		(
			function ( index )
			{
				var j_select = jQuery( this );

				//	determine if the option is already in the list

				var exists = ( j_select.find( "option[value='" + option[ 0 ] + "']" ).length > 0 );

				//	if not, add it

				if ( !exists )
				{
					j_select.prepend( "<option value=\"" + option[ 0 ] + "\">" + option[ 1 ] + "</option>" );	
				}

				//	select it

				if ( do_select )
				{
					j_select.val( option[ 0 ] );
				}
			}
		);
}

//======================================================================
//	select_prepend
//======================================================================

function select_prepend( j_selects, options, use_new, use_existing )
{
	j_selects
		.each
		(
			function ( index )
			{
				var j_select = jQuery( this );

				//	get the currently selected value

				var selected_option = j_select.val();

				//	populate the select box with at least one option

				jQuery
					.each
					(
						options,
						function ( index, value )
						{
							//	use new values

							if ( use_new )
							{
								j_select.find( "option[value='" + value[ 0 ] + "']" ).remove();

								//	prepend the new option

								j_select.prepend( "<option value=\"" + value[ 0 ] + "\">" + value[ 1 ] + "</option>" );	
							}

							//	use existing values

							else if ( use_existing )
							{
							}
						}
					);

				//	you must select at least one option (either the old value or the first option)

				if ( selected_option === null )
				{
					j_select.val( j_select.find( "option:first" ).val() );
				}
				else
				{
					j_select.val( selected_option );

					if ( j_select.val() != selected_option )
					{
						j_select.val( jQuery( "option:first", j_select ).val() );
					}
				}
			}
		);
}

//======================================================================
//	pretty_date
//======================================================================

function pretty_date( timestamp_seconds_utc )
{
	var d = new Date();
	d.setTime( timestamp_seconds_utc * 1000 );
	return d.format( "MMM d, yyyy" );
}

//======================================================================
//	pretty_date_and_time
//======================================================================

function pretty_date_and_time( timestamp_seconds_utc )
{
	var d = new Date();
	d.setTime( timestamp_seconds_utc * 1000 );
	return d.format( "MMM d, yyyy, h:mm a" );
}

//======================================================================
//	pretty_time
//======================================================================

function pretty_time( timestamp_seconds_utc )
{
	var d = new Date();
	d.setTime( timestamp_seconds_utc * 1000 );
	return d.format( "h:mm a" );
}

//======================================================================
//	pretty_range
//======================================================================

function pretty_range( timestamp_seconds_range_start_utc, timestamp_seconds_range_end_utc, do_abbreviate, do_print_inclusive_only )
{
	//	set some defaults

	if ( typeof do_abbreviate === "undefined" ) do_abbreviate = true;
	if ( typeof do_print_inclusive_only === "undefined" ) do_print_inclusive_only = false;

	var d = new Date();
	var this_year = d.format( "yyyy" );

	if ( timestamp_seconds_range_start_utc !== null )
	{
		var d = new Date();
		d.setTime( timestamp_seconds_range_start_utc * 1000 );

		if ( do_abbreviate && d.format( "yyyy" ) === this_year )
		{
			var start = d.format( "MMM d" );
		}
		else
		{
			var start = d.format( "MMM d, yyyy" );
		}
	}

	if ( timestamp_seconds_range_end_utc !== null )
	{
		if ( do_print_inclusive_only ) timestamp_seconds_range_end_utc -= 1;	//	if we want to print inclusive dates only, we have to roll back the end time by 1 second.

		var d = new Date();
		d.setTime( timestamp_seconds_range_end_utc * 1000 );

		if ( do_abbreviate && d.format( "yyyy" ) === this_year )
		{
			var end = d.format( "MMM d" );
		}
		else
		{
			var end = d.format( "MMM d, yyyy" );
		}
	}

	if ( timestamp_seconds_range_start_utc === null && timestamp_seconds_range_end_utc === null )
	{
		return false;
	}
	else if ( timestamp_seconds_range_start_utc === null )
	{
		return "Ends " + end;
	}
	else if ( timestamp_seconds_range_end_utc === null )
	{
		return "Starts " + start;
	}
	else
	{
		return start + " to " + end;
	}
}

//======================================================================
//	get_timestamp_seconds_UTC_start_of_today_local
//======================================================================

function get_timestamp_seconds_UTC_start_of_today_local()
{
	return Math.round( Date.parse( pretty_date( Math.round( Date.now() / 1000 ) ) ) / 1000 );
}

//======================================================================
//	get_timestamp_seconds_UTC_start_of_datetime_local
//======================================================================

function get_timestamp_seconds_UTC_start_of_datetime_local( datetime_local )
{
	//	turn text_date into timestamp
	//	turn timestamp into DD MM, YYYY
	//	turn that string into timestamp

	var t = Date.parse( datetime_local );

	var d = new Date();
	d.setTime( t );
	var start = d.format( "MMM d, yyyy" );

	var t = Date.parse( start );

	return Math.round( t / 1000 );
}

//======================================================================
//	trim
//======================================================================

function trim( s )
{
	return s.replace( /^[\r\n\t ]*|[\r\n\t ]*$/gi, "" );
}

//======================================================================
//	make_id
//======================================================================

function make_id( length )
{
	if ( typeof length === "undefined" ) length = 8;

	var text = "";
	var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

	for( var i=0; i < length; i++ )
		text += possible.charAt(Math.floor(Math.random() * possible.length));

	return text;
}

//======================================================================
//	css_select
//======================================================================

function css_select( j_element )
{
	j_element.addClass( "selected" ).siblings().removeClass( "selected" );
}

//======================================================================
//	messages
//======================================================================

function handle_messages( list_properties_messages )
{
	if ( typeof list_properties_messages === "undefined" ) return false;

	for ( var index = 0; index < list_properties_messages.length; index++ ) handle_message( list_properties_messages[ index ] );

	return true;
}

function handle_message( hash_properties_message )
{
	//	if we got a "message" instead of a "j_message", transform it

	if ( typeof hash_properties_message[ "message" ] !== "undefined" )
	{
		hash_properties_message[ "j_message" ] = jQuery( "<div>" + hash_properties_message[ "message" ] + "</div>" );
		hash_properties_message[ "message" ] = null;
	}

	//	check data

	if ( typeof hash_properties_message[ "j_message" ] === "undefined" ) { alert( "No j_message set" ); return false; }

	//	set some defaults

	if ( typeof hash_properties_message[ "id_external" ] === "undefined" ) hash_properties_message[ "id_external" ] = null;
	if ( typeof hash_properties_message[ "do_timeout" ] === "undefined" ) hash_properties_message[ "do_timeout" ] = false;
	if ( typeof hash_properties_message[ "timeout_length" ] === "undefined" ) hash_properties_message[ "timeout_length" ] = 10000;
	if ( typeof hash_properties_message[ "allow_removal" ] === "undefined" ) hash_properties_message[ "allow_removal" ] = true;
	if ( typeof hash_properties_message[ "class_container" ] === "undefined" ) hash_properties_message[ "class_container" ] = "normal";

	//	let the server know you've received the message

	if ( hash_properties_message[ "id_external" ] !== null )
	{
		update_message_state( hash_properties_message[ "id_external" ], "received by target" );
	}

	//	put the message in the queue

	queue_messages.create( hash_properties_message );
}

function handle_message_simple( j_message )
{
	handle_message
	(
		{
			"j_message" : j_message
		}
	);
}

function handle_message_auto_dismiss( j_message, delay )
{
	if ( typeof delay === "undefined" ) delay = 60;

	handle_message
	(
		{
			"j_message" : j_message,
			"do_timeout" : true,
			"timeout_length" : delay * 1000
		}
	);
}

function dismiss_message_last_active()
{
	if ( hash_properties_message_last_active !== null )
	{
		queue_messages.delete_messages( [ hash_properties_message_last_active[ "uri" ] ] );
	}
}

//======================================================================
//	insertAtCaret
//
//	from http://www.scottklarr.com/topic/425/how-to-insert-text-into-a-textarea-where-the-cursor-is/
//======================================================================

function insertAtCaret( txtarea, text )
{
	var scrollPos = txtarea.scrollTop;
	var strPos = 0;
	var br = ((txtarea.selectionStart || txtarea.selectionStart == '0') ? 
		"ff" : (document.selection ? "ie" : false ) );
	if (br == "ie") { 
		txtarea.focus();
		var range = document.selection.createRange();
		range.moveStart ('character', -txtarea.value.length);
		strPos = range.text.length;
	}
	else if (br == "ff") strPos = txtarea.selectionStart;
	
	var front = (txtarea.value).substring(0,strPos);
	var back = (txtarea.value).substring(strPos,txtarea.value.length); 
	txtarea.value=front+text+back;
	strPos = strPos + text.length;
	if (br == "ie") { 
		txtarea.focus();
		var range = document.selection.createRange();
		range.moveStart ('character', -txtarea.value.length);
		range.moveStart ('character', strPos);
		range.moveEnd ('character', 0);
		range.select();
	}
	else if (br == "ff") {
		txtarea.selectionStart = strPos;
		txtarea.selectionEnd = strPos;
		txtarea.focus();
	}
	txtarea.scrollTop = scrollPos;
}

//======================================================================
//	back
//======================================================================

function back( event, j_element )
{
	history.back();
}

//======================================================================
//	Object.create
//
//	from douglas crockford as of september 2010
//======================================================================

if ( typeof Object.create !== "function" )
{
	Object.create = function ( o )
	{
		function F() {}
		F.prototype = o;
		return new F();
	};
}

//======================================================================
//	Date.now
//======================================================================

if ( !Date.now )
{
	Date.now =
		function now()
		{
			return +new Date();
		};
}

//======================================================================
//	make_in_array
//======================================================================

function make_in_array( needle, haystack )
{
	var needle_return;

	if ( haystack.indexOf( needle ) === -1 )
	{
		needle_return = haystack[ 0 ];
	}
	else
	{
		needle_return = needle;
	}

	return needle_return;
}

//======================================================================
//	condition_for_event
//
//	create a function that can be used as an event handler (such as a "click" event)
//
//	the purpose of the function you create is to call the function that gets PASSED IN.
//
//	this function basically acts as a conditioner.
//======================================================================

function condition_for_event( function_f )
{
	var new_f =
		function ( event )
		{
			event.preventDefault();

			var j_element = jQuery( this );

			if ( j_element.is( ".disabled" ) ) return;

			function_f( event, j_element );
		};

	return new_f;
}

//======================================================================
//	submit_form_log_out
//======================================================================

function submit_form_log_out()
{
	request_resource_delete( "/rest/authentication_tokens/" + get_cookie( name_cookie ) )
		.done
		(
			function ()
			{
				//	delete the cookie

				delete_cookie( name_cookie );

				//	delete all rendered components
				//	manually clear our the rendered component div just to be safe

				jQuery.each( WHATAFAG.components, function ( key, value ) { WHATAFAG.components[ key ].delete_templates_children( undefined, true ); WHATAFAG.components[ key ].destroy(); } );
				WHATAFAG.components = {};
				jQuery( "#container_rendered_components" ).empty();

				//	get a new cookie immediately

				fetch_authentication_token()
					.done
					(
						function ( data, text_status, xml_http_request )
						{
							//======================================================================
							//	ajax_refresh
							//======================================================================

							ajax_refresh
							(
								function ()
								{
									WHATAFAG.url_processor.push_new_fragment( "#!/log_out" );
									handle_message_auto_dismiss( "<p>You have been logged off.</p>", 2 );
								}
							);
						}
					);
			}
		);
}

//======================================================================
//	google_api_loaded
//======================================================================

function google_api_loaded()
{
}

//======================================================================
//	delay_until_visible
//
//	wait up to 20 seconds for an element to become visible
//======================================================================

function delay_until_visible( j_template, callback )
{
	var counter = 0;
	var id_interval =
		window.setInterval
		(
			function ()
			{
				if ( j_template.is( ":visible" ) )
				{
					window.clearInterval( id_interval );

					callback();
				}
				else if ( counter === 200 )
				{
					window.clearInterval( id_interval );

					console.info( "Warning : An interval timer was not stopped" );
				}

				counter++;
			},
			100
		);
	
	return id_interval;
}

//======================================================================
//	extract_tease_from_html
//======================================================================

function extract_tease_from_html( j_content )
{
	if ( typeof j_content === "string" ) j_content = jQuery( j_content );

	//	replace <img>s, <br>s and <hr>s with spaces

	j_content.find( "img,br,hr" ).remove();

	//	remove empty paragraphs

	j_content.find( "p" ).each( function ( index ) { var j_element = jQuery( this ); if ( trim( j_element.html() ) === "" ) j_element.remove(); } );

	//	remove all but the first paragraph

	j_content.find( "p:gt(0)" ).remove();

	//	return the first 512 characters of the first paragraph

	var text = j_content.text();

	if ( text.length > 512 )
	{
		text = text.substr( 0, 509 ) + "…";
	}

	return text;
}

//======================================================================
//	create_folded_html
//
//	wrap everything after the first <hr> in a hidden div
//	replace the <hr> with a button that toggles the hidden div
//======================================================================

function create_folded_html( j_content )
{
	var j_hidden = jQuery( "<div style=\"display: none;\"></div>" );

	j_hidden.append( j_content.find( "hr:eq(0) ~ *" ) );

	var j_toggle = jQuery( "<a href=\"#\" class=\"container_more link_action\">See more...</a>" );

	j_toggle.click( condition_for_event( function () { j_hidden.toggle(); } ) );

	j_content.find( "hr:eq(0)" ).replaceWith( jQuery( "<p></p>" ).append( j_toggle ) );

	j_content.append( j_hidden );

	return j_content;
}

//======================================================================
//	collect_resources
//
//	take a list of uris and attempt to fetch all the resources.
//	for each resource returned, add it to a collector object.
//	once all the resources are returned (or failed), reorganize the resources from the collector into a list in the same order as the original list of uris.
//	call the callback with the list.
//======================================================================

function collect_resources( list_uris_resources )
{
	var list_resource_refresh_promise = jQuery.map( list_uris_resources, function ( uri_resource, index ) { return WHATAFAG.resource_library.resource( uri_resource ).refresh(); } );

	return jQuery.when.apply( jQuery, list_resource_refresh_promise );	//	the jQuery.when.apply syntax is a weirdness necessary to pass an ARRAY of deferreds to then when function (it doesn't natively accept an array)
}

//======================================================================
//	mergeo
//======================================================================

function mergeo ( rows, different_key )
{
	var row, previous_row, next_row;
	var is_first_row, is_last_row;
	var output_rows = [];

	for ( var counter = 0; counter < rows.length; counter++ )
	{
		is_first_row = ( counter === 0 );
		is_last_row = ( counter === rows.length - 1 );

		row = rows[ counter ];
		if ( !is_first_row ) previous_row = rows[ counter - 1 ];
		if ( !is_last_row ) next_row = rows[ counter + 1 ];

		if ( is_first_row || previous_row[ different_key ] !== row[ different_key ] )
		{
			//	new output row

			var t = row;

			t[ "list_" + different_key ] = [ row[ different_key ] ];
			delete t[ different_key ];

			output_rows.push( t );
		}
		else
		{
			//	add to previous row

			output_rows[ output_rows.length - 1 ][ "list_" + different_key ].push( row[ different_key ] );
		}
	}

	return output_rows;
}

//======================================================================
//	twitter-entities.js
//	
//	This function converts a tweet with "entity" metadata from plain text to linkified HTML.
//	
//	See the documentation here: http://dev.twitter.com/pages/tweet_entities
//	Basically, add ?include_entities=true to your timeline call
//	
//	Copyright 2010, Wade Simmons Licensed under the MIT license http://wades.im/mons
//======================================================================

function escape_html( text )
{
	return $('<div/>').text(text).html()
}

function linkify_entities( tweet )
{
	if (!(tweet.entities))
	{
		return escape_html(tweet.text)
	}

	// This is very naive, should find a better way to parse this

	var index_map = {}

	$.each(tweet.entities.urls, function(i,entry) {
		index_map[entry.indices[0]] = [entry.indices[1], function(text) {return "<a href='"+escape_html(entry.url)+"'>"+escape_html(text)+"</a>"}]
	})

	$.each(tweet.entities.hashtags, function(i,entry) {
		index_map[entry.indices[0]] = [entry.indices[1], function(text) {return "<a href='http://twitter.com/search?q="+escape("#"+entry.text)+"'>"+escape_html(text)+"</a>"}]
	})

	$.each(tweet.entities.user_mentions, function(i,entry) {
		index_map[entry.indices[0]] = [entry.indices[1], function(text) {return "<a title='"+escape_html(entry.name)+"' href='http://twitter.com/"+escape_html(entry.screen_name)+"'>"+escape_html(text)+"</a>"}]
	})

	var result = ""
	var last_i = 0
	var i = 0

	// iterate through the string looking for matches in the index_map
	for (i=0; i < tweet.text.length; ++i)
	{
		var ind = index_map[i]

		if (ind)
		{
			var end = ind[0]
			var func = ind[1]
			if (i > last_i)
			{
				result += escape_html(tweet.text.substring(last_i, i))
			}
			result += func(tweet.text.substring(i, end))
			i = end - 1
			last_i = end
		}
	}

	if (i > last_i)
	{
		result += escape_html(tweet.text.substring(last_i, i))
	}

	return result
}

//======================================================================
//	diff
//======================================================================

function diff ( a, b )
{
	if ( a === undefined ) a = {};
	if ( b === undefined ) b = {};

	var diff = underscore.clone( b );

	jQuery
		.each
		(
			a,
			function ( key, value )
			{
				if ( diff[ key ] !== undefined && underscore.isEqual( a[ key ], diff[ key ] ) )
				{
					delete diff[ key ];
				}
			}
		);

	return diff;
}

//======================================================================
//	find_applicable_nodes
//======================================================================

function find_applicable_nodes ( j_template )
{
	return j_template.find( "*" ).not( j_template.find( ".template .template" ).add( j_template.find( ".template .template *" ) ) );

	/*
	var r = jQuery();

	r = r.add( j_template );

	j_template
		.children()
		.not( ".template" )
		.each
		(
			function ( index )
			{
				r = r.add( find_applicable_nodes( jQuery( this ) ) );
			}
		);

	return r;
	*/
}

//======================================================================
//	reset_ping_timer
//======================================================================

function reset_ping_timer ()
{
	if ( window.id_timeout_sleep !== null ) window.clearTimeout( window.id_timeout_sleep );

	window.id_timeout_sleep =
		window.setTimeout
		(
			function ()
			{
				send_ping();
			},
			1000*60*5	/* 5 minutes */
		);
}

//======================================================================
//	send_ping
//======================================================================

function send_ping ()
{
	request_resource_create( "/rest/pings", {}, undefined, undefined, false, false );
}
//======================================================================
//	request_resource_show
//======================================================================

function request_resource_show( uri )
{
	return WHATAFAG.resource_library.resource( uri ).refresh();
}

//======================================================================
//	request_resource_create
//======================================================================

function request_resource_create( name_resource, parameters, callback_success, callback_failure, do_fetch_on_success, do_expire_all_resources )
{
	//	defaults

	if ( do_fetch_on_success === undefined ) do_fetch_on_success = true;
	if ( do_expire_all_resources === undefined ) do_expire_all_resources = true;

	//	create

	var dfd = jQuery.Deferred();

	perform_ajax_to_server( "POST", name_resource, parameters )
		.then
		(
			function ( data, text_status, xml_http_request )
			{
				var uri = data[ "uri" ];

				//	caching is never perfect, but it's usually ACCEPTABLE when we're in read-only mode;
				//	however here we have performed a create, update or delete, and caused an unpredictable number of resources to become immediately stale;
				//	the easiest way to re-achieve consistency (in our usual read-only mode) is to expire all resources

				if ( do_expire_all_resources ) WHATAFAG.resource_library.expire_all();

				if ( do_fetch_on_success )
				{
					request_resource_show( uri )
						.done
						(
							function ( properties )
							{
								dfd.resolve( properties );

								if ( typeof callback_success === "function" ) callback_success( properties );
							}
						);
				}
				else
				{
					dfd.resolve( uri );

					if ( typeof callback_success === "function" ) callback_success( uri );
				}
			},
			function ( xml_http_request, text_status, error_thrown )
			{
				dfd.reject( xml_http_request, text_status, error_thrown );

				if ( typeof callback_failure === "function" ) callback_failure( properties );
			}
		);
	
	return dfd.promise();
}

//======================================================================
//	request_resource_update
//======================================================================

function request_resource_update( uri, parameters, do_fetch_on_success, do_expire_all_resources )
{
	//	defaults

	if ( do_fetch_on_success === undefined ) do_fetch_on_success = true;
	if ( do_expire_all_resources === undefined ) do_expire_all_resources = true;

	//	update

	var dfd = jQuery.Deferred();

	perform_ajax_to_server( "PUT", uri, parameters )
		.then
		(
			function ( data, text_status, xml_http_request )
			{
				//	caching is never perfect, but it's usually ACCEPTABLE when we're in read-only mode;
				//	however here we have performed a create, update or delete, and caused an unpredictable number of resources to become immediately stale;
				//	the easiest way to re-achieve consistency (in our usual read-only mode) is to expire all resources

				if ( do_expire_all_resources ) WHATAFAG.resource_library.expire_all();

				//	chrome and safari do not appear to invalidate the cache on PUT
				//	force a re-fetch

				WHATAFAG.resource_library.resource( uri )
					.refresh( false )
					.then
					(
						function ( properties )
						{
							if ( do_fetch_on_success )
							{
								dfd.resolve( properties );
							}
							else
							{
								dfd.resolve( uri );
							}
						},
						function ( xml_http_request, text_status, error_thrown )
						{
							dfd.reject( xml_http_request, text_status, error_thrown );
						}
					);
			},
			function ( xml_http_request, text_status, error_thrown )
			{
				dfd.reject( xml_http_request, text_status, error_thrown );

				if ( typeof callback_failure === "function" ) callback_failure( properties );
			}
		);
	
	return dfd.promise();
}

//======================================================================
//	request_resource_delete
//======================================================================

function request_resource_delete( uri, do_expire_all_resources )
{
	//	defaults

	if ( do_expire_all_resources === undefined ) do_expire_all_resources = true;

	var dfd = jQuery.Deferred();

	perform_ajax_to_server( "DELETE", uri )
		.then
		(
			function ( data, text_status, xml_http_request )
			{
				//	caching is never perfect, but it's usually ACCEPTABLE when we're in read-only mode;
				//	however here we have performed a create, update or delete, and caused an unpredictable number of resources to become immediately stale;
				//	the easiest way to re-achieve consistency (in our usual read-only mode) is to expire all resources

				if ( do_expire_all_resources ) WHATAFAG.resource_library.expire_all();

				//	callback_success

				dfd.resolve();
			},
			function ( xml_http_request, text_status, error_thrown )
			{
				dfd.reject( xml_http_request, text_status, error_thrown );
			}
		);
	
	return dfd.promise();
}

//======================================================================
//	request_resource_search
//======================================================================

function request_resource_search( url, do_send_authentication_token, hash_properties_query, do_fetch_from_cache, context )
{
	var dfd = jQuery.Deferred();

	if ( hash_properties_query === undefined ) hash_properties_query = {};
	var url = jQuery.param.querystring( url, hash_properties_query );

	WHATAFAG.resource_library.resource( url )
		.refresh( do_fetch_from_cache, context )
		.then
		(
			function ( data, text_status, xml_http_request )
			{
				dfd.resolve( data[ "list_uris_resources" ], data[ "count_total_resources_possible" ], data[ "index_resource_first" ] );
			},
			function ( xml_http_request, text_status, error_thrown )
			{
				dfd.reject( xml_http_request, text_status, error_thrown );
			}
		);

	return dfd.promise();
}




















//======================================================================
//	perform_ajax_to_server
//======================================================================

function perform_ajax_to_server( verb, url, hash_properties_query, callback_success, callback_failure, context )
{
	//	defaults

	if ( hash_properties_query === undefined ) hash_properties_query = {};

	var cookie_value;

	//	you need to have a cookie set in order to make an ajax call

	if ( ( cookie_value = get_cookie( name_cookie ) ) === null )
	{
		handle_message_simple( "<p>Your cookie has expired. We're sorry about that (it shouldn't happen very often, or at all).</p><p>You can continue by refreshing the page.</p>" );

		return false;
	}
	else
	{
		var ajax_options =
			{
				"beforeSend" : function ( xml_http_request ) { xml_http_request.setRequestHeader( key_authorization_header, cookie_value ); xml_http_request.setRequestHeader( "X-HTTP-METHOD-OVERRIDE", verb ); },
				"cache" : false,	//	change to true for production!
				"data" : hash_properties_query,
				"dataType" : "json",
				"type" : verb,
				"url" : url
			};

		if ( callback_success !== undefined )
		{
			ajax_options.success = function ( data, text_status, xml_http_request ) { callback_success.call( this, data, text_status, xml_http_request ); };
		}

		if ( callback_failure !== undefined )
		{
			ajax_options.error = callback_failure || handle_ajax_failure;
		}

		if ( context !== undefined && context !== null )
		{
			ajax_options[ "context" ] = context;
		}

		reset_ping_timer();

		return jQuery.ajax( ajax_options );
	}
}

//======================================================================
//	handle_ajax_failure
//
//	possible values for the second argument are null, "timeout", "error",
//	"notmodified" and "parsererror"
//======================================================================

function handle_ajax_failure( xml_http_request, text_status, error_thrown )
{
	if ( xml_http_request.responseText )
	{
		handle_message
		(
			{
				"seconds_offset_created" : 0,
				"seconds_offset_expires" : 60,
				"type_source" : "client",
				"state" : "sent to target",
				"display_method" : "passive",
				"severity" : "critical",
				"auto_dismiss" : true,
				"message" : xml_http_request.responseText
			}
		);
	}
	else
	{
		handle_message
		(
			{
				"seconds_offset_created" : 0,
				"seconds_offset_expires" : 60,
				"type_source" : "client",
				"state" : "sent to target",
				"display_method" : "passive",
				"severity" : "critical",
				"auto_dismiss" : true,
				"message" : "<p>Whoops! There was an unexpected problem talking to the server. Please try again.</p>"
			}
		);
	}
}

//======================================================================
//	handle_update_fragment
//======================================================================

function handle_update_fragment( event )
{
	WHATAFAG.url_processor.process_current_fragment();
}

//======================================================================
//	get_href_for_account
//======================================================================

function get_href_for_account( hash_properties_account )
{
	return "#!/hottie;username=" + hash_properties_account.username;
}

//======================================================================
//
//	an observer is an object which "watches" a resource in the resource library.
//
//	an observer can watch no object, such as when it's first initialized or when unset_target() is called.
//	an observer can only watch one resource at a time, and it does so by calling set_target().
//	an observer can switch from observing one resource to another by calling set_target() again.
//
//	an observer fires a "change" event when the resource it is observing changes the value of its properties.
//	an observer fires an "unset" event when it stops watching a resource (vie unset_target()).
//	an observer fires an "unavailable" event when the resource it is observing becomes unavailable.
//
//	an observer can forward a refresh() to its resource if it is observing one.
//
//	an observer that is pointed to a new resource will always fire a "change" event with the new resource.
//
//======================================================================

var new_observer =
	function ( callback_changed /* convenience argument */ )
	{
		var that = {}
		
		that.event_manager = WHATAFAG.new_event_manager();

		that.target = undefined;

		that.handle_target_change = function ( a, b ) { that.event_manager.trigger( "change", a, b ); };
		that.handle_target_unavailable = function () { that.event_manager.trigger( "unavailable" ); };
		that.handle_target_expire = function () { that.event_manager.trigger( "expire" ); };

		that.refresh =
			function ()
			{
				if ( that.target )
				{
					var promise = that.target.refresh();

					return promise;
				}
				else
				{
					//	console.info( "no target to refresh" );
				}
			};

		that.set_target =
			function ( target )
			{
				var that = this;
				
				//	re-pointing to the same target does nothing

				if ( underscore.isEqual( target, that.target ) )
				{
				}

				//	point to a new target

				else
				{
					//	remove "change" and "unavailable" handlers from a previous target

					if ( that.target ) that.unbind_all();

					//	set up new target

					that.target = target;

					//	refresh the target
					//
					//	attach "change" and "unavailable" handlers to the target

					that.target.refresh_and_add_change_callback( that.handle_target_change );
					that.target.event_manager.bind( "unavailable", that.handle_target_unavailable );
					that.target.event_manager.bind( "expire", that.handle_target_expire );
				}

				return that;
			};

		that.unset_target =
			function ()
			{
				var that = this;

				if ( that.target )
				{
					that.unbind_all();

					that.target = undefined;

					that.event_manager.trigger( "unset" );
				}

				return that;
			};

		that.bind =
			function ( event_name, callback, do_now )
			{
				if ( do_now === undefined ) do_now = false;

				that.event_manager.bind( event_name, callback );

				if ( do_now )
				{
					if ( event_name === "unset" )
					{
						if ( !that.target )
						{
							callback();
						}
					}

					if ( event_name === "change" )
					{
						if ( that.target )
						{
							that.target.refresh().done( function ( resource_properties ) { callback( that.target.properties, that.target.properties ); } );
						}
					}
				}

				return that;
			};

		that.unbind = function () { that.event_manager.unbind_by_function.apply( that.event_manager, arguments ); };	//	alias for older code

		that.unbind_all =
			function ()
			{
				var that = this;

				//	remove "change" and "unavailable" handlers from the target

				if ( that.target )
				{
					that.target.event_manager.unbind_by_function( "change", that.handle_target_change );
					that.target.event_manager.unbind_by_function( "unavailable", that.handle_target_unavailable );
					that.target.event_manager.unbind_by_function( "expire", that.handle_target_expire );
				}

				return that;
			};

		that.destroy =
			function ()
			{
				var that = this;

				if ( that.target )
				{
					that.unbind_all();	//	unbind things that we bind TO

					that.target = undefined;
				}

				that.event_manager.unbind_all();	//	unbind things that bind TO US

				return that;
			};

		if ( typeof callback_changed !== "undefined" )
		{
			that.bind( "change", callback_changed );
		}

		return that;
	};

//======================================================================
//
//	WHATAFAG
//
//======================================================================

var WHATAFAG = {};

WHATAFAG.components = {};
WHATAFAG.validators = {};
WHATAFAG.actions = {};

//======================================================================
//
//	url_processor
//
//======================================================================

WHATAFAG.url_processor =
	{
		//======================================================================
		//	push_new_fragment
		//
		//	accepts partial/relative urls, fills them out as necessary, then pushes them out
		//======================================================================

		"push_new_fragment" :
			function ( fragment_new_partial )
			{
				var fragment_new_complete = WHATAFAG.url_processor.flesh_out_fragment( fragment_new_partial );

				jQuery.bbq.pushState( "#!" + fragment_new_complete, 2 );
			},

		//======================================================================
		//	process_current_fragment
		//======================================================================

		"process_current_fragment" :
			function ()
			{
				//======================================================================
				//	get and parse the fragment
				//======================================================================

				var fragment_partial = jQuery.param.fragment();

				var hash_fragment_parts_partial = WHATAFAG.url_processor.get_hash_fragment_parts( fragment_partial );
				var hash_fragment_parts_complete = WHATAFAG.url_processor.supplement_hash_fragment_parts( hash_fragment_parts_partial );

				//======================================================================
				//	change the fragment watcher
				//======================================================================

				watched_fragment.set( hash_fragment_parts_complete );

				return true;
			},

		//======================================================================
		//	get_hash_fragment_parts
		//======================================================================

		"get_hash_fragment_parts" :
			function ( fragment )
			{
				var list_matches;

				//	find as many valid parts of the fragment as possible
				//
				//	if the fragment is impenetrable, don't fail completely, just set the groups to undefined
				//
				//	fragments have a general 3-part structure :
				//
				//		#!
				//		page_command : /log_in or /switch_community
				//		page_arguments : ;a=1&b=2
				
				list_matches = fragment.match( /^#?!?\/?([^;]+)?(;.*)?$/ );

				if ( list_matches === null )
				{
					list_matches = [ null, undefined, undefined ];
				}

				var page_command = ( list_matches[ 1 ] ? list_matches[ 1 ] : undefined );	//	IE work-around; unmatched groups SHOULD be undefined, but IE gives ""
				var page_arguments = ( list_matches[ 2 ] ? list_matches[ 2 ] : undefined );

				//	if the page_command wasn't given

				if ( page_command === undefined )
				{
				}
				else
				{
					list_matches = page_command.match( /\/?(.+)/ );

					if ( list_matches === null )
					{
						page_command = undefined;
					}
					else
					{
						page_command = list_matches[ 1 ];
					}
				}

				//	if the page_argument wasn't given

				if ( page_arguments === undefined )
				{
				}
				else
				{
					list_matches = page_arguments.match( /;(.*)/ );

					page_arguments = jQuery.deparam.fragment( list_matches[ 1 ] );
				}

				var hash_fragment_parts =
					{
						"page_command" : page_command,
						"page_arguments" : page_arguments
					};

				return hash_fragment_parts;
			},

		//======================================================================
		//	supplement_hash_fragment_parts
		//======================================================================

		"supplement_hash_fragment_parts" :
			function ( hash_fragment_parts )
			{
				//	page_command defaults to the "profile_browser" or "org_home" components

				if ( hash_fragment_parts[ "page_command" ] === undefined )
				{
					hash_fragment_parts[ "page_command" ] = "profile_browser";
					hash_fragment_parts[ "page_arguments" ] = undefined;
				}

				hash_fragment_parts[ "complete_url" ] =
					"#!"
					+ "/"
					+ hash_fragment_parts[ "page_command" ]
					+ ( hash_fragment_parts[ "page_arguments" ] === undefined ? "" : ";" + jQuery.param( hash_fragment_parts[ "page_arguments" ] ) );

				return hash_fragment_parts;
			},

		//======================================================================
		//	flesh_out_fragment
		//======================================================================

		"flesh_out_fragment" :
			function ( fragment_partial )
			{
				var hash_fragment_parts_partial = WHATAFAG.url_processor.get_hash_fragment_parts( fragment_partial );
				var hash_fragment_parts_complete = WHATAFAG.url_processor.supplement_hash_fragment_parts( hash_fragment_parts_partial );
				return hash_fragment_parts_complete[ "complete_url" ];
			},
	};

//======================================================================
//
//	a watched object is an object which is "watched" for changes
//
//	a watched object starts out as an empty object, {}
//
//	a watched object fires a "change" event when one of its properties is changed, added or deleted.
//
//	the .set and .refresh functions return promises, in order to mimic the behaviour of higher-level,
//	asynchronous extensions of this object (such as a watched object where the data is on the server)
//
//======================================================================

WHATAFAG.watched_async_object =
	(
		function ()
		{
			var that = {};

			//======================================================================
			//	set
			//
			//	completely replace the previous values
			//======================================================================

			that.set =
				function ( properties_new )
				{
					var that = this;

					var properties_old = underscore.clone( that.properties );

					that.properties = properties_new;

					var properties_changed = diff( properties_old, that.properties );

					if ( !underscore.isEqual( properties_changed, {} ) )
					{
						that.event_manager.trigger( "change", that.properties, properties_changed );
					}

					var dfd = jQuery.Deferred();

					dfd.resolve( that.properties );

					return dfd.promise();
				};

			that.get =
				function ( key )
				{
					var that = this;

					if ( that.properties[ key ] === undefined ) return undefined;
					else return that.properties[ key ];
				};

			that.refresh =
				function ()
				{
					var that = this;

					var dfd = jQuery.Deferred();

					dfd.resolve( that.properties );

					return dfd.promise();
				};

			that.refresh_and_add_change_callback =
				function ( callback_change )
				{
					var that = this;

					var promise = that.refresh();

					promise
						.done
						(
							function ( a )
							{
								that.event_manager.bind( "change", callback_change );

								callback_change.call( that, a, a );
							}
						);

					return promise;
				};

			that.destroy =
				function ()
				{
					var that = this;

					that.properties = {};

					that.event_manager.unbind_all();

					return that;
				};

			return that;
		}()
	);

WHATAFAG.new_watched_async_object =
	function ( properties, event_manager )
	{
		if ( properties === undefined ) properties = {};
		if ( event_manager === undefined ) event_manager = WHATAFAG.new_event_manager();

		var watched_async_object = Object.create( WHATAFAG.watched_async_object );

		watched_async_object.id = make_id( 16 );
		watched_async_object.event_manager = event_manager;

		watched_async_object.properties = {};	//	set a starting point
		watched_async_object.set( properties );	//	then set the properties, triggering a change event if appropriate

		return watched_async_object;
	};

//======================================================================
//
//	event_manager
//
//======================================================================

WHATAFAG.event_manager =
	(
		function ()
		{
			var that = {};

			that.trigger =
				function ()
				{
					var that = this;

					var event_name = arguments[ 0 ];
					var args = [];
					for ( var counter = 1; counter < arguments.length; counter++ ) args.push( arguments[ counter ] );

					if ( that.callbacks[ event_name ] !== undefined )
					{
						jQuery
							.each
							(
								that.callbacks[ event_name ],
								function ( id_event, callback )
								{
									callback.apply( that, args );
								}
							);
					}

					return that;
				};

			that.bind =
				function ( event_name, callback, id_event )
				{
					var that = this;

					if ( id_event === undefined ) id_event = make_id( 16 );

					if ( that.callbacks[ event_name ] === undefined ) that.callbacks[ event_name ] = {};

					that.callbacks[ event_name ][ id_event ] = callback;	//	only one callback for each event-name+event-id combo

					return id_event;
				};

			that.unbind_by_id =
				function ( id_event_target )
				{
					var that = this;

					jQuery
						.each
						(
							that.callbacks,
							function ( event_name, list_id_event )
							{
								if ( that.callbacks[ event_name ][ id_event_target ] !== undefined ) delete that.callbacks[ event_name ][ id_event_target ]
							}
						);

					return that;
				};

			that.unbind_by_function =
				function ( event_name_target, callback_target )
				{
					var that = this;

					if ( that.callbacks[ event_name_target ] !== undefined )
					{
						jQuery
							.each
							(
								that.callbacks[ event_name_target ],
								function ( id_event, callback )
								{
									if ( callback === callback_target )
									{
										delete that.callbacks[ event_name_target ][ id_event ]
									}
								}
							);
					}

					return that;
				};

			that.unbind_all =
				function ()
				{
					var that = this;

					jQuery
						.each
						(
							that.callbacks,
							function ( event_name, list_id_event )
							{
								jQuery
									.each
									(
										list_id_event,
										function ( id_event, callback )
										{
											delete that.callbacks[ event_name ][ id_event ];
										}
									);
							}
						);
				};

			return that;
		}()
	);

WHATAFAG.new_event_manager =
	function ()
	{
		var event_manager = Object.create( WHATAFAG.event_manager );

		event_manager.callbacks = {};

		return event_manager;
	};

//======================================================================
//
//	template
//
//======================================================================

WHATAFAG.template =
	(
		function ()
		{
			//======================================================================
			//	constructor
			//======================================================================

			var that = {};

			//======================================================================
			//	get_args_default
			//======================================================================

			that.get_args_default =
				function()
				{
					var that = this;

					return {};
				};

			//======================================================================
			//	refresh
			//======================================================================

			that.refresh =
				function( callback )
				{
					var that = this;

					//	refresh observers

					//	that.refresh_observers();

					//	refresh child templates

					return that.refresh_children( callback );
				};

			//======================================================================
			//	refresh_observers
			//======================================================================

			that.refresh_observers =
				function()
				{
					var that = this;

					//	refresh observers

					jQuery.each( that.observers, function ( key, value ) { that.observers[ key ].refresh(); } );
				};

			//======================================================================
			//	refresh_children
			//======================================================================

			that.refresh_children =
				function( callback, namespace )
				{
					var that = this;

					if ( namespace === undefined )
					{
						jQuery.each( that.templates_children, function ( key, value ) { that.refresh_children( null, key ); } );
					}
					else if ( that.templates_children[ namespace ] !== undefined )	//	does this namespace exist
					{
						//	refresh children

						for ( var counter = 0; counter < that.templates_children[ namespace ].length; counter++ )
						{
							that.templates_children[ namespace ][ counter ].refresh();
						}
					}

					//	callback

					if ( typeof callback === "function" ) callback( that );

					return that;
				};

			//======================================================================
			//	flag_children
			//======================================================================

			that.flag_children =
				function( namespace )
				{
					var that = this;

					if ( namespace === undefined )
					{
						jQuery.each( that.templates_children, function ( key, value ) { that.flag_children( key ); } );
					}
					else if ( that.templates_children[ namespace ] !== undefined )	//	does this namespace exist
					{
						//	refresh children

						for ( var counter = 0; counter < that.templates_children[ namespace ].length; counter++ )
						{
							that.templates_children[ namespace ][ counter ].is_present = true;
						}
					}

					return that;
				};

			//======================================================================
			//	refresh_flagged_children
			//======================================================================

			that.refresh_flagged_children =
				function( namespace )
				{
					var that = this;

					if ( namespace === undefined )
					{
						jQuery.each( that.templates_children, function ( key, value ) { that.refresh_flagged_children( key ); } );
					}
					else if ( that.templates_children[ namespace ] !== undefined )	//	does this namespace exist
					{
						//	refresh children

						for ( var counter = 0; counter < that.templates_children[ namespace ].length; counter++ )
						{
							if ( that.templates_children[ namespace ][ counter ].is_present )
							{
								delete that.templates_children[ namespace ][ counter ].is_present;
								that.templates_children[ namespace ][ counter ].refresh();
							}
							else
							{
								//	console.info( "refresh() observed that new child templates were created by refreshed resources; these should have already been refresh()ed" );
							}
						}
					}

					return that;
				};

			//======================================================================
			//	add_template_child
			//======================================================================

			that.add_template_child =
				function ( template_child, j_parent, namespace )
				{
					var that = this;

					if ( typeof namespace === "undefined" )
					{
						namespace = "all";
					}

					if ( that.templates_children[ namespace ] === undefined )
					{
						that.templates_children[ namespace ] = [];
					}

					that.templates_children[ namespace ].push( template_child );

					j_parent.append( template_child.j_template );

					if ( that.is_active && !template_child.is_active ) template_child.set_active();
					else if ( !that.is_active && template_child.is_active ) template_child.set_inactive();

					return that;
				};

			//======================================================================
			//	follow
			//
			//	a front-end to the add_callback() function of the watched object
			//
			//	we assume there is only going to be one event_name for this template, so
			//	we can build the event id from these two pieces of information
			//
			//	record the event id so that we can automatically delete it when this template is destroyed
			//======================================================================

			that.follow =
				function ( watched_async_object, event_name, callback )
				{
					var that = this;

					var id_event = event_name + "_" + that.id;

					watched_async_object.event_manager.bind( event_name, underscore.bind( callback, that ), id_event );	//	bind the callback's context to this template

					that.callbacks_to_unbind_on_destroy[ id_event ] = [ watched_async_object, event_name ];

					return that;
				};

			//======================================================================
			//	unfollow
			//
			//	similar to follow, sneak in the id of the template to the event id
			//======================================================================

			that.unfollow =
				function ( watched_async_object, event_name )
				{
					var that = this;

					var id_event = event_name + "_" + that.id;

					watched_async_object.event_manager.unbind_by_id( id_event );

					delete that.callbacks_to_unbind_on_destroy[ id_event ];

					return that;
				};

			//======================================================================
			//	follow_changes
			//
			//	shorthand for refreshing the watched object then follow "change"
			//======================================================================

			that.follow_changes =
				function ( watched_async_object, callback_change )
				{
					var that = this;

					var promise = watched_async_object.refresh();

					promise
						.done
						(
							function ( a )
							{
								that.follow( watched_async_object, "change", callback_change );

								callback_change.call( that, a, a );
							}
						);

					return promise;
				};

			//======================================================================
			//	destroy
			//======================================================================

			that.destroy =
				function ( do_force_delete )
				{
					var that = this;

					that.destroy_root( do_force_delete );

					return that;
				};

			//======================================================================
			//	destroy_root
			//======================================================================

			that.destroy_root =
				function ( do_force_delete )
				{
					var that = this;

					that.margs.destroy();

					jQuery.each( that.callbacks_to_unbind_on_destroy, function ( key, value ) { that.unfollow( value[ 0 ], value[ 1 ] ); } );

					jQuery.each( that.observers, function ( key, value ) { that.observers[ key ].destroy(); } );

					that.event_manager.unbind_all();

					that.delete_templates_children( undefined, do_force_delete );

					that.j_template.remove();

					if ( that.is_active ) that.set_inactive();

					return that;
				};

			//======================================================================
			//	delete_templates_children
			//======================================================================

			that.delete_templates_children =
				function( namespace, do_force_delete )
				{
					var that = this;

					if ( do_force_delete === undefined ) do_force_delete = false;

					if ( namespace === undefined )
					{
						jQuery.each( that.templates_children, function ( key, value ) { that.delete_templates_children( key, do_force_delete ); } );
					}
					else
					{
						//	destroy children

						var template_child;

						if ( that.templates_children[ namespace ] !== undefined )
						{
							while ( template_child = that.templates_children[ namespace ].pop() )
							{
								if ( template_child.is_component && !do_force_delete )
								{
									jQuery( "#container_rendered_components" ).prepend( template_child.j_template );

									if ( template_child.is_active ) template_child.set_inactive();
								}
								else
								{
									template_child.destroy();
								}
							}
						}
					}

					return that;
				};

			//======================================================================
			//	has_new_args
			//======================================================================

			that.has_new_args =
				function ( arguments_new )
				{
					var that = this;

					if ( arguments_new === undefined )
					{
						return false;
					}
					else
					{
						var arguments_extended = jQuery.extend( {}, that.get_args_default(), arguments_new );

						return ( !underscore.isEqual( that.args, arguments_extended ) );
					}
				};

			//======================================================================
			//	set_args
			//======================================================================

			that.set_args =
				function ( arguments_new )
				{
					var that = this;

					var args_merged = jQuery.extend( {}, that.get_args_default(), arguments_new );

					that.args = args_merged;

					that.margs.set( args_merged );

					return that;
				};

			//======================================================================
			//	update_arguments
			//
			//	if the arguments are new, set them and return the old arguments
			//======================================================================

			that.update_arguments =
				function ( arguments_new )
				{
					var that = this;

					if ( arguments_new === undefined )
					{
						return that.args;
					}
					else if ( that.has_new_args( arguments_new ) )
					{
						var arguments_old = that.args;

						that.set_args( arguments_new );

						return arguments_old;
					}
					else
					{
						return that.args;
					}
				};

			//======================================================================
			//	handle_updated_margs
			//======================================================================

			that.handle_updated_margs =
				function ( properties, properties_changed )
				{
					//	nothing
				};

			//======================================================================
			//	set_active
			//======================================================================

			that.set_active =
				function ()
				{
					var that = this;
					
					that.is_active = true;

					//	delayed images

					WHATAFAG.show_delayed_images( that.j_template );

					that.event_manager.trigger( "active" );

					return that.set_active_children();
				};

			that.set_active_children =
				function( namespace )
				{
					var that = this;

					//	loop over each name space in turn

					if ( namespace === undefined )
					{
						jQuery.each( that.templates_children, function ( key, value ) { that.set_active_children( key ); } );
					}

					//	for one namespace, loop over each child template in turn

					else if ( that.templates_children[ namespace ] !== undefined )
					{
						for ( var counter = 0; counter < that.templates_children[ namespace ].length; counter++ ) that.templates_children[ namespace ][ counter ].set_active();
					}

					return that;
				};

			//======================================================================
			//	set_inactive
			//======================================================================

			that.set_inactive =
				function ()
				{
					var that = this;
					
					that.is_active = false;

					that.event_manager.trigger( "inactive" );

					return that.set_inactive_children();
				};

			that.set_inactive_children =
				function( namespace )
				{
					var that = this;

					//	loop over each name space in turn

					if ( namespace === undefined )
					{
						jQuery.each( that.templates_children, function ( key, value ) { that.set_inactive_children( key ); } );
					}

					//	for one namespace, loop over each child template in turn

					else if ( that.templates_children[ namespace ] !== undefined )
					{
						for ( var counter = 0; counter < that.templates_children[ namespace ].length; counter++ ) that.templates_children[ namespace ][ counter ].set_inactive();
					}

					return that;
				};

			//======================================================================
			//	populate_template_with_data
			//======================================================================

			that.populate_template_with_data =
				function ( hash_properties, hash_properties_changed, label_group )
				{
					var that = this;

					var applicable_nodes = that.j_template.find( "*" ).not( that.j_template.find( ".template .template" ).add( that.j_template.find( ".template .template *" ) ) );

					//	that.delete_templates_children( "generic" );

					//======================================================================
					//	data- templates
					//======================================================================

					//	template_html

					applicable_nodes
						.filter( ".template_html" )
						.each
						(
							function ( index )
							{
								var j_element = jQuery( this );
								var properties_data = j_element.data();

								if ( hash_properties_changed[ properties_data.source ] !== undefined )
								{
									if ( properties_data.group === label_group )
									{
										j_element.html( hash_properties[ properties_data.source ] );
									}
								}
							}
						);

					//	template_attr

					applicable_nodes
						.filter( ".template_attr" )
						.each
						(
							function ( index )
							{
								var j_element = jQuery( this );
								var properties_data = j_element.data();

								if ( hash_properties_changed[ properties_data.source ] !== undefined )
								{
									//	prepend

									if ( properties_data.position && properties_data.position === "prepend" )
									{
										j_element.attr( properties_data.attr, hash_properties[ properties_data.source ] + j_element.attr( properties_data.attr ) );
									}

									//	append

									else if ( properties_data.position && properties_data.position === "append" )
									{
										j_element.attr( properties_data.attr, j_element.attr( properties_data.attr ) + hash_properties[ properties_data.source ] );
									}

									//	replace

									else
									{
										j_element.attr( properties_data.attr, hash_properties[ properties_data.source ] );
									}
								}
							}
						);

					//	template_if_true

					applicable_nodes
						.filter( ".template_if_true" )
						.each
						(
							function ( index )
							{
								var j_element = jQuery( this );
								var properties_data = j_element.data();

								if ( hash_properties_changed[ properties_data.source ] !== undefined )
								{
									if ( hash_properties[ properties_data.source ] )
									{
										j_element.show();
									}
									else
									{
										j_element.hide();
									}
								}
							}
						);

					//	template_if_set

					applicable_nodes
						.filter( ".template_if_set" )
						.each
						(
							function ( index )
							{
								var j_element = jQuery( this );
								var properties_data = j_element.data();

								if ( hash_properties_changed[ properties_data.source ] !== undefined && hash_properties_changed[ properties_data.source ] !== null )
								{
									j_element.show();
								}
								else
								{
									j_element.hide();
								}
							}
						);

					//	template_sub

					applicable_nodes
						.filter( ".template_sub" )
						.each
						(
							function ( index )
							{
								var j_element = jQuery( this );
								var properties_data = j_element.data();

								//	experimental : try adding/deleting child templates only as-needed (when the uri in properties_data.source changes)
								
								if ( hash_properties_changed[ properties_data.source ] !== undefined )
								{
									j_element.children().length && that.delete_templates_children( j_element.children().first().data( "sub-id" ) );

									var id_sub_new = make_id( 16 );

									var template_sub = WHATAFAG.new_template_multi();
									that.add_template_child( template_sub, j_element, id_sub_new );
									/* template_sub.update_arguments( { "name_template" : properties_data.name_template, "uri" : hash_properties[ properties_data.source ] } ); */
									template_sub.update_arguments( jQuery.extend( {}, properties_data, { "uri" : hash_properties[ properties_data.source ] } ) );	//	pass in all data- arguments PLUS a "uri" that is *calculated* from data-source (1-level of redirection)

									template_sub.j_template.data( "sub-id", id_sub_new );
								}
							}
						);

					//	template_simple

					applicable_nodes
						.filter( ".template_simple" )
						.each
						(
							function ( index )
							{
								var j_element = jQuery( this );
								var properties_data = j_element.data();

								//	experimental : try adding/deleting child templates only as-needed (when the uri in properties_data.source changes)
								
								if ( hash_properties_changed[ properties_data.source ] !== undefined )
								{
									j_element.children().length && that.delete_templates_children( j_element.children().first().data( "sub-id" ) );

									var id_sub_new = make_id( 16 );

									var template_simple = WHATAFAG[ properties_data.name_constructor ]();
									that.add_template_child( template_simple, j_element, id_sub_new );
									template_simple.update_arguments( jQuery.extend( {}, properties_data, { "uri" : hash_properties[ properties_data.source ] } ) );	//	pass in all data- arguments PLUS a "uri" that is *calculated* from data-source (1-level of redirection)

									template_simple.j_template.data( "sub-id", id_sub_new );
								}
							}
						);

					//	template_fetch_and_html

					applicable_nodes
						.filter( ".template_fetch_and_html" )
						.each
						(
							function ( index )
							{
								var j_element = jQuery( this );
								var properties_data = j_element.data();

								if ( hash_properties_changed[ properties_data.uri ] !== undefined )
								{
									request_resource_show( hash_properties[ properties_data.uri ] )
										.done
										(
											function ( hash_properties_resource )
											{
												j_element.html( hash_properties_resource[ properties_data.source ] );
											}
										);
								}
							}
						);

					/*
					//	delayed images

					applicable_nodes
						.find( "img[data-src-delayed]" )
						.each
						(
							function ( index, element )
							{
								var j_element = jQuery( this );

								j_element.attr( "src", j_element.data( "src-delayed" ) );
							}
						);
					*/

					//	template_date_and_time

					applicable_nodes
						.filter( ".template_date_and_time" )
						.each
						(
							function ( index )
							{
								var j_element = jQuery( this );
								var properties_data = j_element.data();

								if ( hash_properties_changed[ properties_data.source ] !== undefined )
								{
									j_element.html( pretty_date_and_time( hash_properties[ properties_data.source ] ) );
								}
							}
						);

					//	template_avatar

					applicable_nodes
						.filter( ".template_avatar" )
						.each
						(
							function ( index )
							{
								var j_element = jQuery( this );
								var properties_data = j_element.data();

								if ( hash_properties_changed[ properties_data.source ] !== undefined )
								{
									j_element.hide();

									request_resource_show( hash_properties[ properties_data.source ] )
										.done
										(
											function ( hash_properties_source )
											{
												var j_img = jQuery( "<img src=\"" + hash_properties_source[ "uri_image_avatar" ] + "/" + properties_data.dimensions + "\" />" );
												j_element.empty().append( j_img ).show();
											}
										);
								}
							}
						);

					return that;
				};

			//	return that

			return that;
		}()
	);

WHATAFAG.new_template =
	function ()
	{
		var template = Object.create( WHATAFAG.template );

		template.event_manager = WHATAFAG.new_event_manager();
		template.margs = WHATAFAG.new_watched_async_object();

		template.templates_children = {};
		template.args = undefined;
		template.args_default = {};
		template.is_initialized = false;
		template.is_eligible_for_component = false;
		template.is_component = false;
		template.callbacks_to_unbind_on_destroy = {};
		template.observers = {};
		template.is_active = false;
		template.id = make_id( 16 );

		template.follow( template.margs, "change", function ( a, b ) { template.handle_updated_margs.call( template, a, b ); } );

		return template;
	};

//======================================================================
//
//	processing area
//
//======================================================================

WHATAFAG.new_processing_area =
	function ( j_processing_area )
	{
		//======================================================================
		//	constructor
		//======================================================================

		var that = {};

		that.id_timer_status_message = undefined;
		that.spinner = null;

		j_processing_area.find( ".status" ).hide();
		j_processing_area.find( ".message" ).hide();

		//======================================================================
		//	set_message
		//======================================================================

		that.set_message =
			function( j_message )
			{
				if ( typeof j_message === "string" )
				{
					j_message = jQuery( "<div class=\"p\">" + j_message + "</div>" );
				}

				j_processing_area.find( ".message" ).empty().append( j_message );
			};

		//======================================================================
		//	show_message
		//======================================================================

		that.show_message =
			function()
			{
				j_processing_area.find( ".message" ).show();
			};

		//======================================================================
		//	clear_message
		//======================================================================

		that.clear_message =
			function()
			{
				j_processing_area.find( ".message" ).empty().hide();
			};

		//======================================================================
		//	set_and_show_message
		//======================================================================

		that.set_and_show_message =
			function( j_message )
			{
				that.set_message( j_message );
				that.show_message();
			};

		//======================================================================
		//	set_status
		//======================================================================

		that.set_status =
			function( j_status )
			{
				if ( typeof j_status === "string" )
				{
					j_status = jQuery( "<div>" + j_status + "</div>" );
				}

				that.clear_status();

				j_processing_area.find( ".status" ).css( { "display" : "inline-block", "vertical-align" : "middle" } ).append( j_status );
			};

		//======================================================================
		//	set_status_auto_dismiss
		//======================================================================

		that.set_status_auto_dismiss =
			function( j_status, delay )
			{
				if ( typeof delay === "undefined" ) delay = 10;

				that.set_status( j_status );

				var callback = function () { that.id_timer_status_message = undefined; that.clear_status(); };

				that.id_timer_status_message = window.setTimeout( callback, delay * 1000 );
			};

		//======================================================================
		//	clear_status
		//======================================================================

		that.clear_status =
			function()
			{
				//	cancel a queued auto-dismiss timer

				if ( that.id_timer_status_message !== undefined ) { window.clearTimeout( that.id_timer_status_message ); that.id_timer_status_message = undefined; }

				//	remove old spinners

				if ( that.spinner ) { spinner.inactivity(); spinner = undefined; }

				j_processing_area.find( ".status" ).empty().hide();
			};

		//======================================================================
		//	show_processing
		//======================================================================

		that.show_processing =
			function()
			{
				that.clear_message();

				var spinner = jQuery( "<div></div>" );
				spinner.activity( { segments: 8, steps: 3, width: 3, space: 0, length: 5, color: '#ffffff', speed: 1 } );

				that.set_status( spinner );
			};

		//======================================================================
		//	reset
		//======================================================================

		that.reset =
			function()
			{
				that.clear_message();
				that.clear_status();
			};

		//	return that

		return that;
	};

//======================================================================
//
//	watched_server_resource
//
//======================================================================

WHATAFAG.watched_server_resource =
	(
		function ()
		{
			var that = {};

			//======================================================================
			//	set
			//
			//	completely replace the previous values
			//======================================================================

			that.set =
				function ( properties_replacements )
				{
					var that = this;

					return request_resource_update( that.get( "uri" ), properties_replacements );
				};

			//======================================================================
			//	get
			//======================================================================

			that.get =
				function ( key )
				{
					var that = this;

					if ( that.properties[ key ] === undefined ) return undefined;
					else return that.properties[ key ];
				};

			//======================================================================
			//	update
			//======================================================================

			that.update =
				function ( properties_replacements )
				{
					var that = this;

					return request_resource_update( that.get( "uri" ), properties_replacements );
				};

			//======================================================================
			//	refresh
			//======================================================================

			that.refresh =
				function ( do_use_cache, context )
				{
					var that = this;

					//======================================================================
					//	defaults
					//======================================================================

					if ( typeof do_use_cache === "undefined" || do_use_cache === null ) do_use_cache = true;

					//======================================================================
					//	if we've just gotten the data and are processing it then you might
					//	be here because of a success callback; we should trust the data and
					//	use it immediately (it can't get much fresher!)
					//
					//	do not worry about expiration times -- in this way, resources that
					//	expire immediately do NOT actually expire immediately -- they expire
					//	one the callbacks are done.
					//
					//	alternatively, we're not processing but we were told we can use the cache.
					//	however only use the cache if the last request was successful. otherwise
					//	we must go back to the server again.
					//======================================================================

					if (  that.state === "processing" || ( that.count_holds > 0 ) || ( that.state === "idle" && do_use_cache && that.expires >= Date.now() && !that.did_receive_error_on_last_request ) )
					{
						var dfd = jQuery.Deferred();

						dfd.resolve( that.properties );

						return dfd.promise();
					}

					else
					{
						//======================================================================
						//	this uri is already being retrieved
						//
						//	we must wait for the response before doing anything
						//======================================================================

						if ( that.state === "request_in_progress" )
						{
							//	do nothing else

							return that.promise_active;
						}

						//======================================================================
						//	make the request
						//======================================================================

						else
						{
							var callback;
							var cookie_value;
							var dfd = jQuery.Deferred();

							//	you need to have a cookie set in order to make an ajax call

							if ( ( cookie_value = get_cookie( name_cookie ) ) === null )
							{
								handle_message_simple( "<p>Your cookie has expired. We're sorry about that (it shouldn't happen very often, or at all).</p><p>You can continue by refreshing the page.</p>" );

								dfd.reject( xml_http_request, "<p>Your cookie has expired. We're sorry about that (it shouldn't happen very often, or at all).</p><p>You can continue by refreshing the page.</p>", error_thrown );
							}
							else
							{
								//	indicate that this uri is in the process of being retrieved

								that.state = "request_in_progress";

								that.promise_active = dfd.promise();

								//	we're getting a fresh resource so we're not allowed to use the old one anymore

								//	do we really need this? taking it out...
								//	that.expire();

								var ajax_options =
									{
										"beforeSend" :
											function ( xml_http_request )
											{
												xml_http_request.setRequestHeader( key_authorization_header, cookie_value );

												if ( !do_use_cache )
												{
													xml_http_request.setRequestHeader( "Cache-Control", "no-cache" );
												}
											},
										"dataType" : "json",
										"error" :
											function ( xml_http_request, text_status, error_thrown )
											{
												that.state = "processing";

												that.did_receive_error_on_last_request = true;

												//	trigger change event; fires callbacks that were assigned through .bind()

												dfd.reject( xml_http_request, text_status, error_thrown );

												that.event_manager.trigger( "unavailable" );

												//	log an error message

												console.info( "Error retrieving " + that.uri + " : " + text_status );

												that.state = "idle";
											},
										"success" :
											function ( data, text_status, xml_http_request )
											{
												that.did_receive_error_on_last_request = false;

												var expires = Date.parse( xml_http_request.getResponseHeader( "Expires" ) );
												that.expires = expires;

												//	tell any tag-along resources that -- even if they expire immediately -- to hang on long enough to survive through all the success callbacks below

												var list_resources_tag_alongs = [];

												if ( data.resources_tag_along )
												{
													jQuery
														.each
														(
															data.resources_tag_along,
															function ( key, value )
															{
																var resource = WHATAFAG.resource_library.resource( value.uri );

																list_resources_tag_alongs.push( resource );

																resource.count_holds++;
															}
														);
												}

												//	set the data (and strip any tag-alongs)
												//	this will trigger the "change" event if appropriate

												that.populate_from_server( data );

												that.state = "processing";

												dfd.resolve( that.properties );

												//	release holds on any tag-alongs

												for ( var counter = 0; counter < list_resources_tag_alongs.length; counter++ )
												{
													list_resources_tag_alongs[ counter ].count_holds--;
												}

												that.state = "idle";	//	future requests will start fresh with a new request
											},
										"type" : "GET",
										"url" : that.uri
									};

								if ( context !== undefined && context !== null )
								{
									ajax_options[ "context" ] = context;
								}

								reset_ping_timer();

								jQuery.ajax( ajax_options );
							}

							return that.promise_active;
						}
					}
				};

			//======================================================================
			//	refresh_and_add_change_callback
			//======================================================================

			that.refresh_and_add_change_callback =
				function ( callback_change )
				{
					var that = this;

					var promise = that.refresh();

					promise
						.done
						(
							function ( a )
							{
								that.event_manager.bind( "change", callback_change );	//	does callback_change need to be underscore.bind() to "that"'s context?

								callback_change.call( that, a, a );
							}
						);

					return promise;
				};

			//======================================================================
			//	populate_from_server
			//======================================================================

			that.populate_from_server =
				function ( data )
				{
					var that = this;

					//	prefer the new format where there is no "hash_properties_resource" sub-array, but all code isn't changed over yet

					var properties = data[ "hash_properties_resource" ] || data;

					//	check for tag-alongs

					if ( properties.resources_tag_along )
					{
						var expires = Date.parse( properties.expires_tag_along );
						
						jQuery
							.each
							(
								properties.resources_tag_along,
								function ( key, value )
								{
									var v = WHATAFAG.resource_library.resource( value.uri );

									//	only update a resource with data from a tag-along if it expires AFTER the expiration we already have; othrwise, the tag-along data might be stale or short-lived; we should always defer to actual non-tag-along resources, which presumably have longer expiration times, in addition to presumably more-up-to-date data

									if ( v.expires < expires )
									{
										v.populate_from_server( value );
										v.expires = expires;
									}
								}
							);

						delete properties.resources_tag_along;
						delete properties.expires_tag_along;
					}

					//	set the new properties and look for changes

					var properties_changed = diff( that.properties, properties );

					that.properties = properties;

					if ( !underscore.isEqual( properties_changed, {} ) )
					{
						that.event_manager.trigger( "change", that.properties, properties_changed );
					}
				};

			//======================================================================
			//	expire
			//======================================================================

			that.expire =
				function ()
				{
					var that = this;

					if ( that.expires !== 0 )
					{
						that.expires = 0;

						that.event_manager.trigger( "expire" );
					}

					return that;
				};

			return that;
		}()
	);

WHATAFAG.new_watched_server_resource =
	function ( uri )
	{
		var watched_server_resource = Object.create( WHATAFAG.watched_server_resource );

		watched_server_resource.event_manager = WHATAFAG.new_event_manager();
		watched_server_resource.properties = {};

		watched_server_resource.did_receive_error_on_last_request = false;
		watched_server_resource.uri = uri;
		watched_server_resource.expires = 0;
		watched_server_resource.state = "idle";
		watched_server_resource.promise_active = null;
		watched_server_resource.count_holds = 0;

		return watched_server_resource;
	};

//======================================================================
//	WHATAFAG.resource_library
//======================================================================

WHATAFAG.resource_library =
	(
		function ()
		{
			var that = {};

			that.resources = {};

			that.resource =
				function ( uri )
				{
					if ( !that.resources[ uri ] )
					{
						that.resources[ uri ] = WHATAFAG.new_watched_server_resource( uri );
					}

					return that.resources[ uri ];
				};

			that.expire_all =
				function ()
				{
console.info( "expiring all" );
					var that = this;

					jQuery.each( that.resources, function ( key, value ) { that.resources[ key ].expire(); } );

					return that;
				};

			return that;
		}()
	);

//======================================================================
//	WHATAFAG.show_delayed_images
//======================================================================

WHATAFAG.show_delayed_images =
	function ( j_template )
	{
		j_template
			.find( "img[data-src-delayed]" )
			.each
			(
				function ( index, element )
				{
					var j_element = jQuery( this );

					j_element.attr( "src", j_element.data( "src-delayed" ) );

					//	remove the attribute and data value

					j_element.removeAttr( "data-src-delayed" );
					j_element.data( "src-delayed", undefined );
				}
			);

	};

//======================================================================
//	WHATAFAG.collect_form_properties
//======================================================================

WHATAFAG.collect_form_properties =
	function ( j_form )
	{
		var list_properties = j_form.serializeArray();

		var properties = {};

		jQuery
			.map
			(
				list_properties,
				function( n, i )
				{
					properties[ n[ "name" ] ] = trim( n[ "value" ] );
				}
			);

		return properties;
	};

