    var CalendarsByTable = new Array();
    var CalendarIdPrefix = "Calendar_";
    var IsIE = document.all;

    /**
    * Maps from <table> to Calendar -- for use in event handlers to get the Calendar in question.
    */
    function getCalendarForCell (cell)
    {
        var tbl = cell;
        while (tbl.tagName != 'TABLE') {
            tbl = tbl.offsetParent;
        }
        var index = tbl.id.replace(CalendarIdPrefix, '');
        var calendar = CalendarsByTable[parseInt(index)];
        return calendar;
    }

    function Calendar (calTable, userFunction, userFormat, userTarget, initFunction)
    {
        var index = CalendarsByTable.length;
        calTable.setAttribute("id", CalendarIdPrefix + index);
        CalendarsByTable[index] = this;

        this.calendarTable = calTable;
        this.userFunction = userFunction;
        this.userFormat = userFormat;
        this.userTarget = userTarget;

        var currentDate;
        if (initFunction) {
            currentDate = eval(initFunction+"()");
        }

        if (!currentDate) {
            currentDate = new Date();
        }
        this.calendarDate = currentDate;    // month to show
        this.currentDate = currentDate;     // highlight date

        // Methods
        this.renderCalendarDays = CAL_renderCalendarDays;
        this.renderDayNames = CAL_renderDayNames;

        this.renderDayNames();
        this.renderCalendarDays();
    }

    function CAL_renderDayNames ()
    {
        var calTable = this.calendarTable;
        var rows = calTable.rows;
        var cells = getRowCells(rows[1]);
        for (var i=0; i < cells.length; i++) {
            var index = i+AWDayOfWeekStart;
            if (index > 6) {
                index -= 7;
            }
            cells[i].innerHTML = AWShortWeekdayNames[index];
        }
    }

    function CAL_renderCalendarDays ()
    {
        var calTable = this.calendarTable;

        var currentYear = this.currentDate.getFullYear();
        var currentMonth = this.currentDate.getMonth();
        var currentDay = this.currentDate.getDate();

        var date = new Date(Date.parse(this.calendarDate));
        var daysInMonth = computeDaysInMonth(date);
        date.setDate(1);
        var firstDayOfMonth = date.getDay() - AWDayOfWeekStart;
        if (firstDayOfMonth < 0) {
            firstDayOfMonth+=7;
        }

        var rows = calTable.rows;
        var month = date.getMonth();
        var fullYear = date.getFullYear();

        var cells = getRowCells(rows[0]);
        var cellChildren = awgetChildren(cells[2]);
        var monthYearSpan = cellChildren[0];

        monthYearSpan.innerHTML = formatMonthYear(AWCalendarLabelPattern, date);

        var dayOfMonth = 1;
        var FirstRowIndex = 2;
        for (rowIndex = FirstRowIndex; rowIndex < rows.length; rowIndex++) {
            var currentRow = rows[rowIndex];
            if (dayOfMonth <= daysInMonth) {
                currentRow.style.display = "";
                var cols = getRowCells(currentRow);
                for (colIndex = 0; colIndex < cols.length; colIndex++) {
                    var cell = cols[colIndex];
                    if ((rowIndex > FirstRowIndex || colIndex >= firstDayOfMonth) &&
                        dayOfMonth <= daysInMonth) {
                        var isToday = (dayOfMonth == currentDay) && (month == currentMonth) && (fullYear == currentYear);
                        var className = isToday ? " class='today'" : "";
                        var dayString = dayOfMonth;
                        cell.innerHTML = "<a href='#' onFocus='awdayMouseOver(this)' onMouseOut='awclearLastMouseOverDay();' onMouseOver='awdayMouseOver(this)' onMouseDown='return awdayClicked(this);' onKeyDown='return awdayKeyDown(event, this);' tabIndex='1'" + className + ">" + dayString + "</a>";
                        dayOfMonth++;
                    }
                    else {
                        cell.innerHTML = "&nbsp;";
                    }
                }
            }
            else {
                currentRow.style.display = "none";
            }
        }
    }

    //////////////////
    // Event Handlers
    //////////////////
    var AWLastDay;
    var AWFocusStyle = "calendar_focus";

    function awclearLastMouseOverDay ()
    {
        if (AWLastDay) {
            awRemoveClass(AWLastDay, AWFocusStyle);
            AWLastDay = null;
        }
    }

    function awdayMouseOver (cell)
    {
        awclearLastMouseOverDay();
        awAddClass(cell, AWFocusStyle);
        AWLastDay = cell;
    }

    function awdayClicked (cell)
    {
        // Need to the cell bgcolor since the hover doesn't get reset as
        // the calendar goes away when clicked and the mouse never exits the cell.
        // Can't use "parse css stylesheet for rule" strategy since IE will throw
        // a "access is denied" violation if the style sheet is served from a
        // different host than the page
        awclearLastMouseOverDay();

        // now handle clicked
        var calendar = getCalendarForCell(cell);
        var calendarDate = calendar.calendarDate;
        var dayString = strtrim(cell.innerHTML);
        if (dayString.length > 0) {
            var date = new Date(calendarDate.getFullYear(), calendarDate.getMonth(), parseInt(dayString));
            var expr = calendar.userFunction + "(date, calendar.userFormat, calendar.userTarget)";
            eval(expr);
        }
        return true;
    }

    function awdayKeyDown (mevent, cell)
    {
        var keyCode = awkeyCode(mevent);
        if (keyCode == 16) {
            // ignore shift button press
            return false;
        }
        else if (keyCode == 13) {
            awdayClicked(cell);
            awhideActiveMenu();
            return false;
        }
        else if (keyCode == 27) {
            // Escape key
            awhideActiveMenu();
            return false;
        }
        else if (keyCode == 9) {
            // tab key
            if (mevent.shiftKey) {
                var firstMenuLink = awfirstMenuLinkRecursive(AWActiveMenu);
                if (firstMenuLink != null && firstMenuLink == aweventSourceElement(mevent)) {
                    var lastMenuLink = awlastMenuLinkRecursive(AWActiveMenu);
                    if (lastMenuLink != null) {
                        lastMenuLink.focus();
                        return false;
                    }
                }
            }
            else {
                var lastMenuLink = awlastMenuLinkRecursive(AWActiveMenu);
                if (lastMenuLink != null && lastMenuLink == aweventSourceElement(mevent)) {
                    var firstMenuLink = awfirstMenuLinkRecursive(AWActiveMenu);
                    if (firstMenuLink != null) {
                        firstMenuLink.focus();
                        return false;
                    }
                }
            }
            return true;
        }
        // should not get here
        return false;
    }

    function awMonthYearKeyDown (mevent, cell)
    {
        var keyCode = awkeyCode(mevent);
        if (keyCode == 13) {
            // let onclick handle this
            aweventSourceElement(mevent).onmousedown(mevent);
            return true;
        }
        else {
            return awdayKeyDown(mevent, cell);
        }

        // should not get here
        return false;
    }

    function awPreviousMonthClicked (event)
    {
        var cell = aweventSourceElement(event);
        var calendar = getCalendarForCell(cell);
        var calendarDate = calendar.calendarDate;
        var year = calendarDate.getFullYear();
        var month = calendarDate.getMonth() - 1;
        if (month == -1) {
            month = 11;
            year--
        }
        var date = new Date(year, month, 1);
        calendar.calendarDate = date;
        calendar.renderCalendarDays();
        awcancelBubble(event);
        return false;
    }

    function awNextMonthClicked (event)
    {
        var cell = aweventSourceElement(event);
        var calendar = getCalendarForCell(cell);
        var calendarDate = calendar.calendarDate;
        var year = calendarDate.getFullYear();
        var month = calendarDate.getMonth() + 1;
        if (month == 12) {
            month = 0;
            year++;
        }
        var date = new Date(year, month, 1);
        calendar.calendarDate = date;
        calendar.renderCalendarDays();
        awcancelBubble(event);
        return false;
    }

    function awPreviousYearClicked (event)
    {
        var cell = aweventSourceElement(event);
        var calendar = getCalendarForCell(cell);
        var calendarDate = calendar.calendarDate;
        var year = calendarDate.getFullYear() - 1;
        var month = calendarDate.getMonth();
        var date = new Date(year, month, 1);
        calendar.calendarDate = date;
        calendar.renderCalendarDays();
        awcancelBubble(event);
    }

    function awNextYearClicked (event)
    {
        var cell = aweventSourceElement(event);
        var calendar = getCalendarForCell(cell);
        var calendarDate = calendar.calendarDate;

        var year = calendarDate.getFullYear() + 1;
        var month = calendarDate.getMonth();
        var date = new Date(year, month, 1);
        calendar.calendarDate = date;
        calendar.renderCalendarDays();
        awcancelBubble(event);
    }

    ///////////////
    // String util
    ///////////////

    function strtrim (str)
    {
        //Match spaces at beginning and end of text and replace with null strings
        return str.replace(/^\s+/, '').replace(/\s+$/, '');
    }

    /////////////
    // DOM util
    /////////////

    /**
        This handles the differences between netscape and safari.
    */
    function getRowCells (trow)
    {
        var cells = trow.children;
        if (cells == null) {
            cells = trow.cells;
        }
        return cells;
    }

    /////////////
    // Date util
    /////////////

    function computeDaysInMonth (date)
    {
        var temp = new Date(date.getFullYear(), date.getMonth(), date.getDate());
        temp.setDate(32);
        return 32 - temp.getDate();
    }

    function getMonthName (date)
    {
        var month = date.getMonth();
        return AWMonthNames[month];
    }

    ////////////////
    // Div display
    ////////////////
    function hideDiv (div)
    {
        div.style.display = "none";
    }

    function showDiv (div)
    {
        div.style.display = "";
    }

    function toggleDiv (divName)
    {
        var div = document.getElementById(divName);
        if (div.style.display == "none") {
            showDiv(div);
        }
        else {
            hideDiv(div);
        }
    }

    ////////////////
    // Month / Year Label Format
    ////////////////
    var AWShortMonthPattern = /\{AWShortMonth\}/;
    var AWMonthPattern = /\{AWMonth\}/;
    var AWShortYearPattern = /\{AWShortYear\}/;
    var AWYearPattern = /\{AWYear\}/;

    function formatMonthYear (monthYearLabelPattern, date)
    {
        var month = date.getMonth();
        var fullYear = new String(date.getFullYear());

        var monthYearLabel =
            monthYearLabelPattern.replace(AWShortMonthPattern, AWShortMonthNames[month]);
        monthYearLabel = monthYearLabel.replace(AWMonthPattern, AWMonthNames[month]);
        monthYearLabel = monthYearLabel.replace(AWShortYearPattern, fullYear.substr(2));
        monthYearLabel = monthYearLabel.replace(AWYearPattern, fullYear);
        return monthYearLabel;
    }

////////////////
// Date Format
////////////////
function awParseDate (dateString, pattern)
{
    // ### todo: implement this to allow bidirectional binding to/from calendar
    debug("awParseDate not yet implemented");
}

function awformatDate (date, pattern)
{
    if (null == pattern) {
        return "";
    }

    var toAppendTo = new String();
    // inQuote set true when hits 1st single quote
    var inQuote = new Boolean(false);
    var prevCh = "";
    // number of time pattern characters repeated
    var count = 0;
    // Number of characters between quotes
    var interQuoteCount = 1;

    for (var i=0; i < pattern.length; i++) {
        var ch = pattern.charAt(i);
        if (true == inQuote) {
            if (ch == '\'') {
                    // ends with 2nd single quote
                inQuote = false;
                if (0 == count) {
            // two consecutive quotes outside a quote: ''
                    toAppendTo = toAppendTo + ch;
                }
                else {
                    count = 0;
                }
                interQuoteCount = 0;
            }
            else {
                toAppendTo = toAppendTo + ch;
                count++;
            }
        }
        // !inQuote
        else {
            if ('\'' == ch) {
                inQuote = true;
                // handle cases like: yyyy'....
                if (count > 0) {
                    toAppendTo = toAppendTo + awsubFormat(prevCh, count, date);
                    count = 0;
                    prevCh = "";
                }

                // We count characters between quotes so we can recognize
                // two single quotes inside a quote.  Example: 'o''clock'.
                if (0 == interQuoteCount) {
                    toAppendTo = toAppendTo + ch;
                    // Make it look like we never left.
                    count = 1;
                }
            }
            else if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) {
                // ch is a date-time pattern
                // handle cases: eg, yyyyMMdd
                if (ch != prevCh && count > 0) {
                    toAppendTo = toAppendTo + awsubFormat(prevCh, count, date);
                    prevCh = ch;
                    count = 1;
                }
                else {
                    if (ch != prevCh) {
                        prevCh = ch;
                    }
                    count++;
                }
            }
            // handle cases like: MM-dd-yy or HH:mm:ss
            else if (count > 0) {
                toAppendTo = toAppendTo + awsubFormat(prevCh, count, date);
                toAppendTo = toAppendTo + ch;
                prevCh = "";
                count = 0;
            }
            // any other unquoted characters
            else {
                toAppendTo = toAppendTo + ch;
            }
            interQuoteCount++;
        }
    }
    // Format the last item in the pattern
    if (count > 0) {
        toAppendTo = toAppendTo + awsubFormat(prevCh, count, date);
    }
    return toAppendTo;
}

// Private member function that does the real date/time formatting.
// returns String
function awsubFormat (ch, count, date)
{
    var current = "";
    var value = 0;

    switch (ch) {
    case 'y':
        if (count >= 4) {
            current = date.getFullYear();
        }
        else { // count < 4
            current = date.getYear();
        }
        break;
    case 'M':
        if (count >= 4) {
            current = AWMonthNames[date.getMonth()];
        }
        else if (count == 3) {
            current = AWShortMonthNames[date.getMonth()];
        }
        else {
            current = date.getMonth()+1;
            if (count > 1) {
                if (current < 10) {
                    current = "0" + current;
                }
            }
        }
        break;
    // DAY_OF_WEEK
    case 'E':
        if (count >= 4) {
            current = AWWeekdayNames[date.getDay()];
        }
        // count < 4, use abbreviated form if exists
        else {
            current = AWShortWeekdayNames[date.getDay()];
        }
        break;
    case 'K': // HOUR:1-based.  eg, 11PM + 1 hour =>> 12 AM (no idea why there is both 'K' and 'h'!)
    case 'h': // HOUR:1-based.  eg, 11PM + 1 hour =>> 12 AM
        current = d.getHours();
        if (current > 12) {
            current -= 12;
        }
        if (count > 1) {
            if (current < 10) {
                current = "0" + current;
            }
        }
        break;
    case 'k': // HOUR_OF_DAY:0-based.  eg, 23:59 + 1 hour =>> 00:59 (no idea why there is both 'k' and 'H'!)
    case 'H': // HOUR_OF_DAY:0-based.  eg, 23:59 + 1 hour =>> 00:59
        current = date.getHours();
        if (count > 1) {
            if (current < 10) {
                current = "0" + current;
            }
        }
        break;
    case 'd':
        current = date.getDate();
        if (count > 1) {
            if (current < 10) {
                current = "0" + current;
            }
        }
        break;
    case 'm':
        current = date.getMinutes();
        if (count > 1) {
            if (current < 10) {
                current = "0" + current;
            }
        }
        break;
    case 's':
        current = date.getSeconds();
        if (count > 1) {
            if (current < 10) {
                current = "0" + current;
            }
        }
        break;
    case 'a':
        var hours = date.getHours();
        if (hours < 12) {
            current = AWAMString;
        }
        else {
            current = AWPMString;
        }
    default:
        break;
    }
    return current;
}
