/**
*   functions.js is a toolbag of useful functions
*
*	most js files need functions.js and should call 
*	ctad_require("functions.js") at the top
*	functions.js itself requires settings.js
*
*	this file is for generic code onl;y: no magic numbers or html writing.
*	legacy functions are in legacy.js, menu functions are in menu.js
*
*   compatible: ns6+, ie4+ 
* 
*	@author: geoff stead
*	@author: lee meadows
*	@author: jon halle
*	@author: Ivars Veksins
*
*	$Id: functions.js,v 2.22 2006/09/25 11:53:00 ivarsv Exp $
*/


//###############################################################
// code which runs when functions.js is included.
//###############################################################

var CTAD_INCLUDED = new Array(); //declare global array for includes

// first check for settings.js. this does not use require structure
// as its not available when setting.js runs
if (typeof(CTAD_SETTINGS)=="undefined"){
	ctad_error("settings.js was not included before functions.js");
}else{
	ctad_included("settings.js");
}

//and of course this one is included too
ctad_included("functions.js");

//now install browser extensions
var ctad_extend= new Extend();

ctad_extend.getElementById();
ctad_extend.getElementsByTagName();
ctad_extend.string_slice();
ctad_extend.array_pop();
ctad_extend.array_contains();



//###############################################################
// require functionality
//###############################################################

/**
make sure the specified js file is included
similar to functionality in other languages, except cannot actually include the file
if you use document.write "<scr"+"ipt src='my.js'></scr"+"ipt>"); it will be included 
_after_ the current code completes

@argument js_file_required name of js file that the calling code needs ie myjs.js
@argument js_file_asking name of js file that is requiring. optional (only used for error display)
*/
function ctad_require (js_file_required,js_file_asking){

	if (typeof(CTAD_INCLUDED[js_file_required])=="undefined"){
		if (!js_file_asking){
			js_file_asking="unidentifed javascript file";
		}
		ctad_error(js_file_asking+" requires the javascript file "+js_file_required);
	}

}

/**
register a js file as being included. complement to ctad_require

@argument js_file_name name of js file ie myjs.js
*/
function ctad_included (js_file_name){
	
	//first check to see if it is already included
	
	if (typeof(CTAD_INCLUDED[js_file_name])!="undefined"){
		ctad_error(js_file_name+" already included");
	}
	CTAD_INCLUDED[js_file_name]=true;
}

//#########################################################################
// error handling
//#########################################################################

/**
display an error if debug mode on

@argument error_string description of error
*/
function ctad_error(error_string){
	if (CTAD_SETTINGS["functions_debug"]){
		alert("Developer Error\n"+error_string);
	}
}

/**
display an error even if debug mode off (ie to end user)
probably of limited use

@argument error_string description of error
*/
function ctad_error_user(error_string){
		alert("Sorry, an error occurred\n"+error_string);
}

//#########################################################################
// browser extensions class
//#########################################################################

/**
Class containing extensions to and fixes for browser bugs and incompatibilities
*/
function Extend(){
//all methods are within constructor

	/**
	IE4 and netscape don't have getElementById so try to install a replacement
	not using prototype because ie4 doesnt have document.protoype
	*/
	Extend.prototype.getElementById=function(){
		if (!document.getElementById){
			if (document.all){
				document.getElementById=function(id){
					return(document.all[id]);
				};
			}else{
				document.getElementById=function(id){
					alert("Sorry, this browser is not supported");
					return;
				};
			}
		}
	};

	/**
	IE4 and netscape don't have getElementsByTagName so try to install a replacement
	not using prototype because ie4 doesnt have document.protoype
	*/
	Extend.prototype.getElementsByTagName=function(){
		if (!document.getElementsByTagName){
			if (document.all){
				document.getElementsByTagName=function(tag_name){
					return(document.all.tags[tag_name]);
				};
			}else{
				document.getElementsByTagName=function(id){
					alert("Sorry, this browser is not supported");
					return;
				};
			}
		}
	};

	/**
	repair String.slice in browsers where its broken (does not accept negative start)
	kudos to TIBET project for this approach and mostly the code
	*/
	Extend.prototype.string_slice=function(){
		if ('abcde'.slice(-3,-2).length == 1){
			//if it aint broke don't fix it
			return;
		}
		
		//  copy the reference to the existing implementation to a new slot
		String.prototype.$$slice = String.prototype.slice;
			/**
				same functionality as documented slice
			*/
		    String.prototype.slice = function(start, end){

		        if (start < 0){ //ie4 has a bug with negative indexes
		            //  choose either 0 or the valid index
		            start = Math.max(0, this.length + start);
		        }
				
				//gotta have something in end param
				if (!end){
					end=this.length;
				}
		        //  pass on to the original version with the indexes recomputed
		        return this.$$slice(start, end);
		    };
	};

	/**
	extend the Array object to add a 'pop' method if it doesnt already have one
	IE up to 5.5 does not have Array.pop()
	*/
	Extend.prototype.array_pop=function(){

		if (Array.prototype.pop){
			//if it aint broke don't fix it
			return;
		}
		
			/**
				same functionality as documented pop
				returns last element in Array
				removes that element from the array
			*/
			Array.prototype.pop=function(){
				if (this.length<1){
					return;
				}
				var value=this[this.length-1];
				this.length=this.length-1;
				return(value);
			};
	};

	/**
	extend the Array object to add a 'contains' method
	*/
	Extend.prototype.array_contains=function(){
		/**
		test if array contains a value
		@argument value value to test for in array
		
		@return true if array contains value, otherwise false
		*/
		Array.prototype.contains = function(value){
			for (var i=0;i<this.length;i++){
				if (this[i]==value){
					return(true);
				}
			}
			
			return(false);
		};
	};
	
}

//#########################################################################
// general utilities and extensions
//#########################################################################

/**
Use this function to test for an empty value.
*/
function MT(value) {
	return (value == null) || (value == "");
}


/**
helper function for inheritance
dont extend the native Object object
or it ends up confusing, 
ie when iterating with for/in you get extra properties

called as first thing in constructor
inherit_object (this,new MamaClass());

@argument sub_class class which is inheriting
@argument super_class class to inherit from
*/
function inherit_object (sub_class,super_class) { 

       for (prop in super_class) { 
               sub_class[prop] = super_class[prop]; 
       } 
}

//#####################################
// stylesheet functions
//#####################################

/**
find whether a stylesheet has already specified an attribute
remember to try likely selectors ie .null_anchor and a.null_anchor

@argument selector what selector to look for in stylesheet ie 'a' or '.myclass'
@argument style_attribute what attribute to look for ie 'color'
@return true if attribute set in stylesheet otherwise false
*/
function style_in_stylesheet(selector,style_attribute){

	if (!document.styleSheets){
		return (false);
	}
	
	for (var ss = 0; ss < document.styleSheets.length; ss++){
	     var tss = document.styleSheets[ss];

	
		if (!tss.cssRules){
			tss.cssRules = tss.rules;
		}
		
	    for (var ru = 0; ru < tss.cssRules.length; ru++){

			if (typeof(tss.cssRules[ru].selectorText) != "undefined"){
				_match = new RegExp(selector,"gi"); 
				if (tss.cssRules[ru].selectorText.match(_match)) { 
					if (typeof(tss.cssRules[ru].style[style_attribute])!="undefined"){
						return (true);
					}	
				}				
			}		
		}
	}
	return(false);
}

//#####################################
// cookie functions
//#####################################

/**
detect if cookies are available
*/
function cookies_available(){

	var use_cookies=false;

	//use browser defined value if available
	if (typeof navigator.cookieEnabled=="undefined"){ 
		if (!document.cookie){
			//try a test cookie
			document.cookie="testcookie";
			use_cookies=(document.cookie=="testcookie");
			
			//erase dummy value
			document.cookie=""; 
		}else{
			//if there is a cookie then cookies work
			use_cookies=true;
		}
	}else{
		use_cookies =navigator.cookieEnabled;
	}

	return (use_cookies);

}

/**
determine whether to use cookies based on availability and 
persist_force_frameset setting
*/
function cookies_use(){
	return (cookies_available() && !CTAD_SETTINGS["persist_force_frameset"]);
}


/**
Cookie class from rhino book doctored by jh to use cookies more efficiently
a single key/value pairs string is created	which gets split up into cookie-sized chunks
All the predefined properties of this object begin with '$'
to distinguish them from other properties which are the values to
be stored in the cookie.

@argument document:		Required Document object value.
@argument prefix:		Required string which all the cookies made by this object 
				will start with.
@argument days:			Optional number that specifies the days from now
				that the cookie should expire.
@argument path:			Optional string that specifies the cookie path attribute.
@argument domain:		Optional string that specifies the cookie domain attribute.
@argument secure:		Optional Boolean value, if true, requests a secure cookie.
@argument max_size:		Optional max size of cookie
@argument max_cookies:	Optional max number of cookies

*/
function Cookie(document, prefix, days, path, domain, secure, max_size, max_cookies) {


  this.$document = document;
  this.$prefix = prefix;
	if (days){
		this.$expiration =  new Date((new Date()).getTime() + days * 86400000);
	}else{
		this.$expiration = null;
	}
  if (path){   this.$path   = path;   }else{ this.$path   = null;}
  if (domain){ this.$domain = domain; }else{ this.$domain = null;}
  if (secure){ this.$secure = true;   }else{ this.$secure = false;}
  if (max_size){ this.$max_size = max_size;   }else{ this.$max_size = 4000;}
  if (max_cookies){ this.$max_cookies = max_cookies;   }else{ this.$max_cookies = 20;}

//now doctor the max size based on the other stuff we are puting into each cookie
	
	var metadata= this.metadata();
	this.$max_size-=metadata.length;

}

/** 
Store method of cookie object

Loop through the properties of the Cookie object and
put together an array of cookie strings.

then create the cookie(s)
*/
Cookie.prototype.store=function() {  

this.$ar_cookie_strings=Array(); 
this.$cookie_index=0;


  for(var prop in this) {  // Ignore "$" properties, and methods
		if ((prop.charAt(0) == '$') || ((typeof this[prop]) == 'function')) {
		    continue;
		}

		var pair=prop + ":" + escape(this[prop]);
		this.fill_cookie(pair);
 
   }
   

//for each string in the array we create a new cookie
	
	for (var i=0;i<this.$cookie_index+1;i++){
		this.create_one(this.$prefix+i,this.$ar_cookie_strings[i]);
	}

};

/**
Recursive function to fill up cookies as much as possible and then move on to next one.
creates $ar_cookie_strings
@argument pair key-value pair
*/
Cookie.prototype.fill_cookie=function(pair){


	//initialise array if needed
	if (!this.$ar_cookie_strings[this.$cookie_index]){
		this.$ar_cookie_strings[this.$cookie_index]="";
	}
	
	var current_cookie_length=this.$ar_cookie_strings[this.$cookie_index].length;
	
	//set up pair with ampersand if needed
	if (current_cookie_length!=0){
		pair="&"+pair;
	}
	
	// check to see that the key-value pair is not too long
		if (pair.length>this.$max_size){
			ctad_error('cookie: string not saved, it was too long: ' + pair.length + '/' + this.$max_size + ' bytes');
			return;
		}
		
	//if cookie prop and val would push us into the next cookie
       if ((current_cookie_length+pair.length)>this.$max_size){
		
			//check to see that we aren't out of cookies
			if (this.$cookie_index+2>this.$max_cookies){
				ctad_error('cookie: string not saved, we ran out of cookies');
				return;
			}
			
			//try the next cookie
			this.$cookie_index++;
			this.fill_cookie(pair);
		
     }else{
	
			//otherwise now add to the cookie array
			this.$ar_cookie_strings[this.$cookie_index]+=pair;
	}
};

/**
returns the cookie metadata
*/
Cookie.prototype.metadata=function(){
	var metadata = "";

	if (this.$expiration)
      metadata += '; expires=' + this.$expiration.toGMTString();
	if (this.$path) metadata += '; path=' + this.$path;
	if (this.$domain) metadata += '; domain=' + this.$domain;
	if (this.$secure) metadata += '; secure';
  
	return(metadata);
};

/**
creates one cookie
@argument name name of the cookie to make
@argument val value to store
*/
Cookie.prototype.create_one=function (name,val){

	var cookie = name + '=' + val;
	cookie+=this.metadata();

  this.$document.cookie = cookie;  // store with magic property
};

/**
get a list of all cookies that pertain to this document.
We do this by reading the magic Document.cookie property. 
*/
Cookie.prototype.load=function () {  
	var allcookies = this.$document.cookie;
	  if (allcookies == "") return false;
		
	var ar_cookies_all = allcookies.split(/;\s*/);
	var cookieval="";
	var i=0;
	var ar_pairs = Array();
	var ar_crumb = Array();

	for(i = 0; i < ar_cookies_all.length; i++) {
		var ar_cookie = ar_cookies_all[i].split("=");
		if (ar_cookie[0].substring(0,this.$prefix.length)==this.$prefix){
			cookieval=ar_cookie[1];
			ar_crumb=cookieval.split("&");
			ar_pairs=ar_pairs.concat(ar_crumb);
		}
	}


	/* Now that we've parsed the cookie value, set all the names and values
	of the state variables in this Cookie object. Note that we unescape()
	the property value, because we called escape() when we stored it.
	*/
  for(var i = 0; i < ar_pairs.length; i++) {
	var ar_pair=ar_pairs[i].split(":");
    this[ar_pair[0]] = unescape(ar_pair[1]);
  }

  // We're done, so return the success code.
  return true;
};

/**
remove a cookie
*/
Cookie.prototype.remove =function () {  
	var cookie;
	cookie = this.$name + '=';
	if (this.$path) cookie += '; path=' + this.$path;
	if (this.$domain) cookie += '; domain=' + this.$domain;
	cookie += '; expires=Fri, 02-Jan-1970 00:00:00 GMT';

	this.$document.cookie = cookie;  // magic store
};

//#####################################
// general window fun mostly from dreamweaver
//#####################################

var win= null;
function newwindow(mypage,myname,w,h,scroll,top){
	
	var winl = (screen.width-w)/2;
	if (typeof(top)!="undefined"){
		var wint = (screen.height-h)*top;
	}else{
		var wint = (screen.height-h)/2;
	}

	settings='height='+h+',width='+w+',top='+wint+',left='+winl+',scrollbars='+scroll+',toolbar=no,location=no,status=no,menubar=no,resizable=yes,dependent=no'
	win=window.open(mypage,myname,settings)
	if(parseInt(navigator.appVersion) >= 4){win.window.focus();}
	
	return win; 
}

/**
newwindowfocus checks first if the window has been already opened
used when opening the calculator
**/
function newwindowfocus(mypage,myname,w,h,scroll){
	if ( win && win.open && !win.closed) { 
		win.focus();
	} else { 
		var winl = (screen.width-w)/2;
		var wint = (screen.height-h)/2;
		
		settings='height='+h+',width='+w+',top='+wint+',left='+winl+',scrollbars='+scroll+',toolbar=no,location=no,status=no,menubar=no,resizable=yes,dependent=no'
		win=window.open(mypage,myname,settings)
		
		if(parseInt(navigator.appVersion) >= 4)
			win.window.focus();
			
	}
}



// This function is used for the flash templates Scanning and WhatNext (it could perhaps belong elsewhere)
function evSetInstructionText(order)
{
	
	var instructions = instruction_text[qnum]; //passed from template (q number to get data from qdata)
	
	var brokenstring = instructions.split("^");
	
	var displayText = document.getElementById("instruction_text");
	
	if (!displayText) 
		return; 

	displayText.innerHTML = brokenstring[order]; 
	displayText = null; 
	
}


// -- GARBAGE COLLECTOR -- 

/** 
  * Implementation of garbage collector. Collects DOM 
  * references and releases it on before unload. 
  * 
  * @author Ivars Veksins (ivarsv@gmail.com) 
  */
  
function Garbage() 
{ 
}

/** 
  * Initialise garbage collections of data to be destroyed 
  */
Garbage.init = function() 
{
	if (!window._OBJECTS_)  {
		
		// create storage array 
		window._OBJECTS_ = []; 
		window._FUNCTIONS_ = []; 
		window._EVENTS_ = []; 
		window._CLASSES_ = []; 
	}
}

/** 
  * Called before unload. Goes through all reference 
  * arrays and releases DOM elements. 
  */
Garbage.release = function() 
{
	if (!window._OBJECTS_) 
		return false; 
	
	// release events first. 
	Garbage.release_events(); 
	
	// release objects 
	for (var index in window._OBJECTS_) {
		var element = window._OBJECTS_[index]; 
		
		if (typeof element == "function") 
			continue; 
		
		window._OBJECTS_[index] = null; 
	}
	
	window._OBJECTS_ = null; 
	
	// delete all functions as well 
	for (var index in window._FUNCTIONS_) { 
		var funct = window._FUNCTIONS_[index]; 
		
		funct = null; 
	} 
	
	window._FUNCTIONS_ = null
	
	// call unload 
	for (var index in window._CLASSES_) {
		var cls = window._CLASSES_[index]; 

		if (cls.dispose)
			cls.dispose(); 
			
		window._CLASSES_[index] = null; 
	}
	
	window._CLASSES_ = null; 
	
	return true; 
}

/** 
  * Detaches all events installed using event_install 
  * function. 
  */
Garbage.release_events = function () 
{
	var events = window._EVENTS_; 

	for (var i = 0; i < events.length; i++) { 
		var event = events[i]; 

		detach_event(event[0],event[1],event[2]); 
	}
	
	window._EVENTS_ = null; 
}

/** 
  * Picks up an event to be released on before unload 
  * @argument DOM element event has to be attached to 
  * @argument name of the event i.e. onclick etc.. 
  * @argumetn f function to action on event 
  */
Garbage.push_event = function(obj, event, f) 
{ 
	// init storage arrays 
	Garbage.init(); 

	var length = window._EVENTS_.length; 
	window._EVENTS_[length] = [obj,event,f]; 
}

/** 
  * Collect garbage information about a single object 
  * specified in arguments. 
  */
Garbage.collect = function(obj) {
	
	// initialise garbage 
	Garbage.init(); 
	
	var func = this; 
	
	// populate objects array 
	var object_id = obj._OBJID; 
	if (!object_id) {
		// object id is the last item in the objects array 
		object_id = obj_OBJID = window._OBJECTS_.length;
		window._OBJECTS_ [object_id] = obj; 
	}
	
	// do the same stuff for functions 
	var funct_id = func._FUNCID; 
	if (!funct_id) {
		funct_id = func._FUNCID = window._FUNCTIONS_.length; 
		window._FUNCTIONS_[funct_id] = obj._FUNCT = func; 
	}

	if (!obj._CLOSURES) 	
		obj._CLOSURES = []; 
	
	var closure = obj._CLOSURES[funct_id]; 
	if (closure) 
		return closure; 
		
	// release 
	obj = func = null; 
	
	// Create the closure, store in cache and return result.
	return window._OBJECTS_[object_id]._CLOSURES[funct_id] = function() {
		// page unloaded 
		if (!window._FUNCTIONS_) 
			return; 
			
		return window._FUNCTIONS_[funct_id].apply(window._OBJECTS_[object_id], arguments);
	};	
}


// assign closure function to garbage collector
Function.prototype.garbage = Garbage.collect;	

/** 
  * Function for attaching events to a
  * DOM element. 
  */
function event_install(obj,event_name,f){

	if (obj.addEventListener){	//W3C
		obj.addEventListener(event_name.substr(2), f.garbage(obj),false);
	}else if (obj.attachEvent){	 //IE 
		obj.attachEvent(event_name,f.garbage(obj));
	}
	
	Garbage.push_event(obj, event_name, f); 
}

/** 
  * Detach event from the DOM object 
  */
function detach_event(obj,event_name, f){

	if (obj.removeEventListener) { 
		obj.removeEventListener(event_name.substr(2), f,false);
	} else if (obj.detachEvent) { 
		obj.detachEvent(event_name, f.garbage(obj)); 

	}
}

/** 
  * Extension to custom classes which will implement dispose 
  * method. This allows classes clean their data from memory before page is unloaded
  * 
  * Note that calls will be static 
  * 
  * @argument name of the class
  */
function IMPLEMENT_GARBAGE(f) {
	Garbage.init(); 
	window._CLASSES_[ window._CLASSES_.length ] = f; 
}

// install event which will release garbage information 
event_install(window,"onunload",Garbage.release); 



// implement mouse capture 

/** 
 * Functions for capturing mouse position
 * mouse positions are saved in XPOS and YPOS attributes 
 * and are access friendly to anyone
 */ 
function MouseCapture()
{ 
}

MouseCapture.XPOS = 0; 
MouseCapture.YPOS = 0; 

// called by garbage collector 
MouseCapture.dispose = function() 
{ 
	MouseCapture.XPOS = null; 
	MouseCapture.YPOS = null; 
	
	if (document.releaseEvents) 
		document.releaseEvents(Event.MOUSEMOVE); 
	
	document.onmousemove = null; 
}

// initialises mouse capture 
MouseCapture.init = function() { 
	// browsers who support capture events must have this 
	if (document.captureEvents) {
		document.captureEvents(Event.MOUSEMOVE); 
	}	
	document.onmousemove = MouseCapture.GetPos; 
}

// capture event
MouseCapture.GetPos = function(captured_event) { 
	if (captured_event) { 
	
		MouseCapture.XPOS = captured_event.pageX;
		MouseCapture.YPOS = captured_event.pageY; 
	} else { 
	
		// most probably IE 
		MouseCapture.XPOS = event.clientX + document.body.scrollLeft; 
		MouseCapture.YPOS = event.clientY + document.body.scrollTop; 
	}
}

// if using implement mouse capture 
IMPLEMENT_GARBAGE(MouseCapture); 
event_install(window,"onload", MouseCapture.init); 


/** 
  * Placement class consists of functions which help to 
  * locate elments on document 
  */
function Placement() 
{ 
}


/** 
  * Gets top left position of an element 
  * @argument element reference in document 
  * @return array containing x and y
  */
Placement.get_tl_pos = function (element) 
{ 
	// initialise x and y positions 
	var x = 0; 
	var y = 0; 
	
	do { 
		y += element.offsetTop || 0;
		x += element.offsetLeft || 0;
		
		element = element.offsetParent;
	} while (element); 
	
	// returns values 
	return [x,y]; 
}
