////////////////////////////////////////////////////////////////////////////////
// Constants
////////////////////////////////////////////////////////////////////////////////

var __CFCALENDAR_DATE_BOX_AMOUNT = 42;
var __CFCALENDAR_DAYS_IN_WEEK = 7;

var __CFCALENDAR_ERROR_DATE_BOX_ID = "invalid CFCalendarDateBox id";
var __CFCALENDAR_ERROR_DATE_BOX_IDS = "invalid dateBoxIds argument";
var __CFCALENDAR_ERROR_DAY_BOX_NUMBER = "invalid day box number";
var __CFCALENDAR_ERROR_DATE_DIALOG_BOX_ID =
    "invalid CFCalendarDateDialogBox id";
var __CFCALENDAR_ERROR_DAY_BOX_IDS = "invalid dayBoxIds argument";
var __CFCALENDAR_ERROR_ID = "invalid CFCalendar id";

var __CFCALENDAR_DATE_BOX_HOVER_CLASS = "cf-calendar-date-box-hover";
var __CFCALENDAR_DATE_BOX_SCHEDULED_CLASS = "cf-calendar-date-box-scheduled"
var __CFCALENDAR_DATE_BOX_SELECTED_CLASS = "cf-calendar-date-box-selected";
var __CFCALENDAR_DAY_BOX_HOVER_CLASS = "cf-calendar-day-box-hover";

////////////////////////////////////////////////////////////////////////////////
// Static Variables
////////////////////////////////////////////////////////////////////////////////

var __cfCalendarDateBoxMap = {};
var __cfCalendarDateDialogBoxMap = {};
var __cfCalendarMap = {};

////////////////////////////////////////////////////////////////////////////////
// Classes
////////////////////////////////////////////////////////////////////////////////

// CFCalendar

function CFCalendar(id, yearSpinnerId, monthSelectBoxId, dayBoxIds, dateBoxIds,
                    dialogBoxIds, yearValue, monthValue, dateValue)
{
    CFWidget.call(this, id);
    if (dateBoxIds.length != __CFCALENDAR_DATE_BOX_AMOUNT) {
        return cfErrorTrigger("CFCalendar: " + __CFCALENDAR_ERROR_DATE_BOX_IDS);
    }
    if (dayBoxIds.length != __CFCALENDAR_DAYS_IN_WEEK) {
        return cfErrorTrigger("CFCalendar: " + __CFCALENDAR_ERROR_DAY_BOX_IDS);
    }
    var dateBoxes = new Array();
    for (var i = 0; i < __CFCALENDAR_DATE_BOX_AMOUNT; i++) {
        dateBoxes.push(cfCalendarDateBoxGet(dateBoxIds[i]));
    }
    var dayBoxes = new Array();
    var dayNames = new Array();
    for (var i = 0; i < __CFCALENDAR_DAYS_IN_WEEK; i++) {
        var dayBox = cfElementGet(dayBoxIds[i]);
        dayBoxes.push(dayBox);
        dayNames.push(cfElementGetInnerHTML(dayBox));
    }
    var dialogBoxMap = {};
    for (var i = 0; i < dialogBoxIds.length; i++) {
        var box = cfCalendarDateDialogBoxGet(dialogBoxIds[i]);
        var date = box.getDate();
        var year = date.getYear().toString();
        var yearData = dialogBoxMap[year];
        if (typeof(yearData) == "undefined") {
            dialogBoxMap[year] = yearData = {};
        }
        var month = date.getMonth().toString();
        var monthData = yearData[month];
        if (typeof(monthData) == "undefined") {
            yearData[month] = monthData = {};
        }
        monthData[date.getDate().toString()] = box;
    }
    var monthSelectBox = cfElementGet(monthSelectBoxId);
    var yearSpinner = cfSpinnerGet(yearSpinnerId);
    this.__dateBoxes = dateBoxes;
    this.__dayBoxes = dayBoxes;
    this.__dayNames = dayNames;
    this.__dialogBoxMap = dialogBoxMap;
    this.__firstDay = 0;
    this.__monthSelectBox = monthSelectBox;
    this.__yearSpinner = yearSpinner;
    this.setValue(new Date(yearValue, monthValue - 1, dateValue), true);
    __cfCalendarMap[id] = this;
    var f = cfEventHandlerCreate(this.__handleMonthUpdate.bind(this));
    cfSelectSetChangeCallback(monthSelectBox, f);
    f = cfEventHandlerCreate(this.__handleYearUpdate.bind(this));
    yearSpinner.addEventHandler(CFWIDGET_EVENT_USER_UPDATE, f);
    for (var i = 0; i < __CFCALENDAR_DATE_BOX_AMOUNT; i++) {
        var box = dateBoxes[i];
        f = cfEventHandlerCreate(this.__handleDateBoxSelect.bind(this, box));
        box.addEventHandler(CFWIDGET_EVENT_USER_SELECT, f);
    }
    for (var i = 0; i < __CFCALENDAR_DAYS_IN_WEEK; i++) {
        var box = dayBoxes[i];
        f = cfEventHandlerCreate(this.__handleDayBoxClick.bind(this, i));
        box.onclick = f;
        f = cfEventHandlerCreate(this.__handleDayBoxMouseOut.bind(this, i));
        box.onmouseout = f;
        f = cfEventHandlerCreate(this.__handleDayBoxMouseOver.bind(this, i));
        box.onmouseover = f;
    }
}

CFCalendar.extendClasses(CFWidget);

CFCalendar.prototype.__handleDateBoxSelect = function(box)
{
    var oldValue = this.getValue();
    var value = box.getDate();
    value.setHours(oldValue.getHours());
    value.setMinutes(oldValue.getMinutes());
    value.setSeconds(oldValue.getSeconds());
    this.setValue(value, false);
    this.__triggerEvent(CFWIDGET_EVENT_USER_UPDATE);
}

CFCalendar.prototype.__handleDayBoxClick = function(dayBoxNumber)
{
    if (dayBoxNumber) {
        var firstDay = this.__firstDay;
        if ((dayBoxNumber < 0) || (dayBoxNumber >= __CFCALENDAR_DAYS_IN_WEEK)) {
            return cfErrorTrigger("CFCalendar::__handleDayBoxSelect: '" +
                                  dayBoxNumber + "': " +
                                  __CFCALENDAR_ERROR_DAY_BOX_NUMBER);
        }
        var dayBoxes = this.__dayBoxes;
        var dayNames = this.__dayNames;
        var newFirstDay = (dayBoxNumber + firstDay) % __CFCALENDAR_DAYS_IN_WEEK;
        for (var i = 0, n = newFirstDay; i < __CFCALENDAR_DAYS_IN_WEEK;
             i++, n++) {
            n %= __CFCALENDAR_DAYS_IN_WEEK;
            cfElementSetInnerText(dayBoxes[i], dayNames[n]);
        }
        this.__firstDay = newFirstDay;
        this.__refreshDateBoxes(true);
        return false;
    }
}

CFCalendar.prototype.__handleDayBoxMouseOut = function(dayBoxNumber)
{
    cfElementDiscardClass(this.__dayBoxes[dayBoxNumber],
                          __CFCALENDAR_DAY_BOX_HOVER_CLASS);
    return false;
}

CFCalendar.prototype.__handleDayBoxMouseOver = function(dayBoxNumber)
{
    cfElementAddClass(this.__dayBoxes[dayBoxNumber],
                      __CFCALENDAR_DAY_BOX_HOVER_CLASS);
    return false;
}

CFCalendar.prototype.__handleMonthUpdate = function()
{
    var month = cfSelectGetValue(this.__monthSelectBox) - 1;
    var value = this.__value;
    if (value.getMonth() != month) {
        value = new Date(value);
        value.setMonth(month);
        if (value.getMonth() != month) {
            value.addDays(-value.getDate());
        }
        this.setValue(value, true);
        this.__triggerEvent(CFWIDGET_EVENT_USER_UPDATE);
    }
    return false;
}

CFCalendar.prototype.__handleYearUpdate = function()
{
    var year = this.__yearSpinner.getValue();
    var value = this.__value;
    if (value.getFullYear() != year) {
        value = new Date(value);
        value.setFullYear(year);
        this.setValue(value, true);
        this.__triggerEvent(CFWIDGET_EVENT_USER_UPDATE);
    }
    return false;
}

CFCalendar.prototype.__refreshDateBoxes = function(ignorePopup)
{
    var value = new Date(this.__value);
    var d = new Date(value);
    d.setDate(1);
    var dateBoxes = this.__dateBoxes;
    var firstDay = this.__firstDay;
    var boxData = this.__dialogBoxMap[d.getYear().toString()];
    if (typeof(boxData) != "undefined") {
        boxData = boxData[d.getMonth().toString()];
        if (typeof(boxData) == "undefined") {
            boxData = {};
        }
    } else {
        boxData = {};
    }
    var day = d.getDay();
    var index = 0;
    for (var dIter = firstDay; (dIter % __CFCALENDAR_DAYS_IN_WEEK) != day;
         dIter++, index++) {
        dateBoxes[index].setData(undefined, undefined);
    }
    this.__firstDateIndex = index;
    var date = value.getDate();
    var month = value.getMonth();
    for (; d.getMonth() == month; index++) {
        var dateBox = dateBoxes[index];
        var dateNumber = d.getDate();
        var selected = (date == dateNumber);
        dateBox.setData(d, boxData[dateNumber.toString()], selected,
                        ignorePopup);
        if (selected) {
            this.__selectedDateBox = dateBox;
        }
        d.addDays(1);
    }
    for (; index < __CFCALENDAR_DATE_BOX_AMOUNT; index++) {
        dateBoxes[index].setData(undefined, undefined);
    }
}

CFCalendar.prototype.getValue = function()
{
    return new Date(this.__value);
}

CFCalendar.prototype.setValue = function(value, ignorePopup)
{
    var oldValue = this.__value;
    var value = new Date(value);
    var updatedMonth = value.getMonth();
    var updatedYear = value.getFullYear();
    this.__value = value;
    if ((typeof(oldValue) == "undefined") ||
        (oldValue.getMonth() != updatedMonth) ||
        (oldValue.getFullYear() != updatedYear)) {
        cfSelectSetValue(this.__monthSelectBox, (updatedMonth + 1).toString());
        this.__yearSpinner.setValue(updatedYear);
        this.__refreshDateBoxes(ignorePopup);
    } else {
        // Speeds up date selects to separate this code from the above code.
        var dateBox;
        var newDate = value.getDate();
        if (oldValue.getDate() != newDate) {
            dateBox = this.__selectedDateBox;
            dateBox.setData(dateBox.getDate(), dateBox.getDialogBox(), false);
        }
        dateBox = this.__dateBoxes[this.__firstDateIndex + newDate - 1];
        dateBox.setData(dateBox.getDate(), dateBox.getDialogBox(), true,
                        ignorePopup);
        this.__selectedDateBox = dateBox;
    }
}

// CFCalendarDateBox

function CFCalendarDateBox(id, dateDisplayBoxId, dataBoxId)
{
    CFWidget.call(this, id);
    this.__dataBox = dataBoxId.length ? cfElementGet(dataBoxId) : undefined;
    this.__dateDisplayBox = cfElementGet(dateDisplayBoxId);
    this.setData(undefined, undefined);
    __cfCalendarDateBoxMap[id] = this;
    var element = this.getElement();
    var f = cfEventHandlerCreate(this.__handleClickEvent.bind(this));
    element.onclick = f;
    f = cfEventHandlerCreate(this.__handleMouseOutEvent.bind(this));
    element.onmouseout = f;
    f = cfEventHandlerCreate(this.__handleMouseOverEvent.bind(this));
    element.onmouseover = f;
}

CFCalendarDateBox.extendClasses(CFWidget);

CFCalendarDateBox.prototype.__handleClickEvent = function()
{
    var date = this.__date;
    if (typeof(date) != "undefined") {
        this.__triggerEvent(CFWIDGET_EVENT_USER_SELECT);
        return true;
    }
}

CFCalendarDateBox.prototype.__handleMouseOutEvent = function()
{
    cfElementDiscardClasses(this.getElement(),
                            __CFCALENDAR_DATE_BOX_HOVER_CLASS);
}

CFCalendarDateBox.prototype.__handleMouseOverEvent = function()
{
    cfElementAddClass(this.getElement(), __CFCALENDAR_DATE_BOX_HOVER_CLASS);
}

CFCalendarDateBox.prototype.getDate = function()
{
    var date = this.__date;
    if (typeof(date) != "undefined") {
        date = new Date(date);
    }
    return date;
}

CFCalendarDateBox.prototype.getDialogBox = function()
{
    return this.__dialogBox;
}

CFCalendarDateBox.prototype.setData = function(date, box, selected, ignorePopup)
{
    cfElementSetInnerText(this.__dateDisplayBox,
                          (typeof(date) == "undefined") ? "  " :
                          date.getDate());
    var dataBox = this.__dataBox;
    var element = this.getElement();
    var classes = cfElementGetClasses(element);
    var index;
    if (typeof(box) != "undefined") {
        if (! classes.contains(__CFCALENDAR_DATE_BOX_SCHEDULED_CLASS)) {
            classes.push(__CFCALENDAR_DATE_BOX_SCHEDULED_CLASS);
        }
        if (typeof(dataBox) != "undefined") {
            cfElementSetInnerHTML(dataBox, box.getContentPaneHTML());
        }
    } else {
        classes.discardAll(__CFCALENDAR_DATE_BOX_SCHEDULED_CLASS);
        if (typeof(dataBox) != "undefined") {
            cfElementSetInnerHTML(dataBox, "&nbsp;");
        }
    }
    if (selected) {
        if (! classes.contains(__CFCALENDAR_DATE_BOX_SELECTED_CLASS)) {
            classes.push(__CFCALENDAR_DATE_BOX_SELECTED_CLASS);
        }
        if ((typeof(dataBox) == "undefined") && (typeof(box) != "undefined") &&
            (! ignorePopup)) {
            box.show();
        }
    } else {
        classes.discardAll(__CFCALENDAR_DATE_BOX_SELECTED_CLASS);
    }
    if (typeof(date) != "undefined") {
        date = new Date(date);
    }
    cfElementSetClasses(element, classes);
    this.__date = date;
    this.__dialogBox = box;
}

// CFCalendarDateDialogBox

function CFCalendarDateDialogBox(id, titleBoxId, contentPaneId, closeButtonId,
                                 year, month, date)
{
    CFDialogBox.call(this, id, titleBoxId, contentPaneId, closeButtonId);
    this.__date = new Date(year, month - 1, date);
    __cfCalendarDateDialogBoxMap[id] = this;
}

CFCalendarDateDialogBox.extendClasses(CFDialogBox);

CFCalendarDateDialogBox.prototype.getDate = function()
{
    return new Date(this.__date);
}

////////////////////////////////////////////////////////////////////////////////
// Public API
////////////////////////////////////////////////////////////////////////////////

function cfCalendarCreate(arg)
{
    return new CFCalendar(arg.id, arg.yearSpinnerId, arg.monthSelectBoxId,
                          arg.dayBoxIds, arg.dateBoxIds, arg.dialogBoxIds,
                          arg.yearValue, arg.monthValue, arg.dateValue);
}

function cfCalendarDateBoxCreate(arg)
{
    return new CFCalendarDateBox(arg.id, arg.dateDisplayBoxId, arg.dataBoxId);
}

function cfCalendarDateBoxGet(id)
{
    var box = __cfCalendarDateBoxMap[id];
    if (! box) {
        return cfErrorTrigger("cfCalendarDateBoxGet: '" + id + "': " +
                              __CFCALENDAR_ERROR_DATE_BOX_ID);
    }
    return box;
}

function cfCalendarDateDialogBoxCreate(arg)
{
    return new CFCalendarDateDialogBox(arg.id, arg.titleBoxId,
                                       arg.contentPaneId, arg.closeButtonId,
                                       arg.year, arg.month, arg.date);
}

function cfCalendarDateDialogBoxGet(id)
{
    var box = __cfCalendarDateDialogBoxMap[id];
    if (! box) {
        return cfErrorTrigger("cfCalendarDateDialogBoxGet: '" + id + "': " +
                              __CFCALENDAR_ERROR_DATE_DIALOG_BOX_ID);
    }
    return box;
}

function cfCalendarGet(id)
{
    var calendar = __cfCalendarMap[id];
    if (! calendar) {
        return cfErrorTrigger("cfCalendarGet: '" + id + "': " +
                              __CFCALENDAR_ERROR_ID);
    }
    return calendar;
}

