/**************************************************************
* UI.js
* MATH 3134: Applied Combinatorics & Graph Theory
* Zachary Rattner
* 
* This file handles the user interface (UI) of the web page. 
**************************************************************/

// Global variables
var CitySelectListCount = 4;  // Number of city selection lists on the page. Grows and shrinks with road map
var Step = 1;				  // Step 1: Intro, Step 2: Define graph, Step 3: Load, Step 4: View results
var RoadNumber = 0;			  // Total number of roads in map
var CityList = new Array();	  // Total list of cities in map
var OriginCity;				  // Intial vertex
var DestinationCity;		  // Final vertex
var Cities1 = new Array();	  // First column of roads table
var Cities2 = new Array();	  // Second column of roads table
var Distances = new Array(1); // Distance column of roads table
var MAX_CITIES = 25;		  // Maximum number of allowable cities on one map
var MAX_ROADS = 300;		  // Maximum number of allowable roads on one map (K25 has 300 edges)
var FirstRun = true;          // Checks to see if the user is running the program for the first time
var ARRAY_DELIMITER = "|||";  /* We can't pass an array to the PHP file, so we need to temporarily
							     convert the array to a string. This delimiter is used to denote
								 the end of one item and the beginning of another. */
								 
/**************************************************************
* remove
* @param: String value to remove
* @return: None, but invoking object affected
* This function extends the built-in Array object to allow for
* remove-by-value function calls. Removes all instances of 
* specified value in the given array.
**************************************************************/
Array.prototype.remove = function(Value) {
    var Length = this.length;
    for (var i = 0; i < Length; i++) {
        if (Value == this[i]) {
            this.splice(i, 1);
        }
    }
}

/**************************************************************
* getCityName
* @param: None
* @return: string City Name
* This function uses a basic algorithm to generate a random,
* pronouncable, and non-profane city name.
**************************************************************/
function getCityName() {
	// Define legal city name characters
	var Consonants = "bcdfghjklmnpqrtsvwxyz";
	var Vowels = "aeiou";
	var CityName = "";
	
	// Define legal city name suffixes
	var Suffixes = new Array("land", "ville", "town", "ton", "burg", "city");

	// Generate a random 4-5 character pronouncable city name
	CityName += Math.floor(Math.random() * 4) > 1 ? Vowels.charAt(Math.floor(Math.random() * 5)) : "";	
	CityName += Consonants.charAt(Math.floor(Math.random() * 21));
	CityName += Vowels.charAt(Math.floor(Math.random() * 5));
	CityName += Math.floor(Math.random() * 2) > 1 ? Vowels.charAt(Math.floor(Math.random() * 5)) : "";
	CityName += Consonants.charAt(Math.floor(Math.random() * 21));
	CityName += Suffixes[Math.floor(Math.random() * 6)];
	
	// Check city name for offensive words
	var OffensiveWords = new Array("fuck", "shit", "hell", "fag", "sex");
	for (var Word in OffensiveWords) {
		if (CityName.indexOf(OffensiveWords[Word]) > -1) {
			// Recursively repeat until the city name is G-rated
			CityName = getCityName();
		}
	}
	
	return Capitalize(CityName);
}

/**************************************************************
* Capitalize
* @param: string City Name
* @return: string City Name
* This function capitalizes the first letter of a string, and 
* makes all the other letters lowercase.
**************************************************************/
function Capitalize(Text) {
    return Text.charAt(0).toUpperCase() + Text.substr(1).toLowerCase();
}

/**************************************************************
* getCitySelectList
* @param: int List Index
* @return: string City Select List
* This function generates a select list with all cities as 
* options with id CityList<List Index>, where <List Index> 
* is the parameter passed into the function.
**************************************************************/
function getCitySelectList(ListIndex) {
	// Initiate select list
	var SelectList = "<select id='CityList" + ListIndex + "'>";
	
	// Add an option for each city in the list
	var CityListLength = CityList.length;
	for (var i = 0; i < CityListLength; i++) {
		SelectList += "<option value='" + CityList[i] + "'>" + CityList[i]  + "</option>";
	}
	
	// End the list
	SelectList += "</select>";	
	return SelectList;
}

/**************************************************************
* UpdateCitySelectLists
* @param: None
* @return: None, but page altered
* This function rebuilds all city select lists on the page. 
* This is useful when a city has been added or removed.
**************************************************************/
function UpdateCitySelectLists() {
	// For every select list on the page, print out the new list
	for (var i = 1; i <= CitySelectListCount; i++) {
		$(".CityListWrapper" + i).html(getCitySelectList(i));
	}
}

/**************************************************************
* AddCity
* @param: None
* @return: None, but page altered
* This function generates a new city and adds it to the map.
* For computational complexity's sake, this function will not
* add more than 25 cities to a map.
**************************************************************/
function AddCity() {
	// Stop execution if the city limit is reached
	if (!CheckCityListLength("Add")) { return; }

	// Get the new city name
	var CityName = getCityName();

	// Add the city to the bookkeeping list
	CityList.push(CityName);
	
	// Update the master listing
	var OldData = $("#Cities").html();
	$("#Cities").html(OldData + '<div class="City">' + CityName + 
	'</div><div class="CityOption" onclick="RemoveCity(\'' + CityName + 
	'\');" onmousedown="return false;" onclick="return false;">remove</div>' +  
	'<div class="Clear"></div>');
	
	// Update the selection lists to reflect the change
	UpdateCitySelectLists();
	
	// Update city heading to reflect the new amount of cities
	$("#CitiesHeading").html(CityList.length + " Cities on the Map");
}

/**************************************************************
* UpdateCityHeading
* @param: None
* @return: None, but page altered
* This function updates the master city list heading according
* to the number of cities on the map.
**************************************************************/
function UpdateCityHeading() {
	var CityListLength = CityList.length;
	
	// Use proper grammar
	if (CityListLength == 1) {
		$("#CitiesHeading").html(CityList.length + " City on the Map");
	}
	else {
		$("#CitiesHeading").html(CityList.length + " Cities on the Map");
	}
}

/**************************************************************
* CheckCityListLength
* @param: string Action, element of {Add, Remove}
* @return: True if a city can be added or removed, false 
* otherwise
* This function checks the total number of cities on the map
* and returns a flag so AddCity can ignore the request if 
* there are already 25 cities on the map. This function also
* handles the status notification in the event of failure.
**************************************************************/
function CheckCityListLength(Action) {
	var CityListLength = CityList.length;
	
	/* Determine which action the invoking function is attempting to perform
	   and take appropriate action. */
	if (Action == "Add") {
		if (CityListLength >= MAX_CITIES) {
			setStatus("For computational complexity's sake, maps are limited to a maximum of 25 cities.", 5000);
			return false;
		}
		return true;
	}
	else if (Action == "Remove") {
		if (CityListLength < 3) {
			setStatus("Your map must contain at least two cities!", 3000);
			return false;
		}
		return true;
	}
}

/**************************************************************
* SetStatus
* @param: string Message, int Time
* @return: none, but page affected
* This function sets the status message area to a specified
* message. The message will fade in, and the browser will
* scroll back uo to the top of the page. The time parameter
* defines the time in milliseconds before the status message
* fades out.
**************************************************************/
function setStatus(Message, Time) {
	$("#Status").html(Message);
	$("#Status").fadeIn(300);
	setTimeout('$("#Status").fadeOut(1000);', Time);
	window.scroll(0,0);	
}

/**************************************************************
* CheckStep
* @param: int Step Number
* @return: None
* This function checks to see if certain criteria are met for
* continuing. If the user input is valid, it calls goToStep to
* advance the page to the next step. Otherwise, it displays 
* errors using setStatus.
**************************************************************/
function CheckStep(Step) {
	
	if (Step == 2) {
		OriginCity = $("#CityList1").val();
		DestinationCity = $("#CityList2").val();
		
		// Make sure the user isn't trying to end at the same location as the start
		if (OriginCity == DestinationCity) {
			setStatus("The origin city must be different than the destination city.", 3000);
		}
		else {
		    if (FirstRun) {
			    AddRoad(); // Add 1 road the the list
            }	
            		
			// Set intstructional paragraph text based on user's parameters
			$("#OriginCity").html(OriginCity);
			$("#DestinationCity").html(DestinationCity);
			
			// Advance to the next step
			goToStep(3);
		}
	}
}

/**************************************************************
* RemoveCity
* @param: string CityName
* @return: None, but page altered
* This function checks to see if there are at least 3 cities
* on the map. If there are, it removes the specified city
* from the master list, and updates all the select lists on
* the page to reflect the removal. If there are fewer than 
* three cities, it displays the notification using setStatus.
**************************************************************/
function RemoveCity(CityName) {
	// Disallow removal if it would result in fewer than 2 cities on the map
	if (!CheckCityListLength("Remove")) { return; }

	// Remove the city from the bookkeeping list
	CityList.remove(CityName);
	
	// Update the master listing
	var CityListLength = CityList.length;
	$("#Cities").html("");
	var OldData;
	for (var i = 0; i < CityListLength; i++) {
		OldData = $("#Cities").html();
		$("#Cities").html(OldData + '<div class="City">' + CityList[i] + 
		'</div><div class="CityOption" onclick="RemoveCity(\'' + CityList[i] + 
		'\');" onmousedown="return false;" onmouseup="return false;">remove</div>' + 
		'<div class="Clear"></div>');
	}
	
	// Update the selection lists to reflect the change
	UpdateCitySelectLists();
	
	// Update city heading to reflect the new amount of cities
	UpdateCityHeading();
}

/**************************************************************
* goToStep
* @param: int Step Number, element of {1, 2, 3, 4}
* @return: None, but page altered
* This function handles advancing steps. It hides all elements
* not pertaining to the specified step, and shows the element
* pertaining to the parameter-specified step.
**************************************************************/
function goToStep(Step) {
	for (var i = 1; i <= 5; i++) {
		// If the current step is the desired step, show the information.
		if (Step == i) {
			$("#Step" + i).css("display", "block");
			$("#Instructions" + i).css("display", "block");
		}
		// Otherwise, the information.
		else {
			$("#Step" + i).css("display", "none");
			$("#Instructions" + i).css("display", "none");
		}
	}
	
	// Allow the user to run the program multiple times on one page load
	if (Step == 3) {
	    FirstRun = false;
	}
}

/**************************************************************
* CheckRoadNumber
* @param: int Action, element of {Add, Remove}
* @return: True if a road can be added or removed, false 
* otherwise 
* This function handles step management. If a step limit is
* reached, a notification is printed to the status area and 
* the function returns false. Otherwise, the function returns
* true.
**************************************************************/
function CheckRoadNumber(Action) {
	if (Action == "Add" && RoadNumber >= MAX_ROADS) {
		setStatus("For computational complexity&#39;s sake, your map is limited to 250 roads.", 5000);	
		return false;
	}
	else if (Action == "Remove" && RoadNumber < 2) {
		setStatus("Your map must contain at least one road!", 3000);
		return false;
	}
	return true;
}

/**************************************************************
* RemoveRoad
* @param: int Road Index
* @return: None, but page altered
* This function removes a specified road from the map. Before
* removing, it checks to make sure the removal will not put
* the total road count outside of the allowable limits.
**************************************************************/
function RemoveRoad(RoadIndex) {
	// Make sure we are within the legal road limit
	if (!CheckRoadNumber("Remove")) { return; }
	$("#Road" + RoadIndex).remove();
	RoadNumber--;
}

/**************************************************************
* AddRoad
* @param: None
* @return: None, but page altered
* This function adds a road to the map, if adding a road would
* not cause the total road count to exceed the allowable limit.
**************************************************************/
function AddRoad() {
	// Make sure we are within the legal road limit
	if (!CheckRoadNumber("Add")) { return; }
	
	// Get 2 new city lists
	RoadNumber++;
	var CitySelectList1 = getCitySelectList(CitySelectListCount);
	CitySelectListCount++;
	var CitySelectList2 = getCitySelectList(CitySelectListCount);	
	
	// Generate code for new table row
	var NewRow = '<tr id="Road' + RoadNumber + '"><td class="CityListWrapper' + 
	              (CitySelectListCount - 1) + '">' + CitySelectList1 + 
	              '</td><td class="CityListWrapper' + CitySelectListCount + '">' + 
	              CitySelectList2 + '</td><td><input type="text" class="Distance" id="Distance' + 
	              RoadNumber + '" value="3" size="3" maxlength="3" /> miles</td>' + 
	              '<td><div onclick="RemoveRoad(\'' + 
	              RoadNumber + '\');" class="CityOption">remove</div></td></tr>';
	
	// Add the new table row to the page
	$("#RoadMap").append(NewRow);
}

/**************************************************************
* CollectRoadData
* @param: None
* @return: None, but global variables altered
* This function gathers all road data from the map and stores
* it in the global Cities1, Cities2, and Distances arrays.
**************************************************************/
function CollectRoadData() {
	// Collect first column
	Cities1 = new Array();
	var CurrentCity = "";
	$("#RoadMap").children(":first").children().each(function (i) {
		CurrentCity = $(this).children(":first").children(":first").val();
		if (typeof(CurrentCity) != "undefined") {
			Cities1.push(CurrentCity);													  
		}
	});
	
	// Collect second column
	Cities2 = new Array();
	$("#RoadMap").children(":first").children().each(function (i) {
		CurrentCity = $(this).children(":nth-child(2)").children(":first").val();
		if (typeof(CurrentCity) != "undefined") {
			Cities2.push(CurrentCity);													  
		}
	});
	
	// Collect distance column
	Distances = new Array();
	$(".Distance").each(function () {
		Distances.push($(this).attr("value"));
	});
	
	// Check for road from one city to the same city
	/* var RoadListLength = Cities1.length;
	for (var i = 0; i < RoadListLength; i++) {
		if (Cities1[i] == Cities2[i]) {
			setStatus("All roads must include two different cities. Please revise your map and try again.", 4000);	
			return;
		}
	} */
	
	// Check for non-numeric distances and non-positive integer distances
	var DistancesLength = Distances.length;
	for (i = 0; i < DistancesLength; i++) {
		if (Number(Distances[i]) != Distances[i] || 
		    Math.round(Distances[i]) != Distances[i] ||
			Distances[i] <= 0) {
			setStatus("All distances must be positive integers. Please revise your map and try again.", 4000);
			return;	
		}
	}
	
	// Convert arrays to strings to pass back to the PHP file
	Cities1 = Cities1.join(ARRAY_DELIMITER);
	Cities2 = Cities2.join(ARRAY_DELIMITER);
	Distances = Distances.join(ARRAY_DELIMITER);
	
	PerformSearch();
}

/**************************************************************
* PerformSearch
* @param: None
* @return: None, but AJAX call made and page altered
* This function assumes global map data is set and makes the
* AJAX call to actually perform the search. It also advances
* the page to the loading step and handles the response data.
**************************************************************/
function PerformSearch() {
	
	// Prepare master list and update interface to show loading animation
	var MasterCityList = CityList.join(ARRAY_DELIMITER);
	goToStep(4);
	
	// Send the data to be analyzed
	$.post("AJAX/PerformSearch.php",{
		   MasterCityList: MasterCityList,
		   OriginCity: OriginCity,
		   DestinationCity: DestinationCity,
		   Cities1: Cities1,
		   Cities2: Cities2,
		   Distances: Distances,
		   Delimiter: ARRAY_DELIMITER},
	function (ResponseData) {
	    // Stop loading sequence when done
	    goToStep(5);
	    
	    // Populate page with results
	    ResponseData = ResponseData.split(ARRAY_DELIMITER);
		setStatus(ResponseData[0], 5000);
		
		// If more data was returned, put it in the main page area
		if (ResponseData[1]) {
    		$("#ResponseData").html(ResponseData[1]);
	    }
	});
}

/**************************************************************
* OnLoad Function
* @param: None
* @return: None, but page altered
* This function intiailizes the page to 5 cities when the
* page loads.
**************************************************************/
$(document).ready(function() {
	// Initialize page to 5 cities
	for (var i = 0; i < 5; i++) {
		AddCity();
	}	
});
