// --------------------------------------------------------------------------------
// ow_cssswitch.js
// Matthew Hogg 20-Apr-2004
// Contains core functions for generation and population of stylesheet switcher.
// Travis Musika 2006-11-06
// Updated to version 2.1 of IOTBS - http://www.stuffandnonsense.co.uk/resources/iotbs.html
// --------------------------------------------------------------------------------
// IOTBS2.1 :: Invasion of the Body Switchers - Look Who's Switching Too
// --------------------------------------------------------------------------------
// This copyright statement must remain in place for both personal and commercial use
// GNU General Public License -- http://www.gnu.org/copyleft/gpl.html
// --------------------------------------------------------------------------------
// Original concept by Andy Clarke -- http://www.stuffandnonsense.co.uk/
// DOM scripting by brothercake -- http://www.brothercake.com/
// Create element and attributes based on a method by beetle -- http://www.peterbailey.net/
// --------------------------------------------------------------------------------

//global preferences manager reference
var switcher;
var separatorClassName = "swchSeparator";
var firstIdName = "swchFirst";

// --------------------------------------------------------------------------------
// This function is run upon page loading and sets up the stylewheet switcher.
// Modify this to change the options available via the switcher.
// --------------------------------------------------------------------------------

ow_f_AppendInitEvent(
function() {
	var switcher = new switchManager("html", "/site-tcl/styles/");
	var swch = new bodySwitcher("css_switch", "text size:", "no", "");
	// with this script, the Default element must be first
	swch.defineClass("default", "A");
//	swch.defineSeparator("|");
	swch.defineClass("big", "A");
//	swch.defineSeparator("|");
	swch.defineClass("bigger", "A");
}
);

// --------------------------------------------------------------------------------
// switchManager functions.
// --------------------------------------------------------------------------------

//preferences manager
function switchManager(canvas, path) {
	//save to global reference
	switcher = this;

	//store reference to canvas element
	//if the canvas is 'body' and the document.body object exists, use that reference
	//because in older moz builds (eg ns7.1) document.getElementsByTagName('body')[0] is undefined
	//unless this script is in the body section (which it isn't)
	this.canvas = canvas == "body" && typeof document.body != "undefined" ? document.body : document.getElementsByTagName(canvas)[0];

	//store the initial classname
	this.initial = this.canvas.className;

	//if the default classname is empty, add "iotbs"
	//because we need there to be at least one classname already -
	//the leading and trailing space in each custom classname is required,
	//but we can't set the body classname as " something" (beginning with a leading space)
	//because that fails in some Opera 7 builds
	if(this.initial == "") { this.initial = "itobs"; }

	//the stylesheet path argument is used to switch on load-mode
	//so store the path, or a null value if none or an empty string is present
	this.path = (typeof path != "undefined" && path != "" ? path : null);

	//regex for finding the native integration token within cookie data
	this.itoken = /(\:\[[0-9]+\])/;

	//if load mode is in use
	if(this.path != null) {
		//ID reference and token index data extractable from integration token
		//set to empty string and -1 by default (discountable values)
		//to make testing them easier - it saves additional typeof discriminators
		this.tokenrefs = ["", -1];

		//collection of <link> elements we also need for integration mode
		this.linkeles = document.getElementsByTagName("link");
	}

	//whether one of the switchers is part of the native group
	//this will be a reference to the applicable oject, or null if none
	this.master = null;

	//string for storing the overall custom classname, for writing to the body classname
	//I was originally storing it in the body class name directly
	//but 1.7+ mozilla builds were not honouring the trailing whitespace we need
	this.string  = "";

	//string for storing the switcher idents and classnames as name/value pairs
	//for storing in the cookie and identifying link elements when load-mode is used
	this.idstring = "";


	//identify opera for applying tweaks
	this.isop = typeof window.opera != "undefined";

	//identify IE for applying hacks
	this.isie = typeof window.attachEvent != "undefined" && !this.isop;

	//identify konqueror for applying hacks
	this.iskde = navigator.vendor == "KDE";


	//look for a stored cookie
	this.cookie = this.read();

	//if it exists
	if(this.cookie != null) {
		//store cookie value to string
		this.string = this.cookie;

		//if load-mode is not in use
		if(this.path == null)
			//set new body class name
			this.canvas.className = this.initial + this.string;

		//for each value in the idcookie array
		for(var i=0; i<this.idcookie.length; i++) {
			//add data to id string
			this.idstring += this.idcookie[i][0] + "=" + this.idcookie[i][1] + "&";

			//if load-mode is in use
			if(this.path != null) {
				//get a reference to the applicable <link> element
				this.linkele = document.getElementById(this.idcookie[i][0] + "_stylesheet");

				//if it exists
				if(this.linkele != null) {
					//if the integration token ID reference matches this switcher ident
					if(this.tokenrefs[0] == this.idcookie[i][0]) {
						//array of stylesheets and alternate stylesheets
						//which comprise this native switching group
						//the first (default) is the one we already have
						this.alternates = [this.linkele];

						//set an in-scope flag that we'll use to determine
						//when we've reached, then gone past,
						//the stylesheets we're interested in
						var inscope = false;

						//for each <link> element
						var len = this.linkeles.length;
						for(var j=0; j<len; j++) {
							//if this link is our master default stylesheet
							if(this.linkeles[j] == this.alternates[0]) {
								//set the flag to say we're in scope
								inscope = true;

								//remember this index
								var ind = j;
							} else if(inscope && (this.linkeles[j].getAttribute("rel") == null || !/(alternat)(iv)?(e stylesheet)/i.test(this.linkeles[j].rel))) {
								//otherwise if we're in scope and this is not an "alternate" stylesheet
								//set the flag to say we're out of scope
								inscope = false;

								//finally.. enable the indicated stylesheet
								//which will be the integration token index + stored index of master
								this.linkeles[this.tokenrefs[1] + ind].disabled = false;

								//and we can stop now
								break;
							}

							//if we're in scope and this is a titled stylesheet
							if(inscope && this.linkeles[j].getAttribute("rel") != null && /(stylesheet)/i.test(this.linkeles[j].rel) && this.linkeles[j].getAttribute('title') != null && this.linkeles[j].title != '') {
								//disable it
								this.linkeles[j].disabled = true;
							}
						}
					} else {	//if it doesn't match (or the token ID ref is empty)
						// change its href to load the relevant stylesheet
						//using path value, plus established naming conventions, to create the filename
						this.linkele.href = this.path + this.idcookie[i][0] + "_" + this.idcookie[i][1] + ".css";
					}
				}
			}
		}
	}

	//*** dev
	//document.title = "<" + this.canvas.className.replace(/ /g,"+") + ">   [" + this.string.replace(/ /g,"+") + "]";

	//add a DOM memory cleaner for win/ie
	if(typeof window.attachEvent != "undefined") {
		window.attachEvent("onunload", function() {
			var closures = ["onchange", "onclick", "onkeydown", "checker"];
			for(var i=0; i<document.all.length; i++) {
				for(var j=0; j<closures.length; j++) { document.all[i][closures[j]] = null; }
			}
		});
	}
};


//set a cookie method
switchManager.prototype.set = function(days) {
	//format expiry date
	var thedate = new Date();
	thedate.setTime(thedate.getTime() + ( days *24*60*60*1000));

	//store the idstring
	var info = this.idstring;

	//if the value is empty, set its expiry in the past to delete the cookie
	if(info == "") { thedate.setTime(0); }

	//create the cookie
	document.cookie = "OWCSSSwitch=" + info	+ ";expires=" + thedate.toGMTString() + ";path=/";

};

//read a cookie method
switchManager.prototype.read = function() {
	//set null reference so we always have something to return
	this.cookie = null;

	//if a cookie exists and it's ours
	if(document.cookie && document.cookie.indexOf("OWCSSSwitch=")!=-1)
	{
		//idcookie array is where extracted data will be stored
		this.idcookie = [];

		//split cookie to extract relevant information
		//creating a classname string to apply to the body (this.cookie string)
		//and name/value pairs array for loading a new stylesheet in load-mode (this.idcookie array)
		this.cookie = document.cookie.split("OWCSSSwitch=")[1].split(";")[0].split("&");
		var tmp = "", len = this.cookie.length;
		for(var i=0; i<len; i++) {
			this.cookie[i] = this.cookie[i].split("=");
			if(this.cookie[i].length > 1) {
				//if load mode is in use and the integration token is there
				if(this.path != null && this.itoken.test(this.cookie[i][1])) {
					//save the ID reference (switcher ident)
					//and the token index (position in alternates group)
					this.tokenrefs = [ this.cookie[i][0], parseInt(this.cookie[i][1].split(":[")[1], 10) ];
				}
				//clean the integration token from the value
				this.cookie[i][1] = this.cookie[i][1].replace(this.itoken, "");

				//store to idcookie array
				tmp += " " + this.cookie[i][1] + " ";
				this.idcookie[i] = this.cookie[i];
			}
		}
		//re-save cleaned value to this.cookie
		//so it can be returned as the data
		this.cookie = tmp;
	}

	return this.cookie;
};


//save the current switcher state
switchManager.prototype.save = function(ident, chosen, ind, obj) {
	//switcher ident (label)
	this.ident = ident;

	//remove this ident from switcher idstring
	this.idstring = this.idstring.replace(this.ident + "=", "");

	//run through classnames array
	var len = obj.classes.length;
	for(var i=0; i < len; i++) {
		//remove this key (custom class name) from string
		this.string = this.string.replace(" " + obj.classes[i] + " ","");

		//remove this key and possible integration token from idstring
		var reg = new RegExp("(" + obj.classes[i] + ")" + "(\:\[[0-9]+\])?" + "&", "");
		this.idstring = this.idstring.replace(reg,"");
	}

	//if chosen value isn't default
	if(chosen != "default")	{
		//add to classname string
		//we need both a leading and a trailing space to work with
		//to avoid confusion with identical leading or trailing substrings in classnames,
		//such as "high" and "highcontrast" or "large-serif" and "small-serif"
		this.string += " " + chosen + " ";

		//add to switcher idstring
		this.idstring += this.ident + "=" + chosen;

		//if this is the native switching group
		if(this.master == obj) 	{
			//add a token to the string that indicates integration containing a number for its index in the group,
			//corresponding to its index in the alternates array. the colon is a safe delimiter to use here,
			//because the key value is ultimately a CSS class name, which isn't allowed to contain a colon
			this.idstring += ":[" + ind + "]";
		}

		//add the trailing delimiter
		this.idstring +=  "&";
	}

	//if load-mode is in use
	if(this.path != null) {
		//if this isn't the native switching group
		if(this.master != obj) {
			//get a reference to applicable <link> element
			var linkele = document.getElementById(this.ident + "_stylesheet");

			//if it exists
			if(linkele != null) {
				//stylesheet src path
				var sheetpath = this.path + this.ident + "_" + chosen + ".css";

				//if this is opera we have to set the href on a timer
				//otherwise the change doesn't kick in unless the stylesheet is already in cache
				if(this.isop) setTimeout(function() { linkele.href = sheetpath; }, 10);

				//if this is IE, we have to preload the stylesheet for exactly the same reason as opera
				//but we can't use the timeout method because that causes win/ie5.0 on 2k and 98se to crash
				if(this.isie) {
					//so preload it using XMLHttpRequest
					//I originally tried doing it using createStyleSheet
					//which was simpler and took less code
					//but it didn't solve the problem ... this does
					var request = new ActiveXObject("Microsoft.XMLHTTP");

					//once the stylesheet has loaded
					request.onreadystatechange = function() {
						//readyState of 4 = document has finished loading
						//status of 200 = okay, 304 = not modified
						if(request.readyState == 4 && /(200|304)/.test(request.status.toString())) {
							//load it into the relevant link element but for some reason if we just set the href to the path directly
							//and the new sheet has an @import statement in it, IE6 will crash! but equally obscurely, if we first set it to an empty path
							//the crash doesn't happen and the whole process works smoothly!
							linkele.href = "";
							linkele.href = sheetpath;
							//alert("LOADER change");
						}
					};

					//the request must come after the readystate function is defined
					//otherwise that function may not be called if the request is fulfilled very quickly
					request.open("GET", sheetpath, true);
					request.send(null);
				}

				//now load the stylesheet for *everyone* [including opera and IE so that if the stylesheet is already in cache
				// the change will happen straight away this does mean that under some circumstances, those browsers
				// will load each stylesheet twice, making two server requests but that can't be helped  - there's no way to say, don't do this
				// if the stylesheet is in cache, because there's no way to know and we can't just set a flag so it only goes through the preload routine once
				// because someone may have their browser set not to cache stylesheets (or anything)]
				linkele.href = sheetpath;
				//alert("default change");
			}
		} else {
			//if it is the native switching group
			//for each stylesheet in the native group
			len = this.alternates.length;
			for(i=0; i<len; i++)
				//set it to disabled unless i matches the index of this option in the group
				this.alternates[i].disabled = i != ind;
		}
	} else {
		//if load-mode is not in use set new body class name
		this.canvas.className = this.initial + this.string;
	}

	//store changes to a cookie which expires a year from now
	this.set(365);

	//*** dev
	//document.title = '<' + switcher.canvas.className.replace(/ /g,'+') + '>   [' + switcher.string.replace(/ /g,'+') + ']';
};


//compile preferred/alternate stylesheets collection for integration mode
switchManager.prototype.integrate = function(obj, divid) {
	//save bodySwitcher object to master reference
	this.master = obj;

	//array of stylesheets and alternate stylesheets
	//which comprise this native switching group
	//the first (default) can be idetified by its ID
	this.alternates = [document.getElementById(divid + "_stylesheet")];

	//then the others are the next link element and all following
	//which have both a title, and rel="alternate stylesheet"

	//set an in-scope flag that we'll use to determine
	//when we've reached, then gone past,
	//the stylesheets we're interested in
	var inscope = false;

	//for each <link> element
	var len = this.linkeles.length;
	for(var i=0; i<len; i++) {
		//if the *previous* link is our master default stylesheet
		//(so obviously it can't be the first one)
		if(i > 0 && this.linkeles[i - 1] == this.alternates[0])
			inscope = true;			//set the flag to say we're in scope

		//if we're in scope
		if(inscope) {
			//if this <link> has a title, and rel="alternate stylesheet"
			if(this.linkeles[i].getAttribute("title") != null && this.linkeles[i].getAttribute("rel") != null && /(alternat)(iv)?(e stylesheet)/i.test(this.linkeles[i].rel)) {
				//add it to the alternates array
				this.alternates[this.alternates.length] = this.linkeles[i];
			} else { //else it doesn't have the necessary attributes
				//set the flag to say we're out of scope again
				inscope = false;

				//and in fact we can stop now since there's only ever one native switching group
				//and if the page does contain more than one the first one takes precedence anyway
				break;
			}
		}
	}


	//remember which stylesheet is enabled as an index in the alternates array
	//we're just setting 0 here instead of finding out which one is enabled now
	//which does mean that an unecessary save will happen if the default is not enabled
	//but it's much less code to do it this way
	//the reason we need this information is so that we only save when necessary
	//partly because it's more efficient, but mostly because
	//the save action changes the selectedIndex, and if we didn't discriminate
	//you wouldn't be able to use the selector manually at all, because on every timeout loop
	//the selector would reset back to show the active stylesheet,
	var isenabled = 0;

	//create a watcher to monitor alternate stylesheets
	var watcher = window.setInterval(function() {
		//run through alternate stylesheets
		var len = switcher.alternates.length;
		for(var i=0; i<len; i++) {
			//if this sheet is enabled
			if(!switcher.alternates[i].disabled) {
				//if the enabled index is different from the last enabled index
				if(i != isenabled) {
					//update last enabled index
					isenabled = i;

					//send index to interface callback function
					//which updates the switcher control, if present
					obj.update(i);

					//save the current switcher state
					switcher.save(
						divid, //switcher ident (divid)
						switcher.alternates[i].href.split(divid + "_")[1].split(".css")[0], //extrapolate class name value from stylesheet href
						i, //the index of this option in the switcher group
						obj //a reference to this bodySwitcher object
						);
				}

				//stop now - only one is enabled
				break;
			}
		}
	},55);
};


//create element and attributes method -- http://www.codingforums.com/showthread.php?s=&postid=151108
switchManager.prototype.create = function(tag, attrs) {
	//detect support for namespaced element creation, in case we're in the XML DOM
	var ele = (typeof document.createElementNS != "undefined") ? document.createElementNS("http://www.w3.org/1999/xhtml",tag) : document.createElement(tag);

	//run through attributes argument
	if(typeof attrs != "undefined") {
		for(var i in attrs) {
			switch(i) {
				//create a text node
				case "text" : ele.appendChild(document.createTextNode(attrs[i])); break;

				//create a class name
				case "class" : ele.className = attrs[i]; break;

				//create a for attribute
				case "for" : ele.setAttribute("htmlFor",attrs[i]); break;

				//create a generic attribute using IE-safe attribute creation
				default : ele.setAttribute(i,""); ele[i] = attrs[i]; break;
			}
		}
	}
	return ele;
};


// --------------------------------------------------------------------------------
// bodySwitcher functions.
// --------------------------------------------------------------------------------


//switcher-control constructor
function bodySwitcher(divid, label, isnative, selected) {
	//if load-mode is in use, and this is not ie, and this is the master of the native switching group
	if(switcher.path != null && !switcher.isie && typeof isnative != "undefined" && isnative == "yes") {
		//compile preferred/alternate stylesheets collection
		//and integrate with native switching
		switcher.integrate(
			this, //reference to this bodySwitcher object
			divid //switching control id
			);
	}

	//create an array for storing the switcher's option classnames and labels as they're added
	//so we can later iterate through and remove them from the custom classname string
	this.classes = [];

	//don't continue if the container doesn't exist
	if(document.getElementById(divid) == null) { return false; }

	//create an array of labels (text labels for each class)
	//to store as values in javascript: URIs
	this.labels = [];

	//definition list
	var attrs = { "id" : "select_" + divid };
	this.dl = document.getElementById(divid).appendChild(switcher.create("dl", attrs));

	//definition term [switcher heading containing label text]
	attrs = { "text" : label };
	this.dl.appendChild(switcher.create("dt", attrs));

	//"selected" text
	this.selected = typeof selected != "undefined" ? selected : "";

	return true;
};


//add a new class option method
bodySwitcher.prototype.defineClass = function(key, val) {
	//store the classname
	this.classes[this.classes.length] = key;

	//don't continue if the list doesn't exist
	if(typeof this.dl == "undefined") { return false; }

	//create a reference to 'this'
	var self = this;

	//store the link text labels
	this.labels[this.labels.length] = val;

	//definition inside list
	var item = this.dl.appendChild(switcher.create("dd"));

	//add selected class name to definition
	item.className = "selected " + key;

	// add the switch first id	
	if (this.dl.childNodes.length == 2) item.id = firstIdName;

	//if key is default
	if(key == "default") {
		//add plain text inside definition
		var link = item.appendChild(document.createTextNode(val + this.selected));
	
	} else if(switcher.cookie != null && switcher.cookie.indexOf(" " + key + " ")!=-1) {
	//else if cookie exists and its value matches this key
		//add plain text inside definition
		link = item.appendChild(document.createTextNode(val + this.selected));

		//if this is *not* the default item
		if(key != "default") {
			//turn default text back into a link 
			// we're doing it this way, instead of just creating a default link because at the point where the default item is created
			//we don't know whether it should be a link or not so we create it as plain text to begin with, and convert it here
			//because we know, now, that the default should be a link
			var defitem = this.dl.childNodes[1];

			//remove selected class name
			defitem.className = "default";

			//remove the text node
			this.dl.childNodes[1].removeChild(defitem.firstChild);

			//and replace it with a link to select the default
			var attrs = { "href" : "javascript:void(\"" + this.classes[0] + "\", \"" + this.labels[0] + "\")", "text" : this.labels[0] };
			link = defitem.appendChild(switcher.create("a", attrs));
		}
	} else {
	//else if cookie doesn't exist or value doesn't contains this key
		//remove selected class name
		item.className = key;

		//add link inside definition
		//javascript: uri is used to store key value
		//and so that the link is navigable with the keyboard
		attrs = { "href" : "javascript:void(\"" + key + "\", \"" + val + "\")", "text" : val };
		link = item.appendChild(switcher.create("a", attrs));
	}


	//bind onclick handler to definition item
	item.onclick = function()
	{
		//if this item has no links, don't continue
		if(this.getElementsByTagName("a").length == 0) { return false; }

		//get definitions in this list
		var items = self.dl.getElementsByTagName("dd");
		var len = items.length;

		// Matthew's inserted separator code
		var dd = new Array(0);
		for (var i = 0; i < len; i++) if (items[i].className != separatorClassName) dd[dd.length] = items[i];
		var len = dd.length;

		//for each definition
		for(var i=0; i<len; i++) {
			//if it's this one
			if(dd[i] == this) {
				//store the value of i
				//which is the index of this option in the switcher group
				var ind = i;
				break;
			}
		}

		//save the current switcher state
		switcher.save(
			self.dl.id.replace("select_", ""), //convert id of selector into switcher ident (label)
			self.classes[ind], //get key from class array
			ind, //the index of this option in the switcher group
			self //a reference to this bodySwitcher object
			);

		//redraw the list
		self.redraw(ind, this);

		return true;
	};

	return true;
};

bodySwitcher.prototype.defineSeparator = function(sep) {
	if (typeof this.dl == "undefined") { return false; }
	var self = this;
	var item = this.dl.appendChild(switcher.create("dd"));
	item.className = separatorClassName;
	if (this.dl.childNodes.length == 2) item.id = firstIdName;
	attrs = { "text" : sep };
	link = item.appendChild(switcher.create("span", attrs));
	return true;
};


//redraw the DD items with links or selected text as appropriate
bodySwitcher.prototype.redraw = function(ind, link) {
	//get definitions in this list
	var items = this.dl.getElementsByTagName("dd");
	var len = items.length;

	// Matthew's inserted separator code
	var dd = new Array(0);
	for (var i = 0; i < len; i++) if (items[i].className != separatorClassName) dd[dd.length] = items[i];
	len = dd.length;

	//for each definition
	for(var i=0; i<len; i++) {
		//if it's the previously selected value it will only be a text node
		if(dd[i].firstChild.nodeName == "#text") {
			//remove "selected" class name from definition
			dd[i].className = this.classes[i];

			//remove the text node
			dd[i].removeChild(dd[i].firstChild);

			//and replace it with a link to reselect this style
			var attrs = { "href" : "javascript:void(\"" + this.classes[i] + "\", \"" + this.labels[i] + "\")", "text" : this.labels[i] };
			dd[i].appendChild(switcher.create("a", attrs));
		}
	}


	//we're doing this outside the previous loop
	//because the previous process needs to have run over the whole list already
	//so that the link we're sending focus to in this process definitely exists

	//if this option the last item, send focus to the first item
	//if it's any other item, send focus to the next item
	//this is because the activated link just had the focus
	//and removing a focussed element can cause
	//the page focus caret to be lost or reset to the top
	//an added benefit is that it allows you to toggle between
	//all styles in a group by repeatedly pressing enter

	// ** setting this focus causes the wrong element to be focussed, and the focus pseudo-element incorrectly shows the element as selected
	//items[(ind == len - 1 ? 0 : ind + 1)].firstChild.focus();

	//remove the link
	link.removeChild(link.firstChild);

	//add a text node label to replace the link
	link.appendChild(document.createTextNode(this.labels[ind] + this.selected));

	//add "selected" class name to definition
	dd[ind].className += " selected";
};


//update the switcher interface from programatic option changes
bodySwitcher.prototype.update = function(ind) {
	//redraw the list, if the switcher control exists
	if(typeof this.dl != "undefined") { this.redraw(ind, this.dl.getElementsByTagName("dd")[ind]); }
};
