//////////////////////////////////////////////////////////////////////////////////
//                                                                               //
// This is the episode management code. It gathers all the episodic timing       //
// information, returns that as a data structure, and can send it as a beacon    //
// to a specified URL.                                                           //
//                                                                               //
// This code could come from one of three places:                                //
//   1. implemented by the owner of the web page                                 //
//   2. from the implementation of "episodes.js" shared across the industry      //
//   3. in the future, this functionality would be built into the browser itself //
//                                                                               //
///////////////////////////////////////////////////////////////////////////////////


// Don't overwrite pre-existing instances of the object (esp. for older browsers).
var EPISODES = EPISODES || {};

// logging
EPISODES.log=function(m){
  // to log every message to console, as long as console is available
  //if (typeof window.console != "undefined") { console.debug(m) }
  // to only log when a parameter is added to the URL
  if(window.location.href.indexOf('utlogger') !=-1){ console.debug(m) }
};

EPISODES.init = function() {
  EPISODES.beaconUrl = EPISODES.beaconUrl == null ? "http://usertimeapp.com/beacon.gif" : EPISODES.beaconUrl;
  EPISODES.done = false;
  EPISODES.domready = false;
  EPISODES.marks = {};
  EPISODES.measures = {};
  EPISODES.starts = {};
  EPISODES.numScripts = 0;
  EPISODES.numScriptsInHead = 0;
  EPISODES.numInlineScripts = 0;
  EPISODES.numStylesheets = 0;
  EPISODES.numImages = 0;
  EPISODES.numIncompleteImages = 0;
  EPISODES.slowImages = {};
  EPISODES.bindDomReady();
  EPISODES.findStartTime();
  EPISODES.handleEpisodeMessage("EPISODES:mark:firstbyte:" + EPISODES.firstbyte);
  EPISODES.handleEpisodeMessage("EPISODES:measure:firstbyte:backendstarttime:firstbyte");
  EPISODES.addEventListener("load", function() {
    // Punting on perceivedrendertime -- it needs to be calculated somehow. The measurement might not ultimately go here.
    EPISODES.handleEpisodeMessage("EPISODES:measure:perceivedrendertime:firstbyte");     
    EPISODES.handleEpisodeMessage("EPISODES:measure:pageready:firstbyte");
    EPISODES.handleEpisodeMessage("EPISODES:done");
  }, false);
  EPISODES.addEventListener("beforeunload", EPISODES.beforeUnload, false);
  EPISODES.countsTimer = setTimeout(EPISODES.getCounts,1500);
  EPISODES.countsDone = false;
};

// Parse an EPISODES message and perform the desired function.
EPISODES.handleEpisodeMessage = function(message) {
  var handlerStartTime=new Date().getTime();
  var aParts = message.split(':');
  if ("EPISODES" === aParts[0]) {
    var action = aParts[1];
    if ("init" === action) {
      // "EPISODES:init"
      EPISODES.init();
    }
    else if ("mark" === action) {
      // "EPISODES:mark:markName[:markTime]"
      var markName = aParts[2];
      EPISODES.marks[markName] = parseInt(aParts[3] || Number(new Date()));
    }
    else if ("measure" === action) {
      // "EPISODES:measure:episodeName[:startMarkName|startEpochTime[:endMarkName|endEpochTime]]"
      var episodeName = aParts[2];

      // If no startMarkName is specified, assume it's the same as the episode name.
      var startMarkName = ( "undefined" != typeof(aParts[3]) ? aParts[3] : episodeName );
      // If the startMarkName doesn't exist, assume it's an actual time measurement.

      var startEpochTime = ( "undefined" != typeof(EPISODES.marks[startMarkName]) ? EPISODES.marks[startMarkName] :
                             ( ("" + startMarkName) === parseInt(startMarkName) ? startMarkName : undefined ) );

      var endEpochTime = ( "undefined" === typeof(aParts[4]) ? Number(new Date()) :
                           ( "undefined" != typeof(EPISODES.marks[aParts[4]]) ? EPISODES.marks[aParts[4]] : aParts[4] ) );

      if (startEpochTime) {
        EPISODES.measures[episodeName] = parseInt(endEpochTime - startEpochTime);
        EPISODES.starts[episodeName] = parseInt(startEpochTime);
      }
    }
    else if ("done" === action) {
      // "EPISODES:done"
      EPISODES.done = true;

      setTimeout(EPISODES.sendBeacon,0);
    }
    var now= new Date().getTime();
    EPISODES.log("Since start: "+(now-EPISODES.firstbyte)+"ms. In handler: "+(now-handlerStartTime)+"ms. "+message);
  }
};

// gather some additional information. ACL added; this is experimental.
EPISODES.getCounts=function(){
  EPISODES.countsDone=true;

  // num scripts: inline vs external source (with a src attribute) and in head.
  // We only count external scripts in head.
  var scripts=document.getElementsByTagName('script');
  var numScripts=scripts.length;
  var head=document.getElementsByTagName('head')[0];
  for(i=0;i<numScripts; i++){
    s=scripts[i];
    if(s.src == '') {
      EPISODES.numInlineScripts+=1;
    }else {
      EPISODES.numScripts+=1;
      if(s.parentNode==head){EPISODES.numScriptsInHead+=1}
    }
  }

  // num stylesheets
  EPISODES.numStylesheets=document.styleSheets.length;

  // num images
  var images=document.images;
  EPISODES.numImages=images.length;

  // num unloaded Images
  for(i=0;i<EPISODES.numImages;i++){
    image=images[i];
    //EPISODES.log(image.src +" is " + (image.complete ? 'complete' : 'incomplete') );
    if(!image.complete){
      // Note, the slow_image_id isn't strictly necessary right now.break We use it
      // to remember *when* we started timing the image, so we can calculate duration when the image finally loads.
      // But, we only take one sample right now, so there's not actually any variation in the starting time. 
//      image.slow_image_id=i;
      EPISODES.numIncompleteImages+=1;
//      EPISODES.slowImages[i]=[image,Number(new Date())]; // { arbitrary number: [dom element, time now] }
//
//      image.onerror = image.onabort = image.onload = function(){
//        duration = Number(new Date()) - EPISODES.slowImages[this.slow_image_id][1];
//      };
    }
  }
};

// Return an object of episode names and their corresponding durations.
EPISODES.getMeasures = function() {
  return EPISODES.measures;
};

// Return an object of episode names and their corresponding durations.
EPISODES.getStarts = function() {
  return EPISODES.starts;
};

// Construct a querystring of episodic time measurements and send it to the specified URL.
EPISODES.sendBeacon = function() {
  var url = EPISODES.beaconUrl;
  var measures = EPISODES.getMeasures();
  var sTimes = "";
  for (var key in measures) {
    sTimes += "," + key + ":" + measures[key];
  }

  // fetch the counts if they haven't already been done
  if(!EPISODES.countsDone){
    EPISODES.getCounts();
  }

  if (sTimes) {
    // strip the leading ","
    sTimes = sTimes.substring(1);

    url += "?ets=" + sTimes;

    url += "&counts="+EPISODES.numScripts+"."+EPISODES.numScriptsInHead+"."+EPISODES.numInlineScripts+"."+
            EPISODES.numStylesheets+"."+EPISODES.numImages+"."+EPISODES.numIncompleteImages;
    if (EPISODES.key) {
    url += "&key="+EPISODES.key;
    }
    url += "&title="+encodeURIComponent(document.title);
    // add the label, if provided
    if (EPISODES.label) {
        url += '&label=' + encodeURIComponent(EPISODES.label);
    }

    // send back the urls of the images taking longer than 1500 MS -- NOT CURRENTLY USED
    //    var images = [];
    //    for (var k in EPISODES.slowImages){
    //      i=EPISODES.slowImages[k][0].src;
    //      images.push(encodeURIComponent(i));
    //    }
    //    url += "&slow_images="+images.join(',');

    // Send the beacon.
    img = new Image();
    img.src = url;
  }
  return "";
};

// Use various techniques to determine the time at which this page started.
EPISODES.findStartTime = function() {
  var aCookies = document.cookie.split(' ');
  for (var i = 0; i < aCookies.length; i++) {
    if (0 === aCookies[i].indexOf("EPISODES=")) {
      var aSubCookies = aCookies[i].substring("EPISODES=".length).split('&');
      var startTime, bReferrerMatch;
      for (var j = 0; j < aSubCookies.length; j++) {
        if (0 === aSubCookies[j].indexOf("s=")) {
          startTime = aSubCookies[j].substring(2);
        }
        else if (0 === aSubCookies[j].indexOf("r=")) {
          // Some browsers return the semi-colon at the end of each stored
          // value. Remove it before we perform referrer matching to make it
          // work reliably.
          if (aSubCookies[j][aSubCookies[j].length - 1] == ';') {
            aSubCookies[j] = aSubCookies[j].substring(0, aSubCookies[j].length - 1);
          }
          var startPage = aSubCookies[j].substring(2, aSubCookies[j].length);
          bReferrerMatch = ( escape(document.referrer) == startPage );
        }
      }
      if (bReferrerMatch && startTime) {
        EPISODES.handleEpisodeMessage("EPISODES:mark:backendstarttime:" + startTime);
      }
    }
  }
};

// Set a cookie when the page unloads. Consume this cookie on the next page to get a "start time".
EPISODES.beforeUnload = function(e) {
  document.cookie = "EPISODES=s=" + Number(new Date()) + "&r=" + escape(document.location) + "; path=/";
};

// Wrapper for FF's window.addEventListener and IE's window.attachEvent.
EPISODES.addEventListener = function(sType, callback, bCapture) {
  if ("undefined" != typeof(window.attachEvent)) {
    return window.attachEvent("on" + sType, callback);
  }
  else if (window.addEventListener) {
    return window.addEventListener(sType, callback, bCapture);
  }
};

// Add a domready measurement. This event occurs before all images and other
// referenced binaries have finished loading. It uses the DOMContentLoaded
// event in browsers that support it, and an emulation of that for Internet
// Explorer.
// Shamelessly copied from jQuery.
EPISODES.bindDomReady = function() {
  // Mozilla, Opera and webkit nightlies currently support this event
  if (document.addEventListener) {
    // Use the handy event callback
    document.addEventListener("DOMContentLoaded", function() {
      document.removeEventListener("DOMContentLoaded", arguments.callee, false);
      EPISODES.domIsReady();
    }, false);

    // If IE event model is used
  } else if (document.attachEvent) {
    // ensure firing before onload,
    // maybe late but safe also for iframes
    document.attachEvent("onreadystatechange", function() {
      if (document.readyState === "complete") {
        document.detachEvent("onreadystatechange", arguments.callee);
        EPISODES.domIsReady();
      }
    });

    // If IE and not an iframe
    // continually check to see if the document is ready
    if (document.documentElement.doScroll && window == window.top) (function() {
      if (EPISODES.domready) return;

      try {
        // If IE is used, use the trick by Diego Perini
        // http://javascript.nwbox.com/IEContentLoaded/
        document.documentElement.doScroll("left");
      } catch(error) {
        setTimeout(arguments.callee, 0);
        return;
      }

      // and execute any waiting functions
      EPISODES.domIsReady();
    })();
  }

  // A fallback to window.onload, that will always work
  EPISODES.addEventListener("load", function() {
    EPISODES.domIsReady();
  }, false);
};

EPISODES.domIsReady = function() {
  if (!EPISODES.domready) {
    EPISODES.domready = true;
    EPISODES.handleEpisodeMessage("EPISODES:measure:domready:firstbyte");
  }
};

EPISODES.headDone = function(){
  EPISODES.handleEpisodeMessage("EPISODES:measure:headdone:firstbyte");
};


// Kick  off Episodes initialization (defined at top)
EPISODES.init();

