// sift.js - functions for implementing SIFT in HTML pages (Even for FLASH elements since they are contained in HTML pages).

// Supported protocols
var sift_protocols = new Array();
sift_protocols[0] = "http";
sift_protocols[1] = "https";

// Server tracking script location
var sift_server_script = "empiretheatres.siftanalytics.com/sift_track.php";

// Cookie timeouts
var sift_last_element_timeout = null;   //session based
var sift_last_form_timeout = null;   //session based
var sift_visitor_timeout = 5*365*24*60; //5 years
var sift_visit_timeout_default = 30;    //30 minutes

// Cookie names
var sift_visitor_cookie = "sift_visitor_code";
var sift_visit_cookie = "sift_visit_code";
var sift_campaign_cookie = "sift_campaign_id";
var sift_last_element_cookie = "sift_last_element";
var sift_last_form_cookie = "sift_last_form";

// Override parameters
var enable_campaign_override = false;
var campaign_override_param = "sift_campaign";
var sift_add_form_delay_default = 750;

// **** PAGE VIEW TRACKING **** //

/**
 * The function that pages call in order to be tracked by SIFT
 * NOTE: you can call this function from FLASH using Action Script: 
 *       getURL("Javascript:sift_track_campaign_element('campaign','element');", "_self", "GET");
 */
function sift_track_campaign_element (campaign, element, user, state, country, city) {

  // set sift_protocol for this pageview/action to be validated before logging
  var sift_protocol = document.location.protocol;

  // set sift_campaign based as follows: if campaign override is enabled and a sift_campaign parameter
  // is passed in the request query string this will be the campaign used, if not the campaign set on the 
  // page will be used, if no campaign is set try to get campaign for session cookie, finally if all else 
  // fails the value will be 0 meaning use the default campaign 
  var sift_campaign;
  var sift_campaign_changed = false;
  if (enable_campaign_override && get_query_param(campaign_override_param) != "none") {
    sift_campaign = get_query_param(campaign_override_param);    
  } else if (campaign) {
    sift_campaign = campaign;
  } else if (get_cookie(sift_campaign_cookie)!=null) {
    sift_campaign = get_cookie(sift_campaign_cookie);
  } else {
    sift_campaign = 0;
  }

  // set true flag on when campaign has changed to denote a new visit
  var campaign_existing_cookie = get_cookie(sift_campaign_cookie);
  if (campaign_existing_cookie != sift_campaign)
    sift_campaign_changed = true;
  set_cookie(sift_campaign_cookie, sift_campaign, '', '/', document.domain, '');

  // set sift_element based on 1. the passed in element name, 2. the html page title tag, 3. the page url
  var sift_element;
  if(element) {
    sift_element = element;
  } else if (document.title) {
    sift_element = document.title;
  } else {
    sift_element = document.location.pathname + document.location.search + document.location.hash;
  }

  // set sift state based on 1. the passed in state name, 2. to be determined at processing.
  var sift_state;
  if(state && element) {
    sift_state = state;
  } else {
    sift_state = "none";
  }
  
  // set sift country based on 1. the passed in country name, 2. to be determined at processing.
  var sift_country;
  if(country && state && element) {
    sift_country = country;
  } else {
    sift_country = "none";
  }

  // set sift city based on 1. the passed in city name, 2. to be determined at processing.
  var sift_city;
  if(city && country && state && element) {
    sift_city = city;
  } else {
    sift_city = "none";
  }

  // set the sift user based on the passed in username (if applicable)
  if (user) {
    sift_client_user = user;
  } else {
    sift_client_user = "none";
  }

  // set the sift referrer based on the query string param
  if ((sift_referrer = get_query_param("sift_ref")) == "none") {
    if ((sift_referrer = get_query_param("sift")) == "none") {
      sift_referrer = get_query_param("bam_ref");
    }
  }

  // set the sift email referrer based on the query string param
  if ((sift_email_referrer = get_query_param("sift_email_referrer")) == "none") {
    if ((sift_email_referrer = get_query_param("sift_email_ref")) == "none") {
      if ((sift_email_referrer = get_query_param("sift_mail_ref")) == "none") {
        //Or an adword referrer
        if ((sift_email_referrer = get_query_param("sift_adword_referrer")) == "none") {
          //Or an online referrer
          if ((sift_email_referrer = get_query_param("sift_online_referrer")) == "none") {
            sift_email_referrer = get_query_param("sm_ref");
          }
        }
      }
    }
  }

  // set the referrer tracker (user-referred) based on the query string param
  if ((user_referred = get_query_param("user_ref")) == "none") {
    if ((user_referred = get_query_param("user_referrer")) == "none") {
      if ((user_referred = get_query_param("ref_trac")) == "none") {
        if ((user_referred = get_query_param("ref_track")) == "none") {
          user_referred = get_query_param("rt");
        }
      }
    }
  }

  // set the sift email based on the query string param
  if ((sift_email = get_query_param("sift_email")) == "none") {
    sift_email = get_query_param("email_ref");
  }

  // full request path
  sift_search = document.location.pathname + document.location.search + document.location.hash;

  // check if there is visit timeout override set on the page
  if(!window.sift_visit_timeout)
    sift_visit_timeout = sift_visit_timeout_default;
  if(!window.sift_add_form_delay)
    sift_add_form_delay = sift_add_form_delay_default;

  sift_visitor_cookie_domain = "";
  if (window.sift_cookie_domain && location.host.indexOf(window.sift_cookie_domain)>=0)
    sift_visitor_cookie_domain = window.sift_cookie_domain;

  // get the visitor code cookie or create it if it does not exist
  var sift_visitor_code = (!get_cookie(sift_visitor_cookie)) ? generate_guid() : get_cookie(sift_visitor_cookie);
  set_cookie(sift_visitor_cookie, sift_visitor_code, sift_visitor_timeout, '/', sift_visitor_cookie_domain, '');

  // get the visit code cookie or create it if it does not exist
  var sift_visit_code = (!get_cookie(sift_visit_cookie) || sift_campaign_changed) ? generate_guid() : get_cookie(sift_visit_cookie);
  set_cookie(sift_visit_cookie, sift_visit_code, sift_visit_timeout, '/', '', '');

  // set this element as the current url path
  var this_element = document.location.pathname + document.location.search + document.location.hash;

  // set last element cookie to be checked on each page load to allow us to ignore reloads of the content as impressions
  var last_element = (!get_cookie(sift_last_element_cookie)) ? "" : get_cookie(sift_last_element_cookie);
  set_cookie(sift_last_element_cookie, this_element, sift_last_element_timeout, '/', document.domain, '');

  // get the page referrer
  var page_referrer = document.referrer;

  // if the content was not reloaded, create an image tag that loads the sift_track.php logging application
  //if (last_element != this_element) {

    var src_location = document.location.protocol + "//" + sift_server_script
              + "?sift_element=" + escape(sift_element) 
              + "&data_type=" + "element"
              + "&sift_visitor_code=" + sift_visitor_code 
              + "&sift_visit_code=" + sift_visit_code
              + "&sift_campaign=" + sift_campaign 
              + "&sift_referrer=" + escape(sift_referrer) 
              + "&sift_email_referrer=" + escape(sift_email_referrer)
              + "&sift_email=" + escape(sift_email)
              + "&sift_country=" + escape(sift_country)
              + "&sift_state=" + escape(sift_state)
              + "&sift_city=" + escape(sift_city)
              + "&sift_client_user=" + escape(sift_client_user)
              + "&user_referred=" + escape(user_referred)
              + "&page_referrer=" + escape(page_referrer)
              + "&sift_search=" + escape(sift_search)
              + "&sift_visit_timeout=" + sift_visit_timeout
              + "&sift_add_form_delay=" + sift_add_form_delay
              + "&flash_enabled=" + detect_plugin("Flash") 
              + "&java_enabled=" + detect_plugin("Java")
              + "&screen_resolution=" + screen.width + "x" + screen.height
              + "&color_depth=" + screen.colorDepth;
 
    send_request(src_location);

  //} else {
    //return;
  //}
}

// **** FORM TRACKING **** //

var sift_form_fields = new Array();
var element_name;


/**
 * Add a form to be tracked based on the form id attribute, called on page load
 */
function sift_add_form_by_id(form_id, fields, element) {
  element_name = element;
  sift_track_form_fields(form_id, fields);
  enable_sift_form_tracking(document.getElementById(form_id));
}


/**
 * Add a form to be tracked based on the form name attribute, called on page 
 * load.
 */
function sift_add_form_by_name(form_name, fields, element) {
  element_name = element;
  sift_track_form_fields(form_name, fields);
  for(var i=0; i<document.forms.length; i++) {
    if (document.forms[i].getAttribute("name")==form_name) {
      enable_sift_form_tracking(document.forms[i]);
      break;
    }
  }
}


/**
 * Store the fields to be tracked in an array on page load so they are 
 * available when submitted
 */
function sift_track_form_fields(frm_id, fields) {
  for(var i=0; i<fields.length; i++)
    sift_form_fields[sift_form_fields.length] = frm_id + "-" + fields[i];
}


/**
 * Check if form exists and if so alter submit functionality
 */
function enable_sift_form_tracking(frm) {
  if (frm != null)
    //add_sift_event(frm, "submit", sift_track_form);
    xb.addEvent(frm, "submit", sift_track_form, false);
}


/**
 * Add the tracking event to the onsubmit of the form
 */
function add_sift_event(obj, type, fn) {
  if (obj.addEventListener) {
    obj.addEventListener(type, fn, false);
    //return true;
  } else if (obj.attachEvent) {
    var r = obj.attachEvent("on"+type, fn);
    return r;
  } else {
    return false;
  }
}


/**
 * Gather the client data from cookies and environment and send to sift server,
 * called on form submit.
 */
function sift_track_form() {
  // get the field to be tracked
  var track_fields = get_fields_to_track(this);
  // determine the form name
  var sift_form_name = (this.getAttribute("id")) ? this.getAttribute("id") : this.getAttribute("name");

  // get the cookie data
  var sift_visitor_code = get_cookie(sift_visitor_cookie);
  var sift_visit_code = get_cookie(sift_visit_cookie);
  var sift_campaign = get_cookie(sift_campaign_cookie);

  // if visitor is not being tracked don't track form submission
  if (!sift_visitor_code)
    return;

  // if visit is no longer active create a new visit
  if (!sift_visit_code) {
    sift_visit_code = generate_guid();
    set_cookie(sift_visit_cookie, sift_visit_code, sift_visit_timeout, '/', '', '');
  }

  // set sift_element for this pageview to be validated before logging
  if(element_name) {
    sift_element = element_name;
  } else if (document.title) {
    sift_element = document.title;
  } else {
    sift_element = document.location.pathname + document.location.search + document.location.hash;
  }

  // full request path
  sift_search = document.location.pathname + document.location.search + document.location.hash;

  // set last element cookie to be checked on each page load to allow us to ignore reloads of the content as impressions
  var last_form = (!get_cookie(sift_last_form_cookie)) ? "" : get_cookie(sift_last_form_cookie);
  set_cookie(sift_last_form_cookie, sift_form_name, sift_last_form_timeout, '/', '', '');

  // check the last form submitted cookie to prevent double submitting and send data to sift server
  if (last_form != sift_form_name) {
  
    var params = "?sift_element=" + sift_element
      + "&data_type=" + "form"
      + "&sift_form_name=" + sift_form_name
      + "&sift_visitor_code=" + sift_visitor_code
      + "&sift_visit_code=" + sift_visit_code
      + "&sift_campaign=" + sift_campaign
      + "&sift_search=" + sift_search 
      + "&";
    for(var i=0; i<track_fields.length; i++)
      params += track_fields[i].getAttribute("name") + "=" + escape(track_fields[i].value) + "&";
    params = params.substring(0, params.length - 1);

    var request_url = document.location.protocol + "//" + sift_server_script + params;

    send_request(request_url);

  }
}


/**
 * Determine which fields exist based on form element tags
 */
function get_fields_to_track(frm) {
  var track_fields = new Array();
  track_fields = track_fields.concat(get_fields_to_track_by_tag(frm, "input"));
  track_fields = track_fields.concat(get_fields_to_track_by_tag(frm, "textarea"));
  return track_fields;
}


/**
 * Determine which fields exist for tracking
 */
function get_fields_to_track_by_tag(frm, tag) {
  var track_fields = new Array();
  var input_fields = frm.getElementsByTagName(tag);
  var k = 0;
  // match specified fields to actual form fields to determine what should be tracked
  for(var i=0; i<input_fields.length; i++) {
    for (var j=0; j<sift_form_fields.length; j++) {
      if(frm.getAttribute("name")+"-"+input_fields[i].getAttribute("name")==sift_form_fields[j] || 
        frm.getAttribute("id")+"-"+input_fields[i].getAttribute("name")==sift_form_fields[j]) {

        track_fields[k] = input_fields[i];
        k++;
      }
    }
  }
  return track_fields;
}

// **** GENERAL FUNCTIONS **** //


/**
 * Use a web beacon image to make a sift server request
 */
function send_request(src_location) {
  // See if element already exists, and just change the source if so
  if (document.getElementById("sift_img")) {

    var sift_img = document.getElementById("sift_img");

    sift_img.setAttribute("src", src_location);

    // Add a delay to the submit so that the client has enough time to render
    // the image.
    var p = new Pause(sift_add_form_delay);
  
  // Element does not exist - let's create it
  } else {

    var sift_img = document.createElement("img");

    // Set the IMG tag attributes
    sift_img.setAttribute("id", "sift_img");
    sift_img.setAttribute("alt", "");
    sift_img.setAttribute("width", "1");
    sift_img.setAttribute("height", "1");

    // Set some style attributes to make the IMG blend in a little better with the rest of the document
    sift_img.style.position="absolute";
    sift_img.style.bottom="0";
    sift_img.style.left="0";
    sift_img.style.border="none";
    sift_img.setAttribute("src", src_location);

    // Insert the IMG into the document (this is the containing HTML document 
    // if you are calling this from Flash)
    document.body.appendChild(sift_img);

    // Add a delay to the submit so that the client has enough time to render
    // the image.
    var p = new Pause(sift_add_form_delay);
  }
}


/**
 * Set a cookie
 *
 * The set_cookie and get_cookie scripts both came from 
 * http://techpatterns.com/downloads/javascript_cookies.php
 */
function set_cookie( name, value, expires, path, domain, secure ) {
  // set time, it's in milliseconds
  var today = new Date();
  today.setTime( today.getTime() );

  if( domain == '' ) domain = null;

  /*
    if the expires variable is set, make the correct
    expires time, the current script below will set
    it for x number of days, to make it for hours,
    delete * 24, for minutes, delete * 60 * 24
  */
  if ( expires ) {
    expires = expires * 1000 * 60;
  }
  var expires_date = new Date( today.getTime() + (expires) );

  document.cookie = name + "=" + escape( value ) +
    ( ( expires ) ? ";expires=" + expires_date.toGMTString() : "" ) +
    ( ( path ) ? ";path=" + path : "" ) +
    ( ( domain ) ? ";domain=" + domain : "" ) +
    ( ( secure ) ? ";secure" : "" );
}


/**
 * Get a cookie
 *
 * This fixes an issue with the old method, ambiguous values
 * with this test document.cookie.indexOf( name + "=" );
 *
 * The set_cookie and get_cookie scripts both came from 
 * http://techpatterns.com/downloads/javascript_cookies.php
 */
function get_cookie( check_name ) {
  // first we'll split this cookie up into name/value pairs
  // note: document.cookie only returns name=value, not the other components
  var a_all_cookies = document.cookie.split( ';' );
  var a_temp_cookie = '';
  var cookie_name = '';
  var cookie_value = '';
  var b_cookie_found = false; // set boolean t/f default f

  for ( i = 0; i < a_all_cookies.length; i++ )
  {
    // now we'll split apart each name=value pair
    a_temp_cookie = a_all_cookies[i].split( '=' );

    // and trim left/right whitespace while we're at it
    cookie_name = a_temp_cookie[0].replace(/^\s+|\s+$/g, '');

    // if the extracted name matches passed check_name
    if ( cookie_name == check_name )
    {
      b_cookie_found = true;
      // we need to handle case where cookie has no value but exists (no = sign, that is):
      if ( a_temp_cookie.length > 1 )
      {
        cookie_value = unescape( a_temp_cookie[1].replace(/^\s+|\s+$/g, '') );
      }
      // note that in cases where cookie is initialized but no value, null is returned
      return cookie_value;
      break;
    }
    a_temp_cookie = null;
    cookie_name = '';
  }
  if ( !b_cookie_found )
  {
    return null;
  }
}

/**
 * Check whether protocol is supported
 */
function is_sift_protocol(sift_protocol) {
  for (i = 0; i < sift_protocols.length; i++) {
    if (sift_protocols[i] == sift_protocol) {
      return true;
    }
  }
  return false;
}


/**
 * Get a query param value
 */
function get_query_param (search_param) {

  if (document.location.search) {
    var url = document.location + '';
    q=url.split('?');
    if (q[1]) {
      //Get all Name/Value pairs from the QueryString
      var pairs = q[1].split('&');
      for (i=0;i<pairs.length;i++) {

        //Get the Name from given Name/Value pair
        var keyval = pairs[i].split('=');

        if (keyval[0] == search_param) {
          //Get the Value from given Name/Value pair and set to the return ID
          return keyval[1];
        }
      }
    }
      }
  return 'none';
}


/**
 * Generate and return a random guid
 */
function generate_guid() {
  var guid, i, j;
  guid = '';
  for(j=0; j<32; j++) {
    if( j == 8 || j == 12|| j == 16|| j == 20)
      guid += '-';
    i = Math.floor(Math.random()*16).toString(16).toUpperCase();
    guid += i;
  }
  return guid;
}


/**
 * Detects the following plugins in the user's browser: 
 *   - Flash
 *   - Windows Media Player *
 *   - Java
 *   - Shockwave
 *   - RealPlayer
 *   - QuickTime *
 *   - Acrobat Reader
 *   - SVG Viewer
 * 
 * http://www.javascriptkit.com/script/script2/plugins.js
 */
function detect_plugin (plugin_name) {
  var agt=navigator.userAgent.toLowerCase();
  var ie  = (agt.indexOf("msie") != -1);
  var ns  = (navigator.appName.indexOf("Netscape") != -1);
  var win = ((agt.indexOf("win")!=-1) || (agt.indexOf("32bit")!=-1));
  var mac = (agt.indexOf("mac")!=-1);

  /**
   * 20090121
   * Ian Bezanson (ibezanson@modernmedia.ca)
   * Modern Media
   *
   * We noticed a bug today where IE would kick an error when hooking into 
   * Sift.  It wanted users to confirm that it was okay to install a Windows 
   * Media Player Extension plugin.  Through investigation, it seemed the
   * same would occur with Quicktime on IE.  After some digging, we came to
   * realize that it's happening in the line below when checking whether 
   * browser supports X,Y,Z.
   *
   * We don't store whether user's browser supports either of these, so I'm
   * simply disabling them for IE AND Non-IE browsers.
   */
  if (ie && win) {
    //pluginlist = detectIE("Adobe.SVGCtl","SVG Viewer") + detectIE("SWCtl.SWCtl.1","Shockwave Director") + detectIE("ShockwaveFlash.ShockwaveFlash.1","Shockwave Flash") + detectIE("rmocx.RealPlayer G2 Control.1","RealPlayer") + detectIE("QuickTimeCheckObject.QuickTimeCheck.1","QuickTime") + detectIE("MediaPlayer.MediaPlayer.1","Windows Media Player") + detectIE("PDF.PdfCtrl.5","Acrobat Reader");
    pluginlist = detectIE("ShockwaveFlash.ShockwaveFlash.1","Shockwave Flash") + detectIE("PDF.PdfCtrl.5","Acrobat Reader");
  }
  if (ns || !win) {
    nse = ""; for (var i=0;i<navigator.mimeTypes.length;i++) nse += navigator.mimeTypes[i].type.toLowerCase();
    //pluginlist = detectNS("image/svg-xml","SVG Viewer") + detectNS("application/x-director","Shockwave Director") + detectNS("application/x-shockwave-flash","Shockwave Flash") + detectNS("audio/x-pn-realaudio-plugin","RealPlayer") + detectNS("video/quicktime","QuickTime") + detectNS("application/x-mplayer2","Windows Media Player") + detectNS("application/pdf","Acrobat Reader");
    pluginlist = detectNS("application/x-shockwave-flash","Shockwave Flash") + detectNS("application/pdf","Acrobat Reader");
  }

  function detectIE(ClassID,name) { result = false; document.write('<SCRIPT LANGUAGE=VBScript>\n on error resume next \n result = IsObject(CreateObject("' + ClassID + '"))</SCRIPT>\n'); if (result) return name+','; else return ''; }
  function detectNS(ClassID,name) { n = ""; if (nse.indexOf(ClassID) != -1) if (navigator.mimeTypes[ClassID].enabledPlugin != null) n = name+","; return n; }

  pluginlist += navigator.javaEnabled() ? "Java," : "";
  if (pluginlist.length > 0) pluginlist = pluginlist.substring(0,pluginlist.length-1);

  return (pluginlist.indexOf(plugin_name)!=-1) ? 1 : 0;
}


/** 
 *  constructor 
 *  @param duration integer milliseconds
 *  @param <optional> function to run while waiting.
 */
function Pause(duration, busy){
  this.duration = duration;
  this.busywork = null; // function to call while waiting.
  this.runner = 0;

  if (arguments.length == 2) {
    this.busywork = busy;
  }

  this.pause(this.duration);

} // Pause class

/**
 *  pause method 
 *  @param duration: integer in milliseconds
 */
Pause.prototype.pause = function(duration){
  if ( (duration == null) || (duration < 0)) {return;}
  var later = (new Date()).getTime() + duration;
  while(true){
    if ((new Date()).getTime() > later) {
      break;
    }
    this.runner++;
    if (this.busywork != null) {
      this.busywork(this.runner);
    }
  } // while
} // pause method


/**
 * IE Doesn't inherit an attached object properly, allowing you to reference the parent object "this"
 * as Moz does.  So, we have to use some custom code to add events, retrieved from:
 *
 * http://blog.stchur.com/2006/10/12/fixing-ies-attachevent-failures/
 */
var xb = {
  evtHash: [],

  ieGetUniqueID: function(_elem) {
    if (_elem === window) {
      return 'theWindow';
    }
    else if (_elem === document) {
      return 'theDocument';
    }
    else {
      return _elem.uniqueID;
    }
  },

  addEvent: function(_elem, _evtName, _fn, _useCapture) {
    //If addEventListener is an option (read: Not IE), just call the usual function,
    //as Moz handles 'this' properly.
    if (typeof _elem.addEventListener != 'undefined'){
      _elem.addEventListener(_evtName, _fn, _useCapture);
    }
    //If attachEvent is an option (read: IE), we need to do some mojo
    else if (typeof _elem.attachEvent != 'undefined') {

      //Create The Key
      var key = '{FNKEY::obj_' + xb.ieGetUniqueID(_elem) + '::evt_' + _evtName + '::fn_' + _fn + '}';

      //see if the event hash already has this key, and if so, ignore the event wire-up request
      var f = xb.evtHash[key];
      if (typeof f != 'undefined'){
        return;
      }

      // create a lambda function that uses .call(..) to maintain a proper 'this' keyword reference
      f = function(){
        _fn.call(_elem);
      };

      // store the lambda function in the event hash using the key that was created in the beginning
      xb.evtHash[key] = f;

      // attach the desired event to the element, but execute the lamba fu nction instead of _fn, so that
      // the 'this' keyword is correclty maintained
      _elem.attachEvent('on' + _evtName, f);

      // attach unload event to the window to clean up possibly IE memory leaks
      // IMPORTANT: You may want to omit this 'onunload' bit, as it could potentially
      // increase memory consumption, especially in cases where you do a lot of
      // manual detaching of events during the life of your app . use this technique with care
      window.attachEvent('onunload', function() {
        _elem.detachEvent('on' + _evtName, f);
      });

      // null out the key so the GC knows it can be eaten
      key = null;
      //f = null;   /* DON'T null this out, or we won't be able to detach it */
    }
    else {
      _elem['on' + _evtName] = _fn;
    }
  },

  removeEvent: function(_elem, _evtName, _fn, _useCapture) {
    if (typeof _elem.removeEventListener != 'undefined') {
      _elem.removeEventListener(_evtName, _fn, _useCapture);
    }
    else if (typeof _elem.detachEvent != 'undefined') {
      // create the key
      var key = '{FNKEY::obj_' + xb.ieGetUniqueID(_elem) + '::evt' + _evtName + '::fn_' + _fn + '}';

      // see if the event has already has this key, and if so, store its value in the f variable
      var f = xb.evtHash[key];
      if (typeof f != 'undefined') {
        // detach the lambda function, f, that was retrieve from the event hash, populated from .addEvent(..)
        _elem.detachEvent('on' + _evtName, f);

        // delete this key from the event hash
        delete xb.evtHash[key];
      }

      // null out the key so the GC knows it can be eaten
      key = null;
      //f = null;   /* DON'T null this out, or we won't be able to detach it */
    }
  }
};


