/* ***** BEGIN LICENSE BLOCK *****
 * Licensed under Version: MPL 1.1/GPL 2.0/LGPL 2.1
 * Full Terms at http://bclary.com/lib/js/license/mpl-tri-license.txt
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Jeremy Hoppe code.
 *
 * The Initial Developer of the Original Code is
 * Jeremy Hoppe.
 * Portions created by the Initial Developer are Copyright (C) 2002
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s): Jeremy Hoppe <jeremyah83@yahoo.com>
 *
 * ***** END LICENSE BLOCK ***** */

/*
 * Thanks to Bob Clary for his many helpful suggestions.
 */

// Array of day-names used by xbCalendar.
xbCalendar.dayNames = [
  'Monday', 'Tuesday', 'Wednesday', 'Thursday',
  'Friday', 'Saturday', 'Sunday'
];

// Array of month-names used by xbCalendar
xbCalendar.monthNames = [
  'January', 'February', 'March', 'April', 'May',
  'June', 'July', 'August', 'September', 'October',
  'November', 'December'
];

// Various static class properties
xbCalendar._name = -1;
xbCalendar.SPACE_CHAR = String.fromCharCode(160);

_classes.registerClass('xbCalendar');

// xbCalendar Constructor
function xbCalendar(id, date, classprefix, dayNameFormat, firstDayOfWeek, windowRef)
{
  _classes.defineClass('xbCalendar', _prototype_func);

  this.init(id, date, classprefix, dayNameFormat, firstDayOfWeek, windowRef);

  function _prototype_func()
  {

    xbCalendar.prototype.init = init;
    function init(id, date, classprefix, dayNameFormat, firstDayOfWeek, windowRef)
    {
      this.parentMethod('init'); // super()
      this.id             = id;
      this.date           = date ? date : new Date();
      this.classprefix    = classprefix ? classprefix : 'cal';
      this.dayNameFormat  = dayNameFormat ? dayNameFormat.toLowerCase() : 'short';
      this.firstDayOfWeek = (typeof(firstDayOfWeek) == 'number') ? firstDayOfWeek : 6;
      this.window         = windowRef ? windowRef : window;

      // Properties
      this.cells   = new Array(41);
      this.headers = new Array(7);
      this.minDate = new Date(1970, 0, 1);
      this.maxDate = new Date(2099, 11, 31, 23, 59, 59);
      this.name    = 'xbCalendar' + (++xbCalendar._name);
      this.showStatusInfo = true;
      this.useTitles      = true;

      // Internal state properties
      this._needsLinkRefocus = false;
      this._nextIndex     = -1;
      this._prevIndex     = -1;
      this._selectedIndex = -1;

      // Events
      this.ondatechange = null;

      window[this.name] = this;

      // Calendar creation
      var doc = this.window.document;
      var clsprefix = this.classprefix;
      var thead, tbody;

      firstDayOfWeek = this.firstDayOfWeek;
      if ((firstDayOfWeek < 0) || (firstDayOfWeek > 6))
        throw new xbException('firstDayOfWeek out of range: ' + firstDayOfWeek, 'xbCalendar.js', 'xbCalendar::init()');

      // Create table    
      this.table = doc.createElement('table');
      this.table.setAttribute('id', this.id);
      this.table.className = clsprefix + 'calendar';

      // Create caption
      var caption = doc.createElement('caption');
      caption.className = (clsprefix + 'caption');

      var captionText = doc.createTextNode(xbCalendar.SPACE_CHAR);
      caption.appendChild(captionText);

      this.caption = caption;
      this.table.appendChild(caption);

      // Create thead
      thead = doc.createElement('thead');
      this.table.appendChild(thead);

      // Create tbody
      tbody = doc.createElement('tbody');
      this.table.appendChild(tbody);

      // Create header row
      var headerRow = doc.createElement('tr');
      thead.appendChild(headerRow);

      // Create day-name headers
      var headers = this.headers;
      for(var i=0, nameCounter=firstDayOfWeek; i < 7; i++)
      {
        var th = doc.createElement('th');
        headerRow.appendChild(th);
        th.className = clsprefix + 'header';
        var thText = doc.createTextNode(this._getDayName(nameCounter));

        th.appendChild(thText);
        headers[i] = th;

        if (++nameCounter >= 7)
          nameCounter = 0;
      }

      headerRow = null;
   
      // Create the rows and cols of the calendar
      var cells = this.cells;
      var tr;
      for(var i=0, counter = 0; i < 42; i++)
      {
        if (counter == 0)
        {
          tr = doc.createElement('tr');
          tbody.appendChild(tr);
        }
        counter = (++counter % 7);

        var td = doc.createElement('td');
        tr.appendChild(td);
        // seems unneeded -> td.id = (this.name + '_Cell_' + i);
        var tdNode = new xbCalendarCellNodeMap(i, td, this);
        cells[i] = tdNode;
      }
    }

    /* = = = = = = = = = = = = = = = = = = = =
     * Protected methods
     * = = = = = = = = = = = = = = = = = = = */

    // Formats the text for the calendar table's caption.
    xbCalendar.prototype._getCaptionFormat = _getCaptionFormat;
    function _getCaptionFormat(month, year)
    {
      return xbCalendar.monthNames[month] + ' ' + year;
    }

    // General date formatting.
    xbCalendar.prototype._getDateFormat = _getDateFormat;
    function _getDateFormat(dateObj)
    {
      // [Date].getDay() in ECMAScript/JavaScript returns 0 for Sunday,
      // 1 for Monday, etc, so we need to convert JS days to be compliant
      // with the ISO standard (Monday=1, Tuesday=2,...)
      var dayName = xbCalendar.dayNames[ (dateObj.getDay() + 6) % 7 ];

      var s = dayName + ', ' +
             (dateObj.getMonth() + 1) + '/' + dateObj.getDate() + '/' + dateObj.getFullYear();
      return s;
    }


    /* = = = = = = = = = = = = = = = = =
     * Public methods
     * = = = = = = = = = = = = = = = = */

    // Returns a Date object for the specified calendar table-cell index.
    xbCalendar.prototype.dateFromIndex = dateFromIndex;
    function dateFromIndex(index)
    {
      if (index < 0 || index > 41)
        throw new xbException('Index out of range: ' + index, 'xbCalendar.js', 'xbCalendar::dateFromIndex');

      var thisdate = this.date;
      var m = thisdate.getMonth();
      var y = thisdate.getFullYear();
      var daysInMonth  = (this._nextIndex - this._prevIndex);
      var startDay     = this._prevIndex + 1;
      var dateObj;

      if (index < startDay)
      {
        dateObj = this._getPrevMonth();
        var date = this.getMonthLength(dateObj.getMonth(), dateObj.getFullYear());
        date = ((date - startDay) + index) + 1;
        dateObj.setDate(date);
      }
      else if (index >= (startDay + daysInMonth))
      {
        dateObj = this._getNextMonth();
        var date = index - ((startDay + daysInMonth) - 1);
        dateObj.setDate(date);
      }
      else
      {
        dateObj = new Date(this.date.toUTCString());
        dateObj.setDate((index - startDay) + 1);
      }
      return dateObj;
    }

    // Destroys the current instance and all of the node-maps stored in arrays.
    xbCalendar.prototype.destroy = destroy;
    function destroy()
    {
      freeArray(this.cells);
      freeArray(this.headers);

      this.date    = null;
      this.maxDate = null;
      this.minDate = null;
      this.caption = null;
      this.table = null;
      this.ondatechange = null;
      this.window = null;

      window[this.name] = null;

      function freeArray(a)
      {
        for(var i=a.length -1; i >= 0; i--)
        {
          var o = a[i];
          if (typeof(o.destroy) == 'function')
            o.destroy();
          o = null;
          a[i] = null;
        }
      }
      this.parentMethod('destroy');
    }

    // Returns a Date object describing the calendar's currently selected date.
    xbCalendar.prototype.getDate = getDate;
    function getDate()
    {
      return new Date(this.date.toUTCString());
    }

    // Returns the length of a given month in the specified year.
    xbCalendar.prototype.getMonthLength = getMonthLength;
    function getMonthLength(monthIndex, year)
    {
      var days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
      if ((year % 4) == 0)
        days[1] = 29;
      return days[monthIndex];
    }

    // Returns the HTMLTableElement for this calendar.
    xbCalendar.prototype.getNode = getNode;
    function getNode()
    {
      return this.table;
    }

    // Returns the index in the cells[] array of the table
    // cell showing the specified Date, or -1 if not found.
    xbCalendar.prototype.indexFromDate = indexFromDate;
    function indexFromDate(dateObj)
    {
      var d = dateObj.getDate();
      var m = dateObj.getMonth();
      var y = dateObj.getFullYear();

      // We determine whether the querying Date should
      // be treated as a date before, during, or after
      // the current month by comparing millisecond values.

      // Set querying Date object's time to 00:00:00
      dateObj.setTime( new Date(y, m, d).valueOf() );

      var date      = this.date;
      var thisYear  = date.getFullYear();
      var thisMonth = date.getMonth();

      // The first day of this month at 00:00:00
      var startDate = new Date(thisYear, thisMonth, 1, 0, 0, 0);

      // The last day of this month at 23:59:59
      var endDate   = new Date(thisYear, thisMonth, this.getMonthLength(thisMonth, thisYear), 23, 59, 59);

      // Treat as previous month?
      if (dateObj < startDate)
      {
        var l = this.getMonthLength(m, y);
        var minDate = (l - this._prevIndex);

        if (d < minDate)
          return -1;
        else
          return (d - minDate);
      }
      // Treat as next month?
      else if (dateObj > endDate)
      {
        var maxDate = 41 - this._nextIndex;

        if (d > maxDate)
          return -1;
        else
          return (41 - (maxDate - d));
      }
      // In the current month
      else
      {
        return (this._prevIndex + d);
      }
    }

    // Refreshes the calendar with the next month
    xbCalendar.prototype.nextMonth = nextMonth;
    function nextMonth()
    {
      this.setDate( this._getNextMonth() );
    }

    // Refreshes the calendar with the next year
    xbCalendar.prototype.nextYear = nextYear;
    function nextYear()
    {
      var d = this.date;
      var m = d.getMonth();
      var y = d.getFullYear();

      this.setDate( new Date(++y, m, 1) );
    }

    // Refreshes the calendar with the previous month
    xbCalendar.prototype.prevMonth = prevMonth;
    function prevMonth()
    {
      var dateObj = this._getEarliestDateInRange( this._getPrevMonth() );
      this.setDate( dateObj );
    }

    // Refreshes the calendar with the next year
    xbCalendar.prototype.prevYear = prevYear;
    function prevYear()
    {
      var d = this.date;
      var m = d.getMonth();
      var y = d.getFullYear();

      this.setDate( new Date(--y, m, 1) );
    }

    // Sets the calendar's date to the specified Date object and calls refresh()
    xbCalendar.prototype.setDate = setDate;
    function setDate(dateObj)
    {
      var doRepaintOnly = false;
      var rv = true;
      var y = dateObj.getFullYear();

      // Return if dateObj is outside of the range of valid dates.
      if ((dateObj < this.minDate) || (dateObj > this.maxDate))
        return;

      if ((dateObj.getMonth() == this.date.getMonth()) &&
          (y == this.date.getFullYear()))
      {
        doRepaintOnly = true;
      }

      // Invoke any registered datechange handler
      if (typeof(this.ondatechange) == 'function')
      {
        rv = this.ondatechange( new Date(dateObj.toUTCString()) );
      }

      if (rv == false)
      {
        this._needsLinkRefocus = false; // Reset flag
        return;
      }
      this.date = dateObj;
      this.refresh(doRepaintOnly);
    }


    // Refreshes the calendar UI
    xbCalendar.prototype.refresh = refresh;
    function refresh(doRepaintOnly)
    {
      // Initialize variables we'll need
      var date = this.date;
      var d = date.getDate();
      var m = date.getMonth();
      var y = date.getFullYear();

      var cells       = this.cells;
      var cellLen     = cells.length;
      var clsprefix   = this.classprefix;
      var daysInMonth = this.getMonthLength(m, y);
      var minDate     = this.minDate;
      var maxDate     = this.maxDate;
      var maxLoop;
      var startDate   = new Date(y, m, 1);
      var startDay    = startDate.getDay();
      var useTitles   = this.useTitles;

      var firstDayOfWeek = this.firstDayOfWeek;
      if ((firstDayOfWeek < 0) || (firstDayOfWeek > 6))
        throw new xbException('firstDayOfWeek out of range: ' + firstDayOfWeek, 'xbCalendar.js', 'xbCalendar::refresh()');

      startDay += (6 - firstDayOfWeek);  // Add the firstDayOfWeek difference

      // Avoid allocating a whole seven days/cells to the previous month
      if (startDay >= 7)
        startDay -= 7;

      // Are we only repainting within the current month?
      if (doRepaintOnly)
      {
        var toCell = cells[startDay + d - 1];
        var fromCell = cells[this._selectedIndex];
        if (toCell == fromCell) return;

        fromCell.node.className     = clsprefix + 'date';
        fromCell.linkNode.className = clsprefix + 'dateLink';
        toCell.node.className       = clsprefix + 'currentDate';
        toCell.linkNode.className   = clsprefix + 'currentDateLink';
        this._selectedIndex         = startDay  + d - 1;
        return;
      }

      // Update _prevIndex and _nextIndex properties
      this._prevIndex = (startDay -1);
      this._nextIndex = (startDay + daysInMonth) -1;

      // Get the prev and next date and month info
      var prevDate     = this._getPrevMonth();
      var prevMonth    = prevDate.getMonth();
      var prevYear     = prevDate.getFullYear();
      var prevMonthLen = this.getMonthLength(prevMonth, prevYear);
      var nextDate     = this._getNextMonth();

      // Update calendar caption
      this.caption.firstChild.nodeValue = this._getCaptionFormat(m, y);

      // Update day-name headers
      for(var i=0, nameCounter = firstDayOfWeek; i < 7; i++)
      {
        this.headers[i].firstChild.nodeValue = this._getDayName(nameCounter);
        if (++nameCounter >= 7)
          nameCounter = 0;
      }

      // Update all cells in the current month
      maxLoop = (startDay + daysInMonth);
      for(var i=maxLoop-1, dayCounter=daysInMonth; i >= 0; i--, dayCounter--)
      {
        var map = cells[i];
        var link = map.linkNode;

        if (dayCounter == d)
        {
          map.node.className = clsprefix + 'currentDate';
          link.className     = clsprefix + 'currentDateLink';
        }
        else
        {
          map.node.className = clsprefix + 'date';
          link.className     = clsprefix + 'dateLink';
        }

        startDate.setDate(dayCounter);

        // If the current date is within the set range,
        // update link-text and title, and show the
        // link if it's currently hidden.
        if ( (startDate >= minDate) && (startDate <= maxDate) )
        {
          link.firstChild.nodeValue = dayCounter.toString();
          if (map.hidden)
          {
            link.style.visibility = 'inherit';
            map.hidden = false;
          }
          if (useTitles)
            link.title = this._getDateFormat(startDate);
        }
        // Otherwise, hide the link, and replace the
        // link-text with white-space.
        else
        {
          link.style.visibility = 'hidden';
          link.firstChild.nodeValue = xbCalendar.SPACE_CHAR;
          if (useTitles)
            link.title = '';
          map.hidden = true;
        }
      }

      this._selectedIndex = (startDay + d) - 1; // Save selected index

      // Update all cells in the previous month
      for(var i=startDay -1, dayCounter=prevMonthLen; i >= 0; i--, --dayCounter)
      {
        var map = cells[i];
        var link = map.linkNode;

        map.node.className = clsprefix + 'offDate';
        link.className     = clsprefix + 'offDateLink';

        prevDate.setDate(dayCounter);

        if ( (prevDate >= minDate) && (prevDate <= maxDate) )
        {
          link.firstChild.nodeValue = dayCounter.toString();
          if (map.hidden)
          {
            link.style.visibility = 'inherit';
            map.hidden = false;
          }
          if (useTitles)
            link.title = this._getDateFormat(prevDate);
        }
        else
        {
          link.style.visibility = 'hidden';
          link.firstChild.nodeValue = xbCalendar.SPACE_CHAR;
          if (useTitles)
            link.title = '';
          map.hidden = true;
        }
      }

      // Update all cells in the next month
      for(var i=startDay + daysInMonth, dayCounter=1; i < cellLen; i++, dayCounter++)
      {
        var map = cells[i];
        var link = map.linkNode;

        map.node.className = clsprefix + 'offDate';
        link.className = clsprefix + 'offDateLink';

        nextDate.setDate(dayCounter);

        if ( (nextDate >= minDate) && (nextDate <= maxDate) )
        {
          link.firstChild.nodeValue = dayCounter.toString();
          if (map.hidden)
          {
            link.style.visibility = 'inherit';
            map.hidden = false;
          }
          if (useTitles)
            link.title = this._getDateFormat(nextDate);
        }
        else
        {
          link.style.visibility = 'hidden';
          link.firstChild.nodeValue = xbCalendar.SPACE_CHAR;
          if (useTitles)
            link.title = '';
          map.hidden = true;
        }
      }

      if (this._needsLinkRefocus)
      {
        var node = this.cells[this._selectedIndex].linkNode;

        try
        {
          node.focus();
        }
        catch(e)
        {
          /* should never happen */
        }
        this._needsLinkRefocus = false;
      }
    } // end refresh()


    /* ===== (X)HTML Table Attribute setting functions ===== */

    // Sets the border of the calendar table
    xbCalendar.prototype.setBorder = setBorder;
    function setBorder(i)
    {
      this.table.border = i;
    }

    // Sets the cell-padding of the calendar table
    xbCalendar.prototype.setCellPadding = setCellPadding;
    function setCellPadding(i)
    {
      i = i.toString();
      this.table.cellPadding = i;
    }

    // Sets the cell-spacing of the calendar table
    xbCalendar.prototype.setCellSpacing = setCellSpacing;
    function setCellSpacing(i)
    {
      i = i.toString();
      this.table.cellSpacing = i;
    }

    /* = = = = = = = = = = = = = = = = = = = =
     * Private methods
     * These methods are for internal use only.
     * = = = = = = = = = = = = = = = = = = = */

    // A simple event-handler workaround for IE.
    // Returns a function to be used as an event handler,
    // and fixes part of IE's broken event model by passing
    // the Event object to the handler.
    xbCalendar.prototype._createEventHandler = _createEventHandler;
    function _createEventHandler(callbackFunc, windowRef)
    {
      // Workaround for IE
      if (document.all && !document.addEventListener)
      {
        if (!windowRef)
          windowRef = window;

        var handler = function(evt)
        {
          if (!evt)
            evt = windowRef.event;

          // Saves time later to do this here
          if (!evt.target && evt.srcElement)
            evt.target = evt.srcElement;

          evt.view = windowRef;
          var scopeRef = this;
          callbackFunc.call(scopeRef, evt);
        }
        return handler;
      }
      // Compliant UAs don't need any fix; just return the original function.
      else
      {
        return callbackFunc;
      }
    }

    // Returns the day name for the specified day of the week
    xbCalendar.prototype._getDayName = _getDayName;
    function _getDayName(day)
    {
      var s = xbCalendar.dayNames[day];
      switch(this.dayNameFormat)
      {
        case 'long':
          return s;

        case 'medium':
          return s.substring(0, 3);

        case 'short':
        default:
          return s.charAt(0);
      }
    }

    // This method is used by prevMonth() to enable users to
    // navigate to the first in-range date in a previous month,
    // even if the first-of-the-month is outside the defined range.
    xbCalendar.prototype._getEarliestDateInRange = function(dateObj)
    {
      var minDate = this.minDate;

      // If dateObj is in the same month and year as minDate,
      // check to see if the date field needs to be adjusted.
      if ( (dateObj.getMonth() == minDate.getMonth()) &&
           (dateObj.getFullYear() == minDate.getFullYear()) )
      {
        var date = minDate.getDate();
        if (dateObj.getDate() >= date)
        {
          return dateObj;
        }
        else
        {
          // Adjust the date field so that users can navigate backwards
          dateObj.setDate(date);
          return dateObj;
        }
      }
      else
      {
        return dateObj;
      }
    }

    // Returns a Date object for the next month
    xbCalendar.prototype._getNextMonth = _getNextMonth;
    function _getNextMonth()
    {
      var thisdate = this.date;
      var m = thisdate.getMonth();
      var y = thisdate.getFullYear();

      if ((++m) > 11)
      {
        m = 0;
        ++y;
      }
      return new Date(y, m, 1);
    }

    // Returns a Date object for the previous month
    xbCalendar.prototype._getPrevMonth = _getPrevMonth;
    function _getPrevMonth()
    {
      var thisdate = this.date;
      var m = thisdate.getMonth();
      var y = thisdate.getFullYear();

      if ((--m) < 0)
      {
        m = 11;
        --y;
      }
      return new Date(y, m, 1);
    }

    // Invoked when one of the following events occurs on a link
    // in one of the calendar cells: click, mouseover, mouseout
    xbCalendar.prototype._linkEvent = _linkEvent;
    function _linkEvent(evt)
    {
      // Event object should always be passed to us!
      if (!evt)
        throw new xbException('Event object not passed to _linkEvent', 'xbCalendar.js', 'xbCalendar::_linkEvent');

      // Get a reference to event target's Window object
      var windowRef = evt.view;

      // Get event target
      var elem = evt.target;

      if (elem.nodeType == 3)
        elem = elem.parentNode;

      // IDs appear like this: "xbCalendarX_LinkNode_index"
      var id      = elem.id;
      var calName = id.substring(0, id.indexOf('_')); 
      var inst    = window[calName];
      var index   = parseInt(id.substring(id.lastIndexOf('_') + 1));

      var evtType = evt.type; // Get event type

      // We work with IE's returnValue differently depending on event type
      var IEReturnVal = false;
      if (evtType == 'mouseover' || evtType == 'mouseout')
          IEReturnVal = true;

      // Stop event propagation
      if (evt.stopPropagation)
        evt.stopPropagation();
      else
        evt.cancelBubble = true;

      // Prevent the event's default action
      if (evt.preventDefault)
        evt.preventDefault();
      else
        evt.returnValue = IEReturnVal;

      if (!inst)  // Make sure we have an object
        return;

      if (evtType == 'click')
      {
        // Make sure the focus is placed on the correct link
        // (it wouldn't otherwise be if we change months,
        // as the orientation of the dates in the table would change).
        inst._needsLinkRefocus = true;

        var dateObj = inst.dateFromIndex(index);
        inst.setDate(dateObj);
      }

      else if (evtType == 'mouseover' && inst.showStatusInfo)
      {
        windowRef.status = inst._getDateFormat(inst.dateFromIndex(index));
      }

      else if (evtType == 'mouseout' && inst.showStatusInfo)
      {
        windowRef.status = (windowRef.defaultStatus) ? windowRef.defaultStatus : '';
      }
    }
  } // end prototype func
}


/*
 * Class xbCalendarCellNodeMap
 * Creates the link architecture for an individual
 * table cell and registers event handlers on the link.
 */
_classes.registerClass('xbCalendarCellNodeMap');

function xbCalendarCellNodeMap(index, cellNode, ownerCalendar)
{
  _classes.defineClass('xbCalendarCellNodeMap', _prototype_func);

  this.init(index, cellNode, ownerCalendar);

  function _prototype_func()
  {
    xbCalendarCellNodeMap.prototype.init = init;
    function init(index, cellNode, ownerCalendar)
    {
      this.parentMethod('init');

      var doc = ownerCalendar.window.document;
      this.index = index;
      this.node = cellNode;
      this.linkNode = doc.createElement('a');
      this.hidden   = false;

      var a = this.linkNode;
      a.id = (ownerCalendar.name + '_CellLink_' + this.index);
      a.href = '#';

      var text = doc.createTextNode(xbCalendar.SPACE_CHAR);
      a.appendChild(text);

      var windowRef = ownerCalendar.window;
      var callbackFunc = ownerCalendar._linkEvent;

      var handler = ownerCalendar._createEventHandler(callbackFunc, windowRef);
      a.onmouseover = handler;
      a.onmouseout  = handler;
      a.onclick     = handler;

      cellNode.appendChild(a);
    }

    xbCalendarCellNodeMap.prototype.destroy = destroy;
    function destroy()
    {
      this.linkNode = null;
      this.node = null;
    }
  }
}
