// This script copyright Tom Rogers 2008, all rights reserved
/*
this.terms = {
	'20093' : {
		age: date,
		yrtr : '20093',
		title : 'Fall 2008',
		subjects : {
			'ACCT' : {
				courses : [],
				age: date
			},
			'BIOL' : {
				courses: [],
				age: date
			}
		}
	}
}
*/

function Viewer(config) {
	this.config = config; // tada
	this.elements = {};
	
	this.terms = {}; // list of terms & term names terms['20093'] = { yrtr: '20093', title: 'Fall 2008', subjects: {'ACCT'= { courses: [], age: date },'BIOL'=[],'PHIL'=[]}, age: 0 }
	this.currentTerm = null; // tracks current term
	this.currentSubj = null; // tracks current subj (not sure why yet)
	
	// NOTE: it's important that these initial values be the same as those indicated by the controls on the page, to avoid redundant filtering passes
	this.filters = {
		days:'n,m,t,w,h,f,s',
		conflict: false,
		subject: null,
		full: true,
		permission: true,
		
		online: true,
		offline: false,
		partialsession: true,
		lowerlevel: true,
		upperlevel: true,
		gradlevel: true,
		
		geneds: false
		};
	
	this.currentSet = null;
	
	// methods
	var viewer = this; // for closures
	var iActiveSubjectRequests = 0;
	this.setSubjectBusyStatus = function(bBusy) {
		// first, tick the number of active requests the proper way
		if(bBusy) iActiveSubjectRequests++;
		else iActiveSubjectRequests--;
		
		var eSubjectList = config.subjects;
		// second, based on the number of active requests, show or hide the loading indicator
		if(iActiveSubjectRequests > 0) {
			// remove current list items
			while(eSubjectList.firstChild)
				$Cut(eSubjectList.firstChild);
			// show busy signal
			addClass(eSubjectList, 'loading');
		}
		else removeClass(eSubjectList, 'loading'); // remove/hide busy signal
		return iActiveSubjectRequests;
	};
	
	var iActiveCourseRequests = 0;
	this.setCourseBusyStatus = function(bBusy) {
		// first, tick the number of active requests the proper way
		if(bBusy) iActiveCourseRequests++;
		else iActiveCourseRequests--;
		
		var eTable = config.list;
		// second, based on number of active requests, show or hide loading indicator
		if(iActiveCourseRequests > 0) { // show indicator
			// do not create a new indicator if there already is one
			if(viewer.elements.loadingCoursesIndicator) return iActiveCourseRequests;
			// otherwise, create one
			var eInd = $$('TD');
				eInd.colSpan = 8;
				eInd.setAttribute('colspan', 8);
			addClass(eInd, 'loading');
			var eRow = viewer.elements.loadingCoursesIndicator = $$('TR', eInd);
			var nlHeads = eTable.getElementsByTagName('thead');
			nlHeads[0].appendChild(eRow);
		} else {
			// hide indicator
			if(viewer.elements.loadingCoursesIndicator) {
				$Cut(viewer.elements.loadingCoursesIndicator);
				delete viewer.elements.loadingCoursesIndicator;
			}
		}
		
		return iActiveCourseRequests;
	};
	
	this.getDataStatus(function(bEmpty) {
		if(bEmpty) {
			alert('Course data currently unavailable. Please check back in a few minutes.\n\nContact Jeff Hundstad for further assistance:\n     jeffrey.hundstad@mnsu.edu');
			// viewer.config.terms.disabled = true;
			return this;
		}
		// populate data
		viewer.showTerms(function() {
			// afterwards, check to see if there are courseids in the URL
			var c = /[\?&]c=([^&]+)/.exec(location.search);
			if(c && c[1].length) {
				dump('root > permalink found\n');
				viewer.loadCourses(c[1]);
			}
			else
			if(config.autoload) {
				viewer.loadCourses(config.autoload);
			}
		});
		
		viewer.applyCurrentFilters();
	});
	
	return this;
}

// BASIC DATA LOADING & DISPLAYING METHODS
Viewer.prototype.getDataStatus = function(onComplete) { // determine whether the database is empty
	var viewer = this; // for closures
	dump('viewer > checking database status...\n');
	sendRequest(viewer.config.dataPath + '?action=dbstatus', { onComplete: function(X) {
		if(!X.responseXML) return die('viewer > dbstatus returned no XML!\n');
		var bEmpty = true;
		
		try {
			var nlStatus = X.responseXML.getElementsByTagName('dbstatus');
			bEmpty = (nlStatus[0].getAttribute('empty') == 'true');
		}
		catch(e) {
			dump('viewer > unable to parse dbstatus results; assuming "good"\n');
			bEmpty = false; // default to good because the DB is sound 99% of the time
		}
		
		dump('viewer > database status is ' + (bEmpty === true ? 'bad' : 'good') + '\n');
		
		// run oncomplete code
		if(typeof onComplete == 'function') onComplete(bEmpty);
	}});
}

Viewer.prototype.setTerm = function(sVal) { // updates "active" term
	dump('viewer > setting term to ' + sVal + '\n');
	
	var bSwitch = true;
	var sMsg = 'You are about to switch terms. This will clear the schedule, and any classes you have added will need to be added again manually.\n - press OK to clear the schedule and switch terms\n - press Cancel to leave everything as it is';
	
	if(this.currentTerm !== null && sVal !== this.currentTerm) { // if user is actually really switching
		if(confirm(sMsg)) {
			this.currentTerm = sVal;
			this.setSubject('');
			this.getScheduler().clear();
		} else {
			bSwitch = false;
			sVal = this.currentTerm;
		}
	}
	else this.currentTerm = sVal;
		
	// update select box to current value
	var eTerm = this.config.terms;
	for(var i = 0, imax = eTerm.childNodes.length; i < imax; i++)
		if(eTerm.childNodes[i].value == sVal)
		{
			eTerm.childNodes[i].selected = 'selected';
			// update currentTermName ("Fall 2009" instead of "20101")
			this.currentTermName = eTerm.childNodes[i].firstChild.nodeValue;
			break;
		}
	
	// update list of subjects
	if(bSwitch)
		this.showSubjects(this.currentTerm);
	
	return true;	
};

Viewer.prototype.showTerms = function(onComplete) { // populates term drop down from server (never from cache)
	dump('viewer > populating term list...');
	var viewer = this; // for closures
	// load term data
	this.getTerms(function(oTerms) {
		var eTerms = viewer.config.terms; // shorthand
		// clear out terms drop-down
		while(eTerms.firstChild)
			$Cut(eTerms.firstChild);
		
		// add new term options
		// blank item
		var eOpt = $$('OPTION', $T('SELECT TERM'));
			eOpt.setAttribute('value', '');
		eTerms.appendChild(eOpt);
		var i = 0;
		for(p in viewer.terms) {
			i++;
			eOpt = $$('OPTION', $T(viewer.terms[p].title));
			eOpt.setAttribute('value', viewer.terms[p].yrtr);
			eTerms.appendChild(eOpt);
		}
		
		// finally, perform oncomplete work if there is some
		if(typeof onComplete == 'function') onComplete();
		return true;
	});
};

Viewer.prototype.getTerms = function(onComplete) { // populates term data and returns ref to cache of terms
	var viewer = this; // for closures
	// STEP 1: request XML doc
	sendRequest(this.config.dataPath + '?action=terms', { onComplete: function(X) {
		if(!X) return die('viewer > terms return not XML!\n');
		// STEP 2: parse XML into data structure
		var nlTerms = X.responseXML.getElementsByTagName('term');
		if(!nlTerms.length) return die('viewer > no term data received\n');
		// update data cache
		for(var i = 0; i < nlTerms.length; i++) {
			var xTerm = nlTerms[i]; // shorthand
			// check to see if there is already term info
			if(viewer.terms[xTerm.getAttribute('yrtr')]) { // update existing TERM (not subject) data
				viewer.terms[xTerm.getAttribute('yrtr')].title = xTerm.text;
			} else {
				// otherwise, create new entry
				viewer.terms[xTerm.getAttribute('yrtr')] = {
					yrtr: xTerm.getAttribute('yrtr'),
					title: xTerm.text,
					subjects: {},
					age: -1 // age = -1, because there are no subjects in the list
				};
			}
		}
		
		dump('viewer > terms data loaded; ' + i + ' terms loaded\n');
		// STEP 3: run oncomplete code
		if(typeof onComplete == 'function') onComplete(viewer.terms);
		return true;
	}});
};

Viewer.prototype.showSubjects = function(sTerm) { // populates subject drop down from cache
	dump('viewer > showing subject list for ' + sTerm + '..\n');
	if(sTerm == "") return false;
	var viewer = this; // for closures
	// load course area data
	this.getSubjectsForTerm(sTerm, function(oSubjects) {
		// clear out course areas drop down
		while(viewer.config.subjects.firstChild)
			$Cut(viewer.config.subjects.firstChild);
		
		// add new course area options
		for(p in oSubjects) {
			var eLabel = $$('LABEL', $T(oSubjects[p].title));
				eLabel.setAttribute('for', 'cb_' + sTerm + '_' + p);
				eLabel.title = oSubjects[p].name;
			var eCheck = $$('INPUT');
				eCheck.setAttribute('type', 'checkbox');
				eCheck.setAttribute('value', p);
				eCheck.setAttribute('id', 'cb_' + sTerm + '_' + p);
				eCheck.title = oSubjects[p].name;
			var eRow = $$('span', eCheck, eLabel);
			addClass(eRow, 'option');
			viewer.config.subjects.appendChild(eRow);
		}
	});
	dump('viewer > done showing subjects\n');
	return true;
};

Viewer.prototype.getSubjectsForTerm = function(sTerm, onComplete) { // loads subject list from cache; refreshes cache first if necessary
	dump('viewer > loading course areas for ' + sTerm + '...\n');
	var viewer = this; // for closures
	// if current subject data is too old, load new data
	if(!this.terms[sTerm].subjects || this.terms[sTerm].age && (new Date() - this.terms[sTerm].age > this.config.maxAge)) {
		// STEP 1: request XML doc
		dump('viewer > no subject data or data too old: fetching subject list for term "' + sTerm + '"...\n');
		this.setSubjectBusyStatus(true);
		sendRequest(this.config.dataPath + '?action=subjects&term=' + sTerm, { onComplete: function(X) {
			viewer.setSubjectBusyStatus(false);
			if(!X.responseXML) return die(' ----- RESPONSE NOT XML -----\n' + X.responseText + '\n ----------------------------\n');
			
			// STEP 2: parse XML into data structure
			var nlAreas = X.responseXML.getElementsByTagName('subject');
			if(!nlAreas.length) return die('viewer > no subject data received\n');
			
			var oSubjects = viewer.terms[sTerm].subjects;
			for(var i = 0; i < nlAreas.length; i++) {
				var xS = nlAreas[i]; // shorthand
				
				// check to see if this subject already exists in the subject list
				if(xS.getAttribute('symbol') in oSubjects) {
					// yes; update name & title text
					oSubjects[xS.getAttribute('symbol')].name = xS.text;
					oSubjects[xS.getAttribute('symbol')].title = xS.getAttribute('title');
				}
				else
					// no; create entry
					oSubjects[xS.getAttribute('symbol')] = { name: xS.text , title: xS.getAttribute('title')};
			}
			
			// finally, update age in cache
			viewer.terms[sTerm].age = new Date(); // update freshness
			
			dump('viewer > subjects collection updated\n');
			
			// STEP 3: run oncomplete code
			if(typeof onComplete == 'function') onComplete(oSubjects);
			return true;
		}});
		return true;
	}
	
	// if data is fresh enough, return data from cache
	onComplete(this.terms[sTerm].subjects);
	return true;
};

Viewer.prototype.setSubject = function(sSubjects, onComplete) { // updates "active" subject(s)
	dump('viewer > setting subject filter to "' + sSubjects + '"...\n');
	var viewer = this;
	// STEP 0: update the list of boxes that are checked
	setTimeout(function() {
		var aSubs = sSubjects.split(',');
		for(var i = 0, imax = aSubs.length; i < imax; i++) {
			var e = $('cb_' + viewer.currentTerm + '_' + aSubs[i]);
			if(e) e.checked = true;
		}
		}, 100);
	
	// STEP 1: update the subject filter
	var bShouldReapply = this.setFilter('subject', sSubjects.toLowerCase());
	// STEP 2: make sure the course data that should be visible is actually present (and fresh)
	this.populateCourseTable(sSubjects, this.currentTerm, function(bNewData) {
		dump('viewer > course table populated\n');
		// STEP 3: reapply the filter to current set if either filter or dataset have changed
		if(bShouldReapply || bNewData) viewer.applyCurrentFilters();
		
		// STEP 4: run oncomplete if there is one
		if(typeof onComplete == 'function') onComplete();
	});
};

Viewer.prototype.populateCourseTable = function(sSubjects, sTerm, onComplete) { // makes sure that TERM/COURSE chunks are present and fresh in table
	dump('viewer > populating course table...\n');
	if(!sTerm || !sTerm.length) var sTerm = this.currentTerm;
	if(!sSubjects || !sSubjects.length) {
		setDirs(true);
		if(typeof onComplete == 'function') {
			onComplete(false);
			return;
		}
	}
	
	setDirs(false);
	var aSubjects = sSubjects.trim().toLowerCase().split(',').sort();
	var viewer = this; // for closures
	
	var aSubjectsToFetch = [];
	
	// for each specified subject, check to see if cached version is good enough
	dump('viewer > checking cache for [' + sTerm + '/' + sSubjects + ']...\n');
	for(var i = 0; i < aSubjects.length; i++) {
		var oSubject = this.terms[sTerm].subjects[aSubjects[i]];
		if(oSubject && oSubject.age && (new Date() - oSubject.age <= this.config.maxAge)) continue; // cache is fresh!
		aSubjectsToFetch.push(aSubjects[i]); // subject data didn't pass test, so add it to fetch list
	}
	
	// if no subjects need to be fetched, continue work, and notify that no data is available
	// NOTE: no further work needs to be done if cache is fresh, because all cached data will already be rendered
	if(!aSubjectsToFetch.length) {
		if(typeof onComplete == 'function') {
			dump('viewer > no course data needs to be fetched; continuing...\n');
			onComplete(false); return;
		} else return false;
	}
	
	// OTHERWISE, update cache before actually populating XHTML table
	this.getCoursesBySubject(aSubjectsToFetch.join(','), sTerm, function(aCourses) {
		// NOTE: this is how the data is actually placed in the page
		// STEP 0: prep work
		var eDataTable = viewer.config.list; // ref to table
		var nlSubjectChunks = eDataTable.getElementsByTagName('TBODY');
		
		var aSubjectList = []; // will contain a list of namespaced term/subjects for all the courselists that actually exist in HTML table
		var cChunks = {}; // will contain handles to each of the individual courselist tbodies, both those in doc and those that are about to be created
		for(var i = 0; i < nlSubjectChunks.length; i++) { // populate data stores with data from existing HTML table
			aSubjectList.push(nlSubjectChunks[i].subject);
			cChunks[nlSubjectChunks[i].subject] = nlSubjectChunks[i];
		}
		
		// loop through each of the subjects for which there is new data and update/add its page content
		for(var i = 0; i < aSubjectsToFetch.length; i++) {
			// STEP 1: remove from table any existing tbodies that are about to be obsoleted by new data
			for(var j = 0; j < nlSubjectChunks.length; j++)
				if(nlSubjectChunks[j].subject == sTerm + '/' + aSubjectsToFetch[i])
					$Cut(nlSubjectChunks[j]);
				
			// STEP 2: create new tbody
			var eSubjectChunk = viewer.renderSubjectChunk(viewer.terms[sTerm].subjects[aSubjectsToFetch[i]]);
				eSubjectChunk.subject = sTerm + '/' + aSubjectsToFetch[i]; // populate subject value for later
			
			// STEP 3: add tbody to temporary data stores for sorting & insertion
			if(aSubjectList.indexOf(eSubjectChunk.subject) == -1) aSubjectList.push(eSubjectChunk.subject);
			cChunks[eSubjectChunk.subject] = eSubjectChunk; // will overwrite pointers for old chunks; this is good, b/c old chunks were removed from doc a few lines ago
		}
		
		// STEP 4: add any new chunks to the table
		// determine correct order for all chunks (those in memory and those in doc)
		aSubjectList.sort();
		
		// simply append to the table every chunk in new order; elements that are already present are implicitly moved to new location
		for(var i = 0; i < aSubjectList.length; i++)
			eDataTable.appendChild(cChunks[aSubjectList[i]]); // this compact syntax is made possible by existence of cChunks assoc. array
		
		// FINALLY, call onComplete function
		if(typeof onComplete == 'function') onComplete(true); // pass TRUE, because new course data was requested
		return;
	});
};

Viewer.prototype.getCoursesBySubject = function(sSubj, sTerm, onComplete) { // returns array of course objects for given term & subject(s)
	dump('viewer > course data missing or old; fetching "' + sSubj + '" ...\n');
	var viewer = this;
	this.setCourseBusyStatus(true);
	// STEP 1: request XML doc from server
	dump('>> [' + this.config.dataPath + '?action=courses&term=' + sTerm + '&subject=' + sSubj + ']\n');
	sendRequest(this.config.dataPath + '?action=courses&term=' + sTerm + '&subject=' + sSubj, { onComplete: function(X) {
		viewer.setCourseBusyStatus(false);
		// ### perhaps put some kind of notification here for the user if response wasn't XML
		// STEP 2: parse each subject area into data structure
		var nlSubjects = X.responseXML.getElementsByTagName('courselist');
		if(!nlSubjects.length) return die('viewer > no course data received!\n');
		dump('viewer > ' + nlSubjects.length + ' subjects returned\n');
		// treat each subject separately, because caching should occur on a single-subject basis
		for(var S = 0, Smax = nlSubjects.length; S < Smax; S++) {
			dump('viewer > parsing subject "' + nlSubjects[S].getAttribute('subject') + '"...\n');
			var nlCourses = nlSubjects[S].getElementsByTagName('course');
			if(!nlCourses.length) continue;
			
			var aCourses = []; // array for new data
			// loop through all the courses and process them
			for(var i = 0, imax = nlCourses.length; i < imax; i++) {
				// extract course data into the course layer
				var xC = nlCourses[i]; // shorthand
				var c = new Course(XML2Course(xC));
				c.viewer = viewer;
				// process this course's sections
				var nlSections = xC.getElementsByTagName('section');
				for(var j = 0, jmax = nlSections.length; j < jmax; j++) {
					var xS = nlSections[j]; // shorthand
					var s = new Section(XML2Section(xS));
						s.course = c; // attach upward reference
					// process this section's meetings
					var nlMeetings = xS.getElementsByTagName('meeting');
					for(var k = 0, kmax = nlMeetings.length; k < kmax; k++) {
						var xM = nlMeetings[k]; // shorthand
						var m = new Meeting(XML2Meeting(xM));
							m.section = s; // attach upward reference
						// add this meeting to the section
						s.meetings.push(m);
					}
					// grab notes for this section
					var nlNotes = xS.getElementsByTagName('note');
					for(var k = 0, kmax = nlNotes.length; k < kmax; k++) {
						var xN = nlNotes[k]; // shorthand
						var n = XML2Note(xN);
						// add this note to the section
						s.notes.push(n);
						// if this note is a permission required note, mark that on the section itself
						if(n.type == 'permission')
							s.needpermission = true;
					}
					// add this section to the course
					c.sections.push(s);
				}
				// finally, add this course to the list
				aCourses.push(c);
			}
			
			// pull department note(s) and add them to the subject structure
			var nlDNotes = nlSubjects[S].getElementsByTagName('dnote');
			var aDNotes = [];
			for(var i = 0, imax = nlDNotes.length; i < imax; i++)
				aDNotes.push(XML2DeptNote(nlDNotes[i]));
			dump('viewer > ' + imax + ' department notes parsed\n');
			
			// now that this subject is parsed, replace old course data with new, timestamped data
			dump('viewer > caching courses (' + aCourses.length + ' items)\n');
			viewer.terms[sTerm].subjects[nlSubjects[S].getAttribute('subject').trim().toLowerCase()] = {
				age: new Date(),
				term: sTerm,
				title: nlSubjects[S].getAttribute('title'),
				symbol: nlSubjects[S].getAttribute('subject').trim().toUpperCase(),
				notes: aDNotes,
				courses: aCourses
				};
		}
		var aCourses = [];
		var aSubjects = sSubj.split(','); // NOTE: this is keyed to the list of subjects that was requested, not the list that was received
		
		for(var i = 0; i < aSubjects.length; i++)
			aCourses = aCourses.concat(viewer.terms[sTerm].subjects[aSubjects[i].trim().toLowerCase()].courses);
		dump('viewer > executing oncomplete...\n');
		
		if(typeof onComplete == 'function') onComplete(aCourses);
	}});
};

Viewer.prototype.renderSubjectChunk = function(oSubject) { // given a single subject object (from cache), create a TBODY containing that data
	// STEP 3: create new course area stuff inside table
	dump('viewer > rendering subject chunk\n');
	var aCourses = oSubject.courses;
	
	var eBody = $$('TBODY');
	// create subject area row
	var eCell1 = $$('TH', $T(oSubject.title + ' (' + oSubject.symbol.toUpperCase() + ')'));
		eCell1.setAttribute('colspan', 7);
		eCell1.colSpan = 7; // for IE
	
	var eHideText = $$('SPAN', $T('Hide this subject'));
	var eHideLink = $$('A', eHideText);
		eHideLink.id = 'hide-' + oSubject.symbol.toLowerCase();
		eHideLink.title = 'Hide this subject';
	var eCell2 = $$('TH', eHideLink);
		addClass(eCell2, 'subjectcut');
		
	var eRow = $$('TR', eCell1, eCell2);
	addClass(eRow, 'subject-heading');
	eBody.appendChild(eRow);
	
	// render department notes here
	for(var i = 0, imax = oSubject.notes.length; i < imax; i++) {
		var eNoteRow = $$('TR', $$('TD', $T(oSubject.notes[i].note)));
			eNoteRow.firstChild.colSpan = 8;
			addClass(eNoteRow, 'deptnote');
		eBody.appendChild(eNoteRow);
	}
	
	var sCurrentSessionType = null;
	for(var i = 0; i < aCourses.length; i++) {
		var c = aCourses[i]; // shorthand
		// before rendering course, check to see if session type has changed
		if(sCurrentSessionType !== c.sessionType) {
			// dump('viewer > new session type: ' + c.sessionType + '\n');
			// type has changed, so render a new heading
			var eSessionHeader = $$('TR', $$('TD', $T(c.sessionType)));
			eSessionHeader.firstChild.colSpan = 8;
			addClass(eSessionHeader, 'sessiontype-heading');
			eBody.appendChild(eSessionHeader);
			sCurrentSessionType = c.sessionType; // update current session type
			
			// create local column headings
			var eSection =		$$('TH', $T('Section')); eSection.colSpan = 2;	addClass(eSection, 'hdg-section');
			var eDays = 		$$('TH', $T('Days'));							addClass(eDays, 'hdg-days');
			var eTimes =		$$('TH', $T('Times'));							addClass(eTimes, 'hdg-times');
			var eDates =		$$('TH', $T('Dates'));							addClass(eDates, 'hdg-dates');
			var eLocation =		$$('TH', $T('Location'));						addClass(eLocation, 'hdg-location');
			var eInstructor =	$$('TH', $T('Instructor'));						addClass(eInstructor, 'hdg-instructor');
			var eCapacity =		$$('TH', $T('Capacity'));						addClass(eCapacity, 'hdg-capacity');
			var eHeadings =		$$('TR', eSection, eDays, eTimes, eDates, eLocation, eInstructor, eCapacity);
				addClass(eHeadings, 'headings');
			eBody.appendChild(eHeadings);
		}
		
		// create the course heading row
		eBody.appendChild(c.render());
		// now, create section rows
		for(var j = 0; j < c.sections.length; j++) {
			var s = c.sections[j];
			// extra bookmarks for meeting #0, b/c it shares section row
			var sectionRow = s.render();
			eBody.appendChild(sectionRow);
			
			// finally, create meeting rows
			if(s.meetings.length > 1)
				for(var k = 1; k < s.meetings.length; k++) {
					var m = s.meetings[k]; // shorthand
					var mSA = $$('TD');															addClass(mSA, 'quickadd');
					var mSB = $$('TD');															addClass(mSB, 'section');
					var mSC = $$('TD', $T(m.days.replace(new RegExp('( )', 'g'), '\u00a0')));	addClass(mSC, 'days');
					var mSD = $$('TD', $T(Mil2Civ(m.starttime) + ' - ' + Mil2Civ(m.endtime)));	addClass(mSD, 'times');
					var mSE = $$('TD', $T(m.startdate + ' - ' + m.enddate));					addClass(mSE, 'dates');
					var mSF = $$('TD', $T(m.location));											addClass(mSF, 'location');
					var mSG = $$('TD', $T(m.instructor));										addClass(mSG, 'instructor');
					var mSH = $$('TD');															addClass(mSH, 'capacity');
					var eMeetingRow = $$('TR', mSA, mSB, mSC, mSD, mSE, mSF, mSG, mSH);
						eMeetingRow._meeting = m;
						eMeetingRow.title = 'Click for more information';
						m.elements = { row: eMeetingRow };
					addClass(eMeetingRow, 'section-info');
					
					if(s.isFull()) addClass(eMeetingRow, 'section-status-closed');
					if(!s.permission())  addClass(eMeetingRow, 'section-status-permission');
					eBody.appendChild(eMeetingRow);
				}
		}
		// attach hover behavior to tbody thing
		attachBehavior({ viewer: this, chunk: eBody, subject: oSubject.symbol, term: oSubject.term });
		
	}
	return eBody;
};

function attachBehavior(options) {
	if(!options || !('viewer' in options) || !('chunk' in options)) return die('unable to attach behavior to chunk!\n');
	
	// onclick = show popup
	options.chunk.onclick = function(event) {
		if(!event) var event = window.event; if(event.target && !event.srcElement) event.srcElement = event.target;
		event.cancelBubble = true; if(event.stopPropagation) event.stopPropagation();
		
		// STEP 0: handle "subjectcut" links first
		if(event.srcElement.id.indexOf('hide-') === 0) {
			var eCheck = $('cb_' + options.term + '_' + options.subject);
			if(!eCheck) return false;
			eCheck.checked = false;
			setTimeout(function() {
				options.viewer.setSubject(getCheckedValues($('subjects')));
				setTimeout(function() { options.viewer.applyCurrentFilters(); }, 100);
			}, 100);
			return false;
		}
		
		// STEP 1: identify the section that owns the srcElement (and check to see if click was within quickadd)
		var e = event.srcElement, bQuickAdd = false;
		while(!(e._section || e._meeting) && e.parentNode) {
			e = e.parentNode; if(hasClass(e, 'quickadd')) bQuickAdd = true;
		}
		if(!e._section && !e._meeting) return die('viewer > click not over a section object.\n');
		var oSection = e._section ? e._section : e._meeting.section;
		dump('viewer > user clicked on section ' + oSection.courseid + '\n');
		
		// quickadd interrupt: if user clicked the quickadd button, immediately add course and do not create popup
		if(bQuickAdd) {
			dump('viewer > quickadd detected: skipping popup\n');
			options.viewer.getScheduler().acceptSection(oSection);
			if(options.viewer.filters.conflict === false)
				options.viewer.applyCurrentFilters();
			return true;
		}
		
		// STEP 2: create the popup
		var ePopup = options.viewer.createSectionPopup(oSection);
		
		// STEP 3: add close behavior
		ePopup.old = { onmousemove: document.onmousemove, onmousedown: document.onmousedown };
		document.onmousemove = document.onmousedown = function(event) {
			if(!event) var event = window.event; if(event.target && !event.srcElement) event.srcElement = event.target;
			// NOTE: capturing taken out to allow title text to pop up on elements
			// e.cancelBubble = true; if(e.stopPropagation) e.stopPropagation();
			
			// determine if the element that was mouseout-ed was inside the popup, or the popup itself
			var o = event.srcElement;
			while(!o._isPopup && o.parentNode)
				o = o.parentNode;
			if(o._isPopup) return (ePopup._bMouseCaptured = true);
			else if(event.type != 'mousedown' && !ePopup._bMouseCaptured) return;
			
			// destroy the popup!!!
			ePopup.fDestroy();
		};
		
		// STEP 4: add popup and position it near the mouse
		ePopup._parent.appendChild(ePopup);
		var cursor = getCursorPosition(event);
		var iTop = Math.ceil(cursor.y - 235 - 10);
		var iLeft = Math.ceil(cursor.x - 205);
		
		// make sure the popup appears ON SCREEN, based on current scroll position
		var iTopA = 0, iTopB = 0;
		try { iTopA = document.documentElement.scrollTop; } catch(e) {}
		try { iTopB = document.body.scrollTop; } catch(e) {}
		var iLow = Math.max(iTopA, iTopB);
		
		var iHigh = iLow + getScreenSize().height;
		
		ePopup.style.top = Math.max(iLow, Math.min(iTop, iHigh)) + 'px';
		ePopup.style.left = iLeft + 'px';
		
		return false;
	};
	
	return;
}

Viewer.prototype.createSectionPopup = function(oSection) {
	if(!oSection) return die('viewer > cannot create section popup - no section object!\n');
	var viewer = this; // for closures
	
	// STEP 1: if there is already a popup, destroy it
	if(this.elements.sectionPopup) {
		$Cut(this.elements.sectionPopup);
		delete this.elements.sectionPopup;
		dump('viewer > existing popup destroyed\n');
	}
	
	// STEP 2: create popup
	// HEADER
	var eSectionID = $$('SPAN', $T('CourseID: ' + oSection.courseid)); addClass(eSectionID, 'sectionid');
	var eTitle = $$('P', eSectionID, $T(oSection.course.title));
	addClass(eTitle, 'title');
	
	var eCNum = $$('SPAN', $T(oSection.course.num));
	var eSNum = $$('SPAN', $T(oSection.section));
	var eCredits = $$('SPAN', $T('(' + oSection.course.credits + ' credits)'));
	var eIdent = $$('P', eCNum, $T('-'), eSNum, $T(' '), eCredits);
	addClass(eIdent, 'ident');
	
	var eHeader = $$('DIV', eTitle, eIdent);
	addClass(eHeader, 'header');
	
	// meeting data
	var eMeetings = $$('DIV');
	addClass(eMeetings, 'meetings');
	for(var i = 0, imax = oSection.meetings.length; i < imax; i++) {
		var meeting = oSection.meetings[i]; // shorthand
		eMeetings.appendChild($$('P', $$('SPAN', $T(meeting.days.replace(new RegExp('( )', 'g'), '\u00a0'))), $T(' ' + Mil2Civ(meeting.starttime) + ' - ' + Mil2Civ(meeting.endtime))));
	}
	
	// BASIC COURSE DATA
	// first, values
	// combine all meeting instructors into a single value
	var aInstructors = [];
	for(var i = 0; i < oSection.meetings.length; i++)
		aInstructors.push(oSection.meetings[i].instructor);
	var sInstructorList = aInstructors.join('; ');
	// calculate grading method string
	switch(oSection.gradingmethod.toUpperCase()) {
		case 'GR': var sGradingMethod = 'Available for grade only'; break;
		case 'PN': var sGradingMethod = 'Available for pass / no-credit only'; break;
		case 'CU': var sGradingMethod = 'Continuing Education'; break;
		case 'OPT': var sGradingMethod = 'Grade or Pass / No-credit'; break;
		default: oSection.gradingmethod;
	}
	
	var eAddImg = $$('IMG');
		eAddImg.setAttribute('src', 'images/clear.png');
	var eAdd = $$('A', eAddImg);
		eAdd.setAttribute('title', 'Add to Schedule');
		eAdd.setAttribute('alt', 'Add to Schedule');
	addClass(eAdd, 'add-button');
	
	var eInstructor = $$('P', $$('LABEL', $T('Instructor:')), $T(' ' + sInstructorList));
	var eGradingMethod = $$('P', $$('LABEL', $T('Grading Method:')), $T(' ' + sGradingMethod));
	var eDetails = $$('DIV', eAdd, eMeetings, eInstructor, eGradingMethod);
	addClass(eDetails, 'details');
	
	// NOTES
	var aIncludedNotes = [];
	var eNotes = $$('DIV');
	addClass(eNotes, 'notes');
	
	for(var i = 0; i < oSection.notes.length; i++) {
		var note = oSection.notes[i]; // shorthand
		
		// do test to remove redundant notes
		if(aIncludedNotes.indexOf(note.message + '|' + note.url) === -1)
			aIncludedNotes.push(note.message + '|' + note.url);
		else continue;
		
		if(note.url && note.url.length) {
			var _m = (note.message.length >= 70) ? note.message.substr(0, 67) + '...' : note.message;
			var eText = $$('A', $T(_m));
			eText.href = note.url;
			eText.target = '_blank';
		}
		else {
			var _m = (note.message.length >= 70) ? note.message.substr(0, 67) + '...' : note.message;
			var eText = $T(_m);
		}
		var eNote = $$('P', eText);
			eNote.title = note.message;
		// addClass(eNote, 'info');
		if(note.type == 'permission')
			addClass(eNote, 'permission');
		eNotes.appendChild(eNote);
	}
	
	var eContents = $$('DIV', eHeader, eDetails, eNotes);
	addClass(eContents, 'wrap');
	var ePopup = this.elements.sectionPopup = $$('DIV', eContents);
		ePopup._isPopup = true;
	addClass(ePopup, 'course-popup');
	
	// add bookmarks
	ePopup.section = oSection;
	ePopup.header = eHeader;
	ePopup.details = eDetails;
	ePopup.notes = eNotes;
	ePopup._parent = viewer.config.list.parentNode; // this is the element the popup should be attached to
	ePopup._bMouseCaptured = false;
	
	// STEP 3: define behaviors, and attach them to things
	ePopup.fAddFunction = function(event) {
		dump('viewer > adding section!\n');
		if(!event) var event = window.event; if(event.target && !event.srcElement) event.srcElement = event.target;
		event.cancelBubble = true; if(event.stopPropagation) event.stopPropagation();
		viewer.getScheduler().acceptSection(oSection);
		eAdd.onclick = ePopup.fRemoveFunction;
		addClass(eAdd, 'add-button-remove');
		eAdd.setAttribute('title', 'Remove Course');
		eAdd.setAttribute('alt', 'Remove Course');
		
		if(viewer.filters.conflict === false)
			viewer.applyCurrentFilters();
		
		ePopup.fDestroy(); // close popup when adding course
		
		return false;
	};
	
	ePopup.fRemoveFunction = function(event) {
		dump('viewer > removing section!\n');
		if(!event) var event = window.event; if(event.target && !event.srcElement) event.srcElement = event.target;
		event.cancelBubble = true; if(event.stopPropagation) event.stopPropagation();
		viewer.getScheduler().removeSection(oSection.courseid);
		eAdd.onclick = ePopup.fAddFunction;
		removeClass(eAdd, 'add-button-remove');
		eAdd.setAttribute('title', 'Add to Schedule');
		eAdd.setAttribute('alt', 'Add to Schedule');
		
		if(viewer.filters.conflict === false)
			viewer.applyCurrentFilters();
		return false;
	};
	
	ePopup.fDestroy = function() {
		$Cut(ePopup);
		delete viewer.elements.sectionPopup;
		if(ePopup.old)
			for(p in ePopup.old) document[p] = ePopup.old[p];
		return true;
	};
	
	// finally, attach proper behavior and style, based on whether section is already added
	if(viewer.getScheduler().hasSection(oSection.courseid)) {
		eAdd.onclick = ePopup.fRemoveFunction;
		addClass(eAdd, 'add-button-remove');
		eAdd.setAttribute('title', 'Remove Course');
		eAdd.setAttribute('alt', 'Remove Course');
	}
	else eAdd.onclick = ePopup.fAddFunction;
	
	return ePopup;
};

Viewer.prototype.getSectionById = function(sCourseID) { // returns a reference to a section object, identified by course id
	// iterate through every course in every subject area for every term in the cache, looking for the section by sectionid
	for(var t in this.terms) {
		var term = this.terms[t]; // shorthand
		for(var s in term.subjects) {
			var subject = term.subjects[s]; // shorthand
			if(!subject.courses) continue; // skip subjects for which there is no data
			for(var i = 0; i < subject.courses.length; i++) {
				var oSection = subject.courses[i].getSectionById(sCourseID);
				if(oSection) return oSection;
			}
		}
	}
	
	return false;
};

Viewer.prototype.loadCourses = function(string) { // loads a set of courses into the scheduler by default
	dump('viewer > beginning autoload sequence "' + string + '"...\n');
	var viewer = this; // for closures
	
	// STEP 1: pass the incoming string to the server for parsing and planning
	sendRequest(viewer.config.dataPath + '?action=autoload&c=' + string, { onComplete: function(X) {
		// STEP 2: perform data retrieval based on information sent by server
		// term
		dump('viewer > response back; setting term...\n');
		var nlTerms = X.responseXML.getElementsByTagName('term');
		if(!nlTerms.length) return die('viewer > ERROR: no term identified!\n');
		viewer.setTerm(nlTerms[0].text);
		
		// subject(s)
		dump('viewer > loading identified course areas...\n');
		var nlSubjects = X.responseXML.getElementsByTagName('subject');
		if(!nlSubjects.length) return die('viewer > ERROR: no subjects identified!\n');
		var aSubs = [];
		for(var i = 0, imax = nlSubjects.length; i < imax; i++)
			aSubs.push(nlSubjects[i].text);
		var sSubs = aSubs.join(',');
		
		viewer.setSubject(sSubs, function() {
			// STEP 3: add the specific courses identified
			dump('viewer > auto-adding identified sections...\n');
			
			var nlCIDs = X.responseXML.getElementsByTagName('cid');
			if(!nlCIDs.length) return die('viewer > ERROR: no courseids identified!\n');
			for(var i = 0, imax = nlCIDs.length; i < imax; i++) {
				var sCourseID = nlCIDs[i].text;
				// presumably unnecessary safari fix
				if(sCourseID == undefined) sCourseID = nlCIDs[i].firstChild.nodeValue;
				
				var oSection = viewer.getSectionById(sCourseID);
				if(oSection.courseid == 'undefined') {
					dump('---- UNDEFINED! -----\n' + X.responseText + '--------------\n');
				}
				dump('viewer > adding section "' + oSection.courseid + '"...\n')
				if(!oSection) {
					dump('viewer > unable to locate section "' + sCourseID + '"; skipping\n');
					continue;
				}
				viewer.getScheduler().acceptSection(oSection);
			}
			
			// this is important
			viewer.applyCurrentFilters();
			return true;
		});
		return;
	}});
	return;
};

// DATA FILTERING METHODS
Viewer.prototype.setFilter = function(sDomain, sValue) { // accepts a domain and value, and returns boolean indicating whether new value is different
	if(!sDomain || !sDomain.length) return die('viewer > setfilter failed: no domain!\n'); // you can't set a domainless filter
	var sOldValue = this.filters[sDomain]; // store old value temporarily
	this.filters[sDomain] = sValue; // update with new value
	// now compare values to see if there was a change and return that value
	return (sOldValue != sValue);
};

Viewer.prototype.setWeekdayFilter = function(sBadDays) { // accepts a comma-delimited string of weekday abbrevs
	dump('viewer > setting weekday filter to "' + sBadDays + '"...\n');
	var bShouldReapply = this.setFilter('days', sBadDays); // update filter value
	// if filter has actually changed, reapply it
	if(bShouldReapply) this.applyCurrentFilters();
	return true;
};

Viewer.prototype.setOtherFilter = function(sFilterName, sValue) { // handles values from other options
	dump('viewer > setting other filter:' + sFilterName + '=' + sValue + '\n');
	var bShouldReapply = this.setFilter(sFilterName, sValue);
	if(bShouldReapply) this.applyCurrentFilters();
	return true;
};

Viewer.prototype.applyCurrentFilters = function() {
	dump('viewer > applying current filter set...\n');
	for(p in this.filters) dump('  ' + p + ': ' + this.filters[p] + '\n');
	
	// STEP 1: hide/show tbodies based on term & subject filters
	var nlChunks = this.config.list.getElementsByTagName('TBODY');
	for(var i = 0; i < nlChunks.length; i++) {
		var bShow = true;
		if(!nlChunks[i].subject) continue; // fix for IE
		var aTS = nlChunks[i].subject.split('/');
		// is in same term?
		if(aTS[0] != this.currentTerm) bShow = false;
		// is this subject in list of subjects to view?
		if(this.filters.subject.split(',').indexOf(aTS[1]) < 0) bShow = false;
		dump('  show ' + nlChunks[i].subject + ' ? ' + bShow + '\n');
		nlChunks[i].style.display = bShow ? '' : 'none';
	}
	dump('viewer > done filtering subjects\n');
	
	// STEP 2: now, iterate though active term's visible subjects and apply other filters to those courses
	if(!this.filters.subject) return die('viewer > no more filtering necessary: no subjects selected.\n');
	
	/*
	** if the filter being set is "conflict", the "true" value should not be "true", it should
	** be some kind of data structure representing the user's current schedule (or the free time therein)
	** NOTE: this calculation MUST NOT be done on a per-section basis, but it also must not be done
	** simply at the time of setting, because the user might (will!) change their schedule after checking
	** the box, and this ought to take into account those schedule changes
	*/
	
	var oldConflictFilter = this.filters.conflict;
	if(!this.filters.conflict) // user has unchecked the "conflict" box
		this.filters.conflict = this.getScheduler().determineAvailability();
	
	// for gened filters, combine separate datastructures into one that can be handled by session's methods
	
	var oldGenedFilter = this.filters.geneds;
	if(this.filters.geneds) { // if user is doing gened filters, copy selected terms into structure
		this.filters.geneds = {};
		var aTerms = oldGenedFilter.split(',');
		for(var i = 0; i < aTerms.length; i++)
			this.filters.geneds[aTerms[i]] = GenEds[aTerms[i]];
		// at this point, the gened filter will contain only the lists from the currently-checked bulletins (instead of all lists that have been downloaded)
	}
	
	var aSubjects = this.filters.subject.split(',');
	
	var aCourses = [];
	for(var i = 0; i < aSubjects.length; i++)
		aCourses = aCourses.concat(this.terms[this.currentTerm].subjects[aSubjects[i]].courses);		
		
	dump('viewer > preparing to filter ' + aCourses.length + ' courses...\n');
	
	// iterate
	for(var i = 0; i < aCourses.length; i++) {
		var c = aCourses[i]; // shorthand
		// assume each course should be hidden (unless at least one section survives filters)
		var bShowCourse = false;
		
		// dump('viewer > preparing to filter ' + c.num + '\'s ' + c.sections.length + ' sections: [');
		
		// apply filters to sections, not courses
		for(var j = 0; j < c.sections.length; j++) {
			var s = c.sections[j]; // shorthand
			// assume each section should be shown, until proven otherwise
			var bShowSection = true;
			// apply each filter by assuming filter name corresponds to a method of section object, that will return false if section fails test
			for(filterName in this.filters) {
				if(filterName == 'subject') continue; // ignore the SUBJECT filter, as it is handled by above code
				if(typeof s[filterName] == 'function') { // check to make sure filter exists as method
					if(s[filterName](this.filters[filterName]) === false) bShowSection = false;
				}
				else dump('viewer > filter "' + filterName + '" has no matching section method\n');
			}
			// at this point, set style on meetings using bShowSection
			for(var k = 0; k < s.meetings.length; k++)
				s.meetings[k].elements.row.style.display = (bShowSection) ? '' : 'none';
			
			// dump( (bShowSection)?'+':'-' );
			
			if(bShowSection) bShowCourse = true;
		}
		// at this point, set style on course based on whether any of its sections are visible
		c.elements.row.style.display = (bShowCourse) ? '' : 'none';
		// dump('] ' + ((bShowCourse)?'+':'-') + '\n');
	}
	
	if(!oldConflictFilter) this.filters.conflict = false; // this line is important
	if(oldGenedFilter) this.filters.geneds = oldGenedFilter;
	dump('\n');
	return true;
};

// DATA TYPE DEFINITIONS
function Course(props) {
	this.num = props.num; // "PHIL100"
	var aParts = /^([A-Z]{2,4})(\d{3})/.exec(props.num);
	this.number = { 'department': aParts[1], 'number': parseFloat(aParts[2]) };
	this.title = props.title; // "Intro to Philosophy"
	this.credits = props.credits; // "3.00" (string)
	this.sessionType = props.sessionType; // "10" (string)
	this.sections = [];
	// if there are sections, load them into the section array
	if('sections' in props)
		for(var i = 0; i < props.sections.length; i++)
			this.sections.push(new Section(props.sections[i]));
	return this;
}

Course.prototype.render = function() { // creates XHTML rep of item; populates elements collection;
	var sCredits = this.credits.indexOf('.') === 0 ? '0' : this.credits;
	var eCourseCell = $$('TH', $T(this.num + ' - ' + this.title + ' (' + sCredits + ' credits)'));
		eCourseCell.setAttribute('colspan', 8);
		eCourseCell.colSpan = 8;
	addClass(eCourseCell, 'courseheading');
	var eCourseRow = $$('TR', eCourseCell);
		eCourseRow._course = this; // for DOM-based access
		this.elements = { row: eCourseRow }; // for data-based access
	if(this.isFull()) addClass(eCourseRow, 'course-status-closed');
	return eCourseRow;
};

Course.prototype.isFull = function() { // returns true IFF all sections are at or above capacity
	for(var i = 0; i < this.sections.length; i++)
		if(!this.sections[i].isFull()) return false;
	return true;
};

Course.prototype.getSectionById = function(sCourseID) { // yes, the names are idiosyncratic; get over it
	if(!sCourseID) return false;
	for(var i = 0; i < this.sections.length; i++)
		if(this.sections[i].courseid == sCourseID) return this.sections[i];
	return false;
};


function Section(props) {
	// copy specific values out of props into this object
	this.course = null;
	this.courseid = props.courseid; // "001234"
	this.section = props.section; // "01"
	this.gradingmethod = props.gradingmethod; // "GR"
	this.capacity = props.capacity; // "35"
	this.seated = props.seated; // "12"
	this.status = props.status; // "open" vs. "closed"
	this.isonline = (props.online == '1') ? true : false;
	this.needpermission = false;
	this.meetings = [];
	this.notes = [];
	// if there are meetings attached to this section, load them into the meeting array
	if('meetings' in props)
		for(var i = 0; i < props.meetings.length; i++)
			this.meetings.push(new Meeting(props.meetings[i]));
	return this;
}

Section.prototype.render = function() { // creates XHTML rep of item; populates elements election
	var eSA = $$('TD', $$('A'));															addClass(eSA, 'quickadd');
	var eSB = $$('TD', $T(this.section));													addClass(eSB, 'section');
	//var eSC = $$('TD', $T(this.gradingmethod));												addClass(eSC, 'gradingmethod');
	var eSD = $$('TD', $T(this.meetings[0].days.replace(new RegExp('( )', 'g'), '\u00a0')));	addClass(eSD, 'days');
	var eSE = $$('TD', $T(Mil2Civ(this.meetings[0].starttime) + ' - ' + Mil2Civ(this.meetings[0].endtime)));	addClass(eSE, 'times');
	var eSF = $$('TD', $T(this.meetings[0].startdate + ' - ' + this.meetings[0].enddate));	addClass(eSF, 'dates');
	var eSG = $$('TD', $T(this.meetings[0].location));										addClass(eSG, 'location');
	var eSH = $$('TD', $T(this.meetings[0].instructor));									addClass(eSH, 'instructor');
	var eSI = $$('TD', $T(this.seated + '/' + this.capacity));								addClass(eSI, 'seating');
	var eSectionRow = $$('TR', eSA, eSB, eSD, eSE, eSF, eSG, eSH, eSI);
		eSectionRow._section = this;
		eSectionRow._meeting = this.meetings[0];
		this.meetings[0].elements = { row: eSectionRow };
	addClass(eSectionRow, 'section-info');
	this.elements = { row: eSectionRow };
	// style for filled sections
	if(this.isFull()) addClass(eSectionRow, 'section-status-closed');
	if(!this.permission()) addClass(eSectionRow, 'section-status-permission');
	
	// add behavior
	var course = this.course;
	var section = this; // for closures
	eSectionRow.style.cursor = 'pointer';
	eSectionRow.title = 'Click for more information';
	eSA.firstChild.title = 'Add to schedule';
	return eSectionRow;
};

Section.prototype.isFull = function() { // returns true if seated >= capacity
	return (this.seated >= this.capacity);
};

Section.prototype.days = function(sAcceptableDays) { // returns false if section meets on any day not explicitly listed in arg; true otherwise
	if(!sAcceptableDays || !sAcceptableDays.length) return false;
	for(var i = 0; i < this.meetings.length; i++)
		if(!this.meetings[i].meetsOnlyOnDays(sAcceptableDays)) return false;
	return true;
};

Section.prototype.full = function(bCanBeFull) { // returns true if: section is not full, OR section is full and full is allowed
	// if not filtering, accept section
	if(bCanBeFull) return true;
	// otherwise, return true if section is not at-capacity
	return (parseInt(this.seated) < parseInt(this.capacity));
};

Section.prototype.permission = function(bCanRequirePermission) { // returns true unless class requires permission and requiring permission is not acceptable
	if(bCanRequirePermission) return true;
	if(this.needpermission === true) return false;
	return true;
};

Section.prototype.online = function(bCanBeOnline) { // returns true unless at least one meeting is online, and meeting online is unacceptable
	if(bCanBeOnline) return true;
	// otherwise, we need to check each meeting (meetings can have different locations, after all)
	for(var i = 0; i < this.meetings.length; i++)
		if(this.meetings[i].location.toUpperCase() == 'ON LINE') return false;
	return true;
};

Section.prototype.offline = function(bHideOffline) { // returns true unless section meets offline & offline sections are unacceptable
	return !(bHideOffline && !this.isonline);
};

Section.prototype.conflict = function(oBusySchedule) {
	if(!oBusySchedule) return true;
	// farm the actual tests out to the individual meetings, which will inspect only the days on which it occurs with its own begin & end times
	for(var i = 0; i < this.meetings.length; i++)
		if(!this.meetings[i].conflict(oBusySchedule))
			return false;
	return true;
};

Section.prototype.partialsession = function(bCanBePartial) { // returns true if: section belongs to a course that is full-session OR partial-session is allowed
	if(bCanBePartial || this.course.sessionType.toLowerCase() == 'complete session courses') return true;
	return false;
}

Section.prototype.lowerlevel = function(bCanBeLower) { // returns true if: section is not lowerlevel OR lowerlevel is allowed
	if(bCanBeLower) return true;
	return (/[a-z ]+(\d+)/i.exec(this.course.num)[1] >= 300);
}

Section.prototype.upperlevel = function(bCanBeUpper) { // returns true if: section is not upperlevel OR upperlevel is allowed
	if(bCanBeUpper) return true;
	var iNum = /[a-z ]+(\d+)/i.exec(this.course.num)[1];
	return (iNum < 300 || iNum >= 500);
}

Section.prototype.gradlevel = function(bCanBeGrad) { // returns true if: section is not grad level OR grad level is allowed
	if(bCanBeGrad) return true;
	return (/[a-z ]+(\d+)/i.exec(this.course.num)[1] < 500);
}

Section.prototype.geneds = function(oWhitelists) { // returns true IFF section belongs to a course that is explicitly mentioned in the whitelist structure
	if(!oWhitelists) return true;
	// break course number up into dept. & numeric parts
	// var aParts = /^([A-Z]{2,4})(\d{3})/.exec(this.course.num);
	var sDept = this.course.number['department']; // aParts[1];
	var iNum = this.course.number['number']; // parseFloat(aParts[2]);
	// for each term in oWhitelists, check to see if this course number appears in the list; if no: return false
	for(var sTerm in oWhitelists) {
		try {
			var bIsPresent = (oWhitelists[sTerm][sDept].indexOf(iNum) !== -1);
			if(!bIsPresent) return false;
		}
		catch(e) { return false; }
	}
	
	return true; // for now
};

function Meeting(props) {
	this.section = null;
	// this.days = props.days;
	this.days = props.days.length ? 'NMTWHFS'.replace(new RegExp('([^' + props.days.replace(new RegExp('( )', 'g'), '') + '])', 'g'), ' ') : ' ';
	this.location = props.location;
	this.instructor = props.instructor;
	// process date & time information
	this.starttime = props.starttime;
	this.endtime = props.endtime;
	this.startdate = props.startdate;
	this.enddate = props.enddate;
	return this;
}

Meeting.prototype.meetsOnlyOnDays = function(sAcceptableDays) { // returns false if section meets on any day not explicitly listed in arg; true otherwise
	if(!sAcceptableDays || !sAcceptableDays.length) return false;
	sAcceptableDays += ' '; // add a space to filter, so spaces in original value are okay
	if(this.days.toUpperCase() == 'ARR') return true; // "ARR" courses are always okay for day-filtering
	// loop through days that are actually met on, and make sure it appears in the list that is acceptable
	for(var i = 0; i < this.days.length; i++)
		if(sAcceptableDays.toLowerCase().indexOf(this.days.toLowerCase().substr(i, 1)) === -1) return false; // this.days contains an element not present in acceptable days
	return true;
};

Meeting.prototype.conflict = function(oBusySchedule) { // returns true unless at least one meeting occurs during at least one of the busy blocks in busySchedule
	// loop through this meeting's days for block comparison
	for(var i = 0; i < this.days.length; i++) {
		var DOW = this.days.charAt(i).toUpperCase();
		// var DOW = this.days[i].toUpperCase(); // shorthand
		if(!oBusySchedule[DOW]) continue;
		
		// skip days that don't have any busy blocks; based on array length, because even blank days will have entries in schedule (so, we can't base test on !sched[DOW])
		for(var j = 0; j < oBusySchedule[DOW].length; j++) {
			var oBlock = oBusySchedule[DOW][j]; // shorthand
			if(!(oBlock.begin <= this.starttime && oBlock.end <= this.starttime) && !(oBlock.begin >= this.endtime && oBlock.end >= this.endtime)) return false;
		}
	}
	
	return true; // no conflicts, so return true
};


function XML2Course(xNode) {
	if(!xNode) return false;
	var c = {};
		c.num = xNode.getAttribute('num');
		c.title = xNode.getAttribute('title');
		c.credits = xNode.getAttribute('credits'); // not necessarily an int; "1-6"
		c.sessionType = xNode.getAttribute('sessiontype'); // differentiates between e.g. half-session and full-session courses
		// EXTRA
		// 1 - clean credits value
		if(c.credits == '.00') c.credits = '0';
		else c.credits = c.credits.replace(/(\s|\.|00)/gi, '');
		
	return c;
}

function XML2Section(xNode) {
	if(!xNode) return false;
	var s = {};
		s.courseid = xNode.getAttribute('courseid'); // not an int; "001234"
		s.section = xNode.getAttribute('section'); // not an int; "01"
		s.gradingmethod = xNode.getAttribute('gradingmethod');
		s.capacity = parseInt(xNode.getAttribute('capacity'));
		s.seated = parseInt(xNode.getAttribute('seated'));
		s.status = xNode.getAttribute('status');
		s.online = xNode.getAttribute('online');
	return s;
}

function XML2Meeting(xNode) {
	if(!xNode) return false;
	var m = {};
		m.days = xNode.getAttribute('days');
		m.starttime = xNode.getAttribute('starttime');
		m.endtime = xNode.getAttribute('endtime');
		m.startdate = xNode.getAttribute('startdate');
		m.enddate = xNode.getAttribute('enddate');
		m.location = xNode.getAttribute('location');
		m.instructor = xNode.getAttribute('instructor');
	return m;
}

function XML2Note(xNode) {
	if(!xNode) return false;
	var n = {};
		n.code = xNode.getAttribute('code'); // message coding
		n.url = xNode.getAttribute('url'); // a referenced URL
		n.type = xNode.getAttribute('type'); // for machine readability
		n.message = xNode.firstChild.nodeValue; // the written message text: "Notebook Computer & $125 fee required"; fix for Safari
	return n;
}

function XML2DeptNote(xNode) {
	if(!xNode) return false;
	var n = {};
		n.note = xNode.firstChild.nodeValue; // copying absurd Safari fix
	return n;
}

function getSelectValue(eSelect) { // figure out the current value of a drop down and return it
	if(!eSelect) return null;
	var aSelected = []; // for multiple values
	for(var i = 0; i < eSelect.options.length; i++)
		if(eSelect.options[i].selected) aSelected.push(eSelect.options[i].value);
	return aSelected.join(',');
}

function getCheckedValues(element) { // returns a comma-separate list of values from checkboxes within listed element(s)
	dump('viewer > getting selected subjects\n');
	if(!element) return false;
	var aSubs = [];
	for(var a = 0; a < arguments.length; a++) {
		var nlChecks = arguments[a].getElementsByTagName('INPUT');
		for(var i = 0; i < nlChecks.length; i++) {
			if(nlChecks[i].checked) aSubs.push(nlChecks[i].value);
		}
	}
	return aSubs.join(',');
}	

function Mil2Civ(sMil) { // converts military time values (0700, 1530, etc.) into civilian time (7:00 AM, 3:30 PM, etc.)
	if(!sMil || !sMil.length) return '';
	if(!/^\d{4}$/.test(sMil)) return sMil;
	
	var iHrs = parseFloat(sMil.substr(0, 2));
	
	var aT = 'AM';
	if(iHrs == 12) aT = 'PM';
	if(iHrs > 12) { iHrs -= 12; aT = 'PM'; }
	
	return iHrs + ':' + sMil.substr(2, 2) + ' ' + aT;
}
