/**
 * Horde javascript calendar widget.
 *
 * Custom Events:
 * --------------
 * Horde_Calendar:select
 *   params: Date object
 *   Fired when a date is selected.
 *
 * Horde_Calendar:selectMonth
 *   params: Date object
 *   Fired when a month is selected.
 *
 * Horde_Calendar:selectWeek
 *   params: Date object
 *   Fired when a week is selected.
 *
 * Horde_Calendar:selectYear
 *   params: Date object
 *   Fired when a year is selected.
 *
 * @category Horde
 * @package  Core
 */

var Horde_Calendar =
{
    // Variables set externally: click_month, click_week, click_year,
    //                           firstDayOfWeek, fullweekdays, months,
    //                           weekdays
    // Variables defaulting to null: date, month, openDate, trigger, year

    open: function(trigger, data)
    {
        var date = data ? data : new Date();

        this.openDate = date.getTime();
        this.trigger = $(trigger);
        this.draw(this.openDate, true);
    },

    /**
     * Days in the month (month is a zero-indexed javascript month).
     */
    daysInMonth: function(month, year)
    {
        switch (month) {
        case 3:
        case 5:
        case 8:
        case 10:
            return 30;

        case 1:
            return (year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0))
                ? 29
                : 28;

        default:
            return 31;
        }
    },

    weeksInMonth: function(month, year)
    {
        var firstWeekDays, weeks,
            firstOfMonth = (new Date(year, month, 1)).getDay();

        if ((this.firstDayOfWeek == 1 && firstOfMonth === 0) ||
            (this.firstDayOfWeek === 0 && firstOfMonth == 6)) {
            firstWeekDays = 7 - firstOfMonth + this.firstDayOfWeek;
            weeks = 1;
        } else {
            firstWeekDays = this.firstDayOfWeek - firstOfMonth;
            weeks = 0;
        }

        firstWeekDays %= 7;

        return Math.ceil((this.daysInMonth(month, year) - firstWeekDays) / 7) + weeks;
    },

    // http://javascript.about.com/library/blstdweek.htm
    weekOfYear: function(d)
    {
        var newYear = new Date(d.getFullYear(), 0, 1),
            day = newYear.getDay();
        if (this.firstDayOfWeek !== 0) {
            day = ((day + (7 - this.firstDayOfWeek)) % 7);
        }
        return Math.ceil((((d - newYear) / 86400000) + day + 1) / 7);
    },

    draw: function(timestamp, init)
    {
        this.date = new Date(timestamp);
        this.month = this.date.getMonth();
        this.year = this.date.getFullYear();

        var cell, i, i_max, p, row, startOfView, vp,
            count = 1,
            div = $('hordeCalendar'),
            tbody = div.down('TBODY'),

            // Requires init above
            daysInMonth = this.daysInMonth(this.month, this.year),
            daysInView = this.weeksInMonth(this.month, this.year) * 7,
            firstOfMonth = (new Date(this.year, this.month, 1)).getDay(),

            // Cache today and open date.
            today = new Date(),
            today_year = today.getFullYear(),
            today_month = today.getMonth(),
            today_day = today.getDate(),
            open = new Date(this.openDate),
            open_year = open.getFullYear(),
            open_month = open.getMonth(),
            open_day = open.getDate();

        if (this.firstDayOfWeek === 0) {
            startOfView = 1 - firstOfMonth;
        } else {
            // @TODO Adjust this for days other than Monday.
            startOfView = (firstOfMonth === 0)
                ? -5
                : 2 - firstOfMonth;
        }

        div.down('.hordeCalendarYear').update(this.year);
        div.down('.hordeCalendarMonth').update(this.months[this.month]);
        tbody.update('');

        for (i = startOfView, i_max = startOfView + daysInView; i < i_max; ++i) {
            if (count == 1) {
                row = new Element('TR');
                if (this.click_week) {
                    row.insert(new Element('TD').insert(new Element('A', { className: 'hordeCalendarWeek' }).insert(this.weekOfYear(new Date(this.year, this.month, (i < 1) ? 1 : i)))));
                }
            }

            cell = new Element('TD');

            if (i < 1 || i > daysInMonth) {
                cell.addClassName('hordeCalendarEmpty');
                row.insert(cell);
            } else {
                if (today_year == this.year &&
                    today_month == this.month &&
                    today_day == i) {
                    cell.writeAttribute({ className: 'hordeCalendarToday' });
                }

                if (open_year == this.year &&
                    open_month == this.month &&
                    open_day == i) {
                    cell.addClassName('hordeCalendarCurrent');
                }

                row.insert(cell.insert(new Element('A', { className: 'hordeCalendarDay', href: '#' }).insert(i)));
            }

            if (count == 7) {
                tbody.insert(row);
                count = 0;
            }
            ++count;
        }

        if (count > 1) {
            tbody.insert(row);
        }

        div.show();

        // Position the popup every time in case of a different input,
        // window sizing changes, etc.
        if (init) {
            p = this.trigger.cumulativeOffset();
            vp = document.viewport.getDimensions();

            if (p.left + div.offsetWidth > vp.width) {
                div.setStyle({ left: (vp.width - 10 - div.offsetWidth) + 'px' });
            } else {
                div.setStyle({ left: p.left + 'px' });
            }

            if (p.top + div.offsetHeight > vp.height) {
                div.setStyle({ top: (vp.height - 10 - div.offsetHeight) + 'px' });
            } else {
                div.setStyle({ top: p.top + 'px' });
            }
        }

        div.setStyle({ zIndex: 999 });
    },

    hideCal: function()
    {
        $('hordeCalendar').hide();
    },

    changeYear: function(by)
    {
        this.draw((new Date(this.date.getFullYear() + by, this.date.getMonth(), 1)).getTime());
    },

    changeMonth: function(by)
    {
        var newMonth = this.date.getMonth() + by,
            newYear = this.date.getFullYear();

        if (newMonth == -1) {
            newMonth = 11;
            newYear -= 1;
        }

        this.draw((new Date(newYear, newMonth, 1)).getTime());
    },

    init: function()
    {
        var i, link, row,
            offset = this.click_week ? 1 : 0,
            thead = new Element('THEAD'),
            table = new Element('TABLE', { className: 'hordeCalendarPopup', cellSpacing: 0 }).insert(thead).insert(new Element('TBODY'));

        // Title bar.
        link = new Element('A', { href: '#', className: 'hordeCalendarClose rightAlign' }).insert('x');
        thead.insert(new Element('TR').insert(new Element('TD', { colspan: 6 + offset })).insert(new Element('TD').insert(link)));

        // Year.
        row = new Element('TR');
        link = new Element('A', { className: 'hordeCalendarPrevYear', href: '#' }).insert('&laquo;');
        row.insert(new Element('TD').insert(link));

        tmp = new Element('TD', { align: 'center', colspan: 5 + offset });
        if (this.click_year) {
            tmp.insert(new Element('A', { className: 'hordeCalendarYear' }));
        } else {
            tmp.addClassName('hordeCalendarYear');
        }
        row.insert(tmp);

        link = new Element('A', { className: 'hordeCalendarNextYear', href: '#' }).insert('&raquo;');
        row.insert(new Element('TD', { className: 'rightAlign' }).insert(link));

        thead.insert(row);

        // Month name.
        row = new Element('TR');
        link = new Element('A', { className: 'hordeCalendarPrevMonth', href: '#' }).insert('&laquo;');
        row.insert(new Element('TD').insert(link));

        tmp = new Element('TD', { align: 'center', colspan: 5 + offset });
        if (this.click_year) {
            tmp.insert(new Element('A', { className: 'hordeCalendarMonth' }));
        } else {
            tmp.addClassName('hordeCalendarMonth');
        }
        row.insert(tmp);

        link = new Element('A', { className: 'hordeCalendarNextMonth', href: '#' }).insert('&raquo;');
        row.insert(new Element('TD', { className: 'rightAlign' }).insert(link));

        thead.insert(row);

        // Weekdays.
        row = new Element('TR');
        if (this.click_week) {
            row.insert(new Element('TH'));
        }
        for (i = 0; i < 7; ++i) {
            row.insert(new Element('TH').insert(this.weekdays[(i + this.firstDayOfWeek) % 7]));
        }
        thead.insert(row);

        $(document.body).insert({ bottom: new Element('DIV', { id: 'hordeCalendar' }).setStyle({ position: 'absolute', 'z-index': 999 }).hide().insert(table) });

        $('hordeCalendar').observe('click', this.clickHandler.bindAsEventListener(this));
    },

    clickHandler: function(e)
    {
        var elt = e.element(), day;

        if (elt.hasClassName('hordeCalendarDay')) {
            this.hideCal();
            this.trigger.fire('Horde_Calendar:select', new Date(this.year, this.month, parseInt(elt.textContent || elt.innerText, 10)));
        } else if (elt.hasClassName('hordeCalendarClose')) {
            this.hideCal();
        } else if (elt.hasClassName('hordeCalendarPrevYear')) {
            this.changeYear(-1);
        } else if (elt.hasClassName('hordeCalendarNextYear')) {
            this.changeYear(1);
        } else if (elt.hasClassName('hordeCalendarPrevMonth')) {
            this.changeMonth(-1);
        } else if (elt.hasClassName('hordeCalendarNextMonth')) {
            this.changeMonth(1);
        } else if (this.click_year && elt.hasClassName('hordeCalendarYear')) {
            this.trigger.fire('Horde_Calendar:selectYear', new Date(this.year, this.month, 1));
            this.hideCal();
        } else if (this.click_month && elt.hasClassName('hordeCalendarMonth')) {
            this.trigger.fire('Horde_Calendar:selectMonth', new Date(this.year, this.month, 1));
            this.hideCal();
        } else if (this.click_week && elt.hasClassName('hordeCalendarWeek')) {
            day = elt.up('TR').down('A.hordeCalendarDay');
            this.trigger.fire('Horde_Calendar:selectWeek', new Date(this.year, this.month, day.textContent || day.innerText));
            this.hideCal();
        }

        e.stop();
    }

};

document.observe('dom:loaded', Horde_Calendar.init.bind(Horde_Calendar));
