// implements the logic of the Works With page

// constants
var WW = {
	targetIdPrefix: "target_",
	catIdPrefix: "cat_",
	wwDir: "/en/products/mathtype/ww/",
	
	// enumeration constants used in the JSON file
	// each values will be set to strings preceded by '#' (eg, xx = null ==> xx = "#xx")
	c: {
		Win: null, 
		Mac: null, 
		Both: null, 
		Any: null
	}
};

// the model as in model/view/controller
var Model = {
// public
	init : function(inline) {
		// read the "works with" database into memory
		this._db = LoadWorksWithDB(inline);
		// initialize our constants
		FixupConstants(WW.c);
	},

	setCurrentTarget : function(
		targ	// select a new target (must not be null)
	){
		if( ! targ )
			alert("setCurrentTarget(null)");
		this._target = null;
		this._search = "";

		// make sure current category contains the target
		switch( this._category ) {

		case "top":
			// if target is not a top target ...
			if( ! this.targetIsTop(targ) )
				this.setCurrentCategory("all");
			break;

		case "all":
			break;

		default:
			// if this category is not the target's category ...			
			if( this._category != this.categoryOfTarget(targ) )
				this.setCurrentCategory("all");
		}
		
		this._target = targ;
	},
	
	getCurrentTarget : function() { return this._target; },
	
	getTarget : function(target) {
		if( typeof(target) == "string" )
			return this._db.targets[target];
		else
			return target;
	},

	targetExists: function(targ) {
		return this._db.targets.hasOwnProperty(targ);
	},
	
	setCurrentCategory : function(
		cat // new category
	){
		// selecting a new category deselects the target and clears search
		if( cat != this._category ) {
			this._target = null;
			this._category = cat;
			this._search = "";
		}
	},
	
	getCurrentCategory : function() { return this._category; },
	
	getCategory : function(cat) {
		return this._db.categories[cat];
	},

	categoryExists : function(cat) {
		return (cat == "all") || (cat == "top") || this._db.categories.hasOwnProperty(cat);
	},
	
	getCategories : function() { return this._db.categories; },
	
	getTargets : function() { return this._db.targets; },
	
	categoryOfTarget : function(
		target
	){
		target = this.getTarget(target);
		return target.CatShortName;
	},
	
	targetIsTop : function(
		targ
	){
		return this._db.targets[targ].Popular;
	},

	targetIsPreliminary : function(target) {
		return WorksWith.presales ? target.PreliminaryPresales :  target.PreliminaryPostsales;
	},
	
	targetIsToBeShown : function(target) {
		return ! (target.Hidden || (target.CatShortName == "TO_BE_DETERMINED"));
	},
	
	forAllTargets : function(thisObj,func) {
		var index = 1;
		for( var memberName in this._db.targets )
			func.call(thisObj,memberName,this._db.targets[memberName],index++);
	},

	getTopTargets : function() {
		var topTargets = new Array();
		
		for( var i = 0; i < this._db.targets.length; i++ ) {
			var target = this._db.targets[i];
			if( target.Popular )
				topTargets.push(target);
		}

		return topTargets;
	},

	getSearch : function() { return this._search; },
	
	setSearch : function(search) {
		if( search == null )
			alert("setSearch(null)");
		this._search = search;
		
		// if there is a search ...
		if( search != "" ) {
			// the category must be "all"
			this._category = "all";
			// if there's a current target and it doesn't match the search string, deselect it
			if( this._target && (! this.isTargetMatch(this._target,search)) )
				this._target = null;
		}
	},
	
	isTargetMatch : function(
		target,		// target object (or its name) to match
		searchRE		// search string or regular expression
	){
		if( typeof(target) == "string" )
			target = this._db.targets[target];
			
		if( typeof(searchRE) == "string" )
			searchRE = this.stringToRegexp(searchRE);
		
		return target.FormalName.search(searchRE) >= 0;
	},

	stringToRegexp : function(
		search	// search string
	){
		return new RegExp(search,"i");
	},
	
	getAssocTargets : function(target) {
		var assocTargets = this.getTarget(target).AssociatedTargets;
		// we have to test for an empty list as the split method always returns a 1-element array in this case
		if( assocTargets == "" )
			return null;
		return assocTargets.split(/\s*,\s*/);
	},
	
	test : function(testParam) {
		var numCats = CountOfMembers(this._db.categories);
		var numTargs = CountOfMembers(this._db.targets);
		Controller.addTestLine(true,"number of categories = " + numCats.toString() + ", number of targets = " + numTargs.toString());
		Controller.addTestLine(true,"JSON: " + this._db.gentime);		
		Controller.addTestLine(true,"&nbsp;");		

		if( testParam == "all" )
			this.forAllTargets(this,this.testTarget);
		else if( this.targetExists(testParam) )
			this.testTarget(testParam,this.getTarget(testParam));
		else
			Controller.addTestLine(false,"error: test parameter '" + testParam + "' is not a known target.");
	},

	testTarget : function(targName,targObj,index) {
		if( targObj.CatShortName != "TO_BE_DETERMINED" ) {
			// check that the target's category is a real category
			if( ! this._db.categories.hasOwnProperty(targObj.CatShortName) ) {
				Controller.addTestLine(false,"error: target '" + targName + "' has undefined category '" + targObj.CatShortName + "'.");
			}
		}
		
		// check that every enumeration constant (a member whose value is a string of the form "#xxx") is a member of our constants
		for( var memberName in targObj ) {
			var value = targObj[memberName];
			if( (typeof(value) == "string") && (value.charAt(0) == "#") ) {
				var constName = value.split("#");
				if( ! WW.c.hasOwnProperty(constName[1]) )
					Controller.addTestLine(false,"error: target '" + targName + "' has unknown constant '" + value + "'.");
			}
		}
		
		// check that every associated target is a real target
		var assocTargets = this.getAssocTargets(targObj);
		if( assocTargets ) {
			var first = true;
			var errorMsg;
			for( var i = 0; i < assocTargets.length; i++ ) {
				var assocShortName = assocTargets[i];
				if( ! this.targetExists(assocShortName) ) {
					if( first ) {
						first = false;
						errorMsg = "error: target '" + targName + "' has undefined AssociatedTargets:<br>   '" + assocShortName + "'";
					}
					else
						errorMsg += ", '" + assocShortName + "'";
				}
			}

			if( errorMsg )
				Controller.addTestLine(false,errorMsg);
		}
	},
	
// private
	// if target is set, category must contain the target or be "all"
	// if search is not "", then category must be "all"
	_target : null,		// target's ShortName, null if none
	_category : "top",	// category's ShortName, "top", or "all"
	_search : "",			// search string
	_db : null				// target and category database loaded from JSON
};

// the controller as in model/view/controller
var Controller = {
	// public
	init: function() {
		var testParam = this._processURLParams();
		View.init();
		if( testParam )
			this._createTestReport(testParam);
	},

	selectCategoryOfTarget: function() {
		var target = Model.getCurrentTarget();
		if (target) {
			var category = Model.categoryOfTarget(target);
			Model.setCurrentCategory(category);
			Model.setCurrentTarget(target);
			View.update();
		}
	},

	gotoPostSalesPage: function(
	) {
		var target = Model.getCurrentTarget();
		if (target) {
			window.location = "/en/support/mathtype/workswith/" + target + ".htm";
		}
	},

	gotoTargetHomePage: function(
	) {
		var target = Model.getCurrentTarget();
		if (target) {
			var homepage = Model.getTarget(target).Homepage;
			if (homepage)
				window.open(homepage);
		}
	},
	
	setTarget : function(targetName) {
		Model.setCurrentTarget(targetName);
		View.update();	// must use View as 'this' is not defined
	},

	getTargetPageURL : function(
		target,		// target object
		presales,	// true for presales, false for postsales
		reverse		// true to reverse directory (used only in testing)
	){
		var dir = WW.wwDir;
		
		dir += presales ? "pre/" : "post/";
		
		var prelim = Model.targetIsPreliminary(target);
		if( reverse )
			prelim = ! prelim;
		
		dir += prelim ? "targets_nc/" : "targets/";
		
		return dir + target.ShortName + ".htm";
	},
	
	addTestLine: function(success,line) {
		if( ! success )
			this._testErrorCount++;
		this._testReport += line + "<br>";
	},
	
	// private
	// returns test parameter perform tests, null for normal operation
	_processURLParams: function() {
		// select items in menus based on URL params
		var test = GetURLParam("test");
		var target = GetURLParam("target");
		var cat = GetURLParam("cat");
		var search = GetURLParam("search");
		var source = GetURLParam("source");

		Model.init(source == "inline")
		
		// if test=xxx was present in the URL ...
		if (test) {
			return test;
		}
		
		// if target=xxx was present in the URL ...	
		if( target && Model.targetExists(target) ) {
			Model.setCurrentCategory("all");
			Model.setCurrentTarget(target);
		}
		// else if cat=xxx was present in the URL ...	
		else if (cat && Model.categoryExists(cat)) {
			Model.setCurrentCategory(cat);
		}
		// else if search=xxx was present in the URL ...	
		else if (search) {
			Model.setCurrentCategory("all");
			Model.setSearch(search);
		}

		return null;
	},
	
	_testReport: "",
	_testErrorCount: 0,

	_createTestReport: function(testParam) {
		this._startTestReport();
		this._doTests(testParam);
		this._finishTestReport();
	},
	
	_startTestReport: function() {
		this._testReport = "";
		this._testErrorCount = 0;
		
		View.startTestReport();
	},
	
	_doTests: function(testParam) {
		try {
			Model.test(testParam);
			View.test(testParam);
		}
		catch( err ) {
			this.addTestLine(false,"error: " + err.toString());
		}
	},
	
	_finishTestReport: function() {
		View.finishTestReport(this._testErrorCount,this._testReport);
	}
	
};

// the view as in model/view/controller
var View = {
// public
	init : function() {
		// hide the target-specific content  and show the default content with instructions to the user
		this._showTargetContent(false);

		// save the search box prompt string
		this._searchPrompt = document.getElementById("searchInput").value;		

		// hook up the Target menu to its event handler
		SetMenuEventHandler("targetMenu",this._onTargetChange);

		// hook up the Category menu to its event handler
		SetMenuEventHandler("catMenu",this._onCategoryChange);
		
		// hook up the Search text field to its event handlers
		SetTextBoxEventHandler("searchInput","onfocus",this._onSearchFocus);
		SetTextBoxEventHandler("searchInput","onblur",this._onSearchBlur);
		SetTextBoxEventHandler("searchInput","onkeyup",this._onSearchKey);

		// fill the category menu with items based on the database
		this._fillCatMenu();
		
		// update the view based on the model
		this.update();
	},
	
	// update the view by reading the model
	update : function() {
		var targName = Model.getCurrentTarget();
		var cat = Model.getCurrentCategory();
		var search = Model.getSearch();

		// select the category in the menu
		this._selectCategory(cat);
		// populate the target menu with the targets in the category
		this._fillTargetMenu(cat,search);
		// update the search field
		this._setSearchText(search);
			
		// if a target is chosen ...	
		if( targName ) {
			// select the target in the menu
			this._selectTarget(targName);
			// set the content to that of the target
			this._setTargetContent(targName);
		}
		// else no target chosen ...	
		else {
			// clear the target content
			this._setTargetContent(null);
		}
	},
	
	startTestReport: function() {
		// test report is made in the "targetNotLoaded" area
		this._showTargetContent(false);
			
		targetContentDiv = document.getElementById("targetNotLoaded");
		targetContentDiv.innerHTML =
			"<H1>Test Report: <span id=\"reportPassFail\">WORKING</span></H1>" +
			"<p id=\"reportBody\"></p>";
	},
	
	setTestProgress: function(
		progress // HTML to place in progress area
	){
		var reportBody = document.getElementById("reportBody");
		reportBody.innerHTML = progress;
	},
	
	finishTestReport: function(
		errorCount,	// true if testing succeeded, false if at least one test failed
		report		// HTML content of report
	){
		targetContentDiv = document.getElementById("targetNotLoaded");
		if( errorCount > 0 )
			targetContentDiv.style.backgroundColor = "pink";
			
		var reportPassFail = document.getElementById("reportPassFail");
		reportPassFail.innerHTML = (errorCount > 0) ? ("FAILURE: # of errors = " + errorCount) : "SUCCESS";
		
		var reportBody = document.getElementById("reportBody");
		reportBody.innerHTML = report;
	},
	
	test : function(testParam) {
		if( testParam == "all" ) {
			Model.forAllTargets(
				this,
				function(targName,targObj,index) {
					this._testPreAndPostTargetPages(targName);
					this.setTestProgress("testing target " + index + ": " + targName);
				}
			);
		}
		else if( Model.targetExists(testParam) ) {
			this._testPreAndPostTargetPages(testParam);
		}
		else {
			Controller.addTestLine(false,"error: test parameter '" + testParam + "' is not a known target.");
		}
	},
	
// private
	_setSearchText : function(
		newSearchText
	){
		var searchBoxNode = document.getElementById("searchInput");
		
		// if the text has changed, update it
		if( newSearchText != searchBoxNode.value )
			searchBoxNode.value = newSearchText;
	},

	_fillCatMenu : function() {
		var selectObj = IdToDOMNode("catMenu");
		
		// we don't need to clear the menu as we only do this once
		// also clearing it would delete the "All" item in the HTML
		var top = { text: "-- Top 10 apps and sites --",	value: "top" };
		var all = { text: "-- All apps and sites --",		value: "all" };
		var gen = { text: "-- General techniques --",		value: "General_techniques" };
		
		AppendMenuItem(selectObj,top.text,top.value,"cat_top");
		AppendMenuItem(selectObj,all.text,all.value,"cat_all");
		AppendMenuItem(selectObj,gen.text,gen.value,"cat_gen");
		
		AppendMenuFromObjMembers(selectObj,Model.getCategories(),"FormalName",WW.catIdPrefix,null);

		AppendMenuItem(selectObj,top.text,top.value,"cat_top_end");
		AppendMenuItem(selectObj,all.text,all.value,"cat_all_end");
		AppendMenuItem(selectObj,gen.text,gen.value,"cat_gen_end");
	},
	
	// fill the target menu with items based on the database
	_fillTargetMenu : function(
		catShortName,	// category of targets to add to menu, "all", or "top"
		search			// if category == "all", filter targets using search string
	){
		if( (search != "") && (catShortName != "all") )
			alert("search string only valid with category == \"all\".");
			
		// if the items being shown are already correct, just return
		if( this._targetListCat == catShortName ) {
			if( (catShortName != "all") || (this._targetSearch == search) )
				return 
		}
		
		// remember we have already got this target list
		this._targetListCat = catShortName;
		this._targetSearch = search;

		var searchRE = null;
		if( search != "" )
			searchRE = Model.stringToRegexp(search);
		
		// filter function returns true if the target should be added, false if not
		var filter = function(
			targetObj // the member of the targets object to be tested
		){
			if( ! Model.targetIsToBeShown(targetObj) )
				return false;
			
			if( catShortName == "all" ) {
				if( searchRE )
					return Model.isTargetMatch(targetObj,searchRE);
				else
					return true;
			}
			else if( catShortName == "top" ) {
				return targetObj.Popular != 0;
			}
			// else if this target is in the given category, include it
			else if( targetObj.CatShortName == catShortName ) {
				return true;
			}
			
			return false;
		}
		
		// this gets called more than once so we have to clear the menu before adding the targets
		ClearMenu("targetMenu");
		AppendMenuFromObjMembers("targetMenu",Model.getTargets(),"FormalName",WW.targetIdPrefix,filter);
	},

	_selectTarget : function(
		targName
	){
		SelectMenuItem(WW.targetIdPrefix+targName);
	},
	
	_showTargetContent : function(
		show	// true to show, false to hide
	){
		ShowHideDiv("targetNotLoaded",! show);
		ShowHideDiv("targetLoaded",show);
	},
	
	_loadTargetPage : function(
		target,		// target object
		presales,	// true for presales, false for postsales
		reverse		// true to reverse directory (used only in testing)
	){
		var targetContentDiv = document.getElementById(presales ? "presalesContent" : "postsalesContent");

		// load the page into a string
		var targetPageURL = Controller.getTargetPageURL(target,presales,reverse);
        var url = window.location.href;
        var pos = url.indexOf("?");
        var tar = "";
        if (pos < 0) {
 		   tar = url + "?target=" + target.ShortName;
 		}
 		else {
 		   var str = url.substring(0, pos); 
 		   tar = str + "?target=" + target.ShortName;
 		}
		var targetHTML = ajaxRequest(targetPageURL);
		if( targetHTML != null ) {
			// replace the div's contents by the entire loaded page 
//			targetContentDiv.innerHTML = "<p><a href='" + tar + "'>Works With URL</a></p>" + targetHTML;
			targetContentDiv.innerHTML = targetHTML;
			var container = document.getElementById("targetWWurl");
            container.innerHTML = "<a href='" + tar + "'>Link to this entry</a>";
			return true;
		}
		else {
			targetContentDiv.innerHTML = "<H2 style=\"background-color:pink\">ERROR: Can't load content from " + targetPageURL + "</H2>";
			return false;
		}
	},
	
	_setTargetContent : function(
		targName	// target's ShortName, null for default "please select a target" text
	){
		// if a target name is given ...
		if( targName ) {
			// hide the targetLoaded area while we modify its content to avoid flashing
			ShowHideDiv("targetLoaded",false);

			var target = Model.getTarget(targName);

			this._googleTrack(targName);

			// load the pre-sales content ...
			this._loadTargetPage(target,true,false);
			
			// if on a pre-sales page ...
			if( WorksWith.presales ) {
				// hide the post-sales content area
				ShowHideDiv("postsalesArea",false);
			}
			// else on a post-sales page ...
			else {
				// load the post-sales content 
				this._loadTargetPage(target,false,false);
				// show the post-sales content area
				ShowHideDiv("postsalesArea",true);
			}
			
			// do replacements and other fixups on the resulting page
			this._fixupPage(target);
				
			// show the per-target content and hide "Please select a target"
			this._showTargetContent(true);
		}
		// else no target name ...
		else
			// hide the per-target content and show "Please select a target"
			this._showTargetContent(false);
	},

	_googleTrack : function(
		targName	// target's ShortName
	){
		var url = "/v/workswith/" + (WorksWith.presales ? "presales/" : "postsales/") + targName;
		if( WorksWith.platform == "Mac" )
			url += "/mac";
		else if( WorksWith.platform == "Win" )
			url += "/win";
		// else if WorksWith.platform == "All", we don't add platform dir to url
		
		// pageTracker._trackPageview(url); //old page tracker
		_gaq.push(['_trackPageview', url]); // new async code
	},
	
	_testPreAndPostTargetPages : function(
		targName	// target's ShortName
	){
		// always test pre-sales target page
		this._testTargetPage(targName,true);
		
		// only test post-sales target page when in post-sales mode
		if( ! WorksWith.presales )
			this._testTargetPage(targName,false);
	},
	
	_testTargetPage : function(
		targName,	// target's ShortName
		presales		// true if pre-sales, false if post-sales
	){
		var target = Model.getTarget(targName);
		
		// if we can load the target page from where it ought to exist ...
		if( this._loadTargetPage(target,presales,false) )
			this._testTargetContent(target,presales,"targetContent");
		// else can't load page ...
		else
			Controller.addTestLine(false,"error: can't load: '" + Controller.getTargetPageURL(target,presales,false) + "'.");
		
		// if we can load the target page from where it ought NOT exist ...
		if( this._loadTargetPage(target,presales,true) )
			Controller.addTestLine(false,"error: unexpected page: '" + Controller.getTargetPageURL(target,presales,true) + "'.");
	},

	_testTargetContent : function(
		target,		// target object
		presales,	// true if pre-sales, false if post-sales
		targDiv		// per-target content div node
	){
		// page must contain an element with id = "wwtInstructions"
		if( ! document.getElementById("wwtInstructions") )
			Controller.addTestLine(false,"error: content for '" + target.ShortName + "' is missing an element with id = \"wwtInstructions\".");
			
		// text inside the first <H1> element should == "[FormalName] Pre-Sales" or "[FormalName] Post-Sales"
		var titleElem = document.getElementById("wwtTitle");
		if( titleElem ) {
			var actualTitle = GetTextContent(titleElem);
			var expectedTitle = target.FormalName + (presales ? " Pre-Sales" : " Post-Sales");
			
			// remove whitespace to make the comparison
			var matchWhiteSpace = /[^a-zA-Z0-9-_]/g;
			var actualTitleNoSpace = actualTitle.replace(matchWhiteSpace,"");
			var expectedTitleNoSpace = expectedTitle.replace(matchWhiteSpace,"");
			
			if( actualTitleNoSpace != expectedTitleNoSpace )
				Controller.addTestLine(false,"error: title for '" + target.ShortName + "' is '" + actualTitle + "', expected '" + expectedTitle + "'.");
		}
		else
			Controller.addTestLine(false,"error: content for '" + target.ShortName + "' is missing an element with id = \"wwtTitle\".");
		
		if( presales ) {
			// presales page must contain an element with id = "wwtPostSalesLink"
			if( ! document.getElementById("wwtPostSalesLink") )
				Controller.addTestLine(false,"error: content for '" + target.ShortName + "' is missing an element with id = \"wwtPostSalesLink\".");
		}
	},
	
	// fix up the page after loading
	_fixupPage : function(
		target	// target object
	){
		var targetContentDiv = document.getElementById("targetContent");
	
		// remove all child nodes with class == "wwtInteropRemove"
		RemoveChildrenByClass(targetContentDiv,"wwtInteropRemove");
		
		// remove all child nodes with class == "wwtComment"
		RemoveChildrenByClass(targetContentDiv,"wwtComment");
		
		// remove nodes with class == wwtPostsalesOnly or wwtPresalesOnly as appropriate
		RemoveChildrenByClass(targetContentDiv, WorksWith.presales ? "wwtPostsalesOnly" : "wwtPresalesOnly" );
		
		// replace contents of all spans with class "formalName" with the target's FormalName
		ReplaceSpanContents("formalName",target.FormalName);
		
		// replace contents of all spans with class "commonName" with the target's CommonName
		ReplaceSpanContents("commonName",target.CommonName);
		
		// replace contents of all spans with class "CategoryName" with the category's name
		var cat = Model.categoryOfTarget(target);
		ReplaceSpanContents("categoryName",Model.getCategory(cat).FormalName);

		var versionsCovered = target.VersionsCovered;
		if( versionsCovered != "" ) {
			// show the "versions" section
			ShowHideDiv( "targetVersion", true );
			// show span with id "versionsPlural" iff VersionsCovered contains a comma 
			ShowHideSpan( "versionPlural", versionsCovered.indexOf(",") >= 0 );
			// replace content of "versionsCovered" span by VersionsCovered
			ReplaceElemContents("versionsCovered",versionsCovered);
		}
		// else no versions ...
		else {
			// hide the "versions" section
			ShowHideDiv( "targetVersion", false );
		}

		var osPlatform = target.OSPlatform;
		
		if( osPlatform == WW.c.Any ) {
			// hide the "platform" area
			ShowHideSpan("platform",false);
		}
		// else Mac-only, Win-only, or Both ..
		else {
			// show the "platform" area
			ShowHideSpan("platform",true);
			// show "macIcon" span iff OSPlatform == "Mac" 
			ShowHideSpan( "macIcon", (osPlatform == WW.c.Both) || (osPlatform == WW.c.Mac) );
			// hide "winIcon" span iff OSPlatform == "Win"
			ShowHideSpan( "winIcon", (osPlatform == WW.c.Both) || (osPlatform == WW.c.Win) );
		}

		// fix up the "See Also=" list
		this._fixupSeeAlsoSection(target);
	},
	
	// fix up the "See Also=" list
	_fixupSeeAlsoSection : function(
		target	// target object
	){
		var seeAlsoSection = IdToDOMNode("seeAlsoSection");
		var seeAlsoList = IdToDOMNode("seeAlsoList");
		var seeAlsoLinks = "";
		
		var assocTargets = Model.getAssocTargets(target);
		if( assocTargets ) {
			for( var i = 0; i < assocTargets.length; i++ ) {
				var assocShortName = assocTargets[i];
				var assocTarget = Model.getTarget(assocShortName);
				if( Model.targetIsToBeShown(assocTarget) ) {
					var assocFormalName = assocTarget.FormalName
					seeAlsoLinks += "&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"javascript: Controller.setTarget(&quot;" + assocShortName + "&quot;)\">" + assocFormalName + "</a><br>";
				}
			}
		}
		
		seeAlsoList.innerHTML = seeAlsoLinks;
		
		ShowHideSpan( seeAlsoSection, seeAlsoLinks != "" );
	},

	_selectCategory : function(
		catShortName
	){
		SelectMenuItem(WW.catIdPrefix+catShortName);
	},
	
	// event handler for the target menu
	_onTargetChange : function(
		domNode	// <select> for menu
	){
		var targetName = GetMenuSelection(domNode);
		Controller.setTarget(targetName);
	},
	
	// event handler for the category menu
	_onCategoryChange : function(
		domNode	// <select> for menu
	){
		var catName = GetMenuSelection(domNode);
		Model.setCurrentCategory(catName);
		View.update();	// must use View as 'this' is not defined
	},
	
	// event handler for the search box receiving focus
	_onSearchFocus : function(
		domNode	// <input> for search box
	){
		var searchText = domNode.value;
		if( searchText == View._searchPrompt )
			domNode.value = "";
		domNode.select();
	},
	
	// event handler for the search box losing focus
	_onSearchBlur : function(
		domNode	// <input> for search box
	){
		var searchText = domNode.value;
		if( searchText == "" )
			domNode.value = View._searchPrompt;
	},
	
	// event handler for the search box changing value
	_onSearchKey : function(
		domNode	// <input> for search box
	){
		var search = domNode.value;
		Model.setSearch(search);
		View.update();	// must use View as 'this' is not defined
	},
	
	_targetListCat : "NONE",	// used to avoid updating the target list when it isn't changing
	_searchPrompt : null			// copy of "Type here" (or whatever) that appears in the search box when it is empty and doesn't have the focus
};

// start with the Controller when the page loads
// done as a function so that 'this' is set properly in Controller.init().
// if we just set window.onload = Controller.init, then 'this' is set to the document DOM object
window.onload = function() { Controller.init(); };

// replace contents of spans having a given class name
function ReplaceSpanContents(
	className,		// class name to match
	replaceText		// replacement text
){
	var spans = document.getElementsByTagName("span");
	for( var i = 0; i < spans.length; i++ ) {
		var spanObj = spans[i];
		if( spanObj.className.indexOf(className) >= 0 ) {
			spanObj.innerHTML = replaceText;
		}
	}
}

// replace contents of an element
function ReplaceElemContents(
	nodeOrId,		// node or id of element
	replaceText		// replacement text or HTML
){
	var node = IdToDOMNode(nodeOrId);
	node.innerHTML = replaceText;
}

// recursively remove child nodes having a given class
function RemoveChildrenByClass(
	domObj,		// node to check
	className	// class to check
){
	var nodes = domObj.childNodes;
	for( var i = 0; i < nodes.length; ) {
		var childNode = nodes[i];
		if( childNode.nodeName == "#text" ) {
			i++;
		}
		else if( childNode.className.indexOf(className) >= 0 ) {
			domObj.removeChild(childNode);
			nodes = domObj.childNodes;
		}
		else {
			RemoveChildrenByClass(childNode,className);
			i++;
		}
	}
}

// show/hide an HTML div element
function ShowHideDiv(
	div,	// <div> node or id
	show	// true to show, false to hide
){
	var divObj = IdToDOMNode(div);
	divObj.style.display = show ? "block" : "none";
}

// show/hide an HTML span element
function ShowHideSpan(
	span,	// span obj or id
	show	// true to show, false to hide
){
	var spanObj = IdToDOMNode(span);
	spanObj.style.display = show ? "inline" : "none";
}

// browser-independent function to get an element's text content
// IE uses innerText and does not support textContent
function GetTextContent(
	nodeOrId	// node or id
){
	var node = IdToDOMNode(nodeOrId);
	var text = node.textContent;
	if( text )
		return text;
	else
		return node.innerText;
}

// hook up a menu to its event handler
function SetMenuEventHandler(
	menu,				// <select> id or node
	eventHandler	// function to handle events to which the <select> node is passed as an argument
){
	var selectObj = IdToDOMNode(menu);
	selectObj.onchange = function(){ eventHandler(this); };
}

// hook up an text entry box to its event handler
function SetTextBoxEventHandler(
	input,			// <input> id or node
	eventName,		// name of event to be handled
	eventHandler	// function to handle events to which the <input> node is passed as an argument
){
	var inputObj = IdToDOMNode(input);
	inputObj[eventName] = function(){ eventHandler(this); };
}

// add items to a menu for each member of an object
function AppendMenuFromObjMembers(
	select,		// select object for menu to be filled or its id
	obj,			// object whose key-values pairs represent the menu items
	keyToShow,	// key in object whose value is to appear in the menu
	idPrefix,	// prefix to use for ids on options, null for no ids
	filter		// filter function for member objects (return true to add), null to add all
){
	// for all the object's members ...	
	for( var memberName in obj ) {
		// create a new <option>
		var optionObj = document.createElement('option');
		
		// get the member object
		var memberObj = obj[memberName];

		// if no filter or filter returns true, add item		
		if( (! filter) || filter(memberObj) ) {
			// set the option's text and value
			optionObj.text = memberObj[keyToShow];
			optionObj.value = memberName;
			// if desired, set the option's id
			if( idPrefix )
				optionObj.id = idPrefix + memberName;
			
			SafeSelectAppend(select,optionObj);
		}
	}
}

function AppendMenuItem(
	select,	// select object for menu appended to
	text,		// text of item
	value,	// item value to be returned on selection
	id			// id for item (null if none)
){
	// create a new <option>
	var optionObj = document.createElement('option');
	// set the option's text and value
	optionObj.text = text;
	optionObj.value = value;
	if( id )
		optionObj.id = id;
	
	SafeSelectAppend(select,optionObj);
}

function SafeSelectAppend(
	select,		// select node or id
	optionObj
){
	var selectObj = IdToDOMNode(select);

	// add the option to the select node (deal with non-standard IE)
	try {
		selectObj.add(optionObj,null); // standards compliant
	}
	catch(ex) {
		selectObj.add(optionObj); // IE only
	}
}

function SelectMenuItem(
	nodeOrId	// the <option> node or its id
){
	var option = IdToDOMNode(nodeOrId);
	// only select it if it is not selected to avoid scrolling, etc.
	if( ! option.selected )
		option.selected = true;
}

// returns the value property of the selected item in a menu
function GetMenuSelection(
	nodeOrId	// the DOM node or its id
){
	var select = IdToDOMNode(nodeOrId);
	var selectedOption = select.options[select.selectedIndex];
	return selectedOption.value;
}

// remove all items from a menu
function ClearMenu(
	nodeOrId	// the DOM node or its id
){
	var select = IdToDOMNode(nodeOrId);
	while( select.length > 0 )
		select.remove(0);
}

// enable/disable an item
function EnableItem(
	nodeOrId,	// the DOM node or its id
	enable		// true to enable, false to disable
){
	IdToDOMNode(nodeOrId).disabled = ! enable;
}

// returns true if the checkbox is checked
function IsChecked(
	nodeOrId	// the DOM node or its id
){
	return IdToDOMNode(nodeOrId).checked;
}

// set or clear a checkbox
function SetCheckbox(
	nodeOrId,	// the DOM node or its id
	check			// true to check, false to uncheck
){
	IdToDOMNode(nodeOrId).checked = check;
}

// resolve an id to its object
function IdToDOMNode(
	nodeOrId	// the DOM node or its id
){
	if( typeof(nodeOrId) == "string" )
		return document.getElementById(nodeOrId);
	else
		return nodeOrId;
}

function CountOfMembers(
	obj
){
	var count = 0;
	for( var memberName in obj )
		count++;
	return count;
}

function InsertAfter(
	refNode,	// node after which to insert newNode
	newNode	// node to insert
){
	refNode.parentNode.insertBefore( newNode, refNode.nextSibling );
}

function FixupConstants(
	constants
){
	for( var memberName in constants ) {
		constants[memberName] = "#" + memberName;
	}
}

// get a single variable from a url
// based on http://www.netlobo.com/url_query_string_javascript.html
// returns value of parameter or "" if not found
function GetURLParam(
	name // name of parameter whose value is to be extracted from the current window's url's query string
){
	name = name.replace(/[\[]/,"\\\[").replace(/[\]]/,"\\\]");  
	var regexS = "[\\?&]"+name+"=([^&#]*)";  
	var regex = new RegExp( regexS );  
	var results = regex.exec( window.location.href );  
	if( results == null )
		return "";
	else
		return results[1];
}

// ajax call, returns text of loaded document, null on failure
function ajaxRequest(
	url  // url for the file to be read from the server
){
	var result = null;
	var xhr = null;
	
	// get an HTTP request object
	if( window.XMLHttpRequest ) {
		try {
			xhr = new XMLHttpRequest();
		} catch(e) {
			xhr = null;
		}
	}
	else if (window.ActiveXObject) {
		try {
			xhr = new ActiveXObject("Msxml2.XMLHTTP");
		}
		catch (e) {
			try {
				xhr = new ActiveXObject("Microsoft.XMLHTTP");
			}
			catch (e) {
				xhr = null;
			}
		}
	}
	
	// if we have an HTTP request object ...
	if (xhr) {
		// request the page synchronously
// the following line doesn't work on IE6; shouldn't be necessary as we create a new xhr object each time
//		xhr.onreadystatechange = null;
		xhr.open("GET",url, false);
		xhr.send(null); // FF requires the null arg
		
		// if the requested page loaded ok ...
		if( xhr.status == 200 )
			// return the entire response as a string
			result = xhr.responseText;
	}
	else {
		alert("Sorry, but I couldn't create an XMLHttpRequest: url = " + url);
	}
	
	return result;
}

// returns the database
function LoadWorksWithDB(
	inline
){
	if( inline )
		return inlineDB;
	else {
		// load our database from the JSON file
		var jsonData = ajaxRequest(WW.wwDir + "workswith.json");
		return eval("("+jsonData+")");
	}
}

// a small database that is easier to work with in development
var inlineDB = {
	gentime: "(inline)",
	"categories": {
		CAS: {
			ShortName: "CAS",
			FormalName: "Computer algebra systems"
		},
		LMS: {
			ShortName: "LMS",
			FormalName: "Learning management systems"
		},
		Science_blog_forum_wiki: {
			ShortName: "SciBlogForumWiki",
			FormalName: "Science blog, forum, or wiki"
		}
	},
	"targets": {
		"mathematica": {
			ShortName: "mathematica",
			FormalName: "Mathematica",
			CommonName: "Mathematica",
			Homepage: "http://www.wolfram.com/products/mathematica/index.html",
			Hidden: false,
			PreliminaryPresales: false,
			PreliminaryPostsales: false,
			Popular: true,
			CatShortName: "CAS",
			AssociatedTargets: "",
			OSPlatform: "#Both",
			VersionsCovered: "7.0"
		},
		"maple": {
			ShortName: "maple",
			FormalName: "Maple",
			CommonName: "Maple",
			Homepage: "http://www.maplesoft.com/products/Maple/index.aspx",
			Hidden: true,
			PreliminaryPresales: false,
			PreliminaryPostsales: false,
			Popular: true,
			CatShortName: "CAS",
			AssociatedTargets: "",
			OSPlatform: "#Any",
			VersionsCovered: "12.0"
		},
		"moodle": {
			ShortName: "moodle",
			FormalName: "Moodle",
			CommonName: "Moodle",
			Homepage: "http://www.moodle.org/",
			Hidden: false,
			PreliminaryPresales: false,
			PreliminaryPostsales: false,
			Popular: true,
			CatShortName: "LMS",
			AssociatedTargets: "maple, mathematica",
			OSPlatform: "#Any",
			VersionsCovered: "1.8.3"
		},
		"n_category_cafe": {
			ShortName: "n_category_cafe",
			FormalName: "The n-Category Caf\u00E9",
			CommonName: "n-Category Caf\u00E9",
			Homepage: "http://golem.ph.utexas.edu/category/",
			Hidden: false,
			PreliminaryPresales: true,
			PreliminaryPostsales: true,
			Popular: false,
			CatShortName: "Science_blog_forum_wiki",
			AssociatedTargets: "",
			OSPlatform: "#Any",
			VersionsCovered: ""
		},
		"ecollege":{
			ShortName:"ecollege", 
			FormalName:"eCollege", 
			CommonName:"eCollege", 
			Homepage:"http://www.ecollege.com/index.learn", 
			Hidden:false, 
			PreliminaryPresales:true, 
			PreliminaryPostsales:true, 
			Popular:false, 
			CatShortName:"LMS", 
			AssociatedTargets:"", 
			OSPlatform:"#Any", 
			VersionsCovered:""	
		} 
	}
};


