<!-- COURSE SCHEDULE BUILDER Copyright MNSU Web Development 2009, 2010 all rights reserved -->
function Scheduler(options) {
	this.config = options; // tada
	this.begin = Mil2MS(options.begin);
	this.end = Mil2MS(options.end);
	this.elements = {};
	this.data = {}; // this object holds locally-cached event data, keyed by SOMETHING
	this.days = {}; // collection of days
	this.colorstack = [
		'FF0000', // r1
		'FF6600', // o1
		'00FF00', // g1
		'0000FF', // b1
		'FF00FF', // v1
		
		'C80000', // r2
		'C85000', // o2
		'00C800', // g2
		'0000C8', // b2
		'C800C8', // v2
		
		'910000', // r3
		'913A00', // o3
		'009100', // g3
		'000091', // b3
		'910091'  // v3
		
	];
	this.colorPointer = 0;
	
	this.aConflictSets = null;
	
	// randomize color list order
	// this.colorstack.sort(function() { var max = 1, min = -1; return Math.floor(Math.random() * (max-min + 1)) + min; });
	
	this.init();
	return this;
}

Scheduler.prototype.init = function() {
	dump('scheduler > initializing\n');
	var scheduler = this; // for closures
	if(!this.config.element) return die('scheduler > no element passed!\n');
	var m = this.elements.main = this.config.element; // shorthand and assignment
	
	// clear out old contents
	// while(m.firstChild) m.removeChild(m.firstChild);
	
	// create the "week" block
	var eWeek = this.elements.week = $$('DIV');
		eWeek._captureMouseDown = 'week';
	addClass(eWeek, 'week');
	m.appendChild(eWeek);
	
	eWeek.onmousedown = 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 1: determine what kind of element was clicked on: empty space? a busy block handle? a course block?
		var o = event.srcElement;
		while(!o._captureMouseDown && o.parentNode)
			o = o.parentNode;
		if(!o._captureMouseDown) return false; // event did not occur within an element of interest, so do nothing
		
		switch(o._captureMouseDown) {
			case 'block-class': // pass event to the class event handler (open section popup)
				dump('scheduler > mousedown on section block\n');
				return scheduler.doSectionPopup(event, o);
			
			// ### NOTE: DO NOT BREAK, so default & week are identical
			case 'block-busy': // do handle-based resizing
			case 'week': // should begin drawing a new block
			default: // pass event to the week's event handler (create new busy block)
				dump('scheduler > mousedown on week\n');
				return scheduler.drawBusyBlock(event, scheduler);
		}
	};
	
	// create a DAY for each weekday
	for(var i = 0; i < 7; i++) {
		if(this.config.days.indexOf(i) < 0) continue; // this day wasn't included, so skip rendering it
		
		// create a day object for this day
		var d = new Day({ daynum: i, begin: this.config.begin, end: this.config.end });
		d.scheduler = this;
		this.days[d.day] = d; // add to DAYS collection
		
		// add day representation to week block
		var eD = d.render();
		eWeek.appendChild(eD);
	}
	
	// finally, create the status area
	this.renderStatus();
	this.updateStatus();
	dump('scheduler > done with init\n');
	return true;
};

Scheduler.prototype.doSectionPopup = function(event, element) {
	if(!event || !element) return false;
	dump('scheduler > showing section popup...\n');
	
	var oBlock = element.block;
	var ePopup = oBlock.day.scheduler.createSectionPopup(oBlock.day.scheduler.data[oBlock.meeting.section.courseid]);
	var cursor = getCursorPosition(event);
	
	var iTop = Math.ceil(cursor.y + 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 - 235;
	var iHigh = iLow + getScreenSize().height - ePopup.offsetHeight;
	
	ePopup.style.top = Math.max(iLow, Math.min(iTop, iHigh)) + 'px';
	ePopup.style.left = iLeft + 'px';
	
	ePopup._parent.appendChild(ePopup);
	
	ePopup.old = { onmousemove: document.onmousemove, onmousedown: document.onmousedown };
	document.onmousemove = document.onmousedown = function(e) {
		if(!e) var e = window.event; if(e.target && !e.srcElement) e.srcElement = e.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 = e.srcElement;
		while(!o._isPopup && o.parentNode)
			o = o.parentNode;
		if(o._isPopup) return (ePopup._bMouseCaptured = true);
		else if(e.type != 'mousedown' && !ePopup._bMouseCaptured) return;
		
		// destroy the popup!!!
		ePopup.fDestroy();
	};
	return true;
};

Scheduler.prototype.drawBusyBlock = function(event, scheduler) {
	if(!event || !scheduler) return false;
	dump('scheduler > drawing busy block...\n');
	
	var eWeek = scheduler.elements.week; // shorthand
	var MPH = 60 * 60 * 1000; // handy constant for later math
	var point = getCursorPosition(event);	
	
	// STEP 1: backup normal behavior
	var old = {
		onmousemove: document.onmousemove,
		onmouseup: document.onmouseup
	};
	
	var origin = getObjectPosition(eWeek); // absolute coords for top-left corner of week div		
	
	// STEP 2: create (or identify) visible marker
	// determine if click was on a filter block or the week itself
	var o = event.srcElement;
	while(!o.block && o.parentNode)
		o = o.parentNode;
	if(o.block) { // allow user to interact with existing filter
		dump('scheduler > existing block found\n');
		var oBlock = o.block;
		var day = oBlock.day;
		
		// also, determine if click was on head or foot
		if(oBlock.elements.head == event.srcElement) var bHead = true;
		if(oBlock.elements.foot == event.srcElement) var bFoot = true;
		if(!bHead && !bFoot) return false; // don't allow clicking in middle of block, off of handles
	} else {
		dump('scheduler > no block found, creating new one\n');
		// determine the "day" & "time" at which MD occurred (based on x/y coords)
		// determine which day column was MDed
		var iDayNum = scheduler.pixel2day(point.x - origin.x);
		
		// determine nearest hour, using price-is-right rules
		var iMS = scheduler.pixel2time(point.y - origin.y);
		var iHour = Math.floor(iMS / MPH);
		
		// STEP 2: create visible marker
		var oBlock = new Block(iHour * MPH, (iHour + 1) * MPH);
			oBlock.setColor('AAAAAA');
			oBlock.meeting = {};
			addClass(oBlock.elements.main, 'reserved');
			
		var day = scheduler.days[['N','M','T','W','H','F','S'][iDayNum]];
		day.addBlock(oBlock);
		
		// at this point, add the labels
		var eHead = oBlock.elements.head = $$('LABEL'); addClass(eHead, 'reserved-handle-head');
		var eFoot = oBlock.elements.foot = $$('LABEL'); addClass(eFoot, 'reserved-handle-foot');
		
		addClass(eHead, 'reserved-handle');
		addClass(eFoot, 'reserved-handle');
		
		eHead.style.top = '0px';
		eFoot.style.top = (oBlock.height - 5) + 'px';
		
		oBlock.elements.main.appendChild(eHead);
		oBlock.elements.main.appendChild(eFoot);
		
		// define close behavior; ### TODO: move this to WEEK.ondblclick
		oBlock.elements.main.title = 'Double-click to remove';
		oBlock.elements.main.ondblclick = function(e) {
			if(!e) var e = window.event;
			e.cancelBubble = true; if(e.stopPropagation) e.stopPropagation();
			
			oBlock.day.removeBlock(oBlock); // remove from block collection
			scheduler.applyCurrentFilters(); // refilter
			return false;
		};
	}
	
	// store original values for comparison
	var iBaseBegin = oBlock.begin;
	var iBaseEnd = oBlock.end;
	
	// also, class block with "active" class
	addClass(oBlock.elements.main, 'reserved-on');
	
	// STEP 3: define new behavior
	var iParts = 2; // determines time resolution in dragging; sets how many parts per hour are stopping point
	document.onmousemove = function(e) {
		if(!e) var e = window.event; if(e.target && !e.srcElement) e.srcElement = e.target;
		e.cancelBubble = true; if(e.stopPropagation) e.stopPropagation();
		
		// determine the time (to the ms) that corresponds to the current cursor position
		var current = getCursorPosition(e);
		var iTime = scheduler.pixel2time(current.y - origin.y);
		
		// change either start or end time of block, based on cursor position
		if(!bFoot && (iTime < iBaseBegin || bHead)) { // user is pushing start time earlier (or later, if handle grab)
			var iNewBegin = Math.floor(iTime / (MPH/iParts)) * (MPH/iParts); // 30-minute resolution
			var iNewEnd = iBaseEnd;
			// don't let begin time become later than end time
			iNewBegin = Math.min(iNewBegin, ((iBaseEnd / (MPH/iParts)) - 1) * (MPH/iParts));
		}
		else
		if(!bHead && (iTime > iBaseEnd || bFoot)) { // user is pushing end time later (or earlier, if handle grab)
			var iNewBegin = iBaseBegin;
			var iNewEnd = Math.ceil(iTime / (MPH/iParts)) * (MPH/iParts); // 30-minute resolution
			// don't let end time get earlier than begin time
			iNewEnd = Math.max(iNewEnd, ((iNewBegin / (MPH/iParts)) + 1) * (MPH/iParts));
		}
		else
		{
			iNewBegin = iBaseBegin;
			iNewEnd = iBaseEnd;
		}
		
		// update block with new values
		oBlock.setBegin(iNewBegin);
		oBlock.setEnd(iNewEnd);
		
		// update foot position
		oBlock.elements.foot.style.top = (oBlock.height - 5) + 'px';
		
		return false;
	};
	
	document.onmouseup = function(e) {
		if(!e) var e = window.event; if(e.target && !e.srcElement) e.srcElement = e.target;
		e.cancelBubble = true; if(e.stopPropagation) e.stopPropagation();
		
		// remove "active" class
		removeClass(oBlock.elements.main, 'reserved-on');
		
		// restore old event handlers
		for(p in old)
			document[p] = old[p];
		
		// update filter
		scheduler.applyCurrentFilters();
		
		return true;
	};
	
	return false;
};

Scheduler.prototype.renderStatus = function() {
	var scheduler = this; // for closures
	this.elements.status = {
		courses: $('status-courses'),
		credittotal: $('status-credits'),
		hourtotal: $('status-hours')
	};
	
	$('schedcal').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();
		$('schedcal').blur();
		scheduler.exportICS();
		return false;
	};
	
	$('schedprint').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();
		$('schedprint').blur();
		scheduler.exportPDF();
		return false;
	};
	
	$('schedreg').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();
		$('schedreg').blur();
		scheduler.registerForClasses();
	};

	return true;
};

Scheduler.prototype.exportICS = function() {
	dump('scheduler > exporting schedule...\n');
	
	// STEP 1: get a set of ics event stuff (only for non-hidden sections)
	var aEvents = this.extractICSData();
	
	if(!aEvents.length) {
		alert('There are no classes to export!\nYou must schedule at least one class before you can export.');
		return die('scheduler > no data to export.\n');
	}
	else dump('scheduler > ' + aEvents.length + ' meetings to export\n');
	
	// STEP 2: create a secret form to contain this data for POSTing
	var eForm = $$('FORM');
		eForm.setAttribute('method', 'post');
		eForm.setAttribute('action', this.config.cmdPath + '?cmd=export');
		eForm.setAttribute('target', '_blank');
	eForm.style.display = 'none';
	document.body.appendChild(eForm); // add it to the page so it will *work*
	
	// add each event as an identically-named hidden field
	for(var i = 0, imax = aEvents.length; i < imax; i++) {
		var eEvent = $$('INPUT');
			eEvent.setAttribute('type', 'hidden');
			eEvent.setAttribute('name', 'events[]');
			eEvent.setAttribute('value', aEvents[i].join('&'));
		eForm.appendChild(eEvent);
	}
	
	// STEP 3: submit the form!
	eForm.submit();
	
	// STEP 4: destroy the form!!
	$Cut(eForm);
	
	return true;
};

Scheduler.prototype.exportPDF = function() {
	dump('scheduler > exporting schedule...\n');
	
	// STEP 1: get a set of ics event stuff (only for non-hidden sections)
	var aEvents = this.extractICSData();
	
	if(!aEvents.length) {
		alert('There are no classes to export!\nYou must schedule at least one class before you can export.');
		return die('scheduler > no data to export.\n');
	}
	else dump('scheduler > ' + aEvents.length + ' meetings to export\n');
	
	// STEP 2: create a secret form to contain this data for POSTing
	var eForm = $$('FORM');
		eForm.setAttribute('method', 'post');
		eForm.setAttribute('action', this.config.cmdPath + '?cmd=print');
		eForm.setAttribute('target', '_blank');
	eForm.style.display = 'none';
	document.body.appendChild(eForm); // add it to the page so it will *work*
	
	// add each event as an identically-named hidden field
	for(var i = 0, imax = aEvents.length; i < imax; i++) {
		var eEvent = $$('INPUT');
			eEvent.setAttribute('type', 'hidden');
			eEvent.setAttribute('name', 'events[]');
			eEvent.setAttribute('value', aEvents[i].join('&'));
		eForm.appendChild(eEvent);
	}
	
	// STEP 3: submit the form!
	eForm.submit();
	
	// STEP 4: destroy the form!!
	$Cut(eForm);
	
	return true;
};

Scheduler.prototype.extractICSData = function(bIncludeHidden) { // create a list of iCal-style "events" based on meetings from accepted sections
	dump('scheduler > producing ICS data...');
	var scheduler = this; // for closures
	var aEvents = [];
	for(var courseid in this.data) {
		var section = this.data[courseid]; // shorthand
		if(section.hidden && !bIncludeHidden) continue; // skip classes that have been hidden by the user (unless otherwise specified
		for(var j = 0, max = section.meetings.length; j < max; j++) {
			var meeting = section.meetings[j]; // shorthand
			if(meeting.blocks.length)
				var color = meeting.blocks[0].color;
			else
				var color = 'none';
			/*
			** the following info about each meeting is needed to render a comprehensible schedule:
			** course title, course number, section number, credits, courseid
			** start and end time of first meeting, days of week on which class meets, section end date
			** location
			*/
			if(meeting.days !== '')
			aEvents.push([
				'title=' + encodeURIComponent(section.course.title), // "Elementary French"
				'number=' + encodeURIComponent(section.course.num), // FREN102
				'section=' + encodeURIComponent(section.section), // "01"
				'credits=' + encodeURIComponent(section.course.credits), // 3.00 (string!)
				'courseid=' + encodeURIComponent(section.courseid), // "001234"
				'starttime=' + encodeURIComponent(meeting.starttime), // "1300" (string)
				'endtime=' + encodeURIComponent(meeting.endtime), // "1350" (string)
				'startdate=' + encodeURIComponent(meeting.startdate), // "08/26/2008"
				'enddate=' + encodeURIComponent(meeting.enddate), // "12/11/2008"
				'days=' + encodeURIComponent(meeting.days), // " M W F "
				'location=' + encodeURIComponent(meeting.location), // "MH 0102"
				'instructor=' + encodeURIComponent(meeting.instructor), // "Brandon Cooke"
				'term=' + encodeURIComponent(scheduler.getCurrentTerm(true)), // "20095"
				'color=' + color
			]);
		}
	}
	dump(' done. ' + aEvents.length + ' iCalendar events\n');
	return aEvents;
};

Scheduler.prototype.registerForClasses = function() {
	var scheduler = this; // for closures
	/*
	** Create a hidden form that can be submitted (target=_blank). Form
	** should contain the data necessary to make ISRS prompt user for
	** credentials before directing user to a failed quick-add submission
	** (because PIN wasn't entered).
	** 
	** Also: these will all be hidden fields, so will be processed assembly-
	** line style.
	** 
	** NOTE: this is a deliberate exploitation of cross-site request
	** forgeries, which have been recently identified in a paper and may be
	** fixed on ISRS at some point; depending on the nature of ISRS's CSRF fix,
	** a different submission method may need to be designed
	*/
	
	dump('scheduler > begin registration process...\n');
	// STEP 1: get list of courses that would be registerd for
	var aCourseIDs = [];
	for(var courseid in this.data)
		if(this.data[courseid].hidden) continue; // skip courses that are hidden
		else aCourseIDs.push(courseid);
	
	if(!aCourseIDs.length) {
		alert('You must schedule at least one class before you can register.\nYour schedule is empty.');
		return false;
	}
	
	// STEP 2: present a popup for confirmation & credentials
	// produce the list of courses "(courseid) coursenum-section: title"
	var eCourses = $$('DIV');
	addClass(eCourses, 'register-courses');
	for(var i = 0, imax = aCourseIDs.length; i < imax; i++)
		eCourses.appendChild($$('P', $T(
			'(' + aCourseIDs[i] + ') ' +
			this.data[aCourseIDs[i]].course.num + '-' +
			this.data[aCourseIDs[i]].section + ': ' +
			this.data[aCourseIDs[i]].course.title
			)));
	
	// the instructions and disclaimer
	var eDirs = $$('DIV', $$('P', $T('To register for the courses listed above, enter your TechID and PIN below and press Register.')));
	addClass(eDirs, 'register-text');
	
	var eWarn = $$('DIV', $$('P', $$('STRONG', $T('Note: ')), $T('This will navigate away from the MNSU website to the MnSCU website.')));
	addClass(eWarn, 'register-text');
	
	// the form
	var eTechID = $$('INPUT');
		eTechID.setAttribute('name', 'userName');
		eTechID.setAttribute('type', 'text');
		eTechID.setAttribute('id', 'reg-techid');
		eTechID.setAttribute('value', this.config.creda);
	var eTechIDL = $$('LABEL', $T('TechID:'));
		eTechIDL.setAttribute('for', 'reg-techid');
	
	var ePIN = $$('INPUT');
		ePIN.setAttribute('name', 'password');
		ePIN.setAttribute('type', 'password');
		ePIN.setAttribute('id', 'reg-pin');
		ePIN.setAttribute('value', this.config.credb);
	var ePINL = $$('LABEL', $T('PIN:'));
		ePINL.setAttribute('for', 'reg-pin');
	
	var ePAU = $$('INPUT');
		ePAU.setAttribute('name', 'postAuthUrl');
		ePAU.setAttribute('type', 'hidden');
		ePAU.setAttribute('value', 'https://webproc.mnscu.edu/registration/register/quickadd.html?campusid=071&functionid=3316');
	var eVLFN = $$('INPUT');
		eVLFN.setAttribute('name', 'viewLoginForwardName');
		eVLFN.setAttribute('type', 'hidden');
		eVLFN.setAttribute('value', '');
	var eRCID = $$('INPUT');
		eRCID.setAttribute('name', 'rcId');
		eRCID.setAttribute('type', 'hidden');
		eRCID.setAttribute('value', '0071');
	var eSIID = $$('INPUT');
		eSIID.setAttribute('name', 'selectedInstitutionId');
		eSIID.setAttribute('type', 'hidden');
		eSIID.setAttribute('value', '0071');
	
	var eSubmit = $$('INPUT');
		eSubmit.setAttribute('type', 'submit');
		eSubmit.setAttribute('value', 'Register');
	var eCancel = $$('INPUT');
		eCancel.setAttribute('type', 'submit');
		eCancel.setAttribute('value', 'Cancel');
	
	// ### TODO: place this text beneath the PIN
	var eRow1 = $$('P', eTechIDL, eTechID);
	var eRow2 = $$('P', ePINL, ePIN);
	var eRow3 = $$('P', $T('You will be asked to confirm your PIN before registering.'));
	addClass(eRow3, 'register-confirm');
	
	var eRow4 = $$('P', eSubmit, $T(' '), eCancel, ePAU, eVLFN, eRCID, eSIID);
	addClass(eRow4, 'register-buttons');
	
	var eForm = $$('FORM', eRow1, eRow2, eRow3, eRow4);
		eForm.setAttribute('method', 'post');
		eForm.setAttribute('action', 'https://webproc.mnscu.edu/esession/login.do');
		eForm.setAttribute('target', 'regwindow');
	
	var eInput = $$('DIV', eForm);
	addClass(eInput, 'register-form');
	
	var eContents = $$('DIV', eCourses, eDirs, eWarn, eInput);
	addClass(eContents, 'register-contents');
	
	var ePopup = $$('DIV', eContents);
	addClass(ePopup, 'register-popup');
	
	// set position and size
	ePopup.style.width = '350px';
	ePopup.style.left = ((document.body.offsetWidth - 350) / 2) + 'px';
	ePopup.style.top = '100px';
	
	// create page-dimming element
	var eShade = $$('DIV');
	eShade.style.width = '100%';
	eShade.style.height = document.body.offsetHeight + 'px';
	addClass(eShade, 'register-shader');
	
	// create parent to contain shade & popup
	var eAll = $$('DIV', ePopup, eShade);
	
	// add to page
	document.body.appendChild(eAll);
	
	// cancel behavior
	eCancel.onclick = eShade.onclick = function() { $Cut(eAll); return false; };
	
	// submit behavior
	// STEP 3: after login form is send, send registration request
	eForm.onsubmit = function() {
		if(!eTechID.value.length || !ePIN.value.length) return false; // do nothing if blank
		
		// TODO: add loading indicator
		setTimeout(function() {
			// STEP 4: now perform the quick-add submission
			aData = [];
			aData.push({ name: 'campusid', value: '071' });
			aData.push({ name: 'searchcampusid', value: '071' });
			aData.push({ name: 'yrtr', value: scheduler.getCurrentTerm() });
			for(var i = 0, imax = aCourseIDs.length; i < imax; i++)
				aData.push({ name: 'courseIds[' + i + ']', value: aCourseIDs[i] });
			while(i < 8)
				aData.push({ name: 'courseIds[' + i++ + ']', value: '' });
			
			// TODO: remove loading indicator
			submitData({
				data: aData,
				method: 'post',
				action: 'https://webproc.mnscu.edu/registration/register/submitQuickAdd.html',
				target: 'regwindow'
				});
			
			dump('scheduler > quickadd submit sent\n');
			
			// remove the popup
			$Cut(ePopup); $Cut(eShade); return false;
			
			}, 2000);
		return true;
	}
	
	return false;
};

Scheduler.prototype.acceptSection = function(section) {
	dump('scheduler > accepting section...\n');
	// STEP 1: check to see if this section is already being displayed
	if(this.data[section.courseid]) return die('scheduler > section already visible\n');
	
	var color = this.colorstack[(this.colorPointer++ % this.colorstack.length)];
	// STEP 2: unpack section so we can recognize individual time blocks that need to be rendered
	for(var i = 0; i < section.meetings.length; i++) {
		var m = section.meetings[i]; // shorthand
		if(m.days == 'ARR') { dump('scheduler > meeting has ARR days; skipping for render\n'); continue; } // skip ARR meetings
		
		m.blocks = []; // each meeting should track its own timeslots
		// for each meeting, create a new block on each day at the specified time
		for(var j = 0; j < m.days.length; j++) {
			if(['N','M','T','W','H','F','S'].indexOf(m.days.charAt(j)) < 0) continue; // skip days which are not displayed in the weekly calendar (e.g. N or S)
			
			// create generic block object
			var oBlock = new Block(Mil2MS(m.starttime), Mil2MS(m.endtime));
			// customize for this section, and attach some useful info
				oBlock.meeting = m;
				oBlock.setColor(color);
				oBlock.elements.main.appendChild($$('SPAN', $T(section.course.num)));
				oBlock.elements.main.title = section.course.num + '-' + section.section + ': ' + section.course.title;
				oBlock.elements.main._captureMouseDown = 'block-class';
				oBlock.elements.main.style.zIndex = '100'; // add here instead of in CSS because of IE & overflow:visible
			m.blocks.push(oBlock); // add to collection
			
			// submit block object to the proper day object
			this.days[m.days.charAt(j)].addBlock(m.blocks[m.blocks.length-1]);
		}
	}
	
	// add this section to the local data store
	this.data[section.courseid] = section;
	
	// update the status
	this.updateStatus();
	
	return true;
};

Scheduler.prototype.removeSection = function(sCourseID) { // removes a section from the scheduler
	if(!arguments.length) return die('scheduler > no courseid passed to remove section!\n');
	dump('scheduler > removing section ' + sCourseID + '\n');
	
	// STEP 1: delegate this task to DAYs
	for(d in this.days)
		this.days[d].removeSection(sCourseID);
	
	// STEP 2: delete visibility flag that may be present on items
	try {
		delete this.data[sCourseID].hidden;
		
		// also, delete block objects from section's meetings
		for(var i = 0; i < this.data[sCourseID].meetings.length; i++)
			delete this.data[sCourseID].meetings[i].blocks;
	} catch(e) { }
	
	// STEP 3: remove from data store
	delete this.data[sCourseID];
	
	this.updateStatus();
	this.applyCurrentFilters();
	return true;
};

Scheduler.prototype.clear = function() { // remove all courses from scheduler
	// loop through all course ids tracked by the scheduler, and remove them!
	for(var courseid in this.data)
		this.removeSection(courseid);
	return true;
};

Scheduler.prototype.findConflicts = function() { // collects entries that conflict with each other
	var aConflictSets = [];
	var aSet = null;
	for(d in this.days) {
		if( (aSet = this.days[d].findConflicts()) )
			aConflictSets.push(aSet);
	}
	this.aConflictSets = aConflictSets;
	return true;
};

Scheduler.prototype.updateStatus = function() { // updates the information in the status thing
	var scheduler = this; // for closures
	dump('scheduler > updating status...\n');
	
	// STEP 1: update list of courses
	// clear out existing list
	while(this.elements.status.courses.firstChild)
		$Cut(this.elements.status.courses.firstChild);
	
	var bEmpty = true;
	
	for(var s in this.data) { // NOTE: "var s" is needed for IE; it thinks "s" already exists
		bEmpty = false;
		this.elements.status.courses.appendChild(this.renderStatusSection(this.data[s]));
	}
	
	if(bEmpty) {
		// render a single course
		var eLabel = $$('LABEL', $T('You have not scheduled any courses yet.'));
		// put it all together
		var eRow = $$('DIV', eLabel);			addClass(eRow, 'row');
		// add it to the order
		this.elements.status.courses.appendChild(eRow);
	}
	else {
		// add the "save schedule" control
		var eLink = $$('A');
			eLink.href = scheduler.producePermalink();
			eLink.id = 'permalink';
			eLink.title = scheduler.getCurrentTerm(true) + ' Schedule';
			eLink.onclick = function(event) {
				if(!event) var event = window.event;
				event.cancelBubble = true; if(event.stopPropagation) event.stopPropagation();
				scheduler.presentPermalink();
				return false;
			};
		
		var eRow = $$('DIV', eLink);
			addClass(eRow, 'row');
			addClass(eRow, 'sched-save');
		
		this.elements.status.courses.appendChild(eRow);
	}
	
	// STEP 2: determine conflicts
	var aConflicts = this.findConflicts();
	// ### TODO: alter styles of conflicting courses so they look naughty
	
	// STEP 3: update credit & hour counts
	// credits
	var iCredits = 0;
	for(var s in this.data) { // NOTE: "var s" is needed for IE; it thinks "s" already exists
		if(this.data[s].hidden) continue;
		iCredits += parseFloat(this.data[s].course.credits);
	}
	
	while(this.elements.status.credittotal.firstChild)
		$Cut(this.elements.status.credittotal.firstChild);
	this.elements.status.credittotal.appendChild($T(iCredits.toFixed(0).toString()));
	
	// hours
	var iMS = 0;
	for(d in this.days) {
		iMS += this.days[d].getBusyTime();
	}
	
	while(this.elements.status.hourtotal.firstChild)
		$Cut(this.elements.status.hourtotal.firstChild);
	this.elements.status.hourtotal.appendChild($T((iMS/(60*60*1000)).toFixed(2).toString()));
	
	return true;
};

Scheduler.prototype.renderStatusSection = function(section) { // creates XHTML rep of a section for right-hand list
	var scheduler = this;
	// render a single course
	var eImg = $$('IMG');
		eImg.src = '/schedule/images/transparent-circle.png';
	var eDot = $$('SPAN', eImg);
	addClass(eDot, 'section-dot');
	var eLabel = $$('LABEL', $T(section.course.num + '-' + section.section));
		eLabel.title = section.course.num + '-' + section.section + ': ' + section.course.title;
	var eToggle = $$('A', $$('SPAN')); addClass(eToggle.firstChild, 'toggle');
		eToggle.href = 'javascript:void(0);';
		eToggle.title = (section.hidden) ? 'Show this course' : 'Hide this course';
		eToggle.onclick = function() {
			scheduler.toggleSectionVisibility(section.courseid);
			this.blur();
			return false;
			};
	var eCut = $$('A', $$('SPAN'));
		eCut.href = 'javascript:void(0);'; addClass(eCut.firstChild, 'cut');
		eCut.onclick = function() { scheduler.removeSection(section.courseid); this.blur(); return false; };
		eCut.title = 'Remove this course';
	
	// put it all together
	var eRow = $$('DIV', eCut, eToggle, eDot, eLabel); addClass(eRow, 'row');
	
	if(section.hidden) addClass(eRow, 'row-hidden');
	
	// figure out which color the section blocks are and use that as this row's bg color
	try {
		var color = section.meetings[0].blocks[0].color;
		if(!section.hidden) eDot.style.backgroundColor = '#' + color;
	}
	catch(e) {}
	
	return eRow;
};

Scheduler.prototype.toggleSectionVisibility = function(sCourseID) { // hides (or shows) a given section
	// STEP 1: grab all the blocks for this section
	var s = this.data[sCourseID];
	if(!s) return die('scheduler > setvis: unable to find section "' + sCourseID + '"\n');
	
	var bShow = (s.hidden);
	
	for(var i = 0; i < s.meetings.length; i++) {
		var m = s.meetings[i]; // shorthand
		for(var j = 0; j < m.blocks.length; j++) {
			var b = m.blocks[j]; // shorthand
			if(bShow) removeClass(b.elements.main, 'off');
			else addClass(b.elements.main, 'off');
		}
	}
	
	// STEP 2: flag section
	if(!bShow) s.hidden = true;
	else delete s.hidden;
	
	// STEP 3: update status
	this.updateStatus();
	
	// STEP 4: refilter
	this.applyCurrentFilters();
	
	return bShow;
};

Scheduler.prototype.determineAvailability = function() {
	var oBusy = {};
	for(dayLetter in this.days)
		oBusy[dayLetter] = this.days[dayLetter].determineAvailability();
		
	return oBusy;
};

Scheduler.prototype.hasSection = function(sCourseID) { // returns true if section has already been added to the schedule; false otherwise
	return ((this.data[sCourseID]));
};

Scheduler.prototype.producePermalink = function() {
	dump('scheduler > preparing permalink URL...\n');
	
	var sURL = '?c=' + this.getCurrentTerm() + '/';
	for(var courseid in this.data)
		sURL += courseid + (this.data[courseid].hidden ? '' : '') + ',';
	
	return sURL.substr(0, sURL.length-1); // strip trailing comma
};

Scheduler.prototype.ms2pixel = function(iMS) { // convert a number of miliseconds into a number of pixels; think DURATION, not POINT IN TIME
	if(!iMS && iMS !== 0) return false;
	return (iMS / (60 * 60 * 1000)) * this.config.scale;
};

Scheduler.prototype.time2pixel = function(iMS) { // convert a time (in MS) into a pixel offset relative to top of viewport; think POINT IN TIME, not DURATION
	if(!iMS && iMS !== 0) return false;
	return this.ms2pixel(iMS - this.begin);
};

Scheduler.prototype.pixel2ms = function(iPX) { // convert a number of pixels into a number of ms; think DURATION, not POINT IN TIME
	if(!iPX && iPX !== 0) return false;
	return (iPX / this.config.scale) * (60 * 60 * 1000);
};

Scheduler.prototype.pixel2time = function(iPX) { // convert a pixel value relative to viewport into a number of ms since midnight; think POINT IN TIME, not duration
	if(!iPX && iPX !== 0) return false;
	return this.pixel2ms(iPX) + this.begin;
};

Scheduler.prototype.pixel2day = function(iPX) { // given a point relative to viewport origin, return a day number (0-6)
	if(!iPX && iPX !== 0) return null;
	return Math.floor(iPX / this.config.dayWidth);
};

Scheduler.prototype.presentPermalink = function() {
	var scheduler = this; // for closures
	
	// header
	var eHead = $$('DIV');
	addClass(eHead, 'head');
	
	// body
	var eText = $$('P', $T("This URL links to the schedule you've put together. You can copy and paste it into email, or bookmark it to save your work for later."));
	
	var eLabel = $$('LABEL', $T('URL:'));
		eLabel.setAttribute('for', 'permalink-display');
	var eLink = $$('INPUT');
		eLink.id = 'permalink-display';
		eLink.setAttribute('type', 'text');
		eLink.setAttribute('size', '40');
		eLink.value = location.protocol + '//' + location.host + location.pathname + scheduler.producePermalink();
	var eLinkP = $$('P', eLabel, eLink);
		eLinkP.style.textAlign = 'center';
	
	var eClose = $$('INPUT');
		eClose.type = 'button';
		eClose.value = 'OK';
	var eCloseP = $$('P', eClose);	
		eCloseP.style.textAlign = 'center';
		// eCloseP.style.marginRight = '20px';
		
	var eBody = $$('DIV', eText, eLinkP, eCloseP);
	addClass(eBody, 'body');
		
	var eFoot = $$('DIV');
	addClass(eFoot, 'foot');
	
	var eMain = $$('DIV', eHead, eBody, eFoot);
		eMain.id = 'permalink-popup';
	addClass(eMain, 'section-popup');
	
	scheduler.elements.main.appendChild(eMain);
	eLink.select();
	
	eClose.onclick = function() {
		eMain.parentNode.removeChild(eMain);
		return;
	};
	
	return false;
};


function Day(options) { // creates day object, which will do intelligent stuff... I hope
	if(arguments.length == 0) return false;
	this.config = options;
	// various ways of understanding date information, all based on passed daynum
	var wd = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
	this.daynum = options.daynum;
	this.weekday = wd[options.daynum];
	this.day = this.weekday.charAt(0); if(options.daynum==0) this.day = 'N'; if(options.daynum==4) this.day = 'H';
	// this.scale = options.scale;
	this.scale = 17;
	this.elements = {}; // collection for elements
	this.blocks = []; // collection of blocks
	
	// grok times that were passed
	this.begin = Mil2MS(options.begin);
	this.end = Mil2MS(options.end);
	
	return this;
}

Day.prototype.render = function() {
	var eMain = this.elements.main = $$('DIV'); addClass(eMain, 'day');
	var day = this;
	return eMain;
};

Day.prototype.addBlock = function(block) { // add this block to day's div
	if(!block) return false;
	this.blocks.push(block); // add to collection
	
	block.day = this; // attach to the block a reference to the day object
	block.updateDisplay(); // tell block to update its height
	this.elements.main.appendChild(block.elements.main); // add block element to day element
	
	/*
	// be done for regular blocks
	if(!block.meeting.section) return true;
	*/
	
	return true;
};

Day.prototype.removeSection = function(sCourseID) { // removes all blocks that belong to given section from this day
	if(!arguments.length) return false;
	// look through local collection of blocks to see if any of them belong to the section identified
	for(var i = this.blocks.length - 1; i >= 0; i--)
		if(this.blocks[i].meeting.section && this.blocks[i].meeting.section.courseid == sCourseID)
			this.removeBlock(this.blocks[i]);
	return true;
};

Day.prototype.removeBlock = function(block) { // removes a specific block from the day
	if(!block) return false;
	try{
		$Cut(block.elements.main);
		delete block.day;
	} catch(e) {}
	// now try to remove this block from the blocks array
	for(var i = 0; i < this.blocks.length; i++)
		if(this.blocks[i] == block)
			this.blocks.splice(i, 1);
	return true;
};

Day.prototype.findConflicts = function() {
	var aConflictedPairs = [];
	// STEP 0: remove conflict class from all blocks
	for(var i = 0; i < this.blocks.length; i++)
		removeClass(this.blocks[i].elements.main, 'conflict-block');
	// STEP 1: sort the blocks by start time
	this.blocks.sort(sort_blockBegin);
	// STEP 2: for each block, make sure no later blocks start before its end time
	for(var i = 0; i < this.blocks.length; i++) {
		if(this.blocks[i].meeting.section && this.blocks[i].meeting.section.hidden) continue; // skip hidden blocks; do not compare hidden blocks to any other block
		var base = this.blocks[i]; // shorthand
		for(var j = i; j < this.blocks.length; j++) {
			if(j == i) continue; // this is better than starting with j = i+1, because i+1 may not be a valid index
			if(this.blocks[j].meeting.section && this.blocks[j].meeting.section.hidden) continue; // skip hidden blocks; do not compare any blocks to hidden blocks
			var target = this.blocks[j]; // shorthand
			if(target.meeting.section === base.meeting.section) continue; // a section shouldn't count as conflicting with itself (this actually happens!)
			if(target.begin <= base.end - 1) { // subtract 1 MS from end time, to prevent erroneous conflicts
				aConflictedPairs.push([base, target]);
				addClass(base.elements.main, 'conflict-block');
				addClass(target.elements.main, 'conflict-block');
			}
		}
	}
	if(!aConflictedPairs.length) return false;
	return aConflictedPairs;
};

Day.prototype.getBusyTime = function() { // calculates the number of miliseconds accounted for by this day's blocks
	var iMS = 0;
	for(var i = 0; i < this.blocks.length; i++)
		if(this.blocks[i].meeting.section && !this.blocks[i].meeting.section.hidden)
			iMS += (this.blocks[i].end - this.blocks[i].begin);
	return iMS;
};

Day.prototype.determineAvailability = function() {
	// sort this day's blocks
	this.blocks.sort(sort_blockBegin);
	
	// iterate through blocks, condensing all start and end points into a single layer
	var aTimeline = new Array();
	var aIndex = new Array(); // track significant times separately as workaround for Chrome/V8's failure to for-in over properties by insertion order
	for(var i = 0; i < this.blocks.length; i++) {
		var block = this.blocks[i]; // shorthand
		
		// do not include hidden sections in availability calculation
		// if(block.meeting.section && this.scheduler.data[block.meeting.section.courseid].hidden) continue;
		if(block.meeting.section && block.meeting.section.hidden) continue;
		
		// convert begin & end times that are in MS into 24-hour time
		var begin = parseFloat(MS2Mil(block.begin));
		var end = parseFloat(MS2Mil(block.end - 1));
		
		aIndex.push(begin);
		aIndex.push(end);
		
		if(aTimeline[begin])
			aTimeline[begin].push(1);
		else aTimeline[begin] = [1];
		if(aTimeline[end])
			aTimeline[end].push(-1);
		else aTimeline[end] = [-1];
	}
	
	// clean up the tracking array (sort, and remove duplicate values)
	aIndex.sort(sort_numbers);
	
	var old = null;
	for(var i = aIndex.length - 1; i >= 0; i--) {
		if(aIndex[i] === old) old = aIndex.splice(i, 1);
		else old = aIndex[i];
	}
	
	// now, iterate through condensed timeline and translate it into blocks of busy time (i.e. [{begin:, end:}, ...] )
	var iClassesInSession = 0;
	var aBusyBlocks = [];
	var currentBlock;
	for(var i = 0; i < aIndex.length; i++) {
		var aChanges = aTimeline[aIndex[i]];
		for(var j = 0; j < aChanges.length; j++)
			iClassesInSession += aChanges[j];
		
		if(iClassesInSession !== 0) {
			// if there is no current busy block, create one
			if(!currentBlock || typeof currentBlock.begin == 'undefined' || typeof currentBlock.end != 'undefined')
				currentBlock = aBusyBlocks[aBusyBlocks.push({ begin: aIndex[i] }) - 1]; // add new entry to array and create REFERENCE to it
			// otherwise, do nothing
		}
		else
		if(iClassesInSession === 0) {
			// close the current busy block if there is one, otherwise do nothing
			if(currentBlock && currentBlock.begin && typeof currentBlock.end == 'undefined')
				currentBlock.end = aIndex[i];
		}
	}
	
	return aBusyBlocks;
};


function Block(iBegin, iEnd) {
	if((!iBegin && iBegin !== 0) || (!iEnd && !iEnd !== 0)) return die('scheduler > error creating block; argument missing\n');
	
	// STEP 1: parse time values (and put MS values in a place where availability matrix looks)
	this.begin = null;
	this.end = null;
	this.color = null;
	this.top = null;
	this.height = null;
	
	// STEP 2: create XHTML rep
	this.elements = { main: $$('P') };
	addClass(this.elements.main, 'block');
	this.elements.main.block = this; // reference back to memory object
	
	// STEP 3: apply time values
	this.setBegin(iBegin);
	this.setEnd(iEnd);
	
	return this;
}

Block.prototype.setBegin = function(iMS) {
	if(!iMS) return false;
	
	// STEP 1: save MS value (important for availability matrix)
	var iMin = this.day ? this.day.begin : 0;
	this.begin = Math.max(iMS, iMin);
	
	// STEP 2: if scale & viewport data are available, set top position and height of block
	if(this.day) this.updateDisplay();
	
	return true;
};

Block.prototype.setEnd = function(iMS) {
	if(!iMS) return false;
	
	// STEP 1: determine MS value
	var iMax = this.day ? this.day.end : (24 * 60 * 60 * 1000) + 1;
	this.end = Math.min(iMS, iMax);
	
	// STEP 2: if scale & viewport data are available, set top position and height of block
	if(this.day) this.updateDisplay();
	
	return true;
};

Block.prototype.updateDisplay = function() {
	if((!this.begin && this.begin !== 0) || (!this.end && this.end !== 0) || !this.day) return false;
	
	// calculate values, store them on the block object, and update styling
	this.top = this.day.scheduler.time2pixel(this.begin);
	this.height = this.day.scheduler.ms2pixel(this.end - this.begin);
	this.elements.main.style.top = this.top + 'px';
	this.elements.main.style.height = this.height + 'px';
	
	return true;
};

Block.prototype.setColor = function(sHex) {
	// first, update background color
	if(sHex) {
		this.elements.main.style.backgroundColor = '#' + sHex;
		this.color = sHex;
	}
	else {
		this.elements.main.style.backgroundColor = 'transparent';
		this.color = null;
	}
	// update text color in response to color change
	this.elements.main.style.color = '#' + this.calculateTextColor(this.color);
	return true;
};

Block.prototype.calculateTextColor = function(sHex) { // figures out if text color should be black or white
	if(sHex == null) return '000000'; // handle transparent value
	
	var dRed = parseInt(sHex.substr(0, 2), 16); // both isolates hex value, and converts it to decimal
	var dGrn = parseInt(sHex.substr(2, 2), 16);
	var dBlu = parseInt(sHex.substr(4, 2), 16);
	
	// this is a widely-accepted formula, apparently
	var y = 0.3 * dRed + 0.59 * dGrn + 0.11 * dBlu;
	if(y < 90) return 'FFFFFF';
	else return '000000';
};


function Mil2MS(sMil) { // convert 24-hour, military time (e.g. 0600) into milliseconds since midnight
	if(!sMil || !sMil.length) return false;
	
	var ms = 0;
	var aTimeParts = [sMil.substr(0,2), sMil.substr(2,2)];
	for(var i = 0; i < aTimeParts.length; i++) ms += (parseFloat(aTimeParts[i]) * Math.pow(60, 2-i) * 1000);
	return ms;
}

function MS2Mil(iMS) { // convert ms since midnight into 24-hour military time
	if(!arguments.length) return false;
	var iHour = 0; var iMin = 0; var sInd = 'AM';
	var MPH = 60 * 60 * 1000; // 60 minutes in ms;
	var MIN = 60 * 1000; // 60 seconds in ms
	while(iMS >= MPH)	{ iHour++; iMS -= MPH; }
	while(iMS >= MIN)	{ iMin++; iMS -= MIN; }
	// minutes formatting
	if(iMin < 10) sMin = '0' + iMin; else sMin = iMin;
	return iHour + '' + sMin;
}


function sort_blockBegin(a, b) {
	try { return a.begin - b.begin; }
	catch(e) { return 0; }
};

function sort_numbers(a, b) {
	if(a > b) return 1;
	if(a < b) return -1;
	return 0;
}

function submitData(options) { // accepts an array of data, submits that data as a form, and then executes an onComplete function if provided
	if(!options) return false;

	// create FORM element
	var eForm = $$('FORM');
	eForm.submitMethod = eForm.submit; // alias of submit method, to allow for access once method name has been shadowed by submit field
	for(var p in options) {
		if(['','data','onComplete'].indexOf(p) !== -1) continue; // skip the "data" and "onComplete" options
		eForm.setAttribute(p, options[p]);
	}
	
	// create form fields based on aData
	for(var i = 0, imax = options.data.length; i < imax; i++) {
		var eField = $$('INPUT');
			eField.setAttribute('type', 'hidden');
			eField.setAttribute('name', options.data[i].name);
			eField.setAttribute('value', options.data[i].value);
		eForm.appendChild(eField);
	}
	
	// STEP 4: add the form to the page, submit the form, and then destroy it
	dump('submitdata > submitting hidden form...\n');
	
	var bSuccess = true;
	try {
		var eBody = document.getElementsByTagName('BODY')[0];
		eBody.appendChild(eForm);
		eForm.submitMethod();
		eBody.removeChild(eForm);
	}
	catch(e) { dump('submitdata > submission failed!!\n'); bSuccess = false; }
	
	dump('submitdata > success? ' + bSuccess + '\n');
	if(typeof options.onComplete == 'function') onComplete(bSuccess);
	return true;
}

