		var BlockManager = function() {
  var blocks = {};
  
  return {
    register: function(block) {
      blocks[block.id] = block;
    },
    
    get: function(id) {
      return blocks[id];
    }
  };
}(); // Singleton


var Block = Class.create({
  initialize: function(id, name, params) {    
    // Store arguments
    this.id = id;
    this.name = name;
    this.params = $H(params);
    
    // Callback options defaults. Pass it to Prototype's hash convert function, so we can
    // call special methods on it.
    this.callbackOptions = $H({
      onStart:      Prototype.emptyFunction,
      onSuccess:    this.reinitialize.bind(this),
      onComplete:   Prototype.emptyFunction,
      showLoading:  true
    });
    
    // This attribute determines whether we are allowed to perform callbacks
    this.registered = false;
    
    // Allow this block to store attributes on itself.
    this.attributes = new Hash();
    
    // Find the block's content element
    this.el = $(id);
    if (this.el === null) throw "Block element for " + this.id + " not found.";
    
    // Find the block's outer container
    this.container = this.el.hasClassName('block') ? this.el : this.el.up('.block');
    if (this.container === null) throw "Container for block element " + this.id + " not found.";
    
    // Try to initialize the block's sub navigation
    this.initSubnav();
    
    // Assume we want to call the reinitialize function also on initialization of the block
    this.reinitialize();
    
    // Register the block with the BlockManager
    BlockManager.register(this);
    
    this.postInitialize();
  },
  
  // Utility method to do some extra initialization, without overloading the default initializer.
  postInitialize: Prototype.emptyFunction,
  
  // Method that gets called when an AJAX request has been performed. By default does nothing
  reinitialize: Prototype.emptyFunction,
  
  registerForCallbacks: function() {
    CallbackManager.register(this);
    
    this.registered = true;
  },
  
  // Perform a callback
  callback: function(action, params, options) {
    var onStart, onComplete, overlay;
    
    if (!this.registered) throw "Block " + this.id + " is not registered to receive callbacks.";
    
    // Overwrite defaults with any options that might have been passed in
    options = this.callbackOptions.merge(options || {});
    
    if (options.get('showLoading')) {
      onStart = options.get('onStart');
      options.set('onStart', function() {
        overlay = Overlay.drawOver(this.container.select('.content')[0]);

        onStart();
      }.bind(this));

      onComplete = options.get('onComplete');
      options.set('onComplete', function() {
        onComplete();

        overlay.remove();
      });
    }
    
    // Delegate the call to the CallbackManager, adding the block's id attribute to
    // the arguments list.
    CallbackManager.callback(this.id, action, params, options);
  },
  
  addCallbackObserver: function(element, action, params, options, eventtype) {
    if (!this.registered) throw "Block " + this.id + " is not registered to receive callbacks.";
    
    if (eventtype == null) eventtype = 'click';
    
    $(element).observe(eventtype, function() {
      this.callback(action, params, options);
    }.bind(this));
  },
  
  setAttribute: function(key, value) {
    this.attributes.set(key, value);
  },
  
  getAttribute: function(key) {
    return this.attributes.get(key);
  },
  
  updateCallbackParams: function(params) {    
    this.params = $H(params);
  },
  
  setCallbackParam: function(key, value) {
    this.params.set(key, value);
  },
  
  getCallbackParam: function(key) {
    return this.params.get(key);
  },
  
  updateContent: function() {
    this.callback('updateContent', {});
  },
  
  initSubnav: function() {
    var subnavs = this.container.select('div.subnav > ul');
    
    // No point in continuing if there is no sub navigation available.
    if (subnavs.size() === 0) return false;
        
    // Fetch all links in the subnav list. Only continue if any items are found.
    subnavs.each(function(subnav) {
      var items,
          currentItem,
          clickHandler;
      
      items = subnav.select('a');
      if (items.size() === 0) return false;

      // Define the current item as the first item
      currentItem = items[0];
      
      clickHandler = function(current) {
        return function(evt) {
          current.up('li').removeClassName('selected');

          // Change the current item of the sub navigation to the button that was clicked.
          current = evt.element();

          current.up('li').addClassName('selected');
        }.bindAsEventListener()
      }(currentItem);

      // Add some default listeners to the sub navigation. These are mostly cosmetic.
      items.invoke('observe', 'click', clickHandler);
    });
  }
});


var PaginatedBlock = Class.create(Block, {  
  reinitialize: function() {
    this.initPagination();
  },
   
  initPagination: function() {
    if ($(this.id + '_pagination')) {
      // Give the navigation functions their correct binding. We pre-bind these functions because we want
      // to be able to stop and start observing. Binding them upon every observe call would make stopping
      // the observe impossible.
      this.previousPage = this.previousPage.bind(this);
      this.nextPage = this.nextPage.bind(this);
      
      this.pagination = true;
      
      this.previousButton = $(this.id + '_previous');
      this.nextButton = $(this.id + '_next');
      
      this.setPaginationHandlers();
    }
    else {
      this.pagination = false;
    }
  },
  
  reinitPagination: function() {
    if (this.pagination) {
      // Remove observers and class names first. We can then re-apply them conditionally.
      this.previousButton.stopObserving('click', this.previousPage).removeClassName('disabled');
      this.nextButton.stopObserving('click', this.nextPage).removeClassName('disabled');
      
      // Re-add the correct class names if applicable
      if (!this.getAttribute('has_previous_page'))
        this.previousButton.addClassName('disabled');
      if (!this.getAttribute('has_next_page'))
        this.nextButton.addClassName('disabled');

      // Add the handlers again, conditionally.
      this.setPaginationHandlers();
    }
  },
  
  setPaginationHandlers: function() {
    if (!this.previousButton.hasClassName('disabled'))
      this.previousButton.observe('click', this.previousPage);
    
    if (!this.nextButton.hasClassName('disabled'))
      this.nextButton.observe('click', this.nextPage);
  },
  
  previousPage: function() {
    var page = this.getCallbackParam('page') - 1;
    this.changePage(page);
  },
  
  nextPage: function() {
    var page = this.getCallbackParam('page') + 1;
    this.changePage(page);
  },
  
  changePage: function(page) {
    // Perform a callback, and pass in the initTable function as the success handler. It should add event listeners
    // to the newly loaded rows, but not reinitialize the pagination.
    this.callback('changePage', {'page': page}, {onSuccess: this.reinitPagination.bind(this)});
  }
});

var MatchesBlock = Class.create(Block, {
  // Template using Prototype's Template class. This allows easy interpolation of values.
  rowTemplate: new Template('<tr class="#{odd_even} loading event"><td colspan="#{colspan_left}"><div></div></td><td class="loading-icon event-icon"><div><span>Loading</span></div></td><td colspan="#{colspan_right}"><div></div></td></tr>'),
  
  reinitialize: function() {
    this.initTable();
    this.initPagination();
  },
  
  initTable: function() {
    // Bind event listeners to the events buttons
    this.el.observe('click', Event.delegate({
      'a.events-button-button': this.eventClickHandler.bindAsEventListener(this)
    }));
    
    this.reinitPagination();
  },
  
  initPagination: function() {
    if ($(this.id + '_pagination')) {
      // Give the navigation functions their correct binding. We pre-bind these functions because we want
      // to be able to stop and start observing. Binding them upon every observe call would make stopping
      // the observe impossible.
      this.previousPage = this.previousPage.bind(this);
      this.nextPage = this.nextPage.bind(this);
      
      this.pagination = true;
      
      this.previousButton = $(this.id + '_previous');
      this.nextButton = $(this.id + '_next');
      
      this.setPaginationHandlers();
    }
    else {
      this.pagination = false;
    }
  },
  
  reinitPagination: function() {
    if (this.pagination) {
      // Remove observers and class names first. We can then re-apply them conditionally.
      this.previousButton.stopObserving('click', this.previousPage).removeClassName('disabled');
      this.nextButton.stopObserving('click', this.nextPage).removeClassName('disabled');
      
      // Re-add the correct class names if applicable
      if (!this.getAttribute('has_previous_page'))
        this.previousButton.addClassName('disabled');
      if (!this.getAttribute('has_next_page'))
        this.nextButton.addClassName('disabled');

      // Add the handlers again, conditionally.
      this.setPaginationHandlers();
    }
  },
  
  setPaginationHandlers: function() {
    if (!this.previousButton.hasClassName('disabled'))
      this.previousButton.observe('click', this.previousPage);
    
    if (!this.nextButton.hasClassName('disabled'))
      this.nextButton.observe('click', this.nextPage);
  },
  
  previousPage: function() {
    var page = this.getCallbackParam('page') - 1;
    this.changePage(page);
  },
  
  nextPage: function() {
    var page = this.getCallbackParam('page') + 1;
    this.changePage(page);
  },
  
  changePage: function(page) {
    // Perform a callback, and pass in the initTable function as the success handler. It should add event listeners
    // to the newly loaded rows, but not reinitialize the pagination.
    this.callback('changePage', {'page': page}, {onSuccess: this.initTable.bind(this)});
  },
  
  eventClickHandler: function(evt) {    
    var button = evt.element(),
        row = button.up('tr'),
        oddEven,
        matchId;
    
    // Prevent the default event from happening, which is following a link. If this line is removed,
    // the link will be opened and no events will be shown.
    evt.stop();

    if (button.hasClassName('expanded')) {
      // The events list is currently expanded, hide the events and remove the expanded class.
      this.hideEvents(row);
      button.removeClassName('expanded');
    }
    else if (row.hasClassName('loaded')) {
      // The events list is collapsed, but the events have already been fetched and inserted
      // into the DOM, so re-add the "expanded" class and show the events.
      button.addClassName('expanded');
      this.expandMatch(row);
    }
    else {
      // Add the "expanded" class first, so the buttons look gets changed immediately (fast response).
      button.addClassName('expanded');
      
      // Determine whether the match's row is an odd or an even row - we want all events belonging to
      // to this match to have the same background color so they appear grouped.
      oddEven = row.hasClassName('odd') ? 'odd' : 'even';
      
      // Show a loading image, passing along the odd/even class.
      this.expandMatchLoading(row, oddEven);
      
      // Fetch the match id from the row's class
      matchId = parseInt(/-(\d+)$/.exec(row.readAttribute('id'))[1], 10);
      
      // Perform the actual callback
      this.callback('expandMatch', {'match_id': matchId, 'odd_even': oddEven}, {
        onSuccess: Prototype.emptyFunction, // Do nothing on success
        onComplete: function() {
          // Hide the loading image upon completion
          this.hideEventsLoading(row);
        }.bind(this),
        
        showLoading: false
      });
      
      // Add a "loaded" class to the row, so we don't fetch the events twice.
      row.addClassName('loaded');
    }
  },
  
  // Helper method to iterate all the event rows for a certain match and call the same method
  // on each row.
  iterateEventRows: function(matchRow, methodName, param) {
    row = $(matchRow).next();
    while (row && row.hasClassName('event')) {
      row[methodName](param);
      row = row.next();
    }
  },
  
  hideEvents: function(matchRow) {
    this.iterateEventRows(matchRow, 'removeClassName', 'expanded');
  },
  
  expandMatch: function(matchRow) {
    this.iterateEventRows(matchRow, 'addClassName', 'expanded');
  },
  
  expandMatchLoading: function(matchRow, oddEven) {
    var output = this.rowTemplate.evaluate({
      'odd_even': oddEven,
      'colspan_left': this.getAttribute('colspan_left'),
      'colspan_right': this.getAttribute('colspan_right')
    });
    
    matchRow.insert({after: output});
},

  hideEventsLoading: function(matchRow) {
    matchRow.next('tr.loading').remove();
  }
});


var GroupedMatchesBlock = Class.create(MatchesBlock, {
  postInitialize: function($super) {
    $super();
    
    this.currentHeadRowId = null;
    
    this.el.observe('click', Event.delegate({
      'tr.group-head.clickable *': this.groupClickHandler.bindAsEventListener(this)
    }));
  },
  
  initTable: function($super) {
    // Call the parent initializer
    $super();
    
    this.el.select('tr.group-head.clickable')
      .invoke('observe', 'mouseover', Hover.over)
      .invoke('observe', 'mouseout', Hover.out)
      ;
      
    // this.el.observe('mouseover', Event.delegate({
    //   'tr.group-head.clickable *': Hover.over
    // }));
    // 
    // this.el.observe('mouseout', Event.delegate({
    //   'tr.group-head.clickable *': Hover.out
    // }));
  },
  
  groupClickHandler: function(evt) {
    if (!evt.findElement('th.competition-link')) {
      var row = evt.findElement('tr');
      this[row.hasClassName('expanded') ? 'hideGroup' : 'showGroup'](row);
    }
  },
  
  hideGroup: function(headRow) {
    var row; 
    headRow = $(headRow).removeClassName('expanded');
    row = headRow.next();
    while (row) {
      if (row.hasClassName('match') || row.hasClassName('round-head')) {
        row.removeClassName('expanded');
      }
      else if (row.hasClassName('event')) {
        // Hide the row manually. The expanded class on the event row is left intact so we can later restore
        // its original visibility.
        row.hide();
      }
      else {
        break;
      }
      
      row = row.next();
    }
    
    this.currentHeadRowId = null;
  },
  
  showGroup: function(headRow) {    
    var row;
    headRow = $(headRow).addClassName('expanded');
    
    this.currentHeadRowId = headRow.id;
    
    if (!headRow.hasClassName('loaded')) {
      this.loadMatches(headRow);
      return;
    }
    
    row = headRow.next();
    while (row) {
      if (row.hasClassName('match') || row.hasClassName('round-head')) {
        row.addClassName('expanded');
      }
      else if (row.hasClassName('event')) {
        // Hide the row manually. The expanded class on the event row is left intact so we can later restore
        // its original visibility.
        row.show();
      }
      else {
        break;
      }
      
      row = row.next();
    }
  },
  
  loadMatches: function(row) {
    var row = $(row),
        idParts = row.id.split('-'),
        params;
      
    if (idParts.size() === 3) params = {'round_id': parseInt(idParts[2], 10), 'competition_id': parseInt(idParts[1], 10)};
    else params = {'competition_id': parseInt(idParts.last(), 10)};
    
    this.expandMatchLoading(row);
    
    // Perform the actual callback
    this.callback('showMatches', params, {
      onSuccess: Prototype.emptyFunction,
      onComplete: function() {
        // Hide the loading image upon completion
        this.hideEventsLoading(row);
      }.bind(this),
      
      showLoading: false
    });
    
    row.addClassName('loaded');
  },
  
  expandAllGroups: function() {
    this.el.select('tr.group-head').each(this.showCompetition.bind(this));
  },
  
  collapseAllGroups: function() {
    this.el.select('tr.group-head').each(this.hideCompetition.bind(this));
  }
});


var HomeMatchesBlock = Class.create(GroupedMatchesBlock, {
  initialize: function($super, id, name, params) {
    $super(id, name, params);
    
    // Start the update interval
    this.startInterval();
  },
  
  startInterval: function() {
    // Refresh every 60 seconds
    this.interval = new PeriodicalExecuter(this.updateContent.bind(this), 30);
  },
  
  stopInterval: function() {
    if (this.interval !== null) {
      this.interval.stop();
      this.interval = null;
    }
  },
  
  updateContent: function(interval) {
    this.callback('updateContent', {}, {
      onSuccess: function() {
        this.initTable();
        this.expandCompetitions();
      }.bind(this),
      
      showLoading: false
    });
  },
  
  expandCompetitions: function() {
    // Expand the ones that we tracked as expanded
    if (this.currentHeadRowId != null) this.showGroup($(this.currentHeadRowId));
  },
  
  filterContent: function(params) {
    this.stopInterval();
    this.callback('filterContent', params, {
      onSuccess: function() {
        if (this.getCallbackParam('display') !== 'odds') {
          this.startInterval();
        }
      }.bind(this)
    });
  }
});


dropDownmenuHandler = Class.create({
	
	container: false,
	picker: false,
	clickEvent: false,
	clickObserver: false,

	initialize:function(container, picker, clickEvent) {
		this.container = $(container);
		this.picker = $(picker);
		this.clickEvent = clickEvent;
		this.container.observe('click', this.handlePicker.bindAsEventListener(this));
		this.picker.select('a').invoke('observe', 'click', this.handlePicker.bindAsEventListener(this, true));

	},

	handlePicker: function(evt, languageLink) {
	  if (evt) evt.stop(); 	 
	  var link = evt.element().tagName != 'A' ? evt.element().up('a') : evt.element();
	  this.clickEvent(link);

	  if (!this.clickObserver) {
		 this.picker.setStyle({'left': this.container.cumulativeOffset().left});	       
		  this.picker.show();
		  this.clickObserver = document.observe('click', this.closeClickHandler.bindAsEventListener(this));
	  } else {
		  this.closeClickHandler();
	  }
  },  
	  
   closeClickHandler: function(evt) {
    if (!evt || evt.element() != this.picker) {
  		this.picker.hide();
  		document.stopObserving('click', this.closeClickHandler);
   		this.clickObserver = false;
		}
  	}
});

var SportsSelectionBlock = Class.create(Block, {
 
  postInitialize: function($super) {
    $super();
	this.i10Handler =  new dropDownmenuHandler('topbar-current-l10n-container', 'topbar-l10n-picker', this.i10Click);
	this.sportHandler =  new dropDownmenuHandler('topbar-current-sport-container', 'topbar-sport-picker', this.sportClick);
 },
 
 i10Click: function(link) {
	 if (link && link.hasAttribute('data-l10n')) {
		 var lang = link.readAttribute('data-l10n')
		 var pageUrl = window.location.href.split('#')[0];
		 window.location.href = (pageUrl.indexOf('language_id') > -1) ? window.location.href.replace(/language_id=[a-z]{2,3}/, 'language_id='+lang) : pageUrl + '&language_id='+lang;
	 }
  },

  sportClick: function(link) {
	  if (link && link.down('span').hasClassName('sport')) {
		 window.location.href = link.href
	  }
  }       

});


/** 
 * SportsMenu class handles creation of new dynamic sport menu.
 * Loads JSON menu object and builds a nested menu 
 *
 */
 var SportsMenu = Class.create(Block, {
    inited : false,
    activeCompetition: false,
	
	  postInitialize: function($super) {
      this.getActiveCompetition();

      $('sports_menu').insert(
          Builder.node('ul', window['tree'].collect(function(entry) {  
            return this.build_tree(entry); }.bind(this)
          ))
        );
      $('sports_menu').down('ul').observe('click', this.menuClick);
      $$('#sports_menu .expanded').each(function(el) {
           if(el.down('ul')) { // if it has a child, activate that.
            el.down('ul').setStyle({'display': 'block'});
       }
      });
      
      if($('activeCompetition')) {
        el = $('activeCompetition').up('ul').setStyle({'display':  'block'});;
        el.up('li').toggleClassName('active');
      }
    },
  
    /**
     * 
     */        
    getActiveCompetition: function() {
      if(document.location.search && document.location.search.toLowerCase().indexOf('page=competition') > -1 ) {
         var id = document.location.search.match(/id\=([0-9]{1,})/);
         if(id && id.length > 1) {
            this.activeCompetition = id[1];
         }                                 
      }
    },

  	menuClick: function(event) {
      var element = event.element(); // find the element.
      el = (element.tagName == 'LI') ? element.down('a') : element; // did we click the link?
      el.up('li').toggleClassName('active'); // set the active class to the li.
      if(el.next('ul')) { // if it has a child, activate that.
        el.next('ul').setStyle({'display': el.next('ul').getStyle('display') == 'block' ? 'none' : 'block'});
      } 
	  },

    //Recursive function to build the menu
    build_tree: function(obj) {
      var childs = []; 
      var options = {};
      if(null == obj) return;
      var title = obj.t.toString();
      var maxlength = 18; // max length of a title
      var linktitle = title.length > maxlength ? title.substr(0,maxlength) + '...' : title;
      var link = Builder.node('a', {className:'item-link', title:obj.t }, linktitle);
      
      if(obj.comp) { // if there's a comp property that's a competition link.
        link.href = 'index.php?sport='+sport+'&page=competition&view=summary&id='+obj.comp;
      }
      if(obj.tenniscomp) { // if there's a tenniscomp property that's a tenniscompetition link.
        link.href = 'index.php?sport='+sport+'&page=racket&view=summary&id='+obj.tenniscomp;
      }
      if(obj.year && obj.tour) {
        link.href = 'index.php?sport='+sport+'&page=racket&view=tour&id='+obj.tour+'&year='+obj.year;
      }

      if(obj.uri) { link.href = obj.uri; }
      if(obj.id) { link.id = obj.id; }
     
      if(obj.expanded || obj.comp == this.activeCompetition) {
        options.className = 'active expanded';
        if(obj.comp == this.activeCompetition) options.id='activeCompetition';
      }
      if(link.href.split('?')[1] == document.location.href.split('?')[1]) {
         link.className += " active";
      }
      childs.push(link);
     
      if (obj.items) {  // if it has subitems, recurse.
        childs.push(Builder.node('ul', { style: 'display: none'}, obj.items.collect(function(entry) { 
        return this.build_tree(entry); }.bind(this))));
      }
      return Builder.node('li', options, childs);
    }

 });