<!-- COURSE SCHEDULE BUILDER Copyright MNSU Web Development 2009, 2010 all rights reserved -->

function Viewer(config) {
	this.config = config; // tada
	this.elements = {};
	
	this.terms = {}; // local data cache
	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: true,
		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;
}

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;
		var eOpt = $$('OPTION', $T(''));
			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; these are the in-table subject hiding links
		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;
		var coords = getObjectPosition(e); // collect coordinates now, because they aren't available later
		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();
			
			// create notification popup
			var ePopup = new InfoPopup(oSection.course.num + '-' + oSection.section + ' added to your schedule.', 'none');
			document.getElementsByTagName('BODY')[0].appendChild(ePopup.elements.main);
			
			ePopup.elements.main.style.top = Math.round(coords.y - 15) + 'px';
			ePopup.elements.main.style.left = Math.round(coords.x + 30) + 'px';
			// cue up removal
			setTimeout(function() { ePopup.destroy(); }, 2000);
			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;
			
			// IE7 bug workaround: Normally it'd be fine to destroy the popup RIGHT HERE.  But IE7 gets confused about which div the mouse is in.  So, do an additional check and don't destroy the popup if we are still in the div's screen area.
			var cursor = getCursorPosition(event);
			var ePopupLoc = getObjectPosition(ePopup);
			if(event.type != 'mousedown' && ePopup._bMouseCaptured && (
				(cursor.x > ePopupLoc.x) &&
				(cursor.y > ePopupLoc.y) &&
				(cursor.x < (ePopupLoc.x + ePopup.clientWidth)) &&
				(cursor.y < (ePopupLoc.y + ePopup.clientHeight))
			)) 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 iTop = Math.ceil(cursor.y - ePopup.offsetHeight - 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', '/schedule/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 eText = $$('A', $T(note.message));
			eText.href = note.url;
			eText.target = '_blank';
		}
		else {
			var eText = $T(note.message);
		}
		var eNote = $$('P', eText);
			eNote.title = note.message;
		if(note.type == 'permission')
			addClass(eNote, 'permission');
		eNotes.appendChild(eNote);
	}
	
	var eHead = $$('DIV');
	addClass(eHead, 'head');
	var eBody = $$('DIV', eHeader, eDetails, eNotes);
	addClass(eBody, 'body');
	var eFoot = $$('DIV');
	addClass(eFoot, 'foot');
	
	var ePopup = this.elements.sectionPopup = $$('DIV', eHead, eBody, eFoot);
		ePopup._isPopup = true;
	addClass(ePopup, 'section-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
	// if value has changed, reapply filters
	if(sDomain != 'subject' && sOldValue != sValue)
		this.applyCurrentFilters();
	
	return (sOldValue != sValue); // this line is important
};

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');
	
	var iShown = 0; // for current results count
	
	// 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;
		
		// 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');
			}
			
			if(bShowSection) iShown++; // decrement shown count
			
			// 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';
			
			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';
	}
	
	if(!oldConflictFilter) this.filters.conflict = false; // this line is important
	if(oldGenedFilter) this.filters.geneds = oldGenedFilter;
	dump('\n');
	
	this.updateCount(iShown);
	
	return true;
};

Viewer.prototype.updateCount = function(iShown) {
	var eUpdate = $('sectioncount');
	if(!eUpdate) return false;
	
	while(eUpdate.firstChild)
		eUpdate.removeChild(eUpdate.firstChild);
	
	eUpdate.appendChild($T(iShown));
	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;
}

