/**
*   persist.js implements persistence
*	it also manages saving,restoring and marking of all question types
*   compatible: ns6+, ie4+
*	requires: requires functions.js for ie4 compatibility
*	@author: jon halle
*	@author Ivars Veksins
*	$Id: persist.js,v 2.50 2005/02/01 12:42:37 jonh Exp $
*/
ctad_require("settings.js",'persist.js');
ctad_require("functions.js",'persist.js');
ctad_require("apiwrapper.js",'persist.js');
ctad_included("persist.js");

/**
setup ctad tree environment within context of current module
called on every page which uses ctad tree/persist except pages
which need to see more than one module ('learning object') at a time
(not implemented yet)

usage:
called in a script tag in the head of the document
<pre>  ctad_init('my_module');</pre>

@argument module_name name of module ('learning object') which variables should be created in 
@argument activity_type can be t (test) p (practice) or x (info)
@argument is_summary can be 'a' if answers page otherwise blank

*/
function ctad_init(module_name,activity_type,is_summary){
	//first check they have supplied module_name
 	if (MT(module_name)){
		ctad_error ("ctad_init needs to supply a module name");
		return;
	}

	//create tree variable
	document.ctad= new Root();
	
	//set up persistence structure
	document.ctad.persist_init();

	// special feature save activity cookie in suspend data 
	if (CTAD_SETTINGS["SCO_at_activity_level"] && CTAD_SETTINGS["persist_activity_in_vle"] && getAPIHandle()) {
		document.persist.restore_suspended(); 	
	} 
	
	
	//if needed create a module variable
	if (typeof document.ctad.Modules=="undefined"){
		document.ctad.add_collection("Modules");
	}
	
	if (typeof document.ctad.Modules[module_name]=="undefined"){
		var obj = new Module();
		document.ctad.Modules.add_member(obj,module_name);
	}
	
	//set a global variable in ctad namespace , not persisted
	document.ctad["current_module"]=document.ctad.Modules[module_name];
	
	//set persisted record of what module we are on
	document.ctad.set_property("latest_module",module_name);
	
	//set the current bookmark
	if(CTAD_SETTINGS["persist_bookmark_vle"])document.ctad.set_bookmark(window.location.href,activity_type,is_summary);
	
	//initialise counter
	document.ctad["question_attempts"]=0; 
	
	//call ctadinitload before ctadinit if they want to run on an activity level
	if(CTAD_SETTINGS['SCO_at_activity_level'])ctadinitload(module_name,1);
}

/**
initialise module
tell module how many activities it contains
initialise VLE if necesssary


usage:
called on first page of module 
must be called after ctad_init
can be called in script tag in head of document
<pre>  ctadinitload('my_module',15);</pre>

@argument module_name name of module ('learning object') which is being started 
@argument module_total_activitys how many activitys in the module
*/
function ctadinitload(module_name,module_total_activitys){ 

	if (!document.ctad.Modules){
		ctad_error ("ctadinitload could not find the values installed by ctad_init");
		return; 
	}
	
	if (!module_name || !module_total_activitys){
		ctad_error ("ctadinitload should be called with a module name and total Activitys in module");
		return; 
	}
	
	var mod = document.ctad.Modules[module_name];
	mod.start(module_total_activitys);
	

	if (!getAPIHandle()){
		//either we have no LMS or it screwed up real bad
		document.ctad.set_property("vle","false");
		return;
	}
		
	//ok we are in a VLE environment.
	document.ctad.set_property("vle","true");
	
	//if coming back to this page in same session then don't do any of this again
	if (LMSIsInitialized()){
		return;
	}
	
	//try to initialize the API 
	var result = LMSInitialize();
	
	if (result == "false"){
		ctad_error("Problem initializing LMS");	
		return;
	}
	
	//set values for new VLE session
	
	var new_date=new Date();
	document.ctad.set_property("vle_initialized",new_date.getTime()); //ie the millisecond version of timestamp
	
	//Get the supported children 
	var ctad_children = LMSGetValue("cmi.core._children");
	
	if (!ctad_children){
		ctad_error("Problem finding LMS supported values");	
		return;
	}
	
	var ar_children=ctad_children.split(/\s*,\s*/);
	
			
	//now set the status for the lesson
	if (ar_children.contains("lesson_status") && LMSGetValue("cmi.core.lesson_status") == "not attempted"){
		// the student is now attempting the lesson
		// so set this in VLE
		document.ctad.save_to_vle( "cmi.core.lesson_status", "incomplete" );
	}
	
	//now set the exit to suspend as a good default.
	if (ar_children.contains("exit")){
		document.ctad.save_to_vle( "cmi.core.exit", new String("") );
	}

	//now check the bookmark
	//if its not already there let them choose to start where left off
	if (CTAD_SETTINGS["persist_bookmark_vle"] && ar_children.contains("lesson_location")){
		var location= LMSGetValue("cmi.core.lesson_location");
		
		//first check for bogus location put in by ADL player.
		//we can spot this because its a relative path not starting with http:
		//its removed if found		
		if (location.slice(0,5)!="http:"){
			document.ctad.save_to_vle("cmi.core.lesson_location", "");	
			location="";
		}
		if (location && location!=window.location.href){
			if (confirm("Would you like to continue from where you left off? Select OK or Cancel")){
					window.location.href = location;
			}
		}
	}
	//get attempts data back from vle
	mod.get_vle_attempts();

}

/**
wrapper for backwards compatibility

@argument this_form name of form to persist questions in
*/
function loadAllQuestions(this_form){
	persist_questions_init(this_form);
}

/**
set up elements in a form to be persistent
elements must have name beginning 'q' with number and optional letter
i.e. q05 or q11a

usage:
<pre>
<BODY onload="persist_questions_init(document.forms['form_name']);" > 
</pre>

@argument this_form name of form to persist questions in
*/
function persist_questions_init(this_form){

	// clear all form elements in case of a refresh
	document.forms[0].reset();
	
 	if (typeof (document.ctad)=="undefined"){
		ctad_error ("document.ctad not set. ctad_init may not have been called");
		return;
	}
	
	//set up current_activity
	var current_activity=document.ctad.current_module.get_child("Activitys",this_form.name);
	
	var obj_questions= new Object();
	
	//set up elements
    	for(var i = 0; i < this_form.length; i++) {
	    	var el =this_form.elements[i];
	    	
	    	var ar_match=el.name.match(/^q(\d*)/i);
	    	
	    	//don't set up element if it aint called qxx
	    	if (!ar_match){
	    		continue;
	    	}
	    	
	    	//attach question class to element to make saving easier
	    	el.question=current_activity.get_child("Questions",ar_match[1]);
	    	
	       	//set flag in object keyed by question names
	    	obj_questions[ar_match[1]]=true;
    	
    		//add event handlers getting rid of anything which is there
	    	if (el.type=="checkbox" || el.type=="radio"){
	    		event_install(el,"onclick",persist_question_save);
				//el.onclick=persist_question_save;
	    		el.onchange='';
	    	}else{
	    		// Jon, event_install for doesn't work in IE :(
				//event_install(el,"onchange",persist_question_save);
				
				el.onchange=persist_question_save;
	    		el.onclick="";
				//call function to update printable  when typing 
				if(el.tagName=="TEXTAREA"){
					event_install(el,'onkeyup',update_printable);
				}
	    	}
    	
   	}
	
 	
	
   	//now loop through all questions
   	for (var qname in obj_questions){
   	
   		//get question type
   		current_activity.Questions[qname].get_question_type();
   		//try to get saved info back into question
		current_activity.Questions[qname].load_user_data(); 
   	
   	}
   	
   	//set last_question if possible to help invocation in cases where only one question on form
	if (current_activity.Questions){
		document.ctad.last_question	=current_activity.Questions[qname];
	}
   		
}

	/* function for image MS and MC */
function image_label(theq){
	document.getElementById(theq).checked=true;
	document.getElementById(theq).click();
}  

/* function for text areas - update printable element in page */
function update_printable(){
	var pre=document.getElementById('pre_'+this.id);
	if(pre && typeof(pre)=='object'){
		set_printable_content(pre,this.value);
	}
	else {
		create_printable(this);
	}	
}



/* function to fill printable element on page for text areas
remember to hide the div in css, and show it in a print stylesheet while hiding the textarea 
Uses dom methods because innerHTML has issues*/
function set_printable_content(pre,value){
	value=value.split('\n');
	pre.innerHTML=''; //easiest way to clear content, dom methods are tricky!
	if(value)for(i=0;i<value.length;i++){
		// maintain textarea formating - line breaks, multiple spaces
		pre.appendChild(document.createTextNode(value[i].replace(/  /g," \u00a0")));
		pre.appendChild(document.createElement('br'));
	}

}

/* function to create printable element on page for text areas
remember to hide the div (classname textarea) in css, and show it in printing while hiding the textarea
Uses dom methods because innerHTML has issues*/
function create_printable(elem){
	var pre=document.createElement('div');
	pre.appendChild(document.createTextNode(elem.value));
	pre.id='pre_'+elem.id;
	pre.style.width=elem.clientWidth;
	pre.className='textarea';
	
	elem.parentNode.insertBefore(pre,elem);
	set_printable_content(pre,elem.value); //cannot set content until in page
}

/**
save question into persistence mechanism
called on event handler for every form elements onchange or onclick
installed by persist_questions_init
*/
function persist_question_save() {

	if (typeof (this.question)=="undefined"){
		ctad_error ("element "+this.name+" has no question object attached. persist_questions_init may not have been called correctly");
		return;
	}
	
	this.question.save_user_data();

	// check if suspend data can be saved 
	var activity = this.form.name;
}


/**
defines a document.persist object which stores persistent data either in a frameset or
in a cookie. also extendable to store in other ways on future

document.persist should not be used to store unstructured data as the ctad Root object
uses it to store data in its own formats.

see the ctad Root object documentation for the standard way of storing an arbitrary variable

@argument store_mechanism can be "cookie" or "parent"
*/
function Persist(store_mechanism){
	
	//set delimiter for magic strings
	this.$delimiter="|";
	
	this.$store_mechanism=store_mechanism;

	if (this.$store_mechanism=="cookie"){
		this.load_from_cookie();
	}
}

/** 
  * Gets suspended cookie data from suspend data and stores in document.persist instance
  * @param activity name name of the activity 
  */ 
Persist.prototype.suspend = function(activity_name) 
{ 

	// VLE has to be initialised 
	if ((!document.ctad.vle && !parent.document.ctad.vle) || !getAPIHandle()) 
		return false; 
		
	if (!LMSIsInitialized())
		return false;
	
	
	
	// get current suspend data and check if data the same 
	var store = LMSGetValue('cmi.suspend_data');

	var suspend  = Array(); 
	// takes all persisted values for an activity and sticks them 
	// in suspend data 
	
	var obj = document.persist; 
	if (this.$store_mechanism!="cookie" && parent.persist)
		obj = parent.persist; 
	
	for (prop in obj) 
		if (prop.match(activity_name)) {
			suspend.push( prop.replace(/\|/g,"^") + "=" + escape(this[prop]) ); 
		}
	
	// split up current suspend data 
	var store = LMSGetValue('cmi.suspend_data');
	
	if (store == null) 
		return false;
	
	// suspend data not present
	if (!store)
		store = ""; 

	store = store.split("|"); 
	
	// remove cookie 
	for (var i = 0; i < store.length; i++) {
		var ar_pairs = store[i].split(":"); 
		
		// check if found 
		if (ar_pairs[0] && ar_pairs[0].match(/cookie/) ) {
			store.pop(i); 
			break; 
		}
	}

	store.push('cookie:' + suspend.join("&") ); 

	// store cookie to suspend data 
	document.ctad.save_to_vle('cmi.suspend_data', store.join("|")); 
	
	return true; 
}


/** 
 * Restores all suspended data
 * 
 */ 
Persist.prototype.restore_suspended = function() 
{ 
	// VLE has to be initialised 
	if (!getAPIHandle()) 
		return; 

	var suspend = LMSGetValue('cmi.suspend_data');

	// we need suspend data to continue; 
	if (!suspend)
		return; 
		
	suspend = suspend.split("|"); 
	
	for (var i = 0; i < suspend.length; i++) { 
		var ar_pairs = suspend[i].split(":"); 
		
		// restore cookie 
		if (ar_pairs[0] && ar_pairs[0].match(/cookie/)) {
			
			var props = ar_pairs[1].split("&"); 
			
			// now, store in persist. 
			for (var c = 0; c < props.length; c++) { 
				var ar_persist = props[c].split("="); 
				
				if (!document.persist[ ar_persist[0].replace(/\^/g,"|") ] ) 
					document.persist[ ar_persist[0].replace(/\^/g,"|") ] = unescape(ar_persist[1]);
			}
			break; 
		}
	}

	
	document.ctad.tree_restore(document.persist); 
}

/**
Store the persist data in the chosen style
*/
Persist.prototype.store= function (){
	
	if (this.$store_mechanism=="cookie"){
		this.set_cookie();
	}else if (this.$store_mechanism=="parent"){
		parent.persist=this ;
	}else{
		ctad_error("Variables could not be saved.\nNo persistence mechanism available");
	}
};

/**
Load data from cookie using cookie class 
and create both flat variables and ctad tree
*/
Persist.prototype.load_from_cookie=function (){

	var c = new Cookie(document,"persist");
		
	c.load();
	
	for ( var prop in c){
		if ((prop.charAt(0) != '$') &&  ((typeof c[prop]) != 'function')){
			//set flat variable
			this[prop]=c[prop];
			
			/*
			//set variable in tree context 
			this.tree_populate(prop,c[prop],document.ctad);
			*/
			
		}
			
	}
	
};

/**
get the persist variables into the cookie
*/
Persist.prototype.set_cookie=function(){

	//create cookie within this whole domain for the length of the session
	var c = new Cookie(document,"persist",null,"/");	
	
	for ( var prop in this){
		if ((typeof this[prop]) != 'function'){
			c[prop]=this[prop];
		}
	}

	c.store();

	
};

/**
returns html representation of persist variables for debugging purposes
*/
Persist.prototype.dump=function(){

	var html="";
		for ( var prop in this){
		if (prop.charAt(0) != '$' && (typeof this[prop]) != 'function'){
			 html+=("\n<p>"+prop+":"+this[prop]);
		}
	}

	return (html);
};

/**
store a value to the persist structure
@argument name name of variable
@argument value value of variable
*/
Persist.prototype.save=function(name,value){

	// set the value in the in-memory tree
	this.tree_populate(name,value);
	
	//set the value in the persist object in string format
	this[name]=value;
	
	//store it
	this.store();
};

/**
save an existing object
go through the children
and store the properties of the object
note: use with care because it persists absolutely everything;
save_property is a more restrained alternative

@argument obj object reference (object based on Ctad object)
@argument recurse  whether to save recursively true /false
*/
Persist.prototype.save_object=function(obj,recurse){

	
	//first recurse
	if (recurse){
		var children=obj.get_children();
		
		for (var c in children){
			this.save_object(children[c],true);
		}
	}
	
	//now save properties
	
	var properties=obj.get_properties();
	
	for (var p in properties){
		//get the property name
		var property_name=this.property_name(obj,p);
		
		//set the value in the persist object in string format
		this[property_name]=properties[p];
	}

	
	//store it
	this.store();
	
};



/**
save an object property
note: single-property alternative to save_object

@argument obj object reference (object based on Ctad object)
@argument property_name  name of the property to save
*/
Persist.prototype.save_property=function(obj,property_name){

	//get the persist name of the property
	var persist_property_name=this.property_name(obj,property_name);
		
	//set the value in the persist object in string format
	this[persist_property_name]=obj[property_name];
	
	//store it
	this.store();
	
};

/**
takes an object variable and property and creates the name
which it will have as a persist variable

for example document.ctad.Modules['01'].Activitys['self'].Questions['01'].value
should become Modules|01|Activitys|self|Questions|01|value

@argument obj object reference (object based on Ctad object)
@argument property_name  name of property

*/
Persist.prototype.property_name=function(obj,property_name){

	

	var var_name = this.var_name(obj);
	
	var_name+=this.$delimiter+property_name;
	
	return (var_name);
};

/**
takes an object variable and creates the name
which it will have as a persist variable

for example document.Modules['01'].Activitys['self']
should become Modules|01|Activitys|self

@argument obj object reference (object based on Ctad object)
*/
Persist.prototype.var_name=function(obj){

	var var_name="";
	var delim = "";
	
	while (obj.$parent){
			var parent=obj.$parent;
	
			if (parent.get_class_name()=="Root"){
				break;
			}else{
				if (parent.$collection_name){
					var_name=parent.$collection_name+this.$delimiter+obj.$name+delim+var_name;
					delim=this.$delimiter; //once it has been set once
				}
			}
			
			obj=parent;
	}
	
	return (var_name);
};

/**
take the name of a property and break it up into parts
create tree as needed and assign property

@argument name name of property as persist variable
@argument value value of property in persist
@argument var_current Ctad object to start from
*/
Persist.prototype.tree_populate=function(name,value,var_current){
	
	//first get the property name off the end
	
	var ar_tokens=name.split(this.$delimiter);
	var prop = ar_tokens.pop();
	var var_name = ar_tokens.join(this.$delimiter);
	
	if (var_name){
		//create the variable in the tree
		var new_var = this.tree_create(var_name,var_current);
	}else{
		//set variable to the root object
		var new_var=var_current;
	}	
	
	//now assign
	new_var[prop]=value;

};

/**
Create the tree as necessary

for example Modules|01|Activitys|self|Questions|01|value
should become document.ctad.Modules['01'].Activitys['self'].Questions['01'].value

@argument name name of variable as persist variable
@argument var_current Ctad object to start from	
*/
Persist.prototype.tree_create=function(name,var_current){
	
	var var_name="";
	var var_type="";
	var var_collection="";
	var var_current;
	var new_current;
	var property_name="";
	
	if (!var_current){
		var_current=document.ctad;
	}
	
	var ar_tokens=name.split(this.$delimiter);
	
	for (var i=0;i<ar_tokens.length;i+=2){
		var_collection=ar_tokens[i];
		var_name=ar_tokens[i+1];
		
		//make the vars as we go along if necessary
		
		this.collection_create(var_collection,var_current);
				
		new_current= this.node_create(var_collection,var_name,var_current);
			
		if (!new_current){
			ctad_error('__Persist_tree_create:Still not created the var');
		}else{
			var_current=new_current;
		}

	
	}
	
	return(var_current);

};

/**
Create a collection in the tree ie Questions collection

@argument collection name of collection to create
@argument current Ctad object to create collection in
*/
Persist.prototype.collection_create=function(collection,current){

	//check if it already exists
	if (current[collection]){
		return;
	}
	
	
	current[collection]=new Collection(collection,current);
};

/**
Create a node on the tree ie Question node

@argument collection name of collection to create node in
@argument name name for new node
@argument current Ctad object containing collection which node will be created in

*/
Persist.prototype.node_create=function(collection,name,current){
	
	//check if it already exists
	if (current[collection][name]){
		return (current[collection][name]);
	}
	
	var type=collection.substring(0,collection.length-1); //remove the s
	
	
	if (!type) {
		ctad_error("__Persist_node_create:Tried to make object without type");
		return;
	}
	
	var obj= eval("new "+type+"()"); //other eval syntaxes give weird effects
	
	if (!obj) {
		ctad_error("__Persist_node_create:Failed to make object with type " +type);
		return;
	}
	
	current[collection][name]=obj;
	
	obj.$parent=current[collection];
	obj.$name=name;
	
	
 return (obj);
};



/**
Ctad is the base object.
it is subclassed by other objects
it provides convenience functions for dealing with collections etc
*/
function Ctad(){

}

/**
get class name of this object
@return string
*/
Ctad.prototype.get_class_name = function(){
	// mod from grant wagner
	// changed regex so it deals with constructors with arguments

    //var re = /function\s+(.+)\s*\(\)\s*\{/;
    var re =  /function\s(.+)\s*\(/;
    
    var result = re.exec(this.constructor);
    
    if (typeof result[1] =="undefined"){
		return ("unknown");
    }
    
    return (result[1]);
    
};

/**
how many children of this object
@return integer
*/
Ctad.prototype.get_children_count = function (){

	var children = this.get_children();
	var i=0;
	
	for(c in children){
		i++;
	}
	
	return(i);
};

/**
get an object (associative array) containing children of this object
@return object
*/
Ctad.prototype.get_children = function (){

	var ar_children=new Object();

	
	for(c in this){
		if ((typeof this[c]) == 'object' && c.charAt(0) != '$'){
			ar_children[c]=this[c];
		}
	}

	return (ar_children);
};

/**
get an array of properties of this object
@return array
*/
Ctad.prototype.get_properties = function (){

	var ar_properties=new Object();
	
	for(c in this){
		if ((typeof this[c]) != 'object' && (typeof this[c])!='function'&& c.charAt(0) != '$'){
			ar_properties[c]=this[c];
		}
	}

	return (ar_properties);
};

/**
set a persistent property of this object
note: property names cannot use delimiter (default delimiter is |)
*/
Ctad.prototype.set_property = function (property_name,value){

	if (property_name.indexOf(document.persist.$delimiter)!=-1){
		ctad_error("Cannot persist property with name:"+property_name+". Character '"+document.persist.$delimiter+"' is used as a delimiter");
		return;
	}
	
	this[property_name]=value;
	
	//now persist it
	document.persist.save_property(this,property_name);
	

};

/**
add a collection to this object

@argument collection_name name of collection to add
*/
Ctad.prototype.add_collection = function (collection_name){

	this[collection_name]=new Collection(collection_name,this);
	
	//now persist it
	document.persist.save_object(this[collection_name]);
	

};

/**
does a child of this object exist
useful for testing long expressions where javascript chokes easily
in scenarios where we dont have error trapping

usage:
<pre>
var ret=this.child_exists("Activitys",act_name,"Questions",q_name);
</pre>

@argument any number of arguments can be specified as collection,key,collection,key...
@return boolean
*/
Ctad.prototype.child_exists = function(){

	var goodvar=this;

	for (var i=0;i<arguments.length;i++){
			if (typeof goodvar[arguments[i]] =="undefined"){
				return (false);
			}else{
				goodvar=goodvar[arguments[i]];
			}
	}

	return(true);
};

/**
get a child of this object
creates child if necessary
useful for testing long expressions where javascript chokes easily
in scenarios where we dont have error trapping

note: child will not be persisted unless set_property is called on it later

usage:
<pre>
var child=this.get_child("Activitys",act_name,"Questions",q_name);
</pre>

@argument any number of arguments can be specified as collection,key,collection,key...
@return Ctad object
*/
Ctad.prototype.get_child = function(){

	if (!arguments.length){
		return;
	}

	var path="";
	var ar_args=Array();

	for (var i=0;i<arguments.length;i++){
		 ar_args[i]=arguments[i];
	}

	var path=ar_args.join(document.persist.$delimiter);

	return(document.persist.tree_create(path,this));

};

/**
clear an existing object
go through the children
and clear the properties of the children
and the children themselves
and properties of the object
also clears persisted properties


@argument clear_self whether to clear the object itself

*/
Ctad.prototype.clear_object=function(clear_self){

	
	//first recurse
	var children=this.get_children();
	
	for (var c in children){
		if (children[c].clear_object){ // check first, not all children are of ctad type
			children[c].clear_object(true);
		}
	}
	
	//now clear properties
	var properties=this.get_properties();
	
	for (var p in properties){
		//get the property name
		var property_name=document.persist.property_name(this,p);
		
		//set the value in the persist object in string format
		delete(document.persist[property_name]);
	}

	
	//store persisted values
	document.persist.store();
	
	if (clear_self){
		delete (this);
	}
	
};


/**
subclass of Ctad

creates a collection with arbitrary name
note that collections by convention contain only one sort of object
and that they are named by adding 's' to the name of that object
hence the nasty spelling of the Activitys collection

@argument collection_name name of new collection
@argument parent_object ctad object to create collection in
*/
function Collection(collection_name,parent_object){
	inherit_object (this,new Ctad()); 
	this.$collection_name=collection_name;
	this.$parent= parent_object;
}


/**
add a member to a collection

@argument obj ctad object to add
@argument member_name key for object in collection
*/
Collection.prototype.add_member = function (obj,member_name){

	this[member_name] = obj;
	obj.$parent= this;
	obj.$name=member_name;
	document.persist.save_object(obj);

};

/**
subclass of Ctad

the root object is installed as document.ctad
it implements mostly convenience methods which are 
easily called by designers

root may have any number of properties set by calling
document.ctad.set_property(prop_name,prop_val);
this is the standard way to persist arbitrary variables 
to persist form data ('questions') use persist_questions_init
*/
function Root(){
	inherit_object (this,new Ctad()); 
}

/**
setup document.persist variable
uses cookies if available and not overridden
*/
Root.prototype.persist_init=function(){

	if (cookies_use()){
		document.persist=new Persist("cookie");
	}else{
		if (parent.frames.length){
			if (!parent.persist){ //initialise persist object if there is none
				parent.persist=new Persist("parent");
			}
	
			// in IE document.persist = parent.persist doesnt work here
			// because it copies by reference and the parent object goes out of scope
			// so we always make new document.persist and copy the properties
			document.persist=new Persist("parent");
			
			for (var p in parent.persist){
				if (typeof(parent.persist[p])!="function"){
					document.persist[p]=parent.persist[p];
				}
			}
			
		}else{
			//ctad_error("Cookies are not available and there is no frameset\nTherefore values you enter will not be saved");
		}
	}
	
	this.tree_restore(document.persist);

}

/**
restore the entire tree
from the flat values

called for both cookie and parent persistence styles

@argument persist_var variable containing persist data ie document.persist
*/
Root.prototype.tree_restore=function(persist_var){

	for ( var prop in persist_var){
		if ((typeof persist_var[prop]) != 'function'){
			persist_var.tree_populate(prop,persist_var[prop],this);
		}
	}

			
};

/**
helper function for saving to VLE

@argument key name of property to save (must be valid, see apiwrapper)
@argument value value of property
*/
Root.prototype.save_to_vle = function (key,value){
//helper function

		value = String(value); //cast value as string		
		LMSSetValue( key,value );
		var iErrorNumber = LMSGetLastError();
		if(iErrorNumber != "0"){
			ctad_error("LMS error:"+LMSGetErrorString(iErrorNumber));
			return (false);
		}
		else{
			LMSCommit();
			return (true);
		}
};

/**
helper function for shutting down VLE
called by SCO frameset in onbeforeunload 
ie:

ctadcontent.document.ctad.shutdown_vle();


*/
Root.prototype.shutdown_vle = function (){

	if (this.vle_present() && LMSIsInitialized()){
		// set the vle timer, this seems like a slightly better place
		this.set_timer();
		
		if(CTAD_SETTINGS["SCO_at_activity_level"] && CTAD_SETTINGS["persist_activity_in_vle"])this.persist.suspend(this.current_module.latest_activity);
		LMSFinish();	
	}

};
/**
set the timer in the vle
*/
Root.prototype.set_timer = function (){
//set the timer in the vle

	// nothing to do if not in vle
	if (!this.vle_present()) {
		return;
	}
	
	if (!LMSIsInitialized()){
		return;
	}
	//this.$parent.$parent refers to the root ctad object
	if (!this.vle_initialized){
		return;
	}

	var new_date = new Date();

	var elapsed_time = (new_date.getTime()-this.vle_initialized)/1000;

	//get time in funky format needed by vle
	//note that slice for ie4 is repaired by code also in this file

	var hours = ("0000"+Math.floor(elapsed_time/(60*60))).slice(-4);
	var minutes = ("00"+Math.floor((elapsed_time-hours*60*60)/60)).slice(-2);
	var seconds = ("00"+Math.floor(elapsed_time-hours*60*60-minutes*60)).slice(-2);

	document.ctad.save_to_vle( "cmi.core.session_time", hours+":"+minutes+":"+seconds );

};


/**
set property of bookmark 
and last bookmark which is useful for timed forms etc
if there is VLE send the bookmark to it

@argument href url of current page
@argument activity_type can be t (test) p (practice) or x (info)
@argument is_summary can be 'a' if answers page otherwise blank

*/
Root.prototype.set_bookmark=function(href,activity_type,is_summary){


	if (this.bookmark){
		this.set_property("last_bookmark",this.bookmark);
	}

	if ((typeof href) =="undefined"){
		return;
	}
	
	//get # off end of bookmark
	href = href.replace(/\#.*$/i,"");
	
	this.set_property("bookmark",href);
	
	//send bookmark to the VLE if relevant
	if (!this.vle_present()) {
		return;
	}

	if (!LMSIsInitialized()){
		return;
	}
	
	//munge the bookmark a little if its a test
	
	var bmk = this.bookmark;
	
	if (activity_type=="t"){
		if (is_summary=="a"){//dont save location if its answer page 
			bmk=""; 
		}else{//otherwise save location of first question 
			var ar_bmk = bmk.split("/");
			ar_bmk.pop();
			bmk = ar_bmk.join("/")+"/body01.htm";
		}
	}
	
	this.save_to_vle("cmi.core.lesson_location",bmk);


}

/**
what is the status of an activity

@argument activity_name name of activity
@return null if invalid, "none" if unstarted, "started" or "finished"
*/
Root.prototype.get_activity_status = function(activity_name){
	if ((typeof this.current_module) =="undefined"){
		return;
	}


	if (this.current_module.child_exists("Activitys",activity_name)){
		return(this.current_module["Activitys"][activity_name].status);
	}else{
		return ("none");
	}


};

/**
convenience function for showing that an activity without questions is started

@argument info_name name of activity
*/
Root.prototype.info_start = function (info_name){

	if (typeof (this.current_module) =="undefined"){
		return;
	}

	var mod = this.current_module;

	var info=mod.get_child("Activitys",info_name);

	info.start();
};

/**
convenience function for showing that an activity without questions has finished

@argument info_name name of activity
*/
Root.prototype.info_complete = function (info_name){

	if (typeof this.current_module =="undefined"){
		return;
	}

	var mod = this.current_module;

	var info=mod.get_child("Activitys",info_name);

	info.complete();

};

/**
convenience function for showing that an activity with questions is finished
and should be scored

@argument test_name name of activity
*/
Root.prototype.test_complete = function (test_name){

	if (typeof this.current_module =="undefined"){
		return;
	}

	var mod = this.current_module;

	var test=mod.get_child("Activitys",test_name);

	test.complete(true);

};

/**
convenience function for showing that an activity with questions is finished
but without scoring

@argument practice_name name of activity
*/
Root.prototype.practice_complete = function (practice_name){

	if (typeof this.current_module =="undefined"){
		return;
	}

	var mod = this.current_module;

	var practice=mod.get_child("Activitys",practice_name);

	practice.complete();

};

/**
convenience function to get activity object

@argument activity_name name of activity
@returns Activity object
*/
Root.prototype.get_activity = function (activity_name){

	if (typeof this.current_module =="undefined"){
		return;
	}

	var mod = this.current_module;

	var act=mod.get_child("Activitys",activity_name);

	return(act);

};

/**
function to show whether we are in vle 
convenient in that it returns boolen rather than
the persisted string "true" or "false"

@returns boolean showing whether in vle or not
*/
Root.prototype.vle_present = function (){

	return((this.vle=="true"));

};

/**
subclass of Ctad

the Module object represents a learning object 
most of its methods relate to communication with a VLE
*/
function Module(){
	inherit_object (this,new Ctad()); 
}

/**
mark a module as started (called by ctadinitload)
@argument total_activitys number of activitys in the module
*/
Module.prototype.start = function (total_activitys){

	this.set_property("total_activitys",total_activitys);
	this.set_property("status","started");
	
};

/**
check whether a module is complete and if so do housekeeping
called whenever an activity completes
*/
Module.prototype.activity_complete = function (act){

	// check if module is complete
	var total_activitys =0;

	//find completed Activitys
	if (this.Activitys){
		for (var i in this.Activitys.get_children()){
			if (this.Activitys[i].status=="completed"){
				total_activitys++;
			}
		}
	}

	if (this.total_activitys==total_activitys){
		this.set_property("status","completed");
	}
	
	//if it was single page it might not have been formally started
	//so lets assert that we are here
	this.set_property("latest_activity",act.$name);
		
	// no more to do if not in vle
	if (!document.ctad.vle_present()) {
		return;
	}

	// kick back aggregated test scores to the vle
	this.set_vle_attempts();

	//if module is complete then mark it as complete in vle
	if (this.status=="completed"){
		document.ctad.save_to_vle( "cmi.core.lesson_status", "completed" );
	}
	

};

/**
aggregate scores of all first attempts of tests and send back to vle
called whenever an attempt is added

@argument attempt Attempt object
*/
Module.prototype.attempt_added=function(attempt){

	// nothing to do if not in vle
	if (!document.ctad.vle_present()) {
		return;
	}
	
	if (!LMSIsInitialized()){
		return;
	}
	
	var score=0;
	var max_score=0;
	
	//find  Activitys and get aggregate scores
	//we only take the score from the first attempt
	for (var i in this.Activitys.get_children()){
		if (this.Activitys[i].Attempts){
			score +=parseInt(this.Activitys[i].Attempts[0].score);
			max_score +=parseInt(this.Activitys[i].max_score);
		}
	}
	
	//send to VLE
	document.ctad.save_to_vle( "cmi.core.score.raw", score );
	document.ctad.save_to_vle( "cmi.core.score.min", "0"); //it always is zero
	document.ctad.save_to_vle( "cmi.core.score.max", max_score);
	
};

/**
loop through all the tests and get scores
into small string
not using the persist serialisation as 
space is extremely tight (4k)
*/
Module.prototype.set_vle_attempts = function (){


	var ar_tests=new Array();
	var j=0;

	var activitys= this.Activitys.get_children();

	for (var act in activitys){
		
		if (!activitys[act].Attempts){
			continue;
		}
		
		var attempts= activitys[act].Attempts.get_children();
		var ar_attempts=new Array(); //erase the old array
		var i=0;
		
		for (var attempt in attempts){	
			ar_attempts[i++]=activitys[act].Attempts[attempt].score+","+activitys[act].Attempts[attempt].date;
		}
		
		ar_tests[j++]=act+":"+ar_attempts.join(",");	
	 }
	 
	 if (!ar_tests.length){
	 	return;
	 }
	 
	 var attempts = ar_tests.join("|");

	 //save to the vle
	 document.ctad.save_to_vle( "cmi.suspend_data", attempts );
	 
};

/**
gets attempts from the vle suspend field and re-populates the attempts tree
pulling apart the  sparse format
*/
Module.prototype.get_vle_attempts = function (){
 

	var attempts =LMSGetValue("cmi.suspend_data");
	if (!attempts){
		return;
	}

	var ar_tests = attempts.split("|");

	for (var i =0;i<ar_tests.length;i++){
		var ar_pair=ar_tests[i].split(":");
		if (!ar_pair || ar_pair.length!=2){
			continue;
		}
		var test_name=ar_pair[0];
		var ar_attempts=ar_pair[1].split(",");
		for (var j =0;j<ar_attempts.length;j+=2){
			var attempt=this.get_child("Activitys",test_name,"Attempts",j/2);
			attempt.set_score(ar_attempts[j],ar_attempts[j+1]);
			document.persist.save_object(attempt);
		}
	}


};


/**
stores persistent record of last activity to be started

@argument activity the activity which was started
*/
Module.prototype.activity_started = function (act){
	
	this.set_property("latest_activity",act.$name);
};

/**
subclass of Ctad

the Activity object represents an activity (ie TSObject in target skills)
*/
function Activity(){
	inherit_object (this,new Ctad()); 
}

/**
called on activity completion
if optional test is passed,  marks and records attempt
event is bubbled back to module so it can check to see if the whole module is complete

@argument is_test should the activity be marked and recorded (true/false)
*/
Activity.prototype.complete = function (is_test){

	//if we are already completed then don't do anything
	if (this.status=="completed"){
		return;
	}

	this.set_property("status","completed");

		
	if (is_test){
		this.set_max_score();
		this.mark();
	
		//now lets record the attempt
		this.add_attempt();
	}
	
	//bubble back event to module
	this.$parent.$parent.activity_complete(this);

};

/**
record a test attempt

event is bubbled back to module so it can create aggregate scores
*/
Activity.prototype.add_attempt= function(){


	if (!this.Attempts){
		this.add_collection("Attempts");
	}

	var attempts_number=this.Attempts.get_children_count();

	var new_attempt=new Attempt();
	new_attempt.set_score(this.score);

	this.Attempts.add_member(new_attempt,attempts_number);
	
	//bubble back event to module
	this.$parent.$parent.attempt_added(new_attempt);
};

/**
gets the max score for a test from theanswers array (in qdata.js file)
*/
Activity.prototype.set_max_score=function(){
	if (typeof(theanswers.length)=="undefined"){
		ctad_error("Could not set max score for this test. no theanswers array found");
		return;
	}
	
	//can't just take theanswers.length 
	//it may not be dense array
	var max_score=0;
	
	for (var i=0;i<theanswers.length;i++){
		if (typeof(theanswers[i])!="undefined"){
			max_score++;
		} 
	}
	
	this.set_property("max_score",max_score);
		
};

/**
mark a test by marking each question individually
*/
Activity.prototype.mark=function(){
	
	if (typeof(this.Questions)=="undefined"){
	//if they tried no questions the score is zero
		this.set_property("score",0);
		return;
	}
	
	 var questions=this.Questions.get_children();
	 var score = 0;
	 var incomplete=false;
	 
	 for (var q in questions){
		var mark = questions[q].mark();
		if (mark=="c"){
			score++;
		}else if (mark=="s"){
			incomplete = true;
		}
	 }

	this.set_property("score",score);
		
};


/*
count the number of skipped questions
*/
Activity.prototype.skipped=function(){
	
	if (typeof(this.Questions)=="undefined"){
		return;
	}
	
	 var questions=this.Questions.get_children();
	 var skipped=0;
	 
	 for (var q in questions){
		var mark = questions[q].mark();
		if(mark=="n")skipped++;
	 }

	return skipped;
};

/*
count the number of attempted questions
*/
Activity.prototype.attempted_questions=function(){
	
	if (typeof(this.Questions)=="undefined"){
		return;
	}
	
	var questions=this.Questions.get_children();
	return questions.length-this.skipped();

}

/**
mark this activity as started

note that it is not necessary to explicitly mark an activity as started if
it has questions as this is set when the first question is attempted



*/
Activity.prototype.start = function(){
	
	//don't start twice	
	// dont's start if completed
	if (this.status=="started" || this.status == "completed"){
		return;
	}
	
	this.set_property("status","started");
	
	//bubble back event to module
	this.$parent.$parent.activity_started(this);
};


/**
subclass of Ctad

the Question object represents a question in a test or practice
note that questions do not necessarily map directly to HTML form elements
though each question will contain at least one html form element
even if it is a hidden field

*/
function Question(){
	inherit_object (this,new Ctad()); 
	this.$answer_delimiter="^";
}

/**
get the name of the form that this question uses
@return string form name
*/
Question.prototype.get_form_name = function(){
	return (this.$parent.$parent.$name);
};

/**
find question type from HTML form elements
presume that each question has only one element type

if a hidden field is present it is checked for the ctad variable 
which indicates that it is bound to a custom question type
(ie clickstick or wordclick)
*/
Question.prototype.get_question_type = function(){


	if (!this.elements){
		this.get_elements();
	}
	
	//default, munge select-one into select_one, etc
	this.question_type=this.elements[0].type.replace("-","_");
	
	//check for ctad questiontypes
	//which attach themselves to hidden fields
	if (this.elements[0].type=="hidden"){
		if (this.elements[0].ctad){
			this.question_type="ctad";
			if (this.elements[0].ctad.match_answers){
				//set flag to match number of user data items with number of answers
				this.match_answers=true;
			}
		}
	}
	
	//persist question type (so marking can work even when not on page containing question)
	this.set_property("question_type",this.question_type);

}

/**
pull question answer from array of question types set in qdata.js
*/
Question.prototype.get_question_answer = function(){

	if (typeof(theanswers)=="undefined"){
		ctad_error("no theanswers array in qdata.js");
		return;
	}

	if (typeof(theanswers[this.$name])=="undefined"){
		ctad_error("no answer specified for this question:"+this.$name);
		return;
	}
	this.answer= theanswers[this.$name];
};


/**
get an array of HTML elements which belong to this question
*/
Question.prototype.get_elements = function(){


var form_name =this.get_form_name();
this.elements = new Array();
var j=0;
var re= new RegExp("^q"+this.$name+".*$");


	for (var i=0;i<document.forms[form_name].length;i++){
		if (re.test(document.forms[form_name][i].name)){
			this.elements[j++]=document.forms[form_name][i];
		}
	}
};


/**
restore the control to the value persisted from earlier user input
fails silently
*/
Question.prototype.load_user_data=function(){

	if (this.user_data){
		this.restore_state(this.user_data);
	}
};

/**
restore the control to the value specified in the answer data (coming from qdata.js)
*/
Question.prototype.load_answer_data=function(){

	if (!this.answer){
		this.get_question_answer();
	}
		
	this.restore_state(this.answer);
};

/**
restore the control to a given value 

calls restore function for that question type
@argument value value to restore question to, must be in correct format for question
*/
Question.prototype.restore_state = function(value){

this.values=value.split(this.$answer_delimiter);

	if (this["restore_"+this.question_type]){
		this["restore_"+this.question_type](value);
	}else{
		ctad_error("no restore function for question type:"+this.question_type);
	}
};

/**
restore method for question types without complex needs
*/
Question.prototype.restore_default = function(){
	for (var i=0;i<this.values.length;i++){
		if (this.values[i] == null) {
			this.elements[i].value = ""
		}else{
			this.elements[i].value = this.values[i];
		}
	}
};

/**
restore method for text types
*/
Question.prototype.restore_text = function(){
	if(this.elements.length>1) this.restore_default();
	//single text type
	else this.elements[0].value = this.values[0];
};

/**
restore method for textarea types
*/
Question.prototype.restore_textarea = function(){
	this.restore_default();
	for (var i=0;i<this.elements.length;i++)
		create_printable(this.elements[i]);
};

/**
restore method for hidden types
*/
Question.prototype.restore_hidden = function(){
	this.restore_default();
};

/**
restore method for checkbox types
*/
Question.prototype.restore_checkbox = function(){

	for (var i=0;i<this.elements.length;i++){
		this.elements[i].checked = this.values.contains(this.elements[i].value);
	}
};

/**
restore method for radiobutton types
*/
Question.prototype.restore_radio = function(){
	for (var i=0;i<this.elements.length;i++){
		this.elements[i].checked = (this.values[0]==this.elements[i].value);
	}
};

/**
restore method for select-one types
*/
Question.prototype.restore_select_one = function(){
	for (var i=0;i<this.elements.length;i++){
		var select =this.elements[i];

		//go through the select options
		for (var j=0; j < select.options.length; j++) {
				select.options[j].selected = (this.values[i] == select.options[j].value);
		}
	}
		
};

/**
new function to clear incorrect checkboxes
called optionally from validate 
*/
Question.prototype.clear_incorrect_checkboxes = function(){
	//if not a checkbox question, or not answered, cancel
	if(this.question_type !="checkbox" || !this.user_data)
		return;
		
	//if the form doesn't exist, cancel
	if(!document.forms[this.get_form_name()])
		return;
	
		
	var data = this.user_data.split(this.$answer_delimiter); 
		
	// loop through answers 
	for (var i = 0; i < data.length; i++) {
		if (this.answer.split(this.$answer_delimiter).contains(data[i])) 
			continue; 
			
		var form = document.forms[this.get_form_name()];
			
		for (var e = 0; e < form.elements.length; e++) { 
			var el = form.elements[e]; 
				
			if (typeof el.value == "undefined" || !el.value.match(data[i])) 
				continue
				
			el.checked = false; 
			el.onclick(); 
		}
	}
}



/**
restore method for ctad custom types
calls restore_data method attached to ctad object of hidden field
where the class which does the work is installed
custom question types must install  ctad object and restore_data method
*/
Question.prototype.restore_ctad = function(){

	this.elements[0].ctad.restore_data(this.values);
}

/**
save user data for this question to the persist mechanism
this function calls other functions depending on question type
called via wrapper event handler for every form elements onchange or onclick

also sets activity status: if question is attempted,activity has been started
*/
Question.prototype.save_user_data = function(){


	//set activity status first (may need to clear vars
	this.$parent.$parent.start();
	
	//now save the values
	this.get_question_type();

	if (this["save_"+this.question_type]){
		this["save_"+this.question_type]();
	}else{
		ctad_error("no save function for question type:"+this.question_type);
	}
	



	//now persist it
	this.set_property("user_data",this.user_data);


};

/**
save method for question types without complex needs
*/
Question.prototype.save_default = function(){

	var ar_user_data=new Array();
	this.incomplete=false; //default set each time
	
	for (var i=0;i<this.elements.length;i++){
		if (this.elements[i].value){
			ar_user_data[i]=this.elements[i].value;
		}else{
			//if an element is not filled in mark as incomplete
			this.incomplete=true;
		}
	}
	
	this.user_data=ar_user_data.join(this.$answer_delimiter);
	
};

/**
save method for text question types
*/
Question.prototype.save_text = function(){
	this.save_default();
};

/**
save method for textarea question types
*/
Question.prototype.save_textarea = function(){
	this.save_default();
};

/**
save method for hidden question types
*/
Question.prototype.save_hidden = function(){
	this.save_default();
};

/**
save method for custom ctad question types
*/
Question.prototype.save_ctad = function(){

	this.user_data=this.elements[0].value;
	
	if (this.match_answers){
		//get the answer from qdata so we can check its got as many parts as user data
		this.get_question_answer();
	
		var answer_parts=this.answer.split(this.$answer_delimiter).length;
		var data_parts=this.user_data.split(this.$answer_delimiter).length;
	
		this.incomplete =(answer_parts!=data_parts);
	}
	
}

/**
save method for checkbox question types
*/
Question.prototype.save_checkbox = function(){

var ar_checked = new Array();

	for (var i=0;i<this.elements.length;i++){

		if (this.elements[i].checked){
			ar_checked[ar_checked.length]=this.elements[i].value;
		}
	}
	
	this.user_data=ar_checked.join(this.$answer_delimiter);
	
	//mark as incomplete if no checkbox checked
	if (!this.user_data){
		this.incomplete=true;
	}else{
		this.incomplete=false;
	}
};

/**
save method for radiobutton question types
*/
Question.prototype.save_radio = function(){
	for (var i=0;i<this.elements.length;i++){

		if (this.elements[i].checked){
			this.user_data=this.elements[i].value;
		}
	}
	
	
	//mark as incomplete if no radio button checked
	if (!this.user_data){
		this.incomplete=true;
	}else{
		this.incomplete=false;
	}
};

/**
save method for select-one question types
*/
Question.prototype.save_select_one = function(){
		
	var ar_user_data= new Array();
	var missing_select = false;
	
	for (var i=0;i<this.elements.length;i++){
		ar_user_data[i]=this.elements[i].options[this.elements[i].selectedIndex].value;
		if (!ar_user_data[i]||ar_user_data[i]=="_blank"){
			missing_select=true;
		}
	}
	
	this.incomplete=missing_select;
	this.user_data=ar_user_data.join(this.$answer_delimiter);
};

/**
mark the question

if there is a specific marking function for this question type use that
otherwise compare answer with user_data simply

@returns "n" for no entry,"c" for correct,"i" for incorrect
*/
Question.prototype.mark = function(){


	//get the answer if we havent got it yet
	if(typeof(this.answer)=="undefined"){
		this.get_question_answer();
	}
	
	//if no data yet been saved
	if (!this.user_data){
		return ("n");
	}
	
	//if data save process sets incomplete flag
	if (this.incomplete){
		return ("n");
	}
	
	if (!this.question_type){
		this.get_question_type();
	}
	
	if (this["mark_"+this.question_type]){
		//check for a validation function specific to the question type
		return(this["mark_"+this.question_type]());
	}else{
		//straightforward equality check
		if (this.user_data==this.answer){
			return ("c");
		}else{
			return ("i");
		}
	}
};

/**
mark textarea question type
*/
Question.prototype.mark_textarea = function(){
	return(this.mark_text());
};

/**
mark text question type
uses Validate object for flexible matching

@returns "n" for no entry,"c" for correct,"i" for incorrect
*/
Question.prototype.mark_text = function(){

	var val = new Validate(this.$answer_delimiter);
	
	var return_value =val.validate_text(this.user_data,this.answer);
	
		
	if (return_value){
		return ("c");
	}else{
		if (val.answers_array){ //get detailed answers from validate
			for (var i in val.answers_array){
				if (!val.answers_array[i]){//clear form field if wrong
					if(this.elements)//feedback page doesn't have elements
					this.elements[i].value="";
				}
			}

		}
		return ("i");
	}
};

/**
subclass of Ctad

the Attempt object represents an attempt at a test 
*/
function Attempt(){
	inherit_object (this,new Ctad()); 
	
}

/**
set the attempt score and optionally the date
if date not set then it will be today
@argument score integer score on this attempt
@argument attempt_date date of attempt in "15/08/2003" format
*/
Attempt.prototype.set_score=function(score,attempt_date){


	this.score=score;
	
	if (!attempt_date){
		var attempt_date=new Date();
		this.date=attempt_date.getTime(); //store millisecond timestamp
	}else{
		this.date = attempt_date;
	}
};


/**
provides text matching functions

@argument delimiter delimiter to use in answer specifications
*/
function Validate(delimiter){
	
	if (delimiter){
		this.$delimiter=delimiter;
	}else{
		this.$delimiter="^";//default
	}
}

/**
see if the given value is correct with some leeway for 
added whitespace, capitalisation, and different ways of 
specifying numbers

@argument value value to match
@argument valid_values delimited string of acceptable values
*/
Validate.prototype.validate_text= function(value,valid_values){

	if (!valid_values){
		ctad_error("validate called without valid values");
		return (false);
	}
	
	//if the answer contains delimiter its either alternative correct responses or its because
	//its a multiple text box question
	//if the user_data has no delimiter its the former
	if (valid_values.toString().indexOf(this.$delimiter)!=-1 ){
		if (value.toString().indexOf(this.$delimiter)!=-1){
			//return(value==valid_values); //simple test
			return this.validate_multiple(value.split(this.$delimiter),valid_values.split(this.$delimiter));
		}else{
			return this.validate_single(value,valid_values.split(this.$delimiter));
		}
	}else{
		var ar_values=new Array;
		ar_values[0]=valid_values;
		return this.validate_single(value,ar_values);
	}

	for (var i = 0;i<ar_values.length;i++){
		//ignore empty strings probably caused by delimiter on end or start
		if (ar_values[i].match(/^\s*$/)){
			continue;
		}
		//look for exact word optionally with whitespace but no other text

		// not working - var re = new RegExp("^\\s*"+ar_values[i]+"\\s*","i"); 
		var re = new RegExp("^\s*"+ar_values[i]+"\s*$","i"); 
		if (re.test(value)) {
			return(true);
		}else{ 
		//try number validation
			if (!isNaN(ar_values[i])){
				if (this.validate_number(value,ar_values[i])){
					return(true);
				}
			}
		}
	}

	return (false);
};

/*

*/
Validate.prototype.validate_multiple= function(ar_values,ar_valid_values){
	
	if (ar_values.length!=ar_valid_values.length){
		ctad_error("number of answers does not match number of questions");
		return false;
	}
	
	this.answers_array=new Array();
	
	var all_right=true;
	
	for (var i = 0;i<ar_values.length;i++){
		
		//make single-value array to pass to function
		var ar_valid_value=new Array();
		ar_valid_value[0]=ar_valid_values[i];

		this.answers_array[i]=this.validate_single(ar_values[i],ar_valid_value);
		
		if (!this.answers_array[i]){
			all_right=false;
		}
	}
	
	return all_right;

};

/*

*/
Validate.prototype.validate_single= function(value,ar_valid_values){


	
	for (var i = 0;i<ar_valid_values.length;i++){
		//ignore empty strings probably caused by delimiter on end or start
		if (ar_valid_values[i].match(/^\s*$/)){
			continue;
		}
		//look for exact word optionally with whitespace but no other text

		// not working - var re = new RegExp("^\\s*"+ar_values[i]+"\\s*","i"); 
		var re = new RegExp("^\s*"+ar_valid_values[i]+"\s*$","i"); 
		if (re.test(value)) {
			return(true);
		}else{ 
		//try number validation
			if (!isNaN(ar_valid_values[i])){
				if (this.validate_number(value,ar_valid_values[i])){
					return(true);
				}
			}
		}
	}

	return (false);
};

/**
see if the given numeric value is correct with some leeway for 
added whitespace only.also supports english textual representations of numbers. 

@argument value value to match
@argument valid_values delimited string of acceptable values
*/
Validate.prototype.validate_number=function(value,valid_number){
	//check for the number value potentially enclosed by spaces
	//no other words
	var re = new RegExp("^\\s*"+valid_number+"\\s*$","i"); 
		if (re.test(value)) {
			return(true);
		}
	
	// check for the mathematical equivalents
	if (!isNaN(parseFloat(value)) &&!isNaN(parseFloat(valid_number))){
		return(parseFloat(value) ==parseFloat(valid_number));
	}
	
	
	
	//check for the word value potentially enclosed by spaces
	//no other words

	var number_as_words=this.number_to_words(valid_number);

	var re = new RegExp("^\\s*"+number_as_words+"\\s*$","i"); 
		if (re.test(value)) {
			return(true);
		}

	// now replace hyphens ie twenty-seven =>twenty seven and try again

	number_as_words=number_as_words.replace(/-/," ");

	var re = new RegExp("^\\s*"+number_as_words+"\\s*$","i"); 
		if (re.test(value)) {
			return(true);
		}
		
	// ok we gave them 'nuff chances
		return(false);

};

/**
convert a number into english words
original in tcl by Richard.Suchenwirth-Bauersachs@siemens.com
what a fantastic bit of coding!
translated by jh into javascript

@argument number integer
@returns string representation of number
*/
Validate.prototype.number_to_words=function(number){


var ar_small= ["zero","one","two","three","four","five","six","seven","eight","nine","ten","eleven","twelve"];

var obj_large= new Object();
obj_large[1000000]="million";
obj_large[1000]="thousand";
obj_large[100]="hundred";

var result="";

//catch small fry up to 12        
if (ar_small[number]){
	 return (ar_small[number])  ;
}


//get the big ones
for (var val in obj_large){
	if (parseInt(number)>parseInt(val)){
		var larges = Math.floor(number/val);
		var remainder = number-(larges*val);
		
		return (this.number_to_words(larges)+" "+obj_large[val]+" "+this.number_to_words(remainder));
	}
}
 
//get the ones between 13 and 99
if (number>=20) {
		var tens = Math.floor(number/10);
        result=ar_small[tens]+"ty";
        if (number-(tens*10)){
			result+="-"+ar_small[number-(tens*10)];
        }
} else {
        result=ar_small[number-10]+"teen";
} 

// make irregular like good ole english 
result=result.replace(/twoty/,"twenty");
result=result.replace(/threet/,"thirt");
result=result.replace(/fourty/,"forty");
result=result.replace(/fivet/,"fift");
result=result.replace(/eightt/,"eight");

return(result);

};

