// All times are rounded to 15-minute intervals.
// Start times are rounded backward; end times are rounded forward.
// Thus, the table cell for a class always covers at least the proper length.

// TODO no Sat unless necessary?
var DAYS = new Array('Mon', 'Tues', 'Wed', 'Thurs', 'Fri', 'Sat');

// TODO user-specified substitutions (really, substitutions at all)
// TODO make an Object, not an Array?
var SUBSTITUTIONS = new Array();
SUBSTITUTIONS['Lecture'] = '';
SUBSTITUTIONS['Dir Study'] = '';
SUBSTITUTIONS['Recitation'] = 'Rec';
SUBSTITUTIONS['Laboratory'] = 'Lab';
SUBSTITUTIONS['Practicum'] = '(W)';

SUBSTITUTIONS['PHIL 0200'] = 'Anc Phil';
SUBSTITUTIONS['PHIL 1580'] = 'Phil Math';
SUBSTITUTIONS['MATH 1530'] = 'Adv Calc';
SUBSTITUTIONS['HAA 0040'] = 'Intro Arch';
SUBSTITUTIONS['CS 1550'] = 'OS';
SUBSTITUTIONS['CS 1571'] = 'AI';

SUBSTITUTIONS['Cathedral of Learning'] = 'CL';
SUBSTITUTIONS['Frick Fine Arts Building'] = 'FKART';
SUBSTITUTIONS['Sennott Square'] = 'SENSQ';
SUBSTITUTIONS['Thackeray Hall'] = 'THACK';

function values(array)
{
  var returnValue = new Array();

  for(var key in array)
  {
    returnValue.push(array[key]);
  }

  return returnValue;
}

function ampmTimeStringToTime(timeString)
{
  var result;

  if(result = timeString.match(/^(\d?\d):(\d\d)([AP]M)/i))
  {
    var hour = result[1];
    var minute = result[2];
    var ampm = result[3];

    if(hour == 12 && ampm == 'AM')
    {
      hour = 0;
    }
    else if(ampm == 'PM' && hour != 12)
    {
      // Need to force a conversion to integer.
      hour = 12 + (hour * 1);
    }

    // Everything is January 1, 1900.
    timeString = new Date(1900, 0, 1, hour, minute);
  }

  return timeString;
}

function timeToAMPMTimeString(time)
{
  var hour = time.getHours();
  var minute = time.getMinutes();

  var ampm;

  if(hour == 0)
  {
    hour = 12;
    ampm = 'AM';
  }
  else if(hour < 12)
  {
    ampm = 'AM';
  }
  else if(hour == 12)
  {
    ampm = 'PM';
  }
  else
  {
    ampm = 'PM';
    hour -= 12;
  }

  if(minute < 10)
  {
    minute = '' + '0' + minute;
  }

  return hour + ":" + minute + ampm;
}

// Changes the object.
function roundBack15Minutes(t)
{
  var oldMin = t.getMinutes();
  var removeMin = oldMin % 15;
  t.setMinutes(oldMin - removeMin);
  return t;
}

// Changes the object.
function roundForward15Minutes(t)
{
  var oldMin = t.getMinutes();

  // I don't know if I can do setMinutes(60) to force it to the next hour, so I won't try to be cute.
  if(oldMin == 0)
  {
  }
  else if(oldMin <= 15)
  {
    t.setMinutes(15);
  }
  else if(oldMin <= 30)
  {
    t.setMinutes(30);
  }
  else if(oldMin <= 45)
  {
    t.setMinutes(45);
  }
  else
  {
    // In theory, this would fail at 11:46 PM, but no one has classes then.
    t.setMinutes(0);
    t.setHours(t.getHours() + 1);
  }

  return t;
}

function substitute(s)
{
  return s;
  // TODO substitutions
/*  foreach my $substitution (keys %SUBSTITUTIONS)
  {
    $s =~ s/$substitution/$SUBSTITUTIONS{$substitution}/;
  }

  return $s;*/
}

// Changes the object.
// Only works for 0, 15, 30, 60, and again, there's probably a problem with midnight.
function add15Minutes(t)
{
  var h = t.getHours();
  var m = t.getMinutes();
  if(m == 0)
  {
    m = 15;
  }
  else if(m == 15)
  {
    m = 30;
  }
  else if(m == 30)
  {
    m = 45;
  }
  else
  {
    m = 0;
    h += 1;
  }
  t.setHours(h);
  t.setMinutes(m);

  return t;
}

// Maps '$day$startTime' to a map, which contains a variety of information about the class OR a notice that a previously begun class continues through this time.
var events;

function format()
{
  // Reset here in case the user wants to format multiple schedules from this window.
  events = new Array();

  var input = document.getElementById('scheduleInput').value;

  // Change \n to \f so that I can treat this as a single line while still recognizing "line breaks"
  input = input.replace(/\n/g, '\f');

  input = input.replace(/.*?Show Enrolled Classes\s+Show Waitlisted Classes\s+Show Dropped Classes\s+/, '');

  // The use of [ ] and [#] is a holdover from when this was a Perl script and I was using /x.
  var classRegexp =
    /.*?Section\s+Component\s+Description\s+Grading[ ]Option\s+Grade\s+Units\s+Status\s+([^\f]*?)\f\s+Cls[#]:\s+\f\s+(\d+)\f\s+(\d+)\f\s+([^\f]+)\f\s+([^\f]+)\f\s+[^\f]+\f\s+(?:[^\f]*)\f\s+[^\f]+\f\s+[\w ]+\f\s+(.*?)Academic[ ]Calendar\s+/g;

  var otherRegexp =
    /-\f\s+(?:([\d:APM]+)\f\s+([\d:APM]+)\f\s+([MonTuesWedThursFriSat,]+)|(Schedule:\s+TBA))\f\s+(?:Location:\s+)?([^\f]+)\f\s+\d\d\/\d\d\/\d\d\d\d\f\s+\d\d\/\d\d\/\d\d\d\d\f\s+Instructor:\s+([\w,.\s\t\f]+?)\s+/g;

  var classResult;
  while(classResult = classRegexp.exec(input))
  {
    var shortName = substitute(classResult[1]);
    var crn = classResult[2];
    var section = classResult[3];
    var lectureRecitation = substitute(classResult[4]);
    var longName = classResult[5];

    var otherClassInfo = classResult[6];
    var otherResult;
    while(otherResult = otherRegexp.exec(otherClassInfo))
    {
      // Check for Schedule TBA.
      if(otherResult[4])
      {
        continue;
      }

      var startTime = roundBack15Minutes(ampmTimeStringToTime(otherResult[1]));
      var endTime = roundForward15Minutes(ampmTimeStringToTime(otherResult[2]));
      var daysString = otherResult[3];
      // $4 is for Schedule TBA, above.
      var place = substitute(otherResult[5]);
      var instructor = otherResult[6];

      var days = daysString.split(/,/);
      for(var i in days)
      {
        var day = days[i];

        var startEvent = new Array();
        startEvent["shortName"] = shortName;
        startEvent["crn"] = crn;
        startEvent["section"] = section;
        startEvent["lectureRecitation"] = lectureRecitation;
        startEvent["longName"] = longName;
        startEvent["startTime"] = startTime;
        startEvent["endTime"] = endTime;
        startEvent["day"] = day;
        startEvent["place"] = place;
        startEvent["instructor"] = instructor;
        startEvent["isContinuation"] = false;

        events[day + startTime] = startEvent;

        var time = new Date(startTime);
        for(add15Minutes(time); time < endTime; add15Minutes(time))
        {
          var continuedEvent = new Array();
          // Need to include startTime and endTime, or sorting will complain.
          // Since sorting is used only to determine min and max (yes, that's probably overkill, but CPU is cheap), it doesn't matter that we're copying the start and end time without including any other information.
          continuedEvent["startTime"] = startTime;
          continuedEvent["endTime"] = endTime;
          continuedEvent["isContinuation"] = true;
          events[day + time] = continuedEvent;
        }
      }
    }
  }

  var win = window.open('tempschedule.html');
  if(win)
  {
    win.addEventListener('load', function(e)
                         {

                           var doc = win.document;

                           function clear(node)
                           {
                             var range = doc.createRange();
                             range.selectNodeContents(node);
                             range.deleteContents();
                           }

                           // Convenience function to create XHTML nodes.
                           function h(type, clazz)
                           {
                             var node = doc.createElementNS('http://www.w3.org/1999/xhtml', type);
                             if(clazz)
                             {
                               node.setAttribute('class', clazz);
                             }
                             return node;
                           }

                           // Convenience function to create XHTML nodes with text node children.
                           function t(text, type, clazz)
                           {
                             var textNode = doc.createTextNode(text);

                             if(type)
                             {
                               var node = h(type, clazz);
                               node.appendChild(textNode);
                               return node;
                             }
                             else
                             {
                               return textNode;
                             }
                           }

                           var nbsp = "\u00a0";

                           clear(doc.getElementById('instructions'));

                           var div = doc.getElementById('scheduleDiv');
                           var table = h('table');

                           {
                             var thead = h('thead');
                             var tr = h('tr');

                             // Time column.
                             tr.appendChild(t(nbsp, 'th', 'dayheader'));

                             for(var i in DAYS)
                             {
                               var day = DAYS[i];
                               tr.appendChild(t(day, 'th', 'dayheader'));
                             }

                             thead.appendChild(tr);
                             table.appendChild(thead);
                           }

                           function cmp(a, b)
                           {
                             if(a < b)
                             {
                               return -1;
                             }
                             else if(a == b)
                             {
                               return 0;
                             }
                             else
                             {
                               return 1;
                             }
                           }

                           function cmpByField(a, b, field)
                           {
                             return cmp(a[field], b[field]);
                           }

                           function startTimeComparator(a, b)
                           {
                             return cmpByField(a, b, 'startTime');
                           }

                           function endTimeComparator(a, b)
                           {
                             return cmpByField(a, b, 'endTime');
                           }

                           // Sort to find the earlier start time and the latest end time.
                           // Perhaps that's excessive, but we're sorting only a dozen or so records.

                           var startTimeSorted = values(events).sort(startTimeComparator);
                           var endTimeSorted = values(events).sort(endTimeComparator);

                           // Grab the start time of the *first* element.
                           // shift() and pop() appear to be Netscape-only?
                           // Or maybe the problem was that I was using startTimeSorted / endTimeSorted as both variable names and functions...
//                           var earliestStart = startTimeSorted.shift()['startTime'];
                           var earliestStart = startTimeSorted[0]['startTime'];
                           // Grab the end time of the *last* element.
//                           var latestEnd = endTimeSorted.pop()['endTime'];
                           var latestEnd = endTimeSorted[endTimeSorted.length - 1]['endTime'];

                           {
                             var tbody = h('tbody');

                             for(var time = earliestStart; time < latestEnd; add15Minutes(time))
                             {
                               var tr = h('tr');

                               var hourRowClass;
                               if(time.getMinutes() == 0)
                               {
                                 hourRowClass = 'hour';

                                 tr.appendChild(t(timeToAMPMTimeString(time), 'th', hourRowClass));
                               }
                               else
                               {

                                 hourRowClass = null;
                                 tr.appendChild(t(nbsp, 'th'));
                               }

                               for(var i in DAYS)
                               {
                                 var day = DAYS[i];

                                 var event = events[day + time];
                                 if(event)
                                 {
                                   if(event['isContinuation'])
                                   {
                                     // Inside a rowspan.
                                   }
                                   else
                                   {
                                     // Result of subtraction is in milliseconds.
                                     var lengthInMinutes = (event['endTime'] - event['startTime']) / 1000 / 60;
                                     var lengthInRows = lengthInMinutes / 15;

                                     // TODO may need to encode & if I use full class names
                                     var nameNode = t(event['shortName'] + " " + event['lectureRecitation'], 'strong');
                                     var brNode = h('br');
                                     var placeNode = t(event['place']);

                                     var td = h('td', 'class');
                                     td.setAttribute('rowspan', lengthInRows);
                                     td.appendChild(nameNode);
                                     td.appendChild(brNode);
                                     td.appendChild(placeNode);

                                     tr.appendChild(td);
                                   }
                                 }
                                 else
                                 {
                                   tr.appendChild(t(nbsp, 'td', hourRowClass));
                                 }
                               }

                               tbody.appendChild(tr);
                             }

                             table.appendChild(tbody);
                           }

                           div.appendChild(table);
                         }, false);
  }
  else
  {
    alert("It looks like your popup blocker is preventing me from opening a window for your schedule.");
  }
}

document.getElementById('formatButton').addEventListener('click', format, false);

