<?php
/*
* schedule_process_ajax.php
* Author: Gabriel Berke-Williams, 2007
*/

$do_full_page = isset($_GET["full"]);
if(
$do_full_page === true){
    
// not an AJAX request
    
unset($_GET["full"]);
?>
<!DOCTYPE html>
<!-- I'm all HTML5 compliant wheeeeee -->
<html>
<head><title>gbw &raquo; Brandeis Schedule</title></head>
<body>
<?php
}

/*********** BEGIN MAGIC ******************/
$parser = new ScheduleParser($_GET);
$parser->parse();
$parser->print_table();
/***********  END MAGIC *******************/
if( $do_full_page === true){
?>
</body>
</html>
<?php
}

// indexed counter for the <td>s so there's max 5/row (5=Mon-Fri)
class Counter {
    var
$counter = array("M"    => array(),
            
"T"   => array(),
            
"W" => array(),
            
"Th"  => array(),
            
"F"    => array());
    function
Counter($hours, $minutes) {
    foreach(
$hours as $h ) {
        foreach(
$minutes as $m ) {
        
$hrmin = "{$h}:{$m}";
        
// indexed counter
        // $counter["Th"]["8:30"]; true means something is
        // happening (an event that started at 8 and overlaps, or
        // something started, either way), false means it's free.
        
$this->counter["M"][$hrmin] = false;
        
$this->counter["T"][$hrmin] = false;
        
$this->counter["W"][$hrmin] = false;
        
$this->counter["Th"][$hrmin] = false;
        
$this->counter["F"][$hrmin] = false;
        }
    }
    }

    
// set $counter to true for all specified days at $hour:$mins
    // - accepts multiple days but only one time
    
function mark($days, $hour, $minutes) {
    foreach(
$days as $mark_day ) {
        
$this->counter[$mark_day]["{$hour}:{$minutes}"] = true;
    }
    }
    
// get whether there's an event on $day @ $hour:$minutes
    
function is_event($day, $hour, $minutes) {
    return
$this->counter[$day]["{$hour}:{$minutes}"];
    }
}

class
classEvent {
    
// declare before setting in constructor
    
var $classtitle;
    var
$teacher;
    var
$location;
    var
$color; // set in the class so that class cells have the same color

    
var $begHour;
    var
$begMins;
    var
$endHour;
    var
$endMins;
    var
$days; // array!
    
    
function classEvent($classtitle, $teacher, $location, $timeArr, $days, $color){
    
$this->classtitle = $classtitle;
    
$this->teacher = $teacher;
    
$this->location = $location;
    
$this->color = $color;
    list(
$this->begHour, $this->begMins,
        
$this->endHour, $this->endMins) = $timeArr;
    
$this->days = $days;
    }
}

class
classEventBlock extends classEvent {
    function
classEventBlock($classtitle, $teacher, $location, $timeArr, $days, $color){
    
parent::classEvent($classtitle, $teacher, $location, $timeArr, $days, $color);
    }
}

class
classEventTime extends classEvent {
    function
classEventTime($classtitle, $teacher, $location, $timeStr, $days, $color){
    global
$parser;
    if(
is_string($timeStr) ){
        
$timeArr = $this->get_timeStr_array($timeStr);
        if( !
is_array($timeArr) || count($timeArr) != 4 ){
        
$msg = "Invalid time.";
        
$more_info = "Either not an array, or <> 4 items.\n";
        
$more_info .= "String: $timeStr / \n";
        if(
is_array($timeArr) ){
            
$more_info .= "Array: " . implode($timeArr);
        } else {
            
$more_info .= "Array: [empty]";
        }
        
$parser->err($classtitle, $msg, $more_info);
        }
    } else {
        
$parser->err($classtitle, "Provided time is not a string.");
    }
    
parent::classEvent($classtitle, $teacher, $location, $timeArr, $days, $color);
    }

    
/**
     * Convert "9:00 AM-1:30 PM" -> array(9,0,13,30)
     */
    
function get_timeStr_array($timeStr){
    list(
$begHour, $endHour, $begMins, $endMins) = array(false, false, false, false);
    if(
preg_match('/([0-9]{1,2}):([0-9]{1,2}) ([AP]M)-([0-9]{1,2}):([0-9]{1,2}) ([AP]M)/i', $timeStr, $matches ) ) {
        
// parse '10:30 AM-2:30 PM'
        /*
        [0] => 10:30 AM-12:00 PM
        [1] => 10
        [2] => 30
        [3] => AM
        [4] => 12
        [5] => 00
        [6] => PM
         */

        
if( $matches[3] == 'AM' || (int)$matches[1] == 12) {
        
$begHour = (int)$matches[1];
        } elseif(
$matches[3] == 'PM' && (int)$matches[1] < 12 ) {
        
$begHour = (int)$matches[1] + 12;
        }

        if(
$matches[6] == 'AM' || (int)$matches[4] == 12) {
        
$endHour = (int)$matches[4];
        } elseif(
$matches[6] == 'PM' && (int)$matches[4] < 12 ) {
        
$endHour = (int)$matches[4] + 12;
        }
        
$bMin = (int)$matches[2];
        
$eMin = (int)$matches[5];
        
// problem: magical brandeis schedule getter
        // passes in times as 9:10 when they should be 9:00
        // or 9:40 when they should be 9:30.
        // So we fix that. :)
        
if( $bMin == 10 ){
        
$bMin = 0;
        } elseif(
$bMin == 40 ){
        
$bMin = 30;
        }
        if(
$eMin == 10 ){
        
$eMin = 0;
        } elseif(
$eMin == 40 ){
        
$eMin = 30;
        }
        
$begMins = $bMin;
        
$endMins = $eMin;
    } else {
        return
null;
    }
    return array(
$begHour, $begMins, $endHour, $endMins);
    }
}

class
ScheduleParser {
    var
$arr;
    var
$err;
    var
$blocks;
    var
$rows;
    var
$colors;
    var
$table;
    var
$minuteArray;
    var
$hourArray;
    var
$dayArray;
    var
$counter;
    function
ScheduleParser($arr) {
    
$this->arr = $arr;
    
    
// 2D array of array($classtitle, $err_msg, $more_info = null).
    
$this->err = array();
    
    
// Beginning times are ten minutes earlier than they actually are to
    // allow for easy tabling.
    // Maybe put in a note about that?
    // Info from: http://www.brandeis.edu/registrar/schedule/search.php
    
$mwt = array('M', 'W', 'Th');
    
$mw = array('M', 'W');
    
$tf = array('T', 'F');

    
$this->blocks = array(
        
"A" => array("time" => array(8, 0, 9, 0), "days" => $mwt),
        
"B" => array("time" => array(9, 0, 10, 0), "days" => $mwt),
        
"C" => array("time" => array(10, 0, 11, 0), "days" => $mwt),
        
"D" => array("time" => array(11, 0, 12, 0), "days" => $mwt),
        
"E" => array("time" => array(12, 0, 13, 0), "days" => $mwt),
        
"F" => array("time" => array(13, 0, 14, 0), "days" => $mwt),
        
"G" => array("time" => array(9, 0, 10, 30), "days" => $tf),
        
"H" => array("time" => array(10, 30, 12, 0), "days" => $tf),
        
// there is no block I.
        
"J" => array("time" => array(12, 0, 13, 30), "days" => $tf),
        
"K" => array("time" => array(14, 0, 15, 30), "days" => $mw),
        
"L" => array("time" => array(15, 30, 17, 0), "days" => $mw),
        
"M" => array("time" => array(17, 0, 18, 30), "days" => $mw),
        
"N" => array("time" => array(13, 30, 15, 0), "days" => $tf),
        
// there is no block O.
        
"P" => array("time" => array(15, 0, 16, 30), "days" => $tf),
        
"Q" => array("time" => array(18, 30, 19, 30), "days" => $mwt),
        
"R" => array("time" => array(14, 0, 15, 0), "days" => array('T', 'Th', 'F')),
        
"S1" => array("time" => array(14, 0, 17, 0), "days" => array('M')),
        
"S2" => array("time" => array(13, 30, 16, 30), "days" => array('T')),
        
"S3" => array("time" => array(14, 0, 17, 0), "days" => array('W')),
        
"S4" => array("time" => array(13, 30, 16, 30), "days" => array('F')),
        
"S5" => array("time" => array(16, 30, 19, 30), "days" => array('T')),
        
"S6" => array("time" => array(9, 0, 12, 0), "days" => array('T')),
        
"S7" => array("time" => array(9, 0, 12, 0), "days" => array('F')),
        
"S8" => array("time" => array(17, 0, 20, 0), "days" => array('Th')),
        
// there are no blocks T or U.
        
"V" => array("time" => array(17, 0, 18, 30), "days" => array('T', 'Th')),
        
// there is no block W.
        
"X1" => array("time" => array(18, 30, 21, 30), "days" => array('M')),
        
"X2" => array("time" => array(18, 30, 21, 30), "days" => array('W')),
        
"X3" => array("time" => array(18, 30, 21, 30), "days" => array('Th')),
        
"X4" => array("time" => array(18, 30, 21, 30), "days" => array('T')),
        
"Y" => array("time" => array(18, 30, 20, 0), "days" => $mw)
        
// there is no block Z.
    
);

    
$this->rows = array();
    
$this->colors = array();
    
// Only turn off colors if it's specifically asked for.
    
$doColor = ($this->arr['setColor'] !== 'off');
    
    if(
$doColor == true ){
        
// pastels used as background colors for the classes
        // the original four are from brandeis, others from
        // http://www.geocities.com/michael27england/pastelcolors.html
        
$this->colors = array("#ccffcc", "#ffcccc", "#ccccff", "#ffccff",
                
"#eeffff", "#ffeeff", "#eeddee", "#ffdddd");
    } else {
        
$this->colors = array("white");
    }

    
$this->table = array(); // will hold the table like $table["Th"]["8:30"]
    
$this->minuteArray = array(0, 30);
    
$this->hourArray = range(8, 23);
    
$this->dayArray = array("M", "T", "W", "Th", "F");
    
$this->counter = new Counter($this->hourArray, $this->minuteArray);

    
// creates $table["<day>"]["<hr:min>"]
    
foreach( $this->dayArray as $day ){
        
$this->table["$day"] = array();
        foreach(
$this->hourArray as $h ) {
        foreach(
$this->minuteArray as $m ) {
            
$this->table["$day"]["{$h}:{$m}"] = array();
        }
        }
    }
    }

    
// Concatenates all errors to print out with the schedule.
    // $more_info is to provide technical information that the user probably doesn't care about.
    
function err($classtitle, $msg, $more_info = null){
    
$this->err []= array($classtitle, $msg, $more_info);
    }

    function
parse(){
    foreach( (array)
$this->arr['classes'] as $class ){
        
$randColor = $this->colors[ array_rand($this->colors) ];

        
$class_title = htmlspecialchars($class['class'], ENT_QUOTES);
        
// backwards compatibility
        
if( $class['teacher'] === null &&
               
strpos($class_title, '&lt;br/&gt;Teacher:') !== false ){
        list(
$class_title, $teacher) = explode('&lt;br/&gt;Teacher: ', $class_title);
        } else {
        
$teacher = htmlspecialchars($class['teacher'], ENT_QUOTES);
        }
        
$location = htmlspecialchars(str_replace("\\\\\\", '', $class['location']), ENT_QUOTES);
        if( isset(
$class['block']) ){
        
$blockName = strtoupper($class['block']);
        
// Use blocks.
        
if( isset($this->blocks[$blockName]) ){
            
$block = $this->blocks[$blockName];
            
$timeArr = $block["time"];
            
$days = $block["days"];
            
$this->rows []= new classEventBlock($class_title, $teacher, $location, $timeArr, $days, $randColor);
        } else {
            
$this->err($class_title, "Invalid block.");
        }
        } else {
        
$time = htmlspecialchars($class['time']);
        
$days = $class['days'];
        if( !
is_array($days) ){
            
// Backwards compatibility.
            
$days = explode(',', $days);
        }
        
$days = array_map('htmlspecialchars', $days);
        
// Some brandeis classes are TBA. In that case, don't process them.
        
if( $time == 'TBA' ){
            
$this->add_TBA_box($class_title);
        } else {
            
$cev = new classEventTime($class_title, $teacher, $location, $time, $days, $randColor);
            
// Yes, this is hacky.
            // It's to get around the fact that if the time can't be
            // parsed, we store an error and begHour etc. are set to NULL.
            // Ideally, this would use a try/catch.
            // Realistically, this is PHP4.
            
if( ! is_null($cev->begHour) ){
            
$this->rows []= $cev;
            }
        }
        }
    }

    
// put each event in $table
    
foreach($this->rows as $event) {
        foreach(
$event->days as $day ) {
        
// $table["W"]["20:30"] = "Business Time"
        
array_push($this->table["$day"]["{$event->begHour}:{$event->begMins}"], $event);
        }
    }
    }

    function
print_table(){
    if( ! empty(
$this->err) ){
        echo(
'<div style="background-color: #FFDF00">');
        echo(
'<h3>Errors</h3>');
        foreach(
$this->err as $e){
        list(
$classtitle, $err_msg, $more_info) = $e;
        if(
is_null($more_info) ){
            
printf("<b>%s</b>: %s<br />\n", $classtitle, $err_msg);
        } else {
            
printf("<b>%s</b>: %s (<small>%s</small>)<br />\n", $classtitle, $err_msg, $more_info);
        }
        }
        echo(
"</div>");
    }

    echo(
'<table border="1">'."\n");
    echo(
"<tbody>\n");
    echo(
"<tr>");
    
// empty cell at the top left corner to make room for times down the left
    
echo('<td></td>');
    
// add days across the top
    
foreach( array('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday') as $day ) {
        echo(
"<td>{$day}</td>");
    }
    echo(
"</tr>\n");
    foreach(
$this->hourArray as $hr ) {
        
$ampm = ( $hr < 12 ? "AM" : "PM" );
        
// don't do modulo if it's 12 PM
        
$hr_pretty = ( $hr == 12 ? $hr : $hr % 12 );

        foreach(
$this->minuteArray as $mins ) {
        echo(
"<tr>");
        
// Use "&nbsp;" not " " (a space) because the table puts stuff
        // after a space on the next line
        
printf("\n<td class=\"time\">%02s:%02d&nbsp;%s</td>",
            
$hr_pretty, $mins, $ampm);

        foreach(
$this->dayArray as $day ) {
            if(
count($this->table[$day]["{$hr}:{$mins}"]) == 0 &&
            
$this->counter->is_event($day, $hr, $mins) === false) {
                
// Nothing happening at that time on that day
                // and we have not yet marked the counter.
                
echo("\n<td> &nbsp; </td>");
                
$this->counter->mark(array($day), $hr, $mins);
            } else {
// Oooh, something is happening!
                
foreach($this->table[$day]["{$hr}:{$mins}"] as $event) {
                
// mark counter for all times/days touched by the event
                // the hours intersecting $event
                // for e.g. 8:30 - 12:30, returns [8, 9, 10, 11]
                
$touched_hours = array_slice($this->hourArray, $event->begHour - $this->hourArray[0], $event->endHour - $event->begHour );
                foreach(
$touched_hours as $hTouch ) {
                    if( !(
$hTouch == $event->begHour && $event->begMins == 30) ){
                    
// Don't mark 8:00 if the event starts at 8:30
                    
$this->counter->mark($event->days, $hTouch, 0);
                    }
                    
$this->counter->mark($event->days, $hTouch, 30);
                }
                
// Now do the final half-hour separately (e.g. increasing 12:00 if it ends at 12:30)
                
if( $event->endMins==30 ){
                    
$this->counter->mark($event->days, $event->endHour, 0);
                }
                
$this->create_table_cell($event);
                }
            }
// end else for something happening
        
} // end iteration through each day at specified time
        
echo("\n</tr>\n");
        }
// end iterating through $minuteArray
    
} // end iterating through $this->hourArray
    
echo("</tbody>\n");
    echo(
"</table>\n");
    }
// end print_table()

    
function add_TBA_box($classTitle){
    
// Add a box at the top with the class saying that it's TBA.
    
echo('<div style="background-color: #FFF6BF">');
    echo(
"<b>$classTitle</b> is TBA.");
    echo(
"</div>");
    }
    
    
// create <td> with event in it
    
function create_table_cell($event) {
    
// Multiply hour difference by two to cover x:30 cells
    
$height = (($event->endHour - $event->begHour) * 2 + ($event->endMins - $event->begMins)/30);
    
/*
    echo("\n<td rowspan=\"{$height}\" style=\"background-color:{$event->color}\" valign=\"top\">");
    echo("<b>{$event->classtitle}</b><br />\n<b>Classroom:</b> {$event->location}</td>");
     */
    
printf("\n".'<td rowspan="%s" style="background-color:%s" valign="top">',
        
$height, $event->color);
    
printf("<b>%s</b><br/><b>Teacher:</b> %s<br/><b>Classroom:</b> %s</td>",
        
$event->classtitle, $event->teacher, $event->location);
    }
}
?>