/** @file
 *
 *  Interface to ServiceU calendar.
 *
 *  Configuration may be found in bootstrap.php
 */
(function($) {

$.Events    = {
    defaults: {
        url: {
            events:         '/events.php',
            methods:        {
                list:       'getEvents',
                detail:     'getEvent'
            }
        },

        shared: {
        	showSubtitles:		false,
            hideRepeats:        false,
            webOnly:            true,
            days:               120,
            maxEvents:          0       // ALL
        },
        calendar:   {
            daysPerRow:     4,
            days:           120,
            maxEvents:      0       // ALL
        }
    }
};

/*****************************************************************************
 * Non-DOM connected interface
 * (i.e. the DOM element does not need to exist until after the event data has
 *       been retrieved).
 */

/** @brief  Attach a ServiceU event list helper to jQuery
 *  @param  config      Presentation configuration:
 *                          el:         the jQuery selector that will be used
 *                                      to locate the presentation container
 *                                      once events are retrieved.
 *                          days:       number of days to retrieve [365]
 *
 *  Note: These events will be presented with a date box with events on the
 *        same day in a single event box.
 *
 *          +-------+
 *          |  Nov  | Event Name
 *          | ----- |
 *          |   14  |               event time
 *          +-------+
 */
$.Events.List = function(config) {

    config = $.extend({render_cb: Events_render},
                      $.Events.defaults.shared,
                      config);

    return new Events(config);
}

/** @brief  Attach a ServiceU calendar helper to jQuery
 *  @param  config      Presentation configuration:
 *                          el:         the jQuery selector that will be used
 *                                      to locate the presentation container
 *                                      once events are retrieved.
 *                          days:       number of days to retrieve [365]
 *
 *  Note: These events will be presented with a date box with events on the
 *        same day in a single event box.
 *
 *          +-------+
 *          |  Nov  | Event Name
 *          | ----- |
 *          |   14  |               event time
 *          +-------+
 */
$.Events.Calendar = function(config) {

    config = $.extend({render_cb: EventsCalendar_render},
                      $.Events.defaults.shared,
                      config);

    return new Events(config);
}

/** @brief  Initialize an event list instance.
 *  @param  config      Presentation configuration:
 *                          - el            The jQuery/DOM element to use for
 *                                          presenting the event list
 *                                              OR
 *                                          The jQuery selector that will be
 *                                          used to locate the presentation
 *                                          container once events are
 *                                          retrieved.
 *                          - render_cb     A callback to invoke to render
 *                                          event data.
 *                          - webOnly       Only include web-available events 
 *                                          [true];
 *                          - days          The maximum number of days from
 *                                          TODAY to include
 *                                          [self.config.days];
 *                          - department    An array or comma-separated string 
 *                                          of Department identifiers;
 *                          - category      An array or comma-separated string 
 *                                          of Category identifiers.
 *                          - event         If provided, search for an event 
 *                                          that matches this (sub)string;
 *                          - maxEvents     The maximum number of events to
 *                                          return.  This filter is applied
 *                                          AFTER the events are retrieved from
 *                                          ServiceU;
 *                          - searchField   the search field that should be
 *                                          associated with this calender to
 *                                          provide user-driven event
 *                                          filtering.
 */
function Events(config)
{
    var self    = this;

    self.config = config;

    // Create the JSON data we wish to send.
    config = {method:       $.Events.defaults.url.methods.list,
              days:         self.config.days,
              webOnly:      self.config.webOnly};
    if (self.config.department !== undefined)
        config.department = self.config.department;
    if (self.config.category !== undefined)
        config.category = self.config.category;
    if (self.config.maxEvents !== undefined)
        config.maxEvents = self.config.maxEvents;

    $.ajax({type:       'GET',
            url:        $.Events.defaults.url.events,
            data:       config,
            success:    function(data) {
                jsonComplete(self, data);
            },
            error:      function(req, textStatus, err) {
                var data    = {
                    error:  {
                        code:       -1,
                        message:    textStatus
                    }
                };

                jsonComplete(self, data);
            },
            dataType:   'json'});

    /*
    $.getJSON($.Events.defaults.url.events,
              config,
              function(ret) {
                if ( ! ret )
                    return;

                var $el = config.el;
                if (! $el.jquery)
                    $el = $(config.el);

                if ($el.length < 1)
                {
                    // Wait for Document.ready...
                    $(document).ready(function() {
                        $el = $(config.el);
                        if ($el.length < 1)
                            return;

                        self.config.el = $el;
                        domReady(self, ret);
                    });
                    return;
                }

                self.config.el = $el;
                domReady(self, ret);
              }
    );
    */
}

/** @brief  Invoked when the JSON call returns.
 *  @param  self    The event instance.
 *  @param  ret     The JSON-RPC return data.
 */
function jsonComplete(self, ret)
{
    if ( ! ret )
        return;

    var $el = self.config.el;
    if (! $el.jquery)
        $el = $(self.config.el);

    if ($el.length < 1)
    {
        // Wait for Document.ready...
        $(document).ready(function() {
            $el = $(self.config.el);
            if ($el.length < 1)
                return;

            self.$el = $el;
            domReady(self, ret);
        });
        return;
    }

    self.$el = $el;
    domReady(self, ret);
}

/** @brief  Invoked after the JSON call returns and once we have access to the
 *          target DOM element (via self.$el).
 *  @param  self    The event instance.
 *  @param  ret     The JSON-RPC return data.
 */
function domReady(self, ret)
{
    var $el     = self.$el;
    var $status = $el.find('.loading').text('Rendering Events...');

    if (ret.error !== undefined)
    {
        $el.html('ERROR '+ ret.error.code +': '+ ret.error.message);
    }
    else
    {
        self.config.render_cb(self, ret.result);
    }

    $(".fancy_loading").hide();
}


/*****************************************************************************
 * DOM connected interface
 * (i.e. the DOM element MUST exist for these methods to be invoked.
 */

/** @brief  Attach a ServiceU event list helper to jQuery
 *  @param  config      Presentation configuration:
 *                          days:       number of days to retrieve [365]
 *
 *  Note: These events will be presented with a date box with events on the
 *        same day in a single event box.
 *
 *          +-------+
 *          |  Nov  | Event Name
 *          | ----- |
 *          |   14  |               event time
 *          +-------+
 */
$.fn.Events = function(config) {

    this.each(function() {
        var $el = $(this);
        var cal = $el.data('Events');

        if (cal)
            return this;

        /*
        config = $.extend({}, $.Events.defaults.shared,
                              config);
        $el.data('Events',
                 new Events( $(this) , config, Events_render ));
        */

        config.el = $el;
        $el.data('Events', $.Events.List(config));
    });

    return this;
}

/** @brief  Attach a ServiceU calendar helper to jQuery
 *  @param  config      Presentation configuration:
 *                          days:       number of days to retrieve [365]
 *
 *  Note: These events will be presented with a date box with events on the
 *        same day in a single event box.
 *
 *          +-------+
 *          |  Nov  | Event Name
 *          | ----- |
 *          |   14  |               event time
 *          +-------+
 */
$.fn.EventsCalendar = function(config) {
    this.each(function() {
        var $el = $(this);
        var cal = $el.data('Events_Calendar');

        if (cal)
            return this;

        /*
        config = $.extend({}, $.Events.defaults.shared,
                              $.Events.defaults.calendar,
                               config);
        $el.data('Events_Calendar',
                 new Events( $(this) , config, EventsCalendar_render ));
        */

        config.el = $el;
        $el.data('Events_Calendar', $.Events.Calendar(config));
    });

    return this;
}

/*****************************************************************************
 * Event Column List
 *
 */

/** @brief  Given new event data and an event instance.
 *  @param  scope   An event instance.
 *  @param  data    New event data.
 *
 *  Render the new event data.
 *
 *  Note: These events will be presented with a date box with events on the
 *        same day in a single event box.
 *
 *          +-------+
 *          |  Nov  | Event Name
 *          | ----- |
 *          |   14  |               event time
 *          +-------+
 */
function Events_render(scope, data)
{
    var $el         = scope.$el;
    var lastDay     = null;
    var lastWeek    = null;
    var $item       = null;
    var html        = '';
    var eventsSeen  = {};   // If config.hideRepeates, track events shown
    var eventsShown = 0;

    $el.empty();

    $.each(data, function(idex, item) {
        /* Convert the OccStartTime and OccEndTime to Date
         * instnace.
         */
        if ((scope.config.maxEvents > 0) &&
            (eventsShown > scope.config.maxEvents))
            return false;

        if (scope.config.hideRepeats)
        {
            // Is this a repeat?
            if (eventsSeen[item.EventID] !== undefined)
                return; // Skip this repeat/recurrsing Event

            eventsSeen[item.EventID] = true;
        }
        eventsShown++;
 
        var start   = dateTime2date(item.OccStartTime);
        var end     = dateTime2date(item.OccEndTime);
        var thisDay = start.getFullYear() +
                      start.getMonth()    +
                      start.getDate();

        if (lastDay != thisDay)
        {
            var extraStyle  = 'float:none;';
            var thisWeek    = start.getWeek();

            if (scope.config.daysPerRow < 2)

            if (html.length > 0)
            {
                html +=  '</ul>'
                     +  '</div>';   // end event_box

                $el.append(html);
                html  = '';
            }

            // $day = _addDay($week, start, extraStyle);
            html += '<div class="event_box"'
                 +          extraStyle +'>'
                 +   event_makeBoxDate(start)
                 +   '<ul>';
            lastWeek = thisWeek;
        }
        lastDay = thisDay;

        html += _eventLi(item, start, end, scope.config.showSubtitles);
    });

    if (html.length > 0)
    {
        html +=  '</ul>'
             +  '</div>';   // end event_box

        $el.append(html);

        /* Hide any event boxes, from end to beginning, that aren't fully
         * visible.
         */
        var pos     = $el.position();
        var maxY    = pos.top + $el.innerHeight();
        var $boxes  = $el.find('.event_box');

        for (var idex = $boxes.length - 1; idex > 0; idex--)
        {
            var $box    = $($boxes.get(idex));
            var top     = $box.position().top;

            if (top > maxY)
            {
                // This box is entirely hidden -- hide it
                $box.hide();
                continue;
            }

            var bottom  = $box.position().top + $box.innerHeight();
            if (bottom > maxY)
            {
                /* This box is only partly visible.  If it contains multiple
                 * items, perhaps we can hide one or more so it will fit.
                 *
                 * Find all li (event_box_info) items.
                 *  .event_box_info:parent
                 */
                var $li     = $box.find('.event_box_info:parent');  //'li');
                var hideBox = true;
                if ($li.length > 1)
                {
                    /* Hide each, from end to beginning, until we're either
                     * down to 1 or the item fits.
                     */
                    for (var jdex = $li.length - 1; jdex > 0; jdex--)
                    {
                        var $cur    = $($li.get(jdex));
                        $cur.hide();

                        bottom = $box.position().top + $box.innerHeight();
                        if (bottom <= maxY)
                        {
                            // It fits!!
                            hideBox = false;
                            break;
                        }
                    }
                }

                if (hideBox)
                    $box.hide();
            }
        }
        html  = '';
    }

    $el.find('.event_box_info').fancybox({
        padding:            0,
        frameHeight:        400,
        frameWidth:         400,
        hideOnContentClick: false,
        getData:            event_getData,
        callbackOnShow:     event_onShow
    });
}

/*****************************************************************************
 * Event Calendar
 *
 */

/** @brief  Given new event data and an event instance.
 *  @param  scope   An event instance.
 *  @param  data    New event data.
 *
 *  Render the new event data.
 *
 *  Note: These events will be presented with a date box with events on the
 *        same day in a single event box.
 *
 *          +-------+
 *          |  Nov  | Event Name
 *          | ----- |
 *          |   14  |               event time
 *          +-------+
 */
function EventsCalendar_render(scope, data)
{
    var $el         = scope.$el;
    var boxCount    = 0;

    var lastYear    = null;
    var lastMonth   = null;
    var lastWeek    = null;
    var lastDay     = null;

    var $year       = null;
    var $month      = null;
    var $week       = null;
    var $day        = null;
    var newYear     = false;
    var newMonth    = false;


    $el.empty();

    $.each(data, function(idex, item) {
        /* Convert the OccStartTime and OccEndTime to Date
         * instnace.
         */
        if ((scope.config.maxEvents > 0) &&
            (idex > scope.config.maxEvents))
            return false;
        
        var start       = dateTime2date(item.OccStartTime);
        var end         = dateTime2date(item.OccEndTime);
        var thisYear    = start.getFullYear();
        var thisMonth   = start.getMonth();
        var thisWeek    = start.getWeek();
        var thisDay     = start.getDate();

        if (lastYear != thisYear)
        {
            $year    = _addYear($el, start);
            lastYear = thisYear;
            newYear  = true;
        }

        if (lastMonth != thisMonth)
        {
            $month    = _addMonth($year, start, thisWeek, (! newYear));
            lastMonth = thisMonth;
            newYear   = false;
            newMonth  = true;
        }

        if (lastWeek != thisWeek)
        {
            $week    = _addWeek($month, start, thisWeek, (! newMonth));
            lastWeek = thisWeek;
            boxCount = 0;
            newMonth = false;
        }

        if (lastDay != thisDay)
        {
            var extraStyle  = ( (scope.config.daysPerRow < 2)
                                    ? 'float:none;'
                                    : '' );

            if ( (scope.config.daysPerRow > 1) &&
                 ( boxCount > 0)               &&
                 ( ((boxCount % scope.config.daysPerRow)
                                                == 0) ) )

            {
                boxCount = 0;

                $week.append('<br style="clear:left;" />');
            }
            boxCount++;

            $day    = _addDay($week, start, extraStyle);
            lastDay = thisDay;
        }

        _addEvent($day, item, start, end, scope.config.showSubtitles);
    });

    $el.append('<br style="clear:left;" />');

    $el.find('.event_box_info').fancybox({
        padding:            0,
        frameHeight:        400,
        frameWidth:         400,
        hideOnContentClick: false,
        getData:            event_getData,
        callbackOnShow:     event_onShow
    });

    if (scope.config.searchField !== undefined)
    {
        _activate_searchField(scope, scope.config.searchField);
    }
}

/** @brief  Activate a search field associated with the given event instance.
 *  @param  scope           The event instance.
 *  @param  searchField     The searchField jQuery item, or selector.
 */
function _activate_searchField(scope, searchField)
{
    var $input  = (searchField.jquery ? searchField : $(searchField));

    // Initialize default values for the form...
    $input.each(function() {
            var $el     = $(this);
            var def     = $el.val();
            var val     = null;
            var timer   = null;

            $el.focus(      function() {
                                // Empty the default value on focus
                                if ($el.val() == def)
                                {
                                    $el.val('');
                                }

                                $el.addClass('focus');
                            })
               .blur(       function() {
                                /* Replace the default value on blur if the
                                 * field is empty.
                                 */
                                if ($el.val() == '')
                                {
                                    $el.val(def);
                                    search_reset(scope);
                                }

                                $el.removeClass('focus');
                            })
               .keyup(   function(e) {
                                /* On keypress:
                                 *  ESC             - clear the field
                                 *  > 3 characters  - set a timer to wait 1
                                 *                    second for an other key
                                 *                    presses.
                                 */
                                if (e.keyCode == 27)
                                {
                                    // ESC -- clear the search
                                    $el.val('');
                                    search_reset(scope);
                                }

                                // ALWAYS clear the keypress timer
                                if (timer !== null)
                                {
                                    clearTimeout(timer);
                                    timer = null;
                                }

                                val = $el.val();
                                if (val.length > 2)
                                {
                                    /* We now have at least 3 characters.
                                     * Set a 1 second timeout to see if there
                                     * will be any more key presses.
                                     * 
                                     * If not, invoke search_go().
                                     */
                                    timer = setTimeout(function() {
                                                        search_go(scope, $el);
                                                       }, 1000);
                                }
                                else
                                {
                                    search_reset(scope);
                                }
                            })
        });
}

/** @brief  Triggered when we have at least 3 characters AND have waited 1
 *          second without any additional key presses.
 *  @param  scope       The event instance.
 *  @param  $input      The jQuery/DOM input element.
 */
function search_go(scope, $input)
{
    var val = $input.val();
    if (val.length < 3)
        return;

    // Filter the events that we're presenting using 'val'
    var valRE   = new RegExp( '(^'+ val +'|\\b'+ val +')', 'i');
    var $events = scope.$el.find('.event_box ul li');

    scope.filtered = true;
    $events.each(function() {
                var $item   = $(this);
                var item    = $item.data('Event_data');
                if (item === undefined)
                    return;

                /*
                if ((item.EventName.indexOf(val)        >= 0) ||
                    (item.EventDescription.indexOf(val) >= 0) ||
                    (item.CategoryList.indexOf(val)     >= 0) ||
                    (item.DepartmentList.indexOf(val)   >= 0) ||
                    (item.SubmittedBy.indexOf(val)      >= 0))
                */
                if ( valRE.test(item.EventName)         ||
                     valRE.test(item.EventDescription)  ||
                     valRE.test(item.CategoryList)      ||
                     valRE.test(item.DepartmentList)    ||
                     valRE.test(item.SubmittedBy) )
                {
                    // match
                    $item.show();
                    return;
                }

                // Hide this event
                $item.hide();
            });
}

/** @brief  Reset all events to be visible.
 *  @param  scope       The event instance.
 */
function search_reset(scope)
{
    if (scope.filtered != true)
        return;

    // Filter the events that we're presenting using 'val'
    var $events = scope.$el.find('.event_box ul li');

    $events.show();

    scope.filtered = false;
}


/** @brief  Generate the HTML for the given event.
 *  @param  item        The event information.
 *  @param  itemStart   The starting date/time of the event
 *                      (if it's already been normalized)
 *  @param  itemEnd     The ending   date/time of the event
 *                      (if it's already been normalized)
 *
 *  Note: This creates an <li> element containing JUST the event date and
 *        possibly time.  This should be included in an Event item that
 *        identifies the name of an event or group of events.
 */
function _eventLi_withDate(item, itemStart, itemEnd)
{
    var html    = '';

    html += '<li id="'+ item.EventID +'_'
         +              item.OccID   +'">'
         +   '<div class="event_box_info">'
         +    '<div class="day_time">'
         +      itemStart.dayName(true) +', '
         +      itemStart.monthName(true) + itemStart.getDate()
                // Include the time??
         +      ((item.DateLabel !== 'true')
                    ? ', '+ itemStart.rangeStr(itemEnd)
                    : '')
         +    '</div>'
         +   '</div>'
         +  '</li>';

    return html;
}

/** @brief  Generate the HTML for the given event.
 *  @param  item        The event information.
 *  @param  itemStart   The starting date/time of the event
 *                      (if it's already been normalized)
 *  @param  itemEnd     The ending   date/time of the event
 *                      (if it's already been normalized)
 *  @param  showSubtitle do we show sub titles?
 *
 *  Note: This creates an <li> element containing the event name and possibly
 *        the time range.
 */
function _eventLi(item, itemStart, itemEnd, showSubtitle)
{
    var html    = '';

    html += '<li id="'+ item.EventID +'_'
         +              item.OccID   +'">'
         +   '<div class="event_box_info">'
         +    '<h6>'+ item.EventName +'</h6>'
         +    ((showSubtitle && item.EventSubtitle !== null && item.EventSubtitle !== undefined)
         			? '<div class="event_subtitle">'+item.EventSubtitle+'</div>'
         			: '')
         +    '<div class="day_time">'
                // Include the time??
         +      ((item.DateLabel !== 'true')
                    ? itemStart.rangeStr(itemEnd)
                    : '&nbsp;')
         +    '</div>'
         +   '</div>'
         +  '</li>';

    return html;
}

/** @brief  Render event item information and add it to the given day.
 *  @param  $day        The jQuery day to add the new event item to.
 *  @param  item        The event information.
 *  @param  itemStart   The starting date/time of the event
 *                      (if it's already been normalized)
 *  @param  itemEnd     The ending   date/time of the event
 *                      (if it's already been normalized)
 */
function _addEvent($day, item, itemStart, itemEnd, showSubtitles)
{
    var html    = _eventLi(item, itemStart, itemEnd, showSubtitles);
    var $li     = $(html);

    $li.data('Event_data', item);

    $day.find('ul').append($li);

    return $li;
}

function _addDay($week, curDate, extraStyle)
{
    var html    = '';
    var $day    = null;
    var id      = curDate.getFullYear()
                + '_'+ curDate.getMonth()
                + '_'+ curDate.getDate();

    extraStyle  = extraStyle || '';

    html += '<div id="day_'+ id +'" class="day event_box"'
         +          extraStyle +'>'
         +   event_makeBoxDate_noMonth(curDate)
         +   '<ul>'
         +   '</ul>'
         +  '</div>';

    $day = $(html);
    $day.find('.event_box_date').css('opacity', 0.6);

    $week.append($day);

    return $day;
}

function _addWeek($month, curDate, weekNum, showLabel)
{
    var html    = '';
    var $week   = null;
    var id      = curDate.getFullYear() +'_'+ weekNum;

    html += '<div id="week_'+ id +'" class="week clearfix">';

    if (showLabel === true)
    {
        html +=  '<div class="title">'
             +     curDate.monthName(true)
             +   '<div class="subtitle">'
             +     'week '+ weekNum
             +   '</div>'
             +   '</div>';
    }

    html += '</div>';

    $week = $(html);

    $month.append($week);

    return $week;
}

function _addMonth($year, curDate, weekNum, showWeekLabel)
{
    var html    = '';
    var $month  = null;
    var id      = curDate.getFullYear() +'_'+ curDate.getMonth();

    html += '<div id="month_'+ id +'" class="month">'
         +   '<div class="title">'
         +     curDate.monthName(true);

    /* The week numbers are a little wonky here.  Sometimes it's the same week
     * as the previous one we displayed, sometimes it's the proper (next) week
     * number, .  For now, let's now show it...
     *
    if (showWeekLabel === true)
    {
        html +=  '<div class="subtitle">'
             +     'week '+ weekNum
             +   '</div>';
    }
    */


    html +=  '</div>'
         +  '</div>';

    $month = $(html);

    $year.append($month);

    return $month;
}

function _addYear($el, curDate)
{
    var html    = '';
    var $year  = null;
    var year    = curDate.getFullYear();

    html += '<div id="year_'+ year +'" class="year">'
         +   '<div class="title">'
         +     year
         +   '</div>'
         +  '</div>';

    $year = $(html);

    $el.append($year);

    return $year;
}

/*****************************************************************************
 * Shared utilities
 *
 */

/** @brief  Given a Date instance, create a event_box_date div.
 *  @param  dateTime    The Date instance.
 *  @param  includeDay  Include the Day Name? [ true ]
 *
 *  @return The HTML of an event_box_date div.
 */
function event_makeBoxDate(dateTime, includeDay)
{
    var html    =   '<div class="event_box_date">'
                +     dateTime.monthName(true)
                +     '<br />'
                +     '<b>'+ dateTime.getDate() +'</b>';

    if (includeDay !== false)
    {
        html    +=    '<br />'
                +     dateTime.dayName(true);
    }

    html        +=  '</div>';

    return html;
}

/** @brief  Given a Date instance, create a event_box_date div WITHOUT the
 *          month name.
 *  @param  dateTime    The Date instance.
 *  @param  includeDay  Include the Day Name? [ true ]
 *
 *  @return The HTML of an event_box_date div.
 */
function event_makeBoxDate_noMonth(dateTime)
{
    var html    =   '<div class="event_box_date">'
                +     '<b>'+ dateTime.getDate() +'</b>'
                +     '<br />'
                +     dateTime.dayName(true)
                +   '</div>';

    return html;
}


/** @brief  fancyBox callback to retrieve data for the given element.
 *  @param  item    The targeted fancyBox item:
 *                      {el:    DOM element,
 *                       href:  Target href  (if any),
 *                       title: Target title (if any)}
 *  @param  cb      The fancyBox render callback:
 *                      cb(data);
 */
function event_getData(item, cb)
{
    var $el         = $(item.el);
    var id          = $el.parent().attr('id');
    var ids         = id.split('_');
    var eventID     = ids[0];
    var occID       = ids[1];
    var detailId    = 'eventDetails_'+ eventID +'_'+ occID;
    var $detail     = $('#'+ detailId);

    if ($detail.length > 0)
    {
        // We've already retrieved this data...
        cb( $detail.html() );
        return;
    }

    /* ids[0] == EventID
     * ids[1] == OccID
     */
    var rpc = {
        method:  $.Events.defaults.url.methods.detail,
        eventID: eventID,
        occID:   occID
    };

    $.getJSON($.Events.defaults.url.events,
              rpc,
              function(ret) {
                if ( ! ret )
                    return;

                var html = '<div id="'+ detailId +'" style="display:none;">'
                         +  '<div class="eventDetails">';

                if (ret.error !== undefined)
                {
                    html = 'ERROR '+ ret.error.code +': '+ ret.error.message;
                }
                else
                {
                    var detail      = ret.result;
                    var start       = dateTime2date(detail.OccStartTime);
                    var end         = dateTime2date(detail.OccEndTime);
                    var registerUrl = detail.ExternalEventURL;

                    // Include the occurance ID
                    detail.OccID = occID;

                    /* Remember the current event detail long enough 
                     * for our event_onShow() callback to be invoked...
                     */
                    $.Events.current = detail;

                    html += event_makeBoxDate(start, false);

                    html += '<div class="header">'
                         +   '<div class="middle">'
                         +    '<h5>'+ detail.EventName +'</h5>'
                         +   '</div>'
                         +  '</div>'
                         +  '<div class="event-details">'
                         +    '<div class="meta_data">'
                         +    ((detail.EventSubtitle !== null && detail.EventSubtitle !== undefined)
                      			? '<div class="event_subtitle">'+detail.EventSubtitle+'</div>'
                      			: '')
                         +      '<div class="day_time">'
                                // Include the time??
                         +      ((detail.DateLabel !== 'true')
                                    ? start.rangeStr(end)
                                    : '&nbsp;')
                         +      '</div>'
                         +      '<div class="location">'
                         +        detail.LocName
                         +      '</div>'
                         +    '</div>'
                         +    '<div class="description">'
                         +      detail.Description
                         +    '</div>'
                         +    '<div class="footer">'
                         +      '<div class="tools">'
                         +        '<div class="calendar">'
                         +          '<a href="#">to my calendar</a>'
                         +        '</div>';

                    if (registerUrl.length > 0)
                    {
                        html +=   '<div class="register">'

                        /*
                        if ( ((detail.RegistrationType > 0) && 
                              (! detail.RegistrationEnabled)) )
                        {
                            html += '<span class="closed">'
                                 +    'registration closed'
                                 +  '</span>';
                        }
                        else
                        // */
                        {
                            html += '<a href="'+ registerUrl +'"'
                                 +  ' class="iframe">'
                                 +  'register'
                                 + '</a>';
                        }

                        html +=   '</div>';
                    }

                    html +=     '</div>'
                         +      '<div class="contact">'
                         +        '<div class="name email">'
                         +           '<a href="mailto:'
                         +                detail.OtherContactEmail +'">'
                         +                detail.OtherContact
                         +           '</a>'
                         +        '</div>';

                    if (detail.OtherContactPhone.length > 0)
                    {
                        html +=   '<div class="phone">'
                             +       detail.OtherContactPhone
                             +    '</div>';
                    }

                    html +=     '</div>'
                         +    '</div>'
                         +  '</div>';

                    /*
                    $.each(ret.result, function(key, val) {
                        html += '<tr>'
                             +   '<td>'+ key +'</td>'
                             +   '<td>'+ val +'</td>'
                             +  '</tr>';
                    });
                    */
                }

                html +=  '</div>'
                     +  '</div>';

                $('body').append(html);

                $detail = $('#'+ detailId);

                cb( $detail.html() );
              }
    );
}

/** @brief  Triggered when the fancyBox is shown.
 *
 *  For this case, the content will have been loaded into <div id='fancy_ajax'>
 *  AND $.Events.current will contain the event details for the currently
 *  presented event.
 */
function event_onShow()
{
    // Activate any 'to my calendar' and 'register' links
    var data    = $.Events.current;
    var $el     = $('#fancy_ajax');

    // :XXX: Probably don't need this since we pass the data directly...
    $el.data('event_data', data);

    $el.find('.calendar').click(function(e) {
        e.preventDefault();
        e.stopPropagation();

        event_addToCalendar($el, data);
    });

    /* IE can't handle the registration in an iframe so
     * use a pop-up window.
     */
	/*if (! $.browser.msie)
    {
        $el.find('.eventDetails .register a').fancybox({
            padding:            0,
            frameWidth:         700,
            frameHeight:        500,
            hideOnContentClick: false,
            callbackOnStart:    function() {
				$.fn.fancybox.close();
                $.fn.fancybox.showLoading();
            }
        });
    }
    else
    {*/
        $el.find('.register a').click(function(e) {
            var $a  = $(this);

            e.preventDefault();
            e.stopPropagation();
            window.open($a.attr('href'), '_registration',
                        'width=700,height=500,resizable=1');
            return false;
        });
    //}
}

function event_addToCalendar($el, data)
{
    //var data    = $el.data('event_data');
    var url     = $.serviceU['url.event.add']
                        .replace(/%id.org%/,        $.serviceU['id.org'])
                        .replace(/%id.event%/,      data.EventID)
                        .replace(/%id.occurrence%/, (data.OccID !== undefined
                                                        ? data.OccID
                                                        : 0));

    window.location = url;
}


/*****************************************************************************
 * Minor String extensions
 *
 */

/** @brief  Left pad a string with 'len' characters of '0'
 *  @param  str     The string to pad.
 *  @param  len     The number of characters to pad to.
 *
 *  @return A padded string.
 */
function padStr(str, len)
{
    if (! len)
        len = 2;
    return ("000"+ str).slice(len * -1);
}

/*****************************************************************************
 * Minor Date extensions to provide a a small amount of date/time manipulation.
 *
 */
var $D  = Date;
var $P  = $D.prototype;
var $C  = {
    dayNames:           ['Sunday',  'Monday','Tuesday','Wednesday',
                         'Thursday','Friday','Saturday'],
    dayNamesAbbrev:     ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
    monthNames:         ['January', 'February', 'March',
                         'April',   'May',      'June',
                         'July',    'August',   'September',
                         'October', 'November', 'December'],
    monthNamesAbbrev:   ['Jan',     'Feb',      'Mar',
                         'Apr',     'May',      'Jun',
                         'Jul',     'Aug',      'Sep',
                         'Oct',     'Nov',      'Dec'],
    //meridian:           ['AM',      'PM']
    meridian:           ['a',    'p']
};

$P.dayName = function(abbrev) {
    return (abbrev === true
                ? $C.dayNamesAbbrev[this.getDay()]
                : $C.dayNames[this.getDay()]);
};
$P.monthName = function(abbrev) {
    return (abbrev === true
                ? $C.monthNamesAbbrev[this.getMonth()]
                : $C.monthNames[this.getMonth()]);
};

/** @brief  Get the week number.
 *
 *  Get the week number. Week one (1) is the week which contains the first
 *  Thursday of the year. Monday is considered the first day of the week.
 *
 *  This algorithm is a JavaScript port of the work presented by Claus
 *  Tøndering at
 *  http://www.tondering.dk/claus/cal/node8.html#SECTION00880000000000000000
 *
 *  .getWeek() Algorithm Copyright (c) 2008 Claus Tondering.
 *
 *  The .getWeek() function does NOT convert the date to UTC. The local
 *  datetime is used. Please use .getISOWeek() to get the week of the UTC
 *  converted date.
 *
 *  @return {Number}  1 to 53
$P.getWeek = function () {
    var a, b, c, d, e, f, g, n, s, w;
    
    var $y = (!$y) ? this.getFullYear() : $y;
    var $m = (!$m) ? this.getMonth() + 1 : $m;
    var $d = (!$d) ? this.getDate() : $d;

    if ($m <= 2)
    {
        a = $y - 1;
        b = (a / 4 | 0) - (a / 100 | 0) + (a / 400 | 0);
        c = ((a - 1) / 4 | 0) - ((a - 1) / 100 | 0) + ((a - 1) / 400 | 0);
        s = b - c;
        e = 0;
        f = $d - 1 + (31 * ($m - 1));
    }
    else
    {
        a = $y;
        b = (a / 4 | 0) - (a / 100 | 0) + (a / 400 | 0);
        c = ((a - 1) / 4 | 0) - ((a - 1) / 100 | 0) + ((a - 1) / 400 | 0);
        s = b - c;
        e = s + 1;
        f = $d + ((153 * ($m - 3) + 2) / 5) + 58 + s;
    }
    
    g = (a + b) % 7;
    d = (f + g - e) % 7;
    n = (f + 3 - d) | 0;

    if (n < 0) {
        w = 53 - ((g - s) / 5 | 0);
    } else if (n > 364 + s) {
        w = 1;
    } else {
        w = (n / 7 | 0) + 1;
    }
    
    $y = $m = $d = null;
    
    return w;
};
 */

/** @brief  Calculate the week number.
 *  
 *  This algorithm treats Sunday as the first day of the week.
 *
 *  http://www.irt.org/script/914.htm
 *
 *  @return {Number}  1 to 53
 */
$P.getWeek = function() {
    var year    = this.getFullYear();
    var newYear = new Date(year, 0, 1);
    var offset  = 7 + 1 - newYear.getDay();
    var daynum  = ((Date.UTC(year, this.getMonth(), this.getDate(),0,0,0) -
                    Date.UTC(year, 0,               1,             0,0,0)) /
                        1000/60/60/24) + 1;
    var weeknum = Math.floor((daynum - offset + 7) / 7);
    if (weeknum == 0)
    {
        year--;
        var prevNewYear = new Date(year, 0, 1);
        var prevOffset  = 7 + 1 - prevNewYear.getDay();
        if ((prevOffset == 2) || (prevOffset == 8))
            weeknum = 53;
        else
            weeknum = 52;
    }

    return weeknum;
};

$P.fromStr = function(str) {
    var parts   = 
            str.match(/([0-9]{4})-([0-9]{2})-([0-9]{2})(?:T([0-9]{2}):([0-9]{2}):([0-9]{2})([+\-][0-9]{2}:[0-9]{2}))?/);

    if (parts && (parts.length > 0))
    {
        /* [1] year, [2] month,  [3] day
         * [4] hour, [5] minute, [6] second, [7] zone offset
         */
        this.setYear(  parts[1] );
        this.setMonth( parseInt(parts[2],10)-1 );
        this.setDate(  parts[3] );

        if (parts.length > 3)
        {
            this.setHours(   parts[4] );
            this.setMinutes( parts[5] );
            this.setSeconds( parts[6] );
        }

        /*
        console.log("str[ %s ] == 1:%s, 2:%s, 3:%s == %d.%d.%d: 4:%s, 5:%s, 6:%s, 7:%s == %d:%d",
                    str,
                    parts[1], parts[2], parts[3],
                    this.getFullYear(), this.getMonth()+1, this.getDate(),
                    parts[4], parts[5], parts[6], parts[7],
                    this.getHours(), this.getMinutes());
        // */
    }

    return this;
};

$P.rangeStr = function(end, incDay, incMonth) {
    var hours1   = this.getHours();
    var minutes1 = this.getMinutes();
    var hours2   = (end ? end.getHours()   : hours1);
    var minutes2 = (end ? end.getMinutes() : minutes1);

    var str      =  '';

    if (incDay === true)
    {
        str      +=  this.dayName() +', ';
    }

    if (incMonth === true)
    {
        str      += this.monthName() + this.getDate() +', ';
    }

    str          += ( (hours1 < 13)
                             ? ( hours1 === 0 ? 12 : hours1 )
                             : ( hours1 - 12 ) );
    if( minutes1 != 0 )
    	str += ':' + padStr( minutes1 );

    /* If the beginning and ending times aren't in the same meridian, show the
     * meridian for both.
     */
    if ( ((hours1 <  12) && (hours2 >= 12)) ||
         ((hours1 >= 12) && (hours2 <  12)) )
    {
        str += ( (hours1 < 12) ? $C.meridian[0] : $C.meridian[1]);
    }

    if ((hours2 > hours1) || (minutes2 > minutes1))
    {
        str      += ' - '
                 +  ( (hours2 < 13)
                              ? ( hours2 === 0 ? 12 : hours2 )
                              : ( hours2 - 12 ) )
        if( minutes2 != 0) 
                 str += ':' + padStr( minutes2 );
    }


    str += ( (hours2 < 12) ? $C.meridian[0] : $C.meridian[1]);

    /*
    console.log("rangeStr: current hour[ %s ], minute[ %s ]; end hour[ %s ], minute[ %s ] == [ %s ]",
                hours1, minutes1,
                hours2, minutes2,
                str);
    // */

    return str;
};

function dateTime2date(str)
{
    var ret     = new Date();

    ret.fromStr(str);

    return ret;
}

})(jQuery);
