// HG javascript functions.
// 
// Copyright (c) Betfair 2006, 2007, 2008. All rights reserved.

// requires jQuery 1.2.3
// requires calendar.js

  
  // Log the given message at the given logLevel. Looks for a console
  // object to send the log messages to - if that object does not exist,
  // messages are ignored.
HG.logger = function(message) {
    try {if (window.console) { console.log(message); }} catch(error) {}
  }
  
  // Template modifiers -----------------------------------
  
HG.templateModifiers = {
    // Filter out any text that matches the given filter.
    filterDefault: function(str, filter) {
      return str.toString().replace(filter, "");
    }
  }
  
  // API Calls --------------------------------------------

HG.API = {
    // Make a call to the timeform database API.
    // 
    // The first parameter is the name of the query, e.g. "future_meetings", 
    // "horse" etc.
    // 
    // The second parameter is a json object providing any required parameters, e.g.:
    // {horse_id: 123}.
    // 
    // The third parameter is a function that will be called if the request is succesful, e.g.:
    // function(response) {alert( response.toString() )}.
    call: function(name, parameters, callback) {
      var queryParameters = parameters || {};
      
      queryParameters.key = name;
      queryParameters.response_mode = "json";
      
      $.getJSON(HG.Config.QueryServerUrl, queryParameters, callback);
    },
    
    search: function(type, searchString, pageNumber) {
      var serviceName = "search_" + type + "s",
          errorSpan   = $("span#" + type + "_search_error"),
          queryParameters;
      
      searchString = $.trim(searchString);
      
      errorSpan.hide().empty();
      
      if (searchString.length < 3) {
        errorSpan.append("The search query must be at least 3 characters long").show();
      } else {        
        pageNumber = pageNumber || 0;
        
        queryParameters = {"search_string": searchString,
                           "page_sz"      : HG.Config.SearchSize,
                           "page_no"      : pageNumber};
        
		ss =  escape(searchString);
		
        this.call(serviceName, queryParameters,
          function(json) { HG.Views.searchResults(type, ss, json); }
        );
      }
    },
    
    // Get a list of all future race meetings.
    getFutureMeetings: function() {
      this.call("future_meetings", {}, 
      function(json) {
        HG.Views.meetingSelect(json, "select_meeting_date", "select_future_meeting");
      });
    },
    
    // Get the race list for a particular meeting. For use by the main meeting navigation sidebar.
    getRaceList: function(raceDate, courseNumber, loadRaceCard) {
      HG.Session.raceDate     = raceDate;
      HG.Session.courseNumber = courseNumber;
      
      this.call("race_list_active", {race_date: raceDate, course_number: courseNumber},
      function(json) { HG.Views.raceList(json, loadRaceCard); });
    },
    
    // Get the racecourse corresponding to the given racecourseId.
    getRacecourse: function(courseNumber, seasonType, raceSurface) {
      this.call("racecourse", {course_number: courseNumber, season_type: seasonType},
      function(json) { HG.Views.racecourse(new HG.Racecourse(json, raceSurface)); });
    },
    
    // Get the top jockey statistics for the given courseNumber and raceDate.
    getTopJockeys: function() {
      if (HG.Session.courseNumber && HG.Session.raceDate) {
        this.call("top_jockeys", {course_number: HG.Session.courseNumber, race_date: HG.Session.raceDate},
        function(json) { HG.Views.topJockeys(json); } );
      }
    },
    
    // Get the top trainer statistics for the given courseNumber and raceDate.
    getTopTrainers: function() {
      if (HG.Session.courseNumber && HG.Session.raceDate) {
        this.call("top_trainers", {course_number: HG.Session.courseNumber, race_date: HG.Session.raceDate},
        function(json) { HG.Views.topTrainers(json); });
      }
    },
    
    // Get the racecard corresponding to the given parameters.
    getRacecard: function(raceDate, courseNumber, raceNumber, shouldDisplay) {
      if (shouldDisplay === undefined) { shouldDisplay = true; }
      
      this.call("racecard_all", {race_number: raceNumber, course_number: courseNumber, race_date: raceDate},
      function(json) {
        var head = json.head;
        
        HG.Views.racecard(json, shouldDisplay);
        
		// Fetch last run perspective comment data for the race:
		HG.API.getLastRun(head.race_date, head.course_number, head.race_number, head.racing_type_code, shouldDisplay);
		
		if (shouldDisplay) {
		  HG.History.addItem("racecard", [raceDate, courseNumber, raceNumber]);
		}
      });
    },
    
    // Get the horse record for horseCode.
    getHorse: function(horseCode) {
      this.call("horse_performance_all", {horse_code: horseCode, page_sz: HG.Performances.PageSize},
      function(json) {
        HG.Views.horseDetail(json);
        HG.Session.horseCode = json.head.horse_code;
        
        HG.History.addItem("horse", horseCode);
      });
    },
    
    // Get the jockey record for jockeyCode.
    getJockey: function(jockeyCode) {
      this.call("jockey_performance_all", {jockey_code: jockeyCode, page_sz: HG.Performances.PageSize},
      function(json) {
        HG.Views.jockeyDetail(json);
        HG.Session.jockeyCode = json.head.jockey_code;
        
        HG.History.addItem("jockey", jockeyCode);
      });
    },
    
    // Get the trainer record for trainerCode.
    getTrainer: function(trainerCode) {
      this.call("trainer_performance_all", {trainer_code: trainerCode, page_sz: HG.Performances.PageSize},
      function(json) {
        HG.Views.trainerDetail(json);
        HG.Session.trainerCode = json.head.trainer_code;
        
        HG.History.addItem("trainer", trainerCode);
      });
    },
    
    // Get performance records of the given type for the give code, applying the filters
    // specified in parameters.
    getPerformances: function(type, code, parameters) {
      var name = type + "_performance_all",
          queryParameters;
      
      queryParameters                 = parameters || {};
      queryParameters[type + "_code"] = code;
      queryParameters.page_no         = parameters.page_no || 0;
      queryParameters.page_sz         = HG.Performances.PageSize;
      
      this.call(name, parameters, function(json) { HG.Views.performance(json, type, code); });
    },
    
    // Get performance records for the given pageNumber.
    getPerformancePage: function(type, code, pageNumber) {
      var name             = type + "_performance_page",
          codeKey          = type + "_code",
          filterParameters = HG.Performances.createFilterParameters(
                               $("div#" + type + "_content table.performance_filters")
                             ),
          queryParameters  = filterParameters || {};
      
      queryParameters[codeKey] = code;
      queryParameters.page_no  = pageNumber;
      queryParameters.page_sz  = HG.Performances.PageSize;
      
      this.call(name, queryParameters, function(json) { HG.Views.performancePage(type, code, json); });
    },
    
    // Get a list of meetings on the given date. For use by the results archive screen.
    getMeetings: function(date) {
      this.call("meetings_on_date", {race_date: date},
      function(json) {
        HG.Views.Helpers.populateMeetingSelect(
          json, "results_select_meeting", 
          HG.Views.Helpers.updateRaceTimeSelect);
      });
    },
    
    // Get result records matching the parameters.
    getResult: function(raceDate, courseNumber, raceNumber) {
      this.call("result", {race_date: raceDate, course_number: courseNumber, race_number: raceNumber},
      function(json) {
        HG.Views.result(json);
        HG.History.addItem("result", [raceDate, courseNumber, raceNumber]);
      });
    },
    
    // Fetch a race result record, but also retrieve the meetings and race list data required
    // to populate the select lists on the results screen.
    getResultAll: function(raceDate, courseNumber, raceNumber) {
      // Create a Date object from the raceDate string:
      var raceDateObject = HG.Utils.createDateObject(raceDate);
      
      this.call("meetings_on_date", {race_date: raceDate},
      function(json) {
        var selectedMeeting = $.grep(json, function(item) {
          return (item.course_number == courseNumber);
        })[0];
        
        HG.Views.Helpers.populateMeetingSelect(json, "results_select_meeting", HG.Views.Helpers.updateRaceTimeSelect, true, selectedMeeting);
      });
      
      this.call("race_list", {race_date: raceDate, course_number: courseNumber},
      function(json) {
        var selectedRace = $.grep(json, function(item) { return (item.race_number == raceNumber); })[0];
        
        HG.Views.Helpers.populateRaceSelect(json, HG.Views.Helpers.updateRaceResult, 
                                            true, selectedRace);
      });
      
      HG.API.getResult(raceDate, courseNumber, raceNumber);
      
      // Update the calendar and date text to reflect the race date, without
      // running the calendar date selected callback:
      HG.ResultsArchive.calendar.selectDate(raceDateObject, true, true);
      HG.ResultsArchive.insertSelectedDate(raceDateObject);
      
      HG.Session.resultsPresent = true;
    },
    
    // Get the PDF form guide for the meeting matching the parameters.
    getPDFForm: function(courseNumber, raceDate) {
      var pdfUrl = HG.Config.QueryServerUrl + "?key=get_pdf_form&course_number=" + 
                   courseNumber + "&race_date=" + raceDate;
      
      // Register PDF downloads with google analytics:
      HG.History.googleAnalytics('/pdf/formguide_' + raceDate + '_' + courseNumber + '.pdf');
      
      open(pdfUrl);
    },
    
    // Get last run perspective comment information.
    getLastRun: function(raceDate, courseNumber, raceNumber, racingType, shouldDisplay) {
      this.call("last_run", {
        race_date: raceDate, course_number: courseNumber,
        race_number: raceNumber, racing_type: racingType
      }, function(json) {
        if (json.length) { HG.Views.lastRun(json, shouldDisplay); }
      });
    }
  }
  
  // ------------------------------------------------------
  
  
  // Views ------------------------------------------------
  
HG.Views = {
    // Speed parameter for all fades:
    FadeSpeed: 200,
    
    // Populate the meeting navigation select lists.
    meetingSelect: function(json, daySelectId, meetingSelectId) {
      this.Helpers.populateMeetingDaySelect(json, daySelectId, meetingSelectId);
    },
    
    // Render the race list template.
	raceList: function(json, loadRaceCard) {
	  if (json.length) {
		HG.Templates.process("pdf_link", json, "raceList", function(template) {
		  $("div#pdf_link_container").html(template).show();
		});
	  } else {
		$("div#pdf_link_container").hide();
	  }
	  
	  HG.Templates.process("race_list", json, "raceList", function(template) {
		$("div#race_listing").html(template);
	  });
	  
	  if (loadRaceCard && json.length) {
		  n = HG.Utils.search_race(json, g_race_number, g_course_number, g_race_date) || 0;
		HG.API.getRacecard(json[n].race_date, json[n].course_number, json[n].race_number, false);
	  }
	},
    
    // Populate the course details template using the given Racecourse object.
    racecourse: function(racecourse) {
      HG.Templates.process("course_details", racecourse, "course", function(template) {
        // Select the "Course information" option of the select list initially,
        // and display the course information template:
        HG.CourseData.switchTo("course_details", template);
      });
    },
    
    // Populate the top jockeys template.
    topJockeys: function(topJockeys) {
      HG.Templates.process("top_jockeys", topJockeys, "jockeys", function(template) {
        HG.CourseData.switchTo("top_jockeys", template);
      });
    },
    
    // Populate the top trainers template.
    topTrainers: function(topTrainers) {
      HG.Templates.process("top_trainers", topTrainers, "trainers", function(template) {
        HG.CourseData.switchTo("top_trainers", template);
      });
    },
    
    // Populate the racecard template using the given Racecard object.
    racecard: function(json, switchTab) {
		json['HG'] = HG;

      HG.Tabs.renderFromTemplate("racecard", json, "card", switchTab);
      if ($.cookie('HG_VERDICT') == 'N') {
	  	HG.Views.hideVerdict();
	  }
	  // Populate the last run cache with new data from the racecard:
	  HG.LastRunPopup.updateCache(json);
	  
	  if (json.head.show_prices) {
		// Search for a matching betfair market:
		HG.BetfairSearch.find(json.head.race_date,
							  json.head.time_of_race,
							  json.head.race_number,
							  json.head.course_number,
							  json.head.course_description);
	  }
    },
    
    // Populate the horse template.
    horseDetail: function(horse) {
      HG.Templates.process("horse_detail", horse, "horse", function(template) {
        $("#horse_search_result").empty();
        $("#horse_details").html(template);
        HG.Performances.assignEventHandlers("horse");
        HG.Tabs.displayTab("horse");
      });
    },
    
    // Populate the jockey template.
    jockeyDetail: function(jockey) {
      HG.Templates.process("jockey_detail", jockey, "jockey", function(template) {
        $("div#jockey_search_result").empty();
        $("div#jockey_details").html(template);
        HG.Performances.assignEventHandlers("jockey");
        HG.Tabs.displayTab("jockey");
      });
    },
    
    // Populate the trainer template.
    trainerDetail: function(trainer) {
      HG.Templates.process("trainer_detail", trainer, "trainer", function(template) {
        $("div#trainer_search_result").empty();
        $("div#trainer_details").html(template);
        HG.Performances.assignEventHandlers("trainer");
        HG.Tabs.displayTab("trainer");
      });
    },
    
    // Display a set of search results.
    searchResults: function(type, searchString, data) {
      var detailsDiv      = $("div#" + type + "_details"),
          searchResultDiv = $("div#" + type + "_search_result"),
          results;
      
      detailsDiv.empty(); searchResultDiv.empty();
      results = HG.Templates.macros.searchResults(type, searchString, data);
      searchResultDiv.hide().append(results).fadeIn(HG.Views.FadeSpeed);
    },
    
    // Update the performance data of the given type using the given data.
    performance: function(data, type, code) {
		
		data.detail.data['HG'] = HG;

      var performanceDiv = $("div#" + type + "_content div.performances_container"),
          summaryDiv     = $("div#" + type + "_content div.summary_container"),
          summary        = HG.Templates.macros.resultsSummary(data.sum),
          performances   = HG.Templates.macros.performances(data.detail.data, HG.Performances[type]),
          paginator      = HG.Templates.macros.paginator(type, code, data.detail.page_no, data.detail.num_pages);
      
      summaryDiv.html(summary);
      performanceDiv.html(performances).append(paginator);
    },
    
    // Update performance data taken from one of the "performance_page" calls - e.g. "horse_performance_page". 
    performancePage: function(type, code, performanceData) {
		performanceData.data['HG'] = HG;

      var performanceDiv = $("div#" + type + "_content div.performances_container"),
          performances   = HG.Templates.macros.performances(performanceData.data, HG.Performances[type]),
          paginator      = HG.Templates.macros.paginator(type, code, performanceData.page_no, performanceData.num_pages);
      
      performanceDiv.hide().html(performances).append(paginator).fadeIn(HG.Views.FadeSpeed);
    },
    
    // Render the result template from the given list of runners.
    result: function(data) {
      HG.Templates.process("race_result", data, "result", function(template) {
        // Ensure the results tab is visible:
        HG.Tabs.displayTab("results");
        $("div#" + HG.ResultsArchive.ResultsContainerId).html(template).show();
      });
    },
    
    // Render last run perspective comment data.
    lastRun: function(data, shouldDisplay) {
      if (shouldDisplay === undefined) { shouldDisplay = true; }
      
      HG.Templates.process("last_run", data, "last_run", function(template) {
        if (shouldDisplay) {
          HG.CourseData.switchTo("last_run", template);
        } else {
          HG.CourseData.updateData("last_run", template);
        }
      });
    },
    
    // Hide and show breeding information
    toggleViewBreeding: function(checkbox) {
      if (checkbox.checked) {
        $('div.breeding').show();
      } else {
        $('div.breeding').hide();
      }
    },
    
    // Hide and show trainer information
    toggleViewTrainer: function(checkbox) {
      if (checkbox.checked) {
        $('div.trainer').show();
      } else {
        $('div.trainer').hide();
      }
    },
    showVerdict: function() {
        $('.verdict-off').hide();
        $('.verdict-on').show();
		$.cookie('HG_VERDICT', 'Y')
    },
    hideVerdict: function() {
        $('.verdict-on').hide();
        $('.verdict-off').show();
		$.cookie('HG_VERDICT', 'N')
    },

    // Get and display quick results data.
    getQuickResults: function() {
      $.ajax({
        type: "GET",
        url:  HG.Config.QueryServerUrl,
        data: { key: "quick_results" },
        success: function (html) {
          $('div#quick_results .content').html(html);
          $('div#quick_results_small .content').html(html);
        }
      });
    },
    
    // Get and display on course information.
    getOnCourseInfo: function() {
      $.ajax({
        type: "GET",
        url:  HG.Config.QueryServerUrl,
        data: {key: "on_course_info"},
        success: function (html) {
          $('div#on_course_info .content').html(html);
        }
      });
    },
    
    // Get and display quick results and on course information.
    getHomePageInfo: function () {
      HG.Views.getQuickResults();
      HG.Views.getOnCourseInfo();
    },
    
    Helpers: {
      populateMeetingDaySelect: function(json, daySelectId, meetingSelectId) {
        var meetings   = json.meetings,
            days       = json.days,
            select     = $("select#" + daySelectId),
            optionList = document.createDocumentFragment(),
            optionElement;

		var selectedDay  = g_selected_day  || days[0];
		var selectedMeetingsForDay = meetings[selectedDay];
		var nMeeting = HG.Utils.search_meeting(selectedMeetingsForDay, 
											   g_course_number) || 0;
		var selectedMeeting = selectedMeetingsForDay[nMeeting];
	
        for (var i=0; i < days.length; i++) {
          optionElement = document.createElement("option");
          optionElement.appendChild(document.createTextNode(days[i]));
          optionElement.value = days[i];
		  if (optionElement.value == selectedDay)
			  optionElement.selected = true;
          optionList.appendChild(optionElement);
        }
        
        select.empty().append(optionList);
        
        select.change(function() {
          HG.Views.Helpers.populateMeetingSelect(meetings[this.value], meetingSelectId, HG.Views.Helpers.updateRacecourseInformation);
        });
        
		HG.Views.Helpers.populateMeetingSelect(selectedMeetingsForDay, meetingSelectId, HG.Views.Helpers.updateRacecourseInformation, true, selectedMeeting, true);
      },
      
      // Populate a meetings select list with the given id, using the 
      // given array. The onChangeFunction will be assigned to the 
      // onChange event for the select list.
      // 
      // triggerOnChange - defaults to true - prevents the onChange 
      // callback being called immediately if false.
      // 
      // selectedMeeting - If given, this should be an item from the meetings
      // array. The option element corresponding to that item will be the 
      // selected option in the dropdown.
      populateMeetingSelect: function(meetings, meetingSelectId, onChangeFunction, preventTrigger, selectedMeeting, loadRaceCard) {
        var meetingSelect = $("select#" + meetingSelectId),
            optionList    = document.createDocumentFragment(),
            optionElement,
            meeting;

		// default values
		selectedMeeting = selectedMeeting || meetings[0];
        
        for (var i=0; i < meetings.length; i++) {
          meeting = meetings[i];
          optionElement = document.createElement("option");
          optionElement.appendChild(document.createTextNode(meeting.course_description.truncate(20)));
          
          optionElement.value = [meeting.race_date, meeting.course_number,
                                 meeting.season_type, 
                                 meeting.race_surface].join("~");
          
          if (meeting == selectedMeeting) { optionElement.selected = true; }
          
          optionList.appendChild(optionElement);
        }
        
        meetingSelect.change(onChangeFunction);
        meetingSelect.empty().append(optionList);
        
        // Fire the onchange event handler for the select list:
        if (!preventTrigger) { meetingSelect.change(); }
        
        if (meetingSelectId == "select_future_meeting") {
          HG.API.getRaceList(selectedMeeting.race_date,
                             selectedMeeting.course_number,
                             loadRaceCard);
          
          HG.API.getRacecourse(selectedMeeting.course_number,
                               selectedMeeting.season_type,
                               selectedMeeting.race_surface);
        }
      },
      
      // Populate the race select list with the given id with the 
      // provided list of races, and assign the given onChangeFunction 
      // to the onchange event.
      // 
      // triggerOnChange - defaults to true - prevents the onChange 
      // callback being called immediately if false.
      populateRaceSelect: function(races, onChangeFunction, preventTrigger, selectedRace) {
        var select = $("select#" + HG.ResultsArchive.RaceTimeSelectId),
            optionList = document.createDocumentFragment();
        
        $(races).each(function(i, race) {
          var optionElement   = document.createElement("option"),
              raceDescription = "";
          
          if (race.time_of_race !== "0:00") {
            raceDescription += race.time_of_race + " ";
          }
          
          raceDescription += race.race_name.truncate(45);
          
          optionElement.appendChild(document.createTextNode(raceDescription));
          optionElement.value = [race.race_date, race.course_number, race.race_number].join("~");
          
          if (race === selectedRace) { optionElement.selected = true; }
          
          optionList.appendChild(optionElement);
        });
        
        // Add an event handler for this select list.
        select.change(onChangeFunction);
        
        select.empty().append(optionList);
        
        // Fire the onchange event handler for the select list:
        if (!preventTrigger) { select.change(); }
      },
      
      // Use as an event handler to update the racecourse info when a new 
      // meeting is selected from the main navigation.
      updateRacecourseInformation: function() {
        var parameters = this.value.split("~");
        
        HG.API.getRaceList(parameters[0], parameters[1]);
        HG.API.getRacecourse(parameters[1], parameters[2], parameters[3]);
      },
      
      // Use as an event handler to update the list of race times in the 
      // results screen when a new meeting is selected.
      updateRaceTimeSelect: function() {
        var parameters = this.value.split("~");
        
        HG.API.call("race_list", 
                    {race_date: parameters[0], course_number: parameters[1]},
          function(json) { 
            HG.Views.Helpers.populateRaceSelect(json, HG.Views.Helpers.updateRaceResult); 
          }
        );
      },
      
      // Use as an event handler to fetch the race result and and re-render
      // the result template, when a new race is selected.
      updateRaceResult: function() {
        var parameters = this.value.split("~");
        HG.API.getResult(parameters[0], parameters[1], parameters[2]);
      },
      
      // Returns an <img> element pointing at the correct filename for the
      // course map matching the given parameters.
      courseMapUrl: function(courseName, racingType, raceSurface) {
        var filename = "", course;
        
        // The first 5 charectors correspond to the course name, padded out
        // with "$" charectors if necessary:
        course = courseName.replace(" ", "");
        
        for (var i in [0,1,2,3,4]) { filename += (course.substring(i, (+i+1)) || "$"); }
        
        // 6th charector is the racing type code, 7th is the race surface code:
        filename += racingType;
        filename += raceSurface;
        
        return (HG.Config.CourseMapsPath + filename.toLowerCase() + "." + HG.Config.CourseMapsFormat);
      },
      
      // Assign event handlers for all searchContainerDivs given.
      assignSearchBoxEvents: function(searchContainerDivs) {
        var searchSubmit = function(input) {
          var container = $(input).parent().parent(),
              type = container.attr("id").split("_")[0];
          
          HG.API.search(type, container.find("input:text").val() );
        };
        
        searchContainerDivs.each(function() {
          $(this).find("input:button").click(function(event) {
            event.stopPropagation(); searchSubmit(this);
          });
          
          $(this).find("input:text").keypress(function(event) {
            event.stopPropagation();
            if (event.keyCode === 13) { 
              searchSubmit(this); 
            } // keyCode 13 is enter.
          });
        });                
      }
    }
  }
  
  // ------------------------------------------------------
  
  // Last Run Popup View ----------------------------------
  
HG.LastRunPopup = {
    cache: {},
    
    // Render the last run popup view. The position of the popup view
    // will be determined relative to the position of the given element.
    display: function(element, horseCode) {
      var popup = $("div#last_run_popup_" + horseCode),
          offset;
      
      if (popup.is(":visible")) {
        popup.hide();
      } else {
        $("div.last_run_popup").hide();
        
        offset = $(element).offset();
        popup.css({position: "absolute", top: (offset.top + 15), left: offset.left});
        
        if (popup.children().length) {
          popup.show();
        } else {
          var data = this.cache[horseCode];
          
          HG.Templates.process("last_run_popup", data, "form", function(template) {
            popup.html(template).show();
          });
        }
      }
    },
    
    close: function(horseCode) {
      var popup = $("div#last_run_popup_" + horseCode);
      popup.hide();
    },
    
    // Cache last run data from the given racecard JSON object.
    updateCache: function(racecardData) {
      var that = this;
      
      // Empty the cache before populating with new data:
      this.cache = {};
      
      $(racecardData.entries).each(function() {
        that.cache[this.horse_code] = {
          "last_run": this.last_run,
          "last_six": this.last_six
        };
      });
    }
  }
  
  // ------------------------------------------------------
  
  
  // Tabs -------------------------------------------------
  
HG.Tabs = {
    // Render and select a new tab.
    // 
    // tabName is the name of the tab (and an associated template) - e.g. "horse".
    // 
    // templateContext is the variable that will be passed to the template for rendering.
    renderFromTemplate: function(tabName, templateContext, contextAlias, displayTab) {
      HG.Templates.process(tabName, templateContext, contextAlias, function(template) {
        if (displayTab === undefined) { displayTab = true; }
        
        HG.Tabs.addContent(tabName, template);
        
        if (displayTab) { HG.Tabs.displayTab(tabName); }
      });
    },
    
    addContent: function(tabName, content) {
      var tabDiv = $("div#tab_content div#" + tabName + "_content");
//      tabDiv.hide().html(content);
	  // Note: to avoid tab to be emptied when we display directly
	  tabDiv.html(content);
    },
    
    // Switch to tabName and display the given content.
    displayTab: function(tabName) {
      this.switchTab($("ul.tabs li#" + tabName + "_tab a"));
    },
    
    // Use for creating groups of tabs.
    // Assumes the tabs are marked up like this:
    // 
    //   <ul id="tab_group"><li><a>Trainers</a></li></ul>
    // 
    // To create a new tab group:
    // 
    //   HG.Tabs.group("tab_group")
    // 
    // An onclick event handler will be added to each anchor element within 
    // the list,  and the current tab will have the class "selected" applied
    // to it.
    // 
    // The corresponding content_container for the tab will be displayed, 
    // and all others in the same tab group will be hidden.
    group: function(ulElementId) {
      var ulElement = $("ul#" + ulElementId),
          anchors   = ulElement.children("li").children("a");
      
      anchors.click(function() {
        HG.Tabs.switchTab($(this));

	// Webtrends click reporting
	dcsCleanup();
	dcsMultiTrack('DCSext.HTMLClick', this.id);
	
        return false;
      });
    },
    
    // Switch to the tab corresponding to the given anchor element.
    switchTab: function(anchor) {
      var listItem = anchor.parent("li"),
          tabName, tabContentDiv;
      
      tabName       = listItem.attr("id").split("_").slice(0, -1).join("_");
      tabContentDiv = $("div#tab_content div#" + tabName + "_content");
      
      if ( !tabContentDiv.is(":visible") ) {
        if (listItem.not(".selected")) {
          listItem.siblings().removeClass("selected");
          listItem.addClass("selected");
        }
        
        listItem.parent("ul").parent().find("div.content_container").hide();
        tabContentDiv.fadeIn(HG.Views.FadeSpeed);
      }
    }
  }
  
  // ------------------------------------------------------
  
  
  // Course data ------------------------------------------
  
HG.CourseData = {
    ContainerId: "course_data_container",
    SelectId:    "course_data_select",
    
    // Assign an onChange event handler to the selectList that updates
    // the container depending on the selected option.
    assignEventHandlers: function(selectList, container) {
      selectList.change(function() {
        switch (this.value) {
          case "top_jockeys":
            HG.API.getTopJockeys();
          break;
          
          case "top_trainers":
            HG.API.getTopTrainers();
          break;
          
          default:
            // By default, just switch to the appropriate div:
            HG.CourseData.switchTo(this.value);
          break;
        }
      });
    },
    
    switchTo: function(name, data) {
      var container = $("div#" + HG.CourseData.ContainerId);
      
      container.find("div.content").hide();
      this.updateData(name, data).show();
      this.selectOption(name);
    },
    
    updateData: function(name, data) {
      var contentDiv = $("#" + name + "_content");
      
      if (data) { contentDiv.html(data); }
      
      return contentDiv;
    },
    
    // Select the option with the given value.
    selectOption: function(name) {
      var select = $("select#" + HG.CourseData.SelectId);
      select.find("options").attr("selected", false);
      select.find("option[@value='" + name + "']").attr("selected", true);
    }
  }
  
  // ------------------------------------------------------
  
  
  // ------------------------------------------------------
  
HG.Performances = {
    PageSize: 15, // Default page size value.
    
    horse: {
		"Date" : {
			type: "result_link",
			field: "race_date_fmt",
			params: ["race_date_fmt", "course_number", "race_number"]
		},
      "Course" : "course_description",
      "Dist." : {
	  		type: "distance",
			furlongs: "distance_furlongs",
			yards: "distance_yards"
		},
      "Going" : "going",
	  "Eqp"  : "horse_equipment_code",
	  "Race type": "racing_type",
	  "Pos." : "position",
      "Jockey" : {
		  	type: "link",
	  		field: "jockey_name",
			method: "HG.API.getJockey",
			param: "jockey_code"},
      "OR"  : "official_rating",
	  "In-Play Price" : {
			type: "multi_col",
			size: 2,
			cols: {
			 	"High"  : "inplay_max",
				"Low"   : "inplay_min"
			}
	  },
	  "SPs and Advantage" : {
			type : "multi_col",
			size : 4,
	  		cols : {
				"BSP"   : "bsp",
				"ISP"   : "price",
	  			"+/-"   : "sp_diff_pct",
	  			"Place" : "bsp_plc"
			}
	   }
	},
    
    jockey: {
		"Date": {
			type: "result_link",
			field: "race_date_fmt",
			params: ["race_date_fmt", "course_number", "race_number"]
	  },
      "Course": "course_description",
      "Dist.": {
	  		type: "distance",
			furlongs: "distance_furlongs",
			yards: "distance_yards"
	  },
      "Going": "going",
	  "Eqp"  : "horse_equipment_code",
	  "Race type": "racing_type",
	  "Pos.": "position",
      "Horse<br>Trainer": {
	  		type: "multi_row",
			rows : [{
	  			type: "link",
				field: "horse_name",
				method: "HG.API.getHorse",
				param: "horse_code"
	  		}, {
	  			type: "link",
				field: "trainer_name",
				method: "HG.API.getTrainer",
				param: "trainer_code"
	  		}]
	  },
      "OR": "official_rating",
	  "In-Play Price" : {
			type: "multi_col",
			size: 2,
			cols: {
			 	"High"  : "inplay_max",
				"Low"   : "inplay_min"
			}
	  },
	  "SPs and Advantage" : {
			type : "multi_col",
			size : 4,
	  		cols : {
				"BSP"   : "bsp",
				"ISP"   : "price",
	  			"+/-"   : "sp_diff_pct",
	  			"Place" : "bsp_plc"
			}
	   }
	},
    
    trainer: {
		"Date": {
			type: "result_link",
			field: "race_date_fmt",
			params: ["race_date_fmt", "course_number", "race_number"]},
      "Course": "course_description",
      "Dist.": {
	  		type: "distance",
			furlongs: "distance_furlongs",
			yards: "distance_yards"},
      "Going": "going",
	  "Eqp"  : "horse_equipment_code",
	  "Race type": "racing_type",
	  "Pos.": "position",
      "Horse <br>Jockey": {
	  		type: "multi_row",
			rows: [{
	  			type: "link",
				field: "horse_name",
				method: "HG.API.getHorse",
				param: "horse_code"
			  }, {
	  			type: "link",
				field: "jockey_name",
				method: "HG.API.getJockey",
				param: "jockey_code"
	  		}]
	  },
      "OR": "official_rating",
	  "In-Play Price" : {
			type: "multi_col",
			size: 2,
			cols: {
			 	"High"  : "inplay_max",
				"Low"   : "inplay_min"
			}
	  },
	  "SPs and Advantage" : {
			type : "multi_col",
			size : 4,
	  		cols : {
				"BSP"   : "bsp",
				"ISP"   : "price",
	  			"+/-"   : "sp_diff_pct",
	  			"Place" : "bsp_plc"
			}
	     }
	  },
    
    // Assign event handlers for the filter <select> elements for the 
    // given type - e.g.: "horse" / "jockey" / "trainer".
    assignEventHandlers: function(type) {
      var selects = $("div#" + type + "_content table.performance_filters select"),
          performanceType, parentRow, parameters;
      
      $(selects).change(function() {
        performanceType = $("ul#main_tabs li.selected").attr("id").split("_")[0];
        parentRow       = $(this).parent().parent();
        parameters      = HG.Performances.createFilterParameters(parentRow);
        
        HG.API.getPerformances(performanceType, HG.Session[performanceType + "Code"], parameters);
      });
    },
    
    // Returns an object containing filter parameters created from all
    // <select> elements within the given filterContainer.
    createFilterParameters: function(filterContainer) {
      var selectElements = filterContainer.find("select"),
          parameters = {},
          values;
      
      for (var i in selectElements) {
        if (selectElements[i].value) {
          values = selectElements[i].value.split("~");
          parameters[ values[0] ] = values[1];
        }
      }
      
      return parameters;
    }
  }
  
  // ------------------------------------------------------
  
  
  // Results Archive --------------------------------------
  
HG.ResultsArchive = {
    SelectedDateId:     "results_selected_date",
    MeetingSelectId:    "results_select_meeting",
    RaceTimeSelectId:   "results_select_race_time",
    ResultsContainerId: "results_container",
    
    // Binds the necessary event handlers to create the results widget in the given div.
    build: function() {
      this.calendar = new SimpleCalendar("results_calendar", 
                      {startDate: new Date(Date.UTC(1980, 0, 1)), 
                       endDate: new Date(), popup: false});
      
      this.assignTodayEventHandler( $("a#reset_results_calendar") );
      
      // Assign a function that will execute when a new date is selected:
      this.calendar.onDateSelected(function(calendar) {
        HG.ResultsArchive.insertSelectedDate( calendar.selectedDate );
        HG.API.getMeetings( HG.Utils.createDateString( calendar.selectedDate ) );
      });
      
      // Fetch yesterday's results the first time that the results archive 
      // tab is clicked:
      $("li#results_tab a").one("click", function() {
        // Only load up today's results if no other results are present:
        if (!HG.Session.resultsPresent) { 
          HG.ResultsArchive.calendar.selectToday(); 
        }
      });
    },
    
    insertSelectedDate: function(dateObject) {
      var dateString = "Results for ";
      dateString    += this.calendar.formatDate(dateObject);
      
      $("#" + this.SelectedDateId).empty().append( dateString );
    },
    
    // Assign an event handler to the given element that will select today's 
    // date when the onclick event fires.
    assignTodayEventHandler: function(element) {
      $(element).click(function() { HG.ResultsArchive.calendar.selectToday(); });
    }
  }
  
  // ------------------------------------------------------
  
  
  // Betfair market search --------------------------------
  
HG.BetfairSearch = {
    // The interval code returned by setInterval();
    IntervalCode : null,
    
    BFMktId      : null,
    BFExchId     : null,
    BFHorseIds   : null,
    
    // Place to keep the rows of the table
    RaceCardRows : null,
    
    // Find a betfair market matching the given date, time and course name, 
    // and add the prices to the racecard if found.
    find: function(raceDate, raceTime, raceNum, courseNum, courseName) {
      if (raceTime !== "0.00") { 
        HG.BetfairSearch.marketSearch(raceDate, raceTime, raceNum, 
                                      courseNum, courseName); }
    },
    
    // Search for a market matching the parameters.
    marketSearch: function(raceDate, raceTime, raceNum, courseNum, courseName) {
      var parameters, that;
      
      that = this;
      
      parameters = {
        "key"   : "get_bf_mkt", 
        "date"  : raceDate, 
        "time"  : raceTime.replace(".", ":"), 
        "race"  : raceNum,
        "course": courseNum,
        "venue" : courseName
      };
      
      this.disablePriceUpdates();
      
      $.getJSON(HG.Config.BetfairSearchUrl, parameters, function(json) {
        if (json) {
          that.BFMktId = json.bf_mkt_id;
          that.BFExchId = json.bf_exch_id;
          that.BFHorseIds = json.bf_horse_codes;
          that.updateRaceCard(json.bf_prices);
          that.enablePriceUpdates();
        }
      });
    },
    
    updateRaceCard: function(bfPrices) {
      var bf_price, row, button_href;
      
      this.RaceCardRows = $("table#racecard_table tr.runner");
      
      for (var i=0; i < this.RaceCardRows.length; i++) {
        row = this.RaceCardRows[i];
        
        // The Timeform horse code is stored as the id of the table row:
        bf_price = bfPrices[$(row).attr('id')];
        
        this.updatePrice(row, bf_price);
      }
    },
    
    updateRaceCardPrices: function(bfPrices) {
      var row, BFHorseId;
      
      for (var i=0; i < this.RaceCardRows.length; i++) {
        row = this.RaceCardRows[i];
        
        // This time the prices are keyed by BF id, so we use the
        // lookup table to convert.
        BFHorseId = this.BFHorseIds[$(row).attr('id')];
        
        this.updatePrice(row, bfPrices[BFHorseId]);
      }
    },
    
    // Update the given row with data contained in the given bf_price object.
    updatePrice: function(row, bf_price) {
      var that         = this,
          row          = $(row),
          backButton   = row.find("td.back_odds  div.back_button"),
          backPrice    = row.find("td.back_odds  span.price"),
          backAvail    = row.find("td.back_odds  span.avail"),
          layButton    = row.find("td.lay_odds   div.lay_button"),
          layPrice     = row.find("td.lay_odds   span.price"),
          layAvail     = row.find("td.lay_odds   span.avail"),
          spButton     = row.find("td.betfair_sp div.sp_button"),
          betfairSP    = row.find("td.betfair_sp span.price");
      
      $([backButton, layButton, spButton]).each(function() {
        HG.BetfairSearch.addPriceEventHandlers(this, that.BFMktId, that.BFExchId);
      });
      
      if (bf_price) {
        if (bf_price.back) {
          backPrice.html(bf_price.back.price);
          backAvail.html("&pound;" + bf_price.back.avail);
        }
        
        if (bf_price.lay) {
          layPrice.html(bf_price.lay.price);
          layAvail.html("&pound;" + bf_price.lay.avail);
        }
        
        if (bf_price.bsp) {
          betfairSP.html(bf_price.bsp);
        }
      }
    },
    
    // Add event handler to price button element, if none is already present.
    addPriceEventHandlers: function(priceButton, marketId, exchangeId) {
      if (!priceButton.attr("hasEventHandlers")) {
        if (marketId && exchangeId) {
          priceButton.click(function() {HG.betfairMarketLink(marketId, exchangeId);});
          priceButton.attr("hasEventHandlers", true);
        }
      }
    },
    
    // Periodically update the prices for the given marketId and exchangeId.
    enablePriceUpdates: function() {
      var that = this, intervalCode;
      
      intervalCode = setInterval(
        function() {
          $.getJSON(HG.Config.BetfairSearchUrl, {
            "key"     : "get_bf_prices",
            "mkt_id"  : that.BFMktId,
            "exch_id" : that.BFExchId
          }, function(json) {
            that.updateRaceCardPrices(json);
          });
        },
        
        (HG.Config.PriceRefreshInterval * 1000)
      );
      
      // Store the interval code:
      this.IntervalCode = intervalCode;
    },
    
    // Disable price updates for the current market, if any.
    disablePriceUpdates: function() {
      var that = this;
      
      if (this.IntervalCode) { clearInterval(that.IntervalCode); }
    }
    
  }
  
  // ------------------------------------------------------
  
  // History records changes to the page state and registers them with Google
  // analytics:
HG.History = {
    addItem: function(type, parameters) {
      // If an array is passed in as the parameters value, convert to a string:
      if (typeof parameters == "object") { parameters = parameters.join('/'); }
      
      var newLocation = [type, parameters].join('/');
      
      HG.History.googleAnalytics(newLocation);
    },
    
    // Register the page view with google analytics, if the urchinTracker function
    // is available.
    googleAnalytics: function(locationString) {
      if (urchinTracker) { urchinTracker(locationString); }
    }
  }
  
  // ------------------------------------------------------
  
HG.Templates = {
    // Parse any embedded templates and ensure any template macros
    // are ready for use.
    prepare: function() {
      $("div#json_templates textarea").each(function() {
        var template = TrimPath.parseDOMTemplate(this.id);
        HG.Templates.cache[this.id] = template;
      });
      
      this.cache.macros_template.process();
    },
    
    process: function(templateName, templateContext, contextAlias, callback) {
      if (this.cache[templateName]) {
        HG.logger("Using template: " + templateName);
        
        var processedTemplate = this.__render(templateName, templateContext, contextAlias);
        
        callback(processedTemplate);
      } else {
        HG.logger("Loading new template: " + templateName);
        
        var pathToTemplate = HG.Config.TemplatesPath + templateName + ".jst";
        
        $.get(pathToTemplate, function(data) {
          var template = TrimPath.parseTemplate(data),
              processedTemplate;
          
          HG.Templates.cache[templateName] = template;
          
          processedTemplate = HG.Templates.__render(templateName, templateContext, contextAlias);
          
          callback(processedTemplate);
        });
      }
    },
    
    // Parse and cache the macros template.
    prepareMacros: function() {
      this.cache.macros_template.process();
    },
    
    // Render the given template within the specified context.
    __render: function(templateName, templateContext, contextAlias) {
      var context = this.__createTemplateContext(templateContext, contextAlias);
      return this.cache[templateName].process(context);
    },
    
    // This is a helper for creating context objects for use by templates.
    // Any custom modifiers are loaded into the context object automatically.
    __createTemplateContext: function(variable, name) {
      var context = {};
      context._MODIFIERS = HG.templateModifiers;
      context[name] = variable;
      return context;
    },
    
    cache: {},
    macros: {}
  }
  
  // ------------------------------------------------------
  
  
  // ------------------------------------------------------
  
  // Opens a new browser window for the market matching the given parameters.
HG.betfairMarketLink = function(marketId, exchangeId) {
    var url = "http://www.betfair.com/?mi=" + marketId + "&ex=" + exchangeId + 
              "&rfr=" + HG.Config.ReferrerId;
    open(url);
  }
  
HG.betfairRadioLink = function() {
    var url = "http://radio.betfair.com";
    
    open(url, "Radio", "width=320,height=393");
  }
  
HG.timeformLink = function() {
    open("http://timeform.com/main.asp");
  }
  
HG.silksImageLink = function(silks_code) {
    return [HG.Config.SilksImageDomain, HG.Config.SilksImagePath, 
            silks_code + '.' + HG.Config.SilksImageFormat].join("");
  }
  
  // Toggle the visibility of the given element depending on whether an AJAX
  // request is currently taking place.
HG.ajaxIndicator = function(element) {
    $(element).ajaxStart(function() { $(this).show(); });
    $(element).ajaxStop(function()  { $(this).hide(); });
  }
  
  // ------------------------------------------------------
  
  
  // Class definitions ------------------------------------
  
  // Racecourse Class.
HG.Racecourse = function(json, raceSurface) {
    var comments;
    
    this.courseName   = json[0].course_description;
    this.courseNumber = json[0].course_number;
    this.raceSurface  = raceSurface;
    this.courseMapUrl = HG.Views.Helpers.courseMapUrl(this.courseName,
                        json[0].season_type, this.raceSurface);
    this.comment      = "";
    
    for (var i = 0; i < json.length; i++) {
      this.comment += (json[i].comment_text + " ");
    }
    
    // An "@@@" in the comment string denotes that there are two comments,
    // one for all-weather and one for turf.
    if (this.comment.match(/@@@/)) {
      comments = this.comment.split("@@@");
      
      // Replace the comment with the appropriate section of text:
      if (raceSurface === "A") {
        this.comment = comments[0];
      } else {
        this.comment = comments[1];
      }
    }
    
    return this;
  }
  
  // ------------------------------------------------------
  
  
  // Various utility methods ------------------------------
  
HG.Utils = {
    // Create a string in the format DD-MM-YYYY from the given dateObject.
    createDateString: function(dateObject) {
      var year  = dateObject.getFullYear();
      var month = dateObject.getMonth() + 1;
      var date  = dateObject.getDate();
      
      if (month.toString().length === 1) { month = "0" + month.toString(); }
      if (date.toString().length  === 1) { date  = "0" + date.toString();  }
      
      return [date, month, year].join("-");
    },
    
    // Create a Date object from the given date string (expected format is DD-MM-YYYY).
    createDateObject: function(dateString) {
      var dateParts = dateString.split("-"), date, month, year;
      
      date  = dateParts[0];
      month = dateParts[1];
      year  = dateParts[2];
      
      return new Date(year, (month - 1), date);
    },
    
    // Truncate a string to the given length.
    truncate: function(string, length, end) {
      var terminate = end || "...";
      
      if (string.length > length) {
        return string.substring(0, (length - 1)) + terminate;
      } else {
        return string;
      }
    },
    
    // Takes the given array and returns a string that can be used 
    // by templates to pass in parameters to a function call.
    toFunctionParamsString: function(propertiesArray, data) {
      var params = [];
      
      for (var i = 0; i < propertiesArray.length; i++) {
        params.push("'" + data[propertiesArray[i]] + "'");
      }
      
      return params.join(", ");
    },

	search_meeting: function(meetings, course_number) {
		for (m in meetings) {
			var meeting = meetings[m];
			if (meeting.course_number == course_number) {
				return m;
			}
		}
		return null;
	},

	search_race: function(races, race_number, course_number, race_date) {
		for (r in races) {
			var race = races[r];
			if (race.race_number == race_number &&
				race.course_number == course_number &&
				race.race_date == race_date) {
				return r;
			}
		}
		return null;
	}
  }
  
  // ------------------------------------------------------
  
  
  // ------------------------------------------------------
  
  // Use to record the user's session state.
  // 
  // e.g.: HG.Session.courseNumber = 123;
HG.Session = {
    horseCode:      null,
    jockeyCode:     null,
    trainerCode:    null,
    raceDate:       null,
    courseNumber:   null,
    resultsPresent: false
  }
  
  // ------------------------------------------------------
  
  // Initialize the page.
HG.init = function() {
    // Ensure ajax requests are not cached:
    $.ajaxSetup({cache: false});
    
    // Set up the ajax activity indicator:
    HG.ajaxIndicator("div#ajax_spinner");
    
    // Set up the tab group:
    HG.Tabs.group("main_tabs");
    
    // Prepare the templates for use:
    HG.Templates.prepare();
    
    // Fetch the initial list of race meetings:
	HG.API.getFutureMeetings();

    // Get the quick results and on course info and start timers:
    HG.Views.getHomePageInfo();
    setInterval(HG.Views.getHomePageInfo, 30000);

    // Set up event handlers for the search boxes:
    HG.Views.Helpers.assignSearchBoxEvents( $("div.search_container") );
    
    // Assign event handlers for the course data select list:
    HG.CourseData.assignEventHandlers( $("select#" + HG.CourseData.SelectId), 
                                       $("div#" + HG.CourseData.ContainerId) );
    
    // Build the results archive:
    HG.ResultsArchive.build();
        
    // Open the homepage, forum, join now and banner links in a new window:
    $("a#homepage_link").click(function()           {open(this.href); return false;} );
    $("a#forum_link").click(function()              {open(this.href, "Forum", "width=790,height=500"); return false;} );
    $("a#join_now_link").click(function()           {open(this.href); return false;} );
    $("a#timeform_static_banner").click( function() {open(this.href); return false;} );
    $("a#manual_pdf_link").click(function()         {open(this.href); return false;} );
    
	// Select the racecard tab initially so that the welcome screen is shown:    
	HG.Tabs.displayTab(g_display_tab);
    
	switch (g_display_tab) {
		
		case 'jockey':
			HG.API.getJockey(g_jockey_code);
			break;

		case 'trainer':
			HG.API.getTrainer(g_trainer_code);
			break;

		case 'results':
			HG.API.getResultAll(g_race_date, g_course_number, g_race_number);
			break;
	}
  }

// Extend the String class to make life a bit easier:
String.prototype.truncate = function(length, end) { return HG.Utils.truncate(this, length, end); };

// global variables - default values
// set them to sensible values to perform actions
var g_selected_day   = g_selected_day   || null;
var g_course_number  = g_course_number  || null;
var g_meeting_number = g_meeting_number || null;
var g_race_number    = g_race_number    || null;
var g_race_date      = g_race_date      || null;
var g_horse_code     = g_horse_code     || null;
var g_jockey_code    = g_jockey_code    || null;
var g_trainer_code   = g_trainer_code   || null;
var	g_display_tab    = 'racecard';

$(document).ready(function() { 
	HG.init();
});
