/**
 * An abstract class for creating a floating popup container on the page. This
 * class should be extended to implement the content method.
 *
 * @author Nick Muerdter
 */
var InfoWindow = new Class({
	Implements: [Events, Options],

	options: {
		content: null,
		trigger: null,
		trigger_action: "click",
		width: 300,
		height: 175,
		padding: 5,
		bounds_container: null,
		direction: { horizontal: "right", vertical: "bottom" },
		values: {},
		shadow: false,
		close_button: true
	},

	/**
	 * Create a new info window.
	 */
	initialize: function(options) {
		// By default, enable the shadow for nice browsers. IE6 and less can't
		// support the transparent shadows, and this info window probably won't
		// be on top of a solid background, so we'll disabled it for IE6 and
		// less.
		if(options.shadow == undefined) {
			options.shadow = (Browser.Engine.trident && !Browser.Engine.trident5) ? false : true;
		}

		// Merge the default options with the passed in options.
		this.setOptions(options);

		// If an existing div is provided for the content, clone it so we have
		// a copy for our content. The clone might not be necessary, since we
		// could just move the div within our info window, but I don't want to
		// steal the existing div away from its current position which might be
		// relevant for screen readers.
		if(this.options.content) {
			this.content = this.options.content.clone();
		}

		// If there is a trigger element, set it up to open the info window.
		if(this.options.trigger) {
			this.options.trigger.addEvent(this.options.trigger_action, this.handleOpenClick.bind(this));
		}

		// Add a listener to the document to close the window if they click
		// somewhere else on the page.
		if(!InfoWindow.body_listener) {
			InfoWindow.body_listener = InfoWindow.handleBodyClick;
			$(document.body).addEvent("click", InfoWindow.body_listener);
		}
	},

	/**
	 * Gets the div representing this info window. If it's being created for
	 * the first time, its added to the document.
	 */
	getInfoWindow: function() {
		if(!this.info_window) {
			// Create the basic div containing the floating info window.
			this.info_window = new Element("div", {
				"class": "info_window_container",
				"styles": {
					"display": "none"
				}
			});

			// Oh, the pains we go through for beauty. But since these info
			// windows float over other screen content, the drop shadow helps
			// make it stand out. So if it's enabled, setup the inane structure
			// for cross-browser compatibility. This is based on this article:
			// http://www.alistapart.com/articles/cssdropshadows/
			if(this.options.shadow) {
				this.shadow_container = new Element("div", {
					"class": "info_window_shadow_container"
				});

				this.shadow = new Element("div", {
					"class": "info_window_shadow"
				});
			}

				this.inner_window_container = new Element("div", {
					"class": "info_window_inner_container"
				});

					// The inner window is setup so the content is always full
					// size, and doesn't react to the info window resizing. So
					// we need to specify an actual height and width that also
					// needs to account for padding.
					this.inner_window = new Element("div", {
						"class": "info_window_inner",
						"styles": {
							"padding": this.options.padding + "px"
						}
					});

						// The div that actually contains the good stuff.
						this.content_container = new Element("div", {
							"class": "info_window_content_container"
						});

							// If we can pop a simple close button in there, do
							// that.
							if(this.options.close_button) {
								this.close_button = new Element("img", {
									"src": "images/close.gif",
									"class": "info_window_close",
									"events": {
										"click": this.handleCloseClick.bind(this)
									}
								});

								this.content_container.adopt(this.close_button);
							}

							if(this.getContent()) {
								this.content_container.adopt(this.getContent());
							}

						this.inner_window.adopt(this.content_container);

					this.inner_window_container.adopt(this.inner_window);

				// Put everything together.
				if(this.options.shadow) {
					this.shadow.adopt(this.inner_window_container);
					this.shadow_container.adopt(this.shadow);
					this.info_window.adopt(this.shadow_container);
				} else {
					this.info_window.adopt(this.inner_window_container);
				}

			// If the content for this window actually needs to be fetched,
			// start that request now.
			if(this.options.content_url) {
				// Setup the remote request and update the content container
				// with the retrieved content.
				var request = new Request.HTML({
					"url": this.options.content_url,
					"method": "get",
					"update": this.content_container,
					"evalScripts": false
				});

				// Adjust the window dimensions to be much smaller for the
				// spinner.
				this.window_width = 75;
				this.window_height = 75;
				this.resetDimensions();

				// Add some listeners to show and hide the progress spinner.
				request.addEvent("request", this.handleContentRequest.bind(this));
				request.addEvent("complete", this.handleContentComplete.bind(this));

				request.send();
			} else {
				// Some of these containers need static dimensions set, so do that
				// now.
				this.resetDimensions();
			}

			// The info window is shoved onto the document in its hidden state.
			$(document.body).adopt(this.info_window);
			
			// If we didn't need to request the content, then it should be
			// ready.
			if(this.getContent()) {
				this.handleContentReady();
			}
		}

		return this.info_window;
	},

	/**
	 * Gets the div containing the content of this window.
	 *
	 * A div can be passed into the constructor for content, subclasses can
	 * overwrite this method to provide generated content, or a URL can be
	 * supplied to the constructor for an Ajax call to be made for the content.
	 */
	getContent: function() {
		return this.content;
	},

	/**
	 * If the content is being fetched from the server, this is fired when the
	 * request first starts. This turns the info window into a spinner image to
	 * indicate progress.
	 *
	 * @see handleContentComplete
	 */
	handleContentRequest: function() {
		// Hide the content container while it's being fetched.
		this.content_container.setStyle("display", "none");

		// Replace the content for the time being with a spinner image.
		this.loading_container = new Element("div", {
			"class": "info_window_loading",
			"styles": {
				"width": this.content_width + "px",
				"height": this.content_height + "px"
			}
		});

			var spinner = new Element("img", {
				"src": "/afdc/lib/images/spinner.gif"
			});

			this.loading_container.adopt(spinner);

		this.inner_window.adopt(this.loading_container);
	},

	/**
	 * If the content is being fetched from the server, this is fired when the
	 * request has finished and the content is inserted into the document. This
	 * switches off the spinner image, displays the content, and grows the
	 * window to fit the content.
	 *
	 * @see handleContentRequest
	 */
	handleContentComplete: function() {
		// Toggle the spinner loading image off and show the newly fetched
		// content.
		this.loading_container.setStyle("display", "none");
		this.content_container.setStyle("display", "");

		// Everything with the content should be set now.
		this.handleContentReady();

		// The window should be the size of the spinner right now, and it
		// probably needs to get bigger to fit all the new content.
		this.grow();
	},

	/**
	 * Handle a few simple things whenever we have the content. This could be
	 * immediately, if the content was provided, but might be triggered later
	 * if the content needed to be fetched from the server.
	 */
	handleContentReady: function() {
		// Calculate the content's dimensions if needs be.
		if(this.options.calculate_dimensions) {
			this.calculateContentDimensions();
		} else {
			// If we're not calculating the dimensions, revert the width and
			// height from the tiny spinner screen. Then adjust all the other
			// dimensions based on that that.
			this.window_width = this.options.width;
			this.window_height = this.options.height;
			this.resetDimensions();
		}

		// If we didn't insert a close button ourselves, its possible there's a
		// close button somewhere else in the content. This might be needed if
		// the close button has to be laid out differently.
		if(!this.options.close_button) {
			var close_button = this.content_container.getElement("a.close");
			if(close_button) {
				close_button.addEvent("click", this.handleCloseClick.bind(this));
			}
		}
	},

	/**
	 * Open the info window at the specified position on the page.
	 *
	 * @see grow()
	 */
	open: function(x, y, direction) {
		// We only want one info window open at a time, so close any existing
		// windows.
		if(InfoWindow.opened_window) {
			InfoWindow.opened_window.close();
		}

		// Store the currently opened window, so we have an easy, global
		// reference to close it later.
		InfoWindow.opened_window = this;

		// Show the window at the specified location, but actually hide all the
		// contents by setting its size to 0.
		this.getInfoWindow().setStyles({
			"display": "block",
			"width": "0px",
			"height": "0px"
		});

		// Fire an event for when the window is technically open, but hasn't
		// yet expanded to show itself.
		this.fireEvent("openStart");

		// Trigger our fancy effect so it looks like the info window is growing
		// from the point that was clicked.
		this.grow(x, y, direction);
	},

	/**
	 * Make the info window appear to grow from a single point to its full
	 * size.
	 *
	 * Note that this might be triggered multiple times for a single info
	 * window growing. If the content is being fetched, we first grow from
	 * nothing to the size of the spinner image. After we have the content, we
	 * need to smoothly grow again to the full size of the content.
	 *
	 * The first call triggered when actually clicked should provide the x and
	 * y parameters. However, subsequent during the opening process will simply
	 * use the coordinates of whatever has been opened so far.
	 *
	 * @param x The x coordinate on the document which was clicked and where
	 * the origin should appear to come from. 
	 * @param y The y coordinate on the document which was clicked and where
	 * the origin should appear to come from. 
	 * @param direction A hash with two keys "horizontal" and "vertical" which
	 * specify which direction the window should open in. Possible values for
	 * the "horizontal" key are "left" and "right" (default). Possible values
	 * for the "vertical" key are "top" and "bottom" (default). Note that these
	 * are only taken as preferred suggestions, and might be overwritten to try
	 * to grow in a direction that will fit all the content on the screen.
	 */
	grow: function(x, y, direction) {
		// If there's already an open effect in progress (maybe the progress
		// window is still opening while we get the content results back), then
		// we need to cancel it, so we can smoothly continue from where it left
		// off.
		if(this.open_effect) {
			this.open_effect.cancel();
		}

		// See how big the info window currently is. It's probably starting out
		// at 0, but in the event the progress window is open, we need to see
		// how big it got (it may not have had time to fully expand).
		var prev_width = 0;
		var prev_height = 0;
		if(this.content_container.getStyle("display") != "none") {
			prev_width = this.getInfoWindow().getStyle("width").toInt();
			prev_height = this.getInfoWindow().getStyle("height").toInt();
		}

		// If an x coordinate is provided, that means we've been clicked on,
		// and this is the very first effect to display. Otherwise, we're in
		// the middle of some other effect, so see where it left things.
		if(x != undefined) {
			this.effect_start_x = x;

			// The starting coordinate of the effect may change if there are
			// multiple effects needed to open this window. But we need to
			// store the real origin of the effect, so the close action can
			// draw itself back to where it came from.
			this.effect_first_x = x;
		} else if(this.open_effect) {
			this.effect_start_x = this.getInfoWindow().getStyle("left").toInt();
		}

		// The same thing as above, but for the y coordinate.
		if(y != undefined) {
			this.effect_start_y = y;
			this.effect_first_y = y;
		} else if(this.open_effect) {
			this.effect_start_y = this.getInfoWindow().getStyle("top").toInt();
		}

		// By default, the end position is actually the same as the start
		// position. In this case, the grow will happen by the height and width
		// expanding. It's only if we need to grow in the left or top direction
		// that the x, y coordinators also need to be morphed at the same time.
		this.effect_end_x = this.effect_start_x;
		this.effect_end_y = this.effect_start_y;

		// Try to determine the direction of growth.
		if(direction == undefined) {
			// Get the default direction.
			direction = this.options.direction;

			// We'll try to keep the default direction, but we first need to do
			// some bounds checking. We try to grow in a direction that will
			// keep all the content on the screen.
			var container = this.options.bounds_container;
			if(!container) {
				container = $(document.body);
			}

			var container_coords = container.getCoordinates();
			var body_size = $(document.body).getSize();
			var body_scroll = $(document.body).getScroll();

			// Determine the various coordinates on the page the window would
			// grow to, depending on the potential grow directions.
			var grow_right_x = this.effect_start_x + this.full_width;
			var grow_left_x = this.effect_start_x - this.full_width;
			var grow_bottom_y = this.effect_start_y + this.full_height;
			var grow_top_y = this.effect_start_y - this.full_height;

			var exceed_left = false;
			var exceed_right = false;
			var exceed_top = false;
			var exceed_bottom = false;

			// See if growing to the left or right will exceed our container
			// boundaries.
			if(grow_left_x < container_coords.left) {
				exceed_left = true;
			}

			if(grow_right_x > container_coords.right) {
				exceed_right = true;
			}

			// If the container's boundaries were exceeded, flip the growth
			// direction. But if doing so will still exceed our boundaries in
			// the other direction, keep whatever the default direction is.
			if(exceed_left) {
				if(!exceed_right) {
					direction.horizontal = "right";
				}
			} else if(exceed_right) {
				if(!exceed_left) {
					direction.horizontal = "left";
				}
			} else {
				// If this will fit inside our container, next see if we can
				// adjust the growth direction to fit on the visible screen.
				if(grow_right_x > body_scroll.x + body_size.x) {
					direction.horizontal = "left";
				} else if(grow_left_x < body_scroll.x) {
					direction.horizontal = "right";
				}
			}

			// See if growing the top or bottom will exceed our container
			// boundaries.
			if(grow_top_y < container_coords.top) {
				exceed_top = true;
			}

			if(grow_bottom_y > container_coords.bottom) {
				exceed_bottom = true;
			}

			// If the container's boundaries were exceeded, flip the growth
			// direction. But if doing so will still exceed our boundaries in
			// the other direction, keep whatever the default direction is.
			if(exceed_top) {
				if(!exceed_bottom) {
					direction.vertical = "bottom";
				}
			} else if(exceed_bottom) {
				if(!exceed_top) {
					direction.vertical = "top";
				}
			} else {
				// If this will fit inside our container, next see if we can
				// adjust the growth direction to fit on the visible screen.
				if(grow_bottom_y > body_scroll.y + body_size.y) {
					direction.vertical = "top";
				} else if(grow_top_y < body_scroll.y) {
					direction.vertical = "bottom";
				}
			}
		}

		// Growing left actually means we need to simultaneously move the
		// element as we reveal more of it.
		if(direction.horizontal == "left") {
			this.effect_end_x -= this.full_width;

			// Take into account the size of any previously opened window
			// that's part of this same effect.
			this.effect_end_x += prev_width;
		}

		// The same deal as above, but in the vertical direction.
		if(direction.vertical == "top") {
			this.effect_end_y -= this.full_height;
			this.effect_end_y += prev_height;
		}

		// If this current window is currently closing itself we need to cancel
		// it so it doesn't interfere with the open that's about to occur.
		if(this.close_effect) {
			this.close_effect.cancel();
		}

		// Mighty morphin' power rangers! Wait, no. They're dumb. Teenage
		// Mutant Ninja Turtles totally rock.
		this.open_effect = new Fx.Morph(this.getInfoWindow(), { "duration": "short" });
		this.open_effect.addEvent("onComplete", this.completeOpenEffect.bind(this));
		this.open_effect.start({
			"height": [prev_height, this.full_height],
			"width": [prev_width, this.full_width],
			"left": [this.effect_start_x, this.effect_end_x],
			"top": [this.effect_start_y, this.effect_end_y]
		});
	},

	/**
	 * When we want the content of the window to simply expand to fill the
	 * available space, we need to calculate its dimensions.
	 */
	calculateContentDimensions: function() {
		// In order to measure the size of something, it needs to be "visible"
		// on the page. Since we don't actually want it to show up on the page,
		// we'll create an area way off screen where we can insert the item to
		// be measured.
		if(!InfoWindow.measuring_container) {
			InfoWindow.measuring_container = new Element("div", {
				"class": "info_window_container info_window_inner_container info_window_inner",
				"styles": {
					"float": "left",
					"position": "absolute",
					"left": "-9000px"
				}
			});

			$(document.body).adopt(InfoWindow.measuring_container);
		}

		// Clone the content and float it, so it should shrink to the needed
		// size.
		var measured_content = this.content_container.clone()
		measured_content.setStyle("float", "left");

		// Insert the cloned element into our measuring area, and find its
		// size.
		InfoWindow.measuring_container.adopt(measured_content);
		var size = InfoWindow.measuring_container.getSize();

		// We don't want to keep the cloned items around, so clear them out.
		InfoWindow.measuring_container.empty();

		// The size we came up with is for the content.
		this.content_width = size.x;
		this.content_height = size.y;

		// The window need to account for padding.
		this.window_width = this.content_width + this.options.padding * 2;
		this.window_height = this.content_height + this.options.padding * 2;

		// Update everything else based on these new sizes.
		this.resetDimensions();
	},

	/**
	 * Throughout the arduous journey of opening the window, the dimensions
	 * might be changed. To handle the various calculations stemming from these
	 * changes, and updating any elements with static dimensions, this method
	 * can be called.
	 *
	 * The calculations are based on the values of this.window_width and
	 * this.window_height, so if needed, those should manually be set before
	 * calling this method.
	 */
	resetDimensions: function() {
		// If we haven't yet been through this, grab our basic window
		// dimensions from the options set.
		if(this.window_width == undefined || this.window_height == undefined) {
			this.window_width = this.options.width;
			this.window_height = this.options.height;
		}

		// The content dimensions need to subtract out padding since IE's
		// box-model is dumb.
		this.content_width = this.window_width - this.options.padding * 2;
		this.content_height = this.window_height - this.options.padding * 2;

		// The "full" dimensions simply account for the shadow if there is one.
		this.full_width = this.window_width;
		this.full_height = this.window_height;
		if(this.options.shadow) {
			this.full_width += 10;
			this.full_height += 10;
		}

		// Now that we have everything calculate, we need to update any
		// elements that need to have their dimensions statically set.
		if(this.options.shadow) {
			this.shadow_container.setStyles({
				"width": this.window_width + "px",
				"height": this.window_height + "px"
			});
		}

		this.inner_window_container.setStyles({
			"width": (this.window_width - 2) + "px",
			"height": (this.window_height - 2) + "px"
		});

		this.inner_window.setStyles({
			"width": this.content_width + "px",
			"height": this.content_height + "px"
		});
	},

	/**
	 * Called when the info window has finished opening and is fully displayed
	 * to the user.
	 */
	completeOpenEffect: function(event) {
		this.fireEvent("open");
	},

	/**
	 * Close this info window.
	 */
	close: function() {
		// Fire an event as the close effect is about to begin.
		this.fireEvent("closeStart");

		// Shrink the window to close it. This usese the same calculations
		// determined in the open method, but in reverse.
		this.close_effect = new Fx.Morph(this.getInfoWindow(), { duration: "short" });
		this.close_effect.addEvent("onComplete", this.completeCloseEffect.bind(this));
		this.close_effect.start({
			"height": [this.full_height, 0],
			"width": [this.full_width, 0],
			"left": [this.effect_end_x, this.effect_first_x],
			"top": [this.effect_end_y, this.effect_first_y]
		});

		// Nothing is opened any more.
		InfoWindow.opened_window = null;
	},

	/**
	 * Called when the info window has finished closing and is fully hidden
	 * from the user.
	 */
	completeCloseEffect: function(event) {
		this.getInfoWindow().setStyle("display", "none");

		this.fireEvent("close");
	},

	/**
	 * Handle the trigger element being clicked on. This will open the info
	 * window at the point clicked.
	 */
	handleOpenClick: function(event) {
		// Position the top left corner of the info window to be where the
		// cursor was clicked. With a little buffer on the left since the
		// cursor swings that way.
		var x = event.page.x;
		var y = event.page.y;

		// Open the window at the calculated position.
		this.open(event.page.x, event.page.y);

		// Kill the link-following and also prevent this click event from
		// bubbling up to the body where we have a listener to close the
		// window. That would be pretty worthless.
		event.stop();
	},

	/**
	 * Handle when the close button is clicked inside the window.
	 */
	handleCloseClick: function(event) {
		this.close();

		// Kill the link-following.
		event.stop();
	}
});

/**
 * Handle a click on the rest of the document. This will close the currently
 * opened info window.
 */
InfoWindow.handleBodyClick = function(event) {
	if(InfoWindow.opened_window) {
		// Only close the opened window if the click isn't somewhere within
		// the open info window.
		if(event.target != InfoWindow.opened_window.getInfoWindow() && !InfoWindow.opened_window.getInfoWindow().hasChild(event.target)) {
			InfoWindow.opened_window.close();
		}
	}
}
