/*
 * AnyDatePicker: yet another Javascript date picker...
 * Copyright (c) Nick Brown, 2009.
 * Credits for default icons:
 * Month/year navigation buttons by Jojo Mendoza (http://www.deleket.com/)
 * Clock by mazenl77 (http://mazenl77.deviantart.com/)
 * Checkmark ("OK") and cross ("Cancel") by Marco Martin (http://www.notmart.org/)
 */

var anyDatePicker = {

/*
 * The following items can be set from outside before the first function call, to customise for local requirements, but
 *  it's better to just pass a "params" object to makeDiv() containing those elements whose values you want to override.
 */
params: {
    allowCancel: false			//if true, user can click "cancel" and return to previous field value
  , ampm: false				//if true, times use the 12-hour clock
  , ampmStrings: ['am', 'pm']		//captions for AM or PM
  , callbackOnAccept:			//function called just before input is accepted; returns false if there's a problem
      function (ts, adpDiv) { return true; }
  , confirmWithOK: false		//if true, user has to click "OK" to finish input
  , developer: false			//if true, some internal workings are revealed
  , divClass: null			//set to a CSS class name if required
  , earliestDate: '19000101'		//can't select days before this
  , earliestTimes: null			//can't select times after the matching element for a given day
  , elementTitles: {			//displayed when hovering over a button or input element
      'cancel': 'Cancel'
    , 'hour': 'Hour'
    , 'minute': 'Minute'
    , 'nextmonth': 'Next month'
    , 'nextyear': 'Next year'
    , 'ok': 'OK'
    , 'prevmonth': 'Previous month'
    , 'prevyear': 'Previous year'
    , 'year': 'Year'
  }
  , firstDayOfWeek: 1			//Monday, for compatibility with ISO - use 0 for Sunday
  , hideMonthButtons: false		//set to true to prevent user from navigating month-by-month
  , hideUnpickableMonths: false		//set to true to prevent user from navigating to or selecting out-of-range dates
  , hideUnpickableRows: false		//don't show rows which are entirely in the previous or next month
  , hideYearButtons: false		//set to true to prevent user from navigating year-by-year
  , holidays: [				//individual holidays
      /* '20090704', '20091126' */	//example usage for USA in 2009 (July 4th, Thanksgiving)
    ]
  , holidaysEveryYear: [		//mmdd of days which are always holidays every year
      '0101', '1225'
    ]
  , holidaysUnpickable: false		//if true, user cannot select a holiday
  , imageFileExtension: '.png'		//extension/"file type" of icon files
  , imagePath: 'anydatepicker/images/'	//URL of icon files; should end with "/"
  , imagePathIE6: 'anydatepicker/images-ie6/'	//URL of degraded icon files for IE6; should end with "/"
  , incomingConvert:			//converts the caller's date format to a simple date/time string of the form yyyymmddhhmm
      function (ts, adpDiv) { return ts; }
  , inputMonthObject: null		//set to 'dropdown' for a dropdown list
  , inputTimeObject: null		//set to 'dropdown' for a dropdown list, or 'text' for a free text field
  , inputYearObject: null		//set to 'dropdown' for a dropdown list, or 'text' for a free text field
  , latestDate: '20991231'		//can't select days after this
  , latestTimes: null			//can't select times before the matching element for a given day
  , minimumTitleWidth: 0		//minimum width for month/year title; helps keep the window a constant size
  , minuteDropdown: [0, 15, 30, 45]	//for 'dropdown' time input
  , monthNames: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
  , niceDateTime:			//converts a simple date/time string of the form yyyymmddhhmm to a friendly format (tag-safe)
      function (ts, adpDiv) { return ts; }
  , outgoingConvert:			//converts a simple date/time string of the form yyyymmddhhmm to the caller's date format
      function (ts, adpDiv) { return ts; }
  , parentElement: null			//DIVs will be attached to this element
  , stylePrefix: 'anyDatePicker'	//identifies our elements in the style sheet
  , userParams: null			//allows user-supplied parameters to be attached to the DIV
  , weekdayNames: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
  , weekdayNamesShort: null		//an array of short names; "false" means to use the first two letters of the full name
  , weekendDays: [0, 6]			//these days will be displayed slightly differently
  , weekendsUnpickable: false		//if true, user cannot select a weekend day
}

, allDivs: []
, baseMinimumTitleWidth: 100
, firstWeekRowNumber: false
, lastWeekRowNumber: false
, inputError: false

/*
 * Pad an integer to two digits (ignoring the possibility that it might be negative or >99).
 */
, padTo2: function (n)
{
  return ((n < 10) ? '0' : '') + n;
}

/*
 * See if an element exists within an array.
 */
, inArray: function (needle, haystack)
{
  var result = false;

  for (var i = 0; i < haystack.length; i++) {
    if (haystack[i] == needle) {
      result = true;
      break;
    }
  }

  return result;
}

/*
 * Convert an internal date/time object value to a date/time string.
 */
, makeDateTimeString: function (adpDiv)
{
  var currentValue = adpDiv.anyDatePicker.currentValue;
  var result = '' + currentValue.year + this.padTo2(currentValue.month) + this.padTo2(currentValue.day);

  if (adpDiv.anyDatePicker.timeFields) {
    result += this.padTo2(currentValue.hour) + this.padTo2(currentValue.minute);
  }

  return result;
}

/*
 * Return the number of days in a given month.
 */
, daysInMonth: function (someMonth, someYear)
{
  var dayCount = [31, (this.isLeapYear(someYear)) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];

  return dayCount[someMonth - 1];
}

/*
 * Map a weekday number (Sunday=0, etc) to its position within "our" week.
 */
, mappedWeekday: function (someDay, firstDay)
{
  return (firstDay + someDay) % 7;
}

/*
 * Determine whether a given year is a leap year.
 */
, isLeapYear: function (someYear)
{
  var result = ((someYear % 4) === 0) && (((someYear % 100) !== 0) || ((someYear % 400) === 0));

  return result;
}

/*
 * Calculate the number of leap years from 1582 until the given year.
 */
, leapYearsUntil: function (someYear)
{
  var result = Math.floor((someYear - 1581) / 4);
  result -= Math.floor((someYear - 1501) / 100);
  result += Math.floor((someYear - 1201) / 400);

  return result;
}

/*
 * Calculate the weekday of January 1 in any given Gregorian year, given that 1582-01-01 was a Friday.
 */
, weekdayOfJan1: function (someYear)
{
  var result = -1;

  if (someYear >= 1582) {
    result = ((someYear - 1582) + this.leapYearsUntil(someYear) + 5) % 7;
  }

  return result;
}

/*
 * Calculate the weekday of any given month in any given Gregorian year.
 */
, weekdayOf1: function (someYear, someMonth)
{
  var result = this.weekdayOfJan1(someYear);

  for (var i = 1; i < someMonth; i++) {
    result += this.daysInMonth(i, someYear);
  }

  result %= 7;

  return result;
}

/*
 * Convert an array of classes to a single className string.
 */
, classString: function (adpDiv, classes)
{
  var result = '';
  var prefix = adpDiv.anyDatePicker.params.stylePrefix;
  var sep = '';

  if (classes) {
    for (var i = 0; i < classes.length; i++) {
      var c = classes[i];
      if (c) {
        result += sep + prefix + '_' + c;
        sep = ' ';
      }
    }
  }
  else {
    result = prefix;
  }

  return result;
}

/*
 * Determine whether a given day is a holiday.
 */
, isHoliday: function (yyyymmdd, adpDiv)
{
  var result = false;
  var params = adpDiv.anyDatePicker.params;

  if (    this.inArray(yyyymmdd, params.holidays)
       || this.inArray(yyyymmdd.substr(4, 4), params.holidaysEveryYear)
     ) {
    result = true;
  }

  return result;
}

/*
 * Determine whether a given day is part of the weekend.
 */
, isWeekend: function (yyyymmdd, adpDiv)
{
  var result = false;
  var params = adpDiv.anyDatePicker.params;

  var weekday = this.weekdayNumber(yyyymmdd);
  if (this.inArray(weekday, params.weekendDays)) {
    result = true;
  }

  return result;
}

/*
 * Build the contents of a date cell (including day name cells, during initialisation of the DIV).
 */
, fillDateCell: function (adpDiv, cell, monthStyle, weekend, cellValue, yyyymmdd, otherClasses)
{
  var classes = [];
  var params = adpDiv.anyDatePicker.params;

  classes.push('datecell');

  var dayType = (weekend) ? 'weekend' : 'midweek';
  var pickable = false;

  if (yyyymmdd) {
    pickable = true;

    if ((dayType == 'weekend') && params.weekendsUnpickable) {
      pickable = false;
    }

    if (this.isHoliday(yyyymmdd, adpDiv)) {
      dayType = 'holiday';
      if (params.holidaysUnpickable) {
        pickable = false;
      }
    }

    if (    (+yyyymmdd > +params.latestDate)
         || (+yyyymmdd < +params.earliestDate)
       ) {
      pickable = false;
      classes.push('datecell_nopick');
    }
  }

  classes.push(monthStyle + '_' + dayType);

  for (var i = 0; i < otherClasses.length; i++) {
    var otherClass = otherClasses[i];

    if ((! pickable) && (otherClass == 'current_value')) {	//don't show current value if it's not pickable
      continue;
    }

    classes.push(otherClass);
  }

  var inner = '' + cellValue;
  if (pickable && (monthStyle == 'thismonth')) {
    inner = '<a width="100%" style="display:block"'
          + ' href="javascript:anyDatePicker.shuffle(\'' + adpDiv.id + '\',\'' + cellValue + '\', 0)"'
          + '>' + cellValue + '</a>';
  }

  cell.innerHTML = inner;
  cell.className = this.classString(adpDiv, classes);

  var cellTitle = params.niceDateTime(yyyymmdd, adpDiv);
  if (params.developer) {
    cellTitle += ((cellTitle) ? ' ' : '') + '(classes: ' + cell.className + ')';
  }
  cell.title = cellTitle;
}

/*
 * Create a table row or cell which contains a new table, spanning a number of cells in the outer table,
 *  and append it to a parent item.  The returned value is the first row of the new table.
 */
, appendSpanningItem: function (itemType, parent, attributes)
{
  var result = null;

  if (! attributes) {
    attributes = {};
  }

  switch (itemType) {
  case 'tr':
    var row = document.createElement('tr');
    parent.appendChild(row);
    parent = row;
//and fall through!

  case 'td':
    var cell = document.createElement('td');
    for (var a in attributes.td) {
      cell[a] = attributes.td[a];
    }
    parent.appendChild(cell);
    parent = cell;
//and fall through!

  case 'table':
    var subTable = document.createElement('table');
    subTable.width = '100%';
    for (var a in attributes.table) {
      subTable[a] = attributes.table[a];
    }
    parent.appendChild(subTable);

    var subTbody = document.createElement('tbody');
    subTable.appendChild(subTbody);

    var subRow = document.createElement('tr');
    subTbody.appendChild(subRow);

    result = subRow;
  }

  return result;
}

/*
 * Handler for time input fields, to force a valid numeric value.
 * This code can handle both text and dropdown input styles.
 */
, inputTimeCheck: function (timeElement, refill)
{
  var string = timeElement.value;
  var adpDiv = timeElement.anyDatePicker.adpDiv;
  var field = timeElement.anyDatePicker.field;		//hour or minute
  var ampmSelect = adpDiv.anyDatePicker.ampmSelect;

  var hour12 = ((field == 'hour') && ampmSelect);
  var afternoonHours = (hour12) ? +(ampmSelect.value) : 0;
  var numericString = string.match(/\d+/)[0];
  var number = +numericString;
  if (hour12) {
    number = (number % 12) + afternoonHours;
  }
  number = Math.min(Math.max(number, timeElement.anyDatePicker.minValue), timeElement.anyDatePicker.maxValue);

  if (! (anyDatePicker.inputError = (string != numericString))) {	//we corrected the string
    adpDiv.anyDatePicker.currentValue[field] = number;
    timeElement.value = '' + ((hour12) ? ((number % 12) || 12) : number);
  }

  if (refill) {				//need to repopulate dropdown lists in some cases
    this.fillDiv(adpDiv.id, adpDiv.anyDatePicker.targetElement.id, false, this.makeDateTimeString(adpDiv));
  }
}

/*
 * Handler for year input field, to force a valid numeric value.
 */
, inputYearCheck: function (input)
{
  var string = input.value;
  var adpDiv = input.anyDatePicker.adpDiv;
  var params = adpDiv.anyDatePicker.params;
  var newString = (string.match(/\d+/)[0]) || '0';

  if ((! window.event) || (window.event.type != 'keyup')) {	//not just a keypress, so check range
    var year = +newString;

    if (year < 1582) {		//probably due to badly formed input (less than 4 digits)
      year = input.anyDatePicker.previousValue;
    }
    else {
      var minYear = +(params.earliestDate.substr(0, 4));
      var maxYear = +(params.latestDate.substr(0, 4));

      year = Math.min(Math.max(year, minYear), maxYear);
    }

    newString = '' + year;
  }

  input.value = newString;

  if (! (anyDatePicker.inputError = (string != newString))) {
    adpDiv.anyDatePicker.currentValue.year = +newString;
  }
}

/*
 * Change handler for the dropdown list to choose a year.
 */
, yearChange: function (selectObject)
{
//  selectObject = selectObject || this.hackForIE6['yearSelect'];
  var adpDiv = selectObject.anyDatePicker.adpDiv;
  var delta = selectObject.value - adpDiv.anyDatePicker.currentValue.year;

  this.shuffle(adpDiv.id, 'nextyear', delta);
}

/*
 * Change handler for the dropdown list to choose a month.
 */
, monthChange: function (selectObject)
{
//  selectObject = selectObject || this.hackForIE6['monthSelect'];
  var adpDiv = selectObject.anyDatePicker.adpDiv;
  var delta = selectObject.value - adpDiv.anyDatePicker.currentValue.month;

  this.shuffle(adpDiv.id, 'nextmonth', delta);
}

/*
 * Hide the date picker DIV, after optionally accepting the user's input and mapping it to the target element.
 */
, accept: function (divId, update)
{
  var adpDiv = document.getElementById(divId);
  var params = adpDiv.anyDatePicker.params;

  if (update) {
    if (anyDatePicker.inputError) {		//perhaps an input value check has just run as the onChange handler
      anyDatePicker.inputError = false;
      return;
    }

    var dateTime = this.makeDateTimeString(adpDiv);
    if (! params.callbackOnAccept(dateTime, adpDiv)) {
      return;
    }

    var string = params.outgoingConvert(dateTime, adpDiv);
    var targetElement = adpDiv.anyDatePicker.targetElement;

    if (targetElement.value !== string) {
      adpDiv.anyDatePicker.targetElement.value = string;

      if (typeof targetElement.onchange == 'function') {
        targetElement.onchange();	//fire any onChange handler which the target element may have
      }
    }
  }

  adpDiv.anyDatePicker.targetDay = 0;		//forget this value, in case we reopen this DIV
  adpDiv.style.display = 'none';		//we have finished displaying
}

/*
 * Return a time limit (earliest or latest) for a given date, if specified.
 */
, timeLimit: function (adpDiv, limitField, yyyymmdd)
{
  var result = null;
  var params = adpDiv.anyDatePicker.params;
  var limit = params[limitField];

  if (limit) {
    var weekday = this.weekdayNumber(yyyymmdd);
    var weekendOrNot = (this.isWeekend(yyyymmdd, adpDiv)) ? 'weekend' : 'weekday';
    var holidayOrNot = (this.isHoliday(yyyymmdd, adpDiv)) ? 'holiday' : 'non-holiday';

    var check = ['*', weekendOrNot, '' + weekday, holidayOrNot, yyyymmdd.substr(4, 4), yyyymmdd];
    for (var i = 0; i < check.length; i++) {
      var key = check[i];
      if (limit[key]) {			//apply limits in priority order
        result = limit[key];
      }
    }
  }

  return result;
}

/*
 * Public utility function to determine whether a given date/time string is valid.
 */
, validDateTime: function (dateTime)
{
  var year = +(dateTime.substr(0, 4) || 0);
  var month = +(dateTime.substr(4, 2) || 0);
  var day = +(dateTime.substr(6, 2) || 0);
  var hour = +(dateTime.substr(8, 2) || 0);
  var minute = +(dateTime.substr(10, 2) || 0);

  if ((year < 1582) || (year > 9999)) {
    return false;
  }

  if ((month < 1) || (month > 12)) {
    return false;
  }

  if ((day < 1) || (day > this.daysInMonth(month, year))) {
    return false;
  }

  if ((hour < 0) || (hour > 23)) {
    return false;
  }

  if ((minute < 0) || (minute > 59)) {
    return false;
  }

  return true;
}

/*
 * Public utility function to return the weekday number (Sunday = 0) of a given date.
 */
, weekdayNumber: function (dateTime)
{
  var year = +(dateTime.substr(0, 4) || 1582);
  var month = +(dateTime.substr(4, 2) || 1);
  var day = +(dateTime.substr(6, 2) || 1);

  var result = (this.weekdayOf1(year, month) + (day - 1)) % 7;

  return result;
}

/*
 * Public utility function to return version information.
 */
, versionInfo: function ()
{
  return '20091008.0705.00.08';
}

/*
 * Public utility function (also called internally) to find the X or Y offset of a DOM element within the window.
 */
, findPos: function (element, direction)
{
  var result = 0;

  if (element.offsetParent) {
    var offset = 'offset' + direction.substr(0, 1).toUpperCase() + direction.substr(1);
    while (element) {
      result += element[offset];
      element = element.offsetParent;
    }
  }
  else {
    var xOrY = {'left': 'x', 'top': 'y'}[direction];
    result = element[xOrY] || 0;
  }

  return result;
}

/*
 * Public function to hide all anyDatePicker DIVs.
 */
, hideAllDivs: function ()
{
  for (var i = 0; i < this.allDivs.length; i++) {
    this.allDivs[i].display = 'none';
  }
}

/*
 * Public function to make the DIV for a new date picker.
 */
, makeDiv: function (divId, callerParams)
{
  var params = {};
  var userAgent = navigator.userAgent.toLowerCase();
  var msie = userAgent.match(/msie ([\d\.]+)/);
  if (msie) {
    var msieVersion = +msie[1];
  }

  for (var p in this.params) {		//use default values for any parameters which the caller didn't supply
    params[p] = (callerParams[p] !== undefined) ? callerParams[p] : this.params[p];
  }

  if (msie && (msieVersion <= 6)) {		//use IE6-specific images
    if (params.imagePathIE6) {
      params.imagePath = params.imagePathIE6;
    }
  }

  if (! params.weekdayNamesShort) {
    params.weekdayNamesShort = [];
    for (var i = 0; i < 7; i++) {
      params.weekdayNamesShort.push(params.weekdayNames[i].substr(0, 2));
    }
  }

  var adpDiv = document.createElement('div');
  adpDiv.style.visibility = 'hidden';

  var parent = params.parentElement;
  if (! parent) {
    if (msie && (msieVersion <= 7)) {		//can't use document.body as the parent, if another element is active
      parent = document.activeElement || document.body;

      while (parent.lastChild && parent.lastChild.lastChild) {
        parent = parent.lastChild;
      }
    }
    else {
      parent = document.body;
    }
  }
  parent.appendChild(adpDiv);
  this.params.parentElement = parent;		//remember parent, otherwise next time IE will think this new DIV is the last element

  adpDiv.id = divId;
  adpDiv.anyDatePicker = {};			//info stored across calls
  adpDiv.anyDatePicker.params = params;		//remember for later function calls

  if (params.divClass) {
    adpDiv.className = params.divClass;
  }

  var table = document.createElement('table');
  adpDiv.appendChild(table);
  table.className = this.classString(adpDiv);

  var tbody = document.createElement('tbody');
  table.appendChild(tbody);

  var titleRow = this.appendSpanningItem('tr', tbody, {td: {colSpan: 3}});	//3 cells in bottom row

  var buttons = [
      (params.hideYearButtons) ? [((params.inputYearObject) ? 'empty1px' : 'empty16px'), false] : ['prevyear', true]
    , (params.hideMonthButtons) ? [((params.inputMonthObject) ? 'empty1px' : 'empty16px'), false] : ['prevmonth', true]
    , ['title']
    , (params.hideMonthButtons) ? [((params.inputMonthObject) ? 'empty1px' : 'empty16px'), false] : ['nextmonth', true]
    , (params.hideYearButtons) ? [((params.inputYearObject) ? 'empty1px' : 'empty16px'), false] : ['nextyear', true]
  ];
  for (var d = 0; d < buttons.length; d++) {
    var button = buttons[d];
    var buttonName = button[0];
    var td = document.createElement('td');
    titleRow.appendChild(td);
    td.align = 'center';
    td.id = adpDiv.id + '_' + buttonName;

    if (buttonName == 'title') {
      if (params.inputMonthObject == 'dropdown') {
        var monthSelect = document.createElement('select');
        td.appendChild(monthSelect);

        monthSelect.className = this.classString(adpDiv);
        monthSelect.anyDatePicker = {'adpDiv': adpDiv};

        monthSelect.onchange =
            (msie && (msieVersion <= 6))
          ? function() { setTimeout(function () {anyDatePicker.monthChange(monthSelect);}, 20); }
          : function() { anyDatePicker.monthChange(this); }
        ;
      }
      else {
        var monthSpan = document.createElement('span');
        td.appendChild(monthSpan);
      }

      var spaceSpan = document.createElement('span');
      td.appendChild(spaceSpan);

      if (params.inputYearObject == 'dropdown') {
        var yearSelect = document.createElement('select');
        td.appendChild(yearSelect);

        yearSelect.className = this.classString(adpDiv);
        yearSelect.anyDatePicker = {'adpDiv': adpDiv};
        yearSelect.onchange =
            (msie && (msieVersion <= 6))
          ? function() { setTimeout(function () {anyDatePicker.yearChange(yearSelect);}, 20); }
          : function() { anyDatePicker.yearChange(this); }
        ;
      }
      else if (params.inputYearObject == 'text') {
        var inputYear = document.createElement('input');
        td.appendChild(inputYear);

        inputYear.className = this.classString(adpDiv);
        inputYear.size = 3;
        inputYear.maxLength = 4;
        inputYear.title = params.elementTitles.year || '';
        inputYear.onchange = inputYear.onkeyup = function() { anyDatePicker.inputYearCheck(this); };
        inputYear.anyDatePicker = {'adpDiv': adpDiv};
      }
      else {
        var yearSpan = document.createElement('span');
        td.appendChild(yearSpan);
      }

      var titleWidth = params.minimumTitleWidth;
      if (titleWidth == 0) {
        titleWidth = this.baseMinimumTitleWidth
                 + ((params.hideMonthButtons) ? 30 : 0)			//we lose 15 pixels per omitted button
                 + ((params.hideYearButtons) ? 30 : 0)
                 + ((params.inputTimeObject && params.ampm) ? 60 : 0)	//compensate for width of dropdowns
        ;
      }

      if (! (params.inputMonthObject && params.inputYearObject)) {
        spaceSpan.innerHTML = '&nbsp;';

        if (! params.inputYearObject) {
          yearSpan.innerHTML = '8888';			//the year with the fattest digits :-)
          titleWidth = Math.max(titleWidth, td.offsetWidth);
        }

        if (! params.inputMonthObject) {
          for (var m = 0; m < params.monthNames.length; m++) {
            monthSpan.innerHTML = params.monthNames[m];
            titleWidth = Math.max(titleWidth, td.offsetWidth);
          }
        }
      }

      td.width = titleWidth + 'px';
      adpDiv.anyDatePicker.titleCell = td;
    }
    else {
      var buttonLink = button[1];
      var buttonFile = params.imagePath + buttonName + params.imageFileExtension;
      var img = '<img class="' + this.classString(adpDiv, ['nav_button']) + '" src="' + buttonFile + '"' + ' />';
      if (buttonLink) {
        td.innerHTML = '<a href="javascript:anyDatePicker.shuffle(\'' + adpDiv.id + '\',\'' + buttonName + '\', 1)">' + img + '</a>';
        td.title = params.elementTitles[buttonName] || '';
      }
      else {
        td.innerHTML = img;
      }
    }
  }

  var dayNamesRow = this.appendSpanningItem('tr', tbody, {'td': {colSpan: 3}, 'table': {className: this.classString(adpDiv, ['daytable']), 'width': '100%'}});
  var dayTable = dayNamesRow.parentNode;

  for (var w = 0; w < 6; w++) {			//a month can require up to six rows for the weeks
    var tr = document.createElement('tr');

    if (w == 0) {
      this.firstWeekRowNumber = dayTable.rows.length;
    }

    dayTable.appendChild(tr);
  }
  this.lastWeekRowNumber = dayTable.rows.length - 1;

  for (var c = 0; c < 7; c++) {
    var td = document.createElement('td');
    dayNamesRow.appendChild(td);

    var weekday = this.mappedWeekday(c, params.firstDayOfWeek);
    var weekend = this.inArray(weekday, params.weekendDays);

    this.fillDateCell(adpDiv, td, 'dayname', weekend, params.weekdayNamesShort[weekday], '', ['dayname']);
    td.title = params.weekdayNames[weekday];
    td.style.textAlign = 'center';
    td.width = '14%';

    for (var w = this.firstWeekRowNumber; w <= this.lastWeekRowNumber; w++) {
      var td = document.createElement('td');
      dayTable.rows[w].appendChild(td);
      td.style.textAlign = 'right';
    }
  }

  if (params.inputTimeObject || params.allowCancel || params.confirmWithOK) {
    var bottomRow = document.createElement('tr');
    tbody.appendChild(bottomRow);

    var td = document.createElement('td');	//for time input
    bottomRow.appendChild(td);
    td.width = '70%';

    if (params.inputTimeObject) {
      var timeRow = this.appendSpanningItem('table', td);

      var td = document.createElement('td');	//for clock icon
      timeRow.appendChild(td);
      td.align = 'center';

      var button = 'clock';
      var img = '<img class="' + this.classString(adpDiv, ['bottom_row_button']) + '" src="' + params.imagePath + button + params.imageFileExtension + '"' + ' />';
      td.innerHTML = img;

      var td = document.createElement('td');	//for time input
      timeRow.appendChild(td);
      var sep = true;
      adpDiv.anyDatePicker.timeFields = {};

      for (var field in {'hour': 0, 'minute': 1}) {
        var timeElement = null;

        if (params.inputTimeObject == 'text') {
          timeElement = document.createElement('input');
          timeElement.size = 1;
          timeElement.maxLength = 2;
        }
        else if (params.inputTimeObject == 'dropdown') {
          timeElement = document.createElement('select');
        }

        if (timeElement) {
          td.appendChild(timeElement);
          timeElement.className = this.classString(adpDiv);
          timeElement.title = params.elementTitles[field] || '';

          var refill = (params.inputTimeObject == 'dropdown') && params.ampm;
          timeElement.onchange = timeElement.onkeyup = function() { anyDatePicker.inputTimeCheck(this, refill); };
          timeElement.anyDatePicker = {'field': field, 'adpDiv': adpDiv};
          adpDiv.anyDatePicker.timeFields[field] = timeElement;
        }

        if (sep) {
          var span = document.createElement('span');
          td.appendChild(span);
          span.innerHTML = ':';
          sep = false;
        }
      }

      if (params.ampm) {
        var span = document.createElement('span');
        td.appendChild(span);
        span.innerHTML = '&nbsp;';

        var ampmSelect = document.createElement('select');
        td.appendChild(ampmSelect);
        ampmSelect.className = this.classString(adpDiv);
        adpDiv.anyDatePicker.ampmSelect = ampmSelect;

        var refill = (params.inputTimeObject == 'dropdown');
        ampmSelect.onchange =
            (msie && (msieVersion <= 6) && refill)
          ? function() { setTimeout(function() { anyDatePicker.inputTimeCheck(adpDiv.anyDatePicker.timeFields.hour, refill); }, 20); }
          : function() { anyDatePicker.inputTimeCheck(adpDiv.anyDatePicker.timeFields.hour, refill); }
        ;
      }
    }

    if (params.confirmWithOK || params.allowCancel) {
      var yesOrNo = {
          'confirmWithOK': {'button': 'ok', 'update': 'true'}
        , 'allowCancel': {'button': 'cancel', 'update': 'false'}
      };

      for (var option in yesOrNo) {
        var td = document.createElement('td');
        bottomRow.appendChild(td);
        td.align = 'center';

        var button = false;
        if (params[option]) {
          var optionStuff = yesOrNo[option];
          button = optionStuff['button'];
        }

        var buttonFile = params.imagePath + (button || 'empty16px') + params.imageFileExtension;
        var img = '<img class="' + this.classString(adpDiv, ['bottom_row_button']) + '" src="' + buttonFile + '"' + ' />';

        if (params[option]) {
          td.innerHTML = '<a href="javascript:anyDatePicker.accept(\'' + adpDiv.id + '\', ' + optionStuff.update + ')">' + img + '</a>';
          td.title = params.elementTitles[button] || '';
        }
        else {
          td.innerHTML = img;
        }
      }
    }
  }

  adpDiv.style.display = 'none';		//DIV will be displayed by fillDiv()
  adpDiv.style.visibility = 'visible';

  adpDiv.anyDatePicker.targetDay = 0;
  adpDiv.anyDatePicker.dayTable = dayTable;

  this.allDivs.push(adpDiv);

  return adpDiv;
}

/*
 * Public function call to fill a DIV with a value and associate it with a DOM input element.
 * The default value is the current value of the input element.
 */
, fillDiv: function (divId, targetElementId, positioning, value)
{
  var adpDiv = document.getElementById(divId);
  var targetElement = document.getElementById(targetElementId);
  var params = adpDiv.anyDatePicker.params;

  var today = new Date();
  var todayStr = '' + today.getFullYear() + this.padTo2(today.getMonth() + 1) + this.padTo2(today.getDate());

  var datetime = value || params.incomingConvert(targetElement.value, adpDiv) || '';
  var dateValue = (datetime + '11111111').substr(0, 8);
  var y = dateValue.substr(0, 4);
  if ((y == '1111') || (y == '9999')) {
    dateValue = todayStr;
  }

  if (params.hideUnpickableMonths) {			//force date into pickable range
    dateValue = '' + Math.min(Math.max(+dateValue, +params.earliestDate), +params.latestDate);
  }

  var year = +dateValue.substr(0, 4);
  var month = +dateValue.substr(4, 2);
  var day = +dateValue.substr(6, 2);

  if (adpDiv.anyDatePicker.targetDay == 0) {
    adpDiv.anyDatePicker.targetDay = day;
  }

  var timeValue = (datetime + '000000000000').substr(8, 4);
  if (adpDiv.anyDatePicker.timeFields) {
    var earliestTime = this.timeLimit(adpDiv, 'earliestTimes', dateValue) || '0000';
    var latestTime = this.timeLimit(adpDiv, 'latestTimes', dateValue) || '2359';

    if (timeValue < earliestTime) {
      timeValue = earliestTime;
    }

    if (timeValue > latestTime) {
      timeValue = latestTime;
    }
  }

  var hour = +timeValue.substr(0, 2);
  var minute = +timeValue.substr(2, 2);

  adpDiv.anyDatePicker.targetElement = targetElement;
  adpDiv.anyDatePicker.currentValue = {			//save for shuffle
    'year': +year
  , 'month': +month
  , 'day': +day
  , 'hour': +hour
  , 'minute': +minute
  };

  var thisMonthFirstWeekday = this.weekdayOf1(year, month);
  var thisMonthDaysInRow1 = (7 - thisMonthFirstWeekday + params.firstDayOfWeek) % 7;
  var thisMonthLastDay = this.daysInMonth(month, year);

  var prevMonth = (month - 1) || 12;
  var prevYear = year - ((month == 1) ? 1 : 0);		//year of previous month; not necessarily previous year
  var nextMonth = (month % 12) + 1;
  var nextYear = year + ((month == 12) ? 1 : 0);	//year of next month; not necessarily next year
  var prevMonthEndNext = this.daysInMonth(prevMonth, prevYear) + 1;

  var d = 0;
  var monthStyle = 'prevmonth';
  for (var r = this.firstWeekRowNumber; r <= this.lastWeekRowNumber; r++) {
    var row = adpDiv.anyDatePicker.dayTable.rows[r];
    var c = 0;

    if (r == this.firstWeekRowNumber) {
      for (d = prevMonthEndNext - (7 - thisMonthDaysInRow1); d < prevMonthEndNext; d++) {
        var yyyymmdd = '' + prevYear + this.padTo2(prevMonth) + this.padTo2(d);
        var weekend = this.isWeekend(yyyymmdd, adpDiv);
        this.fillDateCell(adpDiv, row.cells[c++], monthStyle, weekend, d, yyyymmdd, []);
      }

      if (params.hideUnpickableRows) {
        row.style.display = (c == 7) ? 'none' : 'table-row';
      }

      d = 0;		//will be incremented below
      monthStyle = 'thismonth';
    }

    while (c < 7) {
      if (++d > thisMonthLastDay) {
        d = 1;
        monthStyle = 'nextmonth';
      }

      if (params.hideUnpickableRows) {
        if ((r == this.lastWeekRowNumber) && (c == 0)) {
          row.style.display = (monthStyle == 'nextmonth') ? 'none' : 'table-row';
        }
      }

      var yyyymmdd = ((monthStyle == 'nextmonth') ? ('' + nextYear + this.padTo2(nextMonth)) : ('' + year + this.padTo2(month))) + this.padTo2(d);
      var weekend = this.isWeekend(yyyymmdd, adpDiv);
      var currentValue = ((d == day) && (monthStyle == 'thismonth') && 'current_value');
      this.fillDateCell(adpDiv, row.cells[c++], monthStyle, weekend, d, yyyymmdd, [currentValue]);
    }
  }

  var earliestYear = +params.earliestDate.substr(0, 4);
  var latestYear = +params.latestDate.substr(0, 4);

  var earliestMonth = 1;
  var latestMonth = 12;

  if (params.hideUnpickableMonths) {
    var yearMonth = +('' + year + this.padTo2(month));
    var earliestYearMonth = +params.earliestDate.substr(0, 6);
    var latestYearMonth = +params.latestDate.substr(0, 6);
    var prevMargin = yearMonth - earliestYearMonth;
    var nextMargin = latestYearMonth - yearMonth;

    if (! params.hideMonthButtons) {
      document.getElementById(adpDiv.id + '_' + 'prevmonth').style.visibility = (prevMargin < 1) ? 'hidden' : 'visible';
      document.getElementById(adpDiv.id + '_' + 'nextmonth').style.visibility = (nextMargin < 1) ? 'hidden' : 'visible';
    }

    if (! params.hideYearButtons) {
      document.getElementById(adpDiv.id + '_' + 'prevyear').style.visibility = (prevMargin < 100) ? 'hidden' : 'visible';
      document.getElementById(adpDiv.id + '_' + 'nextyear').style.visibility = (nextMargin < 100) ? 'hidden' : 'visible';
    }

    if (params.inputMonthObject == 'dropdown') {
      if (year == earliestYear) {
        earliestMonth = +params.earliestDate.substr(4, 2);
      }

      if (year == latestYear) {
        latestMonth = +params.latestDate.substr(4, 2);
      }
    }

    if (params.inputYearObject == 'dropdown') {
      var yearMonth = +('' + earliestYear + this.padTo2(month));
      if (yearMonth < earliestYearMonth) {
        ++earliestYear;
      }

      var yearMonth = +('' + latestYear + this.padTo2(month));
      if (yearMonth > latestYearMonth) {
        --latestYear;
      }
    }
  }

  var yearCell = adpDiv.anyDatePicker.titleCell.childNodes[2];
  if (params.inputYearObject == 'dropdown') {
    while (yearCell.length) {		//remove previous options
      yearCell.remove(0);
    }

    for (var i = earliestYear; i <= latestYear; i++) {
      var option = document.createElement('option');
      yearCell.appendChild(option);

      option.className = this.classString(adpDiv);
      option.text = '' + i;
      option.value = i;
      option.selected = (i == year);
    }
  }
  else if (params.inputYearObject == 'text') {
    yearCell.value = '' + year;
    yearCell.anyDatePicker.previousValue = yearCell.value;
  }
  else {
    yearCell.innerHTML = '' + year;
  }

  var monthCell = adpDiv.anyDatePicker.titleCell.childNodes[0];
  if (params.inputMonthObject == 'dropdown') {
    while (monthCell.length) {		//remove previous options
      monthCell.remove(0);
    }

    for (var i = 1; i <= 12; i++) {
      if ((i >= earliestMonth) && (i <= latestMonth)) {
        var option = document.createElement('option');
        monthCell.appendChild(option);

        option.className = this.classString(adpDiv);
        option.text = params.monthNames[i - 1];
        option.value = i;
        option.selected = (i == month);
      }
    }
  }
  else {
    monthCell.innerHTML = params.monthNames[month - 1];
  }

  if (adpDiv.anyDatePicker.timeFields) {
    var minHour = +earliestTime.substr(0, 2);
    var maxHour = +latestTime.substr(0, 2);
    var minMinute = 0;
    var maxMinute = 59;

    if (hour == minHour) {
      minMinute = +earliestTime.substr(2, 2);
    }

    if (hour == maxHour) {
      maxMinute = +latestTime.substr(2, 2);
    }

    var ampmSelect = adpDiv.anyDatePicker.ampmSelect;
    if (ampmSelect) {
      while (ampmSelect.length) {		//remove previous options
        ampmSelect.remove(0);
      }

      var needSelected = true;
      for (var i = Math.floor(minHour / 12); i <= Math.floor(maxHour / 12); i++) {
        var option = document.createElement('option');
        ampmSelect.appendChild(option);

        option.className = this.classString(adpDiv);
        option.text = params.ampmStrings[i];
        option.value = i * 12;

        if (needSelected && (option.value > (hour - 12))) {
          option.selected = true;
          needSelected = false;
        }
      }
    }
  }

  for (var field in adpDiv.anyDatePicker.timeFields) {
    var currentValue = adpDiv.anyDatePicker.currentValue[field];
    var timeElement = adpDiv.anyDatePicker.timeFields[field];

    if (params.inputTimeObject == 'text') {
      if ((field == 'hour') && ampmSelect) {
        currentValue = (currentValue % 12) || 12;	//replace 0 with 12, and no leading zero
      }

      timeElement.value = this.padTo2(currentValue);
    }
    else if (params.inputTimeObject == 'dropdown') {
      while (timeElement.length) {		//remove previous options
        timeElement.remove(0);
      }

      var timeOptionValues = [];
      if (field == 'hour') {
        for (var i = minHour; i <= maxHour; i++) {
          var displayHour = this.padTo2(i);

          if (ampmSelect) {
            if (+ampmSelect.value) {	//afternoon
              if (i < 12) {
                continue;
              }
            }
            else {
              if (i >= 12) {
                continue;
              }
            }

            displayHour = '' + ((i % 12) || 12);
          }

          timeOptionValues.push(displayHour);
        }
      }
      else {
        for (var i = 0; i < params.minuteDropdown.length; i++) {
          var m = params.minuteDropdown[i];
          if ((m >= minMinute) && (m <= maxMinute)) {
            timeOptionValues.push(this.padTo2(m));
          }
        }
      }

      var needSelected = true;
      for (var i = 0; i < timeOptionValues.length; i++) {
        var option = document.createElement('option');
        timeElement.appendChild(option);

        option.className = this.classString(adpDiv);

        var optionValue = +timeOptionValues[i];
        if ((field == 'hour') && ampmSelect) {
          optionValue = (optionValue % 12) + (+ampmSelect.value);
        }

        if (    needSelected		//if the existing value doesn't match the dropdown list, we round it up or down
             && (    (optionValue >= currentValue)
                  || (i == (timeOptionValues.length - 1))	//last chance!
                )
           ) {
          option.selected = true;
          needSelected = false;
          adpDiv.anyDatePicker.currentValue[field] = optionValue;
        }

        option.text = option.value = timeOptionValues[i];
      }
    }

    if (field == 'hour') {
      timeElement.anyDatePicker.minValue = minHour;
      timeElement.anyDatePicker.maxValue = maxHour;
    }
    else if (field == 'minute') {
      timeElement.anyDatePicker.maxValue = maxMinute;
      timeElement.anyDatePicker.minValue = minMinute;
    }
  }

  if (positioning) {
    var positionElement = (positioning.elementId) ? document.getElementById(positioning.elementId) : targetElement;

    adpDiv.style.position = 'absolute';
    adpDiv.style.left = (this.findPos(positionElement, 'left') + (positioning.offsetLeft || 0)) + 'px';
    adpDiv.style.top = (this.findPos(positionElement, 'top') + (positioning.offsetTop || 0)) + 'px';
  }

  for (var i = 0; i < this.allDivs.length; i++) {
    var d = this.allDivs[i];
    d.style.display = (d == adpDiv) ? 'block' : 'none';		//show the DIV to the user, and hide all others
  }
}

/*
 * Handle clicks on the buttons to move the date forwards or backwards by a month or a year,
 *  or the effects of choosing a new year or month from a dropdown list,
 *  or a simple click on a day number (although that isn't really "shuffling").
 * We try to keep the day of the month as close as possible to the original starting value,
 *  while avoiding non-existent and "unpickable" days.
 */
, shuffle: function (divId, button, delta)
{
  var adpDiv = document.getElementById(divId);
  var currentValue = adpDiv.anyDatePicker.currentValue;
  var params = adpDiv.anyDatePicker.params;
  var refill = true;

  switch (button) {
  case 'prevyear':
    delta = -delta;		//and fall through
  case 'nextyear':
    currentValue.year += delta;
    break;

  case 'prevmonth':
    delta = -delta;		//and fall through
  case 'nextmonth':
    currentValue.month += delta;
    currentValue.year += Math.floor((currentValue.month - 1) / 12);
    currentValue.month = (currentValue.month % 12) || 12;
    break;

  default:			//new day number
    adpDiv.anyDatePicker.targetDay = currentValue.day = +button;
    refill = params.confirmWithOK;
    break;
  }

  if (delta) {			//find the closest pickable day to the current value
    currentValue.day = 1;	//as a last resort, this day always exists, even if every day in the month is unpickable
    var tryDay = -99;
    var nDays = this.daysInMonth(currentValue.month, currentValue.year);

    for (var d = 1; d <= nDays; d++) {
      var yyyymmdd = '' + currentValue.year + this.padTo2(currentValue.month) + this.padTo2(d);

      if (    (params.weekendsUnpickable && this.isWeekend(yyyymmdd, adpDiv))
           || (params.holidaysUnpickable && this.isHoliday(yyyymmdd, adpDiv))
         ) {
        continue;
      }

      if (Math.abs(tryDay - adpDiv.anyDatePicker.targetDay) > Math.abs(d - adpDiv.anyDatePicker.targetDay)) {
        currentValue.day = tryDay = d;		//this day is a better fit than the previous best one
      }
    }
  }

  if (refill) {
    this.fillDiv(divId, adpDiv.anyDatePicker.targetElement.id, false, this.makeDateTimeString(adpDiv));
  }
  else {
    this.accept(divId, true);
  }
}

};

