/** 
 * Xero Prototype Popup Calendar library
 * Copyright Xero, www.xero.com
 *
 * Licensed under the terms of the MIT License:
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files 
 * (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, 
 * publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 
 * subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 
 */

/**
 * XERO.widget.Calendar - Prototype.js Popup Calendar
 *
 * @class XERO.widget.Calendar is a Prototype based calendar designed to function as a popup
 * on web pages to allow for easy selection of a single date.
 *
 * @author Xero, www.xero.com
 * @version alpha, revision 1
 * @requires Script.aculo.us if using effects for show, hide, selection
 */
XERO.declare("XERO.widget.Calendar").prototype = {
    
    /**
     * Constant strings and values used within the calendar
     * @final
     */
    constants : {
        yearRange : [2005,2030], /* Year range for select radio */
        dayNames : [['Su','Sunday'],['Mo','Monday'],['Tu','Tuesday'],['We','Wednesday'],['Th','Thursday'],['Fr','Friday'],['Sa','Saturday']], 
        startDay : 0, /* Start week with Sunday [0], through to [6] Saturday */
        monthNames : ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'],
        daysInMonth : [31,28,31,30,31,30,31,31,30,31,30,31],
        /* Error strings */
        errors : {
            yearOutOfRange_Max : "Sorry, this date cannot be displayed as it is past this calendar's year range.",
            yearOutOfRange_Min : "Sorry, this date cannot be displayed as it is before this calendar's year range.",
            idInUse            : "A calendar with an identical ID already exists in the DOM."
        }
    },
    
    id : null,					/* Id of the container div (_element) */
    selectedDate : new Date(),	/* Currently selected date */
    options : {},				/* Calendar options */
    
    /**
     * Prefix for all calendar objects
     * @private
     */
    _idPrefix : 'calendar_',
    
    /** 
     * currently selected date object
     * @type Anchor
     * @private
     */
    _selectedDateCell : null,	
    
    /** 
     * DIV container which contains all calendar html objects
     * @type Div
     * @private
     */
    _element : null,
    
    /**
     * Is the calendar currently visible?
     * @type Bool
     * @private
     */
    _visible : false,
    
    /**
     * Text input element to output to 
     * @private
     * @type Input[type=text]
     */
    _outputField : null,		
    
    /**
     * Creates a new XERO.widget.Calendar control
     * @constructor
     * @param {String} id Id prefix for the Xero calendar
     * @param {String} outputFieldId The ID of the text input HTML element that this control will output to
     * @param {String} toggleElementId the ID of the element which will toggle the visibility of the calendar
     */
    initialize : function(id, outputFieldId, toggleElementId){
        /* Set calendar options */
        this._idPrefix = id+'_';
        //if( $(this._idPrefix+'container') ){
         //   alert(this.constants.errors.idInUse + ' ID=' + this._idPrefix+'container');
       // }

        this._outputField = $(outputFieldId);
        
        this._hasEffectLib = (String.prototype.parseColor != null);
        this.options = Object.extend({
            className:         "calendar",
            initiallyHidden:   true,
            defaultDate:       null,
            showEffect:        (this._hasEffectLib ? Effect.Appear : Element.show),
            hideEffect:        (this._hasEffectLib ? Effect.Fade : Element.hide),
            selectedEffect:    (this._hasEffectLib ? Effect.Fade : Element.hide),
            showEffectOptions: { duration: 0.5 },
            hideEffectOptions: { duration: 0.5 },
            selectedEffectOptions: { duration: 0.5 },
            onload:            Prototype.emptyFunction,
            clearable:         false
        }, arguments[3] || {});

        if(!this.options.initiallyHidden)
            this._visible = true;
            
        if(this.options.defaultDate){
            this.selectedDate = new Date(this.options.defaultDate);
        }
        
        /* Make the calendar */
        this._generateCalendar();
        
        /* Setup the toggle button */
        Event.observe($(toggleElementId),'click',this.toggle.bindAsEventListener(this));
        /* Setup the clear */
        Event.observe($(outputFieldId), 'blur', this._onOutputFieldBlur.bindAsEventListener(this));
        
        this._element.setStyle({
                position: 'absolute',
                //left: pos[0] + 'px',
                //top: (pos[1] + Element.getHeight(this._outputField)) + 'px',
                zIndex: 9999
            }
        );
        
        if(this.options.onload) this.options.onload();
    },

    /**
     * Toggles the visibility of the calendar
     * @param {MouseEvent} e
     */
    toggle : function(e){
        if(this._visible) 
            this.hide(e,false);
        else
            this.show(e);
    },

    _onOutputFieldBlur: function (e){
        // WARNING: VERY XERO SPECIFIC!
        // TODO: Move date tidy stuff into a XERO.util.Dates class
        if(this.options.clearable){
            if(this._outputField.value != "")
                // Only clear field if not null
                completeDate(e); // DateDropDown.js
        } else {
            completeDate(e); // DateDropDown.js
        }
    },
    
    /* Clears the date value */
    clear : function(e){
        this._outputField.value = "";
    },

    /**
     * Displays the calendar. If a show effect is specified in the options then that effect
     * will be used to display the calendar.
     * @param {MouseEvent} e
     */
    show : function(e){
        var pos = Position.cumulativeOffset(this._outputField);
      
        this._element.setStyle({
                position: 'absolute',
                left: pos[0] + 'px',
                top: (pos[1] + Element.getHeight(this._outputField)) + 'px',
                zIndex: 9999
            }
        );
        
        // Show        
        this.options.showEffect(this._element, 
            Object.extend(
                this.options.showEffectOptions,
                {
                    queue: { scope:'calendar', limit: 1 }
                }
            )
        );
        this._visible = true;
        
        // Add event listener
        this._boundPageClickListener = this._pageClick.bindAsEventListener(this);
        Event.observe(document, 'click', this._boundPageClickListener);
        
        if(XERO.widget.currentlyOpenWidget && XERO.widget.currentlyOpenWidget != this){
            XERO.widget.currentlyOpenWidget.hide(e);
        }
        XERO.widget.currentlyOpenWidget = this;
            
        Event.stop(e);
    },
    
    /**
     * Hides the calendar. If a hide effect is specified in the options then that effect
     * will be used to hide the calendar.
     * @param {MouseEvent} e
     * @param {Bool} wasSelected Is the calendar being hidden because the user selected a date? If so we'll use the options.selectedEffect to hide it
     */
    hide : function(e,wasSelected){
        Event.stopObserving(document, 'click', this._boundPageClickListener);
        this._visible = false;
        
        // Hide
        if(wasSelected) {
            this.options.selectedEffect(this._element, 
                Object.extend(
                    this.options.selectedEffectOptions,
                    {
                        queue: { scope:'calendar', limit: 2 }
                    }
                )
            );			
        } else {
            this.options.hideEffect(this._element, 
                Object.extend(
                    this.options.hideEffectOptions,
                    {
                        queue: { scope:'calendar', limit: 2 }
                    }
                )
            );
        }
        
        XERO.widget.currentlyOpenWidget = null;
        
        Event.stop(e);
    },
    
    /**
     * Moves the calendar one month into the future
     * @param {MouseEvent} e
     */
    nextMonth : function(e){	
        var month = this._getSelectedMonth();
        if(month < 11)
            this._selectMonth(++month);
        else {
            var year = this._getSelectedYear();
            if(year+1 >= this.constants.yearRange[1]){
                alert(this.constants.errors.yearOutOfRange_Max);
            } else {
                this._selectYear(++year);
                this._selectMonth(0);
            }
        }

        this._redraw();

        Event.stop(e);
    },
    
    /**
     * Moves the calendar one month into the past
     * @param {MouseEvent} e
     */
    previousMonth : function(e){
        var month = this._getSelectedMonth();
        if(month === 0){
            var year = this._getSelectedYear();
            if(year-1 < this.constants.yearRange[0]){
                alert(this.constants.errors.yearOutOfRange_Min);
            } else {
                this._selectYear(--year);
                this._selectMonth(11);
            }
        } else {
            this._selectMonth(--month);	
        }
        
        this._redraw();
        
        Event.stop(e);
    },
    
    /*-- Date getter\setters -------------------------------------------------------------------- */
    
    /**
     * Selects the specified year in the drop down
     * @private
     * @param {int} year Year to select in the drop down list
     */
    _selectYear : function(year){
        this._selectValueInList($(this._idPrefix + "year"), year);
    },
    
    /**
     * Selects the specified year in the drop down
     * @private
     * @param {int} month Month to select in the drop down list
     */
    _selectMonth : function(month){
        this._selectValueInList($(this._idPrefix + "month"), month);
    },
    
    /**
     * Selects a value within a list
     * @private
     * @param {object} list A list HTML element
     * @param {string} val Value to select
     */
    _selectValueInList : function(list, val){
        if(list){
            for(var i=0; i<list.options.length; i++){
                if(list.options[i].value == val){
                    list.selectedIndex = i;
                    return;
                }
            }
        }
    },
    
    /**
     * @return currently selected year in the drop down list
     * @private
     * @type int
     */
    _getSelectedYear : function(){
        return parseInt($F(this._idPrefix + "year"));
    },

    /**
     * @return currently selected month in the drop down list
     * @private
     * @type int
     */
    _getSelectedMonth : function(){
        return parseInt($F(this._idPrefix + "month"));
    },
    
    /**
     * YOU MIGHT WANT TO CUSTOMISE THIS FOR YOUR NEEDS
     * This function defines what is outputted to the text field. For our needs we simply want something
     * of the format '1 Feb 2007'. 
     *
     * @return Formated date string for outputting into the text field
     * @type String
     * @private
     */
    _formatDate : function(d){
        return d.getDate() + ' ' + this.constants.monthNames[d.getMonth()] + ' ' + d.getFullYear();
    },
    
    /*-- Build the calendar --------------------------------------------------------------------- */

    /**
     * Generate the calendar
     * @private
     */
    _generateCalendar : function(){	
        var div = document.createElement('div');
        this.id = div.id = this._idPrefix + 'container';

        var table = document.createElement('table');
        table.id = this._idPrefix + 'calendar';
        table.cellSpacing = 0;
        table.width = '100%';
        // IE requires a tbody
        var tbody = document.createElement('tbody');
        table.appendChild(tbody);
                
        // Add to page
        this._element = div; // Set div as our container
        Element.addClassName(div, this.options.className);
        if(this.options.dropshadow) 
            Element.addClassName(div, 'shadow');
        div.appendChild(table);
        // append calendar to the body
        document.body.appendChild(div);
        
        /* Generate the inner parts of the calendar */
        tbody.appendChild( this._generateHeader() );
        
        /* Set the initial date */
        this._selectYear(this.selectedDate.getFullYear());
        this._selectMonth(this.selectedDate.getMonth());
        
        /* Update days in month */
        this.constants.daysInMonth[1] = ((this.selectedDate.getFullYear() - 2000) % 4 ? 28 : 29 );
        
        tbody.appendChild( this._generateDates() );
        
        if(this.options.initiallyHidden){
            $(div).setStyle({display: 'none'});
        }
                
        return div;
    },
    
    /**
     * Generate the header row for the calendar
     * @private
     * @return TR element containing header contents
     */
    _generateHeader : function(){
        // Declare html elements
        var tr = document.createElement('tr');
        var td = document.createElement('td');
        var selectMonth = document.createElement('select');
        var selectYear = document.createElement('select');
        var aNext = document.createElement('a');
        var aPrev = document.createElement('a');
        var br = document.createElement('br');
        
        td.className = 'header';
        
        /* Build links */
        // Next
        aNext.appendChild(document.createTextNode('Next Month'));
        aNext.href = '#';
        aNext.title = 'Display next month';
        aNext.className = 'next';
        Event.observe(aNext,'click',this.nextMonth.bindAsEventListener(this));
        // Previous
        aPrev.appendChild(document.createTextNode('Previous Month'));
        aPrev.href = '#';
        aPrev.title = 'Display previous month';
        aPrev.className = 'prev';
        Event.observe(aPrev,'click',this.previousMonth.bindAsEventListener(this));
        
        /* Build drop downs */
        // Month
        selectMonth.id = this._idPrefix + "month";
        for(var i=0;i<12;i++) {
            var opt = document.createElement('option');
            opt.setAttribute('value',i);
            opt.appendChild( document.createTextNode(this.constants.monthNames[i]) );
            selectMonth.appendChild(opt);
        }
        selectMonth.selectedIndex = 0;
        Event.observe(selectMonth, 'click', this._selectMonthClick.bindAsEventListener(this));		
        Event.observe(selectMonth, 'change', this._selectMonthSelectedIndexChanged.bindAsEventListener(this));
        
        // Year
        selectYear.id = this._idPrefix + "year";
        for(var i=this.constants.yearRange[0];i<=this.constants.yearRange[1];i++){
            var opt = document.createElement('option');
            opt.setAttribute('value',i);
            opt.appendChild( document.createTextNode(i) );
            selectYear.appendChild(opt);
        }
        selectYear.selectedIndex = 0;
        Event.observe(selectYear, 'click', this._selectYearClick.bindAsEventListener(this));		
        Event.observe(selectYear, 'change', this._selectYearSelectedIndexChanged.bindAsEventListener(this));
        
        /* Add html elements to cell */
        td.appendChild(selectMonth);
        td.appendChild(selectYear);
        td.appendChild(br);
        td.appendChild(aPrev);
        td.appendChild(aNext);
        tr.appendChild(td);
        
        return tr;
    },
    
    /**
     * Generate the dates table for the calendar
     * @private
     * @return TR containing dates table
     */
    _generateDates : function(){
        /* Create a nested table to store the dates */
        var containerTr = document.createElement('tr');
        containerTr.id = this._idPrefix + 'datesCalendar';
        
        var containerTd = document.createElement('td');
        containerTd.className = 'datesContainer';
                
        var table = document.createElement('table');
        table.cellSpacing = 0;
        
        var tbody = document.createElement('tbody');
        table.appendChild(tbody);
                    
        /* Create the days of week title */
        var tr = document.createElement('tr');
        tbody.appendChild(tr);
        // Add header th's
        for(var dow=0; dow<7; dow++) {
            var th = document.createElement('th');
            th.appendChild( document.createTextNode( this.constants.dayNames[ (dow + this.constants.startDay) % 7 ][0] ));
            th.title = this.constants.dayNames[ (dow + this.constants.startDay) % 7 ][1];
            tr.appendChild(th);
        }
        
        /* Create the date cells */
        var totalCells = 42; // including weekends
        
        var currM = this._getSelectedMonth();
        var currY = this._getSelectedYear();
        
        var beginDate = new Date(currY, currM, 1);
        var endDate = new Date(currY, currM, this.constants.daysInMonth[currM]);
        var d = new Date(beginDate);
        d.setDate(beginDate.getDate() + (this.constants.startDay - beginDate.getDay()) - (this.constants.startDay - beginDate.getDay() > 0 ? 7 : 0));      
        d.setYear(currY);
        
        var test = test = new Date(d.getFullYear(), d.getMonth(), this.constants.daysInMonth[currM] + 1);
        if(test.getFullYear() > endDate.getFullYear()){
            d.setYear(d.getFullYear() - 1);
        }
            
        var a; /* date anchor */
        for(var i=0; i<totalCells; i++){
            /* Create the cell */
            td = document.createElement('td');
            
            a = document.createElement('a');
            a.href = "#";
            a.title = this._formatDate(d);
            a.appendChild(document.createTextNode(d.getDate()));
            Event.observe(a, 'click', this._dateClick.bindAsEventListener(this));
            
            td.appendChild(a);
            
            // Attach date for future use
            a.date = new Date(d);
            
            /* Add classes */
            // Select currently selected date
            if( (d.getDate() == this.selectedDate.getDate()) && 
                (d.getMonth() == this.selectedDate.getMonth()) &&
                (d.getFullYear() == this.selectedDate.getFullYear()) )
            {
                Element.addClassName(td,'selected');	
                this._selectedDateCell = td;
            }
            // Flag weekdays and weekends
            if(d.getDay() > 1 && d.getDay() < 7){
                Element.addClassName(td,'weekday');
            } else {
                Element.addClassName(td,'weekend');
            }
            // Flag days outside of the current month
            if(d.getMonth() != currM){
                Element.addClassName(td,'outside');
            }
                        
            if(i % 7 === 0){
                /* End of row */
                tr = document.createElement('tr');
                tr.appendChild(td);
                tbody.appendChild(tr);
            } else {	
                /* Add cell to table */
                tr.appendChild(td);
            }
                            
            d.setDate(d.getDate() + 1);
        }
        
        /* Add the dates table to the calendar */
        containerTd.appendChild(table);
        containerTr.appendChild(containerTd);
        
        return containerTr;
    },
       
    /**
     * Redraws the dates table with the currently selected date values
     * @private
     */
    _redraw : function(){
        var tr = $(this._idPrefix + 'datesCalendar');
        var parent = tr.parentNode;
        tr.remove();
        parent.appendChild( this._generateDates() );	
    },	
    
    /*-- Event listeners ------------------------------------------------------------------------ */

    /* Month drop down events */
    
    /**
     * Called when clicking on the month drop down. Allows us a place to stop the event from
     * bubbling down and triggering the _pageClick() which would close the calendar
     * @param {MouseEvent} e
     * @private
     */
    _selectMonthClick : function(e){
        Event.stop(e);
    },
    
    /**
     * Called when a user selects a new month from the drop down. Triggers a redraw of the calendar dates
     * @param {MouseEvent} e
     * @private
     */
    _selectMonthSelectedIndexChanged : function(e){
        this._redraw();
    },

    /* Month drop down events */
    
    /**
     * Called when clicking on the year drop down. Allows us a place to stop the event from
     * bubbling down and triggering the _pageClick() which would close the calendar
     * @param {MouseEvent} e
     * @private
     */
    _selectYearClick : function(e){
        Event.stop(e); /* don't pass down - prevents hiding the calendar due to _pageClick() */
    },
    
    /**
     * Called when a user selects a new year from the drop down. Triggers a redraw of the calendar dates
     * @param {MouseEvent} e
     * @private
     */
    _selectYearSelectedIndexChanged : function(e){
        this._redraw();
    },
    
    /* Calendar date events */
    
    /**
     * Called when a user clicks on a date. This method sets the selected date value, highlights the cell
     * and outputs the selected date to the output field.
     * @param {MouseEvent} e
     * @private
     */
    _dateClick : function(e){
        var a = Event.element(e);
        
        /* Is this outside of the current month? */
        var isOutside = a.parentNode.hasClassName('outside');
        if(isOutside){
            if(a.date > this.selectedDate){
                this.selectedDate = a.date;
                this.nextMonth(e);
            } else {
                this.selectedDate = a.date;
                this.previousMonth(e);
            }
        } else {						
            /* Select the current cell */
            this.selectedDate = a.date;
            if(this._selectedDateCell)
                this._selectedDateCell.removeClassName('selected');
            this._selectedDateCell = a.parentNode;
            this._selectedDateCell.addClassName('selected');
            this.hide(e,true);
        }
        
        this._outputField.value = this._formatDate(this.selectedDate);
        
        /* Call onchange event if available */
        if(this._outputField.onchange) this._outputField.onchange();
                
        Event.stop(e);
    },
    
    /* Page events */
    
    /**
     * Called when a user clicks on the document body - if the click is outside of the calendar then we treat
     * this as a request to hide the calendar
     * @param {MouseEvent} e
     * @private
     */
    _pageClick : function(e){
        if(this._visible){
            var x = e.clientX;
            var y = e.clientY;

            if( !Position.within(this._element, x, y) ){
                this.hide(e,false);
            }
        }
        Event.stop(e);
    }
    
};

