﻿/**
 * 
 * @projectDescription     Handles promo events UK
 * @namespace              Promo.Events
 *
 * @author                 $Author: thorsten.diegel@neue-digitale.de $
 * @version                $Revision: 5079 $    
 * @copyright              Neue Digitale / Razorfish GmbH
 * 
 * 
 * 
 * Use GET-Param "eventDate_JJJJMMDD" to open a specific event item onload. e.g. ("eventDate_20100303")
 * 
 */
/* create namespace */
Promo = {};
Promo.Events = Class.create({
    /**
     * @constructor
     * @param {Object} obj_, Proterties: see this._oOptions;
        @return {Void}
     */
    initialize: function(obj_) {
        // set default options
        this._oOptions = {
            'sClassNameEventDatePrefix' : 'event_date_',
            'sIDEventDatePrefix' : 'event_id_',
            'sGETParamDate' : 'eventDate_',
            'sGETParamID' : 'eventID_'
        };
        this._oOptions.oSort = {
            'defaultOrder' : 'DESC',
            'sClassNameDESC' : 'DESC',
            'sClassNameASC' : 'ASC',
            'filters': {
                'date': {
                    sPrefix: 'event_date',
                    sAttribute: 'className'
                },
                'location': {
                    sPrefix: '',
                    sAttribute: 'title'
                }
            }
        };
        
        // copy passed options 
        Object.extend(this._oOptions, obj_);
        // cache wrapper element
        this._$elWrapper = $(obj_.sId);
        // cache overlay
         this._$elOverlay = $("event_overlay");
        
        // open event, using either the GET-param event_date or the default date from "sToday"
        this._openEventByDate(obj_.sId, obj_.sAnchorIdSnippet, obj_.sBoxIdSnippet);
        // add sort events
        this._addSortEvents();
    },
    /**
     * Adds sort events to .event_header_sort_date and .event_header_sort_location,
     * sets classNames indication order. 
     * @return {Void}
     */
    _addSortEvents: function  () {
        // cache scope
        var _self = this;
        // get default order 
        var _sDefaultOrder = _self._oOptions.oSort.defaultOrder;
        // sort by className, using the class "event_date" by default
        _self._$elWrapper.select('.event_header_sort_date')[0]
            .observe('click', function(event_) {
                _self.sortByDate(event_);
            })
            .addClassName(_sDefaultOrder);
            
        // sort by location, using the title attribute by default
        _self._$elWrapper.select('.event_header_sort_location')[0]
            .observe('click',  function(event_) {
                _self.sortByLocation(event_);
            })
            .addClassName(_sDefaultOrder);
    },
    
    /**
     * Sorts elements by date, bridge to _doSort
     * @see _doSort
     * @param {Event} event_
     * @return {void}
     */
    sortByDate: function (event_) {
        // cache scope
        var _self = this ;
        // grab element 
        var _$element = $(event_.target);
        // are we're sorting descendants?
        var _bOrderDesc = _$element.hasClassName(_self._oOptions.oSort.sClassNameDESC) || false;
        // go!
        this._doSort('date',_bOrderDesc,_$element);
    },
    
    
    /**
     * Sorts elements by location, bridge to _doSort
     * @see _doSort
     * @param {Event} event_
     * @return {void}
     */
    sortByLocation: function (event_) {
        // cache scope
        var _self = this ;
        // grab element 
        var _$element = $(event_.target);
        // are we're sorting descendants?
        var _bOrderDesc = _$element.hasClassName(_self._oOptions.oSort.sClassNameDESC) || false;
        // go!
        this._doSort('location',_bOrderDesc,_$element);
    },
//    _getElementsWrapper: function ($element_) {
//        var _$elWrapper = $$('.event_box').each(function ( ){
//            if ($element_.descendantOf(arguments[0])) {
//                return arguments[0];
//
//            }
//        })
//        _$elWrapper = $(_$elWrapper[0]);
//        return _$elWrapper;
//    },
    
    /**
     * Sorts the elements by comapring classname or attributes as defined in options.oSort.filters
     * 
     * @param {String} sFilter_ , filter , e.g. 'date'
     * @param {Bool} bOrderDesc_ true || false 
     * @param {[HTMLElement]} $element_ The HTMLElemenet that was the event.target, optional - needed for toggling classes  
     */
    _doSort: function (sFilter_,bOrderDesc_,$element_) {
        // cache scope
        var _self = this;
        var _aToSort = [];
        // cache stuff
        var $elWrapper_ = _self._$elWrapper;
        var _oOptions = _self._oOptions.oSort.filters;
        
        // cover elements
        _self._$elOverlay.setStyle({
            height: $elWrapper_.getHeight()-47+'px',
            display: 'block'
        });
        // get all event_items 
        $elWrapper_
            .select('.event_item')
            // push sortable attribute into an array 
            .each(function (){
                // extract from .className
                if (_oOptions[sFilter_].sPrefix) {
                    _aToSort.push(_oOptions[sFilter_].sPrefix + arguments[0][_oOptions[sFilter_].sAttribute].split(_oOptions[sFilter_].sPrefix)[1].split(' ')[0]);
                } 
                // push the whole attribute value
                else {
                    _aToSort.push(_oOptions[sFilter_].sPrefix + arguments[0][_oOptions[sFilter_].sAttribute]);
                }
            });
        // remove duplicates
        _aToSort = _aToSort.uniq();
        // use array.sort() - attention: lexical! 
        _aToSort.sort();
        // reverse if necesary 
        if (bOrderDesc_) {
            _aToSort.reverse();
        }
        // cycle all sortables
        $(_aToSort).each(function () {
            // clean up classname to class
            var _sAttrClean = (_oOptions[sFilter_].sAttribute === 'className') ? 'class' : _oOptions[sFilter_].sAttribute;
            // create Query string : e.g. *[class*=event_date_20090322], *[title*=Frankfurt] ]
            var _aQuery = ['*','[',_sAttrClean,'*=',arguments[0],']'];
//            console.log("looking for:", arguments[0], "using query: ", _aQuery.join(''))
            // select all elements with that particular sortable
            $elWrapper_.select(_aQuery.join('')).each(function () {
                // move them to the end of the wrapper DOM
                $(_self._$elOverlay).insert({before: $(arguments[0])});
            });
        });
        
        // set classnames indicating the new order
        if ($element_) {
            $element_.toggleClassName(_self._oOptions.oSort.sClassNameDESC).toggleClassName(_self._oOptions.oSort.sClassNameASC);
        }
        // hide overlay 
        Effect.Fade(_self._$elOverlay.readAttribute('id'),{to: 0, duration:0.5});
    },
    
    
    /**
     * Returns Actual Date in Format yyyymmdd
     * @private
     * @return {sToday_:String}
     */    
    
     _getDateToday: function() {
        var _self = this;
        
        var sToday_ = new Date();
        var strDay = sToday_.getDate().toString();
        if (strDay.length == 1) { strDay = "0" + strDay; }
        var strMonth = (sToday_.getMonth()+1).toString();
        if (strMonth.length == 1) { strMonth = "0" + strMonth; }
        sToday_ = sToday_.getFullYear()+strMonth+strDay;
        return sToday_;
    },
    
    
    /**
     * Opens EventItem by specified GET-Param "eventDate" or constructor options
     * @private
     * @see window.fadeNAppearDiv "Fadet ein Div erst aus und dann das andere ein"
     * @see constructor options
     * @param {String} sId_
     * @param {String} sAnchorIdSnippet_
     * @param {String} sBoxIdSnippet_
     * @return {Bool}
     */    
     _openEventByDate: function(sId_, sAnchorIdSnippet_,sBoxIdSnippet_) {
        var _self = this;
        var sToday_ = this._getDateToday();
        
        // construct classname-selector use GET-Param or suopplied date
        if (window.location.search.indexOf(_self._oOptions.sGETParamID) !== -1) {
            var _sDateSelector = _self._oOptions.sIDEventDatePrefix + window.location.search.split(_self._oOptions.sGETParamID)[1].split('&')[0];
            // get element by id below #sId_
            var _$elementsArr = $(sId_).select('#' + _sDateSelector); 
        }
        else {
            var _sDateSelector = _self._oOptions.sClassNameEventDatePrefix + ((window.location.search.indexOf(_self._oOptions.sGETParamDate) !== -1) ? window.location.search.split(_self._oOptions.sGETParamDate)[1].split('&')[0] : sToday_);
            // get element by classname below #sId_
            var _$elementsArr = $(sId_).select('.' + _sDateSelector);
        }
        
        _$elementsArr.each(function(item) {
            // grab value for fadeNAppearDiv from anchor
            var _sIdSnippet = item.id.split(sAnchorIdSnippet_)[1];    
            window.fadeNAppearDiv(sBoxIdSnippet_ + _sIdSnippet);
        });
        return true;
    }
});

var EventStorage = new (Class.create({
        initialize: function(){
             this._oData = null;
            this._oPostCodeMappings = null;
        },
        
        getEventData: function(){
            if(this._oData == null) {
                this._oData = eventData;
            }   
            return this._oData;
        },
        
        getPostCodeMappings: function() {
               if (this._oPostCodeMappings == null) {
                var postCodeMapping = new Hash();
                
                this._oData = this.getEventData();
                this._oData.POSTCODES.each(function(item){
                    postCodeMapping.set(item.ID, item.VALUES);
                });
                this._oPostCodeMappings = postCodeMapping;
            }
              return this._oPostCodeMappings;
        }
    })
)();


Promo.DateRepository = Class.create({
     /**
     * @constructor
     * @param {Object} obj_, Properties: see this._oOptions;
     * @return {Void}
     */
    initialize: function(obj_) {
         // set default options
        this._oOptions = {
            'oMonthNames': {
              '0' : 'January',
              '1' : 'February',
              '2' : 'March',
              '3' : 'April',
              '4' : 'May',
              '5' : 'June',
              '6' : 'July',
              '7' : 'August',
              '8' : 'September',
              '9' : 'October',
              '10': 'November',
              '11': 'December'
             },
            'oDayNames': {
              '0' : 'Mo',
              '1' : 'Tu',
              '2' : 'We',
              '3' : 'Th',
              '4' : 'Fr',
              '5' : 'Sa',
              '6' : 'Su'
             },
             'oDateDelimiter' : '/'
        };

        this._oOptions.oSort = {};
        
        // copy passed options 
        Object.extend(this._oOptions, obj_);    
        
        this.dateDelimiter = this._oOptions.oDateDelimiter;
     },
     
     
     
     getNameOfWeekday: function(index) {
        return this._oOptions.oDayNames[index];
     },
     
     getNameOfMonth: function(index) {
        return this._oOptions.oMonthNames[index];
     },
     
     renderHeadlineTemplateInit: function() {
        var today = new Date();
         return this._oOptions.headlineTemplateInit.evaluate( {dateString: this.getNameOfMonth(today.getMonth())} );
     },
     
      renderHeadlineTemplateForMonth: function(monthIndex) {
         return this._oOptions.headlineTemplateInit.evaluate( {dateString: this.getNameOfMonth(monthIndex)} );
     },
      
     renderHeadlineTemplateCalendar: function(dateString) {
         return this._oOptions.headlineTemplateCalendar.evaluate( {dateString: dateString} );
     },
      
     renderHeadlineTemplatePeriod: function(dateStart, dateEnd) {
         return this._oOptions.headlineTemplatePeriod.evaluate( {dateStart: dateStart, dateEnd: dateEnd} );
     }
    
});

Promo.Map = Class.create({
     
     /**
     * @constructor
     * @param {Object} obj_, Properties: see this._oOptions;
     * @return {Void}
     */
    initialize: function(obj_) {
        
        // set default options
        this._oOptions = {
            'sIdMapContainer': 'sIdMapContainer',
            'sIdListContainer': 'sIdListContainer',
            'sIdMapWrapper': 'sIdMapWrapper',
            'sIdMapSlider': 'sIdMapSlider',
            'sIdEventHeader' : 'eventHeader',
            'numberOfEventsPerPage' : 5,
            'labelNext' : 'next',
            'labelPrevious' : 'previous'
        };
        
        this._oOptions.oSort = {};
         
        // copy passed options 
        Object.extend(this._oOptions, obj_);
        
        // scope reference
        var _self = this;
        
        // get event data           
        this._oData = EventStorage.getEventData();
        this._oPostCodeMappings = EventStorage.getPostCodeMappings();
                    
        // get all relevant DOM elements as global PrototypElement
        this._getPrototypeElements();
        
        // define constants
        this._numberOfEventsPerPage = this._oOptions.numberOfEventsPerPage;
        
        this._mapDimensions = this._elMap.getDimensions();
        this._viewportDimensions = this._elMapContainer.getDimensions();
        this._currentZoomLevel = this._oOptions.defaultZoomLevel;
        this._zoomLevelMappings = new Array(0, 0.5, 1, 2);
        
        // register Events
        this._registerCustomEvents();
        
        this._initalizeMap(this._currentZoomLevel);
        
        this._initalizeEventHeader();
        this._initalizeEventsForCurrentMonth();
        //this.displayMarkerForAllPostcodes();
    },
    
    
    _getPrototypeElements: function() {
        
        this._elHeader = $(this._oOptions.sIdEventHeader);
        this._elHeaderHeadlineMap = this._elHeader.down('h3.map');
        this._elHeaderHeadlineList = this._elHeader.down('h3.list');
        this._elIconMap = this._elHeader.down('.map a');
        this._elIconList = this._elHeader.down('.list a');
        
        this._elMapContainer = $(this._oOptions.sIdMapContainer);
        this._elListContainer = $(this._oOptions.sIdListContainer);
        
        this._elMapWrapper = $(this._oOptions.sIdMapWrapper);
        this._elMap = this._elMapWrapper.select('.map').first();
        
        this._elNavigationItems = $(this._oOptions.sIdNavigationItems);
    },
    
    
    _registerDragEvents: function() {
        
        var _self = this;
        this._draggable = new Draggable(this._elMapWrapper, {
            starteffect: null,
            endeffect: null,
            onStart: function() {
                _self._elMapWrapper.setStyle({
                    cursor: 'move'
                });
            
                _self._disableAllActiveMarkers();
            },
            onEnd: function() {
                _self._elMapWrapper.setStyle({
                    cursor: 'default'
                });
                
                _self._enableAllActiveMarkers();
            },
            snap: function(x, y) {
                var maxTopValue = (parseInt(_self._elMap.getHeight()) - (parseInt(_self._elMapContainer.getHeight() - 2))) * -1;
                var maxLeftValue = (parseInt(_self._elMap.getWidth()) - (parseInt(_self._elMapContainer.getWidth() - 4) )) * -1;
                
                x = (x > maxLeftValue) ? (x < 0 ? x : 0 ) : maxLeftValue;
                y = (y > maxTopValue) ? (y < 0 ? y : 0) : maxTopValue;
                
                return[ x, y ];
            }
        });
    },
    
    
    _registerZoomEvents: function() {
         
         var _self = this;
                  
         this._elNavigationItems.down('.zoomIn').observe('click', function(){
             //_self.zoomMap(_self._currentZoomFactor + 1);
             _self._objSlider.setValue(_self._currentZoomLevel + 1);
         });
         
         this._elNavigationItems.down('.zoomOut').observe('click', function(){
             //_self.zoomMap(_self._currentZoomFactor - 1);
            _self._objSlider.setValue(_self._currentZoomLevel - 1);
         });    
         
         
         this._objSlider = new Control.Slider(this._elNavigationItems.down('.handle'), this._elNavigationItems.down('.track'), {
                axis: 'vertical',
                range: $R(2, 1),
                values: [2,1],
                sliderValue: 1,
                onChange: function(zoomLevel){
                    _self.zoomMap(zoomLevel);
                }
         });
              
    },
    
    _registerScrollButtons: function() {
        
        var _self = this;
        var scrollSteps = 200 * this._currentZoomLevel;
        var duration = 0.4;
        
        this._elNavigationItems.down('.top').observe('click', function() {
            var topValue = parseInt(_self._elMapWrapper.getStyle('top'));
            var newTopOffset = (topValue + scrollSteps) > 0 ? 0 : topValue + scrollSteps;  
            
            new Effect.Move(_self._elMapWrapper, { x: parseInt(_self._elMapWrapper.getStyle('left')), y: newTopOffset, mode: 'absolute', duration: duration });
        })
        
        this._elNavigationItems.down('.bottom').observe('click', function() {
            
            var topValue = parseInt(_self._elMapWrapper.getStyle('top'));
            var maxTopValue = parseInt(_self._elMap.getHeight()) - (parseInt(_self._elMapContainer.getHeight() - 2) );
            
            var newTopOffset = ((topValue - scrollSteps) <= (maxTopValue * -1) ) ? (maxTopValue * -1) : topValue - scrollSteps;  
            
            new Effect.Move(_self._elMapWrapper, { x: parseInt(_self._elMapWrapper.getStyle('left')), y: newTopOffset, mode: 'absolute', duration: duration });
        })
        
        this._elNavigationItems.down('.left').observe('click', function() {
            var leftValue = parseInt(_self._elMapWrapper.getStyle('left'));
            var newLeftOffset = (leftValue + scrollSteps) > 0 ? 0 : leftValue + scrollSteps;  
            
            new Effect.Move(_self._elMapWrapper, { x: newLeftOffset, y: parseInt(_self._elMapWrapper.getStyle('top')), mode: 'absolute', duration: duration });
        })
        
        
        this._elNavigationItems.down('.right').observe('click', function() {
            
            var leftValue = parseInt(_self._elMapWrapper.getStyle('left'));
            var maxLeftValue = parseInt(_self._elMap.getWidth()) - (parseInt(_self._elMapContainer.getWidth() - 4) );
            
            var newLeftOffset = ((leftValue - scrollSteps) <= (maxLeftValue * -1) ) ? (maxLeftValue * -1) : leftValue - scrollSteps;  
            
            new Effect.Move(_self._elMapWrapper, { x: newLeftOffset, y: parseInt(_self._elMapWrapper.getStyle('top')), mode: 'absolute', duration: duration });
        })
        
    }, 
    
    
    _registerCustomEvents: function() {
         
         var _self = this;
         
         document.observe('Promo.Map:displayMapEvents', function(event) {            
            _self.displayEventsOnMap(event.memo.aIndexes);
         });
         
         document.observe('Promo.Map:changeHeadlineText', function(event) {            
             _self._elHeaderHeadlineMap.innerHTML = event.memo.headline;
         });
    },
    
    
    _initalizeMap: function(zoomFactor) {
    
        // set Map size (depending on zoom level)
        this._elMap.setStyle( { 
            width: this._mapDimensions.width * zoomFactor + 'px',
            height: this._mapDimensions.height * zoomFactor  + 'px'
        });
        
        // set OffsetFor Map Wrapper
        this._elMapWrapper.setStyle({
            top: (this._elMap.getHeight() - this._viewportDimensions.height +2) * -1 + "px",
            left: '0px'
        })
        
        // initalize Drag element
        this._registerDragEvents();    ;
        
        // initalize ScrollButtons
        this._registerScrollButtons();
        
        // register zoom buttons
        this._isZooming = false;
        this._registerZoomEvents();
    },
    _initalizeEventHeader: function() {
        
        var _self = this;
        
        this._elHeaderHeadlineMap.innerHTML = dateRepository.renderHeadlineTemplateInit();
        
        this._elIconMap.addClassName('active');
        this._elHeaderHeadlineList.hide();
        this._elListContainer.hide();
        
        this._elIconMap.observe('click', function(){
            
            if(!_self._elIconMap.hasClassName('active')){
                _self._elIconMap.addClassName('active');
                _self._elIconList.removeClassName('active');
                
                _self._elHeaderHeadlineList.hide();
                _self._elListContainer.hide();
                
                _self._elHeaderHeadlineMap.show();
                _self._elMapContainer.show();
                
                document.fire("Promo.PeriodSelection:toggleModuleVisibility", {});
                document.fire("Promo.TopEvents:toggleModuleVisibility", {});
                document.fire("Promo.EventCalendar:toggleEventPane", {});
            }
            return false;
        });
        
        this._elIconList.observe('click', function(){
            
            if(!_self._elIconList.hasClassName('active')){
                _self._elIconList.addClassName('active');
                _self._elIconMap.removeClassName('active');
                
                _self._elHeaderHeadlineMap.hide();
                _self._elMapContainer.hide();
                
                _self._elHeaderHeadlineList.show();
                _self._elListContainer.show();
                
                document.fire("Promo.PeriodSelection:toggleModuleVisibility", {} );
                document.fire("Promo.TopEvents:toggleModuleVisibility", {});
                document.fire("Promo.EventCalendar:toggleEventPane", {});
            }
            return false;
        });
        
    },
    
    
    _initalizeEventsForCurrentMonth: function() {
        
        var currentMonth = Util.DateHelper.getMapIndexForCurrentMonth();
        var nextMonth = Util.DateHelper.getMapIndexForNextMonth();
    
        // get Indexes of events to be displayed for current month
        var aIndexes = new Array();
        for (var i = 0; i < this._oData.MAP.length; i++) {
            if (this._oData.MAP[i] >= currentMonth && this._oData.MAP[i] < nextMonth) {
                
                aIndexes.push(i);
            }
        } 
        this.displayEventsOnMap(aIndexes);
    },
    
    
    _removeAllMarkers: function() {
        
        this._elMapWrapper.select('.mapMarker').each(function(item) {
          item.remove();
        });
    },
    
    _removeAllTooltips: function() {
        
        $(document.body).select('.mapToolTip').each(function(item) {
          item.remove();
        });
    },
    
    _hideAllVisibleTooltips: function() {
        
        $(document.body).select('.mapToolTip.visible').each(function(item) {
          item.setStyle({ display: 'none' });
          item.removeClassName('visible');
        });
    },
    
    _createNewMarker: function(eventPostcode, eventHTML, diffPerspectiveTop, diffPerspectiveLeft, zoomValue) {
        
        var _self = this;
        
        var postCodeMapping = this._oPostCodeMappings.get(eventPostcode);
        
        if (postCodeMapping != undefined) {
            
            var coordinates = postCodeMapping.COORDINATES.split(",");
            
            var marker = new Element('div', {
                'class': 'mapMarker enabled active',
                'rel': coordinates[0] + ',' + coordinates[1],
                'id': 'marker_' + eventPostcode
            });
            var toolTip = new Element('div', {
                'class': 'mapToolTip',
                'id': eventPostcode
            }).update('<a class="previous" style="display:none">&laquo; ' + this._oOptions.labelPrevious + '</a><a class="next" style="display:none">' + this._oOptions.labelNext + ' &raquo;</a><div class="container clearfix"><div class="page active">' + eventHTML + '</div></div><span class="bottom" style="display:none"></span>');
            
            marker.setStyle({
                top: coordinates[0] * zoomValue + diffPerspectiveTop + "px",
                left: coordinates[1] * zoomValue + diffPerspectiveLeft + "px"
            });
            
            marker.observe('mouseenter', function(){
            
                if (marker.hasClassName('enabled')) {
                    _self._hideAllVisibleTooltips();
                    
                    marker.setStyle({
                        zIndex: '10'
                    });
                    toolTip.setStyle({
                        top: marker.cumulativeOffset()[1] + 'px',
                        left: marker.cumulativeOffset()[0] + 'px',
                        display: 'block'
                    });
                    toolTip.addClassName('visible');
                }
            });
            
            toolTip.observe('mouseleave', function(){
                marker.setStyle({
                    zIndex: '1'
                });
                toolTip.setStyle({
                    display: 'none'
                });
                toolTip.removeClassName('visible');
            });
            
            this._elMapWrapper.appendChild(marker);
            document.body.appendChild(toolTip);
            
        }
    },
    
    
    _disableAllActiveMarkers: function() {
        this._elMapWrapper.select('.mapMarker').each(function(item) {
            item.removeClassName('enabled');
        });
    },
    
    _enableAllActiveMarkers: function() {
        this._elMapWrapper.select('.mapMarker').each(function(item) {
            item.addClassName('enabled');
        });
    },
    
    
    _hideAllActiveMarkers: function() {
        this._elMapWrapper.select('.mapMarker.active').each(function(item) {
            item.hide();
        });
    },
    
    _showAllActiveMarkers: function() {
        this._elMapWrapper.select('.mapMarker.active').each(function(item) {
            item.show();
        });
    },
    
    _rePositionAllMarkers: function(zoomLevel) {
        
        var diffPerspectiveTop = new Array(0,-18,0,36)[zoomLevel];
        var diffPerspectiveLeft = new Array(0,-7.5,0,15)[zoomLevel];
        var zoomValue = this._zoomLevelMappings[zoomLevel];
        this._elMapWrapper.select('.mapMarker').each(function(item) {
            var originalCoordinates = item.readAttribute('rel').split(',');
            item.setStyle({
                  top: (originalCoordinates[0] * zoomValue) + diffPerspectiveTop + 'px',
                left: (originalCoordinates[1] * zoomValue)  + diffPerspectiveLeft + 'px'
            });            
        });
        
    },
    
    _insertAdditionalEvent: function(elPostcode, eventHTML) {
        
        var numberOfEvents = elPostcode.select('.game-mini').length;
        var pageIndex = Math.floor(numberOfEvents / this._numberOfEventsPerPage);
        var eventPage = elPostcode.down('.page', pageIndex);
        
        if(eventPage != undefined) {
            eventPage.insert('<hr>' + eventHTML);
        }
        else {
            elPostcode.down('.container', 0).insert('<div class="page">' + eventHTML + '</div>');
        }        
    },
    
    
    displayMarkerForAllPostcodes: function(){
        this._removeAllMarkers();
        this._removeAllTooltips();
        var diffPerspectiveTop = new Array(0,-18,0,36)[this._currentZoomLevel];
        var diffPerspectiveLeft = new Array(0,-7.5,0,15)[this._currentZoomLevel];
        var zoomValue = this._zoomLevelMappings[this._currentZoomLevel];
        
        for(var i = 0; i <  this._oData.POSTCODES.length; i++) {
          
          var postcode = this._oData.POSTCODES[i];
          var coordinates = postcode.VALUES.COORDINATES.split(",");
          
          var marker = new Element('div', { 'class': 'mapMarker', 'title': postcode.VALUES.NAME, 'rel': coordinates[0]+','+coordinates[1] , 'id' : 'marker_'+postcode.ID });
        
          marker.setStyle( {
                top: coordinates[0] * zoomValue + diffPerspectiveTop  + "px",
                left: coordinates[1] * zoomValue + diffPerspectiveLeft  + "px"
          });
          
          this._elMapWrapper.appendChild(marker);
        }
    },
    displayEventsOnMap: function(aIndexes) {
        this._removeAllMarkers();
        this._removeAllTooltips();
        
        
        var diffPerspectiveTop = new Array(0,-18,0,36)[this._currentZoomLevel];
        var diffPerspectiveLeft = new Array(0,-7.5,0,15)[this._currentZoomLevel];
        var zoomValue = this._zoomLevelMappings[this._currentZoomLevel];
        
        
        for(var i = 0; i < aIndexes.length; i++) {
           
               var event = this._oData.EVENTS[aIndexes[i]];
            
            if (event != undefined) {
            
                var elPostcode = $(event.POSTCODE);
                
                if (elPostcode == null) {
                    this._createNewMarker(event.POSTCODE, event.HTML, diffPerspectiveTop, diffPerspectiveLeft, zoomValue);
                }
                else {
                    this._insertAdditionalEvent(elPostcode, event.HTML);
                }
                
            }
        }
        
        
        document.body.select('.mapToolTip').each(function(el) {
           
            if(el.select('.page').length > 1){
                
                var elPrevious = el.select('.previous').first();
                var elNext = el.select('.next').first();
            
                            
                elNext.observe('click', function() {
                        
                        var activeElement = el.down('.page.active');
                        activeElement.removeClassName('active');
                        activeElement.next().addClassName('active');
                        
                        elPrevious.show();
                        
                        if(activeElement.next(1) == null) {
                            elNext.hide();
                        }
                        return false;
                    })
                .show(); 
                
                
                elPrevious.observe('click', function() {
                        
                        var activeElement = el.down('.page.active');
                        activeElement.removeClassName('active');
                        activeElement.previous().addClassName('active');
                        
                        elNext.show();
                        
                        if(activeElement.previous(1) == null) {
                            elPrevious.hide();
                        }
                        return false;
                    })            
                
            }
           
        });
    },
    
    zoomMap: function(zoomLevel) {
        
        if (zoomLevel != this._currentZoomLevel && zoomLevel > 0 && !this._isZooming) {
                
            var _self = this;
            
            this._isZooming = true;
            
            var zoomDiff = zoomLevel - this._currentZoomLevel;
            var zoomValue = (zoomDiff > 0) ?  zoomDiff * 2 : 1 / (zoomDiff * -2);
            
            var animationSteps = 8;
            
            var mapWidth = parseInt($('map').getWidth());
            var mapHeight = parseInt($('map').getHeight());
            
            var mapContainerWidth = _self._viewportDimensions.width -4;
            var mapContainerHeight = _self._viewportDimensions.height - 2;
            
            var diffWidth = ((mapWidth * zoomValue) - mapWidth) / animationSteps;
            var diffHeight = ((mapHeight * zoomValue) - mapHeight) / animationSteps;
            
            _self._hideAllActiveMarkers();
            
            // zoom animations
            window.step = 0;
            window.timer = window.setInterval(function(){
            
                _self._elMap.setStyle({
                    width: parseInt(_self._elMap.getWidth()) + diffWidth + 'px',
                    height: parseInt(_self._elMap.getHeight()) + diffHeight + 'px'
                })
                
                var offsetTop = Math.round(parseInt(_self._elMapWrapper.getStyle('top')) - (diffHeight / 2));
                var offsetLeft = Math.round(parseInt(_self._elMapWrapper.getStyle('left')) - (diffWidth / 2));
                
                if (zoomValue < 1) {
                    offsetTop = (offsetTop > 0) ? 0 : offsetTop;
                    offsetTop = (_self._elMap.getHeight() < ((parseInt(_self._elMapWrapper.getStyle('top')) - mapContainerHeight) * -1)) ? (_self._elMap.getHeight() - mapContainerHeight) * -1 : offsetTop;
                    
                    offsetLeft = (offsetLeft > 0) ? 0 : offsetLeft;
                    offsetLeft = (_self._elMap.getWidth() < ((parseInt(_self._elMapWrapper.getStyle('left')) - mapContainerWidth) * -1)) ? (_self._elMap.getWidth() - mapContainerWidth) * -1 : offsetLeft;
                }
            
                _self._elMapWrapper.setStyle({
                    top: offsetTop  + 'px',
                    left: offsetLeft + 'px'
                })
                if (++window.step == animationSteps) {
                    window.clearInterval(window.timer);
                    window.timer = null;
                    window.step = 0;
                    _self._rePositionAllMarkers(zoomLevel);
                    _self._showAllActiveMarkers();
                    
                    _self._currentZoomLevel = zoomLevel;
                    _self._isZooming = false;
                }
            }, 13)
        }        
    }
    
});


Promo.Calendar = Class.create({
     /**
     * @constructor
     * @param {Object} obj_, Properties: see this._oOptions;
     * @return {Void}
     */
    initialize: function(obj_) {
         // set default options
        this._oOptions = {
            'sIdCalendar': 'sIdCalendar'
        };
        
        this._oOptions.oSort = {};
        // copy passed options 
        Object.extend(this._oOptions, obj_);
        // the current day
        this._today = new Util.Date();
        
        // the first day of the current month
        this._setFirstDayOfMonth(this._today.getFirstDayOfMonth());
        
        // the offset of pagination to start with
        this._offset = 0;
        
        // scope reference
        var _self = this;
        
        // the content container for the whole calendar
        this._eventCalendar = $(this._oOptions.sIdCalendar);
        this._eventCalendar.addClassName('calendar clearfix');
        this._eventCalendar.update('<div class="table clearfix"><div id="calendar-labels" class="labels clearfix"></div> <div id="calendar-days" class="days clearfix"></div></div><div class="events" id="calendar-events"></div>');
        
        // container for navigation elements
        this._navi = $(document.createElement('div'));
        this._navi.addClassName('navi clearfix');
        this._navi.update('<div class="navi clearfix"><a href="javascript:void(0);" class="prev">prev</a><p></p><a href="javascript:void(0);" class="next">next</a></div>');
        
        // the range of month allowed to paginate to
        var iPrevMonths = 2;
        var iNextMonths = 3;
        
        // previous month link
        this._prevLink = this._navi.getElementsBySelector('a.prev').first();
        this._prevLink.observe('click', function(event) {
            
            if(_self._offset > -iPrevMonths) { 
                _self._offset--;
                _self._setFirstDayOfMonth(_self._firstDayOfMonth.getFirstDayOfPrevMonth());
                _self._buildHtml();
                _self._nextLink.removeClassName("next-off");
            }
            if(_self._offset == -iPrevMonths) { 
                _self._prevLink.addClassName("prev-off");
            }
            _self._eventCalendar.fire("Promo.Calendar:monthChanged", { firstDayOfMonth: _self._firstDayOfMonth });
            
            Event.element(event).blur();
            return false;
        });
        
        // next month link
        this._nextLink = this._navi.getElementsBySelector('a.next').first();
        this._nextLink.observe('click', function(event) {
            if(_self._offset < iNextMonths) { 
                _self._offset++;
                _self._setFirstDayOfMonth(_self._firstDayOfMonth.getFirstDayOfNextMonth());
                _self._buildHtml();
                _self._prevLink.removeClassName("prev-off");
            }
            if(_self._offset == iNextMonths) {
                _self._nextLink.addClassName("next-off");                
            }
            
            _self._eventCalendar.fire("Promo.Calendar:monthChanged", { firstDayOfMonth: _self._firstDayOfMonth });
            
            Event.element(event).blur();
            return false;
        });
        
        this._eventCalendar.insert({'top': this._navi});
        
        // create the labels (monday - sunday)
        var labelContainer = this._eventCalendar.getElementsBySelector('.labels').first();
        for(var d = 0; d < 7; d++) {
            var _label = $(document.createElement('div'));
            _label.addClassName('cell');
            _label.update(this._oOptions.dateRepository.getNameOfWeekday(d));
            labelContainer.insert(_label);
        }
        
        // fill CalendarCells
        this._buildHtml();
        
        // registerCustomEvents
        this._registerCustomEvents();
     },
    
    
     _registerCustomEvents: function() {
         
         var _self = this;
         
         document.observe('Promo.Calendar:deactivateCalendarCells', function(event) {
             _self.deactivateCalendarCells();
         });
             
     },
    
     /**
     * Build HTML data
     * @return {void}
     * @private
     */
    _buildHtml: function(){
    
        var _self = this;
        
        // update the month label
        var monthLabel = this._eventCalendar.getElementsBySelector('.navi p').first();
        monthLabel.update(this._oOptions.dateRepository.getNameOfMonth(this._firstDayOfMonth.getMonthAsInt()) + ' ' + this._firstDayOfMonth.getYearAsInt());
        
        var firstDayOfMonth = this._firstDayOfMonth;
        var firstDayOfWeek = firstDayOfMonth.getFirstDayOfWeek();
        var lastDayOfMonth = firstDayOfMonth.getLastDayOfMonth();
        var lastDayOfWeek = lastDayOfMonth.getLastDayOfWeek();
        
        var cellContainer = this._eventCalendar.getElementsBySelector('.days').first();
        
        //clear the container
        cellContainer.update('');
        
        var date = firstDayOfWeek.clone();
        while (date.getMicrotime() <= lastDayOfWeek.getMicrotime()) {
            cellContainer.insert(this._getCellHtml(date));
            date.addDays(1);
        }
    },
    
    /**
     * Set the first day of the month, used to build all days and calculate next/prev months
     * @param Util.Date
     */
    _setFirstDayOfMonth: function(date) {
        this._firstDayOfMonth = date;
    },
    
    
    /**
     * Get the html for one cell (day)
     * @param Util.Date
     * @return Prototype.Element
     */
    _getCellHtml: function(date) {
        var _self = this;
        var link = $(document.createElement('a'));
        link.href = "javascript:void(0)";
        link.update(date.getDayAsInt());
        link.addClassName('cell');
        
        // If mapIndex equals today add className 'today'
        if(this._today.getMapIndex() == date.getMapIndex()) {
            link.addClassName('today');
        }
        
         // If there is an event for that date add className 'event'
         if (this._oOptions._oData != null) {
             var i = this._oOptions._oData.MAP.indexOf(date.getMapIndex());
             if (i !== -1) {
                 link.addClassName('event');
             }
         }
                
        if(this._firstDayOfMonth.getMonthAsInt() != date.getMonthAsInt()) {
            link.addClassName('off');
        }
        else {
            link.writeAttribute('rel', date.getMapIndex());
        }
        
        // observe click events and fire custom event
        link.stopObserving('click');
        link.observe('click', function(event) {
            _self._eventCalendar.fire("Promo.Calendar:calendarclick", { cell: link });
        });
        return link;
    },
    
    activateCalendarCell: function(element) {
        // remove active cells
        this._eventCalendar.getElementsBySelector('a.cell').each(function(el) {
            $(el).removeClassName('active');
        });
            
        element.addClassName('active');
        element.blur();
    },
    
    deactivateCalendarCells: function() {
        this._eventCalendar.getElementsBySelector('a.cell').each(function(el) {
            $(el).removeClassName('active');
        });
    }
});

Promo.DefaultEventCalendar = Class.create({
    /**
     * @constructor
     * @param {Object} obj_, Proterties: see this._oOptions;
     * @return {Void}
     */
    initialize: function(obj_) {
        
        // set default options
        this._oOptions = {
            'sIdCalendarOverlay': 'event-calendar-12130',
            'sIdPeriodContainter': 'event-period',
            'sIdPeriodSelection' : 'event-period-selection',
            'sIdPostcodeSelection' : 'event-postcode-selection',
            'sIdEventContainter' : 'calendar-events'
        };
        
        this._oOptions.oSort = {};
        
        // copy passed options 
        Object.extend(this._oOptions, obj_);
        
        
        // scope reference
        var _self = this;
        
         // get event data           
        this._oData = EventStorage.getEventData();
        
        // initalize CalendarModule
        this._calendar = new Promo.Calendar({
            'sIdCalendar':   this._oOptions.sIdCalendar,
            'dateRepository': this._oOptions.dateRepository,
            '_oData': this._oData
        });
                
        // get all relevant DOM elements as global PrototypElement
        this._getPrototypeElements();
        
        // register all module related events
        this._registerEvents();
    },

    _getPrototypeElements: function() {
        this._eventCalendar = $(this._oOptions.sIdCalendar);
        this._elEventContainer = $(this._oOptions.sIdEventContainter);
    },
    
    _registerEvents: function() {
        
        var _self = this;
        
         // observe  custom calendar click event for calendar cells
        this._eventCalendar.observe('Promo.Calendar:calendarclick', function(event) {
            
            // get element that triggered event
            var element = event.memo.cell;
            
            // activate Calendar Cell
           _self._calendar.activateCalendarCell(element);
           
            var dateValue = element.readAttribute('rel');
                
            // get all event indexes for one day
            var aIndexes = new Array();
            for (var i = 0; i < _self._oData.MAP.length; i++) {
                if (_self._oData.MAP[i] == dateValue) {
                    aIndexes.push(i);
                }
            }
            _self.displayEvents(aIndexes);
        });    
        
    },
    
    displayEvents: function(aIndexes) {
    
        if(aIndexes.length == 0) {
            this._elEventContainer.update('');            
        }
        else {
            // empty container
            this._elEventContainer.update('');
            for(var i = 0; i < aIndexes.length; i++) {
                var event = this._oData.EVENTS[aIndexes[i]];
                this._elEventContainer.insert(event.HTML);
            }
        }
   }
    
});



Promo.EventCalendar = Class.create({
    /**
     * @constructor
     * @param {Object} obj_, Proterties: see this._oOptions;
     * @return {Void}
     */
    initialize: function(obj_) {
        
        // set default options
        this._oOptions = {
            'sIdCalendarOverlay': 'event-calendar-12130',
            'sIdPeriodContainter': 'event-period',
            'sIdPeriodSelection' : 'event-period-selection',
            'sIdPostcodeSelection' : 'event-postcode-selection',
            'sIdEventContainter' : 'calendar-events'
             
        };
        
        this._oOptions.oSort = {};
        
        // copy passed options 
        Object.extend(this._oOptions, obj_);
        
        
        // scope reference
        var _self = this;
        
         // get event data           
        this._oData = EventStorage.getEventData();
        
        // initalize CalendarModule
        this._calendar = new Promo.Calendar({
            'sIdCalendar':   this._oOptions.sIdCalendar,
            'dateRepository': this._oOptions.dateRepository,
            '_oData': this._oData
        });
        
        this._displayEventsOnMap = true;
        
        // get all relevant DOM elements as global PrototypElement
        this._getPrototypeElements();
        
        // register all module related events
        this._registerEvents();
        
        // register custom events
        this._registerCustomEvents();
    },

    _getPrototypeElements: function() {
        this._eventCalendar = $(this._oOptions.sIdCalendar);
        this._elEventContainer = $(this._oOptions.sIdEventContainter);
    },
    
    _registerEvents: function() {
        
        var _self = this;
        
         // observe  custom calendar click event for calendar cells
        this._eventCalendar.observe('Promo.Calendar:calendarclick', function(event) {
            
            // get element that triggered event
            var element = event.memo.cell;
            
            // activate Calendar Cell
           _self._calendar.activateCalendarCell(element);
           
            var dateValue = element.readAttribute('rel');
                
            // get all event indexes for one day
            var aIndexes = new Array();
            for (var i = 0; i < _self._oData.MAP.length; i++) {
                if (_self._oData.MAP[i] == dateValue) {
                    aIndexes.push(i);
                }
            }
            // fire hide postcode selection event 
            document.fire("Promo.PeriodSelection:resetPeriodSelection", { });
            
            document.fire("Promo.Map:changeHeadlineText", {headline:  dateRepository.renderHeadlineTemplateCalendar(Util.DateHelper.parseMapIndexToDateString(dateValue, _self._oOptions.dateRepository.dateDelimiter)) } );
            
            document.fire("Promo.Map:displayMapEvents", {
                aIndexes: aIndexes
            });
            _self.displayEvents(aIndexes);
        });    
        
         // observe  custom calendar click event for calendar cells
        this._eventCalendar.observe('Promo.Calendar:monthChanged', function(event) {
        
            // get month to be displayed
            var firstDayOfMonth = event.memo.firstDayOfMonth;
            // get Indexes of events to be displayed for current month
            var aIndexes = new Array();
            for (var i = 0; i < _self._oData.MAP.length; i++) {
                if (_self._oData.MAP[i] >= firstDayOfMonth.getMapIndex() && _self._oData.MAP[i] < firstDayOfMonth.getFirstDayOfNextMonth().getMapIndex()) {
                    aIndexes.push(i);
                }
            } 
            
            // fire hide postcode selection event 
            document.fire("Promo.PeriodSelection:resetPeriodSelection", { });
            document.fire("Promo.Map:changeHeadlineText", {headline:  dateRepository.renderHeadlineTemplateForMonth(firstDayOfMonth.getMonthAsInt()) } );
            
            document.fire("Promo.Map:displayMapEvents", {
                aIndexes: aIndexes
            });
        
        });
    },
    
    _registerCustomEvents: function() {
         var _self = this;
         
         _self._elEventContainer.hide();
         
        document.observe('Promo.EventCalendar:toggleEventPane', function(event) {
            _self._displayEventsOnMap = (_self._displayEventsOnMap) ? false : true;
            if(_self._displayEventsOnMap) {_self._elEventContainer.hide()} else  {_self._elEventContainer.show()}
        });
        
        document.observe('Promo.EventCalendar:hideEventContainer', function(event) {
            _self._elEventContainer.update('');   
        });
         
    },
    
    displayEvents: function(aIndexes) {
    
        if(aIndexes.length == 0) {
            this._elEventContainer.update('');            
        }
        else {
            // empty container
            this._elEventContainer.update('');
            for(var i = 0; i < aIndexes.length; i++) {
                var event = this._oData.EVENTS[aIndexes[i]];
                this._elEventContainer.insert(event.HTML);
            }
        }
   }
    
});

Promo.PeriodSelection = Class.create({
    /**
     * @constructor
     * @param {Object} obj_, Proterties: see this._oOptions;
     * @return {Void}
     */
    initialize: function(obj_) {
        // set default options
        this._oOptions = {
            'sURI': 'event-sample.json',
            'sIdCalendarOverlay': 'sIdCalendarOverlay',
            'sIdPeriodContainer' : 'event-period',
            'sIdPostcodeSelection' : 'event-postcode-selection'
        };
        this._oOptions.oSort = {};
        // copy passed options 
        Object.extend(this._oOptions, obj_);
        
        // scope reference
        var _self = this;
        // get event data           
        this._oData = EventStorage.getEventData();
        this._oPostCodeMappings = EventStorage.getPostCodeMappings();
        
        // initalize CalendarOverlay
        this._calendar = new Promo.Calendar({
            'sIdCalendar':   this._oOptions.sIdCalendarOverlay,
            'dateRepository' : this._oOptions.dateRepository
        });    
        // Input field that opened the calendar module
        this._sIdInputElement = null;
        // get all relevant DOM elements as global PrototypElement
        this._getPrototypeElements();
        
        // register all module related events
        this._registerEvents();
        
        this._registerCustomEvents();
        
    },
    
    
    _getPrototypeElements: function() {
        
        this._elCalendarOverlay = $(this._oOptions.sIdCalendarOverlay);
        
        // init other elements
        this._elPeriodContainer = $(this._oOptions.sIdPeriodContainer);
        this._elPeriodSelection = this._elPeriodContainer.down('.filter-content');
        
        this._elValidationContainer = this._elPeriodContainer.down('.validation');
        
        this._elPeriodSelectionInputFields = this._elPeriodSelection.getElementsBySelector('input[type="text"]');
        this._elPeriodSelectionSubmitButton = this._elPeriodSelection.getElementsBySelector('input[type="image"]').first();
        this._elPostcodeSelection = $(this._oOptions.sIdPostcodeSelection);
        this._elPostcodeSelectionDropdown = this._elPostcodeSelection.getElementsBySelector('select').first();
        
    },
    
    
    _registerCustomEvents: function() {
        
         var _self = this;
         
        document.observe('Promo.PeriodSelection:resetPeriodSelection', function() {
              
             // hide postcode selection
             _self._elPostcodeSelection.hide();
             
             // reset input fields for event period selection 
            if (_self._elPeriodSelection != null) {
                _self._elPeriodSelection.getElementsBySelector('input[type="text"]').each(function(el){
                    $(el).value = '';
                });
            }
        });
         
         document.observe('Promo.PeriodSelection:toggleModuleVisibility', function(event) {
            _self._elPeriodContainer.toggle();
        });
    },
    
    _registerEvents: function() {
       
        var _self = this;
        
        
        // bind event for all click events on document that are not related to the input fields or overlay calendar
        // hide overlay calendar by click outside of the module
        document.observe('click', function(event) {
            
            var clickedElement = $(Event.element(event));
            if(clickedElement.up('.calendar') == undefined && clickedElement.id != _self._elPeriodSelectionInputFields[0].id && clickedElement.id != _self._elPeriodSelectionInputFields[1].id) {
                
                _self._elCalendarOverlay.hide();
                _self._elPostcodeSelectionDropdown.show();
            }
        });
        
        
        // observe end & start Date selection
        this._elPeriodSelectionInputFields.each(function(el){
            
            el.observe('focus', function(event){
                _self._sIdInputElement = Event.element(event);
            
                var offsetHeight = _self._sIdInputElement.positionedOffset()[1];
                var elementHeight =  _self._sIdInputElement.getHeight();
                
                _self._elCalendarOverlay.setStyle({
                  top: (offsetHeight + elementHeight) + "px"
                });
    
                _self._elPostcodeSelectionDropdown.hide();
    
                _self._elCalendarOverlay.show();
            
            });
            
        });                        
        
        // observe  custom calendar click event for calendar cells
        this._elCalendarOverlay.observe('Promo.Calendar:calendarclick', function(event) {
            _self._sIdInputElement.value = Util.DateHelper.parseMapIndexToDateString(event.memo.cell.readAttribute('rel'), _self._oOptions.dateRepository.dateDelimiter);
            _self._elCalendarOverlay.hide();
            _self._elPostcodeSelectionDropdown.show();
        });    
        
        
        // observe click event on submit button
        this._elPeriodSelectionSubmitButton.observe('click', function(){
            
            // deactivate active Calendar cell
            document.fire("Promo.Calendar:deactivateCalendarCells", { });
            document.fire("Promo.EventCalendar:hideEventContainer", {});
                                                
            // remove all possible postcode options                                    
            _self.removeAllPostcodeOptions();
            
            // validate date format of end and start date
            if (_self._validateDateFormat(_self._elPeriodSelectionInputFields[0]) && _self._validateDateFormat(_self._elPeriodSelectionInputFields[1])) {
            
                // get Mapindex for start & end date
                var startDate = Util.DateHelper.parseDateStringToMapIndex(_self._elPeriodSelectionInputFields[0].value);
                var endDate = Util.DateHelper.parseDateStringToMapIndex(_self._elPeriodSelectionInputFields[1].value);
                
                // validate if endDate is later than startDate
                if (_self._validateDateDifference(startDate, endDate)) {
                
                    // get Indexes of events to be displayed
                    var aIndexes = new Array();
                    for (var i = 0; i < _self._oData.MAP.length; i++) {
                        
                        if (_self._oData.MAP[i] >= startDate && _self._oData.MAP[i] <= endDate) {
                            aIndexes.push(i);
                        }
                    }
                    
                    if (aIndexes.length > 0) {
                        
                        document.fire("Promo.Map:changeHeadlineText", {headline:  dateRepository.renderHeadlineTemplatePeriod(Util.DateHelper.parseMapIndexToDateString(startDate, _self._oOptions.dateRepository.dateDelimiter), Util.DateHelper.parseMapIndexToDateString(endDate, _self._oOptions.dateRepository.dateDelimiter)) } );
                        document.fire("Promo.Map:displayMapEvents", { aIndexes: aIndexes });
                        
                        _self.displayDistinctPostCodes(aIndexes);
                    }
                    else {
                        document.fire("Promo.Map:displayMapEvents", { aIndexes: aIndexes });
                        _self._elPostcodeSelection.hide();
                    }
                }
            }
        });
        
        // observe postcode selection box
        this._elPostcodeSelectionDropdown.observe('change', function() {
            
            var markerElements = $$('.mapMarker');
            
            if (this.value == '') {
                markerElements.each(function(item){
                    item.addClassName('active');
                    item.show();
                });
            }
            else {
                markerElements.each(function(item){
                    item.removeClassName('active');
                    item.hide();
                });
                
                $('marker_'+this.value).addClassName('active').show();
                
            }
        });
        
    },
    
    _validateDateFormat: function(element) {
        
        this._elValidationContainer.hide();
        element.removeClassName('error');
        
        var reDate = /(0[1-9]|[12][0-9]|3[01])[- \/.](0[1-9]|1[012])[- \/.](19|20)[0-9]{2}/;
        if(!element.value.match(reDate))
        {
            element.focus();
            element.addClassName('error');
            this._elValidationContainer.innerHTML = this._oOptions.strValidationErrorInvalidDate;
            this._elValidationContainer.show();
            return false;
        }
    
        return true;
    },
    
    
    _validateDateDifference: function(startDate, endDate) {
        this._elValidationContainer.hide();
        
        if(startDate > endDate)
        {
            this._elValidationContainer.innerHTML = this._oOptions.strValidationErrorStartDate;
            this._elValidationContainer.show();
            return false;
        }
    
        return true;
    },    
    
    displayDistinctPostCodes: function(aIndexes) {
        
        var _self = this;
        
        var distinctPostcodes = new Array();
    
        for(var i = 0; i < aIndexes.length; i++) {
           var event = this._oData.EVENTS[aIndexes[i]];
           distinctPostcodes.push(event.POSTCODE);
        }
        
        ;
        
        distinctPostcodes.uniq().sort().each(function(item) {
            
            if ((postCodeMapping = _self._oPostCodeMappings.get(item)) != undefined) {
                var optionPostcode = new Element('option', {
                    'class': 'filter',
                    'value': item
                }).insert(item + " - " + postCodeMapping.NAME);
                _self._elPostcodeSelectionDropdown.appendChild(optionPostcode);
            }
        });
        
        
        this._elPostcodeSelection.show();
    },
    
    removeAllPostcodeOptions: function() {
        var elementsToBeRemoved = this._elPostcodeSelectionDropdown.select('option.filter');
        
        elementsToBeRemoved.each(function(item) {
          item.remove();
        });
    }
    
});


Promo.TopEvents = Class.create({
    /**
     * @constructor
     * @param {Object} obj_, Proterties: see this._oOptions;
     * @return {Void}
     */
    initialize: function(obj_) {
        // set default options
        this._oOptions = {
            'sIdTopEvents': 'sIdTopEvents',
            'sIdTopEventsContent' : 'sIdTopEventsContent'
        };
        this._oOptions.oSort = {};
        // copy passed options 
        Object.extend(this._oOptions, obj_);
        
        // scope reference
        var _self = this;
        // get event data           
        this._oData = EventStorage.getEventData();
        this._oPostCodeMappings = EventStorage.getPostCodeMappings();
        
        // get all relevant DOM elements as global PrototypElement
        this._getPrototypeElements();
        
        this._initalizeScrolling();
        
        // register all module related events
        this._registerEvents();
        
        // register all module related custom events
        this._registerCustomEvents();
        
    },
    
    
    _getPrototypeElements: function() {
        
        this._elTopEvents = $(this._oOptions.sIdTopEvents);
        this._elTopEventsContent = $(this._oOptions.sIdTopEventsContent);
    },
    
    
    _initalizeScrolling: function() {
           
        var _self = this;
        
        var slider = new Control.Slider(this._elTopEvents.down('.vertical-handle'), this._elTopEvents.down('.vertical-track'), {
                axis: 'vertical',
                onSlide: function(v) { 
                    _self._elTopEventsContent.scrollTop = Math.round(v/slider.maximum*(_self._elTopEventsContent.scrollHeight - _self._elTopEventsContent.offsetHeight));
                }
         });
    },
    
    _registerEvents: function() {
       
        var _self = this;
       
        this._elTopEvents.select('.event').each(function(item) {
                        
            // get Indexes of events to be displayed
            var aIndexes = new Array();
            
            for (var i = 0; i < _self._oData.EVENTS.length; i++) {
                
                if (_self._oData.EVENTS[i].EVENTCODE == item.id) {
                    aIndexes.push(i);
                } 
            } 
            
            
            if (aIndexes.length > 0) {        
                item.childElements().first().observe('click', function() {
                    document.fire("Promo.Calendar:deactivateCalendarCells", { });
                    document.fire("Promo.PeriodSelection:resetPeriodSelection", { });
                    
                    document.fire("Promo.Map:changeHeadlineText", {headline:  'Top Event: '+ this.title } );
                    document.fire("Promo.Map:displayMapEvents", { aIndexes: aIndexes });
                    return false;
                }); 
            }
            
            
        });
        
    },
    _registerCustomEvents: function() {
        
         var _self = this;
        
         document.observe('Promo.TopEvents:toggleModuleVisibility', function(event) {
            _self._elTopEvents.toggle();
        });
        
    }
    
});



Promo.UpcomingEvents = Class.create({
    /**
     * @constructor
     * @param {Object} obj_, Proterties: see this._oOptions;
     * @return {Void}
     */
    initialize: function(obj_) {
        // set default options
        this._oOptions = {
            'sIdContent': 'json-content',
            'sCNTabActive' : 'current'
        };
        this._oOptions.oSort = {};
        
        // copy passed options 
        Object.extend(this._oOptions, obj_);
        
        // scope reference
        var _self = this;
        // get event data           
        this._oData = EventStorage.getEventData();
        
        // add sort events
        this._addEvents();
        
        // create html for the upcoming events
        this._createHTML(+5);
    },
    
    /**
     * Adds sort to #tab_1 && #tab_2
     * sets classNames indication status.
     * @return {Void}
     */
    _addEvents: function() {
        // cache scope
        var _self = this;
        // sort by className, using the class "event_date" by default
        $('tab-1')
            .observe('click', function(event_) {
                return _self.show(event_,+5);
            })
            .addClassName(this._oOptions.sCNTabActive);
            
        // sort by location, using the title attribute by default
        $('tab-2')
            .observe('click',  function(event_) {
                return _self.show(event_,-5);
            })
            .removeClassName(this._oOptions.sCNTabActive);
    },
    
    /**
     * Shows events and toggles active class. Switch between upcoming and past using the offset values (+/-)
     * @param {Event} event_
     * @param {Number} iOffset_
     * @return {Bool}
     */
    show : function (event_,iOffset_) {
        // switch tabs by changing classNames
        var _$elTab = $(event_.target).up();
        _$elTab
            .toggleClassName(this._oOptions.sCNTabActive)
            .siblings()
            .invoke('toggleClassName', this._oOptions.sCNTabActive);
        // create and append HTML 
        this._createHTML(iOffset_);
        return false;
    },
    
    /**
     * Creates and appends HTML. Data-source: stored JSON-data.
     * @see window.initFading
     * @param {Number} iOffset_ - Switch between upcoming and past using the offset values (+/-)
     * @private
     * @return {Void}
     */
    _createHTML: function (iOffset_) {
        // cache
        var _self = this;
        var _iToday =  new Util.Date().getMapIndex();
        var _aData = _self._oData.EVENTS;
        var _aMap = _self._oData.MAP;
        var _iMax = _aMap.length;
        
        // get the index of the current day or the closest entry.
        var _iIndex =  _self._getDayIndex(_iToday,_aMap);
        
        // cache event 
        var _$element = $(_self._oOptions.sIdContent);
        
        // set start / end values for showing upcoming events
        var _iStart  = _iIndex ;
        var _iEnd  = _iIndex + iOffset_;
        
        // clear element
        _$element.update('');
        
        // neither an entry nor a near match. since we're using Math.max, index === -1 results in no future events
        // upcoming: empty
        if (_iIndex === -1 && iOffset_ > 0 ) {return false;}
        // past: show past events: _iIndex === _iMax
        if (_iIndex === -1 && iOffset_ < 0 ) {_iIndex = _iMax;}
        // inverse start / end values for showing past events
        if (iOffset_ < 0 ) { 
            _iStart =  _iIndex + iOffset_;
            _iEnd = _iIndex ;
        }
        
        // get promo-events using today's index and the MAP property to catch the boundary entries
        for (var _i = _iStart; _i < _iEnd; _i++ ) {
            if (_i < 0 || _i >= _iMax ) {continue;}
            
            // check for inconsitencies
            if(_aData[_i].DATE != _aMap[_i]) {continue;}
            
            
            // append element
            new Insertion.Bottom(_self._oOptions.sIdContent,_aData[_i].HTML );
        }
        // call window.initFading 
        window.initFading(_self._oOptions.sIdContent, 'item');
    },
    
    /**
     * Get today's index at MAP-array. Returns the closest date's index if there's no entry for today or 
     * -1 if no upcoming events can be calculated (no entry, today's closest would be the last element).
     * @param {Number} iToday_
     * @param {Array} aMap_
     * @private
     * @return {Number}
     */
    _getDayIndex: function (iToday_, aMap_) {
        // get today's index
        var _i = aMap_.indexOf(iToday_);
        // today ain't present, look for the closest date using Math.max
        if (_i === -1) {
            aMap_.each(function(iItem_,iIndex_) {
                // if the item (date) is bigger than today (future) it's the closest entry.
                if  (Math.max(iToday_ ,iItem_) !== iToday_) { 
                    _i = iIndex_; 
                    // prototype break; emulation
                    throw $break;
                }
            });
        }
        // return index
        return _i;
    }
    
});



Util = {};
Util.DateHelper = {
    
    /**
     * Parse a Date string and return integer value in format 20100601
     * @param dateString
     * @return int
     */
    parseDateStringToMapIndex: function(dateString) {
        sDay = dateString.substr(0,2);
        sMonth = dateString.substr(3,2);
        sYear = dateString.substr(6,4);
        
        return parseInt((sYear + sMonth + sDay));
    },
    
    parseMapIndexToDateString: function(dateString, delimiter) {
        dateString +='';
        sDay = dateString.substr(6,2);
        sMonth = dateString.substr(4,2);
        sYear = dateString.substr(0,4);
        
        return sDay + delimiter + sMonth  + delimiter + sYear;
    },

    getMapIndexForCurrentMonth: function() {
        
        now = new Date();
        
        var month = "" + (now.getMonth() + 1);
        if(month.length == 1) {
            month = "0" + month;
        }
        var day = "01";
        
        var str = "" + now.getFullYear() + month + day ;
        return parseInt(str);
    },
    
        
    getMapIndexForNextMonth: function() {
        
        now = new Date();
        
        if (now.getMonth() == 11) {
            nextMonth = new Date(now.getFullYear() + 1, 0, 1);
        } else {
            nextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1);
        }
        
        var month = "" + (nextMonth.getMonth() + 1);
        if(month.length == 1) {
            month = "0" + month;
        }
        var day = "01";
        var str = "" + nextMonth.getFullYear() + month + day ;
        return parseInt(str);
    }
    
    
};
Util.Date = Class.create({
    /**
     * Constructor
     * @return object Date
     */
    initialize: function(date) {
        this._date = date ? date : new Date;
        this._factDays = 1000 * 60 * 60 * 24;
    },
    /**
     * Get the first day of this month
     * @return object Util.Date
     */
    getFirstDayOfMonth: function() {
        var newDate = this.clone();
        newDate.setDate(1);
        return newDate;
    },
    
    /**
     * Get the first day of the next month
     * @return object Util.Date
     */
    getFirstDayOfNextMonth: function() {
        var date = this.clone();
        do {
            date.addDays(1);
        } while(date.getDayAsInt() > this.getDayAsInt());
        return date;
    },
    
    /**
     * Get first day of previous month
     * @return object Util.Date
     */
    getFirstDayOfPrevMonth: function() {
        return this.getFirstDayOfMonth().subDays(1).getFirstDayOfMonth();
    },
    
    /**
     * Get the last day of this month
     * @return object Util.Date
     */
    getLastDayOfMonth: function() {
        var date = this.getFirstDayOfNextMonth();
        return date.subDays(1);
    },
    
    /**
     * Get the last day of this week
     * @return object Util.Date
     */
    getLastDayOfWeek: function() {
        var index = this.getWeekdayAsInt();
        // copy date
        var newDate = this.clone()
        if(index == 6) {
            return newDate;
        }
        newDate.addDays(6 - index);
        return newDate;
    },
    
    /**
     * Get the first day in the current week
     * @return object Util.Date
     */
    getFirstDayOfWeek: function() {
        var index = this.getWeekdayAsInt();
        // copy date
        var newDate = this.clone()
        if(index == 0) {
            return newDate;
        }
        newDate.subDays(index);
        return newDate;
    },
    
    /**
     * return the weekday (0 for monday, 6 for sunday)
     * @return int
     */
    getWeekdayAsInt: function() {
        return (this._date.getDay() + 6) % 7;
    },
    
    /**
     * Get the current day of month (1-31)
     * @return int
     */
    getDayAsInt: function() {
        return this._date.getDate();
    },
    
    /**
     * Current month as int (0-11)
     * @return int
     */
    getMonthAsInt: function() {
        return this._date.getMonth();
    },
    
    /**
     * e.g. 2009
     * @return int
     */
    getYearAsInt: function() {
        return this._date.getFullYear();
    },
    
    /**
     * Clone a date
     * @return Util.Date
     */
    clone: function() {
        var date = new Date()
        date.setTime(this.getMicrotime());
        return new Util.Date(date);
    },
    
    /**
     * Add some days
     * @param int
     * @return Util.Date
     */
    addDays: function(i) {
        this._date.setTime(this.getMicrotime()+ (i * this._factDays));
        return this;
    },
    
    /**
     * Subtract some days from this date
     * @param int
     * @return Util.Date
     */
    subDays: function(i) {
        this._date.setTime(this.getMicrotime() - i * this._factDays);
        return this;
    },
    
    /**
     * get the _date object
     * @return object Date
     */
    getDateObject: function() {
        return this._date;
    },
    
    /**
     * Set the day of the month (1 to 31)
     * @param int
     * @return Util.Date
     */
    setDate: function(i) {
        this._date.setDate(i);
        return this;
    },
    
    /**
     * Get the microtime of the current Date
     * @return int
     */
    getMicrotime: function() {
        return this._date.getTime();
    },
    
    /**
     * Index of json map e.g. 20093010
     * @return int
     */
    getMapIndex: function() {
        var month = "" + (this.getMonthAsInt() + 1);
        if(month.length == 1) {
            month = "0" + month;
        }
        var day = "" + this.getDayAsInt();
        if(day.length == 1) {
            day = "0" + day;
        }
        var str = "" + this._date.getFullYear() + month + day ;
        return parseInt(str);
    },
    
    /**
     * Helper function for logging the current Date
     */
    log: function() {
        console.log(this.getDayAsInt() + "." + (this.getMonthAsInt() + 1) + "." + this._date.getFullYear());
    }
});
