//  HULU CONFIDENTIAL MATERIAL. DO NOT DISTRIBUTE.
//  Copyright (C) 2009-2010 Hulu, LLC
//  All Rights Reserved
/**
 * htvBeacons.js 
 * 
 * Contains track methods to interact with Beacons backend.
 */
var $htv;
/*jslint maxerr: 1000, nomen: false, evil: false, immed: true, plusplus: false */
/*global LOG, describe, escape, _trackStandardBeacon, _generateGUID, _generateCacheBuster */
/*global _generateQueryString, _onConfigResponse, _onConfigError, _parseConfigNode, _trackBeacon */
/*global _trackBeaconResult, _trackComScoreBeacon, _trackNielsenBeacon, _trackRealtimeBeacon */
/*global _auditCallback, _trackChapterStart, _trackChapterEnd, _trackPlayerError */
/*global _trackPlaybackStart, _trackPlaybackPosition, _trackPlaybackEnd */

$htv.Beacons = (function () {
    /*jslint onevar: false */
    var CONFIG_SERVICE_PATH = "/config/v3/config";
    var REALTIME_SERVICE_PATH = "/beacon/v3";
    var STANDARD_SERVICE_PATH = "/v3";

    var PRODUCTION_CONFIG_HOST = "http://t.hulu.com";
    var PRODUCTION_REALTIME_HOST = "http://t.hulu.com";
    var PRODUCTION_STANDARD_HOST = "http://t2.hulu.com";
    var PRODUCTION_TWILIGHT_STANDARD_HOST = "http://tw.i.hulu.com";
    
    var QA_CONFIG_HOST = "http://track.qa.hulu.com";
    var QA_REALTIME_HOST = "http://track.qa.hulu.com";
    var QA_STANDARD_HOST = "http://t2.qa.hulu.com";
    var QA_TWILIGHT_STANDARD_HOST = "http://tw.i.hulu.com";
    
    var COMSCORE_HOST = "http://b.scorecardresearch.com/b";
    var COMSCORE_C1 = "1"; //comScore Id
    var COMSCORE_C2 = "3000007"; //Hulu Id
    var COMSCORE_ADS_C5_PREFIX = "01";
    
    var NIELSEN_HOST = "http://secure-us.imrworldwide.com/cgi-bin/m";
    var NIELSEN_PRODUCTION_CLIENT_ID = "us-600346";
    var NIELSEN_QA_CLIENT_ID = "us-502202";
    
    var IAG_HOST = "http://pt.rewardtv.com/otif.do";
    var IAG_S_ID = "1000046";
    var IAG_CONTENT_TF_ID = "301";
    var IAG_AD_TF_ID = "303";
    var IAG_BCR_ID = "Hulu.com";
    var IAG_SEGMENT_ID = "1";
    var IAG_CONTENT_POSITION_ID = "soc";
    
    var DEFAULT_CONFIG_FILE = "<beacons> <realtime host=\"t.hulu.com\"> <beacon type=\"error\" send=\"always\" cdn-specific=\"true\"> <event name=\"applicationerror.appname\" send=\"never\" />" +
        "<event name=\"connectionerror.loadtimeout\" send=\"never\" /> </beacon> <beacon type=\"session\" send=\"always\" cdn-specific=\"true\"> </beacon> <beacon type=\"playback\" send=\"never\" cdn-specific=\"true\">" +
        "<event name=\"start\" send=\"always\"> </event> </beacon> <cdn-hosts> <cdn name=\"akamai\" host=\"t.hulu.com\"> </cdn> <cdn name=\"level3\" host=\"t-l3.hulu.com\"> </cdn> <cdn name=\"limelight\" host=\"t.hulu.com\"> </cdn> </cdn-hosts>" +
        "</realtime> <standard host=\"t2.hulu.com\"> <beacon type=\"dataload\" send=\"onerror\" /> <beacon type=\"playback\" send=\"always\"> <event name=\"connectionerror\" send=\"never\" /> <event name=\"connectionchange\" send=\"never\" />" +
        "<event name=\"netstreamerror\" send=\"never\" /> <event name=\"datastreamerror\" send=\"never\" /> <event name=\"applicationerror\" send=\"never\" /> <event name=\"prerollstart\" send=\"never\" /> <event name=\"prerollposition\" send=\"never\" />" +
        "<event name=\"prerollend\" send=\"never\" /> </beacon> <beacon type=\"revenue\" send=\"always\"> <event name=\"request\" send=\"onerror\" /> <event name=\"response\" send=\"onerror\" /> <event name=\"httpstreamerror\" send=\"onerror\" /> <event name=\"request\" send=\"onerror\" /> <event name=\"request\" send=\"onerror\" /> </beacon> <beacon type=\"abortedsession\" send=\"never\" /> </standard> </beacons>";
    
    var USE_STAGING = false;
    
    var _cdnHosts,
        _commonParams,
        _configHost,
        _extraMetadata,
        _queuedBeacons,
        _ready,
        _realtimeConfig,
        _realtimeHost,
        _standardConfig,
        _needtoUpdateResumeTime,
        _watchedSinceLastPlaybackPositionBeacon,
        _lastPlaybackPositionBeaconTime,
        _playlistReceived,
        _videoMetadataReceived,
        _playbackStarted,
        _playbackStartRecorded,
        _standardHost,
        _chapters,
        _playerState,
        _resumingFromAdBreak,
        _lastChaptersSeen,
        _playbackPositionTimer,
        _firstAdPlayed,
        _lastVisitTime;
    /*jslint onevar: true */
    _commonParams = {};
    _cdnHosts = {};
    _extraMetadata = {};
    _queuedBeacons = [];
    _ready = false;
    _needtoUpdateResumeTime = false;
    _lastPlaybackPositionBeaconTime = 0;
    _watchedSinceLastPlaybackPositionBeacon = 0;
    _playlistReceived = false;
    _videoMetadataReceived = false;
    _playbackStarted = false;
    _playbackStartRecorded = false;
    _realtimeConfig = {};
    _standardConfig = {};
    _chapters = [];
    _playerState = null;
    _lastChaptersSeen = [];
    _resumingFromAdBreak = false;
    _playbackPositionTimer = $htv.Platform.createTimer(0, null, null);
    _firstAdPlayed = false; 
    _lastVisitTime = 0;
    
    function _handleEvent(event_name, eventData) {
        var i, lastSeenAdBreakTime = -1, adBreakPositions, timeProgress,
             enteringChapter, exitingChapter, playlist, startTime, curTime, endTime, authIndex, afterAuthIndex;
        switch (event_name) {
        case "PLAYER_METADATA_RECEIVED":
            _commonParams.cpidentifier = eventData.metadata.cp_identifier;
            _commonParams.pid = eventData.metadata.pid;
            _commonParams.contentid = eventData.metadata.content_id;
            _extraMetadata.episodeNumber = eventData.metadata.episode_number;
            _extraMetadata.seasonNumber = eventData.metadata.season_number;
            _extraMetadata.title = eventData.metadata.title;
            _videoMetadataReceived = true;
            _trackPlaybackStart();
            break;
        case "PLAYER_PLAYBACK_FINISHED":
            _playbackPositionTimer.stop();
            
            exitingChapter = -1;
            for (i = 0; i < _chapters.length; i++) {
                if (_commonParams.position >= _chapters[i].start && _commonParams.position <= _chapters[i].end) {
                    exitingChapter = i;
                    break;
                }
            }
            if (_chapters.length > 0 && exitingChapter >= 0) {
                if (_chapters[exitingChapter].start_beaconed === true && _chapters[exitingChapter].end_beaconed === false) {
                    _trackChapterEnd(exitingChapter, _chapters.length);
                }
            }
            break;
        case "PLAYER_PLAYBACK_REQUESTED":
            // update the beacons "visit" parameter if necessary
            curTime = (new Date()).getTime();
            if (curTime - _lastVisitTime >= 14400000) {
                _commonParams.visit += 1;
                $htv.Platform.writeLocalData("beacons", "visit", _commonParams.visit);
            }
            _lastVisitTime = curTime;
            $htv.Platform.writeLocalData("beacons", "lastVisitTime", _lastVisitTime);
            
            _commonParams.sessionGuid = _generateGUID();
            _commonParams.packageid = 0; // overwritten when playlist received
            _commonParams.position = 0;
            _lastPlaybackPositionBeaconTime = 0;
            _watchedSinceLastPlaybackPositionBeacon = 0;
            _playlistReceived = false;
            _videoMetadataReceived = false;
            _playbackStarted = false;
            _playbackStartRecorded = false;
            _firstAdPlayed = false;
            break;
        case "PLAYER_ADBREAK_PLAYING":
            // clear chaptersseen 
            _lastChaptersSeen = [];
            if (_firstAdPlayed === false && _playbackStarted === true) {
                _firstAdPlayed = true;
                // when the first ad after content playback begins is fired, switch from every 15 sec to 3 min
                
                _playbackPositionTimer.setInterval(180000);
            }            
            break;
        case "PLAYER_PLAYLIST_RECEIVED":
            playlist = eventData.playlist;
            _commonParams.packageid = playlist.package_id;
            _extraMetadata.dpNielsenId = playlist.tp_distributionPartnerNielsenId;
            _extraMetadata.primarySiteChannelNielsenChannelId = playlist.tp_primarySiteChannelNielsenChannelId;
            _extraMetadata.secondarySiteChannelNielsenId = playlist.tp_secondarySiteChannelNielsenId;
            _extraMetadata.siteChannelComscoreId = playlist.tp_siteChannelComScoreId;
            _extraMetadata.adModel = playlist.tp_Ad_Model;
            _extraMetadata.cpComscoreId = playlist.tp_comScoreId;
            _extraMetadata.dpComscoreId = playlist.tp_distributionPartnerComScoreId;
            _extraMetadata.seriesTitle = playlist.tp_Series_Title;
            _extraMetadata.shouldSendIAGBeacon = playlist.iag_send_beacon;
            _extraMetadata.isWebContent = playlist.tp_isWeb;
            _extraMetadata.streamUrl = playlist.stream_url;
            
            // Strip out auth token from stream urls
            if (!$htv.Utils.stringIsNullOrEmpty(_extraMetadata.streamUrl)) {
                authIndex = _extraMetadata.streamUrl.indexOf("&auth=");
                if (authIndex !== -1) {
                    afterAuthIndex = _extraMetadata.streamUrl.indexOf("&", authIndex + 1);
                    if (afterAuthIndex === -1) {
                        // if auth token is last qs param, truncate there
                        _extraMetadata.streamUrl = _extraMetadata.streamUrl.substring(0, authIndex);
                    } else {
                        // if more qs params, tack those on after 
                        _extraMetadata.streamUrl = _extraMetadata.streamUrl.substring(0, authIndex) + _extraMetadata.streamUrl.substring(afterAuthIndex);
                    }
                }
            }
            
            _lastPlaybackPositionBeaconTime = eventData.resumePosition;
            _commonParams.position = eventData.resumePosition;
            _needtoUpdateResumeTime = true;
            
            // TODO: I think it belongs here..
            adBreakPositions = [];
            _chapters = [];
            _lastChaptersSeen = [];
            for (i = 0; i < playlist.breaks.length; i++) {
                lastSeenAdBreakTime = playlist.breaks[i].pos;
                if (adBreakPositions.length === 0 || lastSeenAdBreakTime !== adBreakPositions[adBreakPositions.length - 1]) {
                    adBreakPositions.push(lastSeenAdBreakTime);
                }
            }
            for (i = 0; i < adBreakPositions.length - 1; i++) {
                _chapters.push({
                    start: adBreakPositions[i],
                    end: adBreakPositions[i + 1],
                    start_beaconed: false,
                    end_beaconed: false
                });
                describe(_chapters[i]);
            }
            
            if (adBreakPositions.length === 0) {
                startTime = 0;
            } else {
                startTime = adBreakPositions[adBreakPositions.length - 1];
            }
            
            _chapters.push({
                start: startTime,
                end: playlist.duration,
                start_beaconed: false,
                end_beaconed: false
            });
            
            if (_chapters[0].start !== 0) { // in case there was no pos = 0 break
                
                _chapters.unshift({
                    start: 0,
                    end: _chapters[0].start,
                    start_beaconed: false,
                    end_beaconed: false
                });
            }
            
            _playlistReceived = true;
            _trackPlaybackStart();
            break;
        case "PLAYER_TIME_PROGRESS":
            timeProgress = eventData.milliseconds;
            
            // like with seeks, resume location may be wrong, but should be within 20 seconds of the intended destination
            // in the best case, the seek is complete, within 20 seconds, so both checks are true
            // in the worst case, when the resume location is more than 20 seconds off but the latter constraint eventually becomes true at an unexpected time (which should not happen)
            // _commonParms.position and the next beacon's watched value will be 20 seconds off (at most)
            if (_needtoUpdateResumeTime === true && Math.abs(timeProgress - _commonParams.position) < 20000) {
                
                _lastPlaybackPositionBeaconTime = timeProgress;
                _commonParams.position = timeProgress;
                _needtoUpdateResumeTime = false;
            }
            
            // check where our previous time progress was
            exitingChapter = -1;
            for (i = 0; i < _chapters.length; i++) {
                if (_commonParams.position >= _chapters[i].start && _commonParams.position <= _chapters[i].end) {
                    exitingChapter = i;
                    break;
                }
            }
            // check where our current time progress is
            enteringChapter = -1;
            for (i = 0; i < _chapters.length; i++) {
                if (timeProgress >= _chapters[i].start && timeProgress <= _chapters[i].end) {
                    enteringChapter = i;
                }
            }
            _lastChaptersSeen.push(enteringChapter);
            if (_lastChaptersSeen.length > 3) {
                _lastChaptersSeen.shift();
            }
            if (_lastChaptersSeen[0] === _lastChaptersSeen[1] &&
                _lastChaptersSeen[1] === _lastChaptersSeen[2]) {
                _commonParams.position = timeProgress;
                if (_chapters.length > 0 && exitingChapter !== -1 && enteringChapter !== -1 && 
                    _chapters[exitingChapter] && _chapters[enteringChapter]) {
                    // if we have changed chapters and we haven't beaconed end, beacon it
                    // only if we beaconed start though.
                    if (exitingChapter !== enteringChapter && _chapters[exitingChapter].end_beaconed === false && _chapters[exitingChapter].start_beaconed === true) {
                        _trackChapterEnd(exitingChapter, _chapters.length);
                    }
                    // beacon start if we haven't already.
                    if (_resumingFromAdBreak === true || _chapters[enteringChapter].start_beaconed === false) {
                        _trackChapterStart(enteringChapter, _chapters.length);
                    }
                    _resumingFromAdBreak = false;
                }
            } else {
                
            }
            
            break;
        case "PLAYER_STATE_CHANGED":
            
            if (eventData.state === "BREAK" && _playerState === "VIDEO") {
                
                for (i = 0; i < _chapters.length; i++) {
                    if (_commonParams.position >= _chapters[i].start && _commonParams.position <= _chapters[i].end) {
                        
                        if (_chapters[i].end_beaconed === false && _chapters[i].start_beaconed === true) {
                            // first time progress within this chapter
                            // check for whether or not we're transitioning to the break at the beginning of this chapter,
                            // in which chase we won't do chapterend.
                            
                            if (eventData.breakpos !== _chapters[i].start) {
                                _trackChapterEnd(i, _chapters.length);
                            }
                        }
                        break;
                    }
                }
            }
            if (eventData.state === "BREAK" && _playerState === "VIDEO") {
                _resumingFromAdBreak = true;
            }
            _playerState = eventData.state;
            break;
        case "PROFILE_STATE_CHANGED":
            _commonParams.planid = eventData.state.plan_id;
            _commonParams.userid = eventData.state.userid;
            _commonParams.username = eventData.state.username;
            
            break;
        case "PLAYER_SEEK_REQUESTED":
            _watchedSinceLastPlaybackPositionBeacon += (eventData.current_position - _lastPlaybackPositionBeaconTime);
            
            _lastPlaybackPositionBeaconTime = eventData.expected_destination;
            _commonParams.position = eventData.expected_destination;
            break;
        case "PLAYER_SEEK_COMPLETED":
            if (Math.abs(eventData.actual_destination - eventData.expected_destination) < 20000) {
                
                _lastPlaybackPositionBeaconTime = eventData.actual_destination;
                _commonParams.position = eventData.actual_destination;    
            } 
            else {
                
            }
            break;
        case "PLAYER_CONNECTION_ERROR":
            
            _trackPlayerError(event_name, (eventData && eventData.message ? {message: eventData.message} : {}));
            break;
        case "PLAYER_RENDERING_ERROR":
            
            _trackPlayerError(event_name, (eventData && eventData.message ? {message: eventData.message} : {}));
            break;
        case "PLAYER_PLAYBACK_ERROR":
            
            _trackPlayerError(event_name, (eventData && eventData.message ? {message: eventData.message} : {}));
            break;
        case "PLAYER_STREAM_NOT_FOUND":
            
            _trackPlayerError(event_name, (eventData && eventData.message ? {message: eventData.message} : {}));
            break;
        default: 
            
            break;
        }
    }
    
    function _initialize() {
        _commonParams = {
            // Hard-coded params
            cdn: "akamai",
            computerGuid: $htv.Platform.readLocalData("beacons", "comp_guid"), // checked below
            playermode: "fullscreen",   
            
            // Set constant platform params
            client: $htv.Platform.properties.client,
            deviceid: $htv.Platform.properties.deviceid,
            distro: $htv.Platform.properties.distro,
            distroplatform: $htv.Platform.properties.distroplatform,
            os: $htv.Platform.properties.os,
            player: $htv.Platform.properties.marketing_version,
            userbandwidth: $htv.Platform.properties.userbandwidth,
            
            // Initialize dynamic params
            contentid: 0,
            cpidentifier: "",
            packageid: 0,
            pid: "",
            planid: 0,
            position: 0,
            sessionGuid: _generateGUID(),
            userid: 0, 
            username: "",
            visit: 1
        };
        _extraMetadata = {
            dpNielsenId: 0,
            episodeNumber: 0,
            primarySiteChannelNielsenChannelId: "",
            seasonNumber: 0,
            secondarySiteChannelNielsenId: "",
            title: ""
        };
        
        var prevTime, prevVisit, curTime, loadedComputerGuid, configUrl;
        
        // Check if comp guid needed
        loadedComputerGuid = _commonParams.computerGuid;
        if ($htv.Utils.stringIsNullOrEmpty(loadedComputerGuid)) {
            _commonParams.computerGuid = _generateGUID();
            $htv.Platform.writeLocalData("beacons", "comp_guid", _commonParams.computerGuid);
            
        }
        
        if (USE_STAGING) {
            _configHost = QA_CONFIG_HOST;
            _realtimeHost = QA_REALTIME_HOST;
            _standardHost = QA_STANDARD_HOST;
        }
        else {
            _configHost = PRODUCTION_CONFIG_HOST;
            _realtimeHost = PRODUCTION_REALTIME_HOST;
            _standardHost = PRODUCTION_STANDARD_HOST;
        }
        
        // set initial visit value
        prevTime = $htv.Platform.readLocalData("beacons", "lastVisitTime");
        prevVisit = $htv.Platform.readLocalData("beacons", "visit");
        curTime = (new Date()).getTime();
        
        if (!prevTime || !prevVisit) {
            // first time? if computerguid exists -> start at visit=2 
            _commonParams.visit = $htv.Utils.stringIsNullOrEmpty(loadedComputerGuid) ? 1 : 2;
            $htv.Platform.writeLocalData("beacons", "visit", _commonParams.visit);
            
            _lastVisitTime = curTime;
            $htv.Platform.writeLocalData("beacons", "lastVisitTime", _lastVisitTime);
        } else {
            _commonParams.visit = prevVisit;
            _lastVisitTime = prevTime;
        }
        
        $htv.Controller.addEventListener("PLAYER_METADATA_RECEIVED", this);
        $htv.Controller.addEventListener("PLAYER_PLAYBACK_FINISHED", this);
        $htv.Controller.addEventListener("PLAYER_PLAYBACK_REQUESTED", this);
        $htv.Controller.addEventListener("PLAYER_PLAYLIST_RECEIVED", this);
        $htv.Controller.addEventListener("PLAYER_ADBREAK_PLAYING", this);
        $htv.Controller.addEventListener("PLAYER_TIME_PROGRESS", this);
        $htv.Controller.addEventListener("PLAYER_STATE_CHANGED", this);
        $htv.Controller.addEventListener("PLAYER_SEEK_REQUESTED", this);
        $htv.Controller.addEventListener("PLAYER_SEEK_COMPLETED", this);
        $htv.Controller.addEventListener("PROFILE_STATE_CHANGED", this);
        $htv.Controller.addEventListener("PLAYER_CONNECTION_ERROR", this);
        $htv.Controller.addEventListener("PLAYER_RENDERING_ERROR", this);
        $htv.Controller.addEventListener("PLAYER_PLAYBACK_ERROR", this);
        $htv.Controller.addEventListener("PLAYER_STREAM_NOT_FOUND", this);
        
        // Request config from server
        // TODO: add error case, since no beacons are sent if config fails
        configUrl = _configHost +
            CONFIG_SERVICE_PATH + "?" +
            _generateCacheBuster() +
            _generateQueryString({
                distro: _commonParams.distro,
                distroplatform: _commonParams.distroplatform
            });

        
        $htv.Platform.loadURL(configUrl, this, _onConfigResponse, { xmlparse: true, errorCallback: _onConfigError });
    }
    
    function _onConfigError(status, errorMsg) {
        
        _onConfigResponse($htv.Platform.stringToXMLDoc(DEFAULT_CONFIG_FILE), {});
    }
    
    function _onConfigResponse(responseData, options) {
        var i, realtimeHost, realtimeNode, cdnNode, cdnNodes, standardNode, standardHost;
        // Get realtime host
        realtimeNode = $htv.Platform.getXPathResultAt($htv.Platform.evaluateXPath(responseData, "beacons/realtime"), 0);
        realtimeHost = $htv.Platform.evaluateXPathAttributeString(realtimeNode, "host");
        if (realtimeHost && realtimeHost.length > 0) {
            _realtimeHost = "http://" + realtimeHost;
        }
        
        // Get realtime CDN hosts
        cdnNodes = $htv.Platform.evaluateXPath(realtimeNode, "cdn-hosts/cdn");
        for (i = 0; i < cdnNodes.length; i++) {
            cdnNode = $htv.Platform.getXPathResultAt(cdnNodes, i);
            
            _cdnHosts[$htv.Platform.evaluateXPathAttributeString(cdnNode, "name")] =
                "http://" + $htv.Platform.evaluateXPathAttributeString(cdnNode, "host");
        }
        
        // Process realtime config
        _parseConfigNode(realtimeNode, _realtimeConfig);
        
        // Get standard host
        standardNode = $htv.Platform.getXPathResultAt($htv.Platform.evaluateXPath(responseData, "beacons/standard"), 0);
        standardHost = $htv.Platform.evaluateXPathAttributeString(standardNode, "host");
        if (standardHost && standardHost.length > 0) {
            _standardHost = "http://" + standardHost;
        }
        
        // Process standard config
        _parseConfigNode(standardNode, _standardConfig);
        
        // Allow requests out and kick off the queued beacons
        _ready = true;
        while (_queuedBeacons.length > 0) {
            // NOTE: do host and shouldsend checks in _trackBeacon
            _trackBeacon.apply(this, _queuedBeacons.shift());
        }
    }
    
    function _onPlaybackPositionTimerFired() {
        
        _trackPlaybackPosition(!$htv.Player.isPaused());
    }
    
    // Helper function to parse the beacon config, since realtime and standard are parsed the same way
    function _parseConfigNode(beaconsArrayNode, destinationConfig) {
        var beaconNodes, beaconNode, beaconType, eventNodes, eventNode, eventName, i, j;
        
        beaconNodes = $htv.Platform.evaluateXPath(beaconsArrayNode, "beacon");
        for (i = 0; i < beaconNodes.length; i++) {
            beaconNode = $htv.Platform.getXPathResultAt(beaconNodes, i);
            beaconType = $htv.Platform.evaluateXPathAttributeString(beaconNode, "type");
            
            destinationConfig[beaconType] = {
                send: $htv.Platform.evaluateXPathAttributeString(beaconNode, "send"),
                cdn_specific: $htv.Platform.evaluateXPathAttributeString(beaconNode, "cdn-specific"),
                events: {}
            };
            
            eventNodes = $htv.Platform.evaluateXPath(beaconNode, "event");
            for (j = 0; j < eventNodes.length; j++) {
                eventNode = $htv.Platform.getXPathResultAt(eventNodes, j);
                eventName = $htv.Platform.evaluateXPathAttributeString(eventNode, "name");
                
                destinationConfig[beaconType].events[eventName] = {
                    send: $htv.Platform.evaluateXPathAttributeString(eventNode, "send"),
                    cdn_specific: $htv.Platform.evaluateXPathAttributeString(eventNode, "cdn-specific")
                };
            }
        }
    }
    
    function _generateCacheBuster(cbKey) {
        if (cbKey === undefined) {
            cbKey = "cb";
        }
        return cbKey + "=" + Math.random().toString().substring(2, 10);
    }
    
    function _generateCommonParams(skipEscape, toLowerKeys) {
        return _generateQueryString(_commonParams, skipEscape, toLowerKeys);
    }
    
    /*jslint bitwise: false */
    function _generateGUID() {
        var helper = function () {
            return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
        };
        return helper() + helper() + "-" +
            helper() + "-" +
            helper() + "-" +
            helper() + "-" +
            helper() + helper() + helper();
    }
    /*jslint bitwise: true */
    function _generateQueryString(params, skipEscape, toLowerKeys) {
        var key, str = "";
        for (key in params) {
            if (params[key] !== null && params[key] !== undefined) {
                str = str + 
                    "&" + 
                    (toLowerKeys === true ? key.toString().toLowerCase() : key) + 
                    "=" + 
                    (skipEscape === true ? params[key] : escape(params[key]));
            }
        }
        return str;
    }
    
    function _getComputerGUID() {
        return _commonParams.computerGuid;
    }
    
    function _getHostUrl(type, event, isRealtime, isTwilight) {
        var host, hostSuffix, defaultHost, configObject;
        host = isRealtime ? _realtimeHost : _standardHost;
        hostSuffix = isRealtime ? REALTIME_SERVICE_PATH : STANDARD_SERVICE_PATH;
        
        if (isTwilight) {
            host = USE_STAGING ? QA_TWILIGHT_STANDARD_HOST : PRODUCTION_TWILIGHT_STANDARD_HOST;
        }
        else {
            // Start with default host
            defaultHost = host;
            configObject = isRealtime ? _realtimeConfig : _standardConfig;
            if (configObject[type]) {
                // Use cdn host if cdn-specific attribute is true
                if (!$htv.Utils.stringIsNullOrEmpty(configObject[type].cdn_specific)) {
                    host = configObject[type].cdn_specific.toLowerCase() === "true" ?
                        _cdnHosts[_commonParams.cdn] : defaultHost;
                }
                
                // If child event node exists and has cdn-specific attribute, override parent beacon node
                if (configObject[type].events[event] &&
                    !$htv.Utils.stringIsNullOrEmpty(configObject[type].events[event].cdn_specific)) {
                    host = configObject[type].events[event].cdn_specific.toLowerCase() === "true" ?
                        _cdnHosts[_commonParams.cdn] : defaultHost;
                }
            }
        }
        return host + hostSuffix;
    }
    /*jslint onevar: false */
    var INT_TO_CHAR = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 
                       'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'];
    /*jslint onevar: true */
    function _getPodSlotValue(pod, numSlots, slot) {
        return pod + "_" + numSlots + "_" + INT_TO_CHAR[slot];
    }
    
    function _shouldSendBeacon(type, event, isRealtime, isError) {
        var configObject, shouldSend;
        configObject = isRealtime ? _realtimeConfig : _standardConfig;
        shouldSend = true;
        
        // If beacon type is included in config, use send value
        if (configObject[type]) {
            shouldSend = (configObject[type].send === "always" ||
            (isError && configObject[type].send === "onerror"));
            
            // If event name is included in config, override the beacon's config
            if (configObject[type].events[event]) {
                shouldSend = (configObject[type].events[event].send === "always" ||
                (isError && configObject[type].events[event].send === "onerror"));
            }
        }
        
        return shouldSend;
    }
    
    // audit urls just need to be hit, and don't need any action or parameters
    function _trackAuditURL(url) {
        $htv.Platform.loadURL(url, this, _auditCallback, {
            xmlparse: false
        });
    }
    
    function _auditCallback() {
        
    }
    
    // Performs the actual check and send
    function _trackBeacon(type, event, queryString, isRealtime, isTwilight, isError, onCompleteCallback) {
        if (_shouldSendBeacon(type, event, isRealtime, isError)) {
            var url = _getHostUrl(type, event, isRealtime, isTwilight) + "/";
            if (isRealtime) {
                url += type + "?event=" + event + "&" + queryString;
            } else {
                url += type + "/" + event + "?" + queryString;
            }
            
            if (onCompleteCallback === null || onCompleteCallback === undefined) {
                onCompleteCallback = _trackBeaconResult;
            }
            
            
            $htv.Platform.loadURL(url, this, onCompleteCallback, {
                xmlparse: false,
                errorCallback: onCompleteCallback
            });
        } else {
            
        }
    }
    
    function _trackBeaconResult(responseData, options) {
        
    }
    
    function _trackChapterEnd(contentChapterIndex, contentChapterCount) {
        _trackNielsenBeacon("2");
        
        _chapters[contentChapterIndex].end_beaconed = true;
    }
    
    function _trackChapterStart(contentChapterIndex, contentChapterCount) {
        _playbackStarted = true;
        
        _playbackPositionTimer.setCallback(this, _onPlaybackPositionTimerFired);
        _playbackPositionTimer.setInterval(15000);
        _playbackPositionTimer.start();
        
        _trackPlaybackStart();
        _trackNielsenBeacon("0");
        // todo: we could call this from internally as well.
        // TODO: contentChapterIndex and contentChapterCount are not being passed correctly from player 
        _trackComScoreBeacon(false, contentChapterIndex + 1, contentChapterCount);
        // set flag that we've beaconed
        
        _chapters[contentChapterIndex].start_beaconed = true;
    }
    
    function _trackComScoreBeacon(forAd, contentChapterIndex, contentChapterCount) {
        // First compute special comscore channel param
        // siteChannel is <genre><subgenre>, just need to figure out what <type> is.
        var i, siteChannel, adModel, comscoreChannel, params, url;
        siteChannel = _extraMetadata.siteChannelComscoreId;
        adModel = _extraMetadata.adModel;
        comscoreChannel = "";
        if (!$htv.Utils.stringIsNullOrEmpty(siteChannel)) {
            if (forAd) {
                comscoreChannel = COMSCORE_ADS_C5_PREFIX + siteChannel;
            } else {
                if (_extraMetadata.isWebContent) {
                    if (adModel !== null && adModel.toLowerCase() === "shortform") {
                        comscoreChannel = "04" + siteChannel;
                    } else {
                        comscoreChannel = "05" + siteChannel;
                    }
                } else {
                    if (adModel !== null && adModel.toLowerCase() === "shortform") {
                        comscoreChannel = "02" + siteChannel;
                    } else {
                        comscoreChannel = "03" + siteChannel;
                    }
                }
            }
        }
        
        params = { 
            c1: COMSCORE_C1, 
            c2: COMSCORE_C2, 
            c3: _extraMetadata.cpComscoreId, 
            c4: _extraMetadata.dpComscoreId, 
            c5: comscoreChannel, 
            c6: escape(_extraMetadata.seriesTitle) + (_extraMetadata.episodeNumber > 0 ? "|" + _extraMetadata.episodeNumber : ""),
            c14: $htv.Constants.Endpoints.third_party_beacon_platform
        };
        if (_commonParams.userid > 0) {
            params.c12 = _commonParams.userid;
            params.c13 = _commonParams.computerGuid;
        }
        // TODO: contentChapterIndex and contentChapterCount are not being passed correctly from player
        if (forAd === false) {
            params.c10 = contentChapterIndex + "|" + contentChapterCount;
        }

        // comscore ordering is special
        url = COMSCORE_HOST + "?" + _generateCacheBuster();
        for (i = 0; i < 15; i++) {
            if (params['c' + i] !== undefined && params['c' + i] !== null) {
                url += "&c" + i + "=" + params['c' + i];
            }
        }
        
        
        $htv.Platform.loadURL(url, this, _trackBeaconResult, {
            xmlparse: false
        });
    }
    
    function _trackDataloadDeejay(result) {
        var isError = result && result.toLowerCase() !== "ok";
        _trackStandardBeacon("dataload", "deejay", { result: result }, false, false, isError);
    }
    
    function _trackDataloadGeocheck(result) {
        var isError = result && result.toLowerCase() !== "ok";
        _trackStandardBeacon("dataload", "geocheck", { result: result }, false, false, isError);
    }
    
    function _trackIAGBeacon(extraParams) {
        var params, key, url;
        if (!_extraMetadata.shouldSendIAGBeacon) {
            return;
        }
        params = {
            sid: IAG_S_ID,
            bcr: IAG_BCR_ID,
            pgm: escape(_extraMetadata.seriesTitle),
            epi: _extraMetadata.seasonNumber > 0 ? escape("Season " + _extraMetadata.seasonNumber + " - " + _extraMetadata.title) : escape(_extraMetadata.title),
            seg: IAG_SEGMENT_ID,
            plt: $htv.Constants.Endpoints.third_party_beacon_platform,
            r: Math.random().toString()
        };
        for (key in extraParams) {
            if (extraParams.hasOwnProperty(key)) {
                params[key] = extraParams[key];
            }
        }
        url = IAG_HOST + _generateQueryString(params, true);
        
        $htv.Platform.loadURL(url, this, _trackBeaconResult, {
            xmlparse: false
        });
    }
    
    function _trackNielsenBeacon(nielsenEventId) {
        var url, params = {
            cc: 1,
            cg: escape(_extraMetadata.seriesTitle),
            ci: USE_STAGING ? NIELSEN_QA_CLIENT_ID : NIELSEN_PRODUCTION_CLIENT_ID,
            c4: "mn," + _extraMetadata.dpNielsenId,
            c6: "vc,c" + ($htv.Utils.stringIsNullOrEmpty(_extraMetadata.primarySiteChannelNielsenChannelId) ?
                            "72" : _extraMetadata.primarySiteChannelNielsenChannelId),
            c7: "hg," + _commonParams.computerGuid,
            c9: "pv," + $htv.Constants.Endpoints.third_party_beacon_platform,
            tl: "dav" + nielsenEventId + "-" + escape(_extraMetadata.title + "_" +
                    (_extraMetadata.seasonNumber <= 0 ? "NoData" : _extraMetadata.seasonNumber) + "_" + 
                    (_extraMetadata.episodeNumber <= 0 ? "NoData" : _extraMetadata.episodeNumber))
        };         
        if (!$htv.Utils.stringIsNullOrEmpty(_extraMetadata.secondarySiteChannelNielsenId)) {
            params.c5 = "gn," + _extraMetadata.secondarySiteChannelNielsenId; 
        }
        if (_commonParams.userid > 0) {
            params.c8 = "px," + _commonParams.userid;
        }
        
        url = NIELSEN_HOST + "?" + _generateCacheBuster("rnd") + _generateQueryString(params, true);
        
        $htv.Platform.loadURL(url, this, _trackBeaconResult, {
            xmlparse: false
        });
    }
    
    function _trackPlaybackEnd(watched) {
        if (_playbackStartRecorded === true) {
            _trackStandardBeacon("playback", "end", {
                watched: Math.max((_commonParams.position - _lastPlaybackPositionBeaconTime) + _watchedSinceLastPlaybackPositionBeacon, 0)
            });
            _lastPlaybackPositionBeaconTime = _commonParams.position;
            _watchedSinceLastPlaybackPositionBeacon = 0;
        }
    }
    
    function _trackPlaybackPosition(playing) {
        var watched;
        
        _trackStandardBeacon("playback", "position", {
            status: (playing ? "playing" : "paused"), 
            watched: Math.max((_commonParams.position - _lastPlaybackPositionBeaconTime) + _watchedSinceLastPlaybackPositionBeacon, 0)
        });
        _lastPlaybackPositionBeaconTime = _commonParams.position;
        _watchedSinceLastPlaybackPositionBeacon = 0;
    }
    
    function _trackPlaybackStart(channel, subChannel) {
        if (_playbackStartRecorded === true || _playlistReceived === false || _videoMetadataReceived === false || _playbackStarted === false) {
            return;
        }
        _playbackStartRecorded = true;
        var params = {};
        if (channel !== undefined) {
            params.channel = channel;
        }
        if (subChannel !== undefined) {
            params.subChannel = subChannel;
        }
        
        _trackStandardBeacon("playback", "start", params);
        _trackRealtimeBeacon("playback", "start", {
            os: _commonParams.os,
            client: _commonParams.client,
            flash: "",
            player: _commonParams.player,
            userid: _commonParams.userid,
            computerguid: _commonParams.computerGuid,
            distro: _commonParams.distro,
            requesturl: _extraMetadata.streamUrl,
            pid: _commonParams.pid,
            cpidentifier: _commonParams.cpidentifier,
            distroplatform: _commonParams.distroplatform,
            cdn: _commonParams.cdn,
            contentid: _commonParams.contentid,
            fmsserver: _commonParams.fmsserver,
            planid: _commonParams.planid,
            packageid: _commonParams.packageid
        });
        _trackIAGBeacon({
            tfid: IAG_CONTENT_TF_ID,
            cp: IAG_CONTENT_POSITION_ID
        });        
    }
        
    function _trackRevenueEnd(currentAdBreak, adposition, watched) {
        var auditParams = currentAdBreak.collected_audit_params;
        if (auditParams === null || auditParams === undefined) {
            auditParams = {};
        }
        _trackStandardBeacon("revenue", "end", {
            pod: _getPodSlotValue(currentAdBreak.pod, currentAdBreak.slot_count, currentAdBreak.slot),
            creativeid: auditParams.CreativeId,
            placementid: auditParams.PlacementId,
            adposition: adposition,
            watched: watched
        });
    }
    
    function _trackRevenuePosition(currentAdBreak, adposition, watched) {
        var auditParams = currentAdBreak.collected_audit_params;
        if (auditParams === null || auditParams === undefined) {
            auditParams = {};
        }            
        _trackStandardBeacon("revenue", "position", {
            pod: _getPodSlotValue(currentAdBreak.pod, currentAdBreak.slot_count, currentAdBreak.slot),
            creativeid: auditParams.CreativeId,
            placementid: auditParams.PlacementId,
            adposition: adposition,
            watched: watched
        });
    }
    
    function _trackRevenueStart(currentAdBreak) {
        var auditParams = currentAdBreak.collected_audit_params;
        if (auditParams === null || auditParams === undefined) {
            auditParams = {};
        }
        auditParams.pod = _getPodSlotValue(currentAdBreak.pod, currentAdBreak.slot_count, currentAdBreak.slot);
        auditParams.adposition = currentAdBreak.duration;
        
        _trackStandardBeacon("revenue", "start", auditParams);
        _trackStandardBeacon("revenue", "start", auditParams, false, true); //fire twilight beacons temporarily
        _trackComScoreBeacon(true);
        _trackIAGBeacon({
            tfid: IAG_AD_TF_ID,
            brn: currentAdBreak.advertiser_id,
            cte: currentAdBreak.ad_feed_id,
            ap: currentAdBreak.iag_ap,
            apt: currentAdBreak.iag_apt,
            pd: $htv.Platform.properties.distro,
            cust1: currentAdBreak.pod + "_" + currentAdBreak.slot_count + "_" + (currentAdBreak.slot + 1)
        });
    }
    
    function _trackSessionAppClose(closedByApp, onCompleteCallback) {
        // Default to "app" since usually can't detect 
        if (closedByApp === undefined) {
            closedByApp = true;
        }
        if (!_ready) {
            if (onCompleteCallback) {
                onCompleteCallback();
            }
            return;
        }
        _trackStandardBeacon("session", "appclose", { reason: closedByApp ? "app" : "os" }, false, false, false, onCompleteCallback);
    }
    
    function _trackSessionAppOpen(resume) {
        // Default to "no" since not resuming playback
        if (resume === undefined) {
            resume = false;
        }

        _trackStandardBeacon("session", "appopen", { resume: resume ? "yes" : "no" });
    }
    
    function _trackPlayerError(errorType, errorData) {
        errorData.os = _commonParams.os;
        errorData.client = _commonParams.client;
        errorData.flash = "";
        errorData.player = _commonParams.player;
        errorData.userid = _commonParams.userid;
        errorData.computerguid = _commonParams.computerGuid;
        errorData.distro = _commonParams.distro;
        errorData.requesturl = _extraMetadata.streamUrl;
        errorData.pid = _commonParams.pid;
        errorData.cpidentifier = _commonParams.cpidentifier;
        errorData.distroplatform = _commonParams.distroplatform;
        errorData.cdn = _commonParams.cdn;
        errorData.contentid = _commonParams.contentid;
        errorData.fmsserver = _commonParams.fmsserver;
        errorData.planid = _commonParams.planid;
        errorData.packageid = _commonParams.packageid;
    
        _trackRealtimeBeacon("error", errorType, errorData);
    }
    
    function _trackStandardBeacon(type, event, customParams, ignoreCommonParams, isTwilight, isError, onCompleteCallback) {  
        var queryString = _generateCacheBuster();
        if (customParams !== null) {
            queryString += _generateQueryString(customParams, false, true);
        }
        if (!ignoreCommonParams) {
            queryString += _generateCommonParams(false, true);
        }
        
        // If config not loaded, queue beacon
        // NOTE: compute query string now so params are accurate, but do
        // host and shouldsend checks later (_trackBeacon) once config is loaded
        if (!_ready) {
            
            _queuedBeacons.push([type, event, queryString, false, isTwilight, isError, onCompleteCallback]);
        }
        else {
            _trackBeacon(type, event, queryString, false, isTwilight, isError, onCompleteCallback);
        }
    }
    
    function _trackRealtimeBeacon(type, event, data) {
        // First do special realtime params serialization
        var queryString, dataKey, dictDataString = "";
        for (dataKey in data) {
            if (data.hasOwnProperty(dataKey)) {
                dictDataString += dataKey + ":" + escape(data[dataKey]) + ";";
            }
        }
        if (dictDataString.length > 0) {
            dictDataString = dictDataString.substring(0, dictDataString.length - 1); //remove trailing ';'
        }
        
        queryString = _generateCacheBuster() + _generateQueryString({
            session: _commonParams.sessionGuid, 
            playermode: _commonParams.playermode,
            position: _commonParams.position,
            dict: dictDataString
        });
        
        // If config not loaded, queue beacon
        // NOTE: compute query string now so params are accurate, but do
        // host and shouldsend checks later (_trackBeacon) once config is loaded
        if (!_ready) {
            
            _queuedBeacons.push([type, event, queryString, true, false]);
        }
        else {
            _trackBeacon(type, event, queryString, true, false);
        }
    }    
        
    return {
        // public interface
        generateGUID: _generateGUID,
        getComputerGUID: _getComputerGUID,
        handleEvent: _handleEvent,
        initialize: _initialize,
        trackAuditURL: _trackAuditURL,
        trackChapterEnd: _trackChapterEnd,
        trackChapterStart: _trackChapterStart,
        trackDataloadDeejay: _trackDataloadDeejay,
        trackDataloadGeocheck: _trackDataloadGeocheck,
        trackPlaybackEnd: _trackPlaybackEnd,
        trackPlaybackPosition: _trackPlaybackPosition,
        trackRevenueEnd: _trackRevenueEnd,
        trackRevenuePosition: _trackRevenuePosition,
        trackRevenueStart: _trackRevenueStart,
        trackSessionAppClose: _trackSessionAppClose,
        trackSessionAppOpen: _trackSessionAppOpen
    };
}());