      MSG_JAN = "Jan";
      MSG_FEB = "Feb";
      MSG_MAR = "Mar";
      MSG_APR = "Apr";
      MSG_MAY = "May";
      MSG_JUN = "Jun";
      MSG_JUL = "Jul";
      MSG_AUG = "Aug";
      MSG_SEP = "Sep";
      MSG_OCT = "Oct";
      MSG_NOV = "Nov";
      MSG_DEC = "Dec";
    
      MSG_JANUARY = "January";
      MSG_FEBRUARY = "February";
      MSG_MARCH = "March";
      MSG_APRIL = "April";
      MSG_MAY_LONG = "May";
      MSG_JUNE = "June";
      MSG_JULY = "July";
      MSG_AUGUST = "August";
      MSG_SEPTEMBER = "September";
      MSG_OCTOBER = "October";
      MSG_NOVEMBER = "November";
      MSG_DECEMBER = "December";
    
      MSG_MON = "Mon";
      MSG_TUES = "Tue";
      MSG_WED = "Wed";
      MSG_THUR = "Thu";
      MSG_FRI = "Fri";
      MSG_SAT = "Sat";
      MSG_SUN = "Sun";
    
      MSG_MONDAY = "Monday";
      MSG_TUESDAY = "Tuesday";
      MSG_WEDNESDAY = "Wednesday";
      MSG_THURSDAY = "Thursday";
      MSG_FRIDAY = "Friday";
      MSG_SATURDAY = "Saturday";
      MSG_SUNDAY = "Sunday";

    var MSG_SU = MSG_SUN;
    var MSG_M = MSG_MON;
    var MSG_TU = MSG_TUES;
    var MSG_W = MSG_WED;
    var MSG_TH = MSG_THUR;
    var MSG_F = MSG_FRI;
    var MSG_SA = MSG_SAT;

    var MSG_SUNDAYS = "Sundays";
    var MSG_MONDAYS = "Mondays";
    var MSG_TUESDAYS = "Tuesdays";
    var MSG_WEDNESDAYS = "Wednesdays";
    var MSG_THURSDAYS = "Thursdays";
    var MSG_FRIDAYS = "Fridays";
    var MSG_SATURDAYS = "Saturdays";

    var MSG_ORDINAL_1 = "first";
    var MSG_ORDINAL_2 = "second";
    var MSG_ORDINAL_3 = "third";

/**
 * Partially applies this function to a particular "this object" and zero or 
 * more arguments. The result is a new function with some arguments and the 
 * value of |this| "pre-specified". 
 * 
 * Remaining arguments specified at call-time are appended to the pre-
 * specified ones. 
 *
 * Usage:
 * var barMethBound = fooObj.barMeth.bind(fooObj, "arg1", "arg2");
 * barMethBound("arg3", "arg4");
 *
 * @param thisObj {object} Specifies the object which |this| should point to
 * when the function is run. If the value is null or undefined, it will default
 * to the global object.
 *
 * @returns {function} A partially-applied form of the function bind() was 
 * invoked as a method of.
 */
Function.prototype.bind = function(thisObj, var_args) {
  if (typeof(this) != "function") {
    throw new Error("Bind must be called as a method of a function object.");
  }

  var self = this;

  // The arguments intrinsic is not a true array so it does not have the 
  // splice() method. However, we can borrow Array's splice method on it to do
  // the same thing.

  // Also, it turns out that if you remove items from the arguments object,
  // then the formal parameters which correspond to them stop working. So, 
  // for example, Array.prototype.splice.call(arguemnts, 0, 1) would make the
  // thisObj be undefined. So instead, we remove all arguments *but* the named
  // ones and store them in a separate array.
  
  var staticArgs = Array.prototype.splice.call(arguments, 1, arguments.length);

  return function() { 
    // make a copy of staticArgs (don't modify it because it gets reused for
    // every invocation).
    var args = staticArgs.concat();

    // add all the new arguments
    for (var i = 0; i < arguments.length; i++) {
      args.push(arguments[i]);
    }

    // invoke the original function with the correct thisObj and the combined
    // list of static and dynamic arguments.
    return self.apply(thisObj, args);
  };
}

//------------------------------------------------------------------------
// This file contains common utilities and basic javascript infrastructure.
//
// Notes:
// * Press 'D' to toggle debug mode.
//
// Functions:
//
// - Assertions
// DEPRECATED: Use assert.js
// AssertTrue(): assert an expression. Throws an exception if false.
// Fail(): Throws an exception. (Mark block of code that should be unreachable)
// AssertEquals(): assert that two values are equal.
// AssertNumArgs(): assert number of arguments for the function
// AssertType(): assert that a value has a particular type
//
// - Cookies
// SetCookie(): Sets a cookie.
// ExpireCookie(): Expires a cookie.
// GetCookie(): Gets a cookie value.
//
// - Dynamic HTML/DOM utilities
// MaybeGetElement(): get an element by its id
// GetElement(): get an element by its id
// GetParentNode(): Get the parent of an element
// GetAttribute(): Get attribute value of a DOM node
// SetInnerHTML(): set the inner HTML of a node
// GetInnerHTML(): get the inner HTML of a node
// ClearInnerHTML(): clear the inner HTML of a node
// SetCssStyle(): Sets a CSS property of a node.
// GetStyleProperty(): Get CSS property from a style attribute string
// GetCellIndex(): Get the index of a table cell in a table row
// ShowElement(): Show/hide element by setting the "display" css property.
// ShowBlockElement(): Show/hide block element
// SetButtonText(): Set the text of a button element.
// AppendNewElement(): Create and append a html element to a parent node.
// CreateDIV(): Create a DIV element and append to the document.
// CreateIFRAME(): Create an IFRAME and append to the document.
// HasClass(): check if element has a given class
// AddClass(): add a class to an element
// RemoveClass(): remove a class from an element
//
// - Window/Screen utiltiies
// GetPageOffsetLeft(): get the X page offset of an element
// GetPageOffsetTop(): get the Y page offset of an element
// GetPageOffset(): get the X and Y page offsets of an element
// GetPageOffsetRight() : get X page offset of the right side of an element
// GetPageOffsetRight() : get Y page offset of the bottom of an element
// GetScrollTop(): get the vertical scrolling pos of a window.
// GetScrollLeft(): get the horizontal scrolling pos of a window
// IsScrollAtEnd():  check if window scrollbar has reached its maximum offset
// ScrollTo(): scroll window to a position
// ScrollIntoView(): scroll window so that an element is in view.
// GetWindowWidth(): get width of a window.
// GetWindowHeight(): get height of a window
// GetAvailScreenWidth(): get available screen width
// GetAvailScreenHeight(): get available screen height
// GetNiceWindowHeight(): get a nice height for a new browser window.
// Open{External/Internal}Window(): open a separate window
// CloseWindow(): close a window
//
// - DOM walking utilities
// AnnotateTerms(): find terms in a node and decorate them with some tag
// AnnotateText(): find terms in a text node and decorate them with some tag
//
// - String utilties
// HtmlEscape(): html escapes a string
// HtmlUnescape(): remove html-escaping.
// QuoteEscape(): escape " quotes.
// CollapseWhitespace(): collapse multiple whitespace into one whitespace.
// Trim(): trim whitespace on ends of string
// IsEmpty(): check if CollapseWhiteSpace(String) == ""
// IsLetterOrDigit(): check if a character is a letter or a digit
// ConvertEOLToLF(): normalize the new-lines of a string.
// HtmlEscapeInsertWbrs(): HtmlEscapes and inserts <wbr>s (word break tags)
//   after every n non-space chars and/or after or before certain special chars
//
// - TextArea utilities
// GetCursorPos(): finds the cursor position of a textfield
// SetCursorPos(): sets the cursor position in a textfield
//
// - Array utilities
// FindInArray(): do a linear search to find an element value.
// DeleteArrayElement(): return a new array with a specific value removed.
// CloneObject(): clone an object, copying its values recursively.
// CloneEvent(): clone an event; cannot use CloneObject because it
//               suffers from infinite recursion
//
// - Formatting utilities
// PrintArray(): used to print/generate HTML by combining static text
// and dynamic strings.
// ImageHtml(): create html for an img tag
// FormatJSLink(): formats a link that invokes js code when clicked.
// MakeId3(): formats an id that has two id numbers, eg, foo_3_7
//
// - Timeouts
// SafeTimeout(): sets a timeout with protection against ugly JS-errors
// CancelTimeout(): cancels a timeout with a given ID
// CancelAllTimeouts(): cancels all timeouts on a given window
//
// - Miscellaneous
// IsDefined(): returns true if argument is not undefined
//------------------------------------------------------------------------

// browser detection
function BR_AgentContains_(str) {
  if (str in BR_AgentContains_cache_) {
    return BR_AgentContains_cache_[str];
  }

  return BR_AgentContains_cache_[str] =
    (navigator.userAgent.toLowerCase().indexOf(str) != -1);
}
// We cache the results of the indexOf operation. This gets us a 10x benefit in
// Gecko, 8x in Safari and 4x in MSIE for all of the browser checks
var BR_AgentContains_cache_ = {};

function BR_IsIE() {
  return BR_AgentContains_('msie') && !window.opera;
}

function BR_IsKonqueror() {
  return BR_AgentContains_('konqueror');
}

function BR_IsSafari() {
  return BR_AgentContains_('safari') || BR_IsKonqueror();
}

function BR_IsNav() {
  return !BR_IsIE() &&
         !BR_IsSafari() &&
         BR_AgentContains_('mozilla');
}

function BR_IsWin() {
  return BR_AgentContains_('win');
}

function BR_IsMac() {
  return BR_AgentContains_('macintosh') ||
         BR_AgentContains_('mac_powerpc');
}

function BR_IsLinux() {
  return BR_AgentContains_('linux');
}

var BACKSPACE_KEYCODE = 8;
var COMMA_KEYCODE = 188;                // ',' key
var DEBUG_KEYCODE = 68;                 // 'D' key
var DELETE_KEYCODE = 46;
var DOWN_KEYCODE = 40;                  // DOWN arrow key
var ENTER_KEYCODE = 13;                 // ENTER key
var ESC_KEYCODE = 27;                   // ESC key
var LEFT_KEYCODE = 37;                  // LEFT arrow key
var RIGHT_KEYCODE = 39;                 // RIGHT arrow key
var SPACE_KEYCODE = 32;                 // space bar
var TAB_KEYCODE = 9;                    // TAB key
var UP_KEYCODE = 38;                    // UP arrow key
var SHIFT_KEYCODE = 16;
var PAGE_DOWN_KEYCODE = 34;
var PAGE_UP_KEYCODE = 33;

// This is a "constant" but has different values depending on the browser
function GetSemicolonKeyCode() {
  return BR_IsIE() ? 186 : 59;
}

var MAX_EMAIL_ADDRESS_LENGTH = 320;     // 64 + '@' + 255
var MAX_SIGNATURE_LENGTH = 1000;        // 1000 chars of maximum signature

//------------------------------------------------------------------------
// Assertions
// DEPRECATED: Use assert.js
//------------------------------------------------------------------------
/**
 * DEPRECATED: Use assert.js
 */
function raise(msg) {
  if (typeof Error != 'undefined') {
    throw new Error(msg || 'Assertion Failed');
  } else {
    throw (msg);
  }
}

/**
 * DEPRECATED: Use assert.js
 *
 * Fail() is useful for marking logic paths that should
 * not be reached. For example, if you have a class that uses
 * ints for enums:
 *
 * MyClass.ENUM_FOO = 1;
 * MyClass.ENUM_BAR = 2;
 * MyClass.ENUM_BAZ = 3;
 *
 * And a switch statement elsewhere in your code that
 * has cases for each of these enums, then you can
 * "protect" your code as follows:
 *
 * switch(type) {
 *   case MyClass.ENUM_FOO: doFooThing(); break;
 *   case MyClass.ENUM_BAR: doBarThing(); break;
 *   case MyClass.ENUM_BAZ: doBazThing(); break;
 *   default:
 *     Fail("No enum in MyClass with value: " + type);
 * }
 *
 * This way, if someone introduces a new value for this enum
 * without noticing this switch statement, then the code will
 * fail if the logic allows it to reach the switch with the
 * new value, alerting the developer that he should add a
 * case to the switch to handle the new value he has introduced.
 *
 * @param {String} opt_msg to display for failure
 *                 DEFAULT: "Assertion failed"
 */
function Fail(opt_msg) {
  if (opt_msg === undefined) opt_msg = 'Assertion failed';
  if (IsDefined(DumpError)) DumpError(opt_msg + '\n');
  raise(opt_msg);
}

/**
 * DEPRECATED: Use assert.js
 *
 * Asserts that an expression is true (non-zero and non-null).
 *
 * Note that it is critical not to pass logic
 * with side-effects as the expression for AssertTrue
 * because if the assertions are removed by the
 * JSCompiler, then the expression will be removed
 * as well, in which case the side-effects will
 * be lost. So instead of this:
 *
 *  AssertTrue( criticalComputation() );
 *
 * Do this:
 *
 *  var result = criticalComputation();
 *  AssertTrue(result);
 *
 * @param expression to evaluate
 * @param {String} opt_msg to display if the assertion fails
 *
 */
function AssertTrue(expression, opt_msg) {
  if (!expression) {
    if (opt_msg === undefined) opt_msg = 'Assertion failed';
    Fail(opt_msg);
  }
}

/**
 * DEPRECATED: Use assert.js
 *
 * Asserts that two values are the same.
 *
 * @param {String|Object|Number} val1
 * @param {String|Object|Number} val2
 * @param {String} opt_msg to display if the assertion fails
 */
function AssertEquals(val1, val2, opt_msg) {
  if (val1 != val2) {
    if (opt_msg === undefined) {
      opt_msg = "AssertEquals failed: <" + val1 + "> != <" + val2 + ">";
    }
    Fail(opt_msg);
  }
}

/**
 * DEPRECATED: Use assert.js
 *
 * Asserts that a value is of the provided type.
 *
 *   AssertType(6, Number);
 *   AssertType("ijk", String);
 *   AssertType([], Array);
 *   AssertType({}, Object);
 *   AssertType(ICAL_Date.now(), ICAL_Date);
 *
 * @param value
 * @param type A constructor function
 * @param {String} opt_msg to display if the assertion fails
 */
function AssertType(value, type, opt_msg) {
  // for backwards compatability only
  if (typeof value == type) return;

  if (value || value == "") {
    try {
      if (type == AssertTypeMap[typeof value] || value instanceof type) return;
    } catch (e) { /* failure, type was an illegal argument to instanceof */ }
  }
  if (opt_msg === undefined) {
    if (typeof type == 'function') {
      var match = type.toString().match(/^\s*function\s+([^\s\{]+)/);
      if (match) type = match[1];
    }
    opt_msg = "AssertType failed: <" + value + "> not typeof "+ type;
  }
  Fail(opt_msg);
}

var AssertTypeMap = {
  'string'  : String,
  'number'  : Number,
  'boolean' : Boolean
};

/**
 * DEPRECATED: Use assert.js
 *
 * Asserts that the number of arguments to a
 * function is num. For example:
 *
 * function myFunc(one, two, three) [
 *   AssertNumArgs(3);
 *   ...
 * }
 *
 * myFunc(1, 2); // assertion fails!
 *
 * Note that AssertNumArgs does not take the function
 * as an argument; it is simply used in the context
 * of the function.
 *
 * @param {Number} num of arguments expected
 * @param {String} opt_msg to display if the assertion fails
 */
function AssertNumArgs(num, opt_msg) {
  var caller = AssertNumArgs.caller;  // This is not supported in safari 1.0
  if (caller && caller.arguments.length != num) {
    if (opt_msg === undefined) {
      opt_msg = caller.name + ' expected ' + num + ' arguments '
                  + ' but received ' + caller.arguments.length;
    }
    Fail(opt_msg);
  }
}

//------------------------------------------------------------------------
// Cookies
//------------------------------------------------------------------------
var ILLEGAL_COOKIE_CHARS_RE = /[\s;]/
/**
 * Sets a cookie.
 * The max_age can be -1 to set a session cookie. To expire cookies, use
 * ExpireCookie() instead.
 *
 * @param name The cookie name.
 * @param value The cookie value.
 * @param {Number} opt_max_age The max age in seconds (from now). Use -1 to set a
 *   session cookie. If not provided, the default is -1 (i.e. set a session
 *   cookie).
 * @param {String} opt_path The path of the cookie, or null to not specify a path
 *   attribute (browser will use the full request path). If not provided, the
 *   default is '/' (i.e. path=/).
 * @param {String} opt_domain The domain of the cookie, or null to not specify a domain
 *   attribute (brower will use the full request host name). If not provided,
 *   the default is null (i.e. let browser use full request host name).
 * @return Void.
 */
function SetCookie(name, value, opt_max_age, opt_path, opt_domain) {

  value = '' + value;
  AssertTrue((typeof name == 'string' &&
              typeof value == 'string' &&
              !name.match(ILLEGAL_COOKIE_CHARS_RE) &&
              !value.match(ILLEGAL_COOKIE_CHARS_RE)),
             'trying to set an invalid cookie');

  if (!IsDefined(opt_max_age)) opt_max_age = -1;
  if (!IsDefined(opt_path)) opt_path = '/';
  if (!IsDefined(opt_domain)) opt_domain = null;

  var domain_str = (opt_domain == null) ? '' : ';domain=' + opt_domain;
  var path_str = (opt_path == null) ? '' : ';path=' + opt_path;

  var expires_str;

  // Case 1: Set a session cookie.
  if (opt_max_age < 0) {
    expires_str = '';

  // Case 2: Expire the cookie.
  // Note: We don't tell people about this option in the function doc because
  // we prefer people to use ExpireCookie() to expire cookies.
  } else if (opt_max_age == 0) {
    // Note: Don't use Jan 1, 1970 for date because NS 4.76 will try to convert
    // it to local time, and if the local time is before Jan 1, 1970, then the
    // browser will ignore the Expires attribute altogether.
    var pastDate = new Date(1970, 1 /*Feb*/, 1);  // Feb 1, 1970
    expires_str = ';expires=' + pastDate.toUTCString();

  // Case 3: Set a persistent cookie.
  } else {
    var futureDate = new Date(Now() + opt_max_age * 1000);
    expires_str = ';expires=' + futureDate.toUTCString();
  }

  document.cookie = name + '=' + value + domain_str + path_str + expires_str;
}

var EXPIRED_COOKIE_VALUE = 'EXPIRED';

/**
 * Expires a cookie.
 *
 * @param name The cookie name.
 * @param {String} opt_path The path of the cookie, or null to expire a cookie 
 *   set at the full request path. If not provided, the default is '/' 
 *   (i.e. path=/).
 * @param {String} opt_domain The domain of the cookie, or null to expire a 
 *   cookie set at the full request host name. If not provided, the default is 
 *   null (i.e. cookie at full request host name).
 * @return Void.
 */
function ExpireCookie(name, opt_path, opt_domain) {
  SetCookie(name, EXPIRED_COOKIE_VALUE, 0, opt_path, opt_domain);
}

/** Returns the value for the first cookie with the given name
 * @param name : string
 * @return a string or the empty string if no cookie found.
 */
function GetCookie(name) {
  var nameeq = name + "=";
  var cookie = String(document.cookie);
  for (var pos = -1; (pos = cookie.indexOf(nameeq, pos + 1)) >= 0;) {
    var i = pos;
    // walk back along string skipping whitespace and looking for a ; before
    // the name to make sure that we don't match cookies whose name contains
    // the given name as a suffix.
    while (--i >= 0) {
      var ch = cookie.charAt(i);
      if (ch == ';') {
        i = -1;  // indicate success
        break;
      } else if (' \t'.indexOf(ch) < 0) {
        break;
      }
    }
    if (-1 === i) {  // first cookie in the string or we found a ;
      var end = cookie.indexOf(';', pos);
      if (end < 0) { end = cookie.length; }
      return cookie.substring(pos + nameeq.length, end);
    }
  }
  return "";
}


//------------------------------------------------------------------------
// Time
//------------------------------------------------------------------------
function Now() {
  return (new Date()).getTime();
}

//------------------------------------------------------------------------
// Dynamic HTML/DOM utilities
//------------------------------------------------------------------------
// Gets a element by its id, may return null
function MaybeGetElement(win, id) {
  return win.document.getElementById(id);
}

// Same as MaybeGetElement except that it throws an exception if it's null
function GetElement(win, id) {
  var el = win.document.getElementById(id);
  if (!el) {
    DumpError("Element " + id + " not found.");
  }
  return el;
}

// Gets elements by its id/name
// IE treats getElementsByName as searching over ids, while Moz use names.
// so tags must have both id and name as the same string
function GetElements(win, id) {
  return win.document.getElementsByName(id);
}

// Gets the parent of a html element.
function GetParentNode(n) {
  try {
    return n.parentNode;
  } catch (e) {
    // n.parentNode may throw a permission-denied exception on mozilla
    // (e.g. on text element), ignore this exception.
    return n;
  }
}

function IsDescendant(parent, child) {
  do {
    if (parent === child) return true;
    child = GetParentNode(child);
  } while (child && child !== document.body);
  return false;
}

// Get attribute value of a DOM node
function GetAttribute(node, attribute) {
  if (!node.getAttribute) {
    return null;
  }
  var attr = node.getAttribute(attribute);
  if (BR_IsIE() && attribute == "style") {
    return attr.value;
  } else {
    return attr;
  }
}

// Sets inner html of a html element
function SetInnerHTML(win, id, html) {
  try {
    GetElement(win, id).innerHTML = html;
  } catch (ex) {
    DumpException(ex);
  }
}

// Gets inner-html of a html element
function GetInnerHTML(win, id) {
  try {
    return GetElement(win, id).innerHTML;
  } catch (ex) {
    DumpException(ex);
    return "";
  }
}

// Clears inner html of a html element
function ClearInnerHTML(win, id) {
  try {
    GetElement(win, id).innerHTML = "";
  } catch (ex) {
    DumpException(ex);
  }
}

// Sets a CSS style of an element
function SetCssStyle(win, id, name, value) {
  try {
    var elem = GetElement(win, id);
    elem.style[name] = value;
  } catch (ex) {
    DumpException(ex);
  }
}

// Get CSS property from a style attribute string
function GetStyleProperty(style, name) {
  var i = style.indexOf(name);
  if (i != -1) {
    var j = style.indexOf(";", i);
    if (j == -1) {
      j = style.length;
    }
    // the +1 below is for the colon following the attribute name
    return CollapseWhitespace(style.substring(i + name.length + 1, j));
  }
  return null;
}

// Get the index of a table cell in a table row
function GetCellIndex(cell) {
  // Safari always returns 0, so in this case the value cannot be trusted
  if (cell.cellIndex) {
    return cell.cellIndex;
  } else if (cell.parentNode) {
    return FindInArray(cell.parentNode.cells, cell);
  } else {
    return null;
  }
}

// Show/hide an element.
function ShowElement(el, show) {
  el.style.display = show ? "" : "none";
}

// Show/hide a block element.
// ShowElement() doesn't work if object has an initial class with display:none
function ShowBlockElement(el, show) {
  el.style.display = show ? "block" : "none";
}

// Show/hide an inline element.
// ShowElement() doesn't work when an element starts off display:none.
function ShowInlineElement(el, show) {
  el.style.display = show ? "inline" : "none";
}

// Set the text of a button. This is to get around a bug in mozilla,
// where we can't set the text of a button by setting innerHTML.
function SetButtonText(button, text) {
  button.childNodes[0].nodeValue = text;
}

// Append a new HTML element to a HTML node.
function AppendNewElement(win, parent, tag) {
  var e = win.document.createElement(tag);
  parent.appendChild(e);
  return e;
}

// Finds the child with the given ID, or null if there is node.
// This does not search the children's children.
function FindChildWithID(parent, id) {
  var el;
  for (el = parent.firstChild; el && el.id != id; el = el.nextSibling) {
    // skip
  }
  return el;
}

// Adds a disabled option to the given menu
function AddMenuDisabledOption(win, menu, html) {
  var op = AppendNewElement(win, menu, 'OPTION');
  op.disabled = true;
  op.innerHTML = html;

  return op;
}

// Adds a option to the given menu
function AddMenuOption(win, menu, value, html) {
  var op = AppendNewElement(win, menu, 'OPTION');
  op.value = value;
  op.innerHTML = html;

  return op;
}

// Create a new DIV (append it to the end of the document)
function CreateDIV(win, id) {
  var div = MaybeGetElement(win, id);
  if (!div) {
    div = AppendNewElement(win, win.document.body, "div");
    div.id = id;
  }
  return div;
}

// Create a new IFRAME (append it to the end of the document)
function CreateIFRAME(win, id, url) {
  var iframe = MaybeGetElement(win, id);
  if (!iframe) {
    // We cannot create an IFRAME directly (IE doesn't allow it), so we
    // create a DIV and then insert an IFRAME.
    // We also give the IFRAME a name (same as id)
    var div = AppendNewElement(win, win.document.body, "div");
    div.innerHTML = "<iframe id=" + id + " name=" + id +
             " src=" + url + "></iframe>";
    iframe = GetElement(win, id);
  }
  return iframe;
}

// Create a new TR containing the given td's
function Tr(win, tds) {
  var tr = win.document.createElement("TR");
  for (var i = 0; i < tds.length; i++) {
    tr.appendChild(tds[i]);
  }
  return tr;
}

/**
 * Create a new TD, with an optional colspan
 *
 * @param {Object} win 
 * @param {Number} opt_colspan
 */
function Td(win, opt_colspan) {
  var td = win.document.createElement("TD");
  if (opt_colspan) {
    td.colSpan = opt_colspan;
  }
  return td;
}


// Check if an element has a given class
function HasClass(el, cl) {
  if (el == null || el.className == null) return false;
  if (el.className == cl) {return true;}
  var classes = el.className.split(" ");
  for (var i = 0; i < classes.length; i++) {
    if (classes[i] == cl) {
      return true;
    }
  }
  return false;
}

// Add a class to element
function AddClass(el, cl) {
  if (HasClass(el, cl)) return;
  el.className += " " + cl;
}

// Remove a class from an element
function RemoveClass(el, cl) {
  if (el.className == null) return;
  if (el.className == cl) {
    el.className = "";
    return;
  }
  var classes = el.className.split(" ");
  var result = [];
  var changed = false;
  for (var i = 0; i < classes.length; i++) {
    if (classes[i] != cl) {
      if (classes[i]) { result.push(classes[i]); }
    } else {
      changed = true;
    }
  }
  if (changed) { el.className = result.join(" "); }
}

// Performs an in-order traversal of the tree rooted at the given node
// (excluding the root node) and returns an array of nodes that match the
// given selector. The selector must implement the method:
//
// boolean select(node);
//
// This method is a generalization of the DOM method "getElementsByTagName"
//
function GetElementsBySelector(root, selector) {
  var nodes = [];
  for (var child = root.firstChild; child; child = child.nextSibling) {
    AddElementBySelector_(child, selector, nodes);
  }
  return nodes;
}

// Recursive helper for GetElemnetsBySelector()
function AddElementBySelector_(root, selector, nodes) {
  // First test the parent
  if (selector.select(root)) {
    nodes.push(root);
  }

  // Then recurse through the children
  for (var child = root.firstChild; child; child = child.nextSibling) {
    AddElementBySelector_(child, selector, nodes);
  }
}

//------------------------------------------------------------------------
// Window/screen utilities
// TODO: these should be renamed (e.g. GetWindowWidth to GetWindowInnerWidth
// and moved to geom.js)
//------------------------------------------------------------------------
// Get page offset of an element
function GetPageOffsetLeft(el) {
  var x = el.offsetLeft;
  if (el.offsetParent != null)
    x += GetPageOffsetLeft(el.offsetParent);
  return x;
}

// Get page offset of an element
function GetPageOffsetTop(el) {
  var y = el.offsetTop;
  if (el.offsetParent != null)
    y += GetPageOffsetTop(el.offsetParent);
  return y;
}

// Get page offset of an element
function GetPageOffset(el) {
  var x = el.offsetLeft;
  var y = el.offsetTop;
  if (el.offsetParent != null) {
    var pos = GetPageOffset(el.offsetParent);
    x += pos.x;
    y += pos.y;
  }
  return {x: x, y: y};
}

function GetPageOffsetRight(el) {
  return GetPageOffsetLeft(el) + el.offsetWidth;
}

function GetPageOffsetBottom(el) {
  return GetPageOffsetTop(el) + el.offsetHeight;
}

// Get the y position scroll offset.
function GetScrollTop(win) {
  return GetWindowPropertyByBrowser_(win, getScrollTopGetters_);
}

var getScrollTopGetters_ = {
  ieQuirks_: function(win) {
    return win.document.body.scrollTop;
  },
  ieStandards_: function(win) {
    return win.document.documentElement.scrollTop;
  },
  dom_: function(win) {
    return win.pageYOffset;
  }
};

// Get the x position scroll offset.
function GetScrollLeft(win) {
  return GetWindowPropertyByBrowser_(win, getScrollLeftGetters_);
}

var getScrollLeftGetters_ = {
  ieQuirks_: function(win) {
    return win.document.body.scrollLeft;
  },
  ieStandards_: function(win) {
    return win.document.documentElement.scrollLeft;
  },
  dom_: function(win) {
    return win.pageXOffset;
  }
};

/**
 * Checks if window scrollbar has reached its maximum offset
 *
 * @param win a window object
 * @param {Boolean} opt_isHoriz true if horizontal bar, false if vertical
 */
function IsScrollAtEnd(win, opt_isHoriz) {
  var total =
    (opt_isHoriz) ? document.body.offsetWidth : document.body.offsetHeight;
  var inner =
    (opt_isHoriz) ? GetWindowWidth(win) : GetWindowHeight(win);
  var offset =
    (opt_isHoriz) ? GetScrollLeft(win) : GetScrollTop(win);

  return (inner + offset >= total || total < inner);
}

// Scroll window to pos
// position: 0 = top, 0.5 = middle, 1 = bottom
function ScrollTo(win, el, position) {
  var y = GetPageOffsetTop(el);
  y -= GetWindowHeight(win) * position;
  win.scrollTo(0, y);
}

// Scroll so that as far as possible the entire element is in view.
var ALIGN_BOTTOM = 'b';
var ALIGN_MIDDLE = 'm';
var ALIGN_TOP = 't';
function ScrollIntoView(win, el, alignment) {
  var el_top = GetPageOffsetTop(el);
  var el_bottom = el_top + el.offsetHeight;
  var win_top = GetScrollTop(win);
  var win_height = GetWindowHeight(win);
  var win_bottom = win_top + win_height;

  // Out of view?
  if (el_top < win_top ||
      el_bottom > win_bottom) {

    var scrollto_y;
    if (alignment == ALIGN_BOTTOM) {
      scrollto_y = el_bottom - win_height + 5;
    } else if (alignment == ALIGN_MIDDLE) {
      scrollto_y = (el_top + el_bottom) / 2 - win_height/2;
    } else {
      scrollto_y = el_top - 5;        // ALIGN_TOP
    }

    win.scrollTo(0, scrollto_y);
  }
}

function IsElementVisible(win, id) {
  var el = MaybeGetElement(win, id);
  if (el == null) {
    return false;
  }
  var el_top = GetPageOffsetTop(el);
  var el_bottom = el_top + el.offsetHeight;
  var win_top = GetScrollTop(win);
  var win_bottom = win_top + GetWindowHeight(win);
  if (el_top >= win_top && el_bottom <= win_bottom) {
    return true;
  }
  return false;
}

function GetWindowWidth(win) {
  return GetWindowPropertyByBrowser_(win, getWindowWidthGetters_);
}

var getWindowWidthGetters_ = {
  ieQuirks_: function(win) {
    return win.document.body.clientWidth;
  },
  ieStandards_: function(win) {
    return win.document.documentElement.clientWidth;
  },
  dom_: function(win) {
    return win.innerWidth;
  }
};

function GetWindowHeight(win) {
  return GetWindowPropertyByBrowser_(win, getWindowHeightGetters_);
}

var getWindowHeightGetters_ = {
  ieQuirks_: function(win) {
    return win.document.body.clientHeight;
  },
  ieStandards_: function(win) {
    return win.document.documentElement.clientHeight;
  },
  dom_: function(win) {
    return win.innerHeight;
  }
};

/**
 * Allows the easy use of different getters for IE quirks mode, IE standards
 * mode and fully DOM-compliant browers.
 *
 * @param win window to get the property for
 * @param getters object with various getters. Invoked with the passed window.
 * There are three properties:
 * - ieStandards_: IE 6.0 standards mode
 * - ieQuirks_: IE 6.0 quirks mode and IE 5.5 and older
 * - dom_: Mozilla, Safari and other fully DOM compliant browsers
 *
 * @private
 */
function GetWindowPropertyByBrowser_(win, getters) {
  try {
    if (!window.opera &&
        "compatMode" in win.document &&
        win.document.compatMode == "CSS1Compat") {
      return getters.ieStandards_(win);
    } else if (BR_IsIE()) {
      return getters.ieQuirks_(win);
    }
  } catch (e) {
    // Ignore for now and fall back to DOM method
  }

  return getters.dom_(win);
}

function GetAvailScreenWidth(win) {
  return win.screen.availWidth;
}

function GetAvailScreenHeight(win) {
  return win.screen.availHeight;
}

// Returns a "nice" window height.
// Use the screen height. (Or should we use the height of the current window?)
function GetNiceWindowHeight(win) {
  return Math.floor(0.8 * GetAvailScreenHeight(win));
}

// Used for horizontally centering a new window of the given width in the
// available screen. Set the new window's distance from the left of the screen
// equal to this function's return value.
// Params: width: the width of the new window
// Returns: the distance from the left edge of the screen for the new window to
//   be horizontally centered
function GetCenteringLeft(win, width) {
  return (win.screen.availWidth - width) >> 1;
}

// Used for vertically centering a new window of the given height in the
// available screen. Set the new window's distance from the top of the screen
// equal to this function's return value.
// Params: height: the height of the new window
// Returns: the distance from the top edge of the screen for the new window to
//   be vertically aligned.
function GetCenteringTop(win, height) {
  return (win.screen.availHeight - height) >> 1;
}

/*
 * Opens a child popup window that has no browser toolbar/decorations.
 * (Copied from caribou's common.js library with small modifications.)
 *
 * @param url the URL for the new window (Note: this will be unique-ified)
 * @param opt_name the name of the new window
 * @param opt_width the width of the new window
 * @param opt_height the height of the new window
 * @param opt_center if true, the new window is centered in the available screen
 * @param opt_hide_scrollbars if true, the window hides the scrollbars
 * @param opt_noresize if true, makes window unresizable
 * @param opt_blocked_msg message warning that the popup has been blocked
 * @return a reference to the new child window
 */
function Popup(url, opt_name, opt_width, opt_height, opt_center,
               opt_hide_scrollbars, opt_noresize, opt_blocked_msg) {
  if (!opt_height) {
    opt_height = Math.floor(GetWindowHeight(window.top) * 0.8);
  }
  if (!opt_width) {
    opt_width = Math.min(GetAvailScreenWidth(window), opt_height);
  }

  var features = "resizable=" + (opt_noresize ? "no" : "yes") + "," +
                 "scrollbars=" + (opt_hide_scrollbars ? "no" : "yes") + "," +
                 "width=" + opt_width + ",height=" + opt_height;
  if (opt_center) {
    features += ",left=" + GetCenteringLeft(window, opt_width) + "," +
                "top=" + GetCenteringTop(window, opt_height);
  }
  return OpenWindow(window, url, opt_name, features, opt_blocked_msg);
}

/*
 * Opens a new window. Returns the new window handle. Tries to open the new
 * window using top.open() first. If that doesn't work, then tries win.open().
 * If that still doesn't work, prints an alert.
 * (Copied from caribou's common.js library with small modifications.)
 *
 * @param win the parent window from which to open the new child window
 * @param url the URL for the new window (Note: this will be unique-ified)
 * @param opt_name the name of the new window
 * @param opt_features the properties of the new window
 * @param opt_blocked_msg message warning that the popup has been blocked
 * @return a reference to the new child window
 */
function OpenWindow(win, url, opt_name, opt_features, opt_blocked_msg) {
  var newwin = OpenWindowHelper(top, url, opt_name, opt_features);
  if (!newwin || newwin.closed || !newwin.focus) {
    newwin = OpenWindowHelper(win, url, opt_name, opt_features);
  }
  if (!newwin || newwin.closed || !newwin.focus) {
    if (opt_blocked_msg) alert(opt_blocked_msg);
  } else {
    // Make sure that the window has the focus
    newwin.focus();
  }
  return newwin;
}

/*
 * Helper for OpenWindow().
 * (Copied from caribou's common.js library with small modifications.)
 */
function OpenWindowHelper(win, url, name, features) {
  var newwin;
  if (features) {
    newwin = win.open(url, name, features);
  } else if (name) {
    newwin = win.open(url, name);
  } else {
    newwin = win.open(url);
  }
  return newwin;
}

//------------------------------------------------------------------------
// DOM walking utilities
//------------------------------------------------------------------------

function MaybeEscape(str, escape) {
  return escape ? HtmlEscape(str) : str;
}


//------------------------------------------------------------------------
// Window data
//------------------------------------------------------------------------
// Gets an array, which can store data for the window. This data
// is deleted when the window is unloaded.
var windata = [];
function GetWindowData(win) {
  var data = windata[win.name];
  if (!data) {
    windata[win.name] = data = [];
  }
  return data;
}

// Clear js data for a window.
function ClearWindowData(win_name) {
  if (windata[win_name]) {
    windata[win_name] = null;
  }
}

//------------------------------------------------------------------------
// String utilities
//------------------------------------------------------------------------
// Do html escaping
var amp_re_ = /&/g;
var lt_re_ = /</g;
var gt_re_ = />/g;

// Convert text to HTML format. For efficiency, we just convert '&', '<', '>'
// characters.
// Note: Javascript >= 1.3 supports lambda expression in the replacement
// argument. But it's slower on IE.
// Note: we can also implement HtmlEscape by setting the value
// of a textnode and then reading the 'innerHTML' value, but that
// that turns out to be slower.
// Params: str: String to be escaped.
// Returns: The escaped string.
function HtmlEscape(str) {
  if (!str) return "";
  return str.replace(amp_re_, "&amp;").replace(lt_re_, "&lt;").
    replace(gt_re_, "&gt;").replace(quote_re_, "&quot;");
}

/** converts html entities to plain text.  It covers the most common named
 * entities and numeric entities.
 * It does not cover all named entities -- it covers &{lt,gt,amp,quot,nbsp}; but
 * does not handle some of the more obscure ones like &{ndash,eacute};.
 */
function HtmlUnescape(str) {
  if (!str) return "";
  return str.
    replace(/&#(\d+);/g,
      function (_, n) { return String.fromCharCode(parseInt(n, 10)); }).
    replace(/&#x([a-f0-9]+);/gi,
      function (_, n) { return String.fromCharCode(parseInt(n, 16)); }).
    replace(/&(\w+);/g, function (_, entity) {
      entity = entity.toLowerCase();
      return entity in HtmlUnescape_unesc_ ? HtmlUnescape_unesc_[entity] : '?';
    });
}
var HtmlUnescape_unesc_ = { lt: '<', gt: '>', quot: '"', nbsp: ' ',
			    amp: '&', apos: '\'' };

// Replace multiple spaces with &nbsp; to retain whitespace formatting
// in addition to escaping '&', '<', and '>'.
var dbsp_re_ = /  /g;
var ret_re_ = /\r/g;
var nl_re_ = /\n/g;
function HtmlWhitespaceEscape(str) {
  str = HtmlEscape(str);
  str = str.replace(dbsp_re_, "&nbsp;&nbsp;");
  str = str.replace(ret_re_, "");
  str = str.replace(nl_re_, "<br>");
  return str;
}

// Escape double quote '"' characters in addition to '&', '<', '>' so that a
// string can be included in an HTML tag attribute value within double quotes.
// Params: str: String to be escaped.
// Returns: The escaped string.
var quote_re_ = /\"/g;
function QuoteEscape(str) {
  return HtmlEscape(str).replace(quote_re_, "&quot;");
}

var JS_SPECIAL_RE_ = /[\'\\\r\n\b\"<>&\u0085\u2028\u2029]/g;

function JSEscOne_(s) {
  return JSEscOne_.js_escs_[s];
}

/** convert a string to a javascript string literal.  This function has the
  * property that the return value is also already html escaped, so the output
  * can be embedded in an html handler attribute.
  */
function ToJSString(s) {
  if (!JSEscOne_.js_escs_) {
    var escapes = {};
    escapes['\\'] = '\\\\';
    escapes['\''] = '\\047';
    escapes['\b'] = '\\b';
    escapes['\"'] = '\\042';
    escapes['<'] =  '\\074';
    escapes['>'] =  '\\076';
    escapes['&'] =  '\\046';
    // newline characters according to
    // http://www.mozilla.org/js/language/js20/formal/lexer-grammar.html
    escapes['\n'] = '\\n';
    escapes['\r'] = '\\r';
    escapes['\u0085'] = '\\205';
    escapes['\u2028'] = '\\u2028';
    escapes['\u2029'] = '\\u2029';

    JSEscOne_.js_escs_ = escapes;
  }

  return "'" + s.toString().replace(JS_SPECIAL_RE_, JSEscOne_) + "'";
}

// converts multiple ws chars to a single space, and strips
// leading and trailing ws
var spc_re_ = /\s+/g;
var beg_spc_re_ = /^ /;
var end_spc_re_ = / $/;
function CollapseWhitespace(str) {
  if (!str) return "";
  return str.replace(spc_re_, " ").replace(beg_spc_re_, "").
    replace(end_spc_re_, "");
}

var newline_re_ = /\r?\n/g;
var spctab_re_ = /[ \t]+/g;
var nbsp_re_ = /\xa0/g;
function StripNewlines(str) {
  if (!str) return "";
  return str.replace(newline_re_, " ");
}

function CanonicalizeNewlines(str) {
  if (!str) return "";
  return str.replace(newline_re_, '\n');
}

function HtmlifyNewlines(str) {
  if (!str) return "";
  return str.replace(newline_re_, "<br>");
}

function NormalizeSpaces(str) {
  if (!str) return "";
  return str.replace(spctab_re_, " ").replace(nbsp_re_, " ");
}

// URL encodes the string.
function UrlEncode(str) {
  return encodeURIComponent(str);
}

// URL-decodes the string. We need to specially handle '+'s because
// the javascript library doesn't properly convert them to spaces
var plus_re_ = /\+/g;
function UrlDecode(str) {
  return decodeURIComponent(str.replace(plus_re_, ' '));
}

function Trim(str) {
  if (!str) return "";
  return str.replace(/^\s+/, "").replace(/\s+$/, "");
}

function EndsWith(str, suffix) {
  if (!str) return !suffix;
  return (str.lastIndexOf(suffix) == (str.length - suffix.length));
}

// Check if a string is empty
function IsEmpty(str) {
  return CollapseWhitespace(str) == "";
}

// Check if a character is a letter
function IsLetterOrDigit(ch) {
  return ((ch >= "a" && ch <= "z") ||
          (ch >= "A" && ch <= "Z") ||
         (ch >= '0' && ch <= '9'));
}

// Check if a character is a space character
function IsSpace(ch) {
  return (" \t\r\n".indexOf(ch) >= 0);
}

// Converts any instances of "\r" or "\r\n" style EOLs into "\n" (Line Feed),
// and also trim the extra newlines and whitespaces at the end.
var eol_re_ = /\r\n?/g;
var trailingspc_re_ = /[\n\t ]+$/;
function NormalizeText(str) {
  return str.replace(eol_re_, "\n").replace(trailingspc_re_, "");
}

// Inserts <wbr>s (word break tag) after every n non-space chars and/or
// after or before certain special chars. The input string should be plain
// text that has not yet been HTML-escaped.
// Params:
//   str: The string to insert <wbr>s into.
//   n: The maximum number of consecutive non-space characters to allow before
//     adding a <wbr>. To turn off this rule (i.e. if you only want to add
//     breaks based on special characters), pass in the value -1.
//   chars_to_break_after: The list of special characters (concatenated into a
//     string) after which a <wbr> should be added, if there is no natural
//     break at that point. To turn off this rule, pass in the empty string.
//   chars_to_break_before: The list of special characters (concatenated into a
//     string) before which a <wbr> should be added, if there is no natural
//     break at that point. To turn off this rule, pass in the empty string.
// Returns: The string str htmlescaped, and with <wbr>s inserted according to
//   the rules specified by the other arguments.
function HtmlEscapeInsertWbrs(str, n, chars_to_break_after,
                              chars_to_break_before) {
  AssertNumArgs(4);

  var out = '';
  var strpos = 0;
  var spc = 0;

  for (var i = 1; i < str.length; ++i) {
    var prev_char = str.charAt(i - 1);
    var next_char = str.charAt(i);
    if (IsSpace(next_char)) {
      spc = i;
    } else if (i - spc == n ||
               chars_to_break_after.indexOf(prev_char) != -1 ||
               chars_to_break_before.indexOf(next_char) != -1) {
      out += HtmlEscape(str.substring(strpos, i)) + '<wbr>';
      strpos = i;
      spc = i;
    }
  }
  out += HtmlEscape(str.substr(strpos));
  return out;
}

// Converts a string to its canonicalized label form.
var illegal_chars_re_ = /[ \/(){}&|\\\"\000]/g;
function CanonicalizeLabel(str, lowercase) {
  var uppercase = str.replace(illegal_chars_re_, '-');
  return lowercase ? uppercase.toLowerCase() : uppercase;
}

// Case-insensitive string comparator
function CompareStringsIgnoreCase(s1, s2) {
  s1 = s1.toLowerCase();
  s2 = s2.toLowerCase();

  if (s1 < s2) {
    return -1;
  } else if (s1 == s2) {
    return 0;
  } else {
    return 1;
  }
}

//------------------------------------------------------------------------
// TextArea utilities
//------------------------------------------------------------------------

// Gets the cursor pos in a text area. Returns -1 if the cursor pos cannot
// be determined or if the cursor out of the textfield.
function GetCursorPos(win, textfield) {
  try {
    if (IsDefined(textfield.selectionEnd)) {
      // Mozilla directly supports this
      return textfield.selectionEnd;

    } else if (win.document.selection && win.document.selection.createRange) {
      // IE doesn't export an accessor for the endpoints of a selection.
      // Instead, it uses the TextRange object, which has an extremely obtuse
      // API. Here's what seems to work:

      // (1) Obtain a textfield from the current selection (cursor)
      var tr = win.document.selection.createRange();

      // Check if the current selection is in the textfield
      if (tr.parentElement() != textfield) {
        return -1;
      }

      // (2) Make a text range encompassing the textfield
      var tr2 = tr.duplicate();
      tr2.moveToElementText(textfield);

      // (3) Move the end of the copy to the beginning of the selection
      tr2.setEndPoint("EndToStart", tr);

      // (4) The span of the textrange copy is equivalent to the cursor pos
      var cursor = tr2.text.length;

      // Finally, perform a sanity check to make sure the cursor is in the
      // textfield. IE sometimes screws this up when the window is activated
      if (cursor > textfield.value.length) {
        return -1;
      }
      return cursor;
    } else {
      Debug("Unable to get cursor position for: " + navigator.userAgent);

      // Just return the size of the textfield
      // TODO: Investigate how to get cursor pos in Safari!
      return textfield.value.length;
    }
  } catch (e) {
    DumpException(e, "Cannot get cursor pos");
  }

  return -1;
}

function SetCursorPos(win, textfield, pos) {
  if (IsDefined(textfield.selectionEnd) &&
      IsDefined(textfield.selectionStart)) {
    // Mozilla directly supports this
    textfield.selectionStart = pos;
    textfield.selectionEnd = pos;

  } else if (win.document.selection && textfield.createTextRange) {
    // IE has textranges. A textfield's textrange encompasses the
    // entire textfield's text by default
    var sel = textfield.createTextRange();

    sel.collapse(true);
    sel.move("character", pos);
    sel.select();
  }
}

//------------------------------------------------------------------------
// Array utilities
//------------------------------------------------------------------------
// Find an item in an array, returns the key, or -1 if not found
function FindInArray(array, x) {
  for (var i = 0; i < array.length; i++) {
    if (array[i] == x) {
      return i;
    }
  }
  return -1;
}

// Inserts an item into an array, if it's not already in the array
function InsertArray(array, x) {
  if (FindInArray(array, x) == -1) {
    array[array.length] = x;
  }
}

// Delete an element from an array
function DeleteArrayElement(array, x) {
  var i = 0;
  while (i < array.length && array[i] != x)
    i++;
  array.splice(i, 1);
}

// Copies a flat array
function CopyArray(array) {
  var copy = [];
  for (var i = 0; i < array.length; i++) {
    copy[i] = array[i];
  }
  return copy;
}

// Clone an object (recursively)
function CloneObject(x) {
  if ((typeof x) == "object") {
    var y = [];
    for (var i in x) {
      y[i] = CloneObject(x[i]);
    }
    return y;
  }
  return x;
}

/**
 * Clone an event; cannot use CloneObject(event)
 * because it suffers from infinite recursion.
 * Thus, only a subset of the event properties are
 * cloned -- if you need others, just add them
 * to this function (just don't remove any!)
 */
function CloneEvent(ev) {
  var clone = {};
  clone.clientX = ev.clientX;
  clone.clientY = ev.clientY;
  clone.pageX = ev.pageX;
  clone.pageY = ev.pageY;
  clone.type = ev.type;
  clone.srcElement = ev.srcElement;
  clone.target = ev.target;
  clone.cancelBubble = ev.cancelBubble;
  clone.explicitOriginalTarget = ev.explicitOriginalTarget;
  clone.button = ev.button;
  clone.shiftKey = ev.shiftKey;
  clone.ctrlKey = ev.ctrlKey;
  // add more properties here

  return clone;
}

function GetEventTarget(/*Event*/ ev) {
// Event is not a type in IE; IE uses Object for events
//  AssertType(ev, Event, 'arg passed to GetEventTarget not an Event');
  return ev.srcElement || ev.target;
}

/** cancels the event */
// from http://www.quirksmode.org/js/events_order.html
function CancelEvent(/*Event*/ ev) {
  if (BR_IsIE()) {
    ev.cancelBubble = true;
  } else if (ev.stopPropagation) {
    ev.stopPropagation();
  }
}

/** Cancels any default action associated with the event. */
function CancelDefaultAction(/*Event*/ ev) {
  if (BR_IsIE()) {
    ev.returnValue = false;
  } else {
    ev.preventDefault();
  }
}


//------------------------------------------------------------------------
// Formatting utilities
//------------------------------------------------------------------------
// A simple printf type function that takes in a template array, and a data
// array. e.g. PrintArray(["a",,"b",,"c"], ["x", "y"]) => axbyc
function PrintArray(array, data) {
  // Check that the argument count is correct.
  AssertEquals(array.length, data.length * 2 + 1);

  for (var i = 0, idx = 1; i < data.length; i++, idx += 2) {
    array[idx] = data[i];
  }
  return array.join("");
}

function ImageHtml(url, attributes) {
  return "<img " + attributes + " src=" + url + ">";
}

// Formats an object id that has two id numbers, eg, foo_3_7
function MakeId3(idprefix, m, n) {
  return idprefix + m + "_" + n;
}

//------------------------------------------------------------------------
// Email address parsing
//------------------------------------------------------------------------
// Parse an email address of the form "name" <address> into [name, address]
function ParseAddress(addr) {
  var name = "";
  var address = "";
  for (var i = 0; i < addr.length;) {
    var token = GetEmailToken(addr, i);
    if (token.charAt(0) == '<') {
      var end = token.indexOf(">");
      address = token.substring(1, (end != -1) ? end : token.length);
    } else if (address == "") {
      name += token;
    }
    i += token.length;
  }

  // Check if it's a simple email address
  if (address == "" && name.indexOf("@") != -1) {
    address = name;
    name = "";
  }

  name = CollapseWhitespace(name);
  name = StripQuotes(name, "'");
  name = StripQuotes(name, "\"");
  address = CollapseWhitespace(address);
  return [name, address];
}

// Given an email address, get the address part
function GetAddress(address) {
  return ParseAddress(address)[1];
}

// Get the username part of an email address
function GetAddressUsername(address) {
  address = GetAddress(address);
  var at = address.indexOf("@");
  return (at == -1) ? address : address.substr(0, at);
}

// Given an email address, get the personal part
function GetPersonal(address) {
  return ParseAddress(address)[0];
}

// Given an address, get a short name
function GetPersonalElseUsername(address) {
  var personal = GetPersonal(address);
  if (personal != "") {
    return personal;
  } else {
    return GetAddressUsername(address);
  }
}

// Strip ' or " chars around a string
function StripQuotes(str, quotechar) {
  var len = str.length;
  if (str.charAt(0) == quotechar &&
      str.charAt(len - 1) == quotechar) {
    return str.substring(1, len - 1);
  }
  return str;
}

// Convert a string containing list of email addresses into an array
// of strings
function EmailsToArray(str) {
  var result = [];
  var email = "";
  var token;

  for (var i = 0; i < str.length; ) {
    token = GetEmailToken(str, i);
    if (token == ",") {
      AddEmailAddress(result, email);
      email = "";
      i++;
      continue;
    }
    email += token;
    i += token.length;
  }

  // Add last
  if (email !="" || token == ",") {
    AddEmailAddress(result, email);
  }
  return result;
}

// Get the next token from a position in an address string
var openers_ = "\"<([";
var closers_ = "\">)]";
function GetEmailToken(str, pos) {
  var ch = str.charAt(pos);
  var p = openers_.indexOf(ch);
  if (p == -1)
    return ch;
  var end_pos = str.indexOf(closers_.charAt(p), pos + 1);
  var token = (end_pos >= 0) ? str.substring(pos, end_pos + 1) :
              str.substr(pos);
  return token;
}

// Add an email address to the result array.
function AddEmailAddress(result, email) {
  email = CleanEmailAddress(email);
  result[result.length] = email;
}

// Clean up email address:
// - remove extra spaces
// - Surround name with quotes if it contains special characters
// to check if we need " quotes
// Note: do not use /g in the regular expression, otherwise the
// regular expression cannot be reusable.
var specialchars_re_ = /[()<>@,;:\\\".\[\]]/;

function CleanEmailAddress(str) {
  var name_address = ParseAddress(str);
  var name = name_address[0];
  var address = name_address[1];

  if (name.indexOf("\"") == -1) {  // If there's no "
    var quote_needed = specialchars_re_.test(name);
    if (quote_needed) {
      name = "\"" + name + "\"";
    }
  }

  if (name == "")
    return address;
  else if (address == "")
    return name;
  else
    return name + " <" + address + ">";
}

//------------------------------------------------------------------------
// Timeouts
//
// It is easy to forget to put a try/catch block around a timeout function,
// and the result is an ugly user visible javascript error.
// Also, it would be nice if a timeout associated with a window is
// automatically cancelled when the user navigates away from that window.
//
// When storing timeouts in a window, we can't let that variable be renamed
// since the window could be top.js, and renaming such a property could
// clash with any of the variables/functions defined in top.js.
//------------------------------------------------------------------------
/**
 * Sets a timeout safely.
 * @param win the window object. If null is passed in, then a timeout if set
 *   on the js frame. If the window is closed, or freed, the timeout is
 *   automaticaaly cancelled
 * @param fn the callback function: fn(win) will be called.
 * @param ms number of ms the callback should be called later
 */
function SafeTimeout(win, fn, ms) {
  if (!win) win = window;
  if (!win._tm) {
    win._tm = [];
  }
  var timeoutfn = SafeTimeoutFunction_(win, fn);
  var id = win.setTimeout(timeoutfn, ms);

  // Save the id so that it can be removed from the _tm array
  timeoutfn.id = id;

  // Safe the timeout in the _tm array
  win._tm[id] = 1;

  return id;
}

/** Creates a callback function for a timeout*/
function SafeTimeoutFunction_(win, fn) {
  var timeoutfn = function() {
    try {
      fn(win);

      var t = win._tm;
      if (t) {
        delete t[timeoutfn.id];
      }
    } catch (e) {
      DumpException(e);
    }
  };
  return timeoutfn;
}

/** Cancel a timeout */
function CancelTimeout(win, id) {
  if (!win) win = window;
  win.clearTimeout(id);
  if (win._tm) {
    delete win._tm[id];
  }
}

/** Cancels all timeouts for a given window */
function CancelAllTimeouts(win) {
  if (win && win._tm) {
    try {
      for (var i in win._tm) {
        win.clearTimeout(i);
      }
      win._tm = [];
    } catch (e) {
      DumpException(e);
    }
  }
}

//------------------------------------------------------------------------
// Misc
//------------------------------------------------------------------------
// Compare long hex strings
function CompareID(a, b) {
  if (a.length != b.length) {
    return (a.length - b.length);
  } else {
    return (a < b) ? -1 : (a > b) ? 1 : 0;
  }
}

// Check if a value is defined
function IsDefined(value) {
  return (typeof value) != 'undefined';
}

function GetKeyCode(event) {
  var code;
  if (event.keyCode) {
    code = event.keyCode;
  } else if (event.which) {
    code = event.which;
  }
  return code;
}

// define a forid function to fetch a DOM node by id.
function forid_1(id) {
  return document.getElementById(id);
}
function forid_2(id) {
  return document.all[id];
}

/**
 * Fetch an HtmlElement by id.
 * DEPRECATED: use $ in dom.js
 */
var forid = document.getElementById ? forid_1 : forid_2;

/**
 * GetFnName returns the name of the provided function and
 * adds a property to the function called "name" with the name of the
 * function, if the property is not already present.
 *
 * GetFnName can help reduce the size of the code generated by JSCompiler.
 * For example, if you have code that refers to a function, MyFun, in a string:
 *
 * '<a href="javascript:MyFun()">' + linkLabel + '</a>'
 *
 * MyFun cannot be renamed by JSCompiler because it is inlined in a string.
 * GetFnName can be used to remove the inlining:
 *
 * '<a href="javascript:' + GetFnName(MyFun) + '()">' + linkLabel + '</a>'
 *
 * When written this way, MyFun can be renamed by JS Compiler.
 *
 * @throws an Error if any of the following are true:
 *         (1) the name of the function cannot be extracted,
 *         (2) the name of the function is the empty string, null, or undefined
 *         (3) the name of the function is "anonymous"
 *   An Error is thrown in cases (2) and (3) because neither "" nor "anonymous"
 *   is a name that can be used to invoke the function.
 *
 *   Examples of valid functions that produce these errors are:
 *   f = function(a) {return a;};        // name is ""
 *   g = new Function('a', 'return a');  // name is "anonymous"
 *
 */
function GetFnName(func) {
  // AssertType(func, Function) fails for window.alert on IE, which is why
  // a weaker assertion, AssertTrue, is used in place of AssertType
  AssertTrue(func, "func passed to GetFnName() is undefined");
  var name;
  if (!('name' in func)) {
    var match = /\W*function\s+([\w\$]+)\(/.exec(func);
    if (!match) {
      throw new Error("Cannot extract name from function: " + func);
    }
    name = match[1];
    func.name = name;
  } else {
    name = func.name;
  }
  if (!name || name == 'anonymous') {
    throw new Error("Anonymous function has no name: " + func);
  }
  return func.name;
}

function log(msg) {
  /* a top level window is its own parent.  Use != or else fails on IE with
   * infinite loop.
   */
  try {
    if (window.parent != window && window.parent.log) {
      window.parent.log(window.name + '::' + msg);
      return;
    }
  } catch (e) {
    // Error: uncaught exception: Permission denied to get property Window.log
  }
  var logPane = forid('log');
  if (logPane) {
    var logText = '<p class=logentry><span class=logdate>' + new Date() +
                  '</span><span class=logmsg>' + msg + '</span></p>';
    logPane.innerHTML = logText + logPane.innerHTML;
  } else {
    window.status = msg;
  }
}

/**
 * @fileoverview Date formatting library with locale
 * support
 */


var DateTimeFormat = {};

/**
 * This function is just being used to make the latest 'registered' constants 
 * to be the active one. This function is meaned to share resource with Closure
 * code. 
 */
var DateTimeConstants;
function registerDateTimeConstants(dataObj, localeName) {
  DateTimeConstants = dataObj;
}


/**
 * Formats a given date object according to pattern specified
 * using given locale symbol collections.
 * Pattern specification: (Refer to JDK/ICU/CLDR)
 * <pre>
 * Symbol Meaning Presentation        Example
 * ------   -------                 ------------        -------
 * G        era designator          (Text)              AD
 * y#       year                    (Number)            1996
 * Y*       year (week of year)     (Number)            1997
 * u*       extended year           (Number)            4601
 * M        month in year           (Text & Number)     July & 07
 * d        day in month            (Number)            10
 * h        hour in am/pm (1~12)    (Number)            12
 * H        hour in day (0~23)      (Number)            0
 * m        minute in hour          (Number)            30
 * s        second in minute        (Number)            55
 * S        fractional second       (Number)            978
 * E        day of week             (Text)              Tuesday
 * e*       day of week (local 1~7) (Number)            2
 * D*       day in year             (Number)            189
 * F*       day of week in month    (Number)            2 (2nd Wed in July)
 * w*       week in year            (Number)            27
 * W*       week in month           (Number)            2
 * a        am/pm marker            (Text)              PM
 * k        hour in day (1~24)      (Number)            24
 * K        hour in am/pm (0~11)    (Number)            0
 * z        time zone               (Text)              Pacific Standard Time
 * Z        time zone (RFC 822)     (Number)            -0800
 * v        time zone (generic)     (Text)              Pacific Time
 * g*       Julian day              (Number)            2451334
 * A*       milliseconds in day     (Number)            69540000
 * '        escape for text         (Delimiter)         'Date='
 * ''       single quote            (Literal)           'o''clock'
 *
 * Item marked with '*' are not supported yet.
 * Item marked with '#' works different than java
 *
 * The count of pattern letters determine the format.
 * (Text): 4 or more, use full form, <4, use short or abbreviated form if it
 * exists. (e.g., "EEEE" produces "Monday", "EEE" produces "Mon")
 *
 * (Number): the minimum number of digits. Shorter numbers are zero-padded to
 * this amount (e.g. if "m" produces "6", "mm" produces "06"). Year is handled
 * specially; that is, if the count of 'y' is 2, the Year will be truncated to
 * 2 digits. (e.g., if "yyyy" produces "1997", "yy" produces "97".) Unlike other
 * fields, fractional seconds are padded on the right with zero.
 *
 * (Text & Number): 3 or over, use text, otherwise use number. (e.g., "M"
 * produces "1", "MM" produces "01", "MMM" produces "Jan", and "MMMM" produces
 * "January".)
 *
 * Any characters in the pattern that are not in the ranges of ['a'..'z'] and
 * ['A'..'Z'] will be treated as quoted text. For instance, characters like ':',
 * '.', ' ', '#' and '@' will appear in the resulting time text even they are
 * not embraced within single quotes.
 * </pre>
 *
 * @param {String} pattern string to specify how the date should
 *        be formatted
 * @param {Object} date the date object being formatted.
 * @param {Object} symbols locale symbols collection.
 *
 * @return {String} formatted date representation.
 */

DateTimeFormat.format = function(pattern, date, symbols) {
  return DateTimeFormat.formatter(pattern, symbols)(date, symbols);
};

/**
 * Change the locale symbols collection that will be used in
 * following date formatting functions.
 *
 * @param {Object} localeSymbols locale symbol collection
 *
 * @return {void}
 */
DateTimeFormat.loadLocaleSymbols = function (localeSymbols) {
  DateTimeFormat.symbols = localeSymbols;
};

DateTimeFormat.FULL_FORMAT = 0;
DateTimeFormat.LONG_FORMAT = 1;
DateTimeFormat.MEDIUM_FORMAT = 2;
DateTimeFormat.SHORT_FORMAT = 3;
DateTimeFormat.DEFAULT_FORMAT = 2;

/**
 * Formats a given date object according to a predefined
 * date format pattern specified in symbols object.
 * @param {Number} formatType type used to reference predefined pattern.
 * @param {Object} date the date object being formatted.
 * @param {Object} symbols locale symbols collection.
 *
 * @return {String} formatted date representation.
 */
DateTimeFormat.formatDate = function(formatType, date, symbols) {
  symbols = DateTimeFormat.resolveSymbols_(symbols);
  return DateTimeFormat.format(symbols.DATEFORMATS[formatType], date, symbols);
}

/**
 * return a function that will format date using a predefined date format pattern.
 * @param {Number} format type used to reference predefined pattern.
 * @param {Object} symbols locale symbols collection.
 *
 * @return {Object} the function that will format date using given pattern.
 */
DateTimeFormat.getDateFormatter = function(formatType, symbols) {
  symbols = DateTimeFormat.resolveSymbols_(symbols);
  return DateTimeFormat.formatter(symbols.DATEFORMATS[formatType], symbols);
}

/**
 * Formats a given date object according to a predefined
 * time format pattern specified in symbols object.
 * @param {Number} format type used to reference predefined pattern.
 * @param {Object} date the date object being formatted.
 * @param {Object} symbols locale symbols collection.
 *
 * @return {String} formatted date representation.
 */
DateTimeFormat.formatTime = function(formatType, date, symbols) {
  symbols = DateTimeFormat.resolveSymbols_(symbols);
  return DateTimeFormat.format(symbols.TIMEFORMATS[formatType], date, symbols);
}

/**
 * return a function that will format date using a predefined time format pattern.
 * @param {Number} format type used to reference predefined pattern.
 * @param {Object} symbols locale symbols collection.
 *
 * @return {Object} the function that will format date using given pattern.
 */
DateTimeFormat.getTimeFormatter = function(formatType, symbols) {
  symbols = DateTimeFormat.resolveSymbols_(symbols);
  return DateTimeFormat.formatter(symbols.TIMEFORMATS[formatType], symbols);
}

/**
 * Formats a given date object according to a predefined
 * date/time format pattern specified in symbols object.
 * @param {Number} format type used to reference predefined pattern.
 * @param {Object} date the date object being formatted.
 * @param {Object} symbols locale symbols collection.
 *
 * @return {String} formatted date representation.
 */
DateTimeFormat.formatDateTime = function(formatType, date, symbols) {
  symbols = DateTimeFormat.resolveSymbols_(symbols);
  return DateTimeFormat.format(symbols.DATEFORMATS[formatType] + ' ' +
                               symbols.TIMEFORMATS[formatType], date, symbols);
}

/**
 * return a function that will format date using a predefined date/time format pattern.
 * @param {Number} format type used to reference predefined pattern.
 * @param {Object} symbols locale symbols collection.
 *
 * @return {Object} the function that will format date using given pattern.
 */
DateTimeFormat.getDateTimeFormatter = function(formatType, symbols) {
  symbols = DateTimeFormat.resolveSymbols_(symbols);
  return DateTimeFormat.formatter(symbols.DATEFORMATS[formatType] + ' ' +
                               symbols.TIMEFORMATS[formatType], symbols);
}

/**
 * regular expression pattern for parsing pattern string
 */
DateTimeFormat.TOKENS_ = [
    /^\'(?:[^\']|\'\')*\'/,
    /^(?:G+|y+|M+|k+|S+|E+|a+|h+|K+|H+|c+|L+|Q+|d+|m+|s+|v+|z+|Z+)/,  // pattern chars
    /^[^\'GyMkSEahKHcLQdmsvzZ]+/  // and all the other chars
];

/**
 * return a function that will format date using given pattern and symbols.
 * @param {String} pattern string specify how the date should be formatted.
 * @param {Object} symbols locale symbols collection.
 *
 * @return {Function} the function that will format date using given pattern.
 */
DateTimeFormat.formatter = function(pattern, symbols) {
  symbols = DateTimeFormat.resolveSymbols_(symbols);

  // lex the pattern, once for all uses
  var parts = [];
  while (pattern) {
    for (var i = 0; i < DateTimeFormat.TOKENS_.length; ++i) {
      var m = pattern.match(DateTimeFormat.TOKENS_[i]);
      if (m) {
        var part = m[0];
        pattern = pattern.substring(part.length);
        if (i == 0) {
          if (part == "''") {
            part = "'";  // '' -> '
          } else {
            part = part.substring(1, part.length - 1); // strip quotes
            part = part.replace(/\'\'/, "'");
          }
        }
        parts.push({ text: part, type: i });
        break;
      }
    }
  }

  return function (date) {
    var out = '';
    for (var i = 0; i < parts.length; ++i) {
      if (1 == parts[i].type) {
        var text = parts[i].text;
        var fmt = DateTimeFormat.formatFunctions_[text.charAt(0)];
        out += fmt(text.length, date, symbols);
      } else {
        out += parts[i].text;
      }
    }
    return out;
  };
}

/**
 * Help constant for zeroPaddingNumber_
 */
DateTimeFormat.ZEROES_ = "0000000000000000";

/**
 * Formats a number with the specified minimum number of digits, using zero
 * to fill the gap.
 * @param {Number} value the number value being formatted.
 * @param {Number} minWidth minimum width of the formatted
 *        string. Zero will be padded to reach this width.
 *
 * @return {String} zero padded number string.
 */
DateTimeFormat.zeroPaddingNumber_ = function(value, minWidth) {
  var buf = String(value);
  while (buf.length < minWidth) {
    buf = DateTimeFormat.ZEROES_.substring(0, Math.min(minWidth - buf.length,
                                       DateTimeFormat.ZEROES_.length)) + buf;
  }
  return buf;
};

/**
 * Formats Era field according to pattern specified
 *
 * @param {Number} count number of time pattern char repeats.
 *        This controls how a field should be formatted.
 * @param {Object} date hold the date object to be
 *        formatted.
 * @param {Object} symbols locale symbol collection.
 *
 * @return {String} formatted string that represent this field.
 */
DateTimeFormat.formatEra_ = function(count, date, symbols) {
  var value = date.getFullYear() > 0 ? 1 : 0;
  return count >= 4 ? symbols.ERANAMES[value] : symbols.ERAS[value];
};

/**
 * Formats Year field according to pattern specified
 *   Javascript Date object seems incapable handling 1BC and
 *   year before. It can show you year 0 which does not exists.
 *   following we just keep consistent with javascript's
 *   toString method. But keep in mind those things should be
 *   unsupported.
 * @param {Number} count number of time pattern char repeats.
 *        This controls how a field should be formatted.
 * @param {Object} date hold the date object to be
 *        formatted.
 * @param {Object} symbols locale symbol collection.
 *
 * @return {String} formatted string that represent this field.
 */
DateTimeFormat.formatYear_ = function(count, date, symbols) {
  var value = date.getFullYear();
  if (value < 0) {
    value = -value;
  }
  return count == 2 ? DateTimeFormat.zeroPaddingNumber_(value % 100, 2) :
                      String(value);
};

/**
 * Formats Month field according to pattern specified
 *
 * @param {Number} count number of time pattern char repeats.
 *        This controls how a field should be formatted.
 * @param {Object} date hold the date object to be
 *        formatted.
 * @param {Object} symbols locale symbol collection.
 *
 * @return {String} formatted string that represent this field.
 */
DateTimeFormat.formatMonth_ = function(count, date, symbols) {
  var value = date.getMonth();
  switch(count) {
    case 5: return symbols.NARROWMONTHS[value];
    case 4: return symbols.MONTHS[value];
    case 3: return symbols.SHORTMONTHS[value];
    default: return DateTimeFormat.zeroPaddingNumber_(value + 1, count);
  }
};

/**
 * Formats (1..24) Hours field according to pattern specified
 *
 * @param {Number} count number of time pattern char repeats.
 *        This controls how a field should be formatted.
 * @param {Object} date hold the date object to be
 *        formatted.
 * @param {Object} symbols locale symbol collection.
 *
 * @return {String} formatted string that represent this field.
 */
DateTimeFormat.format24Hours_ = function(count, date, symbols) {
  var value = date.getHours();
  return value == 0 ? DateTimeFormat.zeroPaddingNumber_(24, count) :
                      DateTimeFormat.zeroPaddingNumber_(value, count);
};

/**
 * Formats Fractional seconds field according to pattern
 * specified
 *
 * @param {Number} count number of time pattern char repeats.
 *        This controls how a field should be formatted.
 * @param {Object} date hold the date object to be
 *        formatted.
 * @param {Object} symbols locale symbol collection.
 *
 * @return {String} formatted string that represent this field.
 */
DateTimeFormat.formatFractionalSeconds_ = function(count, date, symbols) {
  // Fractional seconds left-justify
  var value = date.getTime() % 1000;
  if (count == 1) {
    return String(Math.round(value/ 100));
  }
  if (count == 2) {
    value = Math.floor(value/10);
    return DateTimeFormat.zeroPaddingNumber_(value, 2);
  }
  var buf = DateTimeFormat.zeroPaddingNumber_(value, 3);
  if (count > 3) {
    buf += DateTimeFormat.zeroPaddingNumber_(0, count - 3);
  }
  return buf;
};

/**
 * Formats Day of week field according to pattern specified
 *
 * @param {Number} count number of time pattern char repeats.
 *        This controls how a field should be formatted.
 * @param {Object} date hold the date object to be
 *        formatted.
 * @param {Object} symbols locale symbol collection.
 *
 * @return {String} formatted string that represent this field.
 */
DateTimeFormat.formatDayOfWeek_ = function(count, date, symbols) {
  var value = date.getDay();
  return count >= 4 ? symbols.WEEKDAYS[value] : symbols.SHORTWEEKDAYS[value];
};

/**
 * Formats Am/Pm field according to pattern specified
 *
 * @param {Number} count number of time pattern char repeats.
 *        This controls how a field should be formatted.
 * @param {Object} date hold the date object to be
 *        formatted.
 * @param {Object} symbols locale symbol collection.
 *
 * @return {String} formatted string that represent this field.
 */
DateTimeFormat.formatAmPm_ = function(count, date, symbols) {
  return (date.getHours() >= 12 && date.getHours() < 24) ?
         symbols.AMPMS[1] : symbols.AMPMS[0];
};

/**
 * Formats (1..12) Hours field according to pattern specified
 *
 * @param {Number} count number of time pattern char repeats.
 *        This controls how a field should be formatted.
 * @param {Object} date hold the date object to be
 *        formatted.
 * @param {Object} symbols locale symbol collection.
 *
 * @return {String} formatted string that represent this field.
 */
DateTimeFormat.format1To12Hours_ = function(count, date, symbols) {
  var value = date.getHours() % 12;
  return value == 0 ? DateTimeFormat.zeroPaddingNumber_(12, count) :
                      DateTimeFormat.zeroPaddingNumber_(value, count);
};

/**
 * Formats (0..11) Hours field according to pattern specified
 *
 * @param {Number} count number of time pattern char repeats.
 *        This controls how a field should be formatted.
 * @param {Object} date hold the date object to be
 *        formatted.
 * @param {Object} symbols locale symbol collection.
 *
 * @return {String} formatted string that represent this field.
 */
DateTimeFormat.format0To11Hours_ = function(count, date, symbols) {
  var value = date.getHours() % 12;
  return DateTimeFormat.zeroPaddingNumber_(value, count);
};

/**
 * Formats (0..23) Hours field according to pattern specified
 *
 * @param {Number} count number of time pattern char repeats.
 *        This controls how a field should be formatted.
 * @param {Object} date hold the date object to be
 *        formatted.
 * @param {Object} symbols locale symbol collection.
 *
 * @return {String} formatted string that represent this field.
 */
DateTimeFormat.format0To23Hours_ = function(count, date, symbols) {
  var value = date.getHours();
  return DateTimeFormat.zeroPaddingNumber_(value, count);
};

/**
 * Formats Standalone weekday field according to pattern
 * specified
 *
 * @param {Number} count number of time pattern char repeats.
 *        This controls how a field should be formatted.
 * @param {Object} date hold the date object to be
 *        formatted.
 * @param {Object} symbols locale symbol collection.
 *
 * @return {String} formatted string that represent this field.
 */
DateTimeFormat.formatStandaloneDay_ = function(count, date, symbols) {
  var value = date.getDay();
  switch(count) {
    case 5: return symbols.STANDALONENARROWWEEKDAYS[value];
    case 4: return symbols.STANDALONEWEEKDAYS[value];
    case 3: return symbols.STANDALONESHORTWEEKDAYS[value];
    default: return DateTimeFormat.zeroPaddingNumber_(value, 1);
  }
};

/**
 * Formats Standalone Month field according to pattern specified
 *
 * @param {Number} count number of time pattern char repeats.
 *        This controls how a field should be formatted.
 * @param {Object} date hold the date object to be
 *        formatted.
 * @param {Object} symbols locale symbol collection.
 *
 * @return {String} formatted string that represent this field.
 */
DateTimeFormat.formatStandaloneMonth_ = function(count, date, symbols) {
  var value = date.getMonth();
  switch(count) {
    case 5: return symbols.STANDALONENARROWMONTHS[value];
    case 4: return symbols.STANDALONEMONTHS[value];
    case 3: return symbols.STANDALONESHORTMONTHS[value];
    default: return DateTimeFormat.zeroPaddingNumber_(value + 1, count);
  }
};

/**
 * Formats Quarter field according to pattern specified
 *
 * @param {Number} count number of time pattern char repeats.
 *        This controls how a field should be formatted.
 * @param {Object} date hold the date object to be
 *        formatted.
 * @param {Object} symbols locale symbol collection.
 *
 * @return {String} formatted string that represent this field.
 */
DateTimeFormat.formatQuarter_ = function(count, date, symbols) {
  var value = Math.floor(date.getMonth() / 3);
  return count < 4 ? symbols.SHORTQUARTERS[value] : symbols.QUARTERS[value];
};

/**
 * Formats Date field according to pattern specified
 *
 * @param {Number} count number of time pattern char repeats.
 *        This controls how a field should be formatted.
 * @param {Object} date hold the date object to be
 *        formatted.
 * @param {Object} symbols locale symbol collection.
 *
 * @return {String} formatted string that represent this field.
 */
DateTimeFormat.formatDate_ = function(count, date, symbols) {
  var value = date.getDate();
  return DateTimeFormat.zeroPaddingNumber_(value, count);
};

/**
 * Formats Minutes field according to pattern specified
 *
 * @param {Number} count number of time pattern char repeats.
 *        This controls how a field should be formatted.
 * @param {Object} date hold the date object to be
 *        formatted.
 * @param {Object} symbols locale symbol collection.
 *
 * @return {String} formatted string that represent this field.
 */
DateTimeFormat.formatMinutes_ = function(count, date, symbols) {
  var value = date.getMinutes();
  return DateTimeFormat.zeroPaddingNumber_(value, count);
};

/**
 * Formats Seconds field according to pattern specified
 *
 * @param {Number} count number of time pattern char repeats.
 *        This controls how a field should be formatted.
 * @param {Object} date hold the date object to be
 *        formatted.
 * @param {Object} symbols locale symbol collection.
 *
 * @return {String} formatted string that represent this field.
 */
DateTimeFormat.formatSeconds_ = function(count, date, symbols) {
  var value = date.getSeconds();
  return DateTimeFormat.zeroPaddingNumber_(value, count);
};

/**
 * Formats Timezone field following RFC
 *
 * @param {Number} count number of time pattern char repeats.
 *        This controls how a field should be formatted.
 * @param {Object} date hold the date object to be
 *        formatted.
 * @param {Object} symbols locale symbol collection.
 *
 * @return {String} formatted string that represent this field.
 */
DateTimeFormat.formatTimeZoneRFC_ = function(count, date, symbols) {
  if (count < 4) {
    // 'short' (standard Java) form, must use ASCII digits
    var val = date.getTimezoneOffset();
    var sign = '-';
    if (val < 0) {
      val = -val;
      sign = '+';
    }
    val = (val / 3) * 5 + (val % 60);
    // minutes => KKmm
    return sign + DateTimeFormat.zeroPaddingNumber_(val, 4);
  }
  return DateTimeFormat.appendGMT_(count, date, symbols);
};

/**
 * Generate GMT timezone string for given date
 * @param {Object} date whose value being evaluated.
 *
 * @return {String} GMT timezone string
 */
DateTimeFormat.appendGMT_ = function(count, date, symbols) {
  var value = date.getTimezoneOffset();
  var buf = '';
  if (value > 0) {
    buf += 'GMT-';
  } else {
    value = -value;
    buf += 'GMT+';
  }

  buf += DateTimeFormat.zeroPaddingNumber_(value / 60, 2);
  buf += ':';
  buf += DateTimeFormat.zeroPaddingNumber_(value % 60, 2);
  return buf;
};

/**
 * Formatting function map, from pattern char to format function
 */
DateTimeFormat.formatFunctions_ = {
  'G': DateTimeFormat.formatEra_,
  'y': DateTimeFormat.formatYear_,
  'M': DateTimeFormat.formatMonth_,
  'k': DateTimeFormat.format24Hours_,
  'S': DateTimeFormat.formatFractionalSeconds_,
  'E': DateTimeFormat.formatDayOfWeek_,
  'a': DateTimeFormat.formatAmPm_,
  'h': DateTimeFormat.format1To12Hours_,
  'K': DateTimeFormat.format0To11Hours_,
  'H': DateTimeFormat.format0To23Hours_,
  'c': DateTimeFormat.formatStandaloneDay_,
  'L': DateTimeFormat.formatStandaloneMonth_,
  'Q': DateTimeFormat.formatQuarter_,
  'd': DateTimeFormat.formatDate_,
  'm': DateTimeFormat.formatMinutes_,
  's': DateTimeFormat.formatSeconds_,
  'v': DateTimeFormat.appendGMT_,
  'z': DateTimeFormat.appendGMT_,
  'Z': DateTimeFormat.formatTimeZoneRFC_
};


/**
 * resolve the symbols if not passed in.
 * @param {Object} symbols locale symbols collection,
 *        could be null.
 *
 * @return {Object} resolved symbols object
 */
DateTimeFormat.resolveSymbols_ = function(symbols) {
  if (!symbols) {
    if (!DateTimeFormat.symbols) {
      DateTimeFormat.symbols = DateTimeConstants;
    }
    symbols = DateTimeFormat.symbols;
  }
  return symbols;
}

// Support functions for date_messages_en_us.js

var CG_MDAYS_OF_THE_WEEK =
  [ MSG_SUN, MSG_MON, MSG_TUES, MSG_WED, MSG_THUR, MSG_FRI, MSG_SAT ];

var CG_DAYS_OF_THE_WEEK =
  [ MSG_SU, MSG_M, MSG_TU, MSG_W, MSG_TH, MSG_F, MSG_SA ];

var FULL_WEEKDAYS = [MSG_SUNDAY, MSG_MONDAY, MSG_TUESDAY, MSG_WEDNESDAY,
                     MSG_THURSDAY, MSG_FRIDAY, MSG_SATURDAY];
var FULL_WEEKDAYS_PLURAL = [
    MSG_SUNDAYS, MSG_MONDAYS, MSG_TUESDAYS, MSG_WEDNESDAYS, MSG_THURSDAYS,
    MSG_FRIDAYS, MSG_SATURDAYS];

/** 1 indexed array of month names. */
var MONTHS = [ , MSG_JAN, MSG_FEB, MSG_MAR, MSG_APR, MSG_MAY, MSG_JUN, MSG_JUL,
              MSG_AUG, MSG_SEP, MSG_OCT, MSG_NOV, MSG_DEC ];


var FULL_MONTHS = [, MSG_JANUARY, MSG_FEBRUARY, MSG_MARCH, MSG_APRIL,
                   MSG_MAY_LONG, MSG_JUNE, MSG_JULY, MSG_AUGUST, MSG_SEPTEMBER,
                   MSG_OCTOBER, MSG_NOVEMBER, MSG_DECEMBER ];

var MSG_ORDINALS = [, MSG_ORDINAL_1, MSG_ORDINAL_2, MSG_ORDINAL_3];

// A library that enables drag and drop, drag to scroll, drag to resize, and
// gesture capturing interactions.

// See dragdrop.html for a usage example.

// PURPOSE
// This class provides a number of drag and drop interactions.  It differs from
// some other drag and drop libraries in that you never have to register objects
// as draggable ahead of time, and there are no listeners installed on objects,
// so the system need not be aware of the addition, deletion, or modification of
// draggable entities.

// DEPENDENCIES:
//   bind.js
//   listen.js
//   common.js
//   shapes.js
//   geom.js

// INTERACTIONS
// This library supports 4 kinds of interactions:
// 1) DD_REPOSITION: dragging an interface element around the screen.
// 2) DD_RESIZE: resizing an element.  Only supports resizing from the bottom
//    or right edge.
// 3) DD_SCROLL: scroll an iframe or a <div style=overflow:scroll> by pulling
//    the offscreen content towards you.
// 4) DD_GESTURE: capture user generated gestures, rendering the partially
//    drawn gesture
// 5) DD_SELECT: creates a rectangular region spanning the start point and
//    current position.  An abstract drag handler may be extended to
//    keep track of a list of "selected" children of the original element,
//    changing styles as appropriate.


// USAGE
// Drag interactions are handled by "drag handlers".  When the user holds down
// their mouse, each registered drag handler is asked if it wants to handle the
// drag.  If a drag handler does want to, then it handles the drag interactions
// until the drag is cancelled or the user releases the mouse button ending the
// drag.
//
// The drag handler is responsible for classifying the drag as a reposition,
// or resize, or etc. interaction, and for identifying the element the user
// wishes to interact with.  The handler can also impose "constraints" on the
// interaction, specifying where it can be dragged, by how much it can be
// resized, etc.  Finally, the handler can provide a callback which takes action
// when the drag is completed.
//
// Constraints are specified as function compositions.  This file provides a
// number of DD_Create* functions that create constraint functions.


// INTERFACES
// Drag Handler Constructor
// ------------------------
// A drag handler constructor may be "registered" with this module via the
// DD_RegisterHandler() function.  The handler will then be called on every
// mouse down event to see if that handler constructor wants to start a drag
// interaction.

// The signature for a drag handler constructor is (function (node, event))
// where node is an HtmlElement and event is the mouse Event that started
// the drag. The constructor should examine node, e.g. by looking at
// its id, or class. It may also consider the point on the screen where the
// mouse event occurred (see geom.js#GetMousePosition()).
// If the constructor wants to start a drag interaction, it should create
// a drag handler and return it.

// Drag Handler
// ------------
// A Drag Handler is an object with the following methods:
//   boolean startDrag(event, node)
//   boolean handleDragSegment(event, node, type, delta)
//   boolean finishDrag(event, node, type)
//   void dragDone(node, type)
//   void useUserDefinedAlpha(boolean) [optional: defaults to { return false; }]
// "event" is the mouse event object that triggered the call: a mouse down
// event for startDrag, a mouse move event for handleDragSegment, or a mouse up
// or mouse out event for finishDrag,
// "node' is the HtmlElement being dragged,
// "type" is one of (DD_REPOSITION, DD_RESIZE, DD_SCROLL, DD_GESTURE),
// and "delta" is the distance dragged -- a Delta object with dx and dy
// properties.
//
// After a drag handler is created by the constructor, it's startDrag method
// is called (exactly once) to setup the drag.  It must set several globals:
//   dd_dragElement: set it to the node to be dragged --
//                   often node or some ancestor such as node.parentNode.
//   dd_dragType: set it to one of DD_REPOSITION, DD_RESIZE, DD_SCROLL, ...
//   dd_axisMask: a bitmask of DD_X_AXIS and DD_Y_AXIS.  This bitmask controls
//                which axes the node may be moved. resized, or scrolled along.
//                It must be non-zero.
// During startDrag, the handler's useUserDefinedAlpha() method is called,
// if it exists (if not, it is assumed to have returned false). Normally,
// the alpha channel of the element being dragged is managed by the drag drop
// library, but if useUserDefinedAlpha() is true, then the client is repsonsible
// for managing it himself.
//
// The startDrag method should return true to indicate the drag should proceed.

// After the drag is started, the "handleDragSegment" method will be called as
// the user moves the mouse.  It may modify the delta argument to prevent the
// node from being moved in certain ways.  This method can be composed from
// the Create*Constraint methods to limit the drag to certain regions.
// Return false from this method to cancel the drag.

// When the user releases the mouse, the finishDrag method is called.  This
// method may take action on the drag, and may reposition the node.  Returning
// false from finishDrag will cancel the drag.

// Cancelling a drag causes a repositioned or resized element to return to its
// original state.

// Once the drag is finished or cancelled, the dragDone method is called.
// It doesn't receive an event, since it may be called in response to a
// programmattic CancelDrag, but it is guaranteed to be called exactly once per
// startDrag.

// Constraints
// -----------
// The Create*Constraint methods provide constraints on drag motion and drag
// endings, constraining the drag to a region, snapping the drag to a grid, or
// only allowing a dragged element to be dropped within a region.


// PUBLIC API
// - _DD_Install(doc) - should be called after document load to set up handlers
// - DD_RegisterHandler(handler) - registers a "drag handler" constructor.
// - DD_CancelDrag() - cancels the current drag.


// CAVEATS
// 1) This module depends heavily on the CSS style
//   * {
//     -moz-box-sizing: border-box;
//   }
//    which causes borders to be sized from within the elements bounding box in
//    mozilla, as IE does.  This removes the need for a lot of hacks to find the
//    border size.
//    For a complete description of the problem see
//    http://t/mvn [www.activewidgets.com]
// 2) Gesture rendering requires a placeholder object for the gesture such as
//    <div id=gesture></div>


// TODO(msamuel): some cue, such as a changed cursor, when dragging over a
//   region where it is not valid to drop.
// TODO(msamuel): shadow or lasso to show target location when using a snap to
//   grid constrain.
// TODO(msamuel): get transparency working on IE.

/** a constraint that prevents an element from being position or resized outside
  * the element with the given id.
  * @param id an HtmlElement id as per document.getElementById.
  * @param opt_axisMask an axis mask to restrict the constraints
  *           to a particular axis; DD_XY_AXES by default
  */
function DD_CreateRegionBoundaryConstraint(id, opt_axisMask) {
  return DD_CreateBoundingBoxUnionBoundaryConstraint([id], opt_axisMask);
}

/** a contraint that prevents an element from being position or resized outside
  * a group of elements.  It may at a given time, be outside any one of those
  * elements, but is restricted to the smallest rectangle containing all.
  * @param ids HtmlElement ids as per document.getElementById.
  * @param opt_axisMask an axis mask to restrict the constraints
  *           to a particular axis; DD_XY_AXES by default
  */
function DD_CreateBoundingBoxUnionBoundaryConstraint(ids, opt_axisMask) {
  if (!opt_axisMask) opt_axisMask = DD_XY_AXES;
  return function (event, el, type, delta) {
    var region = null;
    for (var i = 0; i < ids.length; ++i) {
      var constraintNode = forid(ids[i]);
      var boundingBox = nodeBounds(constraintNode);
      if (!region) {
        region = boundingBox;
      } else {
        region.w = Math.max(region.x + region.w, boundingBox.x + boundingBox.w);
        region.h = Math.max(region.y + region.h, boundingBox.y + boundingBox.h);
        region.x = Math.min(region.x, boundingBox.x);
        region.y = Math.min(region.y, boundingBox.y);
        region.w -= region.x;
        region.h -= region.y;
      }
    }
    if (region) {
      var bounds = nodeBounds(el);

      var nx0, ny0, nx1, ny1;

      switch (type) {
        case DD_REPOSITION:
          nx0 = (bounds.x + delta.dx);
          ny0 = (bounds.y + delta.dy);
          break;
        case DD_RESIZE:
          nx0 = bounds.x;
          ny0 = bounds.y;
          break;
        default:
          return true;
      }
      nx1 = bounds.x + delta.dx + bounds.w;
      ny1 = bounds.y + delta.dy + bounds.h;
      // should add border width to nx1, ny1 in FF
      // but not needed if using -moz-box-sizing: border-box;

      // calculate the amount of space on between the left, right, top, and
      // bottom of the element and its container.
      var sl = region.x - nx0,
          sr = nx1 - (region.x + region.w),
          st = region.y - ny0,
          sb = ny1 - (region.y + region.h);

      switch (type) {
        case DD_REPOSITION:
          if (opt_axisMask & DD_X_AXIS) {
            if (sl > 0) {
              delta.dx += sl;
            } else if (sr > 0) {
              delta.dx -= sr;
            }
          }
          if (opt_axisMask & DD_Y_AXIS) {
            if (st > 0) {
              delta.dy += st;
            } else if (sb > 0) {
              delta.dy -= sb;
            }
          }
          break;
        case DD_RESIZE:
          if (sr > 0 && (opt_axisMask & DD_X_AXIS)) { delta.dx -= sr; }
          if (sb > 0 && (opt_axisMask & DD_Y_AXIS)) { delta.dy -= sb; }
          break;
      }
    }
    return true;
  };
}

/** a contraint that prevents an element from being repositioned except within a
  * specified set of regions.
  * @param allowedRegionIds ids of HtmlElements as per document.getElementById.
  */
function DD_CreateDropRegionConstraint(allowedRegionIds) {
  return function (event, el, type) {
    // check that the item is contained in one of the boxes
    var bounds = nodeBounds(el);
    for (var i = 0; i < allowedRegionIds.length; i++) {
      var target = forid(allowedRegionIds[i]);
      if (!target) { continue; }
      var region = nodeBounds(target);
      if (region.x <= bounds.x &&
          (region.x + region.w) >= (bounds.x + bounds.w) &&
          region.y <= bounds.y &&
          (region.y + region.h) >= (bounds.y + bounds.h)) {
        return true;
      }
    }
    return false;
  };
}

/** a finishDrag contraint that will align a repositioned or resized element
  * with a grid.  The return value is suitable as either a finishDrag or
  * handleDragSegment handler.
  *
  * @param gridFn a function that defines a grid.
  *    The grid function has the signature
  *      (function (point))
  *    and yields a rectangle object containing the bounds of the cell
  *    containing the given point in the window's coordinate frame.
  *
  *    If the grid function returns null or undefined then this constraint
  *    assumes there is no grid cell, and cancels the drag.  This function may
  *    be wrapped if the handler wants to keep going on an out of bounds drag.
  */
function DD_CreateSnapToGridConstraint(gridFn) {
  // the parameters are a union of the handleDragSegment and finishDrag
  // parameters.  If opt_delta is defined then we assume its being called as
  // a drag segment handler.
  return function (event, el, type, opt_delta) {
    var bounds = nodeBounds(el);
    var offParentLoc = nodeLoc(el.offsetParent);
    var gridCell;
    switch (type) {
      case DD_REPOSITION:
        if (undefined !== opt_delta) {
          if (dd_axisMask & DD_X_AXIS) { bounds.x += opt_delta.dx; }
          if (dd_axisMask & DD_Y_AXIS) { bounds.y += opt_delta.dy; }
        }

        gridCell = gridFn(bounds);
        if (!gridCell) { return false; }  // cancel if not in a cell
        if (dd_axisMask & DD_X_AXIS) {
          if (undefined !== opt_delta) {
            opt_delta.dx += gridCell.x - bounds.x + offParentLoc.x;
          } else {
            el.style.left = (gridCell.x) + 'px';
          }
        }
        if (dd_axisMask & DD_Y_AXIS) {
          if (undefined !== opt_delta) {
            opt_delta.dy += gridCell.y - bounds.y + offParentLoc.y;
          } else {
            el.style.top = (gridCell.y) + 'px';
          }
        }
        break;
      case DD_RESIZE:
        if (undefined !== opt_delta) {
          if (dd_axisMask & DD_X_AXIS) { bounds.w += opt_delta.dx; }
          if (dd_axisMask & DD_Y_AXIS) { bounds.h += opt_delta.dy; }
        }

        if (undefined === opt_delta) {
          var bottomRight =
            new Point(bounds.x + bounds.w, bounds.y + bounds.h, window);
          gridCell = gridFn(bottomRight);
          if (!gridCell) { return false; }  // cancel if not in a cell
          if (dd_axisMask & DD_X_AXIS) {
            var newWidth = (gridCell.x - el.offsetLeft);
            if (newWidth <= 0) { return false; }
            el.style.width = newWidth + 'px';
          }
          if (dd_axisMask & DD_Y_AXIS) {
            var newHeight = (gridCell.y - el.offsetTop);
            if (newHeight <= 0) { return false; }
            el.style.height = newHeight + 'px';
          }
        }
        break;
      case DD_SELECT:
        if (opt_delta) {
          var gridCell = gridFn(dd_mousePos);
          if (!gridCell) { return false; }  // cancel if out of grid bounds

          if (dd_axisMask & DD_X_AXIS) {
            if (dd_mousePos.x - gridCell.x < (gridCell.w / 2)) {
              // snap to left of cell
              opt_delta.dx -= gridCell.x - dd_mousePos.x;
            } else {
              // snap to right of cell
              opt_delta.dx -= gridCell.x + gridCell.w - dd_mousePos.x;
            }
          }

          if (dd_axisMask & DD_Y_AXIS) {
            if (dd_mousePos.y - gridCell.y < (gridCell.h / 2)) {
              // snap to top of cell
              opt_delta.dy -= gridCell.y - dd_mousePos.y;
            } else {
              // snap to bottom of cell
              opt_delta.dy -= gridCell.y + gridCell.h - dd_mousePos.y;
            }
          }
        }
        break;
    }
    return true;
  };
}

/** a drag type ({@link #dd_dragType} indicating that there is no drag in
  * progress.
  */
var DD_NODRAG = 0;
/** a drag type ({@link #dd_dragType} indicating that the user is repositiong
  * an element.
  */
var DD_REPOSITION = 1;
/** a drag type ({@link #dd_dragType} indicating that the user is resizing
  * an element.
  */
var DD_RESIZE = 2;
/** a drag type ({@link #dd_dragType} indicating that the user is scrolling
  * an element.
  */
var DD_SCROLL = 3;
/** a drag type ({@link #dd_dragType} indicating that the user is drawing a
  * gesture.
  */
var DD_GESTURE = 4;
/** a drag type ({@link #dd_dragType} indicating that the user is selecting a
  * region.
  */
var DD_SELECT = 5;
/** the number of valid drag type values and 1 greater than the max drag type
  * value.
  */
var DD_NUM_TYPES = 6;

// bit mask fields for dd_axisMask
/** in {@link #dd_axisMask} indicates that movement along the x axis is
  * significant.
  */
var DD_X_AXIS = 0x1;
/** in {@link #dd_axisMask} indicates that movement along the y axis is
  * significant.
  */
var DD_Y_AXIS = 0x2;

/** {@link #DD_X_AXIS} | {@link #DD_Y_AXIS}. */
var DD_XY_AXES = DD_X_AXIS | DD_Y_AXIS;

/** controls collapsing of gesture points.
  * @private
  */
var DD_GESTURE_THRESHOLD_RADIANS = (2 * Math.PI) / 16;

/** controls collapsing of gesture points.
  * @private
  */
var DD_GESTURE_THRESHOLD_DISP2 = 5 * 5;

/** the element being dragged or undefined. */
var dd_dragElement = undefined;

/** the type of drag -- one of DD_{REPOSITION,RESIZE,SCROLL,...}.  @private */
var dd_dragType = DD_NODRAG;

/** the current drag handler or undefined.  @private */
var dd_handler = undefined;

/** the mouse position that the last drag segment was handled at or undefined.
  * May be modified during DragHandler.handleStart.
  */
var dd_mousePos = undefined;

/** the mouse position of the first drag segment in this drag.  Should not be
  * modified by clients.
  */
var dd_origMousePos = undefined;

/** the original bounds of the dragged element or undefined, so that we can
  * restore the element's state on DD_CancelDrag.
  */
var dd_startRect = undefined;

/** an array of points.  The first is the point in document space of the start
  * of the gesture.  The rest are {@link Delta}s containing the gesture
  * segments.
  * @private
  */
var dd_gesture = undefined;

/** a bitmask of DD_{X,Y}_AXIS.
  * @private
  */
var dd_axisMask = undefined;

/** position info used to backtrack the mouse position when a drag segment
  * constraint rewrites delta.
  *
  * <p>This should be the difference between the mouse position relative to
  * dd_dragElement at the time the drag started, and the mouse position
  * relative to dd_dragElement now.</p>
  *
  * @private
  */
var dd_accumulatedDelta = undefined;

/** Distance that the drag element has moved since the start of the drag.
  * Used for relative drags, especially in the presence of changes to the
  * DOM.  This is only relevant for relative positioning.
  *
  * @private
  */
var dd_dragDistance = undefined;


/** true iff the current dd_dragElement has been manipulated in some way,
  * i.e. has handleDragSegment been called at least once during this drag?
  */
var dd_wasDragged = false;

/** used to restore z index after drag complete. */
var dd_originalZIndex = undefined;

/** The document to register handlers with; defaults to current scope */
var dd_document = undefined;

/** can be used to create a constraint that is a conjunction of two constraints.
  * @param f1 a boolean function.
  * @param f2 a function with the same signature as f1.
  * @return a function with the same signature as f1 and f2 that is the
  *   shortcutting conjunction of the two.
  */
function DD_ComposeBooleanConjunction(f1, f2) {
  return function () {
    return f1.apply(this, arguments) && f2.apply(this, arguments);
  };
}

/** install handlers so that they receive the event object.
  * Should be called after body load.
  */
function _DD_Install(opt_doc) {
  if (!opt_doc) { opt_doc = document; }
  dd_document = opt_doc;
  listen(dd_document.body, 'mousedown', DD_Omd);
}

/** all registered handlers.  @private */
var dd_handlers = [];

/** register a drag handler constructor.
  * @param constructor a drag handler constructor as described at the top of
  *   this file.
  */
function DD_RegisterHandler(constructor) {
  dd_handlers.push(constructor);
}

/** chooses a handler for the given html element.
  * @param node an HtmlElement.
  * @private
  */
function DD_GetHandler(node, event) {
  for (var i = dd_handlers.length - 1; i >= 0; --i) {
    var handler = dd_handlers[i](node, event);
    if (handler) { return handler; }
  }
  return undefined;
}

// stores mouse down event
// can decide later if this is the beginning of a click or a drag
var dd_mouseDownEvent;

function DD_Omd(e) {
  dd_mouseDownEvent = CloneEvent(e || window.event);
  listen(dd_document.body, 'mousemove', DD_Omm);
  listen(dd_document.body, 'mouseup', DD_Omu);

  return false;
}

/** detect the start of a drag -- on mouse down.  @private */
function DD_OmdAndDrag(event) {
  // look for a DragHandler instance
  var handler = undefined;
  for (var el = event.srcElement || event.target; el; el = el.parentNode) {
    handler = DD_GetHandler(el, event);
    if (!handler) continue;
    // TODO(msamuel): disallow any scrolls where the container exceeds the
    // size of the drag element, and all of the drag element is visible

    dd_mousePos = GetMousePosition(event);

    // we've found a handler, see if the handler wants to handle the drag
    if (handler.startDrag(event, el)) {
      // startDrag should have set dd_dragElement, dd_dragType, and
      // dd_axisMask
      if (!(dd_dragElement && dd_dragType >= 0 &&
            dd_dragType < DD_NUM_TYPES && 1 === (1 + (dd_dragType % 1)) &&
            dd_axisMask && 0 === (dd_axisMask & ~(DD_X_AXIS | DD_Y_AXIS)))) {
        var msg = 'Bogus drag: el=' + dd_dragElement +
                  ', type=' + dd_dragType + ', axis mask=' + dd_axisMask;
        DD_EndDrag();
        raise(msg);
      }

      event.cancelBubble = true;  // make sure the event isn't handled twice
      dd_handler = handler;
      dd_origMousePos = dd_mousePos;
      dd_startRect = { x: dd_dragElement.offsetLeft,
                       y: dd_dragElement.offsetTop,
                       w: dd_dragElement.offsetWidth,
                       h: dd_dragElement.offsetHeight };
      if (dd_dragType === DD_GESTURE) {
        dd_gesture = [dd_mousePos];
      }
      dd_accumulatedDelta = new Delta(0, 0);
      dd_dragDistance = new Delta(0, 0);
      if (dd_dragType === DD_REPOSITION || dd_dragType === DD_RESIZE ||
          dd_dragType === DD_SELECT) {

        // Resize does NOT do the alpha styling
        if (dd_dragType !== DD_RESIZE && dd_handler.useUserDefinedAlpha &&
               !dd_handler.useUserDefinedAlpha()) {
          DD_Alpha(dd_dragElement, true);
        }
        dd_originalZIndex = dd_dragElement.style.zIndex;
        dd_dragElement.style.zIndex = 2000;
      }

      // install more mouse handlers
      if (BR_IsIE()) {
        listen(dd_document.body, 'mouseleave', DD_Omo);
      } else {
        listen(dd_document.body, 'mouseout', DD_Omo);
      }
    } else {
      DD_EndDrag();
    }
    return false;
  }
  return true;  // let the event work as normal
}

/** handle the end of a drag -- on mouse up.  @private */
function DD_Omu(e) {
  if (dd_mouseDownEvent) {
    // was a click, not a drag
    dd_mouseDownEvent = null;
    unlisten(dd_document.body, 'mousemove', DD_Omm);
    unlisten(dd_document.body, 'mouseup', DD_Omu);

    // See if a handler wants to process clicks
    var handler = undefined;
    for (var el = e.srcElement || e.target; el; el = el.parentNode) {
      handler = DD_GetHandler(el, e);
      if (handler && handler.handleClick) {
        handler.handleClick(el, e);
        return false;
      }
    }
    return;
  }
  var event = e || dd_document.parentWindow.event;
  if (!dd_handler.finishDrag(event, dd_dragElement, dd_dragType)) {
    DD_CancelDrag();
  } else {
    DD_EndDrag();
  }
}

/** handle a drag segment -- on mouse move.  @private */
function DD_Omm(e) {
  if (dd_mouseDownEvent) {
    var res = DD_OmdAndDrag(dd_mouseDownEvent);
    dd_mouseDownEvent = null;
    if (res) {
      DD_CancelDrag();
      return true;
    }
  }

  if (!dd_dragElement) { return true; }

  // TODO(msamuel) - We occasionally get into a horrible state where omm is
  // called after the dd_handler has been cleaned up. This causes an infinite
  // error loop. We can detect this state and recover here.
  if (!dd_handler) {return true; }

  var event = e || dd_document.parentWindow.event;
  event.cancelBubble = true;

  var pos = GetMousePosition(event);
  var odelta = new Delta(pos.x - dd_mousePos.x + dd_accumulatedDelta.dx,
                         pos.y - dd_mousePos.y + dd_accumulatedDelta.dy);
  if (!(odelta.dx | odelta.dy)) { return false; }
  var delta = new Delta(odelta.dx, odelta.dy);
  dd_mousePos = pos;
  dd_wasDragged = true;
  var oldLoc = nodeLoc(dd_dragElement);

  if(dd_axisMask & DD_X_AXIS) {
    dd_dragDistance.dx += delta.dx;
  }
  if(dd_axisMask & DD_Y_AXIS)  {
    dd_dragDistance.dy += delta.dy;
  }

  if (dd_handler.handleDragSegment(event, dd_dragElement, dd_dragType, delta,
                                   dd_dragDistance)) {
    var newLoc = nodeLoc(dd_dragElement);
    // If the base location was changed by the handler, e.g., by
    // editing the DOM, cancel that change out so the absolute
    // position remains consistent.
    dd_dragDistance.dx -= (newLoc.x - oldLoc.x);
    dd_dragDistance.dy -= (newLoc.y - oldLoc.y);

    // accumulate delta for certain drag types.  A user who drags from the
    // bottom of an item upwards to make it smaller cannot resize to less than
    // zero height, but if the user drags the mouse above the top, then the
    // item should not start resizing to height > 0 until the mouse is below the
    // top of the item.
    // The accumulated delta property addresses this by accumulating all unused
    // delta that is subtracted by the handleDragSegment function
    switch (dd_dragType) {
      case DD_REPOSITION:
      case DD_RESIZE:
      case DD_SELECT:
        dd_accumulatedDelta.dx = odelta.dx - delta.dx;
        dd_accumulatedDelta.dy = odelta.dy - delta.dy;
        break;
    }

    // handler may have modified delta in place to do a fit to grid
    switch (dd_dragType) {
      case DD_REPOSITION:
        if (!((dd_axisMask & DD_X_AXIS))) { delta.dx = 0; }
        if (!((dd_axisMask & DD_Y_AXIS))) { delta.dy = 0; }
        var style = dd_dragElement.currentStyle ? dd_dragElement.currentStyle :
                    dd_dragElement.ownerDocument.defaultView.getComputedStyle(
                        dd_dragElement, "");
        if (style && style.position == "relative") {
          // The offset of a relatively positioned element is the distance
          // between where the mouse is now and where the mouse started.
          dd_dragElement.style.left = dd_dragDistance.dx + "px";
          dd_dragElement.style.top = dd_dragDistance.dy + "px";
        } else {
          var ool = dd_dragElement.offsetLeft + delta.dx,
              oot = dd_dragElement.offsetTop + delta.dy;
          if (delta.dx) {
            dd_dragElement.style.left = ool + 'px';
          }
          if (delta.dy) {
            dd_dragElement.style.top = oot + 'px';
          }
          // in -moz-box-sizing:border-box mode, repositioning an item doesn't
          // take into account it's border, so it may be off
          if (delta.dx && ool !== dd_dragElement.offsetLeft) {
            dd_dragElement.style.left =
              (ool + ool - dd_dragElement.offsetLeft) + 'px';
          }
          if (delta.dy && oot !== dd_dragElement.offsetTop) {
            dd_dragElement.style.top =
              (oot + oot - dd_dragElement.offsetTop) + 'px';
          }
        }
        break;
      case DD_SELECT:
        // reposition as necessary and then drop through to resize to handle
        // resizing
        var bounds = nodeBounds(dd_dragElement);
        if (dd_axisMask & DD_X_AXIS) {
          var mpx = dd_mousePos.x + dd_accumulatedDelta.dx;
          var newX = Math.min(mpx, dd_origMousePos.x);
          var newWidth = Math.abs(mpx - dd_origMousePos.x);

          if (newX !== bounds.x) {
            var ool = newX;
            dd_dragElement.style.left = ool + 'px';
            // in -moz-box-sizing:border-box mode, repositioning an item doesn't
            // take into account it's border, so it may be off
            if (ool !== dd_dragElement.offsetLeft) {
              dd_dragElement.style.left =
                (ool + ool - dd_dragElement.offsetLeft) + 'px';
            }
          }
          if (newWidth !== bounds.w) {
            dd_dragElement.style.width = newWidth + 'px';
            if (newWidth != dd_dragElement.offsetWidth) {
              newWidth = Math.max(
                  0, newWidth + newWidth - dd_dragElement.offsetWidth);
              dd_dragElement.style.width = newWidth + 'px';
            }
          }
        }
        if (dd_axisMask & DD_Y_AXIS) {
          var mpy = dd_mousePos.y + dd_accumulatedDelta.dy;
          var newY = Math.min(mpy, dd_origMousePos.y);
          var newHeight = Math.abs(mpy - dd_origMousePos.y);

          if (newY !== bounds.y) {
            var oot = newY;
            dd_dragElement.style.top = oot + 'px';
            // in -moz-box-sizing:border-box mode, repositioning an item doesn't
            // take into account it's border, so it may be off
            if (oot !== dd_dragElement.offsetTop) {
              dd_dragElement.style.top =
                (oot + oot - dd_dragElement.offsetTop) + 'px';
            }
          }
          if (newHeight !== bounds.h) {
            dd_dragElement.style.height = newHeight + 'px';
            if (newHeight != dd_dragElement.offsetHeight) {
              newHeight = Math.max(
                  0, newHeight + newHeight - dd_dragElement.offsetHeight);
              dd_dragElement.style.height = newHeight + 'px';
            }
          }
        }
        break;
      case DD_RESIZE:
        // 2 should be horizontal/vertical insets as
        // appropriate on navigator only.
        // but not needed if -moz-box-sizing: border-box;
        var MIN_SIZE = 1;
        if (delta.dx && (dd_axisMask & DD_X_AXIS)) {

          var offsetWidth = dd_dragElement.offsetWidth;
          var newWidth = offsetWidth + delta.dx;
          if (newWidth < 0) {
            dd_accumulatedDelta.dx += newWidth;
            newWidth = 0;
          }
          if (newWidth < MIN_SIZE) {
            dd_accumulatedDelta.dx += newWidth - MIN_SIZE;
            newWidth = MIN_SIZE;
          }
          // from http://www.quirksmode.org/dom/getstyles.html
          //   "If you view this example in both browsers, you'll note that in
          //   Explorer the new width is the old width + 100px, but in Mozilla
          //   it isn't. That's because Mozilla is more standards-conforming
          //   here: as per the spec it counts only the width of the actual
          //   content as offsetWidth, while Explorer also adds the padding
          //   and the border. Even though Mozilla is correct here, I tend to
          //   favour the Explorer approach because it's more intuitive."
          dd_dragElement.style.width = newWidth + 'px';
          if (newWidth != dd_dragElement.offsetWidth) {
            newWidth += newWidth - dd_dragElement.offsetWidth;
            if (newWidth < MIN_SIZE) { newWidth = MIN_SIZE; }
            dd_dragElement.style.width = newWidth + 'px';
          }
        }
        if (delta.dy && (dd_axisMask & DD_Y_AXIS)) {
          var offsetHeight = dd_dragElement.offsetHeight;
          var newHeight = offsetHeight + delta.dy;
          if (newHeight < 0) {
            dd_accumulatedDelta.dy += newHeight;
            newHeight = 0;
          }
          if (newHeight < MIN_SIZE) {
            dd_accumulatedDelta.dy += newHeight - MIN_SIZE;
            newHeight = MIN_SIZE;
          }
          dd_dragElement.style.height = newHeight + 'px';
          if (newHeight != dd_dragElement.offsetHeight) {
            newHeight += newHeight - dd_dragElement.offsetHeight;
            if (newHeight < MIN_SIZE) { newHeight = MIN_SIZE; }
            dd_dragElement.style.height = newHeight + 'px';
          }
        }
        break;
      case DD_SCROLL:
        if (!(dd_axisMask & DD_X_AXIS)) { delta.dx = 0; }
        if (!(dd_axisMask & DD_Y_AXIS)) { delta.dy = 0; }
        if (dd_dragElement.scrollBy) {
          // TODO(msamuel): clip scroll delta
          dd_dragElement.scrollBy(delta.dx, delta.dy);
        } else {
          // not an iframe, so treat as a DD_REPOSITION.  We could model this
          // as a DD_REPOSITION, but I'd rather separate drag type as
          // actions as apparent to the user so that we can tie user visible
          // cues such as cursor to the drag type.
          delta.dx *= -1;
          delta.dy *= -1;

          var container = dd_dragElement.parentNode;
          // subtract scrollbars?
          var sl = dd_dragElement.scrollLeft;
          var sr = dd_dragElement.scrollLeft + dd_dragElement.scrollWidth -
                   dd_dragElement.offsetWidth;
          var st = dd_dragElement.scrollTop;
          var sb = dd_dragElement.scrollTop + dd_dragElement.scrollHeight -
                   dd_dragElement.offsetHeight;
          // clip scroll delta
          delta.dx = Math.max(Math.min(delta.dx, sr), -sl);
          delta.dy = Math.max(Math.min(delta.dy, sb), -st);

          if (delta.dx) {
            dd_dragElement.scrollLeft = dd_dragElement.scrollLeft + delta.dx;
//          dd_dragElement.style.left =
//            (dd_dragElement.offsetLeft + delta.dx) + 'px';
          }
          if (delta.dy) {
            dd_dragElement.scrollTop = dd_dragElement.scrollTop + delta.dy;
//          dd_dragElement.style.top =
//            (dd_dragElement.offsetTop + delta.dy) + 'px';
          }
        }
        break;
      case DD_GESTURE:
        // ignore axisMask for gestures
        if (delta.dx | delta.dy) {
          // collapse this delta into the tail segment if the angles differ
          // by less than some threshold
          if (dd_gesture.length >= 1) {
            var segment1 = delta;
            var segment2 = dd_gesture[dd_gesture.length - 1];
            var segment3 =
              new Delta(segment1.dx + segment2.dx, segment1.dy + segment2.dy);
            var collapse = false;
            if (segment3.dx * segment3.dx + segment3.dy * segment3.dy <=
                DD_GESTURE_THRESHOLD_DISP2) {
              collapse = true;
            } else {
              var theta1 = Math.atan2(segment1.dx, segment1.dy);
              var theta2 = Math.atan2(segment2.dx, segment2.dy);
              var diff = Math.abs((theta1 - theta2 + (2 * Math.PI)) %
                                  (2 * Math.PI));
              collapse = (diff < DD_GESTURE_THRESHOLD_RADIANS);
            }
            if (collapse) {
              segment2.dx = segment3.dx;
              segment2.dy = segment3.dy;
            } else {
              dd_gesture.push(delta);
            }
          } else {
            dd_gesture.push(delta);
          }
          DD_RenderGesture(dd_gesture);
        }
        break;
    }
  } else {
    DD_CancelDrag();
  }
  return false;
}

/** called when the mouse leaves an item to detect if the mouse has left the
  * window during a drag.
  */
function DD_Omo(e) {
  e = e || window.event;
  var el = e.relatedTarget || e.toElement;  // the dom element exited
  if (!el) {  // null on IE, undefined on FF
/*
    // do a secondary check to see if we're still inside the window.
    if (e.clientX >= 0 && e.clientX <= document.body.offsetWidth &&
        e.clientY >= 0 && e.clientY <= document.body.offsetHeight) {
      return;
    }
*/
    // cancel the drag since we don't receive window events when the mouse is
    // outside it.
    DD_CancelDrag();
  }
}

/** called to clean up drag state.  @private */
function DD_EndDrag() {

  // If we are doing some form of drag, then clean it up
  if (dd_dragType !== DD_NODRAG) {
    if (dd_dragElement) {
      if (dd_dragType === DD_GESTURE) {
        DD_RenderGesture([]);
      }
      dd_handler.dragDone(dd_dragElement, dd_dragType);
    }

    if (dd_dragType === DD_REPOSITION || dd_dragType === DD_RESIZE ||
        dd_dragType === DD_SELECT) {
      // restore opacity
      if (dd_dragType !== DD_RESIZE && dd_handler.useUserDefinedAlpha &&
                !dd_handler.useUserDefinedAlpha()) {
        DD_Alpha(dd_dragElement, false);
      }
      // restore z index
      if (undefined !== dd_originalZIndex) {
        dd_dragElement.style.zIndex = dd_originalZIndex;
        dd_originalZIndex = undefined;
      } else {
        delete dd_dragElement.style.zIndex;
      }
    }
  }

  // release any drag state
  dd_dragElement = undefined;
  dd_dragType = DD_NODRAG;
  dd_mousePos = undefined;
  dd_startRect = undefined;
  dd_axisMask = 0;
  dd_accumulatedDelta = undefined;
  dd_wasDragged = false;

  if (dd_handler) {
    if (BR_IsIE()) {
      unlisten(dd_document.body, 'mouseleave', DD_Omo);
    } else {
      unlisten(dd_document.body, 'mouseout', DD_Omo);
    }
  }
  dd_handler = undefined;

  unlisten(dd_document.body, 'mousemove', DD_Omm);
  unlisten(dd_document.body, 'mouseup', DD_Omu);
}

/** cancels any drag in progress, possibly restoring the dragged element to its
  * original position and size.
  */
function DD_CancelDrag() {
  switch (dd_dragType) {
    case DD_REPOSITION:
      var style = dd_dragElement.currentStyle ? dd_dragElement.currentStyle :
                  window.getComputedStyle(dd_dragElement, "");
      if (style && style.position == "relative") {
        dd_dragElement.style.left = "0px";
        dd_dragElement.style.top = "0px";
      } else {
        dd_dragElement.style.left = dd_startRect.x + 'px';
        dd_dragElement.style.top = dd_startRect.y + 'px';
      }
      break;
    case DD_RESIZE:
      dd_dragElement.style.width = dd_startRect.w + 'px';
      dd_dragElement.style.height = dd_startRect.h + 'px';
      break;
    case DD_SCROLL:
      // do nothing
      break;
    case DD_GESTURE:
      dd_gesture = [];
      break;
    case DD_NODRAG:
      break;
    case DD_SELECT:
      break;
    default:
      raise(/* SAFE */ 'failed to cancel drag with dd_dragType=' + dd_dragType);
  }
  DD_EndDrag();
}

/** make the given HtmlElement transparent or opaque.
  *
  * @param el an HtmlElement.
  * @param transparent true to make el transparent.
  */
function DD_Alpha(el, transparent) {
  if (BR_IsIE()) {
    el.style.filter = transparent ? 'alpha(opacity=50)' : 'alpha(opacity=100)';
  } else {
    el.style.MozOpacity = transparent ? .50 : 1.00;
  }
}

// gesture rendering state
/** true iff a redraw is scheduled.  Redraws are throttled to only occur
  * periodically.
  * @private
  */
var dd_redrawScheduled = false;
/** the gesture to draw or undefined.
  * @private
  */
var dd_toDraw = undefined;

/** schedule a render redraw if none already scheduled.
  * @param gesture an array with the same form as dd_gesture.
  * @private
  */
function DD_RenderGesture(gesture) {
  dd_toDraw = gesture;
  if (!dd_redrawScheduled) {
    dd_redrawScheduled = true;
    window.setTimeout(DD_DoRender, 100 /* ms */);
  }
}

/** render the gesture.
  * @private
  */
function DD_DoRender() {
  var gesture = dd_toDraw;
  dd_toDraw = undefined;
  dd_redrawScheduled = false;

  if (!gesture) { return; }

  var html = '';
  if (gesture.length) {
    // assemble gesture out of images, and position over easel
    var x = gesture[0].x, y = gesture[0].y;
    var maxx = 0, maxy = 0;
    for (var i = 1; i < gesture.length; i++) {
      var segment = gesture[i];
      if (!(segment.dx | segment.dy)) { continue; }
      if (i != 0) {
        html += '<img class=gestimg src=images/joiner.png width=5 height=5 ' +
                'style=left:' + (x - 2) + 'px;top:' + (y - 2) + 'px>';
      }
      var theta = Math.atan2(segment.dx, segment.dy);
      var angle;
      var k = Math.floor(theta / (Math.PI / 6));
      switch (k) {
        case 0: case 6: case -6:
          angle = 'vert.png';
          break;
        case 1: case -4:
          angle = 'negslope.png';
          break;
        case 2: case -2:
        case 3: case -3:
          angle = 'horz.png';
          break;
        case 4: case -1:
          angle = 'posslope.png';
          break;
        case 5: case -5:
          angle = 'vert.png';
          break;
      }

      var wid = Math.max(1, Math.abs(segment.dx));
      var hgt = Math.max(1, Math.abs(segment.dy));

      html += '<img class=gestimg src=images/' + angle +
              ' width=' + wid +' height=' + hgt +
              ' style=left:' + (x + Math.min(segment.dx, 0)) + 'px;top:' +
              (y + Math.min(segment.dy, 0)) + 'px>';
      x += segment.dx;
      y += segment.dy;
      maxx = Math.max(x, maxx);
      maxy = Math.max(y, maxy);
    }
  }

  var imageholder = forid('gesture');
  imageholder.style.display = 'none';
  imageholder.innerHTML = html;

  imageholder.style.display = 'inline';
}

// Sample drag handler implementation, for the benefit of jscompiler's
// arity checking

function StubDragHandler_() { }
StubDragHandler_.prototype.handleDragSegment = function(event, element, type, delta, distance) {
  throw new Error("Unimplemented");
};

// An abstract drag handler for DD_SELECT styles.

// See dragdrop.html for a usage example.

// static methods

/** a base class that may be used to provide selection over a group of items
 * contained in a container object.
 *
 * <p>
 * Selectable items must have the class 'ddSelectable' (override isSelectable
 * to change this).
 * <p>
 * An item is considered selected if it has the 'ddSelected' class, and the
 * static methods DD_BaseSelectionHandler.{get,clear}Selection can be used
 * to update the selected group.  The ddSelected style can also be used with
 * CSS to change the appearance of selected items.
 * <p>
 * See the dragdrop.html example page for code that does all this.
 * <p>
 * This implementation sets the axis mask to DD_XY_AXES allowing selection in
 * both dimensions.  If a client wants drag along only one axis, set
 * this.axisMask_ in the constructor and override startDrag to initialize the
 * lasso's (x-axis and width if selecting along the y-axis) or
 * (y-axis and height if selecting along the x-axis), after chaining to the
 * superclasses startDrag.
 *
 * @param selectionContainer the DOM ancestor of the selectable items.
 *
 * @constructor
 */
function DD_BaseSelectionHandler(selectionContainer) {
  this.selectionContainer_ = selectionContainer;
  this.selection_ = selectionContainer ?
      DD_BaseSelectionHandler.getSelection(selectionContainer) : undefined;
  this.finished_ = false;
  this.lasso_ = null;
  this.axisMask_ = DD_XY_AXES;
}

/**
 * returns a list of HTMLElements all of whom have the ddSelected class.
 * The list returns all elements that have the ddSelected and that are a
 * descendant of selectionContainer and that do not have an ancestor that is
 * both a descendant of selectionContainer and has the ddSelected class.
 *
 * <p>Both descendant and ancestor are used in the loose term, e.g. a node
 * is both its own descendant and ancestor.
 *
 * @param domNode an HtmlElement
 * @param opt_out optionally, the output list to append to.
 */
DD_BaseSelectionHandler.getSelection = function (domNode, opt_out) {
  opt_out = opt_out || [];
  if (domNode.className && domNode.className.match(/\bddSelected\b/)) {
    opt_out.push(domNode);
  } else {
    for (var child = domNode.firstChild; child;
         child = child.nextSibling) {
      DD_BaseSelectionHandler.getSelection(child, opt_out);
    }
  }
  return opt_out;
};

/**
 * marks all children of the given node not selected, returning the modified
 * nodes.
 *
 * @param selectionContainer an HtmlElement
 * @return a list of HtmlElements that are descendants of selectionContainer.
 */
DD_BaseSelectionHandler.clearSelection = function (selectionContainer) {
  var selected = DD_BaseSelectionHandler.getSelection(selectionContainer);
  for (var i = selected.length; --i >= 0;) {
    var item = selected[i];
    item.className = item.className.replace(/\s*\bddSelected\b/g, '');
  }
  return selected;
};

/**
 * Given the lasso and the mouse position of the start of the drag,
 * give the client the opportunity to position the lasso.
 */
DD_BaseSelectionHandler.prototype.initLassoPosition =
    function(lasso, /*!Point*/ mpos) {

  lasso.style.left = mpos.x + 'px';
  lasso.style.top = mpos.y + 'px';
  if (this.axisMask_ & DD_X_AXIS) { lasso.style.width = '0px'; }
  if (this.axisMask_ & DD_Y_AXIS) { lasso.style.height = '0px'; }
}

DD_BaseSelectionHandler.prototype.initLassoStyle = function(lasso) {
  lasso.style.display = 'block';
}

/** provided to implement the DragHandler interface specified in dragdrop.js */
DD_BaseSelectionHandler.prototype.startDrag = function (event, el) {
  var lasso = forid('ddLasso');
  // create a lasso if none present
  if (!lasso) {
    lasso = document.createElement('div');
    lasso.id = 'ddLasso';
    lasso.style.position = 'absolute';
    lasso.style.display = 'none';
    document.body.appendChild(lasso);
  }

  var mpos = GetMousePosition(event);
  this.initLassoPosition(lasso, mpos);
  this.initLassoStyle(lasso);

  dd_dragType = DD_SELECT;
  dd_axisMask = this.axisMask_;
  dd_dragElement = lasso;

  this.lasso_ = lasso;

  // TODO(msamuel): precompute list of selectable items so that
  // computeSelectionHelper need not recurse every time.
  // Could also precompute and cache bounds for selectable items and the
  // selection container.

  var outer = this;
  window.setTimeout(function () {
    if (!outer.finished) {
      outer.computeSelection(lasso);
    }
  }, 200);

  return true;
};
/**
 * provided to implement the DragHandler interface specified in dragdrop.js.
 * This implementation does nothing but signal successful completion.
 */
DD_BaseSelectionHandler.prototype.finishDrag = function (event, node, type) {
  return true;
};
/**
 * true iff the given node is selectable.  This implementation considers
 * any node with the class "ddSelectable" to be selectable, but this method
 * may be overridden to consider different criteria.
 */
DD_BaseSelectionHandler.prototype.isSelectable = function (node) {
  return node.className && node.className.match(/\bddSelectable\b/);
};
/**
 * walk the DOM tree under a node testing each item for selectability, and
 * adding it to the array if it is in bounds.
 * @private
 */
DD_BaseSelectionHandler.prototype.computeSelectionHelper =
  function (domNode, bounds, output) {
  if (this.isSelectable(domNode)) {
    var b = nodeBounds(domNode);
    if (!(bounds.x + bounds.w < b.x ||
          b.x + b.w < bounds.x ||
          bounds.y + bounds.h < b.y ||
          b.y + b.h < bounds.y)) {
      output.push(domNode);
    }
    // don't recurse to children if domNode is selectable
    return;
  } else {
    // recurse to children
    for (var child = domNode.firstChild; child; child = child.nextSibling) {
      this.computeSelectionHelper(child, bounds, output);
    }
  }
};
/** updates the this.selection_ list, and adds/removes the ddSelected class from
 * selectable items as appropriate.  This will *only* modify nodes that are
 * selectable according to {@link #isSelectable}.
 */
DD_BaseSelectionHandler.prototype.computeSelection = function (node) {
  if (!this.selectionContainer_) return;
  var newSelection = [];
  // compute the new selection
  this.computeSelectionHelper(
    this.selectionContainer_, nodeBounds(node), newSelection);

  this.setSelection(newSelection);

  // recompute the selection in another 200 ms if still dragging
  if (!this.finished_) {
    var outer = this;
    window.setTimeout(function () {
      if (!outer.finished) {
        outer.computeSelection(node);
      }
    }, 200);
  }
};
/** sets the selected list, adding/removing the ddSelected class as
 * appropriate.
 */
DD_BaseSelectionHandler.prototype.setSelection = function (newSelection) {
  var old = this.selection_;
  // annotate selected items so we can figure out what is now selected but
  // wasn't before
  for (var i = newSelection.length; --i >= 0;) {
    newSelection[i].dd_newSelection = true;
  }
  // remove the ddSelected class from any that were selected but no longer are
  for (var i = old.length; --i >= 0;) {
    var selectable = old[i];
    selectable.dd_oldSelection = true;
    if (!selectable.dd_newSelection) {
      selectable.className =
        selectable.className.replace(/\s*\bddSelected\b/g, '');
    }
  }
  // mark new items as selected
  for (var i = newSelection.length; --i >= 0;) {
    var selectable = newSelection[i];
    if (!selectable.dd_oldSelection) {
      selectable.className = (selectable.className || ' ') + ' ddSelected';
    }
    // could delete, but IE doesn't support deletion of properties on all
    // objects that support adding of properties.
    selectable['dd_newSelection'] = false;
  }
  // cleanup
  for (var i = old.length; --i >= 0;) {
    old[i].dd_oldSelection = false;
  }

  // swap the new selected array in place
  this.selection_ = newSelection;
};
/** provided to implement the DragHandler interface specified in dragdrop.js. */
DD_BaseSelectionHandler.prototype.handleDragSegment =
function (event, el, type, delta) {
  return true;
};
/**
 * provided to implement the DragHandler interface specified in dragdrop.js.
 * This makes the lasso invisible, computes the selection so that getSelection
 * will work.  Subclasses may override this to do other things on drag end,
 * but should be sure to chain to the super class.
 */
DD_BaseSelectionHandler.prototype.dragDone = function (el, type) {
  this.finished_ = true;
  this.computeSelection(el);
  el.style.display = 'none';
};


/** a selection handler implementation that snaps to grid.
  * @param selectOwner same as for super class.
  * @param gridFn a grid function that obeys the contract specified in
  *   DD_CreateSnapToGridConstraint.
  */
function DD_SnapSelectionHandler(selectOwner, gridFn) {
  DD_BaseSelectionHandler.call(this, selectOwner);
  this.snapConstraints_ = DD_CreateSnapToGridConstraint(gridFn);
  this.gridFn = gridFn;
}
DD_SnapSelectionHandler.prototype = new DD_BaseSelectionHandler(undefined);
DD_SnapSelectionHandler.prototype.constructor = DD_SnapSelectionHandler;
DD_SnapSelectionHandler.prototype.handleDragSegment =
function (event, el, type, delta) {
  return DD_BaseSelectionHandler.prototype.handleDragSegment(
    event, el, type, delta) && this.snapConstraints_(event, el, type, delta);
};
DD_SnapSelectionHandler.prototype.startDrag =
function (event, el) {
  if (DD_BaseSelectionHandler.prototype.startDrag.call(this, event, el)) {
    var cell = this.gridFn(dd_mousePos, true);
    if (dd_axisMask & DD_X_AXIS) {
      dd_mousePos.x =
        (dd_mousePos.x - cell.x < (cell.w >> 1)) ? cell.x : cell.x + cell.w;
    }
    if (dd_axisMask & DD_Y_AXIS) {
      dd_mousePos.y =
        (dd_mousePos.y - cell.y < (cell.h >> 1)) ? cell.y : cell.y + cell.h;
    }
    return true;
  } else {
    return false;
  }
};

// functions for dealing with layout and geometry of page elements.
// Requires shapes.js

/** returns the bounding box of the given DOM node in document space.
  *
  * @param {Element} obj a DOM node.
  * @return a Rect instance.
  */
function nodeBounds(obj) {
  function fixRectForScrolling(r) {
    // Need to take into account scrolling offset of ancestors (IE already does
    // this)
    for (var o = obj.offsetParent; 
         o && o.offsetParent; 
         o = o.offsetParent) {
      if (o.scrollLeft) {
        r.x -= o.scrollLeft;
      }
      if (o.scrollTop) {
        r.y -= o.scrollTop;
      }
    }
  }

  var refWindow;
  if (obj.ownerDocument && obj.ownerDocument.parentWindow) {
    refWindow = obj.ownerDocument.parentWindow;
  } else {
    refWindow = window;
  }

  // Mozilla
  if (obj.ownerDocument && obj.ownerDocument.getBoxObjectFor) {
    var box = obj.ownerDocument.getBoxObjectFor(obj);
    var r = new Rect(box.x, box.y, box.width, box.height, refWindow);
    fixRectForScrolling(r);
    return r;
  }

  // IE
  if (obj.getBoundingClientRect) {
    var rect = obj.getBoundingClientRect();

    return new Rect(rect.left + GetScrollLeft(refWindow),
                    rect.top + GetScrollTop(refWindow),
                    rect.right - rect.left,
                    rect.bottom - rect.top,
                    refWindow);
  }
  
  // Fallback to recursively computing this
  var left = 0;
  var top = 0;
  for (var o = obj; o.offsetParent; o = o.offsetParent) {
    left += o.offsetLeft;
    top += o.offsetTop;
  }
  
  var r = new Rect(left, top, obj.offsetWidth, obj.offsetHeight, refWindow);
  fixRectForScrolling(r);  
  return r;
}

/** returns the coordinates of the top-left of the given DOM node in document
  * space.
  *
  * @param {Element} obj a DOM node.
  * @return a Point instance.
  */
function nodeLoc(obj) {
  var bounds = nodeBounds(obj);

  return new Point(bounds.x, bounds.y, bounds.coordinateFrame);
}

/** the bounding rect of the content of a node in the window's coordinate frame
  *
  * @param {Element} element non null.
  * @return {Rect}
  */
function geom_innerBounds(element /* : HTMLElement*/) {
  var x, y;
  if (element.ownerDocument && element.ownerDocument.getBoxObjectFor) {
    var box = element.ownerDocument.getBoxObjectFor(element);
    x = box.x;
    y = box.y;
    for (var el = element; el; el = el.offsetParent){
      x -= el.scrollLeft;
      y -= el.scrollTop;
    }
  } else {
    x = element.offsetWidth - element.clientWidth;
    y = element.offsetHeight - element.clientHeight;
    for (var el = element; el; el = el.offsetParent){
      x += el.offsetLeft - el.scrollLeft;
      y += el.offsetTop - el.scrollTop;
    }
    // x and y seem to be off by 1.  why?
  }
  return new Rect(x, y, element.clientWidth, element.clientHeight, window);
}

/** the bounding rect of the content of a node in the window's coordinate frame
  *
  * @param {Element} element non null.
  * @return {Point}
  */
function geom_innerLoc(element /* : HTMLElement*/) {
  var x, y;
  if (element.ownerDocument && element.ownerDocument.getBoxObjectFor) {
    var box = element.ownerDocument.getBoxObjectFor(element);
    x = box.x;
    y = box.y;
    for (var el = element; el; el = el.offsetParent){
      x -= el.scrollLeft;
      y -= el.scrollTop;
    }
  } else {
    x = element.offsetWidth - element.clientWidth;
    y = element.offsetHeight - element.clientHeight;
    for (var el = element; el; el = el.offsetParent){
      x += el.offsetLeft - el.scrollLeft;
      y += el.offsetTop - el.scrollTop;
    }
    // x and y seem to be off by 1.  why?
  }
  return new Point(x, y, window);
}

/** Returns the distance between p1 and p2 as a float */
function Distance(/*Point or Rect*/ p1, /*Point or Rect*/ p2) {
  AssertTrue(p1, "p1 passed to Distance is undefined");
  AssertTrue(p2, "p2 passed to Distance is undefined");
  AssertTrue(p1.coordinateFrame == p2.coordinateFrame);
  var dx = p1.x - p2.x;
  var dy = p1.y - p2.y;
  return Math.sqrt(dx * dx + dy * dy);
}

function GetMousePosition(e) {
  // copied from http://www.quirksmode.org/js/events_compinfo.html
  var posx = 0;
  var posy = 0;
  if (e.pageX || e.pageY) {
    posx = e.pageX;
    posy = e.pageY;
  } else if (e.clientX || e.clientY) {
    var obj = (e.target ? e.target : e.srcElement);
    var refWindow;
    if (obj.ownerDocument && obj.ownerDocument.parentWindow) {
      refWindow = obj.ownerDocument.parentWindow;
    } else {
      refWindow = window;
    }
    posx = e.clientX + GetScrollLeft(refWindow);
    posy = e.clientY + GetScrollTop(refWindow);
  }
  return new Point(posx, posy, window);
}

/** <h3>Functions for manipulating icalendar data values: dates, times,
  * etc.</h3>
  * --------------------------------
  * <p>This implements several iCal objects.</p><blockquote>
  *   ICAL_Date - specifies a day <br>
  *   ICAL_Time - a time object <br>
  *   ICAL_DateTime - a day and time of day <br>
  *   ICAL_Duration - a delta time <br>
  *   ICAL_DTBuilder - a mutable temporal that can be converted to one of the
  *     other types. <br>
  *   ICAL_PeriodOfTime - a range of DateTimes.
  *
  *   ICAL_PartialDate - a day which may be incomplete<br>
  *   ICAL_PartialDateTime - a day and time of day which may be incomplete<br>
  *   ICAL_PartialPeriodOfTime - a range of ICAL_PartialDateTimes.
  * </blockquote>
  *
  * <h3>TimeZones</h3>
  * ---------
  * <p>All Dates are assumed to have been converted to the display timezone
  * before being passed to the client.</p>x
  *
  * <h3>Class Hierarchy</h3>
  * ---------------
  * <p>ICAL_Temporal - defines a set of calendar fields and field access methods
  * <blockquote>
  *   ICAL_Date extends Temporal <br>
  *   ICAL_Time extends Temporal <br>
  *   ICAL_DateTime extends Temporal <br>
  *   ICAL_Duration extends Temporal <br>
  *   ICAL_DTBuilder extends Temporal</blockquote>
  * <p>ICAL_PeriodOfTime contains 2 DateTimes</p>
  *
  * <h3>Field String Representation</h3>
  * ---------------------------
  * <p>Times are represented as sets of integer fields where fields include
  * year, month, date, hour, minute, second, etc.
  * <br>The string representations are as specified in
  * <a href="http://www.corp.google.com/~msamuel/rfc2445.html">RFC 2045</a></p>
  */

// some common functions
function ICAL_sign(n) { return n < 0 ? -1 : 1; }
function ICAL_trunc(n) { return n | 0; }
function ICAL_exception(msg) {
  DumpError(msg);
  throw msg;
}
function ICAL_fmtInt(n, w) {
  var s = n.toString();
  while (s.length < w) { s = '0' + s; }
  return s;
}
/** parses an int as a decimal number.  @private */
function ICAL_toInt_(s, a, b) {
  return parseInt(s.substring(a, b), 10);
}
/** lookup table used by {@link #ICAL_daysInMonth}. @private */
var ICAL_daysInMonthCache_ = [
    undefined,
    31, undefined, 31, 30,  // feb is computed
    31, 30,        31, 31,
    30, 31,        30, 31];
/** returns the number of days in the given month.  This is consistent with the
  * javascript date object.
  */
function ICAL_daysInMonth(yr, mo) {
  if (2 !== mo) { return ICAL_daysInMonthCache_[mo]; }
  var k = yr << 4;
  var n = ICAL_daysInMonthCache_[k];
  if (!n) {
    n = Math.round((Date.UTC(yr, 2, 1) - Date.UTC(yr, 1, 1)) / (24 * 3600000));
    ICAL_daysInMonthCache_[k] = n;
  }
  return n;
}
function ICAL_daysInYear(year) {
  return ((29 !== ICAL_daysInMonth(year, 2)) ? 365 : 366);
}
var ICAL_firstDayOfWeekInMonthCache_ = new Object();
/** the index of the first day of the week in the given month. */
function ICAL_firstDayOfWeekInMonth(year, month) {
  var k = (year << 4) | month;
  var n = ICAL_firstDayOfWeekInMonthCache_[k];
  if (!n) {
    n = (new Date(year, month - 1, 1, 0, 0, 0, 0)).getDay();
    ICAL_firstDayOfWeekInMonthCache_[k] = n;
  }
  return n;
}
/** the day of the week for the given temporal. */
function ICAL_getDayOfWeek(temporal) {
  return (temporal.date - 1 +
          ICAL_firstDayOfWeekInMonth(temporal.year, temporal.month)) % 7;
}
/** the number of days between two dates specified as (yr, month, dayOfMonth)
  * triplets.
  */
function ICAL_daysBetween(y1, m1, d1, y2, m2, d2) {
  var d;
  if (y1 === y2) {
    if ((d = m1 - m2) === 0) {
      return d1 - d2;
    } else if (d < 0) {
      d = d1 - d2;  // 23
      do {
        d -= ICAL_daysInMonth(y1, m1++);
      } while (m1 < m2);
      return d;
    } else {
      d = d1 - d2;
      do {
        d += ICAL_daysInMonth(y2, m2++);
      } while (m2 < m1);
      return d;
    }
  } else {
    return Math.round((Date.UTC(y1, m1 - 1, d1) - Date.UTC(y2, m2 - 1, d2)) /
                      (24 * 3600 * 1000));
  }
}
/** the number of days between two dates specified as (yr, month, dayOfMonth)
  * triplets.
  */
function ICAL_daysBetweenDates(d1, d2) {
  return ICAL_daysBetween(d1.year, d1.month, d1.date,
                          d2.year, d2.month, d2.date);
}

/** true iff the given date falls on today or less than nDays in the past. */
function ICAL_rangeContainsToday(date, nDays) {
  var k = ICAL_daysBetweenDates(ICAL_todaysDate, date);
  return k >= 0 && k < nDays;
}

/** Base class.  @constructor */
function ICAL_Temporal(opt_yr, opt_mo, opt_da, opt_hr, opt_mi, opt_se) {
  if (!isNaN(opt_yr)) { this.year = opt_yr; }
  if (!isNaN(opt_mo)) { this.month = opt_mo; }
  if (!isNaN(opt_da)) { this.date = opt_da; }
  if (!isNaN(opt_hr)) { this.hour = opt_hr; }
  if (!isNaN(opt_mi)) { this.minute = opt_mi; }
  if (!isNaN(opt_se)) { this.second = opt_se; }
}
ICAL_Temporal.prototype.year = NaN;
ICAL_Temporal.prototype.month = NaN;
ICAL_Temporal.prototype.date = NaN;
ICAL_Temporal.prototype.hour = NaN;
ICAL_Temporal.prototype.minute = NaN;
ICAL_Temporal.prototype.second = NaN;
ICAL_Temporal.prototype.getDayOfWeek = function () {
  return ICAL_getDayOfWeek(this);
};
ICAL_Temporal.prototype.toString = function() {
  if (this.str_ !== undefined) return this.str_;
  this.str_ = this.toStringRepresentation_();
  return this.str_;
}

/**
 * a placeholder interface for ICAL_Temporal subclasses that represent a date
 * or datetime value.
 * <p>Prefer <tt>AssertType(foo, ICAL_DateValue);</tt> over
 * <tt>AssertTrue(foo instanceof ICAL_Date || foo instanceof ICAL_DateTime)</tt>
 * @constructor
 */
function ICAL_DateValue() {
  // placeholder
}
ICAL_DateValue.prototype = new ICAL_Temporal();
ICAL_DateValue.prototype.constructor = ICAL_DateValue;

/**
 * ICAL_Date is an icalendar date. This constructor is private --
 * use ICAL_Date.create() instead.
 *
 * @constructor
 * @private
 */
function ICAL_Date(yr, mo, da) {
  AssertTrue(mo && da, /* SAFE */ 'invalid date params: ' + mo + ' ' + da);
  ICAL_Temporal.call(this, yr, mo, da, NaN, NaN, NaN);
}
ICAL_Date.prototype = new ICAL_DateValue();
ICAL_Date.prototype.constructor = ICAL_Date;
ICAL_Date.now = function () {
  var d = new Date();
  return ICAL_Date.create(d.getFullYear(), d.getMonth() + 1, d.getDate());
};
ICAL_Date.prototype.type = /* SAFE */ 'Date';
ICAL_Date.prototype.toDate = function () { return this; };
ICAL_Date.prototype.toDateTime = function () {
  return new ICAL_DateTime(this.year, this.month, this.date, 0, 0, 0);
};
ICAL_Date.prototype.getComparable = function () {
  if (undefined === this.cmp_) {
    // only if someone uses the constructor (which shouldn't happen, but might)
    // will this.cmp_ need to be calculated here
    this.cmp_ =
        ICAL_CalculateDateComparable_(this.year, this.month, this.date);
  }
  return this.cmp_;
};
function ICAL_CalculateDateComparable_(year, month, date) {
  return (((((year - 1970) * 12) + month) << 5) + date) * (24 * 60 * 60);
}
ICAL_Date.prototype.isComplete = function () { return true; };
ICAL_Date.prototype.toStringRepresentation_ = function () {
  return ICAL_fmtInt(this.year, 4) + ICAL_fmtInt(this.month, 2) +
    ICAL_fmtInt(this.date, 2);
};
ICAL_Date.prototype.equals = function (that) {
  return this.constructor === that.constructor && // same class
    this.date === that.date &&
    this.month === that.month &&
    this.year === that.year;
};

/**
 * cache_ is a cache of ICAL_Date objects. The keys are comparables of
 * ICAL_Date objects and the values are the corresponding ICAL_Date objects.
 */
ICAL_Date.cache_ = {};
ICAL_Date.cacheSize_ = 0;
ICAL_Date.MAX_CACHE_SIZE_ = 200;

/**
 * Factory method for creating ICAL_Date objects.
 * @return {ICAL_Date}
 */
ICAL_Date.create = function(year, month, date) {
  var cmp = ICAL_CalculateDateComparable_(year, month, date);
  if (cmp in ICAL_Date.cache_) {
    return ICAL_Date.cache_[cmp];
  } else {
    var dt = new ICAL_Date(year, month, date);
    dt.cmp_ = cmp;
    if (ICAL_Date.cacheSize_ < ICAL_Date.MAX_CACHE_SIZE_) {
      ICAL_Date.cache_[cmp] = dt;
    }
    return dt;
  }
}

/** an icalendar date-time.  @constructor */
function ICAL_DateTime(yr, mo, da, hr, mi, se) {
  ICAL_Temporal.call(this, yr, mo, da, hr, mi, se);
}
ICAL_DateTime.prototype = new ICAL_DateValue();
ICAL_DateTime.prototype.constructor = ICAL_DateTime;
ICAL_DateTime.now = function () {
  var d = new Date();
  return new ICAL_DateTime(d.getFullYear(), d.getMonth() + 1, d.getDate(),
                           d.getHours(), d.getMinutes(), d.getSeconds());
};
ICAL_DateTime.prototype.type = /* SAFE */ 'DateTime';
ICAL_DateTime.prototype.toDate = function () {
  return ICAL_Date.create(this.year, this.month, this.date);
};
ICAL_DateTime.prototype.toDateTime = function () { return this; };
ICAL_DateTime.prototype.toTime = function () {
  return new ICAL_Time(this.hour, this.minute, this.second);
};
ICAL_DateTime.prototype.getComparable = function () {
  if (undefined === this.cmp_) {
    this.cmp_ =
      (((((((((((this.year - 1970) * 12) + this.month) << 5) +
             this.date) * 24) + this.hour) * 60) + this.minute) * 60) +
       this.second);
  }
  return this.cmp_;
};
ICAL_DateTime.prototype.isComplete = function () { return true; };
ICAL_DateTime.prototype.toStringRepresentation_ = function () {
  return ICAL_fmtInt(this.year, 4) + ICAL_fmtInt(this.month, 2) +
    ICAL_fmtInt(this.date, 2) + 'T' + ICAL_fmtInt(this.hour, 2) +
    ICAL_fmtInt(this.minute, 2) + ICAL_fmtInt(this.second, 2);
};
ICAL_DateTime.prototype.equals = function (that) {
  return this.constructor === that.constructor && // same class
    this.date === that.date &&
    this.month === that.month &&
    this.year === that.year &&
    this.hour === that.hour &&
    this.minute === that.minute &&
    this.second === that.second;
};
ICAL_DateTime.prototype.clone = function() {
  var dt = new ICAL_DateTime(this.year, this.month, this.date,
                             this.hour, this.minute, this.second);
  if (this.str_ !== undefined) dt.str_ = this.str_;
  return dt;
}

/** an icalendar time.  @constructor */
function ICAL_Time(hr, mi, se) {
  ICAL_Temporal.call(this, NaN, NaN, NaN, hr, mi, se);
}
ICAL_Time.prototype = new ICAL_Temporal();
ICAL_Time.prototype.constructor = ICAL_Time;
ICAL_Time.prototype.type = /* SAFE */ 'Time';
ICAL_Time.prototype.toTime = function () { return this; };
ICAL_Time.prototype.toStringRepresentation_ = function () {
  return "T" + ICAL_fmtInt(this.hour, 2) +
    ICAL_fmtInt(this.minute, 2) + ICAL_fmtInt(this.second, 2);
};
ICAL_Time.prototype.equals = function (that) {
  return this.constructor === that.constructor && // same class
    this.hour === that.hour &&
    this.minute === that.minute &&
    this.second === that.second;
};
ICAL_Time.prototype.getComparable = function () {
  return ((this.hour) * 60 + this.minute) * 60 + this.second;
};

/** an icalendar duration.  @constructor */
function ICAL_Duration(da, hr, mi, se) {
  // normalize to make sure all non zero fields have the same sign and are
  // in the expected ranges

  var totSecs = se + (60 * (mi + 60 * (hr + 24 * da)));
  var normDays = ICAL_trunc(totSecs / (24 * 60 * 60));
  totSecs -= normDays * (24 * 60 * 60);
  var normHours = ICAL_trunc(totSecs / (60 * 60));
  totSecs -= normHours * (60 * 60);
  var normMinutes = ICAL_trunc(totSecs / 60);
  totSecs -= normMinutes * 60;
  var normSecs = ICAL_trunc(totSecs);

  ICAL_Temporal.call(this, NaN, NaN,
                     normDays, normHours, normMinutes, normSecs);
}
ICAL_Duration.prototype = new ICAL_Temporal();
ICAL_Duration.prototype.constructor = ICAL_Duration;
ICAL_Duration.prototype.type = /* SAFE */ 'Duration';
ICAL_Duration.prototype.toDays = function () {
  return this.date;
};
ICAL_Duration.prototype.toHours = function () {
  return this.date * 24 + this.hour;
};
ICAL_Duration.prototype.toMinutes = function () {
  return 24 * 60 * this.date + this.hour * 60 + this.minute;
};
ICAL_Duration.prototype.toSeconds = function () {
  return this.second + this.minute * 60 + this.hour * 3600 +
    3600 * 24 * this.date;
};
ICAL_Duration.prototype.getComparable = function () {
  if (undefined === this.cmp_) {
    this.cmp_ = ((this.date * 24 + this.hour) * 60 + this.minute) * 60 +
                this.second;
  }
  return this.cmp_;
};
/** string form of a duration.  This is a bit different from the RFC2445 format
  * since icalendar durations don't allow month or year diffs.
  * Well formed durations shouldn't have these either, but I want to allow them
  * in intermediate form.
  * <br>The general form of a duration is
  * <tt>[+-]?P(\d+Y)?(\d+N)?(\d+W)?(\d+D)?(T(\d+H)?(\d+M)?(\d+S)?)?</tt>.
  * <br>where Y = year, N = month, W = week, D = day, H = hour, M = minute,
  * S = second.
  */
ICAL_Duration.prototype.toStringRepresentation_ = function () {
  var sign = this.year ? ICAL_sign(this.year) :
             this.month ? ICAL_sign(this.month) :
             this.date ? ICAL_sign(this.date) :
             this.hour ? ICAL_sign(this.hour) :
             this.minute ? ICAL_sign(this.minute) :
             this.second ? ICAL_sign(this.second) : 0;

  var s = sign < 0 ? '-P' : 'P';
  // duration should not have year or month fields.  include these so bugs are
  // obvious
  if (this.year) { s += (sign * this.year) + 'Y'; }
  if (this.month) { s += (sign * this.month) + 'N'; }
  // duration is specified as a number of weeks, hours, minutes, and seconds
  if (this.date) {
    s += (this.date % 7) ?
         (sign * this.date) + 'D' :
         ((sign * this.date) / 7) + 'W';
  }
  if (this.hour || this.minute || this.second) { s += 'T'; }
  if (this.hour) { s += (sign * this.hour) + 'H'; }
  if (this.minute) { s += (sign * this.minute) + 'M'; }
  if (this.second) { s += (sign * this.second) + 'S'; }

  if (!sign) { s += '0D'; }
  return s;
};
ICAL_Duration.prototype.equals = function (that) {
  return this.constructor === that.constructor && // same class
    this.date === that.date &&
    this.hour === that.hour &&
    this.minute === that.minute &&
    this.second === that.second;
};


/** a mutable temporal.  @constructor */
function ical_createBuilder() {
  return new ICAL_DTBuilder();
}

function ical_builderCopy(/*!ICAL_Temporal*/ t) {
  AssertType(t, ICAL_Temporal);
  var b = new ICAL_DTBuilder();
  b.year = t.year || 0;
  b.month = t.month || 0;
  b.date = t.date || 0;
  b.hour = t.hour || 0;
  b.minute = t.minute || 0;
  b.second = t.second || 0;
  return b;
}

/**
 * a builder that contains a copy of the given, possibly partial, date value.
 */
function ical_partialBuilderCopy(/*!ICAL_DateValue*/ t) {
  AssertType(t, ICAL_DateValue);
  var b = new ICAL_DTBuilder();
  b.year = t.year;
  b.month = t.month;
  b.date = t.date;
  b.hour = t.hour;
  b.minute = t.minute;
  b.second = t.second;
  return b;
}

function ical_dateBuilder(year, month, date) {
  AssertTrue(!(isNaN(year) | isNaN(month) | isNaN(date)));
  var b = new ICAL_DTBuilder();
  b.year = year || 0;
  b.month = month || 0;
  b.date = date || 0;
  return b;
}

function ical_dateTimeBuilder(year, month, date, hour, minute, second) {
  AssertTrue(!(isNaN(year) | isNaN(month) | isNaN(date) |
	       isNaN(hour) | isNaN(minute) | isNaN(second)));
  var b = new ICAL_DTBuilder();
  b.year = year || 0;
  b.month = month || 0;
  b.date = date || 0;
  b.hour = hour || 0;
  b.minute = minute || 0;
  b.second = second || 0;
  return b;
}

function ICAL_DTBuilder() {
}
ICAL_DTBuilder.prototype = new ICAL_Temporal();
ICAL_DTBuilder.prototype.constructor = ICAL_DTBuilder;
ICAL_DTBuilder.prototype.type = /* SAFE */ 'DTBuilder';
ICAL_DTBuilder.prototype.year =
  ICAL_DTBuilder.prototype.month =
  ICAL_DTBuilder.prototype.date =
  ICAL_DTBuilder.prototype.hour =
  ICAL_DTBuilder.prototype.minute =
  ICAL_DTBuilder.prototype.second = 0;
ICAL_DTBuilder.prototype.getComparable = function () {
  this.normalize();
  var result;
  if (isNaN(this.hour)) {
    result = ICAL_CalculateDateComparable_(this.year, this.month, this.date);
  } else {
    result = (((((((((((this.year - 1970) * 12) + this.month) << 5) +
                    this.date) * 24) + this.hour) * 60) + this.minute) * 60) +
              this.second);
  }
  return result;
};
ICAL_DTBuilder.prototype.advance = function (duration) {
  if (duration.date) { this.date += duration.date; }
  if (duration.hour) { this.hour += duration.hour; }
  if (duration.minute) { this.minute += duration.minute; }
  if (duration.second) { this.second += duration.second; }
};
ICAL_DTBuilder.prototype.normalize = function () {
  this.normalizeHMS_();
  this.normalizeMonth_();
  var n = ICAL_daysInMonth(this.year, this.month);
  while (this.date < 1) {
    this.month -= 1;
    this.normalizeMonth_();
    n = ICAL_daysInMonth(this.year, this.month);
    this.date += n;
  }
  while (this.date > n) {
    this.date -= n;
    this.month += 1;
    this.normalizeMonth_();
    n = ICAL_daysInMonth(this.year, this.month);
  }
};
/** normalize the hour, minute, and second fields.  @private */
ICAL_DTBuilder.prototype.normalizeHMS_ = function () {
  var n;
  if (this.second < 0) {
    n = Math.ceil(this.second / -60);
    this.second += 60 * n;
    this.minute -=n;
  } else if (this.second >= 60) {
    n = Math.floor(this.second / 60);
    this.second -= 60 * n;
    this.minute += n;
  }
  if (this.minute < 0) {
    n = Math.ceil(this.minute / -60);
    this.minute += 60 * n;
    this.hour -=n;
  } else if (this.minute >= 60) {
    n = Math.floor(this.minute / 60);
    this.minute -= 60 * n;
    this.hour += n;
  }
  if (this.hour < 0) {
    n = Math.ceil(this.hour / -24);
    this.hour += 24 * n;
    this.date -=n;
  } else if (this.hour >= 24) {
    n = Math.floor(this.hour / 24);
    this.hour -= 24 * n;
    this.date += n;
  }
};
/** normalize the month field.  @private */
ICAL_DTBuilder.prototype.normalizeMonth_ = function () {
  var n;
  if (this.month < 1) {
    n = Math.ceil((this.month - 1) / -12);
    this.month += 12 * n;
    this.year -= n;
  } else if (this.month > 12) {
    n = Math.floor((this.month - 1) / 12);
    this.month -= 12 * n;
    this.year += n;
  }
};
ICAL_DTBuilder.prototype.toDate = function () {
  this.normalize();
  return ICAL_Date.create(this.year, this.month, this.date);
};
ICAL_DTBuilder.prototype.toDateTime = function () {
  this.normalize();
  return new ICAL_DateTime(this.year, this.month, this.date,
                           this.hour, this.minute, this.second);
};
ICAL_DTBuilder.prototype.toPartialDate = function () {
  // comparisons to undefined return false so will not affect partial fields
  this.normalize();
  return new ICAL_PartialDate(isFinite(this.year) ? this.year : undefined,
                              isFinite(this.month) ? this.month : undefined,
                              isFinite(this.date) ? this.date : undefined);
};
ICAL_DTBuilder.prototype.toPartialDateTime = function () {
  // comparisons to undefined return false so will not affect partial fields
  this.normalize();
  return new ICAL_PartialDateTime(
      isFinite(this.year) ? this.year : undefined,
      isFinite(this.month) ? this.month : undefined,
      isFinite(this.date) ? this.date : undefined,
      isFinite(this.hour) ? this.hour : undefined,
      isFinite(this.minute) ? this.minute : undefined,
      isFinite(this.second) ? this.second : undefined);
};
ICAL_DTBuilder.prototype.toTime = function () {
  this.normalize();
  return new ICAL_Time(this.hour, this.minute, this.second);
};
ICAL_DTBuilder.prototype.toDuration = function () {
  if (this.year || this.month) {
    ICAL_exception(
        /* SAFE */ 'Can\'t convert months or years to ICAL_Duration');
    return undefined;
  } else {
    return new ICAL_Duration(this.date, this.hour, this.minute, this.second);
  }
};
/** true iff this date builder can be converted to a proper ICAL_Date
  * (post normalization).
  */
ICAL_DTBuilder.prototype.validateDate = function () {
  // 1 + (n % 1) will evaluate to 1 iff n is an integer.  It will be a fraction
  // greater than 1 for a non-integral n, and NaN for n in (NaN, Infinity)
  /* SAFE */
  return ('number' == typeof this.year && (1 + (this.year % 1)) === 1 &&
          'number' == typeof this.month && (1 + (this.month % 1)) === 1 &&
          'number' == typeof this.date && (1 + (this.date % 1)) === 1);
};
/** true iff this date builder can be converted to a proper ICAL_DateTime
  * (post normalization).
  */
ICAL_DTBuilder.prototype.validateDateTime = function () {
  /* SAFE */
  return this.validateDate() && this.validateTime();
};
/** true iff this date builder can be converted to a proper ICAL_Time
  * (post normalization).
  */
ICAL_DTBuilder.prototype.validateTime = function () {
  /* SAFE */
  return ('number' == typeof this.hour && (1 + (this.hour % 1)) === 1 &&
          'number' == typeof this.minute && (1 + (this.minute % 1)) === 1 &&
          'number' == typeof this.second && (1 + (this.second % 1)) === 1);
};
ICAL_DTBuilder.prototype.toString = function () {
  return (
      '[' + (undefined !== this.year ? ICAL_fmtInt(this.year, 4) : '????') +
      '/' + (undefined !== this.month ? ICAL_fmtInt(this.month, 2) : '??') +
      '/' + (undefined !== this.date ? ICAL_fmtInt(this.date, 2) : '??') +
      ' ' + (undefined !== this.hour ? ICAL_fmtInt(this.hour, 2) : '??') +
      ' ' + (undefined !== this.minute ? ICAL_fmtInt(this.minute, 2) : '??') +
      ' ' + (undefined !== this.second ? ICAL_fmtInt(this.second, 2) : '??') +
      ']');
};
ICAL_DTBuilder.prototype.equals = function (that) {
  return this.constructor === that.constructor && // same class
    this.date === that.date &&
    this.month === that.month &&
    this.year === that.year &&
    this.hour === that.hour &&
    this.minute === that.minute &&
    this.second === that.second;
};

/** an icalendar period-of-time.  @constructor. */
function ICAL_PeriodOfTime(t1, t2) {
  AssertTrue(t1 instanceof ICAL_DateTime || t1 instanceof ICAL_Date);
  this.start = t1;
  if (t2.constructor == ICAL_Duration) {
    var b = ical_builderCopy(t1);
    b.advance(t2);
    this.end =
      this.start instanceof ICAL_DateTime ? b.toDateTime() : b.toDate();
  } else {
    AssertEquals(t2.constructor, t1.constructor);
    this.end = t2;
  }
  this.duration = ICAL_DurationBetween(this.end, this.start);
}
ICAL_PeriodOfTime.prototype.type = /* SAFE */ 'PeriodOfTime';
ICAL_PeriodOfTime.prototype.toString = function () {
  if (this.str_ !== undefined) return this.str_;
  this.str_ = this.start + '/' + this.end;
  return this.str_;
};
ICAL_PeriodOfTime.prototype.equals = function (that) {
  return this.constructor === that.constructor && // same class
    this.start.equals(that.start) && this.end.equals(that.end);
};
ICAL_PeriodOfTime.prototype.overlaps = function (that) {
  // Two regions A and B don't overlap if A ends before B starts or
  // A starts after B ends.  Since ends are exclusive we use >= and <=:
  //   overlap = !(A.end <= B.start || A.start >= B.end)
  // By DeMorgan's:
  //   overlap = (A.end > B.start) && (A.start < B.end)
  return that.end.getComparable() > this.start.getComparable() &&
         that.start.getComparable() < this.end.getComparable();
};
ICAL_PeriodOfTime.prototype.overlapsDateTimes = function (start, end) {
  return end.getComparable() > this.start.getComparable() &&
         start.getComparable() < this.end.getComparable();
};
ICAL_PeriodOfTime.prototype.contains = function (that) {
  return this.start.getComparable() <= that.start.getComparable() &&
         this.end.getComparable() >= that.end.getComparable();
};


/** an icalendar patial period-of-time.  @constructor. */
function ICAL_PartialPeriodOfTime(t1, t2) {
  AssertTrue(t1 instanceof ICAL_PartialDateTime ||
             t1 instanceof ICAL_PartialDate);
  this.start = t1;

  AssertEquals(t2.constructor, t1.constructor);
  this.end = t2;

  // TODO(davem) - make a special partial duration function. In a partial
  // period. Extracting the duration might legitimately fail
  try {
    this.duration = ICAL_DurationBetween(this.end, this.start);
  } catch (e) {
    // Cleanup here
    this.duration = null;
  }
}
ICAL_PartialPeriodOfTime.prototype.type = /* SAFE */ 'PartialPeriodOfTime';
ICAL_PartialPeriodOfTime.prototype.toStringRepresentation_ = function () {
  return this.start + '/' + this.end;
};
ICAL_PartialPeriodOfTime.prototype.equals = function (that) {
  return this.constructor === that.constructor && // same class
    this.start.equals(that.start) && this.end.equals(that.end);
};


// Date operations
function ICAL_DurationBetween(t1, t2) {
  if (isNaN(t1.year) != isNaN(t2.year) ||
      isNaN(t1.hour) != isNaN(t2.hour)) {
    ICAL_exception(/* SAFE */ 'diff(' + t1 + ', ' + t2 + ')');
    return undefined;
  }

  var b = ical_builderCopy(t1);
  if (isNaN(t1.year)) {
    b.hour -= t2.hour;
    b.minute -= t2.minute;
    b.second -= t2.second;
  } else {
    b.year = NaN;
    b.month = NaN;
    b.date = ICAL_daysBetween(t1.year, t1.month, t1.date,
                              t2.year, t2.month, t2.date);
    if (!isNaN(t1.hour)) {
      b.hour -= t2.hour;
      b.minute -= t2.minute;
      b.second -= t2.second;
    }
  }
  return b.toDuration();
}

function ICAL_PartialDate(yr, mo, da) {
  this.year = yr;
  this.month = mo;
  this.date = da;
}
ICAL_PartialDate.prototype = new ICAL_DateValue();
ICAL_PartialDate.prototype.constructor = ICAL_PartialDate;
ICAL_PartialDate.prototype.type = 'PartialDate';
ICAL_PartialDate.prototype.toDate = function () {
  return ICAL_Date.create(this.year || 0, this.month || 1, this.date || 1);
};
ICAL_PartialDate.prototype.toDateTime = function () {
  return new ICAL_DateTime(
      this.year || 0, this.month || 1, this.date || 1, 0, 0, 0);
};
ICAL_PartialDate.prototype.toPartialDate = function () { return this; };
ICAL_PartialDate.prototype.toPartialDateTime = function () {
  return new ICAL_PartialDateTime(this.year, this.month, this.date, 0, 0, 0);
};
ICAL_PartialDate.prototype.isComplete = function () {
  return !isNaN(this.getComparable());
};
ICAL_PartialDate.prototype.getComparable = function () {
  if (undefined === this.cmp_) {
    this.cmp_ = (((((this.year - 1970) * 12) + this.month) << 5) + this.date) *
                (24 * 60 * 60);
  }
  return this.cmp_;
};
ICAL_PartialDate.prototype.equals = function (that) {
  return this.constructor === that.constructor && // same class
    (this.date === that.date || (isNaN(this.date) && isNaN(that.date))) &&
    (this.month === that.month || (isNaN(this.month) && isNaN(that.month))) &&
    (this.year === that.year || (isNaN(this.year) && isNaN(that.year)));
};
ICAL_PartialDate.prototype.toStringRepresentation_ = function () {
  return (undefined !== this.year ? ICAL_fmtInt(this.year, 4) : '????') +
         (undefined !== this.month ? ICAL_fmtInt(this.month, 2) : '??') +
         (undefined !== this.date ? ICAL_fmtInt(this.date, 2) : '??');
};

function ICAL_PartialDateTime(yr, mo, da, hr, mi, se) {
  this.year = yr;
  this.month = mo;
  this.date = da;
  this.hour = hr;
  this.minute = mi;
  this.second = se;
}
ICAL_PartialDateTime.prototype = new ICAL_DateValue();
ICAL_PartialDateTime.prototype.constructor = ICAL_PartialDateTime;
ICAL_PartialDateTime.prototype.type = 'PartialDateTime';
ICAL_PartialDateTime.prototype.toDate = function () {
  return ICAL_Date.create(this.year || 0, this.month || 1, this.date || 1);
};
ICAL_PartialDateTime.prototype.toDateTime = function () {
  return new ICAL_DateTime(
      this.year || 0, this.month || 1, this.date || 1,
      this.hour || 0, this.minute || 0, this.second || 0);
};
ICAL_PartialDateTime.prototype.toPartialDate = function () {
  return new ICAL_PartialDate(this.year, this.month, this.date);
};
ICAL_PartialDateTime.prototype.toPartialDateTime = function () { return this; };
ICAL_PartialDateTime.prototype.isComplete = function () {
  return !isNaN(this.getComparable());
};
ICAL_PartialDateTime.prototype.getComparable = function () {
  if (undefined === this.cmp_) {
    this.cmp_ =
      (((((((((((this.year - 1970) * 12) + this.month) << 5) +
             this.date) * 24) + this.hour) * 60) + this.minute) * 60) +
       this.second);
  }
  return this.cmp_;
};
ICAL_PartialDateTime.prototype.equals = function (that) {
  return this.constructor === that.constructor && // same class
    (this.date === that.date || (isNaN(this.date) && isNaN(that.date))) &&
    (this.month === that.month || (isNaN(this.month) && isNaN(that.month))) &&
    (this.year === that.year || (isNaN(this.year) && isNaN(that.year))) &&
    (this.hour === that.hour || (isNaN(this.hour) && isNaN(that.hour))) &&
    (this.minute === that.minute ||
     (isNaN(this.minute) && isNaN(that.minute))) &&
    (this.second === that.second || (isNaN(this.second) && isNaN(that.second)));
};
ICAL_PartialDateTime.prototype.toStringRepresentation_ = function () {
  return (undefined !== this.year ? ICAL_fmtInt(this.year, 4) : '????') +
         (undefined !== this.month ? ICAL_fmtInt(this.month, 2) : '??') +
         (undefined !== this.date ? ICAL_fmtInt(this.date, 2) : '??') +
         'T' +
         (undefined !== this.hour ? ICAL_fmtInt(this.hour, 2) : '??') +
         (undefined !== this.minute ? ICAL_fmtInt(this.minute, 2) : '??') +
         (undefined !== this.second ? ICAL_fmtInt(this.second, 2) : '??');
};


// Factory
/** parses ICalendar data values.  The types parsed include
  * <ul>
  * <li><a href="http://www.corp.google.com/~msamuel/rfc2445.html#4.3.4"
  *  >Date</a></li>
  * <li><a href="http://www.corp.google.com/~msamuel/rfc2445.html#4.3.12"
  *  >Time</a></li>
  * <li><a href="http://www.corp.google.com/~msamuel/rfc2445.html#4.3.5"
  *  >DateTime</a></li>
  * <li><a href="http://www.corp.google.com/~msamuel/rfc2445.html#4.3.6"
  *  >Duration</a></li>
  * <li><a href="http://www.corp.google.com/~msamuel/rfc2445.html#4.3.9"
  *  >Period of Time</a></li>
  * </ul>
  */
function ICAL_Parse(icalString) {
  var slash = icalString.indexOf('/');
  var n = icalString.length;
  if (slash >= 0) {
    return new ICAL_PeriodOfTime(
      ICAL_Parse(icalString.substring(0, slash)),
      ICAL_Parse(icalString.substring(slash + 1, n)));
  } else {
    var sign = 1;
    var pos = 0;
    switch (icalString.charAt(0)) {
      case 'T': // Time 'T'HHMMSS
        AssertEquals(n, 7);
        return ical_dateTimeBuilder(0, 0, 0,
                                    ICAL_toInt_(icalString, 1, 3),
                                    ICAL_toInt_(icalString, 3, 5),
                                    ICAL_toInt_(icalString, 5, 7)).toTime();
      case 'P': // Duration 'P'(\d+[WD])+('T'(\d+[HMS])+)?
        return ICAL_ParseDuration_(icalString.substring(1, n), 1);
      case '-':
        sign = -1;
        // fallthru
      case '+':
        pos = 1;
        if ('P' == icalString.charAt(1)) {
          return ICAL_ParseDuration_(icalString.substring(2, n), sign);
        }
        // fallthru
      default:
        var t = icalString.indexOf('T');
        if (t === -1) { // Date YYYYMMDD
          AssertTrue(pos < n-4, /* SAFE */ "Invalid date string");
          return ical_dateBuilder(ICAL_toInt_(icalString, pos, n - 4),
                                  ICAL_toInt_(icalString, n - 4, n - 2),
                                  ICAL_toInt_(icalString, n - 2, n)).toDate();
        }
        // DateTime YYYYMMDD'T'HHMMSS
        AssertEquals(n, t + 7);
        return ical_dateTimeBuilder(
            ICAL_toInt_(icalString, pos, t - 4),
            ICAL_toInt_(icalString, t - 4, t - 2),
            ICAL_toInt_(icalString, t - 2, t),
            ICAL_toInt_(icalString, t + 1, t + 3),
            ICAL_toInt_(icalString, t + 3, t + 5),
            ICAL_toInt_(icalString, t + 5, t + 7)).toDateTime();
    }
  }
}

/** @private */
function ICAL_ParseDuration_(s, sign) {
  var n = s.length;
  var b = new ICAL_DTBuilder();
  for (var i = 0; i < n; i += 1) {
    var ni = 0;
    do {
      var ch = s.charAt(i);
      if (ch < '0' || ch > '9') { break; }
      ni += 1;
    } while ((i += 1) < n);
    if (ni === 0) {
      AssertEquals('T', s.charAt(i));
      continue;
    }
    var num = ICAL_toInt_(s, i - ni, i);
    switch(s.charAt(i)) {
      case 'W':
        b.date += sign * 7 * num;
        break;
      case 'D':
        b.date += sign * num;
        break;
      case 'H':
        b.hour += sign * num;
        break;
      case 'M':
        b.minute += sign * num;
        break;
      case 'S':
        b.second += sign * num;
        break;
      default:
        ICAL_exception(/* SAFE */ 'Bad Duration ' + s);
        return undefined;
    }
  }
  return b.toDuration();
}

var FOUR_OR_MORE_OPT_DIGITS_ = '(?:([0-9]{4,})|\\?{4})';
var TWO_OPT_DIGITS_ = '(?:([0-9]{2})|\\?{2})';
/**
 * the below regexps match date time formats like
 * '2005??01T1230??' where the ??? are unknown parts of an ical date/date-time.
 * The digit groups either match a number of digits, or ??.  If the ?? is
 * matched then the corresponding parenthetical group will have value undefined
 * or null.
 */
var PARTIAL_DATE_RE_ = new RegExp(
    '^' + /*year*/ FOUR_OR_MORE_OPT_DIGITS_ + /*month*/TWO_OPT_DIGITS_ +
    /*date*/TWO_OPT_DIGITS_ + '$');
var PARTIAL_DATE_TIME_RE_ = new RegExp(
    '^' + /*year*/ FOUR_OR_MORE_OPT_DIGITS_ + /*month*/TWO_OPT_DIGITS_ +
    /*date*/TWO_OPT_DIGITS_ + 'T' + /*hour*/TWO_OPT_DIGITS_ +
    /*minute*/TWO_OPT_DIGITS_ + /*second*/TWO_OPT_DIGITS_ + '$');

function ICAL_ParsePartial(icalStr) {

  // If there is a slash present, then split it and recurse on both halves
  var slash = icalStr.indexOf('/');
  var n = icalStr.length;
  if (slash >= 0) {
    return new ICAL_PartialPeriodOfTime(
      ICAL_ParsePartial(icalStr.substring(0, slash)),
      ICAL_ParsePartial(icalStr.substring(slash + 1, n)));
  }

  // No slash, parse it in the normal way
  var m = icalStr.match(PARTIAL_DATE_TIME_RE_);
  if (!m) {
    m = icalStr.match(PARTIAL_DATE_RE_);
    if (!m) {
      /* SAFE */
      ICAL_exception(/* SAFE */ "Failed to parse partial date " + icalStr);
    }
  }
  // the blank parenthetical groups will have undefined on firefox and null on
  // IE, so make sure that they're undefined.
  for (var i = m.length; --i >= 1;) {
    if (!m[i]) {  // matched ??
      m[i] = undefined;
    } else {  // parse a run of digits
      m[i] = parseInt(m[i], 10);
    }
  }
  if (7 == m.length) {  // the date-time pattern was matched
    return new ICAL_PartialDateTime(m[1], m[2], m[3], m[4], m[5], m[6]);
  } else {  // the date pattern was matched
    return new ICAL_PartialDate(m[1], m[2], m[3]);
  }
}


// some useful values

/** the ICAL_Date instance corresponding to "today" local time.
  * Updated around midnight.
  */
var ICAL_todaysDate = undefined;

/** Functions to invoke when the date changes */
var ICAL_todayUpdateListeners = [];

/** the day of the year in [0-365] of the given date. */
function ICAL_dayOfYear(year, month, date) {
  var leapAdjust = month > 2 && 29 === ICAL_daysInMonth(year, 2);
  return ICAL_dayOfYear.MONTH_START_TO_DOY_[month] + leapAdjust + date - 1;
}
ICAL_dayOfYear.MONTH_START_TO_DOY_ = [
    undefined, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 ];

/**
 * Called at setup and every midnight to update {@link #ICAL_todaysDate}.
 * To listen for changes to the date, see AddTodayUpdateListener()
 *
 * @private
 */
function updateTodaysDate_() {
  var now = new Date();
  var oldDate = ICAL_todaysDate;
  ICAL_todaysDate =
    ICAL_Date.create(now.getFullYear(), now.getMonth() + 1, now.getDate());
  if (oldDate && !(oldDate.equals(ICAL_todaysDate))) {
    for (var i = 0; i < ICAL_todayUpdateListeners.length; ++i) {
      var func = ICAL_todayUpdateListeners[i];
      try {
        func(ICAL_todaysDate);
      } catch (e) {
        // ignore failures so arbitrary function provided by client
        // does not prevent the changed-date event from being fired
        // for the other listeners
      }
    }
  }
  var tomorrowMidnight =
    new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0);
  tomorrowMidnight.setDate(tomorrowMidnight.getDate() + 1);
  var diffMilli = tomorrowMidnight.getTime() - now.getTime();

  // Try again in 30 minutes [there's an odd bug in IE where occasionally it
  // spins if you call setTimeout with a value that too big]
  if (diffMilli < 0 || diffMilli >= 30 * 60 * 1000) {
    diffMilli = 30 * 60 * 1000;
  }
  window.setTimeout(updateTodaysDate_, diffMilli);
}
updateTodaysDate_();

/**
 * The provided function will be invoked if the current date is updated.
 * (For applications such as Caribou and Doozer that users leave open for
 * days, it is likely that the date will change over the lifetime of the
 * application.)
 *
 * When invoked, the function will take 1 argument, the new (current) date,
 * and any return value will be ignored.
 *
 * @param func {Function}
 */
function AddTodayUpdateListener(/*Function*/ func) {
  AssertType(func, Function,
      'func passed to AddTodayUpdateListener is not a Function');
  ICAL_todayUpdateListeners.push(func);
}

var listen;
var unlisten;
var unlistenByKey;

(function() {
  var listeners = {};
  var nextId = 0;

  function getHashCode_(obj) {
    if (obj.listen_hc_ == null) {
      obj.listen_hc_ = ++nextId;
    }
    return obj.listen_hc_;
  }
  
  /**
   * Takes a node, event, listener, and capture flag to create a key
   * to identify the tuple in the listeners hash.
   *
   * @param node {Element} The node to listen to events on
   * @param event {String} The name of the event without the "on" prefix
   * @param listener {Function} A function to call when the event occurs
   * @param opt_useCapture {Boolean} In DOM-compliant browsers, this determines
   *                                 whether the listener is fired during the
   *                                 capture or bubble phase of the event.
   * @return {String} key to identify this tuple in the listeners hash
   */
  function createKey_(node, event, listener, opt_useCapture) {
    var nodeHc = getHashCode_(node);
    var listenerHc = getHashCode_(listener);
    opt_useCapture = !!opt_useCapture;
    var key = nodeHc + "_" + event + "_" + listenerHc + "_" + opt_useCapture;
    return key;
  }

  /**
   * Adds an event listener to a DOM node for a specific event.
   *
   * Listen() and unlisten() use an indirect lookup of listener functions
   * to avoid circular references between DOM (in IE) or XPCOM (in Mozilla)
   * objects which leak memory. This makes it easier to write OO
   * Javascript/DOM code.
   *
   * Examples:
   * listen(myButton, "click", myHandler, true);
   * listen(myButton, "click", this.myHandler.bind(this), true);
   *
   * @param node {Element} The node to listen to events on
   * @param event {String} The name of the event without the "on" prefix
   * @param listener {Function} A function to call when the event occurs
   * @param opt_useCapture {Boolean} In DOM-compliant browsers, this determines
   *                                 whether the listener is fired during the
   *                                 capture or bubble phase of the event.
   * @return {String} a unique key to indentify this listener
   */
  listen = function(node, event, listener, opt_useCapture) {
    var key = createKey_(node, event, listener, opt_useCapture);

    // addEventListener does not allow multiple listeners
    if (key in listeners) {
      return key;
    }

    var proxy = handleEvent.bind(null, key);
    listeners[key] = {
      listener: listener,
      proxy: proxy,
      event: event,
      node: node,
      useCapture: opt_useCapture
    };

    if (node.addEventListener) {
      node.addEventListener(event, proxy, opt_useCapture);
    } else if (node.attachEvent) {
      node.attachEvent("on" + event, proxy);
    } else {
      throw new Error("Node {" + node + "} does not support event listeners.");
    }

    return key;
  }

  /**
   * Removes an event listener which was added with listen().
   *
   * @param node {Element} The node to stop listening to events on
   * @param event {String} The name of the event without the "on" prefix
   * @param listener {Function} The listener function to remove
   * @param opt_useCapture {Boolean} In DOM-compliant browsers, this determines
   *                                 whether the listener is fired during the
   *                                 capture or bubble phase of the event.
   * @return {Boolean} indicating whether the listener was there to remove
   */
  unlisten = function(node, event, listener, opt_useCapture) {
    var key = createKey_(node, event, listener, opt_useCapture);

    return unlistenByKey(key);
  }
  
  /**
   * Variant of {@link unlisten} that takes a key that was returned by 
   * {@link listen} and removes that listener.
   */
  unlistenByKey = function(key) {
    if (!(key in listeners)) {
      return false;
    }
    var listener = listeners[key];
    var proxy = listener.proxy;
    var event = listener.event;
    var node = listener.node;
    var useCapture = listener.useCapture;

    if (node.removeEventListener) {
      node.removeEventListener(event, proxy, useCapture);
    } else if (node.detachEvent) {
      node.detachEvent("on" + event, proxy);
    }

    delete listeners[key];
    return true;  
  }

  /**
   * The function which is actually called when the DOM event occurs. This
   * function is a proxy for the real listener the user specified.
   */
  function handleEvent(key) {
    // pass all arguments which were sent to this function except listenerID
    // on to the actual listener.
    var args = Array.prototype.splice.call(arguments, 1, arguments.length);
    return listeners[key].listener.apply(null, args);
  }

})();

    var defaultDateFormat = "MMM dd, yyyy";
    function setDefaultDateFormat(format) {
      defaultDateFormat = format;
    }
    
    function getDefaultDateFormat() {
      return defaultDateFormat;
    }

    function LDP_PrefersMonthBeforeDate() {
      var index = 0;
      var dateFormat = getDefaultDateFormat();
      while (index < dateFormat.length) {
        if (dateFormat.charAt(index) == 'M') {
          return true;
        }
        else if (dateFormat.charAt(index) == 'd') {
          return false;
        }
        index++;
      }
      return true;
    }

// loose date parser routines

//// PUBLIC API
// funcion LDP_Prefers24HourTime() should be defined to report whether the
//   user prefers 24 hour time.  If it returns true, then the time parsing will
//   not infer AM/PM ness as agressively.
// function LDP_PrefersMonthBeforeDate() should be defined as well to report
//   whether the user would expect 1/2 to be parsed as Jan 2 insted of Feb 1.
// LDP_ParseInputDate -- parse a date from human entered text
// LDP_ParseInputTime -- parse a time from human entered text
// LDP_ParsedDate -- the output of a parsing operation


/**
 * given a human entered string returns an LDP_ParsedDate which has the
 * following fields
 *   date - an ICAL_Date
 *   confidence - a confidence.  zero is most confident.  -2 or worse means low
 *                confidence.
 *   specified - a bitfield of the fields that were specified.
 *     specified & 1 -> year was specified
 *     specified & 2 -> month was specified
 *     specified & 4 -> day of month was specified
 *
 * This does inference assuming a forward tense bias, but the specified bitfield
 * can be used to do alternative inference.
 *
 * @return null if nothing could be reliably inferred.
 */
function LDP_ParseInputDate(/*!String*/ dateString) {
  var YEAR = 1;
  var MONTH = 2;
  var DAY = 4;
  /** day or month */
  var DAY_OR_MONTH = DAY | MONTH;
  /** is the token a name such as "February" instead of a number "2" */
  var IS_NAME = 8;

  dateString = dateString
    // remove leading and trailing whitespace
    .replace(/^\s+/, '').replace(/\s+$/, '')
    // make sure there is space between numbers and letters so that 1Jan2005
    // becomes 1 Jan 2005 -- 3 tokens
    .replace(/([0-9]+)([a-zA-Z]+)/g, '$1 $2')
    .replace(/([a-zA-Z])([0-9])/g, '$1 $2');
  // split into tokens on punctuation and space, leaving the delimiters in there
  var parts = dateString.split(/\b|_/);
  // the textual length of each part
  var partLen = [];
  // for each part, the mask of fields (YEAR, MONTH, DAY) that it could satisfy
  var candidates = [];

  // classify parts as possibly being a particular part
  for (var i = 0; i < parts.length; ++i) {
    parts[i] = parts[i].replace(/^\s+|\s+$/, '');
    partLen[i] = parts[i].length;
    candidates[i] = 0;

    // a number in 1-12 can be a month
    // a number in 1-31 can be a day
    // any number could be a year (but "0" is probably not), but we only mark
    // as candidate years, those which can't be a day or month since there's
    // a loop that looks at unused stuff to find a year later.

    if (parts[i].match(/^[0-9]+/)) {
      var n = parseInt(parts[i], 10);
      parts[i] = n;
      if (0 === n) {
        if (partLen[i] == 2) {
          candidates[i] |= YEAR;
        }
      } else if (n > 12 && n <= 31) {
        candidates[i] |= DAY;
      } else if (n <= 12) {
        candidates[i] |= DAY_OR_MONTH;
      } else if (n < 100 || n >= 1900) {
        candidates[i] |= YEAR;
      }
    } else {
      // if it's a word (since we split numbers from letters via regexp above)
      // check and see if it looks like a month name
      var n = LDP_ParseMonth_(parts[i]);
      if (n) {
        parts[i] = n[0];
        candidates[i] |= MONTH | IS_NAME;
      }
    }
  }

  // count number of candidates for date, month, year, or both month and date.
  var nDateCandidates = 0, nMonthCandidates = 0, nBothCandidates = 0,
      nYearCandidates = 0, nUnparsable = 0;
  for (var i = 0; i < candidates.length; ++i) {
    if (!candidates[i] && /\w/.test(parts[i])) { ++nUnparsable; }
    if (DAY_OR_MONTH === (candidates[i] & DAY_OR_MONTH)) {
      ++nBothCandidates;
    } else {
      if (candidates[i] & DAY) { ++nDateCandidates; }
      if (candidates[i] & MONTH) { ++nMonthCandidates; }
    }
    if (candidates[i] & YEAR) { ++nYearCandidates; }
  }

  // figure out what to do where we have numbers that could be either a date or
  // a month
  if (nBothCandidates) {
    var asMonth = false;
    var asDate = false;

    if (!nDateCandidates || !nMonthCandidates) {
      // we don't have both unambiguous day and month candidates, so
      // we have to divvy up the candidates between them
      if (nDateCandidates) {
        // we have an unambiguous date candidate, so treat the ambiguous as
        // months
        asMonth = true;
      } else if (nMonthCandidates) {
        // we have an unambiguous month candidate, so treat the ambiguous as
        // days
        asDate = true;
      } else if (1 === nBothCandidates) {
        // we have either a day or month but not both, so treat as month
        asMonth = true;
      } else {
        // we have to find both a day and a month, and each could be either so
        // look at user preferences to figure out which one is month.
        // If the user prefers mm/dd format treat the first one as the month.
        for (var i = 0; i < candidates.length; ++i) {

          // force month-before-day order for the format 2005-02-12
          var inISOFormat = false;
          if (candidates[0] === YEAR &&
              parts[1] && /^\s*-\s*$/.test(parts[1]) &&
              candidates[2] === DAY_OR_MONTH &&
              parts[3] && /^\s*-\s*$/.test(parts[3])) {
            inISOFormat = true;
          }

          if (DAY_OR_MONTH == (candidates[i] & DAY_OR_MONTH)) {
            if (inISOFormat || !LDP_PrefersMonthBeforeDate ||
                LDP_PrefersMonthBeforeDate()) {
              candidates[i] &= ~DAY;
              ++nMonthCandidates;
              --nBothCandidates;
              asDate = true;
            } else {
              candidates[i] &= ~MONTH;
              ++nDateCandidates;
              --nBothCandidates;
              asMonth = true;
            }
            break;
          }
        }
      }
    }

    // reclassify the ambiguous as either definitely days or months based on
    // the algorithm above
    if (asMonth) {
      for (var i = 0; i < candidates.length; ++i) {
        if (DAY_OR_MONTH == (candidates[i] & DAY_OR_MONTH)) {
          candidates[i] &= ~DAY;
          ++nMonthCandidates;
          --nBothCandidates;
        }
      }
    } else if (asDate) {
      for (var i = 0; i < candidates.length; ++i) {
        if (DAY_OR_MONTH == (candidates[i] & DAY_OR_MONTH)) {
          candidates[i] &= ~MONTH;
          ++nDateCandidates;
          --nBothCandidates;
        }
      }
    }
  }

  // start assigning parts to fields.
  // from here on out, we zero out the candidate as we use a field to make sure
  // it's not used later on.  We also decrement the candidate counts so that
  // our confidence is up to date.  The confidence is based on the number of
  // unused fields, and whether we were able to make an assignment to all
  // fields.
  var date;
  var month;
  var year;
  var confidence = 0;
  var specified = YEAR | MONTH | DAY;

  // pick the date
  if (nDateCandidates) {
    for (var i = 0; i < candidates.length; ++i) {
      if (candidates[i] & DAY) {
        date = parts[i];
        candidates[i] = 0;
        --nDateCandidates;
        break;
      }
    }
    AssertTrue(date);
  } else {
    date = null;
    specified &= ~DAY;
  }

  // pick the month
  if (nMonthCandidates) {
    var isLiteral = false;
    var i;
    for (i = 0; i < candidates.length; ++i) {
      if (candidates[i] & MONTH) {
        month = parts[i];
        candidates[i] &= ~MONTH;
        --nMonthCandidates;
        isLiteral = 0 !== (candidates[i] & IS_NAME);
        break;
      }
    }
    if (isLiteral) {
      // make sure we're using the longest literal match.
      // This allows us to match N to November when its the only thing present
      // but to also prefer November to a wayward "o" from "o'clock"
      var bestLength = partLen[i];
      for (var j = i + 1; j < candidates.length; ++j) {
        if (candidates[j] & IS_NAME) {
          if (partLen[j] > bestLength) {
            candidates[i] |= MONTH;
            candidates[j] &= ~MONTH;
            month = parts[j];
            bestLength = partLen[j];
            i = j;
          }
        }
      }
    }
  } else {
    month = null;
    specified &= ~MONTH;
  }

  // pick the year
  var hasGoodYear = !!nYearCandidates;
  if (hasGoodYear) {
    for (var i = 0; i < candidates.length; ++i) {
      if (candidates[i] & YEAR) {
        year = parts[i];
        candidates[i] = 0;
        --nYearCandidates;
        break;
      }
    }
  } else {
    year = null;
    if (nDateCandidates | nMonthCandidates | nBothCandidates) {
      // if we have any leftover numbers, and we need a year, pick the biggest
      // one
      var biggestIndex = -1;
      var biggest = 0;
      for (var i = 0; i < candidates.length; ++i) {
        if (candidates[i] && !(candidates[i] & IS_NAME)) {
          if (parts[i] > biggest) {
            biggestIndex = i;
            biggest = parts[i];
          }
        }
      }
      if (biggestIndex >= 0) {
        year = biggest;
        switch (candidates[biggestIndex]) {  // exludes those with IS_NAME set
          case DAY: --nDateCandidates; break;
          case MONTH: --nMonthCandidates; break;
          case DAY_OR_MONTH: --nBothCandidates; break;
        }
        candidates[biggestIndex] = 0;
      }
    }
    if (null == year) {
      // if we have to choose a year, choose the soonest one in the future given
      // month
      confidence -= 1;
      year = ICAL_todaysDate.year +
             ((month && month < ICAL_todaysDate.month) ? 1 : 0);
      specified &= ~YEAR;
    }
  }

  if (null == date) {
    // if we have to choose a date, choose 1
    date = 1;
    confidence -=.5;
  }

  if (null == month) {
    if (hasGoodYear) {
      month = 1;
    } else {
      // give up
      return null;
    }
  }

  if (year < 100) {
    year += year < 50 ? 2000 : 1900;
  }

  confidence -= nDateCandidates + nMonthCandidates + nYearCandidates +
                nBothCandidates + (nUnparsable >> 2);

  return new LDP_ParsedDate(
      ICAL_Date.create(year, month, date),
      confidence,
      specified);
}

/**
 * parses a time of day given a human entered string returning an ICAL_Time.
 *
 * @param timeString human entered string.
 * @param lowerBound if specified, an ICAL_Time that the result should be on
 *   or after.
 *
 * @return null if unable to parse.
 */
function LDP_ParseInputTime(/*!String*/ timeString, lowerBound) {

  // first make sure that there are two digits following a :
  var s = ' ' + timeString + ' ';
  s = s.replace(/\b([0-9]):/g, '0$1:');
  // now 1:5:30 should be 01:05:30, so we can grab groups of digits from the
  // front in pairs

  // get rid of seconds
  s = s.replace(/(:[0-9][0-9]):[0-9\.]*/, '$1');

  // get rid of fraction
  s = s.replace(/\.[0-9]+/g, '');

  // get rid of delimiters
  s = s.replace(/\W+/g, '') + ' ';


  // break out parts
  var m = s.match(/([0-9]+)([aph]?)/i);  // TODO(i18n): am/pm
  if (!m) { return null; }

  var ampm = null;

  switch (m[2].toLowerCase()) {
    case 'a': ampm = 0;    break;
    case 'p': ampm = 12;   break;
    case 'h': ampm = 24;   break;
    default:  ampm = null; break;
  }

  var hours;
  var minutes;

  var nums = m[1];
  switch (nums.length) {
    case 1: case 2:
      hours = parseInt(nums, 10);
      minutes = 0;
      break;
    case 3: case 5:
      hours = parseInt(nums.charAt(0), 10);
      minutes = parseInt(nums.substring(1, 3), 10);
      break;
    case 4: case 6:
      hours = parseInt(nums.substring(0, 2), 10);
      minutes = parseInt(nums.substring(2, 4), 10);
      break;
    default:  // decimal point should've been handled above
      return null;
  }

  if (hours > 24 || minutes > 60) { return null; }

  var usePm;
  if (ampm != null) {
    usePm = ampm === 12;
    if (0 === ampm && hours == 12) { hours = 0; }
  } else if (hours === 0 || hours > 12) {
    usePm = false;
  } else if (lowerBound &&
             (lowerBound.hour > hours ||
              lowerBound.hour === hours && lowerBound.minute > minutes)) {
    usePm = true;
  } else if (hours <= 6 &&
             !(LDP_Prefers24HourTime && LDP_Prefers24HourTime())) {
    usePm = true;
  }

  if (usePm) { hours = (hours % 12) + 12; }
  else if (24 === hours) { hours = 0; }

  return new ICAL_Time(hours, minutes, 0);
}

/** a record containing information about the result of date parsing. */
function LDP_ParsedDate(date, confidence, specified) {
  /** the ICAL_Date parsed. */
  this.date = date;
  /** 0 is highest.  -2 or lower is suspect. */
  this.confidence = confidence;
  /** a bitmask of the fields specified by the user. */
  this.specified = specified;
}
LDP_ParsedDate.prototype.toString = function () {
  return this.date.toString();
};

/** @private */
function LDP_ParseMonth_(/*!String*/ monthString) {
  var bestMatch = -1;
  var bestLength = -1;
  for (var i = 0; i < FULL_MONTHS.length; ++i) {
    var monthName = FULL_MONTHS[i];
    if (!monthName) { continue; }
    var monthAbbrev = MONTHS[i];

    var nMatching = Math.max(LDP_CommonPrefixLength_(monthName, monthString),
                             LDP_CommonPrefixLength_(monthAbbrev, monthString));
    if (nMatching && nMatching > bestLength) {
      bestMatch = i;
      bestLength = nMatching;
    }
  }
  // Match if most of monthString matches.  This allows for stems to differ,
  // allows for both abbreviation and full forms, but avoids matching "nobody"
  // against "november".
  // TODO(bolinfest): Might be better to use edit distance.
  return bestLength >= ((monthString.length + 1) >> 1)
    ? [bestMatch, bestLength] : null;
}

function LDP_CommonPrefixLength_(a, b) {
  a = a.toLowerCase();
  b = b.toLowerCase();
  var n = Math.min(a.length, b.length);
  var i;
  for (i = 0; i < n; ++i) {
    if (a.charAt(i) != b.charAt(i)) { break; }
  }
  return i;
}

/**
 * It is common to make a DIV temporarily visible to simulate
 * a popup window. Often, this is done by adding an onClick
 * handler to the element that can be clicked on to show the
 * popup.
 *
 * Unfortunately, closing the popup is not as simple.
 * The popup creator often wants to let the user close
 * the popup by clicking elsewhere on the window; however,
 * the popup only receives mouse events that occur
 * on the popup itself. Thus, popups need a mechanism
 * that notifies them that the user has clicked elsewhere
 * to try to get rid of them. 
 *
 * PopupController is such a mechanism --
 * it monitors all mousedown events that
 * occur in the window so that it can notify registered
 * popups of the mousedown, and the popups can choose
 * to deactivate themselves.
 *
 * For an object to qualify as a popup, it must have a
 * function called "deactivate" that takes a mousedown event
 * and returns a boolean indicating that it has deactivated
 * itself as a result of that event.
 *
 * EXAMPLE:
 *
 * // popup that attaches itself to the supplied div
 * function MyPopup(div) {
 *   this._div = div;
 *   this._isVisible = false;
 *   this._innerHTML = ...
 * }
 *
 * MyPopup.prototype.show = function() {
 *   this._div.display = '';
 *   this._isVisible = true;
 *   PC_addPopup(this);
 * }
 *
 * MyPopup.prototype.hide = function() {
 *   this._div.display = 'none';
 *   this._isVisible = false;
 * }
 *
 * MyPopup.prototype.deactivate = function(e) {
 *   if (this._isVisible) {
 *     var p = GetMousePosition(e);
 *     if (nodeBounds(this._div).contains(p)) {
 *       return false; // use clicked on popup, remain visible
 *     } else {
 *       this.hide();
 *       return true; // clicked outside popup, make invisible
 *     }
 *   } else {
 *     return true; // already deactivated, not visible
 *   }
 * }
 *
 * DEPENDENCIES:
 *   bind.js
 *   listen.js
 *   common.js
 *   shapes.js
 *   geom.js
 *
 * USAGE:
 *  _PC_Install() must be called after the body is loaded
 *
 */

function PopupController() {
  this.activePopups_ = [];
}

/**
 * @param {Document} opt_doc document to add PopupController to
 *                   DEFAULT: "document" variable that is currently in scope
 * @return {Boolean} indicating if PopupController installed for the document;
 *                   returns false if document already had PopupController
 */
function _PC_Install(opt_doc) {
  if (gPopupControllerInstalled) return false;
  gPopupControllerInstalled = true;
  var doc = (opt_doc) ? opt_doc : document;

  // insert _notifyPopups in BODY's onmousedown chain
  listen(doc.body, 'mousedown', PC_notifyPopups);
  return true;
}

/**
 * Notifies each popup of a mousedown event, giving
 * each popup the chance to deactivate itself. 
 *
 * @throws Error if a popup does not have a deactivate function
 *
 * @private
 */
function PC_notifyPopups(e) {
  if (gPopupController.activePopups_.length == 0) return false;
  var e = e || window.event;
  for (var i = gPopupController.activePopups_.length - 1; i >= 0; --i) {
    var popup = gPopupController.activePopups_[i];
    PC_assertIsPopup(popup);
    if (popup.deactivate(e)) {
      gPopupController.activePopups_.splice(i, 1);
    }
  }
  return true;
}

/**
 * Adds the popup to the list of popups to be
 * notified of a mousedown event.
 *
 * @return boolean indicating if added popup; false if already contained
 * @throws Error if popup does not have a deactivate function
 */
function PC_addPopup(popup) {
  PC_assertIsPopup(popup);
  for (var i = 0; i < gPopupController.activePopups_.length; ++i) {
    if (popup === gPopupController.activePopups_[i]) return false;
  }
  gPopupController.activePopups_.push(popup);
  return true;
}

// This function should be unnecessary.
//
// function PC_removePopup(popup) {
//   for (var i = gPopupController.activePopups_.length - 1; i >= 0; --i) {
//     var p = gPopupController.activePopups_[i];
//     if (popup === p) {
//       gPopupController.activePopups_.splice(i, 1);
//       return;
//     }
//   }
// }

/** asserts that popup has a deactivate function */
function PC_assertIsPopup(popup) {
  AssertType(popup.deactivate, Function, 'popup missing deactivate function');
}

var gPopupController = new PopupController();
var gPopupControllerInstalled = false;

// shape related classes

/** a point in 2 cartesian dimensions.
  * @constructor
  * @param x x-coord.
  * @param y y-coord.
  * @param opt_coordinateFrame a key that can be passed to a translation function to
  *   convert from one coordinate frame to another.
  *   Coordinate frames might correspond to things like windows, iframes, or
  *   any element with a position style attribute.
  */
function Point(x, y, opt_coordinateFrame) {
  /** a numeric x coordinate. */
  this.x = x;
  /** a numeric y coordinate. */
  this.y = y;
  /** a key that can be passed to a translation function to
    * convert from one coordinate frame to another.
    * Coordinate frames might correspond to things like windows, iframes, or
    * any element with a position style attribute.
    */
  this.coordinateFrame = opt_coordinateFrame || null;
}
Point.prototype.toString = function () {
  return '[P ' + this.x + ',' + this.y + ']';
};
Point.prototype.clone = function() {
  return new Point(this.x, this.y, this.coordinateFrame);
}

/** a distance between two points in 2-space in cartesian form.
  * A delta doesn't have a coordinate frame associated since all the coordinate
  * frames used in the HTML dom are convertible without rotation/scaling.
  * If a delta is not being used in pixel-space then it may be annotated with
  * a coordinate frame, and the undefined coordinate frame can be assumed
  * to represent pixel space.
  * @constructor
  * @param dx distance along x axis
  * @param dy distance along y axis
  */
function Delta(dx, dy) {
  /** a numeric distance along the x dimension. */
  this.dx = dx;
  /** a numeric distance along the y dimension. */
  this.dy = dy;
}
Delta.prototype.toString = function () {
  return '[D ' + this.dx + ',' + this.dy + ']';
};

/** a rectangle or bounding region.
  * @constructor
  * @param x x-coord of the left edge.
  * @param y y-coord of the top edge.
  * @param w width.
  * @param h height.
  * @param opt_coordinateFrame a key that can be passed to a translation function to
  *   convert from one coordinate frame to another.
  *   Coordinate frames might correspond to things like windows, iframes, or
  *   any element with a position style attribute.
  */
function Rect(x, y, w, h, opt_coordinateFrame) {
  /** the numeric x coordinate of the left edge. */
  this.x = x;
  /** the numeric y coordinate of the top edge. */
  this.y = y;
  /** the numeric distance between the right edge and the left. */
  this.w = w;
  /** the numeric distance between the top edge and the bottom. */
  this.h = h;
  /** a key that can be passed to a translation function to
    * convert from one coordinate frame to another.
    * Coordinate frames might correspond to things like windows, iframes, or
    * any element with a position style attribute.
    */
  this.coordinateFrame = opt_coordinateFrame || null;
}

/**
 * Determines whether the Rectangle contains the Point.
 * The Point is considered "contained" if it lies
 * on the boundary of, or in the interior of, the Rectangle.
 *
 * @param {Point} p
 * @return boolean indicating if this Rect contains p
 */
Rect.prototype.contains = function(p) {
  return this.x <= p.x && p.x < (this.x + this.w) &&
             this.y <= p.y && p.y < (this.y + this.h);
}

/**
 * Determines whether the given rectangle intersects this rectangle.
 *
 * @param {Rect} r
 * @return boolean indicating if this the two rectangles intersect
 */
Rect.prototype.intersects = function(r) {
  var p = function(x, y) {
    return new Point(x, y, null);
  }
  
  return this.contains(p(r.x, r.y)) ||
         this.contains(p(r.x + r.w, r.y)) ||
         this.contains(p(r.x + r.w, r.y + r.h)) ||
         this.contains(p(r.x, r.y + r.h)) ||
         r.contains(p(this.x, this.y)) ||
         r.contains(p(this.x + this.w, this.y)) ||
         r.contains(p(this.x + this.w, this.y + this.h)) ||
         r.contains(p(this.x, this.y + this.h));
}

Rect.prototype.toString = function () {
  return '[R ' + this.w + 'x' + this.h + '+' + this.x + '+' + this.y + ']';
};

Rect.prototype.clone = function() {
  return new Rect(this.x, this.y, this.w, this.h, this.coordinateFrame);
};

// Functions for manipulating date and time.
// Clients should replace the tu_getPreferred* functions based on the user's
// locale.

var DATE_FIELD_ORDER_DMY = 'DMY';
var DATE_FIELD_ORDER_YMD = 'YMD';
var DATE_FIELD_ORDER_MDY = 'MDY';

/** Given a string, and base datetime, try to resolve to a time */
function ParseTime(s, basedate) {  // TODO: i18n or preferably whack it
  var time = LDP_ParseInputTime(s, null);
  if (!time) { return null; }
  var date = ical_builderCopy(basedate);
  date.hour = time.hour;
  date.minute = time.minute;
  date.second = time.second;
  return date.validateDateTime() ? date.toDateTime() : null;
}

/**
 * Go from a time to a string, and follow the preference rules
 * ie - "10:40pm" or "20:40"
 *
 * <p>If the abbreviated format option is true, then the time
 * will have the minutes omitted if they are 0 and the suffix
 * ommitted if is "am."  This can also be known as "Carl format."
 *
 * @param t {ICAL_DateTime} - the time
 * @param opt_abbreviated {boolean} if the time should appear in
 *           an abbreviated format. DEFAULT: false
 * @return {string} plain text.
 */
function TimeToString(/*ICAL_DateTime*/ t, opt_abbreviated) {
  if (t instanceof ICAL_PartialDateTime) {
    return PartialTimeToString(t, opt_abbreviated);
  }
  AssertType(t, ICAL_DateTime);
  if (LDP_Prefers24HourTime && LDP_Prefers24HourTime()) {
    return FormatTimeShort(t);
  }
  opt_abbreviated = !!opt_abbreviated;
  var h = t.hour;
  var suffix;
  // TODO: i18n
  if (opt_abbreviated) {
    suffix = (h >= 12) ? 'p' : '';
  } else {
    suffix = (h >= 12) ? 'pm' : 'am';
  }
  if (h > 12) {
    h -= 12;
  } else if (h === 0) {
    h = 12;
  }
  var m = t.minute;
  var str = h;
  if (!opt_abbreviated || m !== 0) {
    str += (m >= 10 ? ':' : ':0') + m;
  }
  return str + suffix;
}

/** formats a time as "H:MM", e.g. "4:30". */
function FormatTimeShort(/*ICAL_DateTime*/ t) {
  var m = t.minute;
  return m < 10 ? t.hour + ':0' + m : t.hour + ':' + m;
}

function PartialTimeToString(/*ICAL_{Partial,}DateTime*/ t, opt_abbreviated) {
  AssertTrue(t instanceof ICAL_PartialDateTime || t instanceof ICAL_DateTime);
  opt_abbreviated = !!opt_abbreviated;
  var h = t.hour;
  var suffix = '';
  // TODO: i18n
  if (undefined !== h) {
    if (!(LDP_Prefers24HourTime && LDP_Prefers24HourTime())) {
      if (opt_abbreviated) {
        suffix = (h >= 12) ? 'p' : '';
      } else {
        suffix = (h >= 12) ? 'pm' : 'am';
      }

      if (h > 12) {
        h -= 12;
      } else if (h === 0) {
        h = 12;
      }
    }
  } else {
    h = '?';
  }
  var str = h;
  var m = t.minute;
  if (undefined !== m) {
    if (!opt_abbreviated || m !== 0) {
      str += (m >= 10 ? ':' : ':0') + m;
    }
  } else {
    str += ':??';
  }
  return str + suffix;
}

// Go from a date to a string, and follow the preference rules
// ie - "12/31/2005" or "31/12/2005"
function DateToString(date) {
  AssertTrue(date instanceof ICAL_Date || date instanceof ICAL_DateTime);
  switch (tu_getPreferredFieldOrder()) {
    case DATE_FIELD_ORDER_DMY:
      return date.date + '/' + date.month + '/' + date.year;
    case DATE_FIELD_ORDER_YMD:
      return date.year + '-' + date.month + '-' + date.date;
    default:
      return date.month + '/' + date.date + '/' + date.year;
  }
}

// Go from a date to a string, and follow the preference rules
// ie - "12/31/2005" or "31/12/2005"
function PartialDateToString(date) {
  AssertType(date, ICAL_DateValue);
  var d = date.date || '??';
  var m = date.month || '??';
  var y = undefined !== date.year ? date.year : '????';
  switch (tu_getPreferredFieldOrder()) {
    case DATE_FIELD_ORDER_DMY:
      return d + '/' + m + '/' + y;
    case DATE_FIELD_ORDER_YMD:
      return y + '-' + m + '-' + d;
    default:
      return m + '/' + d + '/' + y;
  }
}

// Format of date column header which is used in 2-7 day mode
function FormatDateLong(d, opt_shortName) {
  var dayArray = (opt_shortName) ? CG_MDAYS_OF_THE_WEEK : FULL_WEEKDAYS;
  if (!LDP_PrefersMonthBeforeDate()) {
    return dayArray[d.getDayOfWeek()] + ' ' + d.date + '/' + d.month;
  } else {
    return dayArray[d.getDayOfWeek()] + ' ' + d.month + '/' + d.date;
  }
}

function FormatTimeTipSpatially(s, e, type) {
  if (undefined == s || undefined == e) { return undefined; }

  if (DD_REPOSITION === type) {
    return TimeToString(s);
  } else if (DD_RESIZE === type) {
    return TimeToString(s) + '-' + TimeToString(e);
  }
}

function FormatDayTimeTipSpatially(s, e, type, isAllDay) {
  if (undefined == s || undefined == e) { return undefined; }
  switch (type) {
    case DD_REPOSITION:
      return isAllDay ? DateToString(s) : TimeToString(s);
      // break;
    case DD_RESIZE:
      return TimeToString(e);
      // break;
  }
}


// Tools for inspecting the browser and extracting timezone information
// Based loosely on code from:
// http://www.breakingpar.com/bkp/home.nsf/0/87256B280015193F87256CFB006C45F7

// See this site for an excruciating discussion of browser behaviors:
// http://www.shotover.clara.net/datetest.htm

// Given a specific year/month/date, get the diff in hours from UTC
function getTimeHoursDiff(year, month, day) {
  var dateLocal = new Date(year, month, day, 12, 0, 0, 0);
  var temp = dateLocal.toGMTString();
  var dateGmt = new Date(temp.substring(0, temp.lastIndexOf(' ')-1));
  return (dateLocal - dateGmt) / (1000 * 60 * 60);
}

// Brute force search to find days when DST starts and stops in a given year
function ExtractTZBounds(y, result) {
  // Try to find the dates for daylight switching
  var foundStart = false;

  // Loop through all of the months
  for(var m = 0; m < 12; m++) {
    var tz1 = getTimeHoursDiff(y, m, 1);
    var tz2 = getTimeHoursDiff(y, m+1, 1);

    // If the UTC on both months is identical, then no change took place in the
    // first month, so skip it
    if (tz1 == tz2) {
      continue;
    }

    // Now loop through all of the days to see where it flipped
    var monthDays = ICAL_daysInMonth(y, m + 1);

    var cur = tz1;
    for(var d = 2; d <= monthDays; d++) {
      var tz = getTimeHoursDiff(y, m, d);
      if (tz != cur) {

        if (!foundStart) {
          result.dstStartM = m+1;
          result.dstStartD = d;
          foundStart = true;
        }
        else {
          result.dstEndM = m+1;
          result.dstEndD = d;
          return
        }

        cur = tz;
      }
    }

    // Minor exception for last day of month
    if (cur != tz2) {
      if (!foundStart) {
        result.dstStartM = m+2;
        result.dstStartD = 1;
        foundStart = true;
      }
      else {
        result.dstEndM = m+2;
        result.dstEndD = 1;
        return
      }
    }
  }
}

/** Extract an object containing the information. It has the following fields
  * int hourDiffStd - The hours diff for this computer when we are NOT in DST
  * int hourDiffDst - The hours diff for this computer when we are in DST
  * int hourDiffNow - The hours diff for this computer right now
  * bool dstObserved - Is this computer in a TZ that has DST?
  * bool dstNow - Is this computer in DST right now?
  * string tzName - the current timezone [might by null if we can't tell,
                    is NOT standardized between browsers, ok for end user]
  * int dstStartM - the start month for DST [only if dstObserved is true]
  * int dstStartD - the start day for DST [only if dstObserved is true]
  * int dstEndM - the end month for DST [only if dstObserved is true]
  * int dstEndD - the end day for DST [only if dstObserved is true]
  * string tzId - the timezone ID. null if we're not sure
  */

function getTimeInfo() {
  var result = new Object();

  var now = new Date();
  var date1 = new Date(now.getFullYear(), 0, 1, 0, 0, 0, 0)

  // Note, this assumes that middle of summer and middle of winter
  // are in the middle of regular + daylight savings time IF THEY HAPPEN
  // TO EXIST for this computer. This is always true AFAIK
  var hoursDiffStdTime = getTimeHoursDiff(now.getFullYear(), 0, 1);
  var hoursDiffDaylightTime = getTimeHoursDiff(now.getFullYear(), 6, 1);

  // Extract the name of the current timezone
  var localTime = date1.toString();
  var tzStrPos = localTime.lastIndexOf(':00 ');
  var tzStrPosEnd = localTime.lastIndexOf(' ' + date1.getFullYear());

  if (tzStrPos >= 0) {
    if (tzStrPosEnd < tzStrPos) {
      tzStrPosEnd = localTime.length;
    }
    result.tzName = localTime.substring(tzStrPos + 4, tzStrPosEnd);
  }

  result.hourDiffStd = hoursDiffStdTime;
  result.hourDiffDst = hoursDiffDaylightTime;
  result.hourDiffNow = (now.getTimezoneOffset()/60)*(-1);

  result.dstObserved = (hoursDiffDaylightTime != hoursDiffStdTime);
  result.dstNow = ((now.getTimezoneOffset()/60)*(-1) != hoursDiffStdTime);

  if (result.dstObserved) {
    ExtractTZBounds(2005, result);
  }

  // Attempt to determine which tz we are in by looking at the array below
  result.tzId = null;
  for(var i = 0; i < tzInfo.length; ++i) {
    var zone = tzInfo[i];

    // TODO(davem) - we can binary search on this to speed it up
    if (zone[TZ_IND_OFFSET] < result.hourDiffStd) {
      // If we are looking for zone = -10, and we've iterated
      // to -9 without finding a match, then it's not there
      break;
    }
    if (zone[TZ_IND_OFFSET] > result.hourDiffStd) {
      continue;;
    }

    var dst = (zone.length > 2);

    // If the DST mismatches, then leave
    if (result.dstObserved != dst) {
      continue;
    }

    // Need to check some more DST
    if (dst) {
      if (zone[TZ_IND_START_MONTH] != result.dstStartM ||
          zone[TZ_IND_START_DAY] != result.dstStartD ||
          zone[TZ_IND_END_MONTH] != result.dstEndM ||
          zone[TZ_IND_END_DAY] != result.dstEndD)
      continue;
    }

    // We're still here. Match!
    result.tzId = zone[TZ_IND_ID];
    break;
  }

  return result;
}


function tu_getPreferredFieldOrder() {
  // Try to infer the operating system date formatting preferences
  var d1 = new Date(1970, 12, 29, 0, 0, 0, 0);

  // Attempt to do the same thing with day+month
  var s1 = d1.toLocaleDateString();
  var monthPos = s1.indexOf('12'),
       datePos = s1.indexOf('29'),
       yearPos = s1.indexOf('70');

  if (monthPos < 0 && datePos >= 0) {
    // infer month location when month written as Dec or December by finding
    // first character where s1 and s2 differ
    var s2 = new Date(1970, 11, 29, 13, 0, 0, 0).toLocaleDateString();
    var min = Math.min(s1.length, s2.length);
    for (var i = 0; i < min; i++) {
      if (s1.charCodeAt(i) != s2.charCodeAt(i)) {
        monthPos = i;
        break;
      }
    }
  }

  var dateFieldOrder = DATE_FIELD_ORDER_YMD;
  if ((monthPos | datePos) >= 0) {
    if (monthPos < datePos) {
      if (yearPos >= 0 && yearPos < monthPos) {
        dateFieldOrder = DATE_FIELD_ORDER_YMD;
      } else {
        dateFieldOrder = DATE_FIELD_ORDER_MDY;
      }
    } else {
      dateFieldOrder = DATE_FIELD_ORDER_DMY;
    }
  }

  d1 = s1 = monthPos = datePos = yearPos = null;

  // rewrite the function so that we don't have to do this inference every time
  tu_getPreferredFieldOrder = function () {
    return dateFieldOrder;
  };
  return dateFieldOrder;
}

/**
 * what is the first day of the week in the current locale?
 * HACK HACK: clients must replace this function since I can't figure out how to
 * infer it.
 */
function tu_getPreferredStartDayOfWeek() {
  // 0 is Sunday, 1 is Monday, in keeping with the builtin Date object.
  // can't infer this, so overwrite this function
  return 0;
}

/**
 * DP_DatePicker is a DHTML widget that allows the user to
 * select (or simply view) dates from a calendar.
 * It supports various selection modes (single-day, multi-day, none),
 * and can be set to start on any day of the week.
 *
 * Clients may provide an "id prefix" so that every id within the
 * DP_DatePicker starts with the same prefix. This makes it possible
 * to add multiple DP_DatePickers to the same web page, but with
 * different ids so they can be distinguished.
 *
 * The HTML table for the DP_DatePicker is generated by populateHtml().
 * When the user changes to a different month, populateHtml() is called again
 * to change the content of the cells.
 *
 * Each cell has its own listeners for mouse events so that the DP_DatePicker
 * can be responsive. We use DOM0 style event listeners for speed of setup.
 *
 * DP_DatePicker was designed to work with Google Calendar; however,
 * it has been generalized so that it can work in any web page,
 * assuming the following JavaScript files are included:
 *
 * DEPENDENCIES:
 *   common.js
 *   ical.js
 *   shapes.js
 *   geom.js
 *   dragdrop.js
 *   dragdropselect.js
 *
 * ADDITIONAL REQUIREMENTS:
 *   As DP_DatePicker uses the dragdrop library,
 *   the web page on which DP_DatePicker is used
 *   must also meet its two requirements:
 *
 * (1) It should be run in strict mode. [but will run without it]
 * (2) It call _DD_Install() in the onload handler of the <body>
 *
 * DP_DatePicker must work in Firefox 1.0+ and IE 5.5+.
 * It should also work in Safari, but this is a lower priority
 * and has not been tested.
 *
 */

/**
 * public methods:
 *   DP_DatePicker()
 *   show()
 *   hide()
 *   isVisible()
 *   showMonth()
 *   showDate()
 *   showFullWeeks()
 *   {set,get}Selection()
 *   {add,remove}SelectionListener()
 *   {add,remove}MonthChangeListener()
 *   printStatus()
 *   {set,get}SelectionMode()
 *   {set,is}SnapToWeek()
 *   getToday()
 *   clearSelections()
 *   isSelected()
 *   {set,get}UseDayHeaders()
 *   {set,get}FirstDayOfWeek()
 *   {set,get}Decorator()
 *   setDecoratorInline()
 *   refresh()
 *   getFirstCell()
 *   get{First,Last}Date();
 *   {add,remove}Logger()
 *   getTableNode()
 *   getNextCell()
 *   getPrevCell()
 *   getDateForCell()
 *   inDrag()
 *   {set,get}ClickMode()
 *   setPrevMonthHtmlFn()
 *   setCurMonthHtmlFn()
 *   setNextMonthHtmlFn()
 *   getMonths()
 *   getFullMonths()
 *   getDiv()
 *   setFudgeFactor()
 *
 * constants:
 *   DP_SELECTION_SINGLE_DAY
 *   DP_SELECTION_RANGE
 *   DP_SELECTION_MULTI_DAY
 *   DP_SELECTION_NONE
 *
 */

/**
 * INVARIANTS
 * ==========
 * These properties must be preserved throughout the lifetime of DP_DatePicker
 * to ensure that the picker behaves correctly:
 *
 * DP_DatePicker is an HTML table of 7 rows and 7 columns
 * where each cell represents one date in the picker.
 * Each <TD> cell has three fields that are stored in maps
 * (if they were properties of the cell, then they would cause a
 * memory leak on IE):
 *
 * id2NextMap_: maps id of <TD> to the next cell, chronologically
 * id2PrevMap_: maps id of <TD> to the previous cell, chronologically
 * id2DateMap_: maps id of <TD> to the ICAL_Date it currently displays
 *
 * The prevCell and nextCell maps make it simple to iterate over
 * all of the cells in the table in either direction.
 * DP_DatePicker has the private fields _firstCell and _lastCell
 * as starting points for either type of iteration:
 *
 *  for (var cell = this.getFirstCell(); cell; cell = this.getNextCell(cell)) {
 *    // use cell
 *
 *    // also, get cell date and use that, as well
 *    var date = this.getDateForCell(cell);
 *  }
 *
 * The prevCell and nextCell maps are populated in populateHtml(). The values
 * of all of the maps will change every time the displayed month changes.
 * It is populated in the populateHtml() method and should only be
 * mutated by that method.
 *
 * There is a private field, _dateToCellMap that must be maintained as well.
 * It is an Object that maps the toString() of an ICAL_Date to a <TD>
 * in the table that displays that date. Thus, an ICAL_Date has an entry
 * in the map if and only if there is a cell displaying that date.
 * Thus, the values of this map must change when the displayed month changes.
 *
 * _selectedDates is a DateSet that must contain every ICAL_Date that is
 * currently selected. An ICAL_Date does not have to be in view to be
 * considered selected. This allows the user to make a selection and then
 * toggle the displayed month without losing that selection.
 *
 */

/* Important ids and CSS classes used by the DHTML widget produced
 * by the DP_DatePicker.
 *
 * ids:
 *  dp_tbl, top-level table for the datepicker <table>
 *  dp_mhl, month header left <td>   (month "back button)
 *  dp_mhc, month header center <td> (displays current month)
 *  dp_mhr, month header right <td>  (month "forward" button)
 *  dp_dow, days of week row <tr> (may not be displayed)
 *  dp_day_[0-6], day of week heading <td>
 *  dp_day_[0-6]_[0-6], day of the week <td>
 *  dp_week_[0-6], row for a week <tr>
 *  dp_lasso, range selection lasso <div>
 *
 * CSS classes:
 *
 * Prefix for all classes is "DP_" by default, but this can be overridden
 * by the opt_classPrefix parameter in DP_DatePicker constructor, making it
 * possible to have two pickers, styled differently, on the same page.
 *
 *  DP_monthtable
 *  DP_heading
 *  DP_prev
 *  DP_cur
 *  DP_next
 *  DP_today
 *  DP_weekday
 *  DP_weekend
 *  DP_onmonth
 *  DP_offmonth
 *  DP_month_top
 *  DP_month_left
 *  DP_day_top
 *  DP_day_right
 *  DP_day_left
 *  TODO(bolinfest):
 *  # no DP_day_bottom; use bottom of DP_monthtable for now
 */

// TODO(bolinfest): let user indicate that
//                  a selection has been made (closing picker?)

/**
 * Creates a new DatePicker.
 *
 * @param {Element} div
 *     DOM node to which the calendar is added
 *
 * @param {Boolean} opt_evenMonthHeading determines how the 7 cells
 *     in the month heading row should be grouped.
 *     true if the month heading row should be broken up into
 *       a 2-3-2 configuration;
 *     false if it should use a 1-5-1 configuration
 *     DEFAULT: false
 *
 * @param {String} opt_idPrefix
 *     prefix that each id in the calendar should have
 *     DEFAULT: (div.id + '_')
 *
 * @param {String} opt_classPrefix
 *     prefix that each className in the calendar should have
 *     DEFAULT: "DP_"
 *
 * @param {ICAL_Date} opt_today
 *     date that reflects what to display as current date
 *     DEFAULT: current date
 * @throws {Error} if opt_today is not an ICAL_Date
 *
 * @constructor
 */
function DP_DatePicker(/*Element*/   div,
                       /*Boolean*/   opt_evenMonthHeading,
                       /*String*/    opt_idPrefix,
                       /*String*/    opt_classPrefix,
                       /*ICAL_Date*/ opt_today) {
  AssertTrue(div, 'element passed to DP_DatePicker constructor is null');
  this.div_ = div;
  this.id_ = (opt_idPrefix) ? opt_idPrefix : this.div_.id + '_';
  this.class_ = (opt_classPrefix) ? opt_classPrefix : 'DP_';

  // Add the current item to the static cache
  DP_DatePicker.dp_cache_[this.id_] = this;

  if (opt_today) {
    AssertType(opt_today, ICAL_Date, 'opt_today is not an ICAL_Date');
    this.today_ = opt_today;
  } else {
    this.today_ = ICAL_Date.now();
  }
  this.dispDate_ = ICAL_Date.create(this.today_.year, this.today_.month, 1);
  this.firstDay_ = 0; // default Sunday; change using setFirstDayOfWeek()

  this.evenMonthHeading_ = !!opt_evenMonthHeading;
  this.useDayHeaders_ = false;

  // initialized in populateHtml()
  this.prevMonth_ = null;
  this.nextMonth_ = null;
  this.dateToCellMap_ = {}; /*<string,TD_Element>*/

  this.id2NextMap_ = {}; /*<{string}cell_id,{Element}TD>*/
  this.id2PrevMap_ = {}; /*<{string}cell_id,{Element}TD>*/
  this.id2DateMap_ = {}; /*<{string}cell_id,{ICAL_Date}date>*/

  this.firstCell_ = null; // first date cell; init in generateSkeletalHtml()
  this.lastCell_ = null; // last date cell; may not be visible
  this.selectionListeners_ = new ListenerList(this);
  this.monthChangeListeners_ = new ListenerList(this);
  this.showSelection_ = false; // TODO(bolinfest): expose setting through API

  this.isVisible_ = false;
  this.selectedDates_ = new DateSet();
  this.numWeeks_ = 0; // num weeks visible, initialized in populateHtml()
  this.decorator_ = null;
  this.decoratorInline_ = null;
  this.showFullWeeks_ = true; // default true; change using showFullWeeks()

  // these functions can be set to override what HTML is
  // displayed in the "prev," "current," and "next" navigation cells
  this.prevMonthHtmlFn_ = null;
  this.curMonthHtmlFn_ = null;
  this.nextMonthHtmlFn_ = null;

  this.populateHtml();

  this.snapToWeek_ = false;
  this.setSelectionMode(DP_SELECTION_SINGLE_DAY);
  this.setClickMode(DP_CLICK_NONE);

  this.ignoringDrags_ = false;
  this.fudgeFactor_ = null;
  this.dragStartCell_ = null;
  this.dragEndCell_ = null;
  this.rangeStartDate_ = null;
  this.rangeEndDate_ = null;
  this.lastDragEvent_ = null;
  this.inDrag_ = false;

  this.viewableStartDate_ = null;
  this.viewableEndDate_ = null;

  var datepicker = this;
// TODO(bolinfest): fix closure so this does not leak
  var statusSelectionListener = function(e) {
    // TODO(bolinfest): i18n
    var MSG_SELECTED = "Selected: ";

    var date1 = e.startDate;
    var date2 = e.endDate;

    var str;
    if (!date1) { // nothing selected
      str = DP_DatePicker.MSG_DATE_SELECTION[this.selectionMode_];
    } else if (!date2 || date1.equals(date2)) {
      str = MSG_SELECTED + datepicker.formatDate(date1, true);
    } else {
      str = MSG_SELECTED + datepicker.formatDate(date1) +
                ' - ' + datepicker.formatDate(date2);
    }
    datepicker.printStatus(str);
  };
  if (this.showSelection_) this.addSelectionListener(statusSelectionListener);

  this.loggers_ = new ListenerList(this);
}

/** user may only select a single day */
var DP_SELECTION_SINGLE_DAY = 0;

/** user may only select a continuous range of days */
var DP_SELECTION_RANGE = 1;

/** user may select any set of days */
// currently NOT IMPLEMENTED
var DP_SELECTION_MULTI_DAY = 2;

/** user may not select any days; date viewer only */
var DP_SELECTION_NONE = 3;

// TODO(bolinfest): i18n
DP_DatePicker.MSG_DATE_SELECTION = {}
DP_DatePicker.MSG_DATE_SELECTION[DP_SELECTION_SINGLE_DAY] =
    /* SAFE */ "Select a date";
DP_DatePicker.MSG_DATE_SELECTION[DP_SELECTION_RANGE] =
    /* SAFE */ "Select a range of dates";
DP_DatePicker.MSG_DATE_SELECTION[DP_SELECTION_MULTI_DAY] =
    /* SAFE */ "Select dates";
DP_DatePicker.MSG_DATE_SELECTION[DP_SELECTION_NONE] =
    /* SAFE */ "&nbsp;";

/*
 * DP_DatePicker can have different behaviors for clicking
 * when its selection mode is DP_SELECTION_RANGE.
 * Users often drag when in SELECTION_RANGE mode, but this
 * allows specialized behavior for clicks.
 *
 * @see DP_DatePicker.{set,get}ClickMode()
 */

/** in range mode, clicks are ignored  */
var DP_CLICK_NONE = 0;

/** in range mode, clicking selects an individual date */
var DP_CLICK_1_DAY = 1;

/** in range mode, clicking selects the entire week */
var DP_CLICK_WEEK = 7;

/** in range mode, clicking selects the entire month */
var DP_CLICK_MONTH = 30;

/**
 * In range mode, clicking within the current selection selects
 * the day that was clicked. If the click occurs outside the
 * current selection, then the selection is changed to include
 * the clicked cell; however, the length of the selection must
 * be preserved.
 */
var DP_CLICK_PRESERVE_RANGE = -1;

/**
 * Sets the "click" mode to one of the modes
 * defined in the DP_CLICK_ enumeration.
 *
 * @param mode
 * @param opt_clickFunc {Function} function to call when a click is made
 *   on a cell in the datepicker. The function should take the id of the
 *   cell that was clicked (a String) and may return anything, since its
 *   return value will be ignored.
 *
 * @throws Error if mode is not a valid click mode
 */
DP_DatePicker.prototype.setClickMode = function(mode, opt_clickFunc) {
  if (mode != DP_CLICK_NONE &&
      mode != DP_CLICK_1_DAY &&
      mode != DP_CLICK_WEEK &&
      mode != DP_CLICK_MONTH &&
      mode != DP_CLICK_PRESERVE_RANGE &&
      (!(opt_clickFunc instanceof Function))) {
      throw new Error("Invalid click mode: " + mode);
  }
  this.clickMode_ = mode;
  this.clickFunc_ = opt_clickFunc;
}


/** @return the day headers mode */
DP_DatePicker.prototype.getUseDayHeaders = function() {
  return this.useDayHeaders_;
}

/** set the day headers mode
  * @param on should it be set on or off
  */
DP_DatePicker.prototype.setUseDayHeaders = function(on) {
  if (on != this.useDayHeaders_) {
    this.useDayHeaders_ = on;
    this.populateHtml();
  }
}

/** @return the current click mode */
DP_DatePicker.prototype.getClickMode = function() {
  return this.clickMode_;
}

/**
 * Changing selection mode may reset the selection.
 *
 * Set to DP_SELECTION_SINGLE_DAY by default.
 *
 * @throws Error if mode is not a valid selection mode
 */
DP_DatePicker.prototype.setSelectionMode = function(mode) {
  if (!(0 <= mode && mode <= 3)) {
    Fail("Invalid selection mode: " + mode);
  }
  if (this.selectionMode_ == mode) { return; }
  this.selectionMode_ = mode;
  this.clearSelections();
}

/** @return the current selection mode */
DP_DatePicker.prototype.getSelectionMode = function() {
  return this.selectionMode_;
}

/** Make the DP_DatePicker visible. */
DP_DatePicker.prototype.show = function() {
  this.isVisible_ = true;
  this.populateHtml();
}

/** Remove the DP_DatePicker from view. */
DP_DatePicker.prototype.hide = function() {
  this.div_.innerHTML = '';
  this.isVisible_ = false;
}

/** @return {Boolean} indicating whether the picker is visible */
DP_DatePicker.prototype.isVisible = function() {
  return this.isVisible_;
}

/** @return the next cell, chronologically, after the provided cell */
DP_DatePicker.prototype.getNextCell = function (cell) {
  return this.id2NextMap_[cell.id];
}

/** @return the previous cell, chronologically, before the provided cell */
DP_DatePicker.prototype.getPrevCell = function (cell) {
  return this.id2PrevMap_[cell.id];
}

/** @return the ICAL_Date currently displayed in the cell */
DP_DatePicker.prototype.getDateForCell = function (cell) {
  return this.id2DateMap_[cell.id];
}

/** @return the top-level <TABLE> Element for the picker */
DP_DatePicker.prototype.getTableNode = function() {
  return forid(this.id_ + 'tbl');
}

/**
 * Set the day that a week should start on.
 *
 * @param {int} day
 *     day of week that week should start on
 *     0 : Sunday, 1 : Monday,..., 6 : Saturday
 */
DP_DatePicker.prototype.setFirstDayOfWeek = function(day) {
  AssertTrue(0 <= day && day <= 6, day + ' not a valid first day of week');
  this.firstDay_ = day;
  this.populateHtml();
}

/**
 * Get the day that a week starts on.
 *
 * @return {int} day
 *     day of week that week starts on
 *     0 : Sunday, 1 : Monday,..., 6 : Saturday
 */
DP_DatePicker.prototype.getFirstDayOfWeek = function() {
  return this.firstDay_;
}

/**
 * Sets the general decorator function.
 * To remove the decorator function, invoke setDecorator(null).
 *
 * A decorator is a function that is called when the
 * month changes in the picker. This can be used to
 * add extra decoration to the picker when it is redrawn for a month change.
 *
 * The decorator function must take the picker as its first argument.
 * The return value of the decorator function will be ignored.
 *
 * The picker will be redrawn when the decorator function is set.
 */
DP_DatePicker.prototype.setDecorator = function(func) {
  if (func) AssertType(func, Function);
  this.decorator_ = func;
  this.populateHtml();
  return true;
}

/**
 * Sets the incline decorator function.
 * To remove the decorator function, invoke setDecorator(null).
 *
 * A decorator is a function that is called when the calendar is drawn
 * It must be in the form function(ICAL_Date)
 * And return a string which will be placed inline within the style of the TD
 * Note that it may return null if it has no style to apply.
 */
DP_DatePicker.prototype.setDecoratorInline = function(func) {
  if (func) AssertType(func, Function);
  this.decoratorInline_ = func;
}

/** @return the decorator function */
DP_DatePicker.prototype.getDecorator = function() {
  return this.decorator_;
}

/** @return the <TD> for the first date cell in the picker */
DP_DatePicker.prototype.getFirstCell = function() {
  return this.firstCell_;
}

/**
 * The first date visible in the picker.
 * If the picker is not currently visible, returns null.
 *
 * @return {ICAL_Date}
 */
DP_DatePicker.prototype.getFirstDate = function() {
  if (!this.isVisible_) return null;
  return this.id2DateMap_[this.firstCell_.id];
}

/**
 * The last date visible in the picker.
 * If the picker is not currently visible, returns null.
 *
 * @return {ICAL_Date}
 */
DP_DatePicker.prototype.getLastDate = function() {
  if (!this.isVisible_) return null;
  var cell = forid(this.id_ + 'day_' + (this.numWeeks_ - 1) + '_6');
  return this.id2DateMap_[cell.id];
}

DP_DatePicker.prototype.showFullWeeks = function(show) {
  if (show != this.showFullWeeks_) {
    this.showFullWeeks_ = show;
    this.populateHtml();
  }
}

/**
 * User can define his own function to set the text in
 * the "previous month" cell in the row of month headings.
 * If this function is not set, then an arrow pointing
 * to the left will be used.
 *
 * @param {Function} fn of the form ICAL_Date->string where
 *   the input is the date of the previous month and the output
 *   is the text to be displayed
 */
DP_DatePicker.prototype.setPrevMonthHtmlFn = function(fn) {
  AssertType(fn, Function);
  this.prevMonthHtmlFn_ = fn;
}

/**
 * User can define his own function to set the text in
 * the "current month" cell in the row of month headings.
 * If this function is not set, then the name of the current
 * month followed by the year will be used.
 *
 * @param {Function} fn of the form ICAL_Date->string where
 *   the input is the date of the current month and the output
 *   is the text to be displayed
 */
DP_DatePicker.prototype.setCurMonthHtmlFn = function(fn) {
  AssertType(fn, Function);
  this.curMonthHtmlFn_ = fn;
}

/**
 * User can define his own function to set the text in
 * the "next month" cell in the row of month headings.
 * If this function is not set, then an arrow pointing
 * to the right will be used.
 *
 * @param {Function} fn of the form ICAL_Date->string where
 *   the input is the date of the next month and the output
 *   is the text to be displayed
 */
DP_DatePicker.prototype.setNextMonthHtmlFn = function (fn) {
  AssertType(fn, Function);
  this.nextMonthHtmlFn_ = fn;
}

/** !!!client not allowed to mutate array that is returned */
DP_DatePicker.prototype.getMonths = function() {
  return MONTHS;
}

/** !!!client not allowed to mutate array that is returned */
DP_DatePicker.prototype.getFullMonths = function() {
  return FULL_MONTHS;
}

/**
 * Inserts the content for the DP_DatePicker.
 *
 * @private
 */
DP_DatePicker.prototype.populateHtml = function() {
  // If we are not visible, then no need to draw anything
  if (!this.isVisible_) {
    return;
  }

  var id = this.id_;
  var cell;
  var month = this.dispDate_.month;
  var year = this.dispDate_.year;
  var numDays = CG_DAYS_OF_THE_WEEK.length;
  // populate month headers
  var months = [(month == 1) ? 12 : (month - 1),
                (month),
                (month == 12) ? 1 : (month + 1)];
  var todaysMonth = ICAL_Date.create(this.today_.year, this.today_.month, 1);

  // use double-arrow to point in direction of "now;" otherwise, single quotes
  // this helps the user navigate back to the present
  var prevMonth = (ical_dateBuilder(year,
                                    month - 1,
                                    1)).toDate();


  var nextMonth = (ical_dateBuilder(year,
                                    month + 1,
                                    1)).toDate();

  if (this.prevMonthHtmlFn_) {
    months[0] = this.prevMonthHtmlFn_(prevMonth);
  } else {
    // determine if "back" month should have single- or double- arrow
    var la = (prevMonth.getComparable() >= todaysMonth.getComparable())
                 ? '&laquo;'
                 : '&lsaquo;&nbsp;';
    months[0] = la + MONTHS[months[0]];
  }

  if (this.curMonthHtmlFn_) {
    months[1] = this.curMonthHtmlFn_(this.dispDate_);
  } else {
    months[1] = FULL_MONTHS[months[1]] + ' ' + year;
  }

  if (this.nextMonthHtmlFn_) {
    months[2] = this.nextMonthHtmlFn_(nextMonth);
  } else {
    // determine if "forward" month should have single- or double- arrow
    var ra = ((nextMonth.getComparable() - todaysMonth.getComparable()) <= 0)
                 ? '&raquo;'
                 : '&nbsp;&rsaquo;';
    months[2] = MONTHS[months[2]] + ra;
  }

  // populate days
  var thisMonthDays = ICAL_daysInMonth(year, month);
  var prevMonthDays = ICAL_daysInMonth(prevMonth.year, prevMonth.month);
  var dates/*[ICAL_Date]*/ = new Array(7 * 7); // 7 weeks of 7 days each

  var offset = this.dispDate_.getDayOfWeek() - this.firstDay_;
  if (offset < 0) offset += 7;

  // make 6-week months start in the first row instead of the second
  if (thisMonthDays < 30 || offset < 5) offset += 7;

  // dates from end of previous month
  for (var i = 0; i < offset; ++i) {
    dates[i] = ICAL_Date.create(prevMonth.year,
                                prevMonth.month,
                                prevMonthDays - offset + i + 1);
  }
  // dates for this month
  for (var i = offset, j = 0; j < thisMonthDays; ++i) {
    dates[i] = ICAL_Date.create(year,
                                month,
                                ++j);
  }
  // dates for next month
  for (var i = offset + thisMonthDays, j = 0; i < dates.length; ++i) {
    dates[i] = ICAL_Date.create(nextMonth.year,
                                nextMonth.month,
                                ++j);
  }

  this.viewableStartDate_ = dates[0];
  this.viewableEndDate_ = dates[dates.length-1];

  // Draw the main table
  var html = new Array();
  var dist = (this.evenMonthHeading_) ? [2, 3, 2] : [1, 5, 1];

  // TODO(davem) - in IE, add double click handlers on the next+prev
  // TODO(davem) - use a template
  // create TABLE that contains DP_DatePicker
  // first row of table displays month and forward/back month navigation
  html.push('<table cols=7 cellspacing="0" cellpadding="3" id="', id, 'tbl"',
            ' class="', this.class_, 'monthtable" ',
            ' style="-moz-user-select:none; cursor:pointer;">',
            '<tr class="', this.class_, 'heading" id="', id,
            /* SAFE */'header">',
            '<td colspan=', dist[0], ' unselectable=on',
            ' onmousedown="' + GetFnName(DP_staticPrevMonth_) + '(',
            ToJSString(this.id_), ')"',
            ' id="', id, 'mhl" class="', this.class_, 'prev">',
            months[0], '</td>',
            '<td colspan=', dist[1], ' unselectable="on"',
            ' id="', id, 'mhc" class="', this.class_, 'cur">',
            months[1], '</td>',
            '<td colspan=', dist[2], ' unselectable="on"',
            ' onmousedown="' + GetFnName(DP_staticNextMonth_) + '(',
            ToJSString(this.id_), ')"',
            ' id="', id, 'mhr" class="', this.class_, /* SAFE */'next">',
            months[2], '</td>',
            '</tr>');

  if (this.useDayHeaders_) {
    // second row of table displays names of days of the week
    html.push('<tr class="', this.class_, 'days" id="', id, 'dow">');
    for (var i = 0; i < CG_DAYS_OF_THE_WEEK.length; ++i) {
      html.push('<td unselectable="on"',
                ' class="', this.class_, 'dayh" id="', id, 'day_', i, '">',
                CG_DAYS_OF_THE_WEEK[(i + this.firstDay_) % 7], '</td>');
    }
    html.push('</tr>');
  }

  // add weekend class to weekend cells
  // write numbers to boxes and assign CSS style for box
  var sunday   = (7 - this.firstDay_) % 7;
  var saturday = (sunday + 6) % 7;
  this.dateToCellMap_ = {};
  var cell = null;
  var classes = null;
  var clickFunc = GetFnName(DP_staticCellClicked_);

  for (var i = 0, j = -1; i < 7; ++i) {
    html.push('<tr id="', id, 'week_', i, '">');

    for (var k = 0; k < CG_DAYS_OF_THE_WEEK.length; ++k) {
      ++j;

      var selected = this.selectedDates_.contains(dates[j]);
      classes = new Array(); // CSS classes to add to cell
      if (i == 0) classes.push(this.class_ + 'day_top');
      if (k == 0) classes.push(this.class_ + 'day_left');
      else if (k == 6) classes.push(this.class_ + 'day_right');
      /* SAFE */
      classes.push(this.class_ + 'week' +
        ((k == sunday || k == saturday) ? 'end' : 'day') +
        (selected ? '_selected' : ''));
      if (j < offset || j >= offset + thisMonthDays) {
        classes.push(this.class_ + 'offmonth');
        if (dates[j].date <= 7) {
          classes.push(this.class_ + 'month_top');
          if (dates[j].date == 1 && k != 0) {
            classes.push(this.class_ + 'month_left');
          }
        }
      } else {
        classes.push(this.class_ + 'onmonth');
        if (dates[j].date <= 7) {
          classes.push(this.class_ + 'month_top');
          if (dates[j].date == 1 && k != 0) {
            classes.push(this.class_ + 'month_left');
          }
        }
        if (dates[j].date == this.today_.date
                 && month == this.today_.month
                 && year == this.today_.year) {
          classes.push(this.class_ + 'today' +
            (selected ? '_selected' : ''));
        }
      }

      // If there is an inline decorator, then get the info from it
      var extraStyles = '';
      if (this.decoratorInline_) {
        var styles = this.decoratorInline_(dates[j]);
        if (styles) {
          extraStyles = ' style="' + styles + '"';
        }
      }

      html.push('<td id="', id, 'day_', i, '_', k, '"',
                ' class="', classes.join(' '), '"',
                extraStyles,
                ' onclick="', clickFunc, '(this)"',
                ' unselectable="on">', dates[j].date, '</td>');
    }
    html.push('</tr>');
  }

  // bottom row displays current selection
  if (this.showSelection_) {
    html.push('<tr class="', this.class_, 'months">',
              '<td colspan="7" id="', id, /* SAFE */ 'sel"></td></tr>');
  }

  html.push('</table>');

  // assign HTML to element
  this.div_.innerHTML = html.join('');
  this.firstCell_ = forid(id + 'day_0_0');
  this.lastCell_ = forid(id + 'day_6_6');

  var cell = this.firstCell_;
  var cellParent = cell.parentNode;

  var prevCell = null;
  var prevId = null;

  var j = -1;
  var week = -1;
  while (cellParent != null) {
    ++week;
    if (week == 7) break;
    var day = -1;
    while (cell != null) {
      ++j;
      ++day;
      var cellId = id + 'day_' + week + '_' + day;

      this.id2DateMap_[cellId] = dates[j];
      this.dateToCellMap_[dates[j].toString()] = cell;
      this.id2PrevMap_[cellId] = prevCell;
      if (prevCell) this.id2NextMap_[prevId] = cell;
      prevCell = cell;
      prevId = cellId;
      cell = cell.nextSibling;
    }

    cellParent = cellParent.nextSibling;
    if (cellParent != null) {
      cell = cellParent.firstChild;
    }
  }

  this.numWeeks_ = 7;
  if (!this.showFullWeeks_) {
    var week4 = forid(id + 'week_4');
    var week5 = forid(id + 'week_5');
    var week6 = forid(id + 'week_6');
    // hide unnecessary weeks
    if (this.id2DateMap_[id + 'day_4_0'].month != month) {
      week4.style.display = 'none';
      week5.style.display = 'none';
      week6.style.display = 'none';
      this.numWeeks_ = 4;
    }
    else if (this.id2DateMap_[id + 'day_5_0'].month != month) {
      week5.style.display = 'none';
      week6.style.display = 'none';
      this.numWeeks_ = 5;
    }
    else if (this.id2DateMap_[id + 'day_6_0'].month != month) {
      week6.style.display = 'none';
      this.numWeeks_ = 6;
    }
  }

  // store prev and next month
  this.prevMonth_ = prevMonth;
  this.nextMonth_ = nextMonth;
  if (this.decorator_) {
    this.decorator_.call(null, this);
  }
  this.addListeners_();
}

/** reruns the decorator on the picker */
DP_DatePicker.prototype.refresh = function() {
  if (this.decorator_) {
    this.decorator_.call(null, this);
  }
}

DP_DatePicker.prototype.addSelectionListener = function(listener) {
  return this.selectionListeners_.add(listener);
}

DP_DatePicker.prototype.removeSelectionListener = function(listener) {
  return this.selectionListeners_.remove(listener);
}

/**
 * Clears any selected days from the calendar.
 *
 * @param {Boolean} opt_fireEvent indicating whether the selection event should
 *                             be fired. Defaults to true.
 */
DP_DatePicker.prototype.clearSelections = function(opt_fireEvent) {
  opt_fireEvent = (arguments.length === 0 || opt_fireEvent);

  var selectedArray = this.selectedDates_.asArray();
  for(var i = 0; i < selectedArray.length; ++i) {
    var cell = this.dateToCellMap_[selectedArray[i].toString()];
    this.setDotSelection_(cell, false);
  }
  this.selectedDates_.clear();

  if (!this.inDrag_) {
    this.setDragStartCell(null);
    this.setDragEndCell(null);
  }
  if (opt_fireEvent) this.fireSelectionEvent();
}

/** @return true if the date is selected in the picker */
DP_DatePicker.prototype.isSelected = function(/*ICAL_Date*/ date) {
  AssertType(date, ICAL_Temporal);
  return this.selectedDates_.contains(date);
}

/**
 * Responds to a user clicking on a cell in the calendar.
 *
 * @param {string} cellId   The id of the table cell that was clicked
 *
 * @private
 */
DP_DatePicker.prototype.cellClicked_ = function(cellId) {
  if (this.clickFunc_) {
    this.clickFunc_.call(null, cellId);
    return;
  }

  var cell = forid(cellId);
  var dates = this.selectedDates_;

  switch(this.selectionMode_) {

    case DP_SELECTION_RANGE:
      var clickMode = this.getClickMode();
      if (clickMode == DP_CLICK_NONE) break;
      if (clickMode != DP_CLICK_1_DAY
             && (clickMode != DP_CLICK_PRESERVE_RANGE
                 || !dates.contains(this.id2DateMap_[cellId]))) {
        // if click mode is CLICK_1_DAY, or is CLICK_PRESERVE_RANGE and
        // user clicked within the selected range, fallthru to
        // DP_SELECTION_SINGLE_DAY case;
        // otherwise, handled by one of the following range cases:
        var startDate = this.id2DateMap_[cell.id];
        var duration;
        switch (clickMode) {
          case DP_CLICK_PRESERVE_RANGE:
          // select a range of equal length, including the clicked date
            if (dates.getSize() > 7  && this.isSnapToWeek()) {
              // parse week# out of id
              var week = cell.id.substr(cell.id.length - 3, 1);
              startDate = this.id2DateMap_[this.id_ + 'day_' + week + '_0'];
            }
            duration = dates.getSize() - 1;
            break;
          case DP_CLICK_WEEK:
          // select week that the day falls on
            // parse week# out of id
            var week = cell.id.substr(cell.id.length - 3, 1);
            startDate = this.id2DateMap_[this.id_ + 'day_' + week + '_0'];
            duration = 6;
            break;
          case DP_CLICK_MONTH:
          // select entire month that the day belongs to
            startDate = this.id2DateMap_[cell.id];
            startDate = ICAL_Date.create(startDate.year, startDate.month, 1);
            var builder = ical_builderCopy(startDate);
            duration = ICAL_daysInMonth(startDate.year, startDate.month) - 1;
            break;
          default:
            Fail("Invalid click mode: " + clickMode);
        }
        AssertTrue(duration !== undefined, "duration should have been defined");
        var builder = ical_builderCopy(startDate);
        builder.date += duration;
        var endDate = builder.toDate();
        this.setSelection(startDate, endDate);
        return; // break out of DP_SELECTION_RANGE case
      }
      AssertTrue(clickMode == DP_CLICK_1_DAY
                   || (clickMode == DP_CLICK_PRESERVE_RANGE
                           && dates.contains(this.id2DateMap_[cellId])),
                 "not a case for single date selection");
      this.clearSelections(false); // fallthru to single date selection

    case DP_SELECTION_SINGLE_DAY:
      // in this mode, this.selectedDates_ has at most one element
      if (dates.getSize() > 0) {
        var oldDate = dates.asArray()[0];
        dates.remove(oldDate);
        var oldCell = this.dateToCellMap_[oldDate.toString()];
        if (oldCell) this.setDotSelection_(oldCell, false);
      }
      dates.add(this.id2DateMap_[cell.id]);
      this.setDotSelection_(cell);
      this.fireSelectionEvent(this.id2DateMap_[cell.id]);
      break;

    case DP_SELECTION_MULTI_DAY:
      // NOTE: implement for multiday
      break;
    case DP_SELECTION_NONE: //fallthru
    default:
      // no event handlers to add
      break;
  }
}

DP_DatePicker.prototype.setDragStartCell = function(cell) {
  this.dragStartCell_ = cell;
  this.rangeStartDate_ = (cell) ? this.id2DateMap_[cell.id] : null;
}

DP_DatePicker.prototype.setDragEndCell = function(cell) {
  this.dragEndCell_ = cell;
  this.rangeEndDate_ = (cell) ? this.id2DateMap_[cell.id] : null;
}

/** @return boolean indicating if a drag is going on in the datepicker */
DP_DatePicker.prototype.inDrag = function() {
  return this.inDrag_;
}

/**
 *
 * @private
 */
DP_DatePicker.prototype.startDrag = function(event, el) {
  this.clearSelections(false);
  this.inDrag_ = true;
  this.setDragStartCell(this.eventToCell(event));

  var date = this.id2DateMap_[this.dragStartCell_.id];
  this.selectedDates_.add(date);
  this.setDotSelection_(this.dragStartCell_);
  this.setSelection(date);
  this.setDragEndCell(this.dragStartCell_);
}

/**
 *
 * @private
 */
DP_DatePicker.prototype.finishDrag = function(event, node, type) {
  this.inDrag_ = false;
  this.dragEndCell_ = this.eventToCell(event);
  // special case for snap to week;
  // date range may extend beyond area that was dragged
  if (this.isSnapToWeek()) {
    this.fireSelectionEvent(this.rangeStartDate_,
                            this.rangeEndDate_,
                            false);
    return;
  }
  var startDate, endDate;
  if (this.id2DateMap_[this.dragEndCell_.id].getComparable() <
        this.id2DateMap_[this.dragStartCell_.id].getComparable()) {
    startDate = this.id2DateMap_[this.dragEndCell_.id];
    endDate = this.id2DateMap_[this.dragStartCell_.id];
  } else {
    startDate = this.id2DateMap_[this.dragStartCell_.id];
    endDate = this.id2DateMap_[this.dragEndCell_.id];
  }
  this.fireSelectionEvent(startDate, endDate, false);
}

/**
 *
 * @private
 */
DP_DatePicker.prototype.handleDragSegment = function(event, el, type, delta) {
  // Need to pass clonedEvent instead of event to
  // the anonymous function called by the timeout below because
  // it appears that the event object is shared
  // and by the time the function is invoked by the timeout, the values
  // of the event may not be the same ones that were
  // set when it was passed to this handleDragSegment().
  //
  // The event is "cloned" in this manner rather than
  // using CloneObject() in common.js because that function
  // suffered from infinite recursion when called on event.
  // clonedEvent needs these fields for GetMousePosition()
  this.lastDragEvent_ = CloneEvent(event);

  if (this.selectionMode_ != DP_SELECTION_RANGE
      || this.ignoringDrags_) return;
  this.ignoringDrags_ = true;
  var picker = this;
  window.setTimeout(function() {
    try {
      if (picker.inDrag_) {
        picker.finishDragAnonymous.call(picker, el, type, delta);
      }
    } finally {
      picker.ignoringDrags_ = false;
    }
  }, 50); // needs to be less than 100ms so perceptual fusion is not broken
}

/**
 * If snapToWeek is true, then if a user selects 8 or more days, then
 * the selection will grow to include the smallest number of
 * complete weeks that includes those days.
 */
DP_DatePicker.prototype.setSnapToWeek = function(snapToWeek) {
  if (this.snapToWeek_ == snapToWeek) return;
  this.snapToWeek_ = !!snapToWeek;
  this.clearSelections();
}

DP_DatePicker.prototype.isSnapToWeek = function() {
  return this.snapToWeek_;
}

/**
 *
 * @private
 */
DP_DatePicker.prototype.finishDragAnonymous = function(el, type, delta) {
  var event = this.lastDragEvent_;

  var newDragEnd = this.eventToCell(event);
  if (newDragEnd === this.dragEndCell_) return;
  var oldDragEnd = this.dragEndCell_;
  this.setDragEndCell(newDragEnd);
  AssertTrue(oldDragEnd != newDragEnd);

  var dateMap = this.id2DateMap_;
  // rightDrag: true if drag is from earlier date to later date
  var rightDrag = dateMap[oldDragEnd.id].getComparable() <
                      dateMap[newDragEnd.id].getComparable();
  var oldBeforeAnchor = dateMap[oldDragEnd.id].getComparable() <
                            dateMap[this.dragStartCell_.id].getComparable();
  var newBeforeAnchor = dateMap[newDragEnd.id].getComparable() <
                            dateMap[this.dragStartCell_.id].getComparable();
  var anchorBeforeNew = dateMap[this.dragStartCell_.id].getComparable() <
                            dateMap[newDragEnd.id].getComparable();
  var anchorBeforeOld = dateMap[this.dragStartCell_.id].getComparable() <
                            dateMap[oldDragEnd.id].getComparable();

  var start, end;
  var startDate, endDate;
  var sCell = (newBeforeAnchor) ? this.dragEndCell_ : this.dragStartCell_;
  var eCell = (newBeforeAnchor) ? this.dragStartCell_ : this.dragEndCell_;
  if (this.isSnapToWeek()) {
    var duration = ICAL_daysBetweenDates(dateMap[eCell.id], dateMap[sCell.id]);
    if (duration >= 7) {
      var startWeek, endWeek;
      startWeek = parseInt(sCell.id.charAt(sCell.id.length - 3), 10);
      endWeek   = parseInt(eCell.id.charAt(eCell.id.length - 3), 10);
      sCell = forid(this.id_ + 'day_' + startWeek + '_0');
      eCell = forid(this.id_ + 'day_' + endWeek   + '_6');
    }
    // TODO(bolinfest): more conservative dot range setting
    this.setDotRange(this.firstCell_, sCell, false);
    this.setDotRange(eCell, this.lastCell_, false);
    this.setDotRange(sCell, eCell, true);
    this.rangeStartDate_ = dateMap[sCell.id];
    this.rangeEndDate_ = dateMap[eCell.id];
  } else {
    if (rightDrag) { // drag from earlier date to later date
      if (oldBeforeAnchor) {
        end = (newBeforeAnchor) ? this.id2PrevMap_[newDragEnd.id]
                                : this.id2PrevMap_[this.dragStartCell_.id];
        this.setDotRange(oldDragEnd, end, false);
      }
      if (anchorBeforeNew) {
        start = (anchorBeforeOld) ? this.id2NextMap_[oldDragEnd.id]
                                  : this.id2NextMap_[this.dragStartCell_.id];
        this.setDotRange(start, newDragEnd, true);
      }
    } else { // drag from later date to earlier date
      if (anchorBeforeOld) {
        start = (anchorBeforeNew) ? this.id2NextMap_[newDragEnd.id]
                                  : this.id2NextMap_[this.dragStartCell_.id];
        this.setDotRange(start, oldDragEnd, false);
      }
      if (newBeforeAnchor) {
        end = (newBeforeAnchor) ? this.id2PrevMap_[this.dragStartCell_.id]
                                : this.id2PrevMap_[oldDragEnd.id];
        this.setDotRange(newDragEnd, end, true);
      }
    }
  }
  startDate = dateMap[sCell.id];
  endDate = dateMap[eCell.id];
  this.fireSelectionEvent(startDate, endDate, true);
}

/**
 * !!!requires: startCell.getComparable() <= endCell.getComparable()
 *
 * @private
 */
DP_DatePicker.prototype.setDotRange = function(startCell, endCell, selected) {
  AssertTrue(startCell);
  AssertTrue(endCell);
  AssertType(selected, Boolean);

  var originalStart = startCell;

  var changed = false;
  while(startCell) {
    if (selected) {
      changed = this.selectedDates_.add(this.id2DateMap_[startCell.id]);
    } else {
      changed = this.selectedDates_.remove(this.id2DateMap_[startCell.id]);
    }
    if (changed) {
      this.setDotSelection_(startCell, selected);
    }
    if (startCell.id === endCell.id) break;
    startCell = this.id2NextMap_[startCell.id];
    AssertTrue(startCell, "did not find endCell: " + endCell.id);
  }
}

DP_DatePicker.LAST_DAY_OF_WEEK = {
  4 : 'day_3_6',
  5 : 'day_4_6',
  6 : 'day_5_6',
  7 : 'day_6_6'
};

/**
 * On Internet Explorer, there may be a discrepancy between the
 * coordinate of a mouse event and the position of the DatePicker.
 * This can cause problems when mapping a mouse event to a DP cell.
 * To compensate for this, the client may set a "fudge factor"
 * that takes the Point from a MouseEvent, and may mutate it, as
 * necessary.
 *
 * <p>For example, on Doozer, the nodeBounds() for the global datepicker
 * (gDatePicker), is off by 7 pixels on both axes in IE.
 * setFudgeFactor is used to accomodate for this as follows:
 *
 * <pre>if (BR_IsIE()) gDatePicker.setFudgeFactor({x : -7, y : -7});</pre>
 *
 * @param fudgeFactor with Number x and Number y, indicating the translation,
 *        in pixels, that should be applied to a MouseEvent
 */
DP_DatePicker.prototype.setFudgeFactor = function(/*Object*/ fudgeFactor) {
  if (fudgeFactor) {
    this.fudgeFactor_ = {};
    this.fudgeFactor_.x = fudgeFactor.x;
    this.fudgeFactor_.y = fudgeFactor.y;
  } else {
    this.fudgeFactor_ = null;
  }
}

/**
 * Take a Point or Rect and mutate it by applying the translation
 * present in the fudge factor (see setFudgeFactor()). If no fudge factor
 * exists, then pointOrRect will not be mutated. If opt_inverse is true,
 * then the opposite translation will be applied, i.e., the x and y
 * values of pointOrRect will be translated by -fudgeFactor.x and
 * -fudgeFactor.y, respectively.
 * @param pointOrRect {Point|Rect}
 * @param opt_inverse if true, translates in the opposite direction,
 *        defaults to false
 */
DP_DatePicker.prototype.applyFudgeFactor_ = function(pointOrRect, opt_inverse){
  if (!this.fudgeFactor_) return;
  if (opt_inverse) {
    pointOrRect.x -= this.fudgeFactor_.x;
    pointOrRect.y -= this.fudgeFactor_.y;
  } else {
    pointOrRect.x += this.fudgeFactor_.x;
    pointOrRect.y += this.fudgeFactor_.y;
  }
}

/**
 * Convert a mouse event into a table cell.
 */
DP_DatePicker.prototype.eventToCell = function(event) {
  var first = nodeBounds(this.firstCell_);
  var tableRect = this.getTableRect_();
  var p = GetMousePosition(event);
  this.applyFudgeFactor_(p);

  // NOTE: change if there is a 5-day mode
  var numDays = 7;

  var binX = this.findBin(tableRect.x, first.w, numDays, p.x);
  var binY = this.findBin(tableRect.y, first.h, this.numWeeks_, p.y);
  return forid(this.id_ + 'day_' + binY + '_' + binX);
}

/** @private */
DP_DatePicker.prototype.findBin = function(start, interval, numBins, coord) {
  if (coord < start) return 0;
  var index = Math.floor((coord - start) / interval);
  return (index >= numBins) ? (numBins - 1) : index;
}

/** @private */
DP_DatePicker.prototype.getTableRect_ = function() {
  //TODO(bolinfest): cache table rect?
  var id = this.id_;
  var numWeeks = this.numWeeks_;
  var first = nodeBounds(this.firstCell_);
  var last = nodeBounds(forid(id + DP_DatePicker.LAST_DAY_OF_WEEK[numWeeks]));
  // tableRect is the bounding box for the rows of the table containing days
  // so it does not include the month/day header stuff
  return new Rect(first.x,
                  first.y,
                  last.x + last.w - first.x,
                  last.y + last.h - first.y,
                  first.coordinateFrame);
}

/**
 * opt_long boolean true if should display full month name
 */
// TODO(bolinfest): create a general library for formatting dates
// and use it instead of this
DP_DatePicker.prototype.formatDate = function(date, opt_long) {
  var monthArray = (opt_long) ? FULL_MONTHS : MONTHS;
  return monthArray[date.month] + ' ' + date.date;
}

/* @private */
DP_DatePicker.prototype.fireSelectionEvent = function(opt_date1,
                                                      opt_date2,
                                                      opt_inDrag) {
  var event = {};
  event.startDate = opt_date1;
  event.endDate = opt_date2 || opt_date1;
  event.inDrag = !!opt_inDrag;
  event.mode = this.getSelectionMode();
  this.selectionListeners_.fireEvent(event);
}

/**
 * Return the ICAL_Date that is considered "today"
 * by the calendar. May be different from today's date
 * if it was set differently in the DP_DatePicker constructor.
 */
DP_DatePicker.prototype.getToday = function() {
  return this.today_;
}

/**
 * Sets the current date so it can be displayed correctly in the picker.
 * If the value of "today" has changed, populateHtml() will be called.
 *
 * @param today {ICAL_Date}
 */
DP_DatePicker.prototype.setToday = function(today) {
  AssertType(today, ICAL_Date, 'today is not an ICAL_Date');
  if (today.equals(this.today_)) return;
  return this.today_ = today;
  this.populateHtml();
}

/**
 * Take an ICAL_Date or an ICAL_DateTime and return
 * it as an ICAL_Date. This is needed for some functions
 * that are not clear which type of ICAL object they
 * are receiving, but want to use an ICAL_Date only.
 */
DP_DatePicker.prototype.asICAL_Date = function(date) {
  if (date instanceof ICAL_Date) return date;
  if (date instanceof ICAL_DateTime) {
    return ICAL_Date.create(date.year, date.month, date.date);
  } else {
    AssertTrue(false, "Invalid arg: " + date);
  }
}

/**
 * @param opt_start {ICAL_Date} inclusive if present
 * @param opt_end {ICAL_Date} inclusive if present
 * @param opt_fireEvent boolean
 *                 defaults to true
 */
DP_DatePicker.prototype.setSelection = function(opt_start, opt_end, opt_fireEvent) {
  var mode = this.getSelectionMode();
  opt_fireEvent = (opt_fireEvent !== false);

  // clean up inputs
  if (opt_start) opt_start = this.asICAL_Date(opt_start);
  if (opt_end) opt_end = this.asICAL_Date(opt_end);

  // make sure month for start is in view
  if (opt_start) this.showDate(opt_start);
  if (!opt_start || mode == DP_SELECTION_NONE) {
    this.clearSelections(opt_fireEvent);
    return;
  }
  if (mode == DP_SELECTION_SINGLE_DAY) {
    this.clearSelections(false);
    var cell = this.dateToCellMap_[opt_start.toString()];
    this.selectedDates_.add(opt_start);
    this.setDotSelection_(cell);
    if (opt_fireEvent) this.fireSelectionEvent(opt_start);
  } else if (mode == DP_SELECTION_RANGE) {
    if (!opt_end) opt_end = opt_start;
    var duration = ICAL_daysBetweenDates(opt_end, opt_start);
    var clearRequired = false;
    if (this.isSnapToWeek() && duration >= 7) {
      var sDOW = ICAL_getDayOfWeek(opt_start) + 7;
      var eDOW = ICAL_getDayOfWeek(opt_end) + 7;
      sDOW = (sDOW - this.getFirstDayOfWeek()) % 7;
      eDOW = (eDOW - this.getFirstDayOfWeek()) % 7;
      var builder;
      builder = ical_dateBuilder(opt_start.year,
                                 opt_start.month,
                                 opt_start.date - sDOW);
      opt_start = builder.toDate();
      builder = ical_dateBuilder(opt_end.year,
                                 opt_end.month,
                                 opt_end.date + (6 - eDOW));
      opt_end = builder.toDate();
      clearRequired = this.showDate(opt_start);
    }
    if (clearRequired) {
      this.clearSelections(false);
    }

    var cell = this.firstCell_; //this.dateToCellMap_[opt_start];
    this.rangeStartDate_ = opt_start;
    this.rangeEndDate_ = opt_end;
    var endCell = this.lastCell_; //this.dateToCellMap_[opt_end];
    var startComp = opt_start.getComparable();
    var endComp = opt_end.getComparable();
    for ( ; cell; cell = this.id2NextMap_[cell.id]) {

      // find [oldSet - newSet] and remove dot
      // find [newSet - oldSet] and add dot
      // set oldSet = newSet

      var curDate = this.id2DateMap_[cell.id];
      var oldStatus = this.selectedDates_.contains(curDate);
      var newStatus = (curDate.getComparable() >= startComp) &&
                      (curDate.getComparable() <= endComp);
      //if (!this.selectedDates_.contains(curDate)) {
      if (oldStatus != newStatus) {
        if (newStatus) {
          this.selectedDates_.add(curDate);
          this.setDotSelection_(cell);
        } else {
          this.selectedDates_.remove(curDate);
          this.setDotSelection_(cell, false);
        }
      }
      if (cell === endCell) break;
    }
    if (!cell) { // more cells to be selected in next month
       // TODO(bolinfest): this.lastCell_ incorrect if 5,6 rows dipslayed
       endCell = this.lastCell_;
       var builder = ical_builderCopy(this.id2DateMap_[this.lastCell_.id]);
       var nextDate = null;
       do {
         builder.date += 1;
         nextDate = builder.toDate();
         this.selectedDates_.add(nextDate);
       } while (!nextDate.equals(opt_end));
    }
    if (opt_fireEvent) this.fireSelectionEvent(opt_start, opt_end);
  }
}

/** Make sure that DatePicker is centered around date.month
  * @param date ICAL_Date
  * @return true if we actually change
  */
DP_DatePicker.prototype.showMonth = function(date, opt_fireEvent) {
  if (this.dispDate_.month == date.month
         && this.dispDate_.year == date.year) return false;
  opt_fireEvent = (arguments.length == 1) || opt_fireEvent;
  this.dispDate_ = ICAL_Date.create(date.year, date.month, 1);
  this.populateHtml();
  if (opt_fireEvent) this.monthChangeListeners_.fireEvent();
  return true;
}

/** Make sure that the date is visible within the DatePicker
  * This function is more lightweight that showMonth. If the date
  * is visible near the begining or end, it will not scroll.
  *
  * ie - suppose we are viewing november, but Oct 28-31 is visible
  * near the top. If we pass Oct 29 to showdate, then it will
  * not change the view. In contrast, ShowMonth would recenter on October.
  * @param date ICAL_Date
  * @return true if we actually changed
  */
DP_DatePicker.prototype.showDate = function(date, opt_fireEvent) {
  if (date.getComparable() >= this.viewableStartDate_.getComparable() &&
      date.getComparable() <= this.viewableEndDate_.getComparable()) {
    return false;
  }

  return this.showMonth(date, opt_fireEvent);
}

DP_DatePicker.prototype.getSelection = function() {
  switch(this.getSelectionMode()) {
    case DP_SELECTION_SINGLE_DAY:
      if (this.selectedDates_.getSize()) {
        return this.selectedDates_.asArray()[0];
      } else {
        return null;
      }
    case DP_SELECTION_RANGE:
      var start = (this.rangeStartDate_) ? this.rangeStartDate_ : null;
      var end   = (this.rangeEndDate_)   ? this.rangeEndDate_   : null;
      if (!start || !end) return null;
      return [start, end];
    case DP_SELECTION_MULTI_DAY:
      // NOTE: implement for multiday
      return null;
    case DP_SELECTION_NONE: // fallthru
    default:
      return null;
  }
}

/** @return the number of days that are selected */
DP_DatePicker.prototype.getNumDaysSelected = function() {
  return this.selectedDates_.getSize();
}

/**
 * Displays a string of text below the calendar,
 * often a description of what has been selected.
 *
 */
DP_DatePicker.prototype.printStatus = function(str) {
  if (this.showSelection_)  {
    forid(this.id_ + /* SAFE */ 'sel').innerHTML = str;
  }
}

/**
 *
 * cell    Element is the <TD> to be highlighted
 * opt_on  boolean indicating whether the cell should be highlighted
 *           defaults to true (should be selected)
 *
 * @private
 */
DP_DatePicker.prototype.setDotSelection_ = function(cell, opt_on) {
  if (!cell) return;
  if (!IsDefined(opt_on)) opt_on = true;
  var oldClass = [];
  var newClass = [];
  var cellClass = ' ' + cell.className + ' ';
  var dpClass = ' ' + this.class_;
  if (opt_on) /* SAFE */ {
    if (-1 != cellClass.indexOf(dpClass + 'today ')) {
      oldClass.push(dpClass + 'today ');
      newClass.push(dpClass + 'today_selected ');
    }
    if (-1 != cellClass.indexOf(dpClass + 'weekday ')) {
      oldClass.push(dpClass + 'weekday ');
      newClass.push(dpClass + 'weekday_selected ');
    } else if (-1 != cellClass.indexOf(dpClass + 'weekend ')) {
      oldClass.push(dpClass + 'weekend ');
      newClass.push(dpClass + 'weekend_selected ');
    }
  } else /* SAFE */ {
    if (-1 != cellClass.indexOf(dpClass + 'today_selected ')) {
      newClass.push(dpClass + 'today ');
      oldClass.push(dpClass + 'today_selected ');
    }
    if (-1 != cellClass.indexOf(dpClass + 'weekday_selected ')) {
      newClass.push(dpClass + 'weekday ');
      oldClass.push(dpClass + 'weekday_selected ');
    } else if (-1 != cellClass.indexOf(dpClass + 'weekend_selected ')) {
      newClass.push(dpClass + 'weekend ');
      oldClass.push(dpClass + 'weekend_selected ');
    }
  }
  for (var i = 0; i < oldClass.length; ++i) {
    cellClass = cellClass.replace(oldClass[i], newClass[i]);
  }
  if (oldClass.length != 0) {
    cell.className = cellClass;
  }
}

DP_DatePicker.prototype.addMonthChangeListener = function(listener) {
  this.monthChangeListeners_.add(listener);
}

DP_DatePicker.prototype.removeMonthChangeListener = function(listener) {
  this.monthChangeListeners_.remove(listener);
}

// This is a static cache from string ID to the DatePicker objects
DP_DatePicker.dp_cache_ = new Object();

DP_DatePicker.prototype.GetId = function() {
  return this.id_;
}

/** @return the picker identified by the id from GetId() */
DP_DatePicker.staticGetPickerById = function(dpId) {
  return DP_DatePicker.dp_cache_[dpId];
}

// The next 3 are static functions which can trap DOM0 style events
// They don't leak and require no DOM manipulation to setup.
function DP_staticPrevMonth_(dpId) {
  var dp = DP_DatePicker.dp_cache_[dpId];
  return dp.showMonth(dp.prevMonth_);
}

function DP_staticNextMonth_(dpId) {
  var dp = DP_DatePicker.dp_cache_[dpId];
  return dp.showMonth(dp.nextMonth_);
}

function DP_staticCellClicked_(cell) {
  var id = cell.id;
  var match = id.match(/(.*)day_\d+_\d+/);
  var dp = DP_DatePicker.dp_cache_[match[1]];
  return dp.cellClicked_(id);
}

/**
 * Minimum distance user must drag the mouse (in pixels) to
 * be considered a drag rather than a click.
 */
var DP_DRAG_THRESHOLD = 5;

/**
 * Adds the mouse listeners for range selection
 * Called by populateHtml.
 *
 * @private
 */
DP_DatePicker.prototype.addListeners_ = function() {

  // Make sure that this is only called once per instance
  if (this.listenerAdded_ === true) return;
  this.listenerAdded_ = true;

  var id = this.id_;
  var datepicker = this;
  var div = this.div_;


  // If a user drags the mouse slightly upon clicking,
  // then it should be interpreted as a click rather than a drag.
  // Length of drag is compared against DP_DRAG_THRESHOLD
  // to determine whether a drag or a click occurred.
  DD_RegisterHandler(function (node) {
    if (datepicker.selectionMode_ == DP_SELECTION_RANGE
          && node === div) {
      var handler = new DD_BaseSelectionHandler(node);
      // set to true if the user drags past a certain threshold
      handler.escapedStartPoint = false;
      handler.startDrag = function(event, el) {
        // make sure is in date range
        var point = GetMousePosition(event);
        var p1 = nodeBounds(datepicker.firstCell_);
        var p2 = nodeBounds(forid(id +
          DP_DatePicker.LAST_DAY_OF_WEEK[datepicker.numWeeks_]));
        if (point.x < p1.x
            || point.x >= p2.x + p2.w
            || point.y < p1.y
            || point.y >= p2.y + p2.h) return false;
        // record the point and event that started the drag
        this.startPoint = point.clone();
        this.startEvent = event;
        var lasso = forid(id + /* SAFE */'lasso');
        // create a lasso if none present
        if (!lasso) {
          lasso = document.createElement(/* SAFE */ 'div');
          lasso.id = id + /* SAFE */ 'lasso';
          lasso.style.position = /* SAFE */ 'absolute';
          lasso.style.display = /* SAFE */ 'none';
          document.body.appendChild(lasso);
        }
        dd_dragType = DD_SELECT;
        dd_axisMask = DD_XY_AXES;
        dd_dragElement = lasso;
        return true;
      };
      handler.handleDragSegment = function(event, el, type, delta) {
        if (!this.escapedStartPoint) {
          // if the user has not dragged very far, he may be trying to click
          var dist = Distance(this.startPoint, GetMousePosition(event));
          if (dist > DP_DRAG_THRESHOLD) {
            this.escapedStartPoint = true;
            // we have verified that this a drag, so call startDrag()
            datepicker.startDrag.call(datepicker, this.startEvent, el);
          } else {
            // not a drag yet, so exit so handleDragSegment() is not called
            return true;
          }
        }
        datepicker.handleDragSegment.apply(datepicker, arguments);
        return true;
      };
      handler.finishDrag = function(event, node, type) {
        if (this.escapedStartPoint) {
          // this was a drag, so call finishDrag()
          datepicker.finishDrag.apply(datepicker, arguments);
        } else {
          // mouse was not moved far enough to merit a drag,
          // so behave as if it were a click
          if (this.startPoint) {
            var id = datepicker.eventToCell(this.startEvent).id;
            datepicker.cellClicked_.call(datepicker, id);
          }
        }
        return true;
      };
      return handler;
    } else {
      return undefined;
    }
  });

}

DP_DatePicker.prototype.addLogger = function(func) {
  return this.loggers_.add(func);
}

DP_DatePicker.prototype.removeLogger = function(func) {
  return this.loggers_.remove(func);
}

/**
 *
 * @private
 */
DP_DatePicker.prototype.log = function() {
  this.loggers_.fireEvent(arguments);
}

DP_DatePicker.prototype.getDiv = function() {
  return this.div_;
}

// TODO Should DateSet become part of ical.js?

/**
 * A DateSet is a set of ICAL_Date objects.
 *
 * It takes advantage of the JavaScript associative array for hashing.
 * The key for each ICAL_Date is the first 9 characters of its .toString()
 * and the value is the ICAL_Date itself. The first 9 characters are used
 * in the event that an ICAL_DateTime is inserted.
 */
function DateSet() {
  this.dates_ = {}; /*<string,ICAL_Date>*/
  this.size_ = 0;
}

/** @return the number of elements in this set */
DateSet.prototype.getSize = function() {
  return this.size_;
}

/** @return true if date has been added; false if already contained */
DateSet.prototype.add = function(date) {
  var key = this.validateInput_(date);
  if (key in this.dates_) return false;
  this.dates_[key] = date.toDate();
  ++this.size_;
  return true;
}

/** @return true if date has been removed; false if was not already present */
DateSet.prototype.remove = function(date) {
  var key = this.validateInput_(date);
  if (!(key in this.dates_)) return false;
  delete this.dates_[key];
  --this.size_;
  return true;
}

/** remove all entries from the set */
DateSet.prototype.clear = function(date) {
  this.dates_ = {};
  this.size_ = 0;
}

/** @return true if this set contains the date; false otherwise */
DateSet.prototype.contains = function(date) {
  var key = this.validateInput_(date);
  return (key in this.dates_);
}

/**
 * WARNING: If a client mutates an element in this
 * array, it will destroy the representation of the DateSet.
 * Returning a copy of each date is probably too expensive,
 * so it is the client's responsibility not to break the rep!
 *
 * @return each ICAL_Date as an element in an array
 */
DateSet.prototype.asArray = function() {
  var arr = new Array(this.getSize());
  var i = -1;
  for (var key in this.dates_) arr[++i] = this.dates_[key];
  return arr;
}

/** @private */
DateSet.prototype.validateInput_ = function(date) {
  AssertTrue((date instanceof ICAL_Date)
             || (date instanceof ICAL_DateTime),
             'expected a date or datetime: ' + date);
  return date.toString().substr(0, 9);
}

// TODO Is ListenerList useful enough to belong to /javascript/common?

/**
 * A list of listeners where each element in the list is referentially
 * different from every other listener.
 * Each listener is a Function, so a client can call fireEvent()
 * with any arguments and each function in ListenerList will be applied
 * to those arguments with opt_receiver as the receiver.
 *
 * @param opt_receiver the object that should be the receiver when each
 *                     listener in the list is applied to the
 *                     arguments supplied to fireEvent()
 */
function ListenerList(opt_receiver) {
  this.receiver_ = opt_receiver;
  this.listeners_ = [];
}

/**
 * @param {function} listener is a function to be
 *                   called when the client chooses to fireEvent()
 *
 * @return boolean indictating if the add was successful
 */
ListenerList.prototype.add = function(listener) {
  AssertType(listener, Function);
  if (!listener) return false;
  for (var i = 0; i < this.listeners_.length; ++i) {
    if (listener === this.listeners_[i]) return false;
  }
  this.listeners_.push(listener);
  return true;
}

/**
 * @return boolean indictating if the remove was successful
 */
ListenerList.prototype.remove = function(listener) {
  if (!listener) return false;
  for (var i = 0; i < this.listeners_.length; ++i) {
    if (listener === this.listeners_[i]) {
      this.listeners_.splice(i, 1);
      return true;
    }
  }
  return false;
}

ListenerList.prototype.fireEvent = function() {
  for (var i = 0; i < this.listeners_.length; ++i) {
    this.listeners_[i].apply(this.receiver_, arguments);
  }
}

/**
 * @return integer number of listeners in the list
 */
ListenerList.prototype.getSize = function() {
  return this.listeners_.length;
}

ListenerList.prototype.iterator = function() {
  return new ListenerList_Iterator(this);
}

function ListenerList_Iterator(listenerList) {
  this.list_ = listenerList;
  this.index_ = 0;
  this.current_ = null;
}

ListenerList_Iterator.prototype.hasNext = function() {
  return this.index_ < this.list_.getSize();
}

ListenerList_Iterator.prototype.next = function() {
  if (this.hasNext()) {
    this.current_ = this.list_.listeners_[this.index_++];
  } else {
    this.current_ = null;
  }
  return this.current_;
}

ListenerList_Iterator.prototype.current = function() {
  return this.current_;
}

ListenerList_Iterator.prototype.remove = function() {
  if (!this.current_) throw new Error(/* SAFE */ "no current element!");
  this.list_.remove(this.current_);
  this.current_ = null;
  --this.index_;
}

    // ------------------------------------------------------------------
    // Add zero before the number if it's between 0 and 9, inclusively
    // ------------------------------------------------------------------
    function PrependZero(x) {
      return (x < 0 || x > 9? "" : "0") + x;
    }

    // ------------------------------------------------------------------
    // formatDate (date_object, format)
    // Returns a date in the output format specified.
    // The format string uses the same abbreviations as in getDateFromFormat()
    // ------------------------------------------------------------------
    function formatDate(date,format) {
      format=format+"";
      var result="";
      var i_format=0;
      var c="";
      var token="";
      var y=date.year+"";
      var M=date.month;
      var d=date.date;
      var yyyy,yy,MM,dd;
      // Convert real date parts into formatted versions
      var value=new Object();
      if (y.length < 4) {
        y=""+(y-0+1900);
      }
      value["y"]=""+y;
      value["yyyy"]=y;
      value["yy"]=y.substring(2,4);
      value["M"]=M;
      value["MM"]=PrependZero(M);
      value["MMM"]=MONTHS[M];
      value["d"]=d;
      value["dd"]=PrependZero(d);
      while (i_format < format.length) {
        c=format.charAt(i_format);
        token="";
        while ((format.charAt(i_format)==c) && (i_format < format.length)) {
          token += format.charAt(i_format++);
        }
        if (value[token] != null) {
         result=result + value[token];
        } else {
         result=result + token;
        }
      }
      return result;
    }


    // ------------------------------------------------------------------
    // Date output format is localized to the default format pattern
    // Replaced the same function in datepicker_helper.js
    // ------------------------------------------------------------------
    function googleDateToString(date) {
      var dateFormat = getDefaultDateFormat();
      var dateString = formatDate(date, dateFormat);
      return dateString;
    }
    
    // ------------------------------------------------------------------        
    // Parse user specified date using the javascript date parser 
    // Use the default date format to preprocess dates w/ 2 digit years.
    // Use the loose date parser as a last resort.
    // ------------------------------------------------------------------        
    function parseInputDate(localDateString) {
      var dateFormat = getDefaultDateFormat().toLowerCase();

      // Parsing dates with the short year format is 
      // not possible without knowing the date format.
      // Neither Date.parse nor LDP_ParseInputDate
      // can parse these dates correctly.
      if (dateFormat.indexOf("yyyy") < 0) {
        var dateString;

        // taken from LDP_ParseInputDate() 
        dateString = localDateString
            // remove leading and trailing whitespace
            .replace(/^\s+/, '').replace(/\s+$/, '')
            // make sure there is space between numbers and letters so that 1Jan2005
            // becomes 1 Jan 2005 -- 3 tokens
            .replace(/([0-9]+)([a-zA-Z]+)/g, '$1 $2')
            .replace(/([a-zA-Z])([0-9])/g, '$1 $2');

        // split into tokens on punctuation and space, leaving the delimiters in there
        var parts = dateString.split(/\b|_/);

        // try to figure out where the year value is stored in the 'parts' array
        var yearIdx = 1; // guess at the middle
        if (dateFormat.substring(0,1) == "y") {
          yearIdx = 0; // beginning
        }
        else if (dateFormat.substring(dateFormat.length-1) == "y") {
          yearIdx = parts.length - 1; // end
        } else {
          // if the year is somewhere in the middle, then 
          // take the first numeric value that matches.
          // NOTE: this doesn't handle 3 digit year formats.
          for (var i = 1, l = parts.length-1; i < l; i++) {
            if (parts[i].match(/^[0-9]{1,2}$/)) {
              yearIdx = i; 
              break;
            }
          }
        }

        // try to convert the year to 4 digit format
        if (yearIdx < parts.length && parts[yearIdx].match(/^[0-9]{1,2}$/)) {
          var year = parseInt(parts[yearIdx], 10);
          parts[yearIdx] = ((year < 50) ? "20" : "19") + parts[yearIdx];

          // recreate the date using a 4 digit year
          localDateString = "";
          for (var i = 0, l = parts.length; i < l; i++) {
            localDateString += parts[i] + " ";
          }
        }
      }

      var sd = LDP_ParseInputDate(localDateString);
      // throw out garbage input
      if (sd) {
        sd = sd.confidence > -2 ? sd.date : null;
      }
      return sd;
    }
    
    /**
     * Keep year,month, and date fields in sync with the
     * calendar field.
     */
    function syncDateFields(dateField) {
      var sd = parseInputDate(dateField.value);
      if (sd) {
        var form = dateField.form;
        var elements = form.elements;
        var name = dateField.name;
        name = name.substring(0, name.indexOf("dateField"));
        var field;
        field = elements[ name + "day" ];
        if (field) {
          field.selectedIndex = sd.date-1;
        }
        field = elements[ name + "month" ];
        if (field) {
          field.selectedIndex = sd.month-1;
        }
        field = elements[ name + "quarter" ];
        if (field) {
          field.selectedIndex = (sd.month-1)/3;
        }
        field = elements[ name + "year" ];
        if (field) {
          if (field.options) {
            var options = field.options;
            for (var i = 0, l = options.length; i < l; i++) {
              if (options[i].value == sd.year) {
                 field.selectedIndex = i;
                 break;
              }
            }
          } else {
            field.value = sd.year;
          }
        }
      }
    }

/* *----------------------- datepicker_helper.js --------------------- */

var datepickerInitCalled = false;

// A spare iframe used to cover stray select boxes in IE
// IE has a bug where SELECT elements ignore zindex. The way to
// fix them is to cover them with an iframe [which obey z-indexes
// enough to sit under other elements, but can be used to cover SELECT]
// Here's a description of the technique
// http://dotnetjunkies.com/WebLog/jking/archive/2003/10/30/2975.aspx
var datepickerIframe = null;

function DP_prevMonthHtmlFn(picker) {
  return function(date) {
    return '&laquo;';
  };
}

function DP_curMonthWideHtmlFn(picker) {
  return function(date) {
    return picker.getFullMonths()[date.month] + ' ' + date.year;
  };
}

function DP_curMonthNarrowHtmlFn(picker) {
  return function(date) {
    return picker.getMonths()[date.month] + ' ' + date.year;
  };
}

function DP_nextMonthHtmlFn(picker) {
  return function(date) {
    return '&raquo;';
  };
}

/**
 * Maps the id of an <INPUT> to the
 * DP_DatePicker that has been added to it.
 */
var input2DatePickerMap = {};

var datePickerRegistrations = {};

function registerDatePickerInput(inputId) {
  datePickerRegistrations[inputId] = 1;
}

function addDatePickersToInputs() {
  for (inputId in datePickerRegistrations) {
    addDatePickerToInput(inputId);
  }
}

/**
 * @param inputId {string} id of <INPUT> that should have a datepicker popup
 */
function addDatePickerToInput(inputId) {

  if (BR_IsIE() && datepickerIframe == null) {
    // note - we never reference the element by ID right now
    datepickerIframe = CreateIFRAME(window, 'DP_Iframe', 'javascript:false');
    datepickerIframe.parentNode.style.display = 'none';
    datepickerIframe.style.position = 'absolute';
  }

  var dp;
  var div;
  if (input2DatePickerMap[inputId]) {
    dp = input2DatePickerMap[inputId];
    div = dp.getDiv();
  } else {
    div = document.createElement('div');
    div.id = inputId + 'dp_div';
    div.style.position = 'absolute';
    div.style.display = 'none';
    div.style.width = '10em';
    document.body.appendChild(div);
    dp = new DP_DatePicker(div, false, undefined, 'DP_popup_');
    dp.setSelectionMode(DP_SELECTION_SINGLE_DAY);
    dp.setPrevMonthHtmlFn(DP_prevMonthHtmlFn(dp));
    dp.setCurMonthHtmlFn(DP_curMonthNarrowHtmlFn(dp));
    dp.setNextMonthHtmlFn(DP_nextMonthHtmlFn(dp));
    dp.setUseDayHeaders(true);
    dp.setFirstDayOfWeek(tu_getPreferredStartDayOfWeek());
    input2DatePickerMap[inputId] = dp;

    // print the date in the <INPUT> when the user makes a selection
    dp.addSelectionListener(fillInputWithPickerSelection(dp, inputId));

    // test to see if the event should cause the datepicker to close itself
    dp.deactivate = function(e) {
      if (!dp.isVisible()) return true;
      var box = nodeBounds(dp.getTableNode());
      var point = GetMousePosition(e);
      if (!box.contains(point)) {
        dp.hide();
        if (datepickerIframe) {
          datepickerIframe.parentNode.style.display = 'none';
        }
        return true;
      }
      return false;
    }
  }

  // We activate on focus or click
  var activateFunc = inputFocusListener(div.id, inputId, dp);
  var input = forid(inputId);
  listen(input, 'focus', activateFunc, false);
  listen(input, 'click', activateFunc, false);

  // Deactivate on blur
  listen(input, 'blur', inputBlurListener(dp), false);

  listen(input, 'keydown', inputKeyListener(dp, inputId), false);

  return dp;
}

function inputFocusListener(divId, inputId, dp) {
  return function() {
    var input = forid(inputId);
    var box = nodeBounds(input);
    var div = forid(divId);

    // HACK(davem) - make up for the fact that nodeBounds gives us
    // the wrong answer
    var hackX = 10;
    var hackY = 5;
    div.style.left = (box.x - hackX) + 'px';
    div.style.top = (box.y + box.h - hackY) + 'px';
    div.className = 'DP_popup_div';
    div.style.display = '';

    // fetch the z-index for the textbox and add one to get a z-index for the
    // overlay.
    var inputComputedStyle =
      input.currentStyle ? input.currentStyle :
      input.ownerDocument.defaultView.getComputedStyle(input, "");
    if (typeof(inputComputedStyle.zIndex) == "number") {
      div.style.zIndex = inputComputedStyle.zIndex + 1;
    } else {
      div.style.zIndex =  1;
    }

    dp.show();

    if (datepickerIframe) {
      var tableBox = nodeBounds(dp.getTableNode());

      datepickerIframe.parentNode.style.display = '';
      datepickerIframe.style.left = tableBox.x + 'px';
      datepickerIframe.style.top = tableBox.y + 'px';
      datepickerIframe.style.width = tableBox.w + 'px';
      datepickerIframe.style.height = tableBox.h + 'px';
      datepickerIframe.style.zIndex = div.style.zIndex - 1;
      datepickerIframe.style.display = '';
    }

    // if input.value, set current date of dp
    var sd = parseInputDate(input.value);
    if (sd) {
      dp.setSelection(sd, sd, false);
    }

    PC_addPopup(dp);
    return true;
  }
}

function inputBlurListener(dp) {
  return function(e) {
    // If the table is not visible, then we're done
    if (!dp.isVisible()) {
      return true;
    }

    var e = e || window.event;
    var insideTable;
    if (BR_IsIE()) {
      var box = nodeBounds(dp.getTableNode());
      insideTable = box.contains(GetMousePosition(e));
    } else {
      var cellClicked = e.explicitOriginalTarget;
      insideTable = IsDescendant(dp.getTableNode(), cellClicked);
    }
    if (!insideTable) {
      dp.hide();
      if (datepickerIframe) {
        datepickerIframe.parentNode.style.display = 'none';
      }
    }
    return true;
  }
}

function fillInputWithPickerSelection(dp, inputId) {
  return function() {
    var date = dp.getSelection();
    if (!date) return;
    var input = forid(inputId);
    input.value = googleDateToString(date);
    dp.hide();
    if (datepickerIframe) {
      datepickerIframe.parentNode.style.display = 'none';
    }
    if (input.onchange) input.onchange(input);
  }
}

function inputKeyListener(dp, inputId) {
  return function (e) {
    if (!dp.isVisible()) return;
    e = e || window.event;
    var keycode = GetKeyCode(e);
    if (keycode == UP_KEYCODE || keycode == DOWN_KEYCODE) {
      var date = dp.getSelection();
      if (!date) return;
      var deltaDate = (keycode == UP_KEYCODE) ? -1 : 1;
      var builder = ical_builderCopy(date);
      builder.date += deltaDate;
      dp.setSelection(builder.toDate(), undefined, false);
    } else if (keycode == ENTER_KEYCODE) {
      window.setTimeout(fillInputWithPickerSelection(dp, inputId), 0);
    }
  };
}


/**
 * This method is overridden to set the start day of week for the date
 * picker. 
 */
function tu_getPreferredStartDayOfWeek() {
  // 0 is Sunday, 1 is Monday, in keeping with the builtin Date object.
  return parseInt(0);
}

    // Bold the start day of a week definition
    // param date is an ICAL_DATE object from ical.js
    function weeklyStartDateDecor(date){
      var weekIndex = document.getElementById(
           "weekStart").selectedIndex;
      if (ICAL_getDayOfWeek(date) == weekIndex) {
        return "font-weight: bold;";
      }
      return '';
    }

    // Bold the end day of a week definition
    // param date is an ICAL_DATE object from ical.js
    function weeklyEndDateDecor(date) {
      var daysMap = new Array();
      daysMap[0] = 6;
      for(i=1; i != 7; i++) {
        daysMap[i] = i-1;
      }
      var weekIndex = document.getElementById(
          "weekStart").selectedIndex;
      if (ICAL_getDayOfWeek(date) == daysMap[weekIndex]) {
      return "font-weight: bold;";
      }
      return '';
    }

    /**
     * If aggregation type is weekly, bold the start and end days;
     * otherwise leave them as plain.
     */
    function checkAggr() {
      var viewValue = getViewValue();
      var isWeeklyView = (viewValue ==
          "Weekly" );
      var startDP = input2DatePickerMap['beginDateField'];
      var endDP = input2DatePickerMap['endDateField'];

      if (isWeeklyView) {
        setDecorators(startDP, endDP);
      } else {
        startDP.setDecoratorInline(null);
        endDP.setDecoratorInline(null);
      }
    }

    function setDecorators(startDP, endDP){
      startDP.setDecoratorInline(weeklyStartDateDecor);
      endDP.setDecoratorInline(weeklyEndDateDecor);
    }

