﻿/*
    TVI Framework
    Version 1.0
    Copywrite 2010 TVI Design
    Last Updated: 04/05/2010
    
    Contents:
        TVI.Components.Calendar
*/

TVI.Components.Calendar = function(config) {

    /*
        config = {
        
            ID
            loadEventsBy
            loadEvents
            template
            selectors
            month
            year
        
        }
    */

    /* Private */

    /* Properties */

    var cmp = this;
    var currentMonth = 1;
    var currentYear = 2000;
    var loadEventsBy = 'none'; // Possible values - 'month' , 'all', 'none'
    var monthsLoaded = [];


    // Default Calendar template
    var template = '' +
        '<table class="TVI-calendar-grid">' +
            '<thead>' +
                '<tr>' +
                    '<th colspan="7">' +
                        '<table class="TVI-calendar-topRow">' +
                            '<tr>' +
                                '<td class="TVI-calendar-backYear"><a href="#"></a></td>' +
                                '<td class="TVI-calendar-backMonth"><a href="#"></a></td>' +
                                '<td class="TVI-calendar-monthTitle"><span>${monthTitle}</span></td>' +
                                '<td class="TVI-calendar-forwardMonth"><a href="#"></a></td>' +
                                '<td class="TVI-calendar-forwardYear"><a href="#"></a></td>' +
                            '</tr>' +
                        '</table>' +
                    '</th>' +
                '</tr>' +
            '</thead>' +
            '<tbody>' +
                '<tr class="TVI-calendar-daysRow">' +
                    '<td>M</td>' +
                    '<td>T</td>' +
                    '<td>W</td>' +
                    '<td>T</td>' +
                    '<td>F</td>' +
                    '<td>S</td>' +
                    '<td>S</td>' +
                '</tr>' +

                '{for r in rows}' +

                    '<tr class="TVI-calendar-row">' +

                        '{for d in r.days}' +
                            '<td class="TVI-calendar-day{if d.dayClass} ${d.dayClass}{/if}">' +
                                '<!-- {day: "${d.day}", month: "${d.month}", year: "${d.year}"} -->' +
                                '${d.day}' +
                            '</td>' +
                        '{/for}' +

                    '</tr>' +

                '{/for}' +

            '</tbody>' +
        '</table>';


    // Default selectors for buttons
    var selectors = {
        backYear: '.TVI-calendar-backYear',
        backMonth: '.TVI-calendar-backMonth',
        forwardMonth: '.TVI-calendar-forwardMonth',
        forwardYear: '.TVI-calendar-forwardYear',
        day: '.TVI-calendar-day'
    };





    /* Methods */

    var init = function() {

        /* Initialise form */

        //check config exists
        if (!config) { TVI.logWarning({ code: '100', message: 'Config doesn\'t exist' }); return; }


        //if config is a string, set the ID to the config
        if (typeof (config) === 'string') {
            cmp.ID = config;
        }

        //if config is an object, pull apart
        if (typeof (config) === 'object') {

            //check ID exists in config
            if (!config.ID) { TVI.logWarning({ code: '100', message: 'Config.ID doesn\'t exist' }); return; }
            cmp.ID = config.ID;

            // Apply any other config options
            loadEventsBy = config.loadEventsBy || loadEventsBy;
            loadEvents = config.loadEvents !== undefined ? config.loadEvents : loadEvents;
            template = config.template || template;


            TVI.apply(selectors, config.selectors);

        }

        //get the jQuery element
        cmp.el = $('#' + cmp.ID);


        //check element exists
        if (cmp.el.length === 0) { TVI.logWarning({ code: '100', message: 'Calendar element doesn\'t exist for "' + cmp.ID + '"' }); return; }


        // Check to see if a month and year have been supplied
        if (config.month && config.year) {
            currentMonth = config.month;
            currentYear = config.year;
        }
        // Otherwise use today's date
        else {
            var today = new Date();
            currentMonth = today.getMonth() + 1;
            currentYear = today.getFullYear();
        }

        // Add hover class to the days
        TVI.event(cmp.el.find(selectors.day), 'mouseover', function() {
            $(this).addClass('hover');
        });
        TVI.event(cmp.el.find(selectors.day), 'mouseout', function() {
            $(this).removeClass('hover');
        });

        // Add actions to the back and forward buttons
        TVI.event(cmp.el.find(selectors.backYear), 'click', decYear);
        TVI.event(cmp.el.find(selectors.backMonth), 'click', decMonth);
        TVI.event(cmp.el.find(selectors.forwardMonth), 'click', incMonth);
        TVI.event(cmp.el.find(selectors.forwardYear), 'click', incYear);

        // Add actions to the days if a handler exists
        TVI.event(cmp.el.find(selectors.day), 'click', dayClicked);


        // Draw the calendar
        draw();

        // If loadEventsBy is 'all' or 'month' then get all the events that are to be loaded into the calendar
        if (loadEventsBy === 'all' || loadEventsBy === 'month') {
            loadEvents(currentMonth, currentYear, loadEventsSuccess);
        }

    };






    var createCalendar = function() {

        ////////////////////////////////////////////
        /* Produce the data for the current month */
        ////////////////////////////////////////////

        // Get events from the cache that are in the current month
        var eventsThisMonth = getEventsInMonth(currentMonth, currentYear) || [];

        // Get Today's date
        var today = new Date();

        // Get the date of the first of the current month
        var firstOfMonth = new Date(currentYear, currentMonth - 1, 1);

        // Which number day is the first of the month?
        var firstOfMonthDay = firstOfMonth.getDay();

        // If it's a sunday then it should be day 7, not day 0
        if (firstOfMonthDay === 0) {
            firstOfMonthDay = 7;
        }

        // Work out which date is the first cell on the calendar
        var firstOnCalendar = firstOfMonth.subtractDays(firstOfMonthDay - 1);

        // If the first day is a monday then move it to the second row by subtracting another 7 days
        if (firstOfMonthDay === 1) {
            firstOnCalendar = firstOfMonth.subtractDays(7);
        }

        // Start creating the rows for the calendar
        var rows = [];

        // Create a counter date to increment as we add the days
        var counterDate = firstOnCalendar;

        // Create 6 rows
        for (var i = 0; i < 6; i++) {

            var row = { days: [] };

            // Create 7 days in each row
            for (var j = 0; j < 7; j++) {

                // Get the day and month to add
                var day = counterDate.getDate();
                var month = counterDate.getMonth() + 1;
                var year = counterDate.getFullYear();

                // Create the day object and add it to the current row
                var dayToAdd = { day: day, month: month, year: year };

                // If the day is not in the current month then switch it off
                if (month != currentMonth) {
                    dayToAdd.dayClass = 'off';
                }

                // Check to see if there are any events to add to this date by looping through this months events from the cache
                var eventsThisMonthLength = eventsThisMonth.length;
                for (var k = 0; k < eventsThisMonthLength; k++) {

                    // Does the date match?
                    if (eventsThisMonth[k].day == day && eventsThisMonth[k].month == month) {
                        dayToAdd.dayClass = eventsThisMonth[k].dayClass;
                    }
                }

                // Check to see if the date is today
                if (day == today.getDate() && (month - 1) == today.getMonth() && year == (today.getFullYear())) {
                    dayToAdd.dayClass = 'today';
                }

                // Add the day object to the row
                row.days.push(dayToAdd);

                // Increment the date
                counterDate.addDays(1);
            }

            rows.push(row);

        }


        // Start creating the data object
        var data = {};

        data.monthTitle = getMonthName(currentMonth) + " " + currentYear;
        data.month = currentMonth;
        data.year = currentYear;
        data.rows = rows;

        // Return the data
        return data;

    };






    var dayClicked = function(){
    
        /* Run when a day is clicked by the users */
                
        // Get the day, month, and year from the elements metadata
        var data = TVI.meta($(this));
        
        // If the day clicked is not in the current month then ignore the click
        if(data.month != currentMonth){
            return;
        }
        
        // Use that to get the events that are on this day
        var eventsOnDay = getEventsOnDay(data.day, data.month, data.year);

        cmp.fireEvent('dayClicked', {
        
            day: data.day, 
            month: data.month, 
            year: data.year, 
            events: eventsOnDay
        
        });
    
    };
    
    
    
    
    
    
    var decMonth = function() {

        /* Decreases the month */

        currentMonth--;

        if (currentMonth === 0) {
            currentMonth = 12;
            currentYear--;
        }
        
        // Redraw the calendar
        draw();
        
        // If loadEventsBy is 'month' then get all the events that are to be loaded into the calendar
        if (loadEventsBy === 'month') {
            loadEventsForMonth();
        }

        cmp.fireEvent('previousMonthClicked', {
        
            month: currentMonth, 
            year: currentYear
        
        });

    };






    var decYear = function() {

        // Decreases the current year

        currentYear--;
        
        // Redraw the calendar
        draw();
        
        // If loadEventsBy is 'month' then get all the events that are to be loaded into the calendar
        if (loadEventsBy === 'month') {
            loadEventsForMonth();
        }

        cmp.fireEvent('previousYearClicked', {
        
            year: currentYear
        
        });

    };






    var draw = function() {

        /* Redraws the calendar */

        // Process the template and replace the element in the DOM
        cmp.el.html(template.process(createCalendar()));
        
        onDataBind(getEventsInMonth(currentMonth, currentYear));

    };






    // Placeholder for the loadEvents function which can be supplied as a config option
    var loadEvents = function() { };






    var getEventsInMonth = function(month, year) {

        /* Returns the events in a particular month from the cache */

        // Loop through the internal events cache to see if there are any events in that month
        var eventsLength = cmp.events.length;
        for (var j = 0; j < eventsLength; j++) {

            // Check to see whether the month we are looking for matches this month
            if (cmp.events[j].month == month && cmp.events[j].year == year) {
                return cmp.events[j].events;
            }
        }
        
        // If nothing has been returned then return an empty array
        return [];

    };
    
    
    
    
    
    
    var getEventsOnDay = function(day, month, year) {

        var eventsOnDay = [];

        // First of all, get the events that match the month
        var eventsInMonth =  getEventsInMonth(month, year);
        
        // Loop through the events for that month
        var eventsInMonthLength = eventsInMonth.length;
        for(var i=0; i < eventsInMonthLength; i++){
        
            // Check to see if event is on the day we are looking for
            if(eventsInMonth[i].day == day){
            
                // Add it to our array
                eventsOnDay.push(eventsInMonth[i]);
            }
        }
        
        // Return the array of events which match this day
        return eventsOnDay;

    };






    var getMonthName = function(monthNumber) {

        /* Gets the month name from the month number */

        var month = new Array(12);
        month[0] = "January";
        month[1] = "February";
        month[2] = "March";
        month[3] = "April";
        month[4] = "May";
        month[5] = "June";
        month[6] = "July";
        month[7] = "August";
        month[8] = "September";
        month[9] = "October";
        month[10] = "November";
        month[11] = "December";

        return month[monthNumber - 1];
    };






    var hasCurrentMonthLoaded = function() {

        /* Returns whether the events have already been loaded for this month */
        
        var rowCount = cmp.events.length;
                        
        for (var i = 0; i < rowCount; i++) {
            
            if (cmp.events[i].month == currentMonth && cmp.events[i].year == currentYear){
            
                return true;
            
            }
            
        }
        
        return false;

    };






    var incMonth = function() {

        /* Increases the current month */
        currentMonth++;

        if (currentMonth === 13) {
            currentMonth = 1;
            currentYear++;
        }
        
        // Redraw the calendar
        draw();
        
        // If loadEventsBy is 'month' then get all the events that are to be loaded into the calendar
        if (loadEventsBy === 'month') {
            loadEventsForMonth();
        }

        cmp.fireEvent('nextMonthClicked', {
        
            month: currentMonth,
            year: currentYear
        
        });

    };






    var incYear = function() {

        // Increases the current year

        currentYear++;
        
        // Redraw the calendar
        draw();
        
        // If loadEventsBy is 'month' then get all the events that are to be loaded into the calendar
        if (loadEventsBy === 'month') {
            loadEventsForMonth();
        }

        cmp.fireEvent('nextYearClicked', {
        
            year: currentYear
        
        });

    };
    
    
    
    
    
    var loadEventsForMonth = function(){
    
        if (hasCurrentMonthLoaded() === false){
        
            loadEvents(currentMonth, currentYear, loadEventsSuccess);
        
        }        
    
    };






    var loadEventsSuccess = function(newEvents) {

        // This callback function is passed to config.loadEvents() to be run once events have been successfully loaded

        // Loop through all the newly loaded events
        var newEventsLength = newEvents.length;
        for (var i = 0; i < newEventsLength; i++) {

            // Create a shortcut to the event we're adding
            var newEvent = newEvents[i];

            // If the new month starts with a 0 then remove it
            if (newEvent.month.indexOf('0') == 0) {
                newEvent.month = newEvent.month.substring(1);
            }
            
            // If the new day starts with a 0 then remove it
            if (newEvent.day.indexOf('0') == 0) {
                newEvent.day = newEvent.day.substring(1);
            }

            // Create a variable to set if the month is already in the cache
            var monthExists = false;

            // Loop through the internal events cache to see if the month already exists
            var eventsLength = cmp.events.length;
            for (var j = 0; j < eventsLength; j++) {

                // Check to see whether the month matches the month of the event being added
                if (cmp.events[j].month == newEvent.month && cmp.events[j].year == newEvent.year) {

                    // Remember that the month has been found
                    monthExists = true;

                    // If an ID has been supplied then loop through all the events in this month in the cache and check for duplicates 
                    var idExists = false;
                    if (newEvent.ID !== undefined) {

                        var monthEventsLength = cmp.events[j].events.length;
                        for (var k = 0; k < monthEventsLength; k++) {

                            if (cmp.events[j].events[k].ID === newEvent.ID) {
                                idExists = true;
                            }
                        }
                    }


                    // If a duplicate event isn't already in the cache then add the new event to this month in the cache
                    if (idExists === false) {
                        cmp.events[j].events.push(newEvent);
                    }
                }
            }

            // If the month wasn't already in the cache
            if (monthExists === false) {

                // Create a new month
                var newMonth = {
                    month: newEvent.month,
                    year: newEvent.year,
                    events: []
                };

                // Add the new event to the month
                newMonth.events.push(newEvent);

                // Add the new month to the cache
                cmp.events.push(newMonth);

            }


        }

        // Redraw the calendar
        draw();

    };
    
    
    
    
    
    var onDataBind = function(){
    
        /* Fired after events have been bound to calendar */

        cmp.fireEvent('dataBound');
    
    };






    /* Public */

    TVI.apply(cmp, new TVI.Observable());

    TVI.apply(cmp, {

        /* Properties */

        ID: null,
        events: []

    });


    init();

};





TVI.Components.Calendars = function() {

    /* Private */

    /* Properties */

    var cmp = {};
    var componentIndex = -1;





    /* Methods */

    var init = function() {

        //add to components array
        componentIndex = TVI.Components.add({

            type: 'calendar',
            selector: '.TVI-calendar',
            setup: setup,
            create: create,
            layout: layout

        });
        
    };





    var setup = function(container) {

        //add data to elements
        container.find('.TVI-calendar').each(function() {
        
            //ignore if already created
            if ($(this).hasClass('TVI-created')){ return; }

            $(this).data('componentIndex', componentIndex);

        });

    };



    var create = function(element) {
        
        //ignore if already created
        if (element.hasClass('TVI-created')){ return; }

        //instantiate calendars
        if (App === undefined){ App = {}; }

        var id = element.attr('id');

        App[id] = new TVI.Components.Calendar({
        
            ID: id
        
        });

        // Tell the system it doesn't need to be created again.
        element.addClass('TVI-created');

    };





    var layout = function(element) {

        

    };





    /* Public */

    TVI.apply(cmp, {

        /* Properties */

        /* Methods */

        create: function(element) {

            create(element);

        },

        layout: function(element) {

            layout(element);

        }

    });

    TVI.ready(init);


    return cmp;

} ();





//////////////////////////////////////////////////////////////
// A few additions to Javascript's date object.....
//////////////////////////////////////////////////////////////

// Add Milliseconds
Date.prototype.addMilliseconds = function (value) {
    this.setMilliseconds(this.getMilliseconds() + value);
    return this;
};

// Add Days
Date.prototype.addDays = function (value) { 
    return this.addMilliseconds(value * 86400000); /* 60*60*24*1000 */
};

// Subtract Days
Date.prototype.subtractDays = function (value) { 
    return this.addMilliseconds(1-(value * 86400000)); /* 60*60*24*1000 */
};