/*
	Testing mechanisms.  This file helps the user to create online tests that are self-scoring.
	A test is created by combining a set of individual questions.  There are 3 types of questions
	  ShortAnswerQA - the user enters simple text, such as a number, or a word or exact phrase
	  Multiple Choice - the user chooses from a set number of possible answers
	  Matching - the user matches items in 1 column with items in the other

	Each question requires specific types of information in order for it to be created.  The test object
	must first be created.  Then the individual questions are created and attached to the test.  Finally,
	the test offers methods to clear the test and start over, or to score the test and provide
	immediate feedback.
*/

// this browsercheck is only used to turning off disabling for NS4, since it 
// looks awful.  It is not used for feature checking
function BrowserCheck() {
	var b = navigator.appName;
	if (b.indexOf('Netscape')!=-1) this.b="ns";
	else if (b=="Microsoft Internet Explorer") this.b = "ie";

	this.version = navigator.appVersion;
	this.v = parseInt(this.version);
	this.ns = (this.b=="ns" && this.v>=4);
	this.ns4 = (this.b=="ns" && this.v==4);
	this.ns6=(this.b=="ns" && this.v==5);
	this.ns7 = (this.b=="ns" && this.v==7);
	this.ie = (this.b=="ie" && this.v>=4);
	this.ie4 = (this.version.indexOf('MSIE 4')>0);
	this.ie5 = (this.version.indexOf('MSIE 5')>0);
	this.ie55=(this.version.indexOf('MSIE 5.5')>0);
	this.ie6=(this.version.indexOf('MSIE 6.0')>0);
	this.min = (this.ns||this.ie);
	this.mac = (this.version.indexOf("Mac") != -1);
}
is = new BrowserCheck()


/********* Testing functions ************/

// Creates a test object
function WebTest(){
	this.__init();
}

// private method called by all constructors to init the test parameters
WebTest.prototype.__init = function() {
	// scoring parameters
	this.right=0;					// number of question answered correctly
	this.wrong=0;					// number of question answered incorrectly
	this.whichwrong = new Array(); // question numbers denoting which are wrong
	this.pts = 0;					// number of points student scored
	this.totalPts = 0;				// number of total possible points
	this.questions;					// list of all QA objects in the test
	this.q_items;					// list of all scoreable items in the test (# depends on question type)
	this.gradePercents = new Array(90,80,70);
}

// sets the array of questions for the test
WebTest.prototype.setQuestions = function(questions) {
	// in order to handle matching groups, we'll need to ask each question 
	// to add it's own parts
	this.questions = questions;

	this.q_items = new Array()
	for (var i=0; i<questions.length; i++) {
		questions[i].__addItems(this.q_items);
	}
}

// set the grading percentages for the test.  
//The first percent is an 'A', next is a 'B' and so on
WebTest.prototype.setPercents = function(percents) {
	// verify grades make sense
	for (var i=1; i<percents.length; i++) {
		if (percents[i] <= percents[i-1]) {
			alert("Grade percentages must always decrease.  For example:\n(90,80,70)");
		}
	}
	this.gradePercents = percents;
}
	

// resets all answers in the form to their initial state
WebTest.prototype.resetForm = function() {
	// in general, nothing is needed to reset a form
	this.__reset();
	for (var i=0; i<this.q_items.length; i++) {
		this.q_items[i].__reset();
	}
}

// private method, resets the scoring variables ot their initial state
WebTest.prototype.__reset = function() {
	this.percent=0;
	this.wrong=0;
	this.right=0;
	this.whichwrong = new Array(); // NOTE: splice doesn't seem to work in IE 6.0
	this.pts=0;
	this.totalPts=0;
}

// score the test
WebTest.prototype.score = function(){
	for (var i = 0; i < this.q_items.length; i++){ // go through each quetxion and check it
		var pts = this.q_items[i].check();

		if (pts > 0) { // if it got points, it's right
			this.right++;
		} else if (this.q_items[i].isAnswered()) { // if it wasn't answered, isn't right, but isn't wrong
			this.wrong++;
			this.whichwrong.push(this.q_items[i].questionNum);
		}

		this.pts += pts;
		this.totalPts += this.q_items[i].getMaxPoints();
	}
}



// private method that returns the grade this student would have gotten
WebTest.prototype.__getGrade = function(){
	var percent = Math.round(((this.pts*100)/this.totalPts))
	if (this.gradePercents.length > 0 && percent >= this.gradePercents[0]) 
		return "Great Score  --  A!"
	else if (this.gradePercents.length > 1 && percent >= this.gradePercents[1]) 
		return "Good Score  --  B."
	else if (this.gradePercents.length > 2 && percent >= this.gradePercents[2]) 
		return "Average Score  --  C."
	else if (this.gradePercents.length > 3 && percent >= this.gradePercents[3]) 
		return "Low Score  --  D."
	else return "Keep Trying!";
}

// private method stating which answers were wrong
WebTest.prototype.__showWrong = function(){
	if (this.wrong > 0) { msg.document.writeln('You scored incorrectly on the following questions:<br>'); }
	for (var i = 0; i < this.whichwrong.length; i++) {
		msg.document.write(this.whichwrong[i]);
		if (i<this.whichwrong.length-1) {
			msg.document.write(', ');
		}
	}
}

// public method opening a results window and scoring the test
WebTest.prototype.showResults = function() {
	var bgColor="black", textColor="white", titleColor="#ff99ff";
	var title="Your Test Results";

	this.score();  // first, score the test

	msg=open("","TestResults","toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=yes,resizable=yes,copyhistory=no,width=400,height=400");
	msg.document.clear();

	msg.document.writeln('<!DOCTYPE HTML PUBLIC "-\/\/W3C\/\/DTD HTML 3.2 Final\/\/EN"> ');
	msg.document.writeln('<html>');
	msg.document.writeln('<head>');
	msg.document.writeln('<title>'+title+'<\/title>');

	msg.document.writeln('<style type="text/css">');
	msg.document.writeln('	body {background-color:'+bgColor+'; color: '+textColor+'; font-weight: 500; padding:20px 30px 0px 30px}');
	msg.document.writeln('	h3 {text-align:center; font-size: 120% }');
	msg.document.writeln('	hr {text-align:center;width:75%}');
	msg.document.writeln('	.altColor {color:'+titleColor+'}');
	msg.document.writeln('</style>');
	msg.document.writeln('<\/head>');

	msg.document.writeln('<body onBlur="self.setTimeout(\'window.close()\',\'5000\')">');
	msg.document.writeln('<h3 class="altColor">'+title+'<\/h3>');
	msg.document.writeln('<hr\/>');
	msg.document.writeln('<blockquote>');
    
	var grade = this.__getGrade();
	
	msg.document.writeln('<p><div class="altColor">'+grade+'<\/div></p>');

	msg.document.writeln('You scored ' + Math.round(((this.pts*100)/this.totalPts)) + '% on this test.');  
	msg.document.write('You answered a total of ' + this.right + ' questions correctly out of ' + this.q_items.length+', ' );
	msg.document.write('giving you a total of ' + this.pts + ' points out of a possible ' + this.totalPts + '.<p>');
	this.__showWrong();
	msg.document.writeln('<\/blockquote>');
	this.__reset();
	msg.document.writeln('<hr\/>');
	msg.document.writeln('<p><form>');
	msg.document.writeln('<center><input type="submit" value="Done" onclick="window.close();"\/><\/center>');
	msg.document.writeln('<\/form><\/p>');
	msg.document.writeln('<\/body>');
	msg.document.writeln('<\/html>');
	msg.document.close();
}

// print out the entire working test
WebTest.prototype.write = function(testName /*string*/, asTable /*boolean*/) {

	document.write('<style type="text/css">');
	document.write('	.wbtst_table {margin:0 30px 20px 30px}');
	document.write('	.wbtst_table td {vertical-align:top; border-spacing:10px; padding:4px}');
	document.write('	.wbtst_num_cell {width:40px; font-weight:bold}');
	document.write('	.wbtst_q_cell {}');
	document.write('	.wbtst_a_cell {}');
	document.write('	.wbtst_mc_list {list-style-type: lower-alpha}');
	document.write('	.wbtst_match_table {border-spacing:5px;}');
	document.write('	.wbtst_match_table td{vertical-align:middle}');
	document.write('	.wbtst_match_space {width:40px}');
	document.write('	.wbtst_match_a_table {border-spacing:0px; border-collapse:collapse;}');
	document.write('	</style>');

	document.write('<form onsubmit="return false;">');
	if (asTable) {
		document.write('		<table class="wbtst_table">');
	}
	for (var i=0; i<this.questions.length; i++) {
		this.questions[i].write(asTable);
	}
	if (asTable) {
		document.write ('		</table>');
	}
	document.write('<center>');
	document.write('	<table class="wbtst_cells"><tr><td>');
	document.write('		<input onclick="'+testName+'.showResults();" type="submit" value="Evaluate">');
	document.write('	</td><td>');
	document.write('		<input type="reset" value="Restart" onclick="'+testName+'.resetForm(); return true;\">'); 
	document.write('	</td></tr></table>');
	document.write('</center>');

	document.write('</form>');
}



/************************************************/
/********** Question/Answer functions ***********/
/************************************************/
// generic functionality common to all question/answer types

QA.answerID = 1; // create unique answer control names
QA.answer_string = "wbtst_answer_";

// Create a generic question/answer object
function QA(){
	this.__init();
}

// private method used to initialize all variables
QA.prototype.__init = function() {
	this.questionNum;		// text - the number of the question (e.g. 1 or 2a)
	this.questionText;		// text - the question (e.g. "Please solve the following equation")
	this.correctAnswer;		// text stating the correct answer to the question
	this.numPoints;			// number specifiying the number of points the question is worth

	// document object in string form (used to find & update answer)
	this.answerObjStr = QA.answer_string + QA.answerID++;
	this.valid=false;			// is the question able to be used?
}

//  single question general information
QA.prototype.setData = function(num /*string*/, qText /*string*/, answer /*string*/, weight /*int*/) {
	this.questionNum = num;
	this.questionText = qText;
	this.correctAnswer = answer;
	this.weight = weight;
	this.answered = false;
}

QA.prototype.getMaxPoints = function() { return this.weight; } // returns max points a question could have
QA.prototype.isAnswered = function() { return this.answered; }	// returns whether the question was answered

/********************   short answer question   **********************/

// A short answer is a simple text input widget.  It allows users to enter in
// anything that can be typed on their keyboard, such as numbers or variables.

ShortAnswerQA.prototype = new QA(); // call the generic QA constructor
ShortAnswerQA.prototype.constructor = ShortAnswerQA; // call the ShortAnswerQA constructor
ShortAnswerQA.superclass = QA.prototype;

// create a short answer.
function ShortAnswerQA() {
	this.__init();
	this.size = 5;
}

// private method adding the item to the test's item list
ShortAnswerQA.prototype.__addItems= function(itemArray) {
	itemArray.push(this);
}

// private method.  Clears the form field (done automatically by the browser)
ShortAnswerQA.prototype.__reset = function() {} 


// checks the answer.
ShortAnswerQA.prototype.check = function() {
	var curObj = document.getElementById(this.answerObjStr);
	if (!curObj) { showError(this, "no question with name" + this.answerObjStr); return; }

	if (curObj.value != "") {
		this.answered = true;
	}

	if(curObj.value == this.correctAnswer){
		return this.weight;
	} else  {
		return 0;
	}
}

// set the width of the input text box
ShortAnswerQA.prototype.setWidth = function(size /*int*/) { this.size = size; }

// writes out all pieces needed to create a short answer
ShortAnswerQA.prototype.write = function(isWrapped /*boolean*/) {
	var text;
	if (isWrapped) {
		text = "<tr><td class=\"wbtst_num_cell\">"+this.questionNum+"</td>\n";
		text  += "<td class=\"wbtst_q_cell\">"+this.questionText + "</td>\n";
		text += "<td class=\"wbtst_a_cell\">\n";
		text += "  <input type=\"text\" id=\""+this.answerObjStr+"\" size=\""+this.size+"\"/>\n"
		text += "</td></tr>\n";
	} else {
		text = "<p><b>"+this.questionNum+") </b>" + this.questionText+"\n";
		text += "<br/>\n";
		text += "<input type=\"text\" id=\""+this.answerObjStr+"\" size=\""+this.size+"\"/></p>\n"
	}
	document.write(text);
}



/********************   multiple choice question   **********************/
// A MultipleChoiceQAQA uses radio buttons to distiguish between a set number of choices
// that a user can choose from.  Only one answer can be chosen at a time from this
// list of alternatives.  So, only one answer should be the correct choice.

MultipleChoiceQA.prototype = new QA();
MultipleChoiceQA.prototype.constructor = MultipleChoiceQA;
MultipleChoiceQA.superclass = QA.prototype;

// Creates a multiple choice answer type
function MultipleChoiceQA() {
	this.__init();
}

// set the choices that the user can choose from for the MultipleChoiceQA question
MultipleChoiceQA.prototype.setChoices = function(choices) {
	this.choices = choices;
}

// private method adding the item to the test's item list
MultipleChoiceQA.prototype.__addItems= function(itemArray) {
	itemArray.push(this);
}

// private method.  Removes the choice made by the user (automatically done by browser)
MultipleChoiceQA.prototype.__reset = function() {}

// checks the answer to see if user chose the right one.
MultipleChoiceQA.prototype.check = function() {
	// NOTE: radio buttons must be accessed by name, not ID
	var curObj = document.getElementsByName(this.answerObjStr);
	if (!curObj) { showError(this, "no question with name" + this.answerObjStr); return; }

	for (var j=0; j<curObj.length; j++) {
		if (curObj[j].checked) {
			this.answered=true;
			if ((j+1) == this.correctAnswer){
				return this.weight;
			}
		}
	}
	return 0;
}


// writes out all pieces needed to create a multiple choice answer
MultipleChoiceQA.prototype.write = function(isWrapped /*boolean*/) {
	var text;
	if (isWrapped) {
		text = "<tr><td class=\"wbtst_num_cell\">"+this.questionNum+"</td>\n";
		text  += "<td class=\"wbtst_q_cell\">"+this.questionText + "</td>\n";
		text += "<td class=\"wbtst_a_cell\">\n";
		text += "  <ol class=\"wbtst_mc_list\">\n";
		for (var i=0; i<this.choices.length; i++) {
			text += "    <li><input type=\"radio\" name=\""+this.answerObjStr+"\">"+this.choices[i]+"</li>\n";
		}
		text += "  </ol>\n";
		text += "</td></tr>\n";
	} else {
		text = "<p><b>"+this.questionNum+") </b>" + this.questionText+"\n";
		text += "<br/>\n";
		text += "  <ol class=\"wbtst_mc_list\">\n";
		for (var i=0; i<this.choices.length; i++) {
			text += "    <li><input type=\"radio\" name=\""+this.answerObjStr+"\">"+this.choices[i]+"</li>\n";
		}
		text += "  </ol></p>\n";
	}
	document.write(text);
}


/********************   matching answer question   **********************/
// A MatchingItem is a different sort of animal from the other question types in that it
// makes no sense out of the context of a matching group.  A matching group consists of 
// a list of 'questions' and a list of 'answers' and allows the user to match items in the first
// list with items in the second.

// Every item in the first list will have an answer, but there can be more answers than questions
// without causing any problems.  When an answer is matched up to a question, then
// the answer is marked as 'used', hopefully preventing the person taking the test from trying to
// match more than one question with a single answer.

// Because of how it's structured, a MatchingAnsQA is part of a MatchingQA.  And, the number
// of points the user can get from a matching group is based upon the number of question/answer
// matches.
MatchingItem.prototype = new QA();
MatchingItem.prototype.constructor = MatchingItem;
MatchingItem.superclass = QA.prototype;

// Creates a single matching question, which is part of the matching group
function MatchingItem() {
	this.__init();
	this.group=0; // called by the group to create 2-way communication
}

// private method called by MatchingQA.setChoices() on each MatchingAnsQA object
MatchingItem.prototype.__setGroup = function(group) {
	this.group = group;
}

// private method.  Removes the user choices (which is automatically done by browser)
MatchingItem.prototype.__reset = function() {}

// checks the answer for a specific matching question
MatchingItem.prototype.check = function() {
	var curObj = document.getElementById(this.answerObjStr);
	if (!curObj) { showError(this, "no question with name" + this.answerObjStr); return; }

	 if (curObj.selectedIndex != 0) {
		this.answered=true;
	 }

	if (curObj.selectedIndex == this.correctAnswer) {
		return this.weight;
	} else {
		return 0;
	}
}

// NOTE: there is no 'write' method for a single matching answer...


// A matching group contains all of the question and answers that 
// can be matched to each other.  NOTE: this does not inherit from
// the QA class.
MatchingQA.id=1;
MatchingQA.choice_string = "wbtst_choice_";

function MatchingQA(name) {		// need the name to call the __markChosen method
	this.questionNum;			// number associated with the group as a whole
	this.questionText;			// text introducing the matching group as a whole
	this.items;					// collection of the matching items  that are associated with this group
	this.choiceObjStrs;			// set of strings stating which choices are part of this group
	this.choiceTexts;			// overall texts outlining the individual choices
	this.__init(name);
}


// private method used to associate a name
MatchingQA.prototype.__init = function(name) {
	this.objName = name;
}

// set the data for the matching GROUP.  This is the number and text introducing the group as a whole
MatchingQA.prototype.setData = function(num, text) {
	this.questionNum = num;
	this.questionText = text;
}

// set an array containing the questions of the matching group
MatchingQA.prototype.setItems = function(items) {
	this.choiceObjStrs = new Array();
	this.items = items;
	for (var i=0; i<items.length; i++) {
		this.items[i].__setGroup(this);
		// for every matching item, there is 1 choice 
		this.choiceObjStrs.push(MatchingQA.choice_string + MatchingQA.id++);
	}
}

// set an array containing the correct answers to all of the choices
MatchingQA.prototype.setValues = function(choices) {
	this.choiceTexts = choices;
}

// private method adding the items to the test's item list
MatchingQA.prototype.__addItems = function(itemArray) {
	for (var j=0; j<this.items.length; j++) {
		itemArray.push(this.items[j]);
	}
}

// private method used to change the checkbox when a drop down box is selected
MatchingQA.prototype.__markChosen = function() {
	// turn off all checkboxes
	for(n=0; n < this.choiceObjStrs.length; n++)
		document.getElementById(this.choiceObjStrs[n]).checked = false;
	// turn on appropriate checkboxes
	for(n=0; n < this.items.length; n++) {
		choice = document.getElementById(this.items[n].answerObjStr).selectedIndex;
		if (choice != 0) {
			document.getElementById(this.choiceObjStrs[choice-1]).checked = true;
		}
	}
}


// Write out the group of matching questions and answers into their 2 columns.
MatchingQA.prototype.write = function(isWrapped /*boolean*/) {
	// first, construct the select options
	var text = "";
	if (isWrapped) {
		text = "<tr><td class=\"wbtst_num_cell\">"+this.questionNum+"</td>\n";
		text  += "<td class=\"wbtst_q_cell\">"+this.questionText + "</td>\n";
		text += "<td class=\"wbtst_a_cell\">\n";
		text += this.__writeOptionTable();

		text += "</td></tr>\n";
	} else {

		text = "<p><b>"+this.questionNum+") </b>" + this.questionText+"\n";
		text += this.__writeOptionTable();
		text += "</p>\n";
	}

	document.write(text);
}

// contains the logic to actually write out the interactive answer portion
MatchingQA.prototype.__writeOptionTable = function() {
	var optionString = this.__createOptions();
	var text = "  <table class=\"wbtst_match_table\"><tr><td>\n";

	for (var i=0; i<this.items.length; i++) {
		text += "    <p><nobr>\n";
		text += "    <select id=\""+this.items[i].answerObjStr+"\" onchange=\""+this.objName+".__markChosen()\">\n";
		text += optionString;
		text += "    </select>\n";
		text += this.items[i].questionText;
		text += "    </nobr></p>\n";
	}
	text += "</td> <td class=\"wbtst_match_space\"></td><td>\n";

	// now show the options
	text += "<table class=\"wbtst_match_a_table\">\n";

	for (var i=0; i<this.choiceObjStrs.length; i++) {
		// netscape has a horrible way of 'disabling' checkboxes.  So, only do it for IE.
		if (is != null && !is.ns4) {
			text += "    <tr><td>"+(i+1)+". <input type=\"checkbox\" id=\""+this.choiceObjStrs[i]+"\" disabled=\"disabled\"/>"+this.choiceTexts[i]+"</td></tr>\n";
		} else {
			text += "    <tr><td>"+(i+1)+". <input type=\"checkbox\" id=\""+this.choiceObjStrs[i]+"\"/>"+this.choiceTexts[i]+"</td></tr>\n";
		}
	}
	text += "  </table>\n";

	text += "</td></tr></table>\n";
	return text;
}


// internal method, creates the options for inclusion in the choices div
MatchingQA.prototype.__createOptions = function() {
	var text = "      <option value=\"\"></option>\n";
	for (var i=0; i<this.choiceObjStrs.length; i++) {
		text += "      <option value=\""+this.choiceObjStrs[i]+"\">"+(i+1)+"</option>\n";
	}
	return text;
}
