/**
 * A class to manage a set of tabs that are shown separately and can be toggled
 * between.
 *
 * @see Tab
 * @author Nick Muerdter
 */
var TabGroup = new Class({
	/**
	 * Create a new tab group.
	 */
	initialize: function() {
		this.tabs = [];
		this.tab_names = {};

		this.results_container_selector = ".results";
	},

	/**
	 * Create a new tab belonging to this tab group.
	 *
	 * @param name The name of the tab to create. This should follow the
	 * guidelines defined in Tab.initialize() so the existing DOM elements can
	 * be found for this tab.
	 * @return The newly created tab.
	 * @see Tab.initialize()
	 */
	createTab: function(name) {
		var tab = new Tab(this, name)

		// Store the tab in our array and our name-based hash.
		this.tabs.push(tab);
		this.tab_names[name] = tab;

		return tab;
	},

	/**
	 * Retrieve a tab based on its name.
	 *
	 * @param name The name of the tab to find.
	 * @return The tab with the given name. Or undefined if it doesn't exist.
	 */
	getTab: function(name) {
		return this.tab_names[name];
	},

	/**
	 * Set which tab is currently active within this tab group.
	 *
	 * @param tab The Tab object that's currently active.
	 */
	setActiveTab: function(tab) {
		this.active_tab = tab;
	},

	/**
	 * Hide the currently active tab.
	 */
	hideActiveTab: function() {
		if(this.active_tab) {
			this.active_tab.hide();
		}
	},

	getResultsContainerSelector: function() {
		return this.results_container_selector;
	},

	setResultsContainerSelector: function(results_container_selector) {
		this.results_container_selector = results_container_selector;
	}
});

/**
 * A class that represents a single tab.
 *
 * @see TabGroup
 * @author Nick Muerdter
 */
var Tab = new Class({
	/**
	 * Create a new tab that belongs to a tab group. This probably shouldn't be
	 * called directly, instead using TabGroup.createTab().
	 *
	 * The name provided should be a unique name used to reference existing DOM
	 * elements on the page by ID. $("tab_trigger_" + name) should reference
	 * the tab trigger element. $("tab_content_" + name) should reference the
	 * element containing the content for this tab.
	 *
	 * @see TabGroup.createTab()
	 */
	initialize: function(group, name) {
		this.group = group;
		this.name = name;

		// Get the references to the trigger and content elements for this tab.
		this.trigger = $("tab_trigger_" + name);
		this.content = $("tab_content_" + name);

		// Setup the trigger to be the uh, trigger.
		this.trigger.addEvent("click", this.handleTriggerClick.bind(this));

		// Keep track of the checked items for comparison on this page.
		this.compare_checked = new Array();

		this.initializeContent();

		// This is the container that's actually refreshed when the filter
		// submits. By default, this is just the results div, but it may be
		// different if we want the filter to be refreshed too.
		this.results_container = this.content.getElement(this.group.getResultsContainerSelector());

		// Set this tab's loaded state. If there's anything in the results then
		// this is probably the first tab that's being shown on load where we
		// went ahead and rendered the results directly on the page. But the
		// rest of the tabs don't have any results and will need to be fetched
		// via an Ajax call.
		this.loaded = (this.results.getChildren().length > 0) ? true : false;

		// If this page already has its results rendered, we need to set things
		// up so the titles trigger the display of the info.
		if(this.loaded) {
			this.addResultTriggers();
		}
	},

	initializeContent: function() {
		this.filter = $(this.name + "_filter_form");

		// Hitting the back button can lead to some unexpected results with all
		// our dynamic results. So reset any filter or comparison selections on
		// load.
		var forms = this.content.getElements("form");
		for(var i = 0; i < forms.length; i++) {
			forms[i].reset();
		}

		// Change the form action so we only render the results, rather than
		// the whole page needed if we're really submitting this form without
		// javascript.
		$(this.name + "_filter_action").set("value", "results");

		// It seems redundant to have a submit button if the submit action is
		// triggered by the change events on the menus. So hide the submit
		// button from users who don't need it.
		$(this.name + "_filter_submit").setStyle("display", "none");

		// Reset the hidden form element specifying the current tab. This
		// *really* *really* shouldn't be necessary, but Safari is doing
		// incredibly strange things, like changing this value after hitting
		// the back button and then this value is set as part of an Ajax update
		// request.
		var compare_tab = $(this.name + "_compare_tab");
		if(compare_tab) {
			compare_tab.set("value", this.name);
		}

		// Trigger the same Ajax submit method whenever the filter inputs are
		// changed.
		var menus = this.filter.getElements("select");
		for(var i = 0; i < menus.length; i++) {
			menus[i].addEvent("change", this.handleFilterSubmit.bind(this));
		}

		var checkboxes = this.filter.getElements("input[type=checkbox]");
		for(var i = 0; i < checkboxes.length; i++) {
			checkboxes[i].addEvent("click", this.handleFilterSubmit.bind(this));
		}

		// Before pagination links are followed, change their links to include
		// any currently selected items to compare.
		this.pagination_container = this.content.getElement(".pagination");
		if(this.pagination_container) {
			var page_links = this.pagination_container.getElements("a");
			for(var i = 0; i < page_links.length; i++) {
				page_links[i].addEvent("mousedown", this.handlePaginationClick.bindWithEvent(this, page_links[i]));
			}
		}

		// Grab a reference to the loading spinner element and the actual
		// results element. This is used later when we're loading results from
		// the server.
		this.results = this.content.getElement(".results");
		this.spinner = this.content.getElement(".spinner");
		this.pagination_container = this.content.getElement(".pagination");
	},

	/**
	 * Hide this tab.
	 */
	hide: function() {
		// Switch the tab image to its inactive version.
		var image = this.trigger.getElement("img");
		image.set("src", image.get("src").replace("/active/", "/inactive/"));

		// Hide the content.
		this.content.setStyle("display", "none");
	},

	/**
	 * Show this tab.
	 */
	show: function() {
		// Hide the currently active tab within the group, and then let the
		// group know that this is the currently active tab.
		this.group.hideActiveTab();
		this.group.setActiveTab(this);

		// Switch the tab image to its inactive version.
		var image = this.trigger.getElement("img");
		image.set("src", image.get("src").replace("/inactive/", "/active/"));

		// Show the content.
		this.content.setStyle("display", "");
	},

	/**
	 * Show this tab's content when its trigger is clicked. Potentially make a
	 * request to fetch results for this tab if it hasn't been loaded yet.
	 *
	 * @see handleFilterSubmit()
	 */
	handleTriggerClick: function(event) {
		// If nothing has been loaded in this tab yet, trigger a request to the
		// server to fetch some results.
		if(!this.loaded) {
			this.handleFilterSubmit();
		}

		// Show this tab.
		this.show();

		// Kill the link-following.
		event.preventDefault();
	},

	/**
	 * Create a request to fetch the results for this tab based on the current
	 * filter parameters. The results element within this tab will be updated
	 * with the response's HTML.
	 */
	handleFilterSubmit: function() {
		if(this.request) {
			this.request.cancel();
		}

		// Setup the request to fetch the results based on the filter
		// parameters. The HTML result of this request is simply pushed into
		// the results element.
		this.request = new Request({
			"method": "get",
			"data": this.filter.toQueryString(),
			"evalScripts": false
		});

		// Add some events to toggle a spinner image while the request is being
		// loaded from the server.
		this.request.addEvent("request", this.handleRequest.bind(this));
		this.request.addEvent("failure", this.handleRequestFailure.bind(this));
		this.request.addEvent("success", this.handleRequestSuccess.bind(this));

		// Fire!
		this.request.send();
	},

	/**
	 * Handle a remote request just starting. This shows a loading graphic
	 * while the request is being processed.
	 */
	handleRequest: function(request) {
		// Hide the results and show the spinner.
		this.results.setStyle("display", "none");
		this.spinner.setStyle("display", "");
	},

	/**
	 * Handle a remote request failing.
	 */
	handleRequestFailure: function() {
		// Hide the spinner and show the last results.
		this.spinner.setStyle("display", "none");
		this.results.setStyle("display", "");

		alert("Sorry, an unexpected error occurred while fetching your results. Please try again.");
	},

	/**
	 * Handle a remote request finishing. This hides the loading graphic and
	 * displays the newly loaded content after we've finished processing it.
	 */
	handleRequestSuccess: function(html) {
		this.results_container.innerHTML = html;

		// This tab now been loaded at least once.
		this.loaded = true;

		// If we're also refreshing more of the page, like the filter inputs,
		// we need to re-initialize this tab.
		if(this.results_container != this.results) {
			this.initializeContent();
		}

		// The new results need to be massaged so that clicking on the names
		// trigger the display of the info for each result.
		this.addResultTriggers();

		// Hide the spinner and show the results.
		this.spinner.setStyle("display", "none");
		this.results.setStyle("display", "");
	},

	/**
	 * The results display starts out with two rows for each individual result:
	 * a row containing the result's name, and a row containing all of the
	 * details for that result. If the user has JavaScript we want to change
	 * things so all of the info is hidden to start with, and only the much
	 * shorter list of names is displayed. Then clicking on the names will
	 * show/hide the associated details.
	 */
	addResultTriggers: function() {
		// Find all of the triggers .
		var trigger_links = this.results.getElements("a.trigger_link")
		for(var i = 0; i < trigger_links.length; i++) {
			// Figure out what element the link is pointing to. This is the div
			// the actually contains all the info details.
			var info_id = trigger_links[i].get("href").match(/#(.+)$/);
			if(info_id) {
				var info = $(info_id[1] + "_info");

				// Create an effect for sliding the info in and out.
				var effect = new Fx.Slide(info);

				// Add a class to the parent container div that is created.
				effect.wrapper.addClass("info_slider_container");

				// Initially we want everything hidden unless this item is
				// specifically given as an anchor.
				trigger_parent_row = trigger_links[i].getParent("tr");
				if(window.location.hash != "#" + trigger_parent_row.get("id")) {
					effect.hide();
				}

				// Add click events to the trigger link and the close button to
				// toggle the display of the info.
				var toggle_event = this.handleInfoTriggerClick.bindWithEvent(this, effect);
				trigger_links[i].addEvent("click", toggle_event);
				info.getElement("a.close").addEvent("click", toggle_event);
			}
		}

		var compatible_links = this.results.getElements("a.compatible_link")
		for(var i = 0; i < compatible_links.length; i++) {
			compatible_links[i].addEvent("click", this.handleCompatibleClick.bind(this));
		}

		var compare_select_all = this.results.getElement("a.compare_select_all");
		if(compare_select_all) {
			compare_select_all.addEvent("click", this.handleSelectAllClick.bind(this));
		}

		var compare_select_none = this.results.getElement("a.compare_select_none");
		if(compare_select_none) {
			compare_select_none.addEvent("click", this.handleSelectNoneClick.bind(this));
		}
	},

	/**
	 * When a result's name or the close button is clicked we want to toggle
	 * the display of its associated info.
	 *
	 * @param effect The effect created to show/hide the info.
	 */
	handleInfoTriggerClick: function(event, effect) {
		effect.toggle();

		// Kill the link-following.
		event.preventDefault();
	},

	handleCompatibleClick: function(event) {
		var record = event.target.get("href").match(/#(.+)_(\d+)$/);
		if(record) {
			var mode = record[1];
			var id = record[2];

			// See if this compatible record and info window have already been
			// generated. If they have, then just reuse it.
			var info;
			if(TabGroup.compatible_windows && TabGroup.compatible_windows[mode] && TabGroup.compatible_windows[mode][id]) {
				info = TabGroup.compatible_windows[mode][id];
			} else {
				// Create a new info window with the contents the results of an
				// Ajax call.
				info = new InfoWindow({
					"content_url": "?action=popupInfo&tab=" + record[1] + "&id=" + record[2],
					"padding": 0,
					"close_button": false,
					"calculate_dimensions": true,
					"bounds_container": $(event.target).getParent("div.results_list_container"),
					"direction": {
						"horizontal": "left",
						"vertical": "bottom"
					}
				});

				// Cache this info window.
				if(!TabGroup.compatible_windows) {
					TabGroup.compatible_windows = {}
				}

				if(!TabGroup.compatible_windows[mode]) {
					TabGroup.compatible_windows[mode] = {}
				}

				TabGroup.compatible_windows[mode][id] = info;
			}

			// Pop it open.
			info.handleOpenClick(event);
		}

		// Kill the link-following.
		event.preventDefault();
	},

	handleSelectAllClick: function(event) {
		var checkboxes = this.results.getElements("input[type=checkbox]")
		for(var i = 0; i < checkboxes.length; i++) {
			checkboxes[i].checked = true;
		}

		// Kill the link-following.
		event.preventDefault();
	},

	handleSelectNoneClick: function(event) {
		var checkboxes = this.results.getElements("input[type=checkbox]")
		for(var i = 0; i < checkboxes.length; i++) {
			checkboxes[i].checked = false;
		}

		// Kill the link-following.
		event.preventDefault();
	},

	/**
	 * Before pagination links are followed, we need to alter their links to
	 * include extra parameters for the currently selected items to compare.
	 */
	handlePaginationClick: function(event, link) {
		// Gather all of the currently selected items to compare, or the
		// already passed in selected items that are stored in hidden fields.
		var compare_ids = [];
		var id_inputs = this.results.getElements("input.record_ids");
		id_inputs.each(function(id_input) {
			if((id_input.get("type") == "checkbox" && id_input.checked) || id_input.get("type") == "hidden") {
				compare_ids.include(id_input.value);
			}
		});

		var link_target = link.get("href");

		// Clear out any existing compare_ids parameter.
		link_target = link_target.replace(/&?compare_ids=[^&]*/, "");

		// Append our new list of compare_ids.
		if(compare_ids.length > 0) {
			link_target += (link_target.indexOf("?") != -1) ? "&" : "?";
			link_target += "compare_ids=" + compare_ids.join(",");
		}

		link.set("href", link_target);
	}
});
