//
// Copyright (c) 2005-6 Chris Purcell.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
//
// Any derivative works must indicate where the above copyright does not
// apply.
// 

//// Work with most browsers /////////////////////////////////////////////////
function $() {
  var elements = new Array();
  for (var i = 0; i < arguments.length; i++) {
    var element = arguments[i];
    if (typeof element == 'string') {
      if (document.getElementById) {
        element = document.getElementById(element);
      } else if (document.all) {
        element = document.all[element];
      }
    }

    if (arguments.length == 1) {
      return element;
    }

    elements.push(element);
  }

  return elements;
}

function cmp(a,b) { return (a < b)?-1:(a > b)?1:0; }

// Internet Explorer cannot set the name attribute of an element
// OTOH, few other browsers understand IE's hack
document.createNamedElement = function(type, name) {
  var element;
  try {
    element = document.createElement('<'+type+' name="'+name+'">');
  } catch (e) { }
  if (!element || !element.name) { // Not in IE, then
    element = document.createElement(type)
    element.name = name;
  }
  return element;
}

// Early Javascript engines do not provide a push method for Arrays
if (typeof(Array.prototype.push) == "undefined") {
  Array.prototype.push = function(element) {
    this[this.length] = element;
  }
}

// Split a string, including the delimiters in the returned array
String.prototype.splitWithDelimiters = function(delimiter) {
  var results = new Array();
  var trunc = new String(this);
  var match;
  while (match = delimiter.exec(trunc)) {
    if (match.index > 0)
      results.push(trunc.substring(0,match.index));
    results.push(match[0]);
    trunc = trunc.substr(match.index + match[0].length);
  }
  if (trunc.length > 0)
    results.push(trunc);
  return results;
}

// Date is awkward about providing textual names for days/months/etc
Date.prototype.txtMonth = function() {
  var theMonths = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun",
                    "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ];
  return theMonths[this.getMonth()];
}
Date.prototype.txtLongMonth = function() {
  var theLongMonths = [ "January", "February", "March", "April", "May",
                        "June", "July", "August", "September", "October",
                        "November", "December" ];
  return theLongMonths[this.getMonth()];
}
Date.prototype.txtLongDay = function() {
  var theLongDays = [ "Sunday", "Monday", "Tuesday", "Wednesday",
                      "Thursday", "Friday", "Saturday" ];
  return theLongDays[this.getDay()];
}

//// SortedArray class
function SortedArray(compareFn) {
  this.elements = new Array();
  this.compare = compareFn;
  this.length = function() {
    return this.elements.length;
  }
  this.at = function(index) { // O(1)
    return this.elements[index];
  }
  this._find = function(element) { // O(log n)
    var low = 0, high = this.elements.length;
    while (low < high) {
      k = Math.floor((low + high)/2);
      var cval = this.compare(this.elements[k], element);
      if (cval == 0)
        return k;
      else if (cval < 0)
        low = k + 1;
      else // cval > 0
        high = k;
    }
    return low; // low == high at this point
  }
  this.match = function(element,doesMatch) { // O(log n)
    var i = this._find(element);
    if (i < this.elements.length) {
      var foundElem = this.elements[i];
      if (this.compare(foundElem, element) == 0)
        if (doesMatch(foundElem, element))
          return foundElem;
    }
    return null;
  }
  this.insert = function(element) { // O(n)
    var i = this._find(element);
    if (i == this.elements.length ||
        this.compare(this.elements[i], element) != 0)
      this.elements.splice(i,0,element);
  }
  this.remove = function(element,shouldRemove) { // O(n)
    if (typeof(shouldRemove) == "undefined")
      shouldRemove = function(a,b) { return a == b; };
    var i = this._find(element);
    if (i < this.elements.length) {
      var matchedElement = this.elements[i];
      if (this.compare(matchedElement, element) == 0 &&
          shouldRemove(matchedElement, element)) {
        this.elements.splice(i,1);
        return matchedElement;
      }
    }
    return null;
  }
  this.mergePresortedArray = function(array) { // O(n)
    // Merge new array into existing one
    // Prefer to keep existing entries if there are matches
    var destArray = new Array();
    var i = 0, j = 0;
    while (i < this.elements.length && j < array.length) {
      var compareResult = this.compare(this.elements[i], array[j]);
      destArray.push((compareResult <= 0)?this.elements[i]:array[j]);
      if (compareResult <= 0)
        i++;
      if (compareResult >= 0)
        j++;
    }
    for (; i < this.elements.length; i++)
      destArray.push(this.elements[i]);
    for (; j < array.length; j++)
      destArray.push(array[j]);
    this.elements = destArray;
  }
}

// IE does not accept function objects when creating intervals
// The following adds this functionality
//   (but leaks memory when clearTimeout called)
window.setIntervalWithFunction = function(fnRef, msecs) {
  var index = window.setIntervalWithFunction.fnobjects.length;
  window.setIntervalWithFunction.fnobjects[index] = fnRef;
  return window.setInterval("window.setIntervalWithFunction.fnobjects[" +
                            index + "]()", msecs);
}
window.setIntervalWithFunction.fnobjects = new Array();


//// Set up page according to initial customization settings /////////////////
window.runOnLoad = function(fn) {
  var first = window.onload;
  var last = fn;
  window.onload = function() {
    window.onload = first;
    if (first)
      first();
    last();
  }
}

//// Cookie extraction ///////////////////////////////////////////////////////
function getCookie(name) {
  var cookies = document.cookie.split("; ");
  for (var i = 0; i < cookies.length; i++) {
    var cookieCrumbs = cookies[i].split("=");
    if (cookieCrumbs[0] == name) {
      return unescape(cookieCrumbs[1]);
    }
  }
  return null;
}
function setCookie(string) {
  var curCookie = string;
  document.cookie = curCookie;
}

//// Animation effects ///////////////////////////////////////////////////////
// Highlight a block with a temporary red border
function redBorderAnimation(_startTime, _element) {
  this.startTime = _startTime;
  this.element = _element;
} // Default to ten seconds of solid red, then a fade over 3 seconds
redBorderAnimation.prototype.solidRedPeriod = 10 * 1000;
redBorderAnimation.prototype.fadeOutPeriod  = 3 * 1000;
redBorderAnimation.prototype.stillAnimating = function(now) {
    var age = now - this.startTime;
    var elem = this.element();
    if (age >= this.solidRedPeriod + this.fadeOutPeriod) {
      if (elem !== null)
        elem.style.border = "none";
      return false;
    } else {
      var colorVal = (age < this.solidRedPeriod) ? 0 :
                     Math.floor((age - this.solidRedPeriod)
                                * 256 / this.fadeOutPeriod);
      color = "rgb(256," + colorVal + "," + colorVal + ")";
      if (elem !== null)
        elem.style.border = "medium solid " + color;
      return true;
    }
  }

// Manage all animations
function animationManager() {
  this.animations = new Array();
}
animationManager.prototype.periodic = function() {
  if (this.animations.length == 0)
    return;
  var now = new Date();
  for (var i = this.animations.length - 1; i >= 0; i--)
    if (!this.animations[i].stillAnimating(now))
      this.animations.splice(i, 1);
}
animationManager.prototype.push = function(animation) {
  this.animations.push(animation);
}
var animations = new animationManager();


//// XML HTTP Request object /////////////////////////////////////////////////
function newRequest() {
  var req = false;
  if (window.XMLHttpRequest) {
    try {
      req = new XMLHttpRequest();
    } catch(e) {
      req = false;
    }
  } else if (window.ActiveXObject) {
    try {
      req = new ActiveXObject("Msxml2.XMLHTTP");
    } catch(e) {
      try {
        req = new ActiveXObject("Microsoft.XMLHTTP");
      } catch(e) {
        req = false;
      }
    }
  }
  return req;
}
function fireRequest(request,type,url,cb,async,ct,cl,c) {
  var req = request;
  var callback = cb;
  function bindCallback() {
    if (req.readyState == 4) {
      callback(req);
    }
  }
  if (async)
    req.onreadystatechange = bindCallback;
  req.open(type, url, async);
  if (type == "POST") {
    req.setRequestHeader("Content-Type", ct);
    req.setRequestHeader("Content-Length", cl);
    req.setRequestHeader("Connection", "close");
    req.send(c);
  } else {
    req.send(null);
  }
  if (!async && req.readyState == 4)
    callback(req);
}

//// Customization interface /////////////////////////////////////////////////
recentChangesListing.prototype.choiceForm = function() {
  var self = this;
  var styles = [ "Gray block", "Shaded block", "Gray inline", "Bold inline" ];
  var currentStyle = this.rcStyle();

  var form = document.createElement("form");
  form.className = "configuration-form";
  form.onsubmit = function() {
    return false;
  };

  form.appendChild(document.createTextNode('Styles: '));
  for (var i = 0; i < styles.length; i++) {
    var input = document.createNamedElement("input","rc_style");
    input.type = "radio";
    input.id = "rc_style_" + i;
    if (i == currentStyle)
      input.defaultChecked = input.checked = true;
    input.onclick = function() {
      self.setRCStyle(this.onclick.val); return true;
    };
    input.onclick.val = i;
    form.appendChild(input);

    var label = document.createElement("label");
    label.htmlFor = "rc_style_" + i;
    label.innerHTML = ' ' + styles[i];
    form.appendChild(label);
    form.appendChild(document.createTextNode(" "));
  }
  form.appendChild(document.createElement("br"));
  
  var label = document.createElement("label");
  label.htmlFor = "rc_days";
  label.innerHTML = "Days to display:";
  form.appendChild(label);
  form.appendChild(document.createTextNode(" "));
  var input = document.createNamedElement("input", "rc_days");
  input.type = "text";
  input.size = 5;
  input.id = "rc_days";
  input.value = this.rcDays();
  input.onchange = function() {
    self.setRCDays(parseInt(this.value));
  };
  form.appendChild(input);
  form.appendChild(document.createTextNode(" "));

  input = document.createNamedElement("input", "rc_autorefresh");
  input.type = "checkbox";
  input.id = "rc_autorefresh";
  if (this.autoRefresh())
    input.defaultChecked = input.checked = true;
  input.onclick = function() {
    self.setAutoRefresh(this.checked); return true;
  };
  form.appendChild(input);
  form.appendChild(document.createTextNode(" "));
  label = document.createElement("label");
  label.htmlFor = "rc_autorefresh";
  label.appendChild(document.createTextNode("Autorefresh"));
  form.appendChild(label);
  form.appendChild(document.createTextNode(" "));

  form.appendChild(document.createTextNode("("));
  input = document.createNamedElement("input", "rc_display_status");
  this.rcDisplayStatus = input;
  input.type = "checkbox";
  input.id = "rc_display_status";
  if (!this.autoRefresh())
    input.disabled = true;
  if (this.displayStatus())
    input.defaultChecked = input.checked = true;
  input.onclick = function() {
    self.setDisplayStatus(this.checked); return true;
  };
  form.appendChild(input);
  form.appendChild(document.createTextNode(" "));
  label = document.createElement("label");
  label.htmlFor = "rc_display_status";
  label.appendChild(document.createTextNode("Status"));
  form.appendChild(label);
  form.appendChild(document.createTextNode(")"));
  form.appendChild(document.createTextNode(" "));

  form.appendChild(document.createElement("br"));
  input = document.createNamedElement("input", "rc_use_ajax");
  input.type = "checkbox";
  input.id = "rc_use_ajax";
  input.defaultChecked = true;
  input.onclick = function() {
    self.disableAJAX();
    document.location = 'RecentChanges';
    return false;
  };
  form.appendChild(input);
  form.appendChild(document.createTextNode(" "));
  label = document.createElement("label");
  label.htmlFor = "rc_use_ajax";
  label.appendChild(document.createTextNode(
    "Use Javascript for client-side filtering (recommended)"));
  form.appendChild(label);
  
  return form;
}

recentChangesListing.prototype.rcStyleCookie = "rcStyle";
recentChangesListing.prototype.rcStyle = function() {
  var style = getCookie(this.rcStyleCookie);
  if (style === null) style = "1";
  return style;
}
recentChangesListing.prototype.setRCStyle = function(style) {
  var exp = new Date((new Date()).getTime() + 5 * 365 * 24*60*60*1000);
  setCookie(this.rcStyleCookie+"="+style+"; expires="+exp.toGMTString());
  this.updateRC();
}
recentChangesListing.prototype.rcDaysCookie = "rcDays";
recentChangesListing.prototype.rcDays = function() {
  var days = getCookie(this.rcDaysCookie);
  if (days === null || isNaN(days)) days = 5;
  if (days < 1) days = 1/0;
  return days;
}
recentChangesListing.prototype.setRCDays = function(days) {
  var exp = new Date((new Date()).getTime() + 5 * 365 * 24*60*60*1000);
  if (days < 1) days = 1;
  if (isNaN(days)) days = 5;
  setCookie(this.rcDaysCookie+"="+days+"; expires="+exp.toGMTString());
  this.loadJSRecentChanges(false);
}
  // Interface with CGI script via cookie
recentChangesListing.prototype.cgiRCCookie = "RecentChanges";
recentChangesListing.prototype.shouldUseAJAX = function() {
  var cookie = getCookie(this.cgiRCCookie);
  return (cookie === null || cookie.substr(0,6) == "days,0");
}
recentChangesListing.prototype.disableAJAX = function() {
  var cookie = "days,5";
  var exp = new Date((new Date()).getTime() + 5 * 365 * 24*60*60*1000);
  setCookie(this.cgiRCCookie+"="+cookie+"; expires="+exp.toGMTString());
}
recentChangesListing.prototype.disableCGIRC = function() {
  var cookie = "days,0";
  var exp = new Date((new Date()).getTime() + 5 * 365 * 24*60*60*1000);
  setCookie(this.cgiRCCookie+"="+cookie+"; expires="+exp.toGMTString());
}
  // Allow user to filter out seen changes
recentChangesListing.prototype.rcDisplayAfterCookie = "rcDisplayAfter";
recentChangesListing.prototype.rcDisplayAfterDate = function() {
  var date = getCookie(this.rcDisplayAfterCookie);
  if (date >= 1) {
    var dateObj = new Date();
    dateObj.setTime(date);
    return dateObj;
  } else
    return null;
}
recentChangesListing.prototype.rcSetDisplayAfterDate = function(date) {
  var exp = new Date((new Date()).getTime() + 5 * 365 * 24*60*60*1000);
  setCookie(this.rcDisplayAfterCookie+"="+date.getTime()+
            "; expires="+exp.toGMTString());
  this.updateRC();
}
recentChangesListing.prototype.rcSetDisplayAllRecent = function() {
  var exp = new Date(0);
  setCookie(this.rcDisplayAfterCookie+"="+"; expires="+exp.toGMTString());
  this.updateRC();
}
  // Allow user to filter out edit categories
recentChangesListing.prototype.rcCategoryCookie = "rcCategory_";
recentChangesListing.prototype.rcShowCategory = function(cat) {
  var show = getCookie(this.rcCategoryCookie + cat)
  return (show == 1);
}
recentChangesListing.prototype.rcSetShowCategory = function(cat,vis) {
  var exp;
  if (vis) {
    exp = new Date((new Date()).getTime() + 5 * 365 * 24*60*60*1000);
    setCookie(this.rcCategoryCookie+cat+"=1"+"; expires="+exp.toGMTString());
    this.updateRC();
  } else {
    exp = new Date(0);
    setCookie(this.rcCategoryCookie+cat+"=0"+"; expires="+exp.toGMTString());
    this.updateRC();
  }
}
  // 0 = refresh with status
  // 1 = refresh, no status
  // 2 = no refresh (hidden status on)
  // 3 = no refresh (hidden status off)
recentChangesListing.prototype.autoRefreshCookie = "autoRefresh";
recentChangesListing.prototype.getAutoRefreshCookie = function() {
  var refresh = getCookie(this.autoRefreshCookie);
  if (refresh === null)
    refresh = 1;
  return refresh;
}
recentChangesListing.prototype.autoRefresh = function() {
  var refresh = this.getAutoRefreshCookie()
  return (refresh < 2);
}
recentChangesListing.prototype.setAutoRefresh = function(checked) {
  var exp = new Date((new Date()).getTime() + 5 * 365 * 24*60*60*1000);
  var refresh = this.getAutoRefreshCookie();
  if (checked) {
    setCookie(this.autoRefreshCookie + '=' + ((refresh == 3)?1:0)+
              "; expires="+exp.toGMTString());
    this.rcDisplayStatus.disabled = false;
  } else {
    setCookie(this.autoRefreshCookie + '=' + ((refresh == 1)?3:2)+
              "; expires="+exp.toGMTString());
    this.rcDisplayStatus.disabled = true;
  }
  this.AJAXPeriodic();
}
recentChangesListing.prototype.displayStatus = function() {
  var refresh = this.getAutoRefreshCookie();
  return (refresh < 1 || refresh == 2);
}
recentChangesListing.prototype.setDisplayStatus = function(checked) {
  var exp = new Date((new Date()).getTime() + 5 * 365 * 24*60*60*1000);
  if (checked) {
    setCookie(this.autoRefreshCookie + '=' + 0+
              "; expires="+exp.toGMTString());
  } else {
    setCookie(this.autoRefreshCookie + '=' + 1+
              "; expires="+exp.toGMTString());
  }
  this.AJAXPeriodic();
}

//// Manage an individual entry in the Recent Changes listing ////////////////
function RCEntry(title,link,year,month,day,hours,minutes,seconds,
                 description,contributors,otherLinks) {
  this.title = title;
  this.link = link;
  this.date = new Date(0);
  this.date.setUTCFullYear(year);
  this.date.setUTCMonth(month-1);
  this.date.setUTCDate(day);
  this.date.setUTCHours(hours);
  this.date.setUTCMinutes(minutes);
  this.date.setUTCSeconds(seconds);
  var year = this.date.getFullYear();
  var month = this.date.getMonth(); if (month < 10) month = "0" + month;
  var day = this.date.getDate(); if (day < 10) day = "0" + day;
  this.dayText = this.date.txtMonth() + " " + day + ", " + year;
  this.dayID = "" + year + month + day;
  this.description = description;
  this.contributors = contributors;
  this.otherLinks = otherLinks;
}
RCEntry.prototype.li = null;
RCEntry.prototype.day = null;
RCEntry.prototype.html = function(style) {
  var html = '<a href="' + this.link + '">' + this.title + '</a> ';
  var match = this.link.match(/^(.*?\?.*?)\//);
  if( match ) {
    title = this.title.match( /^(.*?)\// );
    html = html + '<strong>discussing <a href="' + match[1] + '">' + title[1] + '</a></strong>'
  }
  html = html + ' at ';
  html += this.date.toLocaleTimeString();
  html += " " + this.otherLinks;
  this.oldDesc = this.filteredDesc;
  var authorString = "";
  if (this.contributors)
    authorString = ' . . . . . ' + this.contributors;
  switch (style) {
  case "3":
    if (this.filteredDesc)
      html += ' <strong class="bold-inline-rc">[' + this.filteredDesc +
              ']</strong>';
    html += authorString;
    break;
  case "2":
    if (this.filteredDesc)
      html += ' <em class="gray-inline-rc">' + this.filteredDesc + '</em>';
    html += authorString;
    break;
  case "1":
    html += authorString + "<br>\n";
    if (this.filteredDesc)
      html += '<span class="shaded-block-rc">' + this.filteredDesc +'</span>';
    break;
  default:
    html += authorString + "<br>\n";
    if (this.filteredDesc)
      html += '<span class="gray-block-rc">' + this.filteredDesc + '</span>';
    break;
  }
  return html;
}

//// RC control object ///////////////////////////////////////////////////////
function recentChangesListing(element) {
  this.root = element;
  this.rcEntriesByTitle = new Object();
  this.rcEntries = new SortedArray(function(a,b) {
    var first = cmp(a.date.valueOf(), b.date.valueOf());
    if (first != 0)
      return first;
    return cmp(a.title, b.title);
  });
  this.displayedDays = new Array();
  this.displayedEntries = new Array();
  this.AJAXIntervals = new Array();
  if (this.shouldUseAJAX())
    this.startAJAX();
}

//// Initial setup of divs ///////////////////////////////////////////////////
recentChangesListing.prototype.AJAXUIShown = false;
recentChangesListing.prototype.setupRC = function() {
  if (!this.AJAXUIShown) {
    this.rcHeader = this.root.firstChild;
    this.root.removeChild(this.root.firstChild);
    html = this.root.innerHTML;
    this.root.innerHTML = '';
    this.root.appendChild(this.rcHeader);
    this.rcErrors = document.createElement("span");
    this.rcErrors.className = "AJAX-message";
    this.root.appendChild(this.rcErrors);
    this.rcNotify = document.createElement("span");
    this.rcNotify.className = "AJAX-message";
    this.root.appendChild(this.rcNotify);
    this.newChanges = document.createElement("span");
    this.newChanges.paragraph = null;
    this.newChanges.listNew = null;
    this.newChanges.listAll = null;
    this.newChanges.comma = document.createTextNode("; ");
    this.root.appendChild(this.newChanges);
    this.editCategoriesForm = document.createElement("span");
    this.editCategories = document.createElement("span");
    this.editCategories.categories = new Object();
    this.root.appendChild(this.editCategoriesForm);
    this.rcUpdates = document.createElement("span");
    this.rcUpdates.innerHTML = html;
    this.root.appendChild(this.rcUpdates);
    this.rcChoices = this.choiceForm();
    this.root.appendChild(this.rcChoices);
    this.AJAXUIShown = true;

    var self = this;
    var fire = function() { self.AJAXPeriodic(); }
    this.AJAXIntervals.push(window.setIntervalWithFunction(fire, 1000));
    this.AJAXIntervals.push(setInterval("animations.periodic()", 200));
    this.disableCGIRC();
  }
}

//// Manage HTML /////////////////////////////////////////////////////////////
recentChangesListing.prototype.updateRCHeader = function(now, days, date) {
  if (date !== null) { // Cut off by "List new changes starting..."
    var html = "Updates since " + date;
    if (this.rcHeader.innerHTML != html)
      this.rcHeader.innerHTML = html;
    delete this.rcDaysElem;
    delete this.rcDayPlural;
  } else if (!isFinite(days)) { // Listing all updates
    if (this.rcHeader.innerHTML != "All updates") {
      this.rcHeader.innerHTML = "All updates";
      delete this.rcDaysElem;
      delete this.rcDayPlural;
    }
  } else if (this.rcDaysElem && this.rcDayPlural) {
    if (this.rcDaysElem.innerHTML != days)
      this.rcDaysElem.data = days;
    if (this.rcDayPlural.innerHTML != ((days == 1)?' ':'s'))
      this.rcDayPlural.data = ((days == 1)?' ':'s');
  } else {
    this.rcHeader.innerHTML = "";
    this.rcHeader.appendChild(document.createTextNode(
      'Updates in the last '));
    this.rcDaysElem = document.createTextNode(days);
    this.rcHeader.appendChild(this.rcDaysElem);
    this.rcHeader.appendChild(document.createTextNode(' day'));
    this.rcDayPlural = document.createTextNode((days == 1)?' ':'s');
    this.rcHeader.appendChild(this.rcDayPlural);
  }
}

recentChangesListing.prototype.updateListNewChanges = function() {
  var lnShownBefore = (this.newChanges.listNew !== null);
  var lnBefore = this.newChanges.listNew;
  var laShownBefore = (this.newChanges.listAll !== null);
  var laBefore = this.newChanges.listAll;
  var displayAfterDate = this.rcDisplayAfterDate();

  // List new changes starting from <date>
  if (this.latestEntryDate === null) {
    this.newChanges.listNew = null;
  } else if (this.newChanges.listNew === null) {
    this.newChanges.listNew = document.createElement("a");
    this.newChanges.listNew.href = "";
    var self = this;
    this.newChanges.listNew.onclick = function() {
      self.rcSetDisplayAfterDate(self.latestEntryDate);
      return false;
    }
    var listText = document.createTextNode("List new changes starting from ");
    this.newChanges.listNew.appendChild(listText);
    var latestChange = document.createTextNode(this.latestEntryDate);
    this.newChanges.latestChange = latestChange;
    this.newChanges.listNew.appendChild(this.newChanges.latestChange);
  } else {
    if (this.newChanges.latestChange.data != this.latestEntryDate)
      this.newChanges.latestChange.data = this.latestEntryDate;
  }

  // List all recent changes
  if (displayAfterDate === null) {
    this.newChanges.listAll = null;
  } else if (this.newChanges.listAll === null) {
    this.newChanges.listAll = document.createElement("a");
    this.newChanges.listAll.href = "";
    var self = this;
    this.newChanges.listAll.onclick = function() {
      self.rcSetDisplayAllRecent();
      return false;
    }
    var laText = document.createTextNode("List all recent changes");
    this.newChanges.listAll.appendChild(laText);
  }

  var paragraph = this.newChanges.paragraph;
  var lnShown = (this.newChanges.listNew !== null);
  var laShown = (this.newChanges.listAll !== null);
  if (lnShown || laShown) {
    if (paragraph === null) {
      paragraph = this.newChanges.paragraph = document.createElement("span");
      this.newChanges.appendChild(this.newChanges.paragraph);
    }
  } else {
    if (paragraph !== null) {
      this.newChanges.paragraph = null;
      this.newChanges.removeChild(paragraph);
    }
  }
  if (lnShownBefore && !lnShown) {
    paragraph.removeChild(lnBefore);
    if (laShownBefore)
      paragraph.removeChild(this.newChanges.comma);
  } else if (!lnShownBefore && lnShown) {
    if (laShownBefore) {
      paragraph.insertBefore(this.newChanges.listNew, laBefore);
      paragraph.insertBefore(this.newChanges.comma, laBefore);
    } else {
      paragraph.appendChild(this.newChanges.listNew);
    }
  }
  if (laShownBefore && !laShown) {
    paragraph.removeChild(laBefore);
    if (lnShown)
      paragraph.removeChild(this.newChanges.comma);
  } else if (!laShownBefore && laShown) {
    if (lnShown)
      paragraph.appendChild(this.newChanges.comma);
    paragraph.appendChild(this.newChanges.listAll);
  }
}

recentChangesListing.prototype.updateEditCategories = function(newCats) {
  var noCats = true; // set to false if there are now categories
  var wasNoCats = true; // set to false if there used to be categories
  var editCats = this.editCategories.categories;
  for (var cat in editCats) {
    wasNoCats = false;
    if (!newCats[cat]) {
      this.editCategories.removeChild(editCats[cat]);
      delete editCats[cat];
    }
  }
  for (var cat in newCats) {
    noCats = false;
    if (!editCats[cat]) {
      var prev = null;
      for (var existCat in editCats)
        if ((prev === null || existCat < prev) && existCat > cat)
          prev = existCat;
      var node = document.createElement("span");
      var checkbox = document.createElement("input");
      checkbox.type = "checkbox";
      checkbox.category = cat;
      if (this.rcShowCategory(cat))
        checkbox.defaultChecked = checkbox.checked = true;
      var self = this;
      checkbox.onclick = function() {
        self.rcSetShowCategory(this.category, this.checked); return true;
      };
      node.appendChild(checkbox);
      if (cat == 'fr') {
        node.appendChild(document.createTextNode(" "));
        var frFlag = document.createElement('img');
        frFlag.src = '/meatball/fr.gif';
        frFlag.alt = '';
        frFlag.style.height = '18px';
        frFlag.style.width = '24px';
        node.appendChild(frFlag);
      }
      node.appendChild(document.createTextNode(" " + cat + " "));
      if (prev === null)
        this.editCategories.appendChild(node);
      else
        this.editCategories.insertBefore(node, editCats[prev]);
      editCats[cat] = node;
    }
  }
  if (!noCats && wasNoCats) {
    var form = document.createElement("form");
    form.appendChild(document.createTextNode("Edit categories: "));
    form.appendChild(this.editCategories);
    this.editCategoriesForm.appendChild(form);
  } else if (noCats && !wasNoCats) {
    this.editCategoriesForm.innerHTML = "";
  }
}

recentChangesListing.prototype.latestEntryDate = null;
recentChangesListing.prototype.editCategoryRegex =
    /([*+-] *)?\[ *([A-Za-z]+( *, *[A-Za-z]+)*) *(:[^\]]*)?\][;.,!?]? */;
recentChangesListing.prototype.splitEditCategories = function(match) {
  return match[2].split(/[ ,]+/);
}

  // Update individual RC entries
recentChangesListing.prototype.updateRC = function() {
  var style = this.rcStyle();
  var days  = this.rcDays();
  var displayAfterDate = this.rcDisplayAfterDate();
  var now   = new Date();
  var cutoffPoint = now - days * 24*60*60*1000;
  if (displayAfterDate !== null && displayAfterDate.getTime() > cutoffPoint) {
    this.updateRCHeader(now, days, displayAfterDate);
    cutoffPoint = displayAfterDate.getTime();
  } else {
    this.updateRCHeader(now, days, null);
  }
  

  var seenCategories = new Object();
  var filteredRCEntries = new Array();
  for (i = 0; i < this.rcEntries.length(); i++) {
    var page = this.rcEntries.at(i);
    if (page.date.getTime() > cutoffPoint) {
      var segs = page.description.splitWithDelimiters(this.editCategoryRegex);
      var filtSegs = new Array();
      for (var j = 0; j < segs.length; j++) {
        if ((match = this.editCategoryRegex.exec(segs[j])) !== null) {
          var categories = this.splitEditCategories(match);
          var numVisibleCategories = 0;
          for (var k = 0; k < categories.length; k++) {
            seenCategories[categories[k]] = 1;
            if (this.rcShowCategory(categories[k]))
              numVisibleCategories++;
          }
          if (categories.length == numVisibleCategories)
            filtSegs.push(segs[j]);
        } else {
          filtSegs.push(segs[j]);
        }
      }
      page.filteredDesc = filtSegs.join("");
      if (segs.length == 0 || filtSegs.length > 0)
        filteredRCEntries.push(page);
    }
  }

  // Update document title, "List new changes" and edit category list
  if (filteredRCEntries.length == 0)
    this.latestEntryDate = null;
  else
    this.latestEntryDate = filteredRCEntries[filteredRCEntries.length-1].date;
  document.title = this.rcTitle();
  this.updateListNewChanges();
  this.updateEditCategories(seenCategories);

  // Print "No updates." and stop if there are no updates
  if (filteredRCEntries.length == 0) {
    this.displayedDays = new Array();
    for (i = this.displayedEntries.length - 1; i >= 0; i--) {
      this.displayedEntries[i].li = null;
    }
    this.displayedEntries = new Array();
    this.latestEntryDate = null;
    if (this.rcUpdates.innerHTML != "<p>No updates.</p>")
      this.rcUpdates.innerHTML = "<p>No updates.</p>";
    return;
  }

  // Remove previous text if it's not managed by displayedDays
  if (this.displayedDays.length == 0)
    this.rcUpdates.innerHTML = "";

  // Clear out old entries
  for (i = this.displayedEntries.length - 1; i >= 0; i--) {
    var entry = this.displayedEntries[i];
    var keep = false;
    for (j = 0; j < filteredRCEntries.length; j++)
      if (entry === filteredRCEntries[j])
        keep = true;
    if (!keep) {
      for (j = entry.day.entries.length - 1; j >= 0; j--)
        if (entry.day.entries[j] === entry)
          entry.day.entries.splice(j, 1);
      entry.day.list.removeChild(entry.li);
      entry.li = null;
      this.displayedEntries.splice(i, 1);
    }
  }

  // Add new entries
  for (i = 0; i < filteredRCEntries.length; i++) {
    var entry = filteredRCEntries[i];
    if (entry.li === null) {
      // Find or create HTML list for specific day
      var day = entry.dayID;
      for (j = 0; j < this.displayedDays.length &&
                  this.displayedDays[j].day > day; j++)
        ;
      if (j == this.displayedDays.length || this.displayedDays[j].day < day) {
        // Create internal object for day
        var dayObj = new Object();
        dayObj.day = day;
        dayObj.entries = new Array();

        // Create HTML header and list for day
        dayObj.header = document.createElement("h4");
        dayObj.header.innerHTML = entry.dayText;
        dayObj.list = document.createElement("ul");

        // Insert HTML elements
        if (j == this.displayedDays.length) {
          this.rcUpdates.appendChild(dayObj.header);
          this.rcUpdates.appendChild(dayObj.list);
        } else {
          var nextElem = this.displayedDays[j].header;
          this.rcUpdates.insertBefore(dayObj.header, nextElem);
          this.rcUpdates.insertBefore(dayObj.list, nextElem);
        }
        
        // Splice object into list of days
        this.displayedDays.splice(j, 0, dayObj);
      }
      entry.day = this.displayedDays[j];

      // Add entry to day's array of entries
      var entries = entry.day.entries;
      for (j = 0; j < entries.length && entries[j].date > entry.date; j++)
        ;

      // Insert entry into day's HTML list
      entry.li = document.createElement("li");
      entry.li.innerHTML = entry.html(style);
      entry.style = style;
      var list = entry.day.list;
      if (j == entries.length) {
        entry.day.list.appendChild(entry.li);
      } else {
        entry.day.list.insertBefore(entry.li, entries[j].li);
      }

      // Splice entry into day's array of entries
      entries.splice(j,0,entry);
      this.displayedEntries.push(entry);
    } else if (entry.style != style) {
      entry.li.innerHTML = entry.html(style);
      entry.style = style;
    } else if (entry.oldDesc != entry.filteredDesc)
      entry.li.innerHTML = entry.html(style);
  }

  // Remove empty days
  for (i = this.displayedDays.length - 1; i >= 0; i--) {
    if (this.displayedDays[i].entries.length == 0) {
      this.rcUpdates.removeChild(this.displayedDays[i].header);
      this.rcUpdates.removeChild(this.displayedDays[i].list);
      this.displayedDays.splice(i,1);
    }
  }
}

//// Handle JS feeds /////////////////////////////////////////////////////////
recentChangesListing.prototype.lastParseDate = false;
recentChangesListing.prototype.parseJSFeed = function(js) { // Add new entries
  var ifOlderPage = function(a,b) {
    return (a.link == b.link && (a.date.valueOf() != b.date.valueOf() ||
            a.description != b.description));
  }
  var ifSamePage = function(a,b) {
    return (a.link == b.link && a.date.valueOf() == b.date.valueOf() &&
            a.description == b.description);
  }
  var now   = new Date();

  // Load in Javascript objects
  var pages = new Array();
  var self = this;
  var newEntry = function(title,link,year,month,day,hours,minutes,seconds,
                          description,contributors,otherlinks) {
    var page = new RCEntry(title,link,year,month,day,hours,minutes,seconds,
                           description,contributors,otherlinks);

    // Remove any outdated entries
    if (self.rcEntriesByTitle[page.title] &&
        ifOlderPage(page, self.rcEntriesByTitle[page.title])) {
      self.rcEntries.remove(self.rcEntriesByTitle[page.title]);
      self.rcEntriesByTitle[page.title] = null;
    }

    // Record only new entries
    if (!self.rcEntriesByTitle[page.title]) {
      if (self.lastParseDate && page.date > self.lastParseDate) {
        page.firstParsed = now;
        animations.push(new redBorderAnimation(now, function() {
          return page.li;
        }));
      }
      pages.push(page);
      self.rcEntriesByTitle[page.title] = page;
    }
  }
  eval(js);

  // Merge new objects into existing list
  // Assume server returns objects correctly sorted
  this.rcEntries.mergePresortedArray(pages);

  // Update the RC display
  this.updateRC();
  this.lastParseDate = now;
}

//// Manage asynchronous requests and status field ///////////////////////////
recentChangesListing.prototype.lastModified            = false;
recentChangesListing.prototype.lastRequestTime         = false;
recentChangesListing.prototype.lastErrorMessage        = false;
recentChangesListing.prototype.largestRequestedJSFile  = false;
recentChangesListing.prototype.largestPreviousRequest  = -1;
recentChangesListing.prototype.currentRequest          = false;
function bestJSFile(timespan) {
  if (timespan <= 1)
    return "http://meatballwiki.org/wiki/action=rcjs&days=1";
  if (timespan <= 5)
    return "http://meatballwiki.org/wiki/action=rcjs&days=5";
  if (timespan <= 14)
    return "http://meatballwiki.org/wiki/action=rcjs&days=14";
  if (timespan <= 30)
    return "http://meatballwiki.org/wiki/action=rcjs&days=30";
  if (timespan <= 365)
    return "http://meatballwiki.org/wiki/action=rcjs&days=365";
  return "http://meatballwiki.org/wiki/action=rcjs&days="+timespan;
}

recentChangesListing.prototype.rcTitle = function() {
  var title = "Meatball Wiki: ";
  if (this.latestEntryDate === null)
    title += "No updates";
  else {
    title += "Updated ";
    var now = new Date();
    var latest = this.latestEntryDate;
    if (now.toDateString() == latest.toDateString())
      title += latest.toTimeString();
    else {
      var nowday = new Date(now.getFullYear(),now.getMonth(),now.getDate());
      var latday = new Date(latest.getFullYear(),latest.getMonth(),
                            latest.getDate());
      var daydiff = (nowday.getTime() - latday.getTime())/24/60/60/1000;
      var monthdiff = (now.getMonth() + now.getYear()*12) -
                      (latest.getMonth() + latest.getYear()*12);
      if (daydiff <= 1)
        title += "yesterday";
      else if (daydiff < 7)
        title += latest.txtLongDay();
      else if (daydiff < 14)
        title += "last week";
      else if (monthdiff < 1)
        title += "this month";
      else if (monthdiff < 2)
        title += "last month";
      else if (monthdiff < 12)
        title += latest.txtLongMonth();
      else
        title += latest.getFullYear();
    }
  }
  return title;
}

recentChangesListing.prototype.AJAXPeriodic = function() {
  if (this.rcNotify === null) return;
  if (!this.autoRefresh()) {
    this.rcNotify.innerHTML = '';
    delete this.rcSecs;
  } else {
    var now = new Date();
    if (!this.currentRequest &&
        (!this.lastRequestTime || now - this.lastRequestTime > 60000)) {
      this.loadJSRecentChanges(true);
    }

    if (this.displayStatus()) {
      if (this.lastErrorMessage) {
        if (this.rcErrors.innerHTML != this.lastErrorMessage + '<br>')
          this.rcErrors.innerHTML = this.lastErrorMessage + '<br>';
      } else {
        if (this.rcErrors.innerHTML != '')
          this.rcErrors.innerHTML = '';
      }
      if (this.currentRequest) {
        if (this.rcNotify.innerHTML != '<p>Loading...</p>')
          this.rcNotify.innerHTML = '<p>Loading...</p>';
        delete this.rcSecs;
      } else {
        var secs = Math.floor(60 - (now - this.lastRequestTime)/1000);
        if (!this.rcSecs) {
          this.rcNotify.innerHTML = '';
          var paragraph = document.createElement("p");
          paragraph.className = "AJAX-message";
          paragraph.appendChild(document.createTextNode("Auto-refresh in "));
          this.rcSecs = document.createTextNode(secs);
          paragraph.appendChild(this.rcSecs);
          paragraph.appendChild(document.createTextNode(" second"));
          this.rcSecsPlural = document.createTextNode((secs == 1)?"":"s");
          paragraph.appendChild(this.rcSecsPlural);
          this.rcNotify.appendChild(paragraph);
        } else {
          this.rcSecs.data = secs;
          var ess = (secs==1)?"":"s";
          if (this.rcSecsPlural.data != ess)
            this.rcSecsPlural.data = ess;
        }
      }
    } else {
      this.rcNotify.innerHTML = '';
      delete this.rcSecs;
    }
    if (this.currentRequest)
      document.title = "Loading...";
    else
      document.title = this.rcTitle();
  }
}

recentChangesListing.prototype.fireRequest = function(req, file, async) {
  var self = this;
  var handleFinishedJSRequest = function(req) {
    self.setupRC();
    if (req.status == 200) {
      self.currentRequest = false;
      try { self.lastModified = req.getResponseHeader("Last-Modified"); }
      catch (e) { delete self.lastModified; }
      self.lastErrorMessage = false;
      self.parseJSFeed(req.responseText);
    } else if (req.status == 304) { // Not Modified
      // Do nothing significant
      self.currentRequest = false;
      self.lastErrorMessage = "Last request returned not modified";
    } else if (typeof(req.status) == "undefined") { // Safari bug
      window.location.reload(false);
      self.lastErrorMessage = "Cannot reload page";
      for (var i = 0; i < self.AJAXIntervals.length; i++)
        window.clearInterval(self.AJAXIntervals[i]);
      self.AJAXIntervals = new Array();
      self.AJAXPeriodic();
    } else { // Not a lot we can do except hope the problem goes away
      self.currentRequest = false;
      self.lastErrorMessage = "Last request returned status " + req.status +
                              "<br>" + '"' + req.statusText + '"';
    }
    self.lastRequestTime = new Date();
  }
  r = file.match(/([^?]*)\??(.*)/);
  fireRequest(req, "GET", r[1], handleFinishedJSRequest, async,
              "application/x-www-form-urlencoded", r[2].length, r[2]);
}

recentChangesListing.prototype.loadJSRecentChanges = function(doDownload) {
  // Determine which feed to download
  var days = parseInt(this.rcDays());
  var bestJS = bestJSFile(days);
  var requestJSFile = bestJSFile(0);
  if (   ( this.largestPreviousRequest < days ||
           (isFinite(this.largestPreviousRequest) && !isFinite(days)))
      && (this.largestRequestedJSFile != bestJS)) {
    // We need to download a new, bigger JS file
    if (this.currentRequest)
      this.currentRequest.abort();
    this.currentRequest = false;
    doDownload = true;
    this.largestPreviousRequest = days;
    this.largestRequestedJSFile = bestJS;
    requestJSFile = bestJS;
  }

  // Attempt to download said feed
  if (!this.currentRequest && doDownload) {
    this.lastRequestTime = new Date(); // Avoid tight loop if there's a bug
    var req = newRequest();
    try {
      if (this.lastModified && req.setRequestHeader) {
        req.setRequestHeader('If-Modified-Since',this.lastModified);
      }
    } catch(e) { }
    if (req) {
      this.currentRequest = req;
      this.fireRequest(req, requestJSFile, true);
      this.lastErrorMessage = false;
    } else {
      this.lastErrorMessage = 'Failed to create request object';
    }
  } else {
    this.updateRC();
  }
}

recentChangesListing.prototype.startAJAX = function() {
  var req = newRequest();
  if (req) {
    this.currentRequest = req;
    this.largestPreviousRequest = this.rcDays();
    this.largestRequestedJSFile = bestJSFile(this.largestPreviousRequest);
    this.fireRequest(req, this.largestRequestedJSFile, false);
  }
}

function startAJAX() {
  if ($("recent-changes") !== null)
    new recentChangesListing($("recent-changes"));
}

window.runOnLoad(startAJAX);

function diffFile(pageName, oldrev, newrev) {
  return "http://meatballwiki.org/wiki/action=rawdiff&id="+pageName+
         "&diffrevision="+oldrev+"&revision="+newrev;
}

  // Control page history interface
function HistoryListing(form, changes) {
  var self = this;
  this.form = form;
  this.changes = changes;
  this.revisions = new Object();
  this.canDoAJAX = false;

  this.oldRevButtons = new Array();
  this.newRevButtons = new Array();

  for (var i = 0; i < form.elements.length; i++) {
    var element = form.elements[i];
    switch (element.type) {
    case "radio":
      switch (element.name) {
      case "diffrevision":
        this.oldRevButtons.push(element);
        element.onclick = function() {
          self.changeOldRevision(this.value);
          return true;
        }
        if (element.checked)
          this.oldRev = element.value;
        break;
      case "revision":
        this.newRevButtons.push(element);
        element.onclick = function() {
          self.changeNewRevision(this.value);
          return true;
        }
        if (element.checked)
          this.newRev = element.value;
        break;
      default:
        break;
      }
      break;
    case "hidden":
      switch (element.name) {
      case "id":
        this.pageName = element.value;
        break;
      default:
        break;
      }
      break;
    case "submit":
      this.submit = element;
      break;
    default:
      break;
    }
  }

  if (this.oldRev && this.newRev) {
    this.hideNonsensicalRevisionButtons();
    this.requestChanges(false,false);
  }
}

  // Disable silly choices for diffs (where newRev <= oldRev)
HistoryListing.prototype.hideNonsensicalRevisionButtons = function() {
  if (this.newRev) {
    for (var i = 0; i < this.oldRevButtons.length; i++) {
      var button = this.oldRevButtons[i];
      button.disabled = (parseInt(button.value) >= parseInt(this.newRev));
    }
  }
  if (this.oldRev) {
    for (var i = 0; i < this.newRevButtons.length; i++) {
      var button = this.newRevButtons[i];
      button.disabled = (parseInt(button.value) <= parseInt(this.oldRev));
    }
  }
}

  // Disable non-AJAXian interface to history module
HistoryListing.prototype.disableNonAJAXInterface = function() {
  if (this.submit) {
    this.submit.parentNode.removeChild(this.submit);
    delete this.submit;
  }
  this.canDoAJAX = true;
}

  // Interface to diff cache
HistoryListing.prototype.cacheDiff = function(oldRev, newRev, diff) {
  this.revisions[oldRev+"-"+newRev] = diff;
}
HistoryListing.prototype.cachedDiff = function(oldRev, newRev) {
  return this.revisions[oldRev+"-"+newRev];
}

  // Request new diff from server
HistoryListing.prototype.requestChanges = function(async,display) {
  var self = this;
  var doDisplay = display;
  var oldRev = this.oldRev;
  var newRev = this.newRev;
  var callback = function(req) {
    if (req.status == 200) {
      self.cacheDiff(oldRev, newRev, req.responseText);
      self.disableNonAJAXInterface();
      if (doDisplay)
        self.displayChanges();
    }
  }
  var request = newRequest();
  if (request) {
    this.cacheDiff(this.oldRev, this.newRev, callback);
    var url = diffFile(this.pageName, oldRev, newRev);
    fireRequest(request, "GET",url,callback,async);
  }
}

  // Display diff
HistoryListing.prototype.displayChanges = function() {
  if (this.canDoAJAX && this.oldRev && this.newRev) {
    var diff = this.cachedDiff(this.oldRev, this.newRev);
    switch (typeof(diff)) {
      case "string": // We have the diff cached
        break;
      case "function": // Still loading it
        diff = "<p><img src=\"/meatball/loading-anim.gif\"></p>";
        break;
      default: // Start loading it asynchronously
        diff = "<p><img src=\"/meatball/loading-anim.gif\"></p>";
        this.requestChanges(true,true);
        break;
    }
    this.changes.innerHTML = diff;
  }
}

HistoryListing.prototype.changeOldRevision = function(revID) {
  this.oldRev = revID;
  this.hideNonsensicalRevisionButtons();
  this.displayChanges();
}
HistoryListing.prototype.changeNewRevision = function(revID) {
  this.newRev = revID;
  this.hideNonsensicalRevisionButtons();
  this.displayChanges();
}

function startHistoryAJAX() {
  var form = $("history-form");
  var changes = $("diff-div");
  if (form !== null && changes !== null) {
    form.historyController = new HistoryListing(form, changes);
  }
}

window.runOnLoad(startHistoryAJAX);

