﻿// namespace atlas
var atlas = atlas || {};

// class LocationSelector
atlas.LocationSelector = function(clientNamespace) {
	if (typeof (clientNamespace) == "undefined") {
		clientNamespace = "";
	}

	// copy for closure
	var self = this;

	//// Private member vars ////

	var _locationSelectorId = "#location-selector" + clientNamespace;
	var _locationTypeDropdownId = "#location-type-dropdown" + clientNamespace;
	var _selectedLocationsContainerId = "#selected-locations" + clientNamespace;
	var _treeContainer = "#location-selection-tree" + clientNamespace;
	var _hdnSelectedLocations = "#hdn-selected-locations" + clientNamespace;

	/**
	* List of selected locations by type, in the order they were selected
	*/
	var _selectedLocationsByType = [];

	/**
	* Associative array of selected locations for quick lookup
	*/
	var _selectedLocationsById = [];

	/**
	* List of available locations types from which to select
	*/
	var _availableLocationsTypes = [];


	//// Private member functions ////

	/**
	* Display "select/unselect all" links for when smaller locations are grouped under a parent
	*/
	this.addSelectAllLinks = function(jsTreeNode) {
		$(jsTreeNode).children("a:first").after($("<span/>")
													.addClass("selectall")
													.click(function() {
														self.selectAllChildren(jsTreeNode.id);
													})
													.html("[select all]"))
										 .after($("<span/>")
													.addClass("selectall")
													.click(function() {
														self.unselectAllChildren(jsTreeNode.id);
													})
													.html("[unselect all]"));
	};

	/** 
	* Given a tree node's ID, adds all selectable children to the list of 
	* selected locations.
	*/
	this.selectAllChildren = function(nodeId) {
		var $nodesToSelect = $("#" + nodeId).find("li.leaf");
		$nodesToSelect.each(function(index, node) {
			//if the max number of locations are already selected, say so and get out of here
			if (!self.canSelectAnotherLocation()) {
				self.showMaxSelectionsExceededAlert();
				return false;
			}

			var locId = self.parseNodeId(node);
			var locName = $.trim($(node).children("a").text());
			var locTypeId = $(_locationTypeDropdownId).val();
			pub.addSelectedLocation(locId, locName, locTypeId, _availableLocationsTypes[locTypeId].name, false);
		});
	};

	/** 
	* Given a tree node's ID, removes all selectable children from the list of 
	* selected locations.
	*/
	this.unselectAllChildren = function(nodeId) {
		var $nodesToUnselect = $("#" + nodeId).find("li.leaf");
		$nodesToUnselect.each(function(index, node) {
			var locId = self.parseNodeId(node);
			var locTypeId = $(_locationTypeDropdownId).val();
			pub.removeSelectedLocation(locId, locTypeId, false);
		});
	};

	/**
	* Removes the "select/unselect all" links when a parent node is closed
	*/
	this.removeSelectAllLinks = function(jsTreeNode) {
		$(jsTreeNode).find("span").remove();
	};

	/**
	* Parses the entity ID from the tree node's ID
	*/
	this.parseNodeId = function(node) {
		var sId = $(node).attr("id");
		return parseInt(sId.split("_")[1], 10);
	};

	/**
	* Updates the location type dropdown from the current list of available location types
	*/
	this.refreshLocationTypeDropDown = function() {
		//remember the currently select locationType;
		//we'll try to re-select it once we've refreshed the DDL
		var selectedLocTypeId = $(_locationTypeDropdownId).val();

		$(_locationTypeDropdownId).empty();
		$(_availableLocationsTypes).each(function(index, locType) {
			if (typeof (locType) == "undefined") {
				return;
			}

			$("<option/>")
				.attr("value", locType.id)
				.html(locType.name)
				.appendTo($(_locationTypeDropdownId));
		});

		$(_locationTypeDropdownId).unbind("change");

		//re-select the previously selected option if it's still in the ddl...
		var selector = _locationTypeDropdownId + ' ' + 'option[value=' + selectedLocTypeId + ']';
		if (selectedLocTypeId && $(selector)) {
			$(_locationTypeDropdownId).val(selectedLocTypeId);
		}
		//...else, select the first option if there is one 
		else if ($(_locationTypeDropdownId + " option:first-child")) {
			$(_locationTypeDropdownId + " option:first-child").attr("selected", "selected");
		}

		$(_locationTypeDropdownId).change(function() {
			self.initializeTree(pub.getCurrentLocationType().id);
			if (!pub.allowMultiTypeSelection) {
				self.refreshSelectedLocations();
			}
		});

		self.initializeTree(pub.getCurrentLocationType().id);
		self.refreshSelectedLocations();
	};

	/** 
	* Updates the display of the currently selected locations
	*/
	this.refreshSelectedLocations = function() {
		$(_selectedLocationsContainerId).empty();
		var $ul = $("<ul/>");
		var selectedLocations = self.collectSelectedLocations(pub.allowMultiTypeSelection);
		$(selectedLocations).each(function(index, location) {
			if (typeof (location) == "undefined") {
				return;
			}
			self.createLocationToken(location).appendTo($ul);
		});
		$(_selectedLocationsContainerId).append($ul);
	};

	/** 
	* Creates an element to represent a selected location
	*/
	this.createLocationToken = function(location) {
		return $("<li/>")
					.attr("id", location.id)
					.html(location.name)
					.prepend($("<span>X</span>&nbsp;").click(function(element) {
						pub.removeSelectedLocation(location.id, location.typeId);
					}))
	};

	/**
	* Returns a list of all selected locations
	*/
	this.collectSelectedLocations = function(includeAllTypes) {
		var locations = [];
		$(_selectedLocationsByType).each(function(typeId, locationList) {
			if (typeof (locationList) == "undefined") {
				return;
			}
			if (!includeAllTypes && typeId != pub.getCurrentLocationType().id
				&& typeId != locationTypeIds.nationId) { //SPECIAL CASE: always include the "National Average"
				return;
			}
			$(locationList).each(function(locId, location) {
				locations.push(location);
			});
		});
		return locations;
	};

	/**
	* Intializes the tree of locations for the specified location type
	*/
	this.initializeTree = function(locTypeId) {
		var treeContainer = $(_treeContainer);
		treeContainer.empty();
		var initialNodes = self.getInitialTreeNodes(locTypeId);
		treeContainer.tree({
			data: {
				type: "json_asmx",
				async: true,
				opts: {
					async: true,
					method: "POST",
					url: "/Services/LocationTree.asmx/ExpandRegionSelector"
				}
			},
			ui: {
				dots: false,
				theme_name: "location_selector"
			},
			types: {
				"default": {
					clickable: true,
					draggable: false
				}
			},
			callback: {
				// Make sure static is not used once the tree has loaded for the first time
				onload: function(tree) {
					tree.settings.data.opts.static = false;
				},
				// Take care of refresh calls - node will be false only when the whole tree is refreshed or loaded of the first time
				beforedata: function(node, tree) {
					if (node == false) {
						tree.settings.data.opts.static = initialNodes;
						return;
					}
					data = {};
					data['nodeId'] = node.attr("id") || 0;
					data['locTypeId'] = $(_locationTypeDropdownId).val();
					data['showStateAverage'] = pub.showStateAverage;
					return data;
				},
				onopen: function(node, tree) {
					if ($(node).hasClass("checkable_children")) {
						self.addSelectAllLinks(node);
					}
				},
				onclose: function(node, tree) {
					if ($(node).hasClass("checkable_children")) {
						self.removeSelectAllLinks(node);
					}
					return true;
				},
				onparse: function(html, tree) {
					var dom = $("<div>" + html + "</div>");
					var openNodesWithCheckableChildren = dom.find("li.checkable_children.open ");
					for (var i = 0; i < openNodesWithCheckableChildren.length; i++) {
						addSelectAllLinks(openNodesWithCheckableChildren[i]);
					}
					return dom.html();
				},
				// use onchange since we're just trying to capture clicks and don't care whether the 
				// clicked node is "selected"
				onchange: function(node, tree) {
					//only leaf nodes are valid locations to be added
					if ($(node).hasClass("leaf")) {
						//add the selected location
						var locId = self.parseNodeId(node);
						var locName = $.trim($(node).children("a").text());
						var locTypeId = $(_locationTypeDropdownId).val();
						var locTypeName = _availableLocationsTypes[locTypeId].name;

						//SPECIAL CASE: "National Average" is always available and is a nation, 
						//SPECIAL CASE: rather than what's currently shown in the dropdown
						if (locName == "National Average") {
							locTypeId = locationTypeIds.nationId;
							locTypeName = locationTypeNames[locationTypeIds.nationId];
						}

						pub.addSelectedLocation(locId, locName, locTypeId, locTypeName);
					}
					else if ($(node).hasClass("branch")) {
						//open or close the branch
						tree.toggle_branch(node);
					}
				}
			}
		});
	};

	/**
	* Returns true if the user can select another location without exceeding the 
	* maximum number of allowed selections
	*/
	this.canSelectAnotherLocation = function() {
		if (pub.maxSelections == -1) {
			return true; //no maximum, can always select another location
		}
		var selectedLocationsCount = pub.getSelectedLocations().length;
		return selectedLocationsCount < pub.maxSelections;
	};

	/*
	* Presents an alert saying the maximum number of sections has been reached
	*/
	this.showMaxSelectionsExceededAlert = function() {
		var message;

		if (pub.maxSelectionsExceededMessage != "")
			message = pub.maxSelections;
		else
			message = "You may only select " + pub.maxSelections + " locations";

		alert(message);
	};

	/**
	* Request to retrieve the initial nodes to show in the tree.  
	* We only need to send current location ID's if the user is requesting to see "state" location types. 
	* This is because all other location types are underneath states and will only appear after the 
	* user expands a selection and not on the initial load.
	*/
	this.getInitialTreeNodes = function(locTypeId) {
		var nodes;
		var reqData = JSON.stringify({
			locTypeId: locTypeId,
			selectedLocIds: "",
			showNationalAverage: pub.showNationalAverage
		});

		$.ajax({
			async: false,
			type: "POST",
			url: "/Services/LocationTree.asmx/GetInitialNodes",
			data: reqData,
			contentType: "application/json; charset=utf-8",
			dataType: "json",
			success: function(msg) {
				nodes = msg.d;
			}
		});
		return nodes;
	}

	//// Public variables & methods ////
	var pub = {

		/**
		* The clientNamespace will be set in situations where we need multiple instances of 
		* a LocationSelector on the same page. This value will be appended to all
		* client element IDs in order to keep them unique.
		*/
		clientNamespace: clientNamespace,

		/**
		* If true, locations of more than types may be selected. Otherwise, selections
		* are limited to only a single location type.
		*/
		allowMultiTypeSelection: false,

		/**
		* If true, the "National Average" will appear as an option for selection
		*/
		showNationalAverage: true,


		/**
		* If true, the "[State Name] State Average" will appear as an option for selection for each state
		*/
		showStateAverage: true,

		/**
		* Maximum number of locations that can be selected.  -1 for unlimitted.
		*/
		maxSelections: -1,

		/**
		* Customize the text of the alert that appears when the user attemtps to
		* select more than the maxSelections number of locations
		*/
		maxSelectionsExceededMessage: "",

		/**
		* Given a list of indicator IDs, the locations available to 
		* choose from are filtered to only those locations with
		* data for the specified indicators
		*/
		setAvailableLocationTypesByIndicator: function(indicatorIds, onFinishedCallback, shouldRefresh) {
			//refresh the dropdown by default
			if (typeof (shouldRefresh) == "undefined") {
				shouldRefresh = true;
			}
			var reqData = {
				'indicatorIds': indicatorIds
			};
			$.ajax({
				type: "POST",
				url: "/Services/LocationSelector.asmx/FindLocationTypesWithData",
				data: JSON.stringify(reqData),
				contentType: "application/json; charset=utf-8",
				dataType: "json",
				success: function(result) {
					var locationTypeList = result.d;
					_availableLocationsTypes = [];
					$(locationTypeList).each(function(index, locationType) {
						_availableLocationsTypes[locationType.id] = locationType;
					});
					if (shouldRefresh) {
						self.refreshLocationTypeDropDown();
					}
					if (onFinishedCallback) {
						onFinishedCallback();
					}
				}
			});
		},

		/**
		* Sets the list of available location types to choose from. Expects
		* an array of objects in the following format:
		* { id: 1, name: "Location Type Name" }
		*/
		setAvailableLocationTypes: function(locationTypeList) {
			_availableLocationsTypes = [];
			$(locationTypeList).each(function(index, locationType) {
				_availableLocationsTypes[locationType.id] = locationType;
			});
			self.refreshLocationTypeDropDown();
		},

		/** 
		* Returns an list of the currently available location types
		*/
		getAvailableLocationTypes: function() {
			var locTypes = [];
			$(_availableLocationsTypes).each(function(locTypeId, locType) {
				if (locType === undefined) {
					return;
				}
				locTypes.push(locType);
			});
			return locTypes;
		},

		/**
		* Adds a location to the list of selected locations
		*/
		addSelectedLocation: function(locId, locName, locTypeId, locTypeName) {

			//don't add this location if it's already selected
			if (_selectedLocationsById[locId]) {
				//TODO: indicate to the user they've already added this location
				return;
			}

			//if the max number of locations are already selected, say so and get out of here
			if (!self.canSelectAnotherLocation()) {
				self.showMaxSelectionsExceededAlert();
				return false;
			}

			var location = { id: locId, name: locName, typeId: locTypeId, type: locTypeName };
			if (!_selectedLocationsByType[locTypeId]) {
				_selectedLocationsByType[locTypeId] = [];
			}

			//add an entry to both lists
			_selectedLocationsById[location.id] = location;
			_selectedLocationsByType[locTypeId].push(location);

			//add the entry to the DOM
			var $ul = $(_selectedLocationsContainerId + " ul");
			var $item = self.createLocationToken(location);
			$item.appendTo($ul);

			//scroll the new entry into view, if necessary
			velir.util.DomUtil.scrollElementIntoView($item[0], $(_selectedLocationsContainerId)[0]);
		},

		/**
		* Adds an array of locations to the list of selected locations.
		* Faster than calling addSelectedLocation multiple times.
		* Expects an array of objects in the following format:
		* { id: 1, name: "Location Name", typeId: 2, type: "Location Type Name" }
		*/
		addSelectedLocations: function(locations) {

			var selLocTypeId = $(_locationTypeDropdownId).val();
			var selLocTypeName = $(_locationTypeDropdownId).text();

			//select each location
			for (var i = 0; i < locations.length; i++) {
				var location = locations[i];
				var locId = location.id;
				var locTypeId = location.typeId;
				var locTypeName = location.type;

				if (pub.showStateAverage) {
					if (location.typeId != selLocTypeId && location.type == 'State') {
						location.name += ' State Average';
						location.typeId = selLocTypeId;
						locTypeId = selLocTypeId;
						location.type = selLocTypeName;
						locTypeName = selLocTypeName;
					}
				}

				//add an entry to both lists
				if (!_selectedLocationsByType[locTypeId]) {
					_selectedLocationsByType[locTypeId] = [];
				}
				_selectedLocationsById[locId] = location;
				_selectedLocationsByType[locTypeId].push(location);

				//add this location's locationType to _availableLocationTypes if it's not there already
				if (_availableLocationsTypes[locTypeId] == undefined) {
					var locTypeObject = { "id": locTypeId, "name": locTypeName };
					_availableLocationsTypes[locTypeId] = locTypeObject;
				}

				//add the entry to the DOM
				var $ul = $(_selectedLocationsContainerId + " ul");
				var $item = self.createLocationToken(location);
				$item.appendTo($ul);
			}
			self.refreshLocationTypeDropDown();
		},

		/**
		* Removes a location from the list of selected locations
		*/
		removeSelectedLocation: function(locationId, locationTypeId) {

			//remove the entry from both lists
			$(_selectedLocationsByType[locationTypeId]).each(function(index, location) {
				if (location.id == locationId) {
					_selectedLocationsByType[locationTypeId].splice(index, 1);
					return false; //assumes no dupe entries
				}
			});
			delete _selectedLocationsById[locationId];

			//remove the entry from the DOM
			var $lis = $(_selectedLocationsContainerId + " ul li");
			$lis.each(function(index, li) {
				if ($(li).attr("id") == locationId) {
					$(li).remove();
					return false; //presumes a single matching entry
				}
			});
		},

		/**
		* Removes all selected locations
		*/
		clearSelections: function() {
			//clear both lists
			_selectedLocationsById = [];
			_selectedLocationsByType = [];
			self.refreshSelectedLocations();
		},

		/**
		* Selects all locations
		*/
		selectAll: function() {
			var tree = $.tree.reference(_treeContainer);
			$(_treeContainer).find("li.leaf").each(function() {

				//if the max number of locations are already selected, say so and get out of here
				if (!self.canSelectAnotherLocation()) {
					self.showMaxSelectionsExceededAlert();
					return false;
				}
				tree.select_branch($(this), true);
			});
		},

		/**
		* Returns a list of all currently selected locations
		*/
		getSelectedLocations: function() {
			return self.collectSelectedLocations(pub.allowMultiTypeSelection);
		},

		/**
		* Retrieves the current location type
		*/
		getCurrentLocationType: function() {
			return _availableLocationsTypes[$(_locationTypeDropdownId).val()];
		},

		/**
		* Sets the currently selected location type
		*/
		setSelectedLocationType: function(locTypeId) {
			$(_locationTypeDropdownId).val(locTypeId);
			$(_locationTypeDropdownId).trigger("change");
		},

		/*
		* Shows the select all button
		*/
		showSelectAllButton: function() {
			$(_locationSelectorId + " #select-all" + clientNamespace).show();
		},

		/*
		* Hide the select all button
		*/
		hideSelectAllButton: function() {
			$(_locationSelectorId + " #select-all" + clientNamespace).hide();
		},

		/*
		* Hide the locationType DropDownList
		*/
		hideLocationTypeDropDownList: function() {
			$(_locationTypeDropdownId).css({ "visibility": "hidden" });
		},

		/*
		* Show the locationType DropDownList
		*/
		showLocationTypeDropDownList: function() {
			$(_locationTypeDropdownId).css({ "visibility": "visible" });
		},

		/**
		* Update the _hdnSelectedLocations input to reflect the current set of selected locations.
		* NOTE: for performance reasons, this is not called automatically.  If you need to
		* access the selections server side, call this before posting back.
		*/
		storeSerializedSelectionsInHiddenField: function() {
			var selectedLocations = pub.getSelectedLocations();
			$(_hdnSelectedLocations).val(JSON.stringify(selectedLocations));
		}
	}

	//// Constructor ////

	// wire up the "clear-all" click event
	$(_locationSelectorId + " #clear-all" + clientNamespace).click(function() {
		pub.clearSelections();
	});

	// wire up the "select-all" click event
	$(_locationSelectorId + " #select-all" + clientNamespace).click(function() {
		pub.selectAll();
	});

	//select-all button is hidden by default
	pub.hideSelectAllButton();

	return pub;
}
