(function (window, document, $, bam) {

  // Create <ABBR> element in IE6 
  document.createElement('abbr');
  
  // cache workaround to dev instance proxy
  $.ajaxSetup({
    cache: !bam.env.host.isDev
  });

  // Shortcuts
  var clubProps                = window.clubProps,
      launchGameday            = window.launchGameday,
      openTIXXWindow           = window.openTIXXWindow,
      datetime                 = bam.datetime,
      toYMD                    = datetime.toYMD,
      floorDate                = datetime.floorDate,
      PeriodicalExecuter       = bam.util.PeriodicalExecuter,
      ensureArray              = bam.util.ensureArray,
      proxy                    = bam.object.proxy,
      eventProxy               = bam.object.eventProxy,
      getDeepValue             = bam.object.getDeepValue,
      setDeepValue             = bam.object.setDeepValue,
      getQueryResults          = bam.util.getQueryResults,
      arraySortIsBroken        = !![{a:0,b:1},{a:1,b:1}].sort(function(a,b){return a.b-b.b;})[0].a,
                                                              
      MLB                      = 'mlb', // Major League Baseball
      AL                       = 'AL',  // American League
      NL                       = 'NL',  // National League
      CL                       = 'CL',  // Grapefruit League
      GL                       = 'GL',  // Cactus League
      WBC                      = 'WBC', // World Baseball Classic
      E                        = 'E',   // Exhibiton
                               
      mlbLeagueCodes = {       
        '103': AL,             
        '104': NL,             
        '114': CL,             
        '115': GL              
      },                       
                               
      homeAwayProperty         = /^(home|away)_(.*)$/;


  datetime.getDateByOffset = function (offset /*, local */) {

    var offsetHours   = ~~(offset / 100),
        offsetMinutes = offset % 100,
        local = (typeof arguments[1] !== 'undefined') ? new Date(arguments[1]) : new Date();

    return new Date(
      local.getUTCFullYear(),
      local.getUTCMonth(),
      local.getUTCDate(),
      local.getUTCHours() + offsetHours,
      local.getUTCMinutes() + offsetMinutes,
      local.getUTCSeconds(),
      local.getUTCMilliseconds()
    );
  };



  /**
   * Adds a leading zero to numbers less than 10
   *
   * @function addLeadingZeros
   * @param {Number|String} val Numeric value
   * @param {Number} len (optional, default = 2) String length
   * @return {String} Modified value
   */
  function addLeadingZeros (val /*, len */) {
    var str = val.toString(),
        len = arguments[1] || 2;
    while (str.length < len) {
      str = '0' + str;
    }
    return str;
  }


  function truncate (str, n) {
    return str.length<n?str:(new RegExp("^(.{0,"+(n-1)+"}\\S)(\\s|$)")).exec(str)[1]+"...";
  }

  function ordinal (n) {
    return["th","st","nd","rd"][(n=n<0?-n:n)>10&&n<14||!(n=~~n%10)||n>3?0:n];
  }


  /**
   * GameDataProvider
   */
  var GameDataProvider = (function () {

    var self;

    /**
     * Generate URI to request Gameday data for a specific date
     */
    function makeUri (date) {
      return [
        '/gdcross/components/game/mlb',
        'year_'  + date.getFullYear(),
        'month_' + addLeadingZeros(date.getMonth() + 1),
        'day_'   + addLeadingZeros(date.getDate()),
        'master_scoreboard.json'
      ].join('/');
    }


    /**
     * Parse Gameday data and trigger onSuccess event
     */
    function handleLoadSuccess (data) {

      data = data.data.games;

      var date  = [data.year, data.month, data.day].join('/'),
          games = ensureArray(data.game);

      if (games.length > 0) {
        self.trigger('onSuccess', [date, games]);
      } else {
        self.trigger('onEmpty', [date]);
      }
    }

    /**
     * Trigger onError event
     */
    function handleLoadError () {
      self.trigger('onError', ['Unable to load game data.']);
    }

    /**
     * Trigger onComplete event
     */
    function handleLoadComplete (xhr, status) {
      self.trigger('onComplete', [status]);
    }

    /**
     * Public API
     */
    self = {

      /**
       * Request Gameday data for a specific date
       */
      load: function (date, type) {

        this.date = date;

        type = type || 'load';

        $.ajax({
          url:         makeUri(date),
          dataType:    'json',
          beforeSend:  function handleBeforeSend () {
            self.trigger('onBeforeSend', [type]);
          },
          success:     handleLoadSuccess,
          error:       handleLoadError,
          complete:    handleLoadComplete
        });
      },

      /**
       * Reload Gameday data for last specified date
       */
      reload: function () {
        if (this.date) {
          this.load(this.date, 'reload');
        }
      }
    };

    /**
     * Augment with bindable behavior
     */
    return $.bindable(self, 'onBeforeSend onSuccess onError onComplete onEmpty');

  })();


  /**
   * TicketingDataProvider
   */
  var TicketingDataProvider = (function () {

    var self;

    /**
     * Generate URI to request ticketing data for a specific date
     */
    function makeUri (date) {
      var ymd = toYMD(date);
      return '/ticketing-client/json/Game.tiksrv?' + $.param({
        sport_id:     1,
        site_section: "'SCHEDULE'",
        begin_date:   ymd,
        end_date:     ymd,
        year:         date.getFullYear()
      });
    }

    /**
     * Trigger onBeforeSend event
     */
    function handleBeforeSend () {
      self.trigger('onBeforeSend');
    }

    /**
     * Parse ticketing data from response and trigger onSuccess event if games
     * are available. Otherwise trigger onEmpty event.
     */
    function handleLoadSuccess (data) {
      var events = ensureArray(getDeepValue(data, 'events.game'));
      if (events.length > 0) {
        self.trigger('onSuccess', [events]);
      } else {
        self.trigger('onEmpty');
      }
    }

    /**
     * Trigger onError event
     */
    function handleLoadError () {
      self.trigger('onError', ['Unable to retrieve ticketing data.']);
    }

    /**
     * Trigger onComplete event
     */
    function handleLoadComplete (xhr, status) {
      self.trigger('onComplete', [status]);
    }

    /**
     * Public API
     */
    self = {

      /**
       * Request ticketing data for a specific date
       */
      load: function (date) {
        this.date = date;
        $.ajax({
          url:        makeUri(date),
          dataType:   'json',
          beforeSend: handleBeforeSend,
          success:    handleLoadSuccess,
          error:      handleLoadError,
          complete:   handleLoadComplete
        });
      },

      /**
       * Reload ticketing data for last specified date
       */
      reload: function () {
        if (this.date) {
          this.load(this.date);
        }
      }
    };

    /**
     * Augment with bindable behavior
     */
    return $.bindable(self, 'onBeforeSend onSuccess onError onComplete onEmpty');

  })();


  /**
   * Game Alerts data provider for Ticker
   *
   * @author jferrer, but cribbed from furf
   */ 
  var GameAlertsDataProvider = (function () {

    var self;

    /**
     * Trigger onBeforeSend event
     */
    function handleBeforeSend () {
      self.trigger('onBeforeSend');
    }

    /**
     * Parse alerts data from response and trigger onSuccess event if alerts
     * are available. Otherwise trigger onEmpty event.
     */
    function handleLoadSuccess (response) {
      var alerts = ensureArray(getDeepValue(response, 'data.alerts.game')); 
      if (alerts.length) {
        self.trigger('onSuccess', [alerts]);
      } else {
        self.trigger('onEmpty');
      }
    }

    /**
     * Trigger onError event
     */
    function handleLoadError () {
      self.trigger('onError', ['Unable to retrieve game alerts data.']);
    }

    /**
     * Trigger onComplete event
     */
    function handleLoadComplete (xhr, status) {
      self.trigger('onComplete', [status]);
    }

    /**
     * Public API
     */
    self = {

      /**
       * Request game alerts data
       */
      load: function () {

        $.ajax({
          url:        '/gdcross/components/game/mlb/alerts.json',
          dataType:   'json',
          beforeSend: handleBeforeSend,
          success:    handleLoadSuccess,
          error:      handleLoadError,
          complete:   handleLoadComplete
        });

      }
    };

    /**
     * Augment with bindable behavior
     */
    return $.bindable(self, 'onBeforeSend onSuccess onError onComplete onEmpty');
    
  })();



  var ScoreboardApp = (function () {

    var self,
        element,
        template,
        gameCache = [],
        ticketingCache = {},
        liveLookInCache = {},
        scoreUpdatedHash = {},
        runnersHash = {},
        
        epgSortOrder = {
          during: 1,
          before: 2,
          after:  3,
          nixed:  4,
          '':     5   // Handle possible error case
        };

    function formatDateAsFEY (dateString) {
      return dateString.replace(formatDateAsFEY.regExp, '$2/$3/$1');
    }

    formatDateAsFEY.regExp = /^\d{2}(\d{2})\/0?(\d{1,2})\/0?(\d{1,2})$/;

    /**
     * Augment game object with time data
     */
    function extendGameTime (game, date) {

      // Add current and original dates
      game.current_date  = date;
      game.original_date = game.id.match(extendGameTime.regExp, '$1')[1];
      game.season = game.original_date.substr(0, 4);
      
      // Start time to be determined (TBD)
      game.is_tbd = (game.time === '3:33' && game.ampm === 'AM');
    }

    extendGameTime.regExp = /^(\d{4}\/\d{2}\/\d{2}).*$/;

    /**
     * Augment game object with game type booleans
     */
    function extendGameType (game) {

      var gameType = game.game_type,
          type = {};

      type.is_regular_season  = (gameType === 'R');
      type.is_first_round     = (gameType === 'F');
      type.is_division_series = (gameType === 'D');
      type.is_league_series   = (gameType === 'L');
      type.is_world_series    = (gameType === 'W');
      type.is_all_star_game   = (gameType === 'A');
      type.is_spring_training = (gameType === 'S');
      type.is_exhibition      = (gameType === 'E');
      type.is_19th_century    = (gameType === 'N');
      type.is_intrasquad      = (gameType === 'I');

      // Additional convenience properties
      type.is_pre_season  = (type.is_spring_training || type.is_exhibition);
      type.is_post_season = (type.is_first_round || type.is_division_series || type.is_league_series || type.is_world_series);

      game.type = type;
    }

    /**
     * Augment game object with game status booleans
     */
    function extendGameStatus (game) {

      var status    = game.status,
          primary   = status.ind.charAt(0),
          secondary = status.ind.charAt(1);

      status.is_scheduled        = (primary === 'S');
      status.is_pre_game         = (primary === 'P' && secondary === '');
      status.is_delayed_start    = (primary === 'P' && secondary !== '' && secondary !== 'W');
      status.is_before_game      = (status.is_scheduled || status.is_pre_game || status.is_delayed_start);

      status.is_warmup           = (primary === 'P' && secondary === 'W');
      status.is_in_progress      = (primary === 'I' && secondary === '');
      status.is_instant_replay   = (primary === 'I' && secondary === 'H');
      status.is_delayed          = (primary === 'I' && secondary !== '');
      status.is_suspended        = (primary === 'U');
      status.is_during_game      = (status.is_warmup || status.is_in_progress || status.is_instant_replay || status.is_delayed || status.is_suspended);

      status.is_over             = (primary === 'O' && secondary === '');
      status.is_final            = (primary === 'F' && (secondary === '' || secondary === 'T'));
      status.is_tied             = (primary === 'F' && secondary === 'T');
      status.is_completed_early  = ((primary === 'O' || primary === 'F') && !status.is_over && !status.is_final);
      status.is_forfeit          = (primary === 'R');
      status.is_after_game       = (status.is_over || status.is_completed_early || status.is_final || status.is_forfeit);

      status.is_postponed        = (primary === 'D');
      status.is_cancelled        = (primary === 'C');
      status.is_nixed            = (status.is_postponed || status.is_cancelled);

      status.is_top_of_inning    = (status.top_inning === 'Y');
      status.is_bottom_of_inning = (status.top_inning === 'N');

      status.epg = status.is_before_game ? 'before' :
                   status.is_during_game ? 'during' :
                   status.is_after_game  ? 'after'  :
                   status.is_nixed       ? 'nixed'  : '';

      status.epgSortOrder = epgSortOrder[status.epg];
      
      if (status.inning) {
        status.inning_ordinal = ordinal(status.inning);
      }

      // @todo remove when no longer necessary
      if (status.is_instant_replay) {
        game.description   = 'Instant Replay';
        status.status = 'Replay';
        status.reason = 'Review';
      }
    }

    /**
     * Augment game object with information about game suspension/resumption
     */
    function extendGameSuspension (game) {

      // Handle suspension rules
      var hasResumeDate = (typeof game.resume_date !== 'undefined' && game.resume_date !== ''),
          currentDateIsResumeDate = (game.current_date === game.resume_date);

      // Game has resumption
      game.has_resumption = (game.status.is_suspended && hasResumeDate && !currentDateIsResumeDate);

      if (game.has_resumption) {
        game.resume_date_formatted = formatDateAsFEY(game.resume_date);
      }

      // Game is suspension resumption
      game.is_resumption = (game.status.is_suspended && hasResumeDate && currentDateIsResumeDate);

      if (game.is_resumption) {
        game.original_date_formatted = formatDateAsFEY(game.original_date);
      }
    }

    /**
     * Augment game object with away and home team objects
     */
    function extendGameTeams (game) {

      var teams = {
            away: {},
            home: {}
          },
          prop,
          matches;

      // Parse away_ and home_ properties and map them to related team object
      for (prop in game) {
        if (game.hasOwnProperty(prop)) {
          matches = prop.match(homeAwayProperty);
          if (matches !== null) {
            teams[matches[1]][matches[2]] = game[matches[0]];
          }
        }
      }

      $.each(teams, function (key, team) {

        // Move sport code to convenient boolean
        team.is_major_league = (team.sport_code === MLB);

        // Set league code to either spring training (if available) or regular
        team.league_code = mlbLeagueCodes[team.league_id_spring || team.league_id];

        // Add team-specific domain to URL
        if (team.sport_code === MLB && team.team_id in clubProps) {
          team.url_cache = 'http://' + clubProps[team.team_id].url_cache;
          team.url = team.url_cache + '/index.jsp?c_id=' + team.file_code;
        }
        
        // add links
        team.links = {
          stats:    '/stats/sortable_player_stats.jsp?c_id=' + team.file_code,
          news:     '/news/index.jsp?c_id=' + team.file_code,
          fantasy:  '/mlb/fantasy/wsfb/news/index.jsp?action=browse&start=1&position=0&status=predraft&team=' + team.name_abbrev,
          schedule: '/schedule/index.jsp?c_id=' + team.file_code,
          more:     '/index.jsp?c_id=' + team.file_code
        };
                
      });
      
      if (game.status.is_top_of_inning) {
        teams.pitching = teams.home;
        teams.batting  = teams.away;
      } else {
        teams.pitching = teams.away;
        teams.batting  = teams.home;
      }

      game.teams = teams;
    }

    /**
     * Sort home runs by inning (until more specific data is included)
     */
    function sortHomeRuns (a, b) {
      return +a.inning > +b.inning;
    }

    /**
     * Parse home run data and augment game team objects with home run arrays
     */
    function extendHomeRuns (game) {

      var away     = game.teams.away,
          home     = game.teams.home,
          homeRuns = {},
          players  = ensureArray(getDeepValue(game, 'home_runs.player')),
          i, n, player;
          
      // Map home runs arrays by team code
      homeRuns[away.code] = away.home_runs = [];
      homeRuns[home.code] = home.home_runs = [];

      for (i = 0, n = players.length; i < n; ++i) {
        player = players[i];
        if (typeof homeRuns[player.team_code] !== 'undefined') {
          homeRuns[player.team_code].push(player);
        }
      }

      away.home_runs.sort(sortHomeRuns);
      home.home_runs.sort(sortHomeRuns);
    }

    /**
     * If game is complete, augment game object with winning and losing team
     * objects
     */
    function extendWinningTeams (game) {

      var away     = game.teams.away,
          home     = game.teams.home,
          runs     = game.linescore.r,
          runsAway = ~~+runs.away,
          runsHome = ~~+runs.home;

      if (runsAway > runsHome) {
        game.winning_team = away;
        game.losing_team  = home;
      } else if (runsHome > runsAway) {
        game.winning_team = home;
        game.losing_team  = away;
      }
    }

    /**
     * Augment game object with doubleheader status
     */
    function extendDoubleheader (game) {
      // Doubleheader
      game.game_number = game.id.substr(-1);
      game.is_doubleheader = (game.game_number === '2');
    }

    /**
     * Ensure game object contains a linescore object
     */
    function extendLinescore (game) {
      game.linescore = game.linescore || { r: {}, h: {}, e: {} };
      game.linescore.inning = ensureArray(game.linescore.inning);
    }


    function makeJavaScriptUrl (code) {
      return 'javascript:void(' + code + ');';
    }

    /**
     * Ensure game object contains a links object
     */
    function extendLinks (game) {

      var links   = game.links || {},
          status  = game.status,
          gameday = game.gameday,
          teams   = game.teams,
          ymd     = game.original_date.replace(/\//g, '');

      if (game.gameday && (status.is_pre_game || status.is_during_game || status.is_after_game)) {
        links.boxscore = '/news/boxscore.jsp?gid=' + game.gameday;
      }
      
      links.buyMLBTV    = 'http://mlb.mlb.com/mlb/subscriptions/index.jsp?product=mlbtv';
      links.probables   = '/news/probable_pitchers/index.jsp?c_id=mlb&ymd=' + ymd + '#' + teams.away.name_abbrev + '@' + teams.home.name_abbrev;
                        
      links.mlbtv       = links.mlbtv && makeJavaScriptUrl(links.mlbtv);
      links.away_audio  = links.away_audio && makeJavaScriptUrl(links.away_audio);
      links.home_audio  = links.home_audio && makeJavaScriptUrl(links.home_audio);
      links.audIo       = links.home_audio || links.away_audio;
      links.press       = '/mlb/presspass/gamenotes.jsp?c_id=mlb';
      
      game.links = links;
    }

    /**
     * Track score updates between data refreshes
     */
    function extendGameScoreUpdated (game) {
      if (game.status.is_during_game) {
        var hash = scoreUpdatedHash[game.gameday];
        var score = getDeepValue(game, 'linescore.r.away') + '-' + getDeepValue(game, 'linescore.r.home');
        game.score_changed = hash && hash !== score;
        scoreUpdatedHash[game.gameday] = score;
      }
    }

    /**
     * Track runners on base between data refreshes
     */
    function extendGameRunners (game) {
      
      game.runners_on_base = game.runners_on_base || {};

      if (game.status.is_during_game) {

        var hash      = runnersHash[game.gameday],
            inningKey = game.status.inning + game.status.top_inning,
            current   = game.runners_on_base,
            previous,
            i,
            prop;

        if (hash && hash.inningKey === inningKey) {

          previous = hash.runnersOnBase;

          for (i = 1; i <= 3; ++i) {
            prop = 'runner_on_' + i + 'b';

            // @todo - remove this after updating expanded scoreboard
            // with new menOnBase
            if (current[prop]) {
              current[prop].previous = previous[prop];

              if ((!previous[prop] || current[prop].id !== previous[prop].id)) {
                current[prop].moved = true;
              }
            }
          }
        }

        runnersHash[game.gameday] = {
          inningKey:     inningKey,
          runnersOnBase: current
        };

      }
    }


    function addTeamToPlayer (player, team) {
      player.team = team;
      if (player.id && team.is_major_league) {
        player.link = '/team/player.jsp?player_id=' + player.id;
      }
    }

    function extendPlayers (game) {

      var away  = game.teams.away,
          home  = game.teams.home,
          teams = {},
          homeRunPlayers = ensureArray(getDeepValue(game, 'home_runs.player')),
          i, n, player, code, team;

      // extend home run hitters
      teams[away.code] = away;
      teams[home.code] = home;

      for (i = 0, n = homeRunPlayers.length; i < n; ++i) {
        player = homeRunPlayers[i];
        if (player.team_code && player.team_code in teams) {
          addTeamToPlayer(player, teams[player.team_code]);
        }
      }
      
      // extend probable pitchers
      for (code in teams) {
        team = teams[code];
        if (team.probable_pitcher && team.probable_pitcher.last) {
          addTeamToPlayer(team.probable_pitcher, team);
        }
      }
      
      // extend pitcher/batters
      if (game.pitcher && game.teams.pitching) {
        addTeamToPlayer(game.pitcher, game.teams.pitching);
      }
      
      if (game.batter && game.teams.batting) {
        addTeamToPlayer(game.batter, game.teams.batting);
      }

      // extend win/lose/save pitchers
      if (game.winning_pitcher && game.winning_team) {
        addTeamToPlayer(game.winning_pitcher, game.winning_team);
      }

      if (game.losing_pitcher && game.losing_team) {
        addTeamToPlayer(game.losing_pitcher, game.losing_team);
      }

      if (game.save_pitcher && game.winning_team) {
        addTeamToPlayer(game.save_pitcher, game.winning_team);
      }
      
      // @todo add player link
    }

    /**
     * Augment game object with league/type for grouping on scoreboard
     */
    function extendLeagueTypeArea (game) {

      var homeLeagueCode = game.teams.home.league_code,
          awayLeagueCode = game.teams.away.league_code,
          gameLeagueCode = homeLeagueCode || awayLeagueCode;

      // Game is considered major league if either team is major league
      game.is_major_league = (game.teams.away.is_major_league || game.teams.home.is_major_league);

      // Exhibition
      if (game.type.is_exhibition) {
        game.leagueAreaKey = 'E';

      // World Series
      } else if (game.type.is_world_series) {
        game.leagueAreaKey = 'WS_' + gameLeagueCode;

      // All-Star Game
      } else if (game.type.is_all_star_game) {
        game.leagueAreaKey = 'AS_' + gameLeagueCode;

      // Undefined
      } else if (typeof gameLeagueCode === 'undefined') {
        game.leagueAreaKey = null;

      // World Baseball Classic
      } else if (gameLeagueCode === WBC) {
        // ignore WBC games
        // game.leagueAreaKey = gameLeagueCode;

      // Spring Training
      } else if (gameLeagueCode === GL || gameLeagueCode === CL) {
        game.leagueAreaKey = gameLeagueCode;

      // Intraleague
      } else if (homeLeagueCode === awayLeagueCode) {
        game.leagueAreaKey = gameLeagueCode;

      // Interleague
      } else {
        game.leagueAreaKey = 'X_' + gameLeagueCode;
      }
    }


    /**
     * Extract media (for thumbnails)
     */
    function extendGameMedia (game) {
      var media = ensureArray(getDeepValue(game, 'game_media.media')),
          i, n;
      for (i = 0, n = media.length; i < n; ++i) {
        setDeepValue(game, 'game_media.' + media[i].type, media[i]);
      }
    }


    /**
     * Augment game object
     */
    function extendGameData (game, date) {

      extendGameTime(game, date);
      extendGameType(game);
      extendGameStatus(game);
      
      
      extendGameSuspension(game);
      extendGameTeams(game);
      extendGameMedia(game);
      extendHomeRuns(game);
      extendLinescore(game);
      extendDoubleheader(game);
      extendLinks(game);
      extendGameScoreUpdated(game);
      extendGameRunners(game);
      extendLeagueTypeArea(game);

      // Add winning & losing teams
      if (game.status.is_after_game) {
        extendWinningTeams(game);
      }

      if (game.pbp && game.pbp.last) {
        game.pbp.last = game.pbp.last.replace(/\s+/g, ' ');
        game.pbp.last_truncated = truncate(game.pbp.last, 120);
      }
      
      extendPlayers(game);
      
      return game;
    }

    /**
     * Initialize various scoreboard DOM events
     */
    function initDOMEvents () {

      /**
       * Link to Gameday
       */
      $('a.gameday, a.atbat').live('click', function (e) {
        e.preventDefault();
        var elem = $(this);
        launchGameday({
          gid:  elem.attr('data-gameday-id'),
          mode: elem.attr('data-gameday-mode')
        });
      });

      /**
       * Link to tickets
       */
      $('a.tickets').live('click', function (e) {
        e.preventDefault();
        openTIXXWindow(this.href, $(this).attr('data-away-or-home'));
      });

      /**
       * Extra-inning pagination
       */
      $('input.prev, input.next').live('click', function (e) {
        e.preventDefault();

        var elem    = $(this),
            innings = elem.parent('th').siblings('th.inning'),
            // first   = innings.not('.hidden').index(), // jQuery 1.4 zoinks!
            first   = innings.index(innings.not('.hidden')[0]) + 1, 
            last    = innings.length + 1,
            range   = 10,
            i, n,
            prefix  = '#' + innings.parents('table').attr('id') + ' .inning-',
            hides   = [],
            shows   = [];

        if (elem.is('.prev')) {

          if (first === 1) { return; }

          first = Math.max(first - range, 1);
          last  = first + range;

        } else {

          if (first + range === last) { return; }

          last  = Math.min(first + range * 2, last);
          first = last - range;
        }

        for (i = 1, n = innings.length; i <= n; ++i) {
          ((i >= first && i < last) ? shows : hides).push(prefix + i);
        }

        $(hides.join(',')).addClass('hidden');
        $(shows.join(',')).removeClass('hidden');
      });

      /**
       * Highlight runners on base infographic
       */
      $.fn.flash = function () {
        var values = { first:1, second:2, third:4 };
        return this.each(function () {
          var elem  = $(this), value = 0;
          elem.find('dd').each(function () { value += values[this.className]; });
          elem.css('background-position', -80 * value + 'px 0');
          elem.find('dl').animate({opacity:0}, 2500);
        });
      };
    }

    /**
     * Sort games by game status
     */
    function sortByGameStatus (a, b) {
      return a.status.epgSortOrder - b.status.epgSortOrder;
    }

    function chromeSortByGameStatus (games) {
      if (games.length === 0) {
        return [];
      }
      var i, n, game, g = {
        during: [],
        before: [],
        after:  [],
        nixed:  []
      };

      for (i = 0, n = games.length; i < n; ++i) {
        game = games[i];
        if (game.status.epg) {
          g[game.status.epg].push(game);
        }
      }
      return [].concat(g.during, g.before, g.after, g.nixed);
    }

    /**
     * Public API
     */
    self = {

      clearLiveLookInCache: function () {
        liveLookInCache = {};        
        self.render();
      },
      
      /**
       * Update Live Look-In data and render
       */
      updateLiveLookInCache: function (media) {

        var i, n;

        // @todo render page only if necessary
        // @todo figure out cache comparison to determine if render is necessary
        
        liveLookInCache = {};        

        for (i = 0, n = media.length; i < n; ++i) {
          liveLookInCache[media[i].game_pk] = media[i];
        }

        self.render();

      },

      /**
       * Update game data and render
       */
      updateGameCache: function (date, games) {

        var i, n, game;
        
        gameCache.length = 0;

        for (i = 0, n = games.length; i < n; ++i) {
          game = extendGameData(games[i], date);
          gameCache.push(game);          
        }

        self.render();
      },

      /**
       * Update ticketing data and render
       */
      updateTicketingCache: function (data) {
        
        var cache = {}, i, n, game, link;

        for (i = 0, n = data.length; i < n; ++i) {

          game = data[i];
          link = game.ticket_link;

          link.away_or_home = (link.team_id === game.home_team_id) ? 'home' :
                              (link.team_id === game.away_team_id) ? 'away' : '';

          cache[game.schedule_id] = link;
        }

        ticketingCache = cache;

        self.render();
      },

      /**
       * Render scoreboard
       */
      render: function () {

        var gamesByLeagueArea = {
              ALL:   [],
              WS_AL: [],
              WS_NL: [],
              AS_AL: [],
              AS_NL: [],
              X_AL:  [],
              X_NL:  [],
              AL:    [],
              NL:    [],
              GL:    [],
              CL:    [],
              WBC:   [],
              E:     []
            },
            i, n, game,
            leagueAreaKey;

        if (gameCache.length > 0) {

          for (i = 0, n = gameCache.length; i < n; ++i) {
            game = gameCache[i];

            if (game.leagueAreaKey) {

              // Add ticketing information
              if (game.game_pk in ticketingCache) {
                game.ticketing = ticketingCache[game.game_pk];
              }

              // Add Live Look-in information
              if (game.game_pk in liveLookInCache) {
                game.lookIn = liveLookInCache[game.game_pk];
              }

              gamesByLeagueArea[game.leagueAreaKey].push(game);
              gamesByLeagueArea['ALL'].push(game);
            }
          }
// @todo -- do sort once before splitting into groups
          for (leagueAreaKey in gamesByLeagueArea) {
            if (gamesByLeagueArea.hasOwnProperty(leagueAreaKey)) {
              if (arraySortIsBroken) {
                gamesByLeagueArea[leagueAreaKey] = chromeSortByGameStatus(gamesByLeagueArea[leagueAreaKey]);
                gamesByLeagueArea['ALL'] = chromeSortByGameStatus(gamesByLeagueArea['ALL']);
              } else {
                gamesByLeagueArea[leagueAreaKey].sort(sortByGameStatus);
                gamesByLeagueArea['ALL'].sort(sortByGameStatus);
              }
            }
          }

          // Render HTML
          element.innerHTML = template(gamesByLeagueArea, true);

          /**
           * ANIMATION! @todo merge with shadow animation -- possibly use <style> animation
           */
          $('.runnersOn').flash();

          var g = $('.boxscore.scoreChanged').css('background-color', '#fc0');
          if (g.length > 0) {
            $({ r:255, g:204, b:0 })
              .animate({ r:229, g:229, b:229 }, {
                duration: 3000,
                step: function () {
                  g.css('background-color', 'rgb('+[~~this.r,~~this.g,~~this.b].join(',')+')');
                }
              });
          }
          
          
          setTimeout(function () {
            $('.menOnBase dd.moved').animate({ opacity: 0 }, 3000);
          }, 1500);
          
          
        }
      },

      /**
       * set template
       */
      setView: function (tmpl) {
        template = $.template(tmpl);
      },

      /**
       * set template
       */
      toggleView: function (tmpl) {
        self.setView(tmpl);
        self.render();
      },
      
      /**
       * Initialize scoreboard
       */
      init: function (el) {
        element = document.getElementById(el);
        initDOMEvents();
      }
    };

    /**
     * Augment with bindable behavior
     */
    return $.bindable(self);

  })();




  /**
   * Omniture tracking calls for scoreboard refresh
   */
  var ScoreboardTracking = {

    trackUserRefresh: function () {
      bam.tracking.simPgView({
        channel:        'Scoreboard',
        pageName:       'Major League Baseball: Scoreboard',
        source:         'Live Major League Baseball Scores',
        events:         'event4'
      });
    },

    trackAutoRefresh: function () {
      bam.tracking.simPgView({
        channel:        'Scoreboard',
        pageName:       'Major League Baseball: Scoreboard Auto Refresh',
        source:         'Live Major League Baseball Scores',
        events:         'event4'
      });
    },
    
    trackCalendarActivity: function (activity) {
      bam.tracking.track({
        async: {
          pageName:     'Major League Baseball: Scoreboard',
          compName:     'MLB Scoreboard Calendar',
          compActivity: 'MLB Scoreboard Calendar: ' + activity,
          actionGen:    true
        }
      });
    },
    
    trackToggleView: function (view) {
      bam.tracking.track({
        async: {
          pageName:     'Major League Baseball: Scoreboard',
          compName:     'MLB Scoreboard View',
          compActivity: 'MLB Scoreboard View: ' + view + ' Click',
          actionGen:    true
        }
      });
    }
  };


  var CountdownApp = (function () {

    var self, elem, icon, text, step, isLoading;
    
    self = {

      /**
       * Initialize the countdown app
       */
      init: function (id) {
        elem = $('#' + id);
        icon = $('.icon', elem);
        text = $('.remaining', elem);
        step = 16;
      },
      
      /**
       * Show countdown icon
       */
      show: function () {
        elem.css('visibility', 'visible');
      },

      /**
       * Hide countdown icon
       */
      hide: function () {
        elem.css('visibility', 'hidden');
      },

      /**
       * Update countdown icon
       */
      update: function (interval, remaining) {

        if (isLoading) {
          return;
        }

        var pct  = ((interval - remaining) / interval),
            left = ~~(pct * 100) * -step,
            secs = Math.max(0, Math.round(remaining / 1000)), // don't display negatives
            word = secs === 1 ? ' second ' : ' seconds ';
        icon.css('background-position', left + 'px 0');
        text.text(secs + word +  'until the next live update');
      },
      
      /**
       * Show loading indicator
       */
      showLoading: function () {
        isLoading = true;
        icon.addClass('loading');
      },
      
      /**
       * Hide loading indicator
       */
      hideLoading: function () {
        isLoading = false;
        icon.removeClass('loading');
      }
    };
    
    return self;
    
  })();


  var ScoreboardTickerApp = (function () {

    var self,
        mode,

        gameAlertIndex,
        lastGameAlertDisplayed,
        gameAlertDisplayed,
        gameAlerts,
        gameAlertsRef,

        tickerUpdateDuration = 10 * 1000, // length of time between alert change

        sbTickerContainer,
        render;

    /**
     * Updates Ticker content
     */
    function updateTickerView( tickerDataObj ) {
      var lastAlert = sbTickerContainer.find('a'),
          thisAlert = render( tickerDataObj ); 

      if( lastAlert.length === 0 || lastAlert.text().trim() !== tickerDataObj.text.trim() ) {
        sbTickerContainer.empty();
        render( tickerDataObj ).css( 'display', 'none').appendTo( sbTickerContainer ).fadeIn();
      }
    }

    /**
     * 
     */
    function displayGameAlerts() {

      if (!gameAlerts || gameAlerts.length < 1) {
        return;
      }

      gameAlertIndex = (gameAlertIndex >= gameAlerts.length) ? 0 : gameAlertIndex;
      gameAlertDisplayed = gameAlerts[gameAlertIndex];

      if(!gameAlertDisplayed) {
        return;
      }

      updateTickerView({
        type:    'gameAlert',
        text:    gameAlertDisplayed.brief_text,
        gameday: gameAlertDisplayed.game_id.replace(/[\W]/g, '_')
      });

      gameAlertIndex = gameAlertIndex + 1;

      if(gameAlerts.length > 1) {
        gameAlertsRef = setTimeout( displayGameAlerts, tickerUpdateDuration );
      }


    }

    function handleLiveLookIn (media) {

      // clear game alerts timeout, so it doesn't crash into live look in
      clearTimeout( gameAlertsRef );

      var medium = media[0];
      
      updateTickerView({
        type:              'liveLookIn',
        text:              medium.headline,
        calendar_event_id: medium.calendar_event_id
      });
    }

    function handleGameAlerts(alerts) {

      // clear timeout for old game alerts
      clearTimeout(gameAlertsRef);

      // reset game alerts variables
      gameAlerts = alerts;
      gameAlertIndex = 0;

      // start displaying alerts
      displayGameAlerts();
    } 

    function initDOMEvents () {
      // $('.alert').live(CLICK, function (e) {
      //   e.preventDefault();
      //   var elem = $(this);
      //   launchGameday({
      //     gid:  elem.attr('data-gameday-id')
      //   });
      // });
      $('a.gameAlert').live('click', function (e) {
        // Quick & dirty tracking
        ScoreboardTracking.trackCalendarActivity('Game Alert Click');
      });
    }

    self = {

      init : function (tickerContainerID, alertDuration) {

        sbTickerContainer  = $( '#' + tickerContainerID );
        tickerUpdateDuration = alertDuration || 5000;

        render = $.template('/scripts/scoreboard/scoreboardTicker.tpl');
        
        initDOMEvents();
      },

      displayAlerts : function (alerts) {

        if (mode !== 'alerts') {
          mode = 'alerts';
          displayGameAlerts();
          this.trigger('tickerMode:alerts');
        } else {
          if (alerts && alerts.length) {
            handleGameAlerts( alerts );
          }
        }
      },

      displayLiveLookIn : function (media) {
        mode = 'liveLookIn';
        handleLiveLookIn(media);
        this.trigger('tickerMode:liveLookIn');
      }
    };

    /**
     * Augment with bindable behavior
     */
    return $.bindable(self, 'tickerMode:alerts tickerMode:liveLookIn');

  })();


  /**
   * Initialize the apps and bind events
   */
  $(function () {

    
    // Requires/includes
    bam.loadSync(bam.homePath + 'bam.url.js');

    var params    = bam.url.parseFragmentIdentifiers(),
        dateParam = params.date,
        lookupDate;

    // Date has been user-specified
    if (typeof dateParam !== 'undefined') {

      lookupDate = new Date(dateParam);

      // Redirect requests for pre-2007 to CMS-generated scoreboards
      if (lookupDate.getFullYear() < 2007) {
        window.location.href = toYMD(lookupDate) + '.html';
      }

    } else {

      lookupDate = new Date();

      var crossover = floorDate("day").getTime() === floorDate('day', '4/19/2010').getTime() ? 9 : 11;

      // Display previous day's scoreboard until 11am EST
      if (datetime.easternHours < crossover) {
        lookupDate.setDate(lookupDate.getDate() - 1);
      }      
    }

    var today     = floorDate('day'),
        todayTime = today.getTime(),

        LiveLookIn = bam.widget.LiveLookIn,

        gameDataPoller = new PeriodicalExecuter(
          proxy(function () {
            GameDataProvider.reload();
            ScoreboardApp.trigger('autoRefresh');
          }, GameDataProvider),
        15000, true), // 15 seconds + defer

        ticketingDataPoller = new PeriodicalExecuter(
          proxy(TicketingDataProvider.reload, TicketingDataProvider),
        300000, true), // 5 minutes + defer

        gameAlertsDataPoller = new PeriodicalExecuter(
          proxy(GameAlertsDataProvider.load, GameAlertsDataProvider),
        15000, true), // 15 seconds 

        countdownPoller = new PeriodicalExecuter(function () {
          CountdownApp.update(gameDataPoller.getInterval(), gameDataPoller.getTimeUntilNextExecution());
        }, 100, true),
        
        trackingPoller = new PeriodicalExecuter(ScoreboardTracking.trackAutoRefresh, 30000, true);

        // saving myself the firebug headaches of constant refresh while
        // inspecting elements
        if (params.poll && params.poll === 'false') {
          gameDataPoller = 
          ticketingDataPoller = 
          gameAlertsDataPoller =
          countdownPoller =
          trackingPoller = {
            start: function () {},
            stop: function () {}
          };
          LiveLookIn.start = LiveLookIn.stop = function () {};
        }


        /* do this until a better solution for tracking is decided */
        trackingPoller.start();

    function handleSelectDate (date) {

      var lookupDate = floorDate('day', date, true),
          lookupTime = lookupDate.getTime(),
          isPast     = (lookupTime < todayTime),
          isToday    = (lookupTime === todayTime);

      GameDataProvider.load(lookupDate);

      // jferrer: for safari extension fun
      if( params.lli && params.lli === "1" ) {
          LiveLookIn.load();
      }
      LiveLookIn.start();

      GameAlertsDataProvider.load();
      gameAlertsDataPoller.start();

      if (isToday) {
        
        gameDataPoller.start();
        
        CountdownApp.show();
        countdownPoller.start();
        
      } else {
        
        gameDataPoller.stop();

        CountdownApp.hide();
        countdownPoller.stop();
      }

      if (!isPast) {
        TicketingDataProvider.load(lookupDate);
        ticketingDataPoller.start();
      } else {
        ticketingDataPoller.stop();
      }
    }
    
    var CalendarCarousel = bam.widget.CalendarCarousel;

    /**
     * Bind component events
     */
    GameDataProvider
      .bind('onBeforeSend', function (e, type) {
        if (type === 'load') {
          CalendarCarousel.disable();
        }
      })
      .bind('onBeforeSend', CountdownApp.showLoading)
      .bind('onSuccess', eventProxy(ScoreboardApp.updateGameCache, ScoreboardApp))
      .bind('onError', function (evt, date) {

        $('#sbBoxscores').html(
          $('<div/>')
            .addClass('noGames')
            .text('There are no games scheduled for this date.')
        );
      })
      .bind('onEmpty', function (evt, date) {

        date = datetime.formatDate(new Date(date), 'MMMM d, yyyy');

        $('#sbBoxscores').html(
          $('<div/>')
            .addClass('noGames')
            .text('There are no games scheduled for ' + date + '.')
        );
      })
      .bind('onComplete', CalendarCarousel.enable)
      .bind('onComplete', CountdownApp.hideLoading);

    TicketingDataProvider
      .bind('onSuccess', eventProxy(ScoreboardApp.updateTicketingCache, ScoreboardApp))
      .bind('onError', proxy(ticketingDataPoller.stop, ticketingDataPoller));
      
    CalendarCarousel
      .bind('onThreeDayClick', function () {
        ScoreboardTracking.trackCalendarActivity('Three Day Click');
      })
      .bind('onLeftArrowClick', function () {
        ScoreboardTracking.trackCalendarActivity('Left Arrow Click');
      })
      .bind('onRightArrowClick', function () {
        ScoreboardTracking.trackCalendarActivity('Right Arrow Click');
      })
      .bind('onSelectDate', function () {
        ScoreboardTracking.trackCalendarActivity('Select Date Click');
      })
      .bind('onSelectToday', function () {
        ScoreboardTracking.trackCalendarActivity('Today\'s Game Click');
      })
      .bind('onChange', ScoreboardTracking.trackUserRefresh)
      .bind('onChange', eventProxy(handleSelectDate));

    LiveLookIn
      .bind('onSuccess', eventProxy(ScoreboardApp.updateLiveLookInCache, ScoreboardApp))
      .bind('onSuccess', eventProxy(ScoreboardTickerApp.displayLiveLookIn, ScoreboardTickerApp))
      .bind('onEmpty', eventProxy(ScoreboardApp.clearLiveLookInCache, ScoreboardApp))
      .bind('onEmpty', eventProxy(ScoreboardTickerApp.displayAlerts, ScoreboardTickerApp));
    
    ScoreboardTickerApp
      .bind('tickerMode:liveLookIn', proxy(gameAlertsDataPoller.stop, gameAlertsDataPoller))
      .bind('tickerMode:alerts', proxy(gameAlertsDataPoller.start, gameAlertsDataPoller));

    GameAlertsDataProvider
      .bind('onSuccess', eventProxy(ScoreboardTickerApp.displayAlerts, ScoreboardTickerApp));


    /**
     * Initialize components
     */
    ScoreboardApp.init('sbBoxscores');




    // Lay-Zee-Mee
    // @todo formalize into a widget
    var SELECTED = 'selected';
    
    var sbViews = $('#sbViewToggle a.sbView').live('click', function (evt) {

      evt.preventDefault();

      var elem = $(this),
          tmpl,
          view;
          
      if (!elem.hasClass(SELECTED)) {
        
        // Update toggle display
        sbViews.not(this).removeClass(SELECTED);
        elem.addClass(SELECTED);

        tmpl = this.href;
        view = this.title;

        // @todo move to event
        // Update scoreboard view
        ScoreboardApp.toggleView(tmpl, view);

        // Store user selection for subsequent page loads
        bam.cookies.set({
          name:  'sbView',
          value: view
        });

        // @todo move to event
        ScoreboardTracking.trackToggleView(view);
      }

    });

    /**
     * Set initial state of view toggle and init app
     */
    var view = bam.cookies.get('sbView') || 0,
        elem,
        tmpl;

    // View is stored by name
    if (isNaN(view)) {
      elem = sbViews.filter('[title=' + view + ']')[0];
      
    // View is stored by index or not yet stored (use first element)
    } else {
      elem = sbViews[view];
      view = elem.title;

      // Set or overwrite stored value with view name
      bam.cookies.set({
        name:  'sbView',
        value: view
      });
    }

    tmpl = elem.href;

    ScoreboardApp.setView(tmpl);
    $(elem).addClass(SELECTED);
    // So-So-Lay-Zee-Mee!
    
    
    
    
    
    CalendarCarousel.init('sbCalendarCarousel', lookupDate);
    CountdownApp.init('sbCountdown');        

    /**
     * Hack for ad
     */
    var start   = '08/20/2010', // first day of campaign
        end     = '10/01/2010', // day after last day of campaign
        current = datetime.getDateByOffset(datetime.timezoneOffset, new Date()).getTime(),
        running = current >= (new Date(start)).getTime() && current < (new Date(end)).getTime(),
        lliCfg  = {};

    if (running) {
      
      // Style Live Look-In zPlayer
      lliCfg.zPlayer = {
        containerId: 'LiveLookinPlayer782x610AdTopCenter', 
				bgImageUrl : '/shared/images/bam/zPlayer/bg_att.png',
				zoomEndDims: { w:'782px', h:'610px' },
				companionAd: { show:true, w:728, h:90 }
  		};
  		
      // Style ticker
      $('#sbTicker').addClass('sponsoredByATT');
    }
    // End hack
    
    LiveLookIn.init(lliCfg);
    
    ScoreboardTickerApp.init('sbTicker');
    
    /**
     * Fire it up!
     */
    handleSelectDate(lookupDate);

  });

})(this, this.document, this.jQuery, this.bam);

// Refreshable <iframe> ad
(function (window, document, $) {

  $.fn.dcRefreshableIFrame = function (cfg) {

    if (this.length === 0 || window.location.protocol === 'https:') {
      return this;
    }

    function rnd () {
      return Math.round(Math.random() * 10000000000000000);
    }

    cfg = $.extend({
      club:    window.club,
      lang:    window.dc_lang,
      section: window.section,
      keyVal:  window.dc_keyVal,
      ord:     rnd(),
      domains: window.c_domain
    }, $.fn.dcRefreshableIFrame.defaults, cfg);

    var club = cfg.club || 'mlb',
        lang = cfg.lang || 'en',
        size = cfg.width + 'x' + cfg.height,
        site = (lang !== 'en' ? lang + '.' : '') + cfg.domains[club] + '.mlb',
        url  = 'http://ad.doubleclick.net/adi/' + site + '/' + [
          cfg.section || 'empty', cfg.keyVal || '', 'sz=' + size, 'ord=' + cfg.ord
        ].join(';'),

        iframes = this.map(function () {

          // Increment global DC counters for each ad
          var pos  = window.dc_tiles[size] = ~~window.dc_tiles[size] + 1,
              tile = ++window.dc_numads;

          return $(cfg.iframe).addClass(cfg.className).attr({
            src:    [url, 'pos=' + pos, 'tile=' + tile].join(';'),
            width:  cfg.width,
            height: cfg.height
          }).appendTo(this);
          
        });

    if (cfg.interval) {
      setInterval(function () {
        iframes.each(function () {
          var elem = $(this);
          elem.attr('src', elem.attr('src').replace(/;ord=(\d+)/, ';ord=' + rnd()));
        });
      }, cfg.interval * 1000);
    }
    
    return this;
  };

  $.fn.dcRefreshableIFrame.defaults = {
    interval:   30,
    width:      728,
    height:     90,
    className:  'refreshable',
    iframe:     '<iframe frameborder="no" border="0" marginwidth="0" marginheight="0" scrolling="no"/>'
  };

})(this, this.document, this.jQuery);

