var BIT = BIT || {};

BIT.api_base_url = "http://api.bandsintown.com";
BIT.base_url = "http://www.bandsintown.com";
BIT.widgets = BIT.widgets || [];
BIT.rsvp_dialog_enabled = true;

// copy properties from source object to target object. similar to jQuery.extend or FB.copy.
BIT.copy = function(target, source, overwrite) {
  for (var property in source) {
    if (typeof target[property] === 'undefined' || overwrite) {
      target[property] = source[property];
    }
  }
  return target;
};

// create an object nested within BIT based on string name. similar to FB.create.
BIT.create_object = function(name) {
  var node = window.BIT;
  var parts = name ? name.split('.') : [];
  var total_parts = parts.length;

  for (var i = 0; i < total_parts; i++) {
    var part = parts[i];
    var current_part = node[part];
    if (!current_part) {
      current_part = {};
      node[part] = current_part;
    }
    node = current_part;
  }
  return node;
};

// extend the target object (creating if not already defined) with source. similar to FB.provide.
BIT.extend = function(target, source) {
  var target_object = typeof(target) == 'string' ? BIT.create_object(target) : target;
  return BIT.copy(target_object, source);
};

BIT.extend("Utils.Color", {
  RGB_REGEX: /\(\s*(\d+)\W+(\d+)\W+(\d+)\s*\)/, // 'rgb(10,20,30)'
  HEX_REGEX: /[A-F0-9]+/i,                      // '#ffffff', '#000', 'ffffff', '000'

  rgb_from_hex_color: function( color_string ) {
    var hex = color_string.match(this.HEX_REGEX)[0];
    if (hex.length == 6) {
      return [
        (parseInt(hex.substr(0,2), 16) / 255),
        (parseInt(hex.substr(2,2), 16) / 255),
        (parseInt(hex.substr(4,2), 16) / 255),
      ];
    } else { // 3-char notation
      return [  
        (parseInt(hex[0] + hex[0], 16) / 255),
        (parseInt(hex[1] + hex[1], 16) / 255),
        (parseInt(hex[2] + hex[2], 16) / 255),
      ];
    }
  },
  
  rgb_from_rgb_color: function ( color_string ) {
    var colors = color_string.match(/\((\d+)\W+(\d+)\W+(\d+)\)/);
    return [
      colors[1] / 255,
      colors[2] / 255,
      colors[3] / 255    
    ];
  },
  
  parse_rgb: function( color_string ) {
    var rgb = [0, 0, 0];
    if ( color_string.match(this.RGB_REGEX) ) {
      rgb = this.rgb_from_rgb_color(color_string);
    } else if ( color_string.match(this.HEX_REGEX)) {
      rgb = this.rgb_from_hex_color(color_string);
    } 
    return {
      red   : rgb[0],
      green : rgb[1],
      blue  : rgb[2]
    };
  },
  
  image_color_for_background_color: function( color_string ) {
    var rgb = this.parse_rgb(color_string);
    var computed_value = ( 0.213 * rgb.red ) + ( 0.715 * rgb.green ) + ( 0.072 * rgb.blue );
    return computed_value < 0.5 ? 'white' : 'black';
  },
});

BIT.extend("Utils.Element", {
  
  get_computed_style: function(element, attribute) {
    var computed_style;
    // IE uses currentStyle, other browsers should use document.defaultView
    if (typeof element.currentStyle != 'undefined' && typeof window.opera == 'undefined') {
      computed_style = element.currentStyle; 
    } else { 
      computed_style = document.defaultView.getComputedStyle(element, null); 
    }

    // if no attribute is passed return the computed style object
    if (!attribute) { return computed_style; }

    var value = computed_style[attribute];
    // IE returns height 'auto' in currentStyle, but clientHeight works pretty well.
    if (attribute == "height" && value == "auto") { value = element.offsetHeight; }
    return value;
  },

  // similar to Element.up from prototype
  get_parent: function(element, tag_name) {
    var found_element = null;
    var current_element = element;
    while (found_element == null && current_element.tagName.toLowerCase() != 'body') {
      current_element = current_element.parentNode;
      if (current_element && current_element.tagName.toLowerCase() == tag_name.toLowerCase()) { found_element = current_element; }
    }
    return found_element;
  },
  
  /* very simple element search.
   *   selector should be passed as "<class>", ".<class>", or "<tag>.<class>".  Single classname only.
   *   opt_element should be a DOM element if passed, otherwise search the whole document
   */
  search: function(selector, opt_element) {
    var selector_array = selector.split(".");
    var tag_name, class_name;
  
    // selector given as "classname"
    if (selector_array.length == 1) {
      tag_name = "*";
      class_name = new RegExp(selector_array[0]);
  
    // selector given as ".classname" or "div.classname"
    } else {
      class_name = new RegExp(selector_array[1])
      tag_name = selector_array[0] == "" ? "*" : selector_array[0];
    }
  
    // if opt_element was not passed, search the whole document
    var scope = opt_element ? opt_element : document;
  
    var i;
    var matches = [];
  
    var tags = scope.getElementsByTagName(tag_name);
    for (i=0; i<tags.length; i++) {
      var current_tag = tags[i];
      if (current_tag.className && class_name.exec(current_tag.className)) {
        matches.push(current_tag);
      }
    }
  
    return matches;
  },
  
  get_outer_height: function(element) {
    var outer_height = 0;
    var height_attributes = ["borderTop", "paddingTop", "height", "paddingBottom", "borderBottom"];
    for (var i=0; i<height_attributes.length; i++) {
      var attribute = height_attributes[i];
      var value = parseFloat(BIT.Utils.Element.get_computed_style(element, attribute));
      if (isNaN(value)) { value = 0; }
      outer_height += value;
     }
    return outer_height;
  },
  
  get_scroll_top: function() {
    if (window.pageYOffset) {
      return window.pageYOffset;
    } else {
      return (document.documentElement || document.body).scrollTop;
    }
  },
  
  // Copy computed CSS attributes from params.source to params.target.
  copy_css: function(params) {
    var source = params.source;
    var target = params.target;
    var computed_style = BIT.Utils.Element.get_computed_style(source);

    // Webkit supports the cssText property which is a string of computed css
    if (computed_style.hasOwnProperty("cssText")) {
      target.style.cssText = computed_style.cssText;

    // For other browers, loop through css attributes
    } else {
      var copy_css_attributes = [
        "fontFamily", "fontSize", "fontWeight", "fontStyle", "color", 
        "textTransform", "textDecoration", "letterSpacing", "wordSpacing", 
        "lineHeight", "textAlign", "verticalAlign", "direction", 
        "backgroundColor", "backgroundImage", "backgroundRepeat", "backgroundPosition", "backgroundAttachment", "opacity", 
        "width", "height", "top", "right", "bottom", "left", 
        "marginTop", "marginRight", "marginBottom", "marginLeft", 
        "paddingTop", "paddingRight", "paddingBottom", "paddingLeft", 
        "borderTopWidth", "borderRightWidth", "borderBottomWidth", "borderLeftWidth", 
        "borderTopColor", "borderRightColor", "borderBottomColor", "borderLeftColor", 
        "borderTopStyle", "borderRightStyle", "borderBottomStyle", "borderLeftStyle", 
        "position", "display", "visibility", "zIndex", "overflowX", "overflowY", "whiteSpace", 
        "clip", "float", "clear", "cursor", 
        "listStyleImage", "listStylePosition", "listStyleType", "markerOffset"
      ];
      for (var i=0; i < copy_css_attributes.length; i++) {
        var attr = copy_css_attributes[i];
        target.style[attr] = computed_style[attr];
      }
    }
  },

  /*  Similar to jQuery.position, returns an object with properties for top, bottom, left, 
   * right, horizontal_center, and vertical_center.
   *
   */
  position: function(element) {
    var position = {};
    var bounds = element.getBoundingClientRect();
    
    // Subtract element margins
    // note: when an element has margin: auto the offsetLeft and marginLeft
    // are the same in Safari causing offset.left to incorrectly be 0
    var margin_top  = parseFloat(BIT.Utils.Element.get_computed_style(element, "marginTop")) || 0;
    var margin_left = parseFloat(BIT.Utils.Element.get_computed_style(element, "marginLeft")) || 0;
    
    position.top = bounds.top - margin_top;
    position.bottom = bounds.bottom
    position.vertical_center = (position.top + position.bottom) / 2;
    
    position.left = bounds.left - margin_left;
    position.right = bounds.right;
    position.horizontal_center = (position.left + position.right) / 2;
    
    return position;
  }
});

BIT.extend("Utils.Window", {
  
  is_iframe: function() {
    return window !== window.top;
  },
  
  dimensions: function() {
    return {
      width: this.width(),
      height: this.height()
    };
  },
  
  // based on jQuery width() and height() functions for window objects
  width: function() {
    if (document.compatMode === "CSS1Compat" && document.documentElement.clientWidth) {
      return document.documentElement.clientWidth;
    } else {
      return document.body.clientWidth;
    }
  },
  
  height: function() {
    if (document.compatMode === "CSS1Compat" && document.documentElement.clientHeight) {
      return document.documentElement.clientHeight;
    } else {
      return document.body.clientHeight;
    }
  }  
});

// parseUri 1.2.2
// (c) Steven Levithan <stevenlevithan.com>
// MIT License
BIT.extend("Utils.URI", {
  parse: function (str) {
    var o   = this.parse_options,
        m   = o.parser[o.strictMode ? "strict" : "loose"].exec(str),
        uri = {},
        i   = 14;

    while (i--) uri[o.key[i]] = m[i] || "";

    uri[o.q.name] = {};
    uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) {
      if ($1) uri[o.q.name][$1] = $2;
    });

    return uri;
  },

  parse_options: {
    strictMode: false,
    key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"],
    q:   {
      name:   "queryKey",
      parser: /(?:^|&)([^&=]*)=?([^&]*)/g
    },
    parser: {
      strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
      loose:  /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
    }
  }
});

BIT.extend("Utils.Text", {
  default_truncate_options : { 
    length : 100,
    omission : "...",
    max_lines : 3
  },
  
  // used to replace "test 123" => "test"
  last_word_with_preceding_whitespace_regexp : /\s*\S+$/,
  
  // used to replace "  test 123  " => "test 123"
  preceding_and_trailing_whitespace_regexp   : /^\s*|\s*$/g,
  
  truncate: function(text_to_truncate, options) {
    options       = options          || {};
    var length    = options.length   || BIT.Utils.Text.default_truncate_options.length;
    var omission  = typeof(options.omission) == "string" ? options.omission : BIT.Utils.Text.default_truncate_options.omission;
    var max_lines = options.max_lines || BIT.Utils.Text.default_truncate_options.max_lines;
  
    // used to replace "test\ntest\ntest" => "test\ntest", for max_lines number of linebreaks
    var max_lines_regexp = new RegExp("(?:.*\?\\n){" + max_lines + "}");
  
    // remove preceding + trailing whitespace
    var text = BIT.Utils.Text.strip(text_to_truncate);
    
    // if the text is already under the specified length and contains fewer line breaks than max_lines, return it
    if (text.length < length && !text.match(max_lines_regexp)) { return text; }
    
    // remove any text after max_lines number of line breaks, including trailing whitespace
    if (text.match(max_lines_regexp)) { text = BIT.Utils.Text.strip(text.match(max_lines_regexp)[0]); }
    
    // remove the last word and preceding whitespace from text until it is under the specified length
    while (text.length > length) {
      text = text.replace(BIT.Utils.Text.last_word_with_preceding_whitespace_regexp, "")
    }
    
    // return the truncated text with the omission
    return text + omission;
  },
  
  strip: function (text) {
    return text.replace(BIT.Utils.Text.preceding_and_trailing_whitespace_regexp, "").replace(/\r\n/g, "\n");
  },
  
  escape_quotes: function (text) {
    return text.replace(/'/g, "\\'").replace(/"/g, "\\\"");
  },

  // taken from prototypes String.escapeHTML()
  escape_html: function(text) {
    return text.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
  }
});

BIT.Utils.Log = function(message) {
  if (typeof(BIT.Utils.Log.enabled) == "undefined") { BIT.Utils.Log.enabled = false; }
  if (!BIT.Utils.Log.enabled) { return; }
  try { 
    var args = Array.prototype.slice.call(arguments);
    if (args.length > 1) {
      var extras = args.slice(1, args.length);
      console.log(message, extras);
    } else {
      console.log(message);
    }
  } catch(ex) {}
}

BIT.extend("Utils.Support", {

  _support: {},
  
  // detect if position:fixed is supported. based on http://kangax.github.com/cft/
  fixed_position: function() {
    if (typeof(this._support.fixed_position) != 'undefined') { return this._support.fixed_position; }
    this._support.fixed_position = (function () {
      var container = document.body;
      if (document.createElement && container && container.appendChild && container.removeChild) {
        var el = document.createElement("div");
        if (!el.getBoundingClientRect) { return null; }
        el.innerHTML = "x";
        el.style.cssText = "position:fixed;top:100px;";
        container.appendChild(el);
        var originalHeight = container.style.height, originalScrollTop = container.scrollTop;
        container.style.height = "3000px";
        container.scrollTop = 500;
        var elementTop = el.getBoundingClientRect().top;
        container.style.height = originalHeight;
        var isSupported;
        var exactValue = 100;
        // IE7 supports fixed position but sometimes the computed elementTop is off by a few pixels, so check within a range.
        if (BIT.Utils.Browser.IE7) {
          var range = 5;
          isSupported = elementTop >= exactValue - range && elementTop <= exactValue + range
        } else {
          isSupported = elementTop === exactValue;
        }
        container.removeChild(el);
        container.scrollTop = originalScrollTop;
        return isSupported;
      }
      return null;
    })();
    return this._support.fixed_position;
  },

  // detect if HTML5 postMessage is available
  post_message: function() {
    return (typeof window.postMessage != 'undefined');
  }
})

BIT.extend("Utils.JSONP", {
  /*
   * BIT.Utils.JSONP.getJSON provides basic JSONP support for the widget.
   *
   * params:
   *   url - a String which will be used for the script 'src' attribute
   *   callback - a function which accepts the JSON response as a single argument
   *   callback_obj - optional, used to bind 'this' when executing the callback function
   *
   */
  getJSON: function(params) {
    var callback_name = this.build_callback_name();
    var callback      = params.callback || function() {};
    var callback_obj  = params.callback_obj;
    
    // setup a self-deleting function that calls the real callback function with 'this' bound to params.callback_obj.
    window[callback_name] = function(data) {
      BIT.Utils.Log("Running JSONP Callback window." + callback_name);
      callback.call(callback_obj, data);
      try {
        delete(window[callback_name]); 
      } catch (e) { 
        BIT.Utils.Log("Error deleting JSONP callback function", e); 
      }
    }
    BIT.Utils.Log("JSONP callback function created: ", "window." + callback_name);
    
    // add the function created above to params.url as the 'callback' param.
    var url = params.url;
    var callback_separator = /\?/.test(url) ? "&" : "?";
    url += callback_separator + "callback=" + callback_name;
    BIT.Utils.Log("JSONP url: " + url);
    
    // setup a new script element with the url.
    var script = document.createElement("script");
        script.type = "text/javascript";
        script.src = url;
        script.async = true;

    // insert the script into the head element.
    // head.insertBefore(script, head.firstChild) is used instead of head.appendChild(script) to avoid a bug with IE6.
    // http://bugs.jquery.com/ticket/2709
    var head = document.getElementsByTagName("head")[0] || document.documentElement;
    head.insertBefore(script, head.firstChild);
  },

  build_callback_name: function() {
    var callback_name;
    do {
      callback_name = "bit_jsonp" + (new Date()).getTime();
    } while (typeof(window[callback_name]) != "undefined")
    return callback_name;
  }
});

BIT.extend("Utils.XD", {
    /*
     * BIT.Utils.XD.send allows sending a message to an iframe on the current page that is hosted on http://www.bandsintown.com.
     * This uses postMessage when supported by the browser and falls back to using proxy iframe messaging.
     *
     * params:
     *   message - String or Object which will be converted to a query string (required)
     *   options -
     *     target_origin - The domain of the window we are sending a message to (used for postMessage)
     *     target_window_name - If not given, the message will be sent to window.parent.
     *                          If given, this should be the name attribute of the iframe in the document which will receive the message.
     *                          It will be looked up as window.frames["name"]
     */  
    send: function(message, options) {
      BIT.Utils.Support.post_message() ? BIT.Utils.XD.PostMessage.send(message, options)
                                       : BIT.Utils.XD.LegacyMessage.send(message, options);
    },

    /*
     * BIT.Utils.XD.listen allows listening for cross domain messages sent via postMessage or legacy/proxy iframe messaging.
     */
    listen: function(callback, options) {
      BIT.Utils.Support.post_message() ? BIT.Utils.XD.PostMessage.listen(callback, options) 
                                       : BIT.Utils.XD.LegacyMessage.listen(callback, options);
    },

    // HTML5 postMessage is supported by FF3+, IE8+, Safari, Chrome, Opera 9.5+
    PostMessage: {
      send: function(message, options) {
        options = options || {};
        
        var target_window = BIT.Utils.XD.PostMessage.get_target_window(options.target_window_name, window);
        var target_origin = options.target_origin || "*";
        
        message = BIT.Utils.XD.encode_message(message);
  
        BIT.Utils.Log("Sending postMessage... target_window / message / options", target_window, message, options);
        target_window.postMessage(message, target_origin);
      },

      get_target_window: function(target_window_name, win) {
        return target_window_name ? win.frames[target_window_name] : win.parent;
      },
      
      listen: function(callback, options) {
        options = options || {};
        BIT.Utils.Event.observe("message", window, function(event) {
          var valid_origin = typeof options.origin == "undefined" || event.origin == options.origin;
          if (valid_origin && BIT.Utils.XD.BIT_MESSAGE_REGEX.test(event.data)) {
            var message_data = BIT.Utils.XD.decode_message(event.data);
            BIT.Utils.Log("Received BIT postMessage", event);
            callback(message_data);
          }
        });
      }
    },
    
    // For IE7 and other older browsers that do not support postMessage
    LegacyMessage: {
      FRAGMENT_REGEX: /#.*/,
      LISTEN_INTERVAL: 200,
      
      send: function(message, options) {
        var proxy_window_name = options.target_window_name + "-proxy";

        message = BIT.Utils.XD.encode_message(message, { target_window_name: options.target_window_name });
        
        var existing_proxy_frame = BIT.Utils.XD.Proxy.get_iframe(proxy_window_name);
        var delay = existing_proxy_frame ? 0 : 500;
        var proxy_frame = existing_proxy_frame || BIT.Utils.XD.Proxy.create_iframe(proxy_window_name);
        
        BIT.Utils.Log("Sending legacy message... target / message / options", proxy_frame, message, options);
        proxy_frame.src = proxy_frame.src.replace(BIT.Utils.XD.LegacyMessage.FRAGMENT_REGEX, "") + "#" + message;
        setTimeout(function() { 
          proxy_frame.width = proxy_frame.width == 50 ? 100 : 50;
        }, delay);
      },
      
      listen: function(callback, options) {
        var old_fragment = window.location.hash;
        var current_fragment;
        setInterval(function() {
          current_fragment = window.location.hash;
          if (BIT.Utils.XD.LegacyMessage.message_received(current_fragment, old_fragment)) {
            old_fragment = current_fragment;
            var message_data = BIT.Utils.XD.decode_message(current_fragment);
            BIT.Utils.Log("Received BIT Legacy message", current_fragment, message_data);
            callback(message_data);
          }
        }, BIT.Utils.XD.LegacyMessage.LISTEN_INTERVAL);
      },

      message_received: function(current_fragment, old_fragment) {
        return current_fragment != old_fragment && BIT.Utils.XD.BIT_MESSAGE_REGEX.test(current_fragment);
      }
    },
    
    decode_message: function(message_string) {
      message_string = message_string.replace(/^#/, "");
      var data = {};
      var pairs = message_string.split("&");
      for (var i=0; i<pairs.length; i++) {
        var key_value = pairs[i].split("=");
        data[key_value[0]] = decodeURIComponent(key_value[1]);
      }
      return data;
    },

    encode_message: function(message, extras) {
      if (typeof message == "object") {
        var message_data = [];
        for (key in message) {
          var pair = key + "=" + encodeURIComponent(message[key]);
          message_data.push(pair);
        }
        message = message_data.join("&");
      }
      // ensure the message data string BIT=true so BIT.Utils.XD.listen will not ignore it
      if (!BIT.Utils.XD.BIT_MESSAGE_REGEX.test(message)) { message += "&BIT=true"; }

      if (extras) {
        for (key in extras) {
          message += "&" + key + "=" + encodeURIComponent(extras[key]);
        }
      }

      return message;
    },
    
    BIT_MESSAGE_REGEX: /BIT=true/,

    Proxy: {
      TARGET_WINDOW_NAME_REGEX: /target_window_name=([^&]+)/i,
      
      forward_message: function(event) {
        var target_frame_name = window.location.hash.match(BIT.Utils.XD.Proxy.TARGET_WINDOW_NAME_REGEX)[1];
        if (!target_frame_name) { return };
        var target_window = parent.frames[target_frame_name];
        if (target_window.location.hash != window.location.hash) {
          target_window.location.hash = window.location.hash;      
        }
      },
      
      create_iframe: function(iframe_name) {
        var iframe = document.createElement("iframe");
        iframe.setAttribute('id', iframe_name);
        iframe.setAttribute('name', iframe_name);
        iframe.setAttribute("src", BIT.base_url + "/xd_proxy.html");
        iframe.setAttribute('frameBorder', '1'); // IE needs this otherwise resize event is not fired
        iframe.setAttribute('scrolling', 'auto');
        iframe.setAttribute('width', 50);  // Need a certain size othwerise IE7 does not fire resize event
        iframe.setAttribute('height', 50);
        iframe.setAttribute('style', "position: absolute; left: -200px; top:0px;");
        // IE needs this because setting style attribute is broken. No really.
        if (iframe.style.setAttribute) {
          iframe.style.setAttribute('cssText', "position: absolute; left: -200px; top:0px;");
        }
        document.body.appendChild(iframe);
        return iframe;
      },

      get_iframe: function(iframe_name) {
        return document.getElementById(iframe_name);
      },
      
      start: function() {
        if (window.addEventListener) {
          window.addEventListener('resize', BIT.Utils.XD.Proxy.forward_message, false);
        } else if (document.body.attachEvent) {
          window.attachEvent('onresize', BIT.Utils.XD.Proxy.forward_message);
        }
      }
    }
});
  

BIT.extend("Utils.Event", {
    /*
     * BIT.Utils.Event.observe provides a basic cross-browser event listener. Example:
     *
     *   BIT.Utils.Event.observe("click", document.getElementsByTagName("td"), function(event) {
     *     console.debug("this was clicked:", event.target);
     *   });
     *
     */
    observe: function(event_name, scope, callback, use_capture) {
      BIT.Utils.Log("adding event listener:", event_name, scope, callback, use_capture);

      // convert scope to an array if passed as a single DOM element or as window
      if (!scope.length || scope === window) { scope = [scope]; }

      // optional third argument to attachEvent/addEventListener
      if (typeof use_capture == "undefined") { use_capture = false; }

      // IE uses attachEvent and on<event_name>, other browsers use addEventListener and event_name
      var add_event_function = window.attachEvent ? "attachEvent" : "addEventListener";
      if (add_event_function == "attachEvent") { event_name = "on" + event_name; }

      for (var i=0; i<scope.length; i++) {
        scope[i][add_event_function](event_name, callback, use_capture);
      }
    },

    prevent_default: function(event) {
      if (event.preventDefault) {
        event.preventDefault();
      } else {
        event.returnValue = false;
      }
    }

});

// Basic browser detection based on user agent
BIT.extend("Utils.Browser", (function() {
  var Browser = {};
  // IE detection based on http://msdn.microsoft.com/en-us/library/ms537509%28v=vs.85%29.aspx
  var IE_REGEX = /MSIE ([0-9]{1,}[\.0-9]{0,})/i;
  if (IE_REGEX.test(navigator.userAgent)) {
    Browser.IE = true;
    var ie_version = parseFloat(navigator.userAgent.match(IE_REGEX)[1]);
    Browser.IE6 = ie_version < 7.0;
    Browser.IE7 = !Browser.IE6 && ie_version < 8.0;
  } else {
    Browser.IE = false;
    Browser.IE6 = false;
    Browser.IE7 = false;
  }
  // Opera detection
  Browser.Opera = !Browser.IE && typeof window.opera != "undefined";
  return Browser;
})());

BIT.FB = BIT.FB || {
  
  validate_access_token: function(access_token, callback) {
    FB.api("/me?access_token=" + access_token, function(response) {
      BIT.Utils.Log("validate access token response:", response);
      var valid_status = !response.error;
      callback(valid_status);
    });
  },

  check_permissions: function(permissions, callback) {
    if (FB.getAuthResponse() && FB.getAuthResponse().accessToken) {
      var granted = true;
      var query = FB.Data.query("SELECT {0} FROM permissions WHERE uid=me()", permissions);
      query.wait(function(rows) {
        for (permission in rows[0]) {
          if (rows[0][permission] != "1") { granted = false; }
        }
        callback(granted);
      });
    } else {
      callback(false);
    }
  },
  
  rsvp_event: function(facebook_event_id, rsvp_status, callback) {
    BIT.Utils.Log("Submitting RSVP:", facebook_event_id, rsvp_status);
    FB.api("/" + facebook_event_id + "/" + rsvp_status, 'POST', callback);
  },
  
  rsvp_wall_post: function(params) {
    var wall_post = BIT.FB.rsvp_wall_post_data(params);
    FB.api(wall_post, params.callback);
  },

  rsvp_wall_post_data: function(params) {
    var artist_event = params.artist_event;
    var artist = artist_event.artists[0];
    var caption;
    if (params.rsvp_status == "attending") {
      caption = "{*actor*} is going to see " + artist.name + "!";
    } else { 
      caption = "{*actor*} might go see " + artist.name + ".";
    }
    var more_tour_dates_url = BIT.RSVPDialog.widget.share_url;
    var fb_event_url = artist_event.facebook_event_url.replace(/came_from=\d+/, "came_from=" + BIT.came_from_codes.iga_rsvp_wall_post);
    return {
      "message": params.message, 
      "attachment": {
        "href": fb_event_url,
        "name": (artist_event.title + " - " + BIT.Widget.formatted_date(artist_event)),
        "media": [{
          "href": fb_event_url,
          "src": decodeURIComponent(artist.image_url),
          "type": "image"
        }],
        "caption": caption,
        "description": "",
        "properties": {
          "More Tour Dates": { "href": BIT.RSVPDialog.widget.share_url, "text": BIT.RSVPDialog.widget.share_url }
        }
      },
      "action_links": [{
        "href": artist_event.facebook_rsvp_url, 
        "text": "RSVP" 
      }],
      "method": "facebook.stream.publish"
    };
  },

  event_attendees: function(facebook_event_id, callback) {
    var query = FB.Data.query("SELECT uid FROM event_member WHERE rsvp_status='attending' AND eid={0}", facebook_event_id);
    query.wait(callback);
  }
};

BIT.Janrain = BIT.Janrain || {
  
  janrain_int: null,
  session: null,
  UUID_REGEX: /uuid=([a-z\d-]+)/i,

  capture_base_url: null,
  capture_client_id: null,

  token_url: function() {
    var redirect_uri = BIT.base_url + "/janrain_close_window.html";
    var domain = window.location.protocol + "//" + window.location.host;
    return [
      BIT.Janrain.capture_base_url + "/oauth/token_url_1",
      "?response_type=code",
      "&domain=", encodeURIComponent(domain),
      "&bp_channel=", encodeURIComponent(Backplane.getChannelID()),
      "&client_id=", encodeURIComponent(BIT.Janrain.capture_client_id),
      "&redirect_uri=", encodeURIComponent(redirect_uri)
    ].join("");
  },
  
  facebook_permissions_url: function(permissions) {
    return [
      "https://signup.universalmusic.com/facebook/start",
      "?ext_perm=", encodeURIComponent(permissions),
      "&token_url=", encodeURIComponent(BIT.Janrain.token_url())
    ].join("");
  },

  open_permissions_window: function(permissions) {
    this.perms_window = window.open(BIT.Janrain.facebook_permissions_url(permissions), "permissions", "width=580,height=500");

    // hide the RSVP dialog when the permissions window is closed
    var permissions_timer; 
    permissions_timer = setInterval(function() {
      if (BIT.Janrain.perms_window && BIT.Janrain.perms_window.closed) {
        BIT.RSVPDialog.hide();
        // stop checking if the window was closed
        clearInterval(permissions_timer);
      }
    }, 1000);
  },

  request_facebook_permissions: function(permissions, permissions_cb) {
    // open a popup window to approve permissions for the Universal app
    this.open_permissions_window(permissions);

    // notify backplane to begin checking more frequently for login messages
    Backplane.expectMessagesWithin(60, "identity/login");
    
    BIT.Utils.Log("setting Backplane subscribe");
    // wait for a login message from Backplane if the user grants permissions
    BIT.Janrain.bp_login_subscription = Backplane.subscribe(function(message) {
      if (message.type == "identity/login") {
        BIT.Utils.Log("Backplane login message:", message);
        
        // stop listening for Backplane login since it happened
        Backplane.unsubscribe(BIT.Janrain.bp_login_subscription);
        
        // retrieve the janrain uuid from the login message
        var uuid = BIT.Janrain.get_uuid(message.payload.identities, true);
        // check if the user logged into janrain with FB
        BIT.Janrain.get_facebook_credentials(uuid, function(fb_response) {
          BIT.Utils.Log("FB credentials response:", fb_response);
          // logged in with FB, and we have an access token
          if (fb_response.type == "success" && fb_response.data.accessToken) {
            // check if the access token is valid (should be since they just granted permissions)
            FB._session = null;
            BIT.FB.validate_access_token(fb_response.data.accessToken, function(valid_access_token) {
              if (valid_access_token) {
                BIT.Utils.Log("valid access token");
                // initialize the FB javascript SDK with the valid access token
                FB.Auth.setAuthResponse({ accessToken: fb_response.data.accessToken });
                // check if perms were granted and handle the response
                BIT.FB.check_permissions(permissions, permissions_cb);
                
              // invalid access token 
              } else {
                BIT.Utils.Log("invalid access token");
                permissions_cb(false);
              }
            });

          // no access token
          } else {
            BIT.Utils.Log("no access token");
            permissions_cb(false);
          }
        });
        
      }
    });
  },

  get_facebook_credentials: function(janrain_uuid, callback) {
    jQuery.ajax({
      url: BIT.base_url + "/janrain/facebook_credentials/" + janrain_uuid,
      dataType: "jsonp",
      success: function(data) { callback({ "type" : "success", "data": data }); },
      error: function(data) { callback({ "type" : "error", "data": data }); }
    });
  },
  
  get_user_session: function(callback) {
    jQuery.ajax({
      url: "http://api.echoenabled.com/v1/users/whoami?appkey=" + this.echo_api_key + "&sessionID=" + encodeURIComponent(Backplane.getChannelID()),
      dataType: "jsonp",
      success: function(data) { 
        callback({ "type": "success", "data": data }); 
      },
      error: function(xhr, textStatus, errorThrown) { 
        callback({ "type": "error", "data": { "xhr":xhr, "textStatus":textStatus, "errorThrown":errorThrown } }); 
      }
    });
  },
  
  enable_capture: function(capture_links) {
    var self = this;
    this.janrain_int = setInterval(function() {
      if (jQuery && jQuery.fn.enableCapture) {
        try {
          jQuery(capture_links).attr("target", "");
          jQuery(capture_links).enableCapture("signinLink");
          // when the user clicks a link, expect a janrain login message and refresh the page when one is received
          jQuery(capture_links).click(function(event) {
            Backplane.expectMessagesWithin(60, "identity/login");
            Backplane.subscribe(function(message) {
              if (message.type == "identity/login") {
                window.location.reload();
              }
            });
          });
        } catch (ex) {
          BIT.Utils.Log("BIT.Janrain.enable_capture failed:", ex);
        }
        clearInterval(self.janrain_int);
      }
    }, 1000);
  },
  
  // returns the user's janrain uuid for the account they are currently logged into.
  // poco is return data from the 'whoami' echo API call, or the 
  // message.payload.identities object from a Backplane "identity/login" message
  get_uuid: function(poco, use_first_account) {
    if (typeof use_first_account == "undefined") { use_first_account = false; }
    var logged_in_account, uuid;
    var accounts = poco.entry.accounts;
    if (use_first_account) {
      logged_in_account = accounts[0]; 
    } else {
      for (var i=0; i<accounts.length; i++) {
        if (accounts[i].loggedIn.toString() == "true") {
          logged_in_account = accounts[i];
          break;
        }
      }
    }
    if (!logged_in_account) { return; }
    return this.UUID_REGEX.test(logged_in_account.identityUrl) ? this.UUID_REGEX.exec(logged_in_account.identityUrl)[1] : null;
  },
  
  process_rsvp_links: function(rsvp_links) {
    // user is logged into janrain
    if (BIT.Janrain.session) {
      // user is logged into janrain with FB account and valid access token
      if (FB.getAuthResponse()) { 
        BIT.RSVPDialog.enable(rsvp_links); 
      } 
      // else user is logged into janrain with non-FB account or invalid access token - use standalone RSVP
      
    // user is not logged into janrain - enable capture
    } else {
      BIT.Janrain.enable_capture(rsvp_links);
    }
  },

  // callback when a widget is written to HTML. 
  // rsvp_links is an array of DOM elements which contains all RSVP links for a widget.
  widget_rendered_callback: function(widget) {
    jQuery(document).ready(function() {
  
      var rsvp_links = BIT.Utils.Element.search("a.bit-rsvp", widget.root_element());

      // wait until Backplane and FB are available
      if (typeof(Backplane)=="undefined" || typeof(FB)=="undefined" ) { 
        setTimeout(function() { BIT.Janrain.widget_rendered_callback(rsvp_links); }, 500);
        
      // FB and Backplane are loaded
      } else {
      
        // check if we already tried to load janrain session data (page with multiple widgets)
        if (BIT.Janrain.session != null) { 
          BIT.Janrain.process_rsvp_links(rsvp_links) 

        // load janrain session if this is the first try
        } else {
          BIT.Utils.Log("checking Janrain login status");
          BIT.Janrain.get_user_session(function(response) {
            if (response.type == "success") {
              // user is logged into janrain
              if (response.data.poco) {
                BIT.Utils.Log("logged into janrain");
              
                // prevent having to check Janrain login status again
                BIT.Janrain.session = true;
          
                // retrieve the janrain uuid
                var uuid = BIT.Janrain.get_uuid(response.data.poco);

                // check if the user logged into janrain with FB
                BIT.Janrain.get_facebook_credentials(uuid, function(fb_response) {
                  BIT.Utils.Log("FB credentials response:", fb_response);
                  // logged in with FB, and we have an access token
                  if (fb_response.type == "success" && fb_response.data.accessToken) {
                    // check if the access token is valid
                    FB._session = null;
                  
                    BIT.FB.validate_access_token(fb_response.data.accessToken, function(valid_access_token) {
                      if (valid_access_token) {
                        BIT.Utils.Log("valid access token");                      
                        // initialize the FB javascript SDK with the valid access token
                        FB.Auth.setAuthResponse({ accessToken: fb_response.data.accessToken });

                        // enable the RSVP dialog
                        BIT.RSVPDialog.enable(rsvp_links);

                      // logged in with FB, but access token is invalid and/or expired.  RSVP links goto standalone
                      } else {
                        BIT.Utils.Log("invalid access token");
                      }
                    });
              
                  // user is logged into janrain with a non-FB account, or no access token could be loaded
                  // rsvp links go to standalone page (default behavior)
                  } else {
                    BIT.Utils.Log("no access token");
                  }
                });
          
              // user is not logged into janrain
              } else {
                BIT.Utils.Log("not logged into Janrain");
                BIT.Janrain.session = false;
                BIT.Janrain.enable_capture(rsvp_links);
              }
      
            // error loading user data from janrain
            } else {
              BIT.Utils.Log("error loading Janrain session");
              BIT.Janrain.session = false;
              BIT.Janrain.enable_capture(rsvp_links);
            }
          });
        }
      }
    });
  },

  rsvp_form_html: "\
    <div class=\"popup-margin\">\
      <form action=\"\" class=\"rsvp-form\" id=\"rsvp-form\" method=\"post\" name=\"rsvp-form\">\
        <div class=\"error-message\" id=\"rsvp-error\" style=\"display: none;\">Something went wrong, please try again.<\/div>\
        <div class=\"message-container\">\
          <div class=\"say-something\">Say something about this event...<\/div>\
          <textarea id=\"rsvp_message\" name=\"rsvp_message\" class=\"message\"><\/textarea>\
          <input id=\"facebook_event_id\" name=\"facebook_event_id\" type=\"hidden\" value=\"\" />\
          <input id=\"rsvp_status\" name=\"rsvp_status\" type=\"hidden\" value=\"\" />\
        <\/div>\
        <div class=\"wall-post\">\
          <span class=\"artist-pic\" id=\"artist-pic\"><\/span>\
          <span class=\"attachment\" id=\"rsvp-preview-container\">\
            <div id=\"rsvp-preview\">\
              <div class=\"title\" id=\"event-title\"><\/div>\
              <div class=\"links\" id=\"message-links\"><\/div>\
            <\/div>\
            <div class=\"event-attendees\" id=\"rsvp-stats\">\
              <div class=\"loading\"></div>\
            </div>\
            <span class=\"buttons\" id=\"rsvp-buttons\">\
              <div class=\"blue-button-outer\"><div class=\"blue-button-inner\"><input type=\"submit\" name=\"rsvp_type\" value=\"I\'m Attending\" class=\"blue-button yes\" /><\/div><\/div>\
              <div class=\"blue-button-outer\"><div class=\"blue-button-inner\"><input type=\"submit\" name=\"rsvp_type\" value=\"No\" class=\"blue-button no\" /><\/div><\/div>\
              <div class=\"blue-button-outer\"><div class=\"blue-button-inner\"><input type=\"submit\" name=\"rsvp_type\" value=\"Maybe\" class=\"blue-button maybe\" /><\/div><\/div>\
              <div class=\"rsvp-loading\" id=\"rsvp-loading\" style=\"display:none;\"><\/div>\
              <div class=\"clear\"><\/div>\
              <div class=\"publish-to-wall\"><input checked=\"checked\" class=\"checkbox\" id=\"wall_post\" name=\"wall_post\" type=\"checkbox\" value=\"true\" /> Publish to wall<\/div>\
            <\/span>\
          <\/span>\
        <\/div>\
      <\/form>\
    <\/div>"
};

BIT.RSVPDialog = {         
  
  dialog_html: "\
    <div class=\"generic_dialog pop_dialog\" style=\"display:none\" id=\"bit-rsvp-dialog-container\">\
      <div class=\"generic_dialog_popup wide-popup\">\
        <div class=\"pop_container_advanced popup-border-outer\" id=\"bit-rsvp-dialog-y-offset\">\
          <div class=\"pop_container_advanced popup-border-inner\" id=\"bit-rsvp-dialog-border\"><\/div>\
          <div class=\"pop_content popup-content\" id=\"bit-rsvp-dialog-content-container\">\
            <h2 class=\"dialog_title\"><span id=\"bit-rsvp-dialog-title\">RSVP<\/span><\/h2>\
            <div class=\"dialog_content\">\
              <div class=\"dialog_body\" id=\"bit-rsvp-dialog-content\">\
                <div id=\"bit-rsvp-dialog-loading\"></div>\
              <\/div>\
              <div class=\"dialog_body\" id=\"bit-fan-app-content\">\
                <div class=\"popup-margin\">\
                  <div class=\"add-fan-app\">\
                    <strong>Would you like to add the Bandsintown application?<\/strong>\
                    <ul class=\"icons\">\
                      <li class=\"track\">Track your Facebook Likes, iTunes, Pandora, and Last.fm artists<\/li>\
                      <li class=\"notifications\">Get concert reminders and notifications<\/li>\
                      <li class=\"local\">Discover local concerts based on the music you like<\/li>\
                    <\/ul>\
                  <\/div>\
                <\/div>\
              <\/div>\
              <div class=\"dialog_buttons clearfix\">\
                <div class=\"rsvp-buttons\" id=\"bit-rsvp-dialog-buttons\">\
                  <label class=\"uiButton uiButtonLarge uiButtonDefault\">\
                    <input type=\"button\" class=\"rsvp-cancel\" value=\"Close\" id=\"bit-rsvp-cancel\" />\
                  <\/label>\
                <\/div>\
                <div class=\"rsvp-buttons\" id=\"bit-fan-app-buttons\">\
                  <a href=\"javascript:void(0);\" class=\"no-thanks\" id=\"bit-fan-app-cancel\">No, Thanks</a>\
                  <a href=\"http://www.bandsintown.com/facebookapp?came_from=80\"class=\"uiButton uiButtonLarge uiButtonConfirm\" target=\"_blank\" id=\"bit-fan-app-install\">\
                    <input type=\"button\" class=\"rsvp-install-app\" value=\"Yes\" \/>\
                  <\/a>\
                <\/div>\
              <\/div>\
            <\/div>\
          <\/div>\
        <\/div>\
      <\/div>\
    <\/div>",

  css: [
    "#bit-rsvp-dialog-container { position: fixed; width: 100%; height: 100%; margin: auto; }",
    "#bit-rsvp-dialog { color: #333333; direction: ltr; font-family: 'lucida grande',tahoma,verdana,arial,sans-serif; font-size: 11px; text-align: left; }",
    "#bit-rsvp-dialog a { color: #3B5998; cursor: pointer; text-decoration: none; }",
    "#bit-rsvp-dialog a:hover { text-decoration: underline; }",
    "#bit-rsvp-dialog textarea, #bit-rsvp-dialog .inputtext, #bit-rsvp-dialog .inputpassword { border: 1px solid #BDC7D8; font-family: 'lucida grande',tahoma,verdana,arial,sans-serif; font-size: 11px; padding: 3px; -webkit-appearance: none; -webkit-border-radius: 0; }",
    "#bit-rsvp-dialog ul { list-style-type: none; margin: 0; padding: 0; }",
    "#bit-rsvp-dialog label { color: #666666; cursor: pointer; font-weight: bold; vertical-align: middle; }",
    "#bit-rsvp-dialog img { border: 0 none; }",
    "body#bit-rsvp-dialog { margin: 0px; padding: 0px; }",
    "#bit-rsvp-dialog-loading { height: 32px; width: 100%; background: transparent url('http://static.bandsintown.com/images/facebook/ajax-loader-large.gif') no-repeat scroll center center; }",
    "body#bit-rsvp-dialog .wall-post .artist-pic { display: inline-block; text-align: center; width: 90px; overflow: hidden; }",
    "body#bit-rsvp-dialog .wall-post #artist-pic.hidden { visibility: hidden; }",
    "#bit-rsvp-dialog .wall-post .artist-pic img.shrink-tall-image { width: auto; max-height: 150px; }",
    "#bit-rsvp-dialog .wall-post .artist-pic img.shrink-wide-image { max-width: 90px; height: auto; }",
    "#bit-rsvp-dialog .pop_content{direction:ltr}",
    "#bit-rsvp-dialog {height:0;left:0;overflow:visible;outline:none;position:absolute;top:0;width:100%;z-index:250}",
    "#bit-rsvp-dialog .generic_dialog_popup{height:0;overflow:visible;position:relative;width:520px;margin:auto}",
    "#bit-rsvp-dialog .pop_content h2.dialog_title{background:#6d84b4;border:1px solid #3b5998;border-bottom:none;color:#fff;font-size: 14px;font-weight:bold;margin:0;}",
    "#bit-rsvp-dialog .pop_content h2 span{display:block;padding:5px 10px}",
    "#bit-rsvp-dialog .pop_content .dialog_content{background:#fff;border:1px solid #555;border-top-width:0}",
    "#bit-rsvp-dialog .pop_content .dialog_body{padding:10px;border-bottom:1px solid #ccc}",
    "#bit-rsvp-dialog .pop_content .dialog_buttons{background:#f2f2f2;padding:8px 10px 8px 10px;position:relative;text-align:right}",
    "#bit-rsvp-dialog .pop_container_advanced{-moz-border-radius:8px;-webkit-border-radius:8px;padding:10px}",
    "#bit-rsvp-dialog .uiButton,#bit-rsvp-dialog .uiButtonSuppressed:active,#bit-rsvp-dialog .uiButtonSuppressed:focus,#bit-rsvp-dialog .uiButtonSuppressed:hover{background:#eee url('http://static.bandsintown.com/images/facebook/silver-blue-bg.png') repeat 0 0;border:1px solid #999;border-bottom-color:#888;box-shadow:0 1px 0 rgba(0, 0, 0, .1);-moz-box-shadow:0 1px 0 rgba(0, 0, 0, .1);cursor:pointer;display:-moz-inline-box;display:inline-block;font-size:11px;font-weight:bold;line-height:normal !important;padding:2px 6px;text-align:center;text-decoration:none;vertical-align:top;white-space:nowrap}",
    "#bit-rsvp-dialog .uiButtonConfirm{background-color:#5b74a8;background-position:0 -48px;border-color:#29447e #29447e #1a356e}",
    "#bit-rsvp-dialog .uiButton:active,#bit-rsvp-dialog .uiButtonDepressed{background:#ddd;border-bottom-color:#999;box-shadow:0 1px 0 rgba(0, 0, 0, .05);-moz-box-shadow:0 1px 0 rgba(0, 0, 0, .05)}",
    "#bit-rsvp-dialog .uiButton .uiButtonText,#bit-rsvp-dialog .uiButton input{background:none;border:0;color:#333;cursor:pointer;display:-moz-inline-box;display:inline-block;font-family:'Lucida Grande', Tahoma, Verdana, Arial, sans-serif;font-size:11px;font-weight:bold;margin:0;outline:none;padding:1px 0 2px;white-space:nowrap}",
    "#bit-rsvp-dialog .uiButtonLarge,#bit-rsvp-dialog .uiButtonLarge .uiButtonText,#bit-rsvp-dialog .uiButtonLarge input{font-size:13px}",
    "#bit-rsvp-dialog .uiButtonSpecial .uiButtonText,#bit-rsvp-dialog .uiButtonSpecial input,#bit-rsvp-dialog .uiButtonSpecial.uiButtonDisabled .uiButtonText,#bit-rsvp-dialog .uiButtonSpecial.uiButtonDisabled input,#bit-rsvp-dialog .uiButtonConfirm .uiButtonText, #bit-rsvp-dialog .uiButtonConfirm input, #bit-rsvp-dialog .uiButtonConfirm.uiButtonDisabled .uiButtonText, #bit-rsvp-dialog .uiButtonConfirm.uiButtonDisabled input { color: #FFFFFF; }",
    "#bit-rsvp-dialog .uiButtonConfirm:active{background:#4f6aa3;border-bottom-color:#29447e}",
    "#bit-rsvp-dialog .popup-border-outer { z-index: 1; background: none; position: relative; }",
    "#bit-rsvp-dialog .popup-border-inner { background-color: #525252; -moz-opacity: 0.7; opacity: 0.7; position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; padding: 0; filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=70); }",
    "#bit-rsvp-dialog .popup-content { position: relative; z-index: 2; }",
    "#bit-rsvp-dialog h2 { padding: 0px; }",
    "#bit-rsvp-dialog .blue-button-outer { display: block; border-bottom: 1px solid #d9d9d9; float: left; margin-right: 4px; }",
    "#bit-rsvp-dialog .blue-button-inner { border: 1px solid #29447E; border-bottom: 1px solid #1a3563; cursor: pointer; }",
    "#bit-rsvp-dialog .blue-button { background-color: #5c79ac; border-top: 1px solid #8a9cc2; border-right: 1px solid #5c79ac; border-left: 1px solid #5c79ac; border-bottom: 1px solid #5c79ac; color: #ffffff !important; cursor: pointer; font-size:13px; font-weight:bold; padding: 1px 5px 2px 5px; margin: 0px; }",
    "#bit-rsvp-dialog .yes { width: 107px; }",
    "#bit-rsvp-dialog .no { width: 34px; }",
    "#bit-rsvp-dialog .maybe { width: 59px; }",
    "#bit-rsvp-dialog .clear { clear: both; }",
    "#bit-rsvp-dialog .say-something { font-size: 11px; font-weight: bold; color: #808080; margin-bottom: 10px; padding: 0px 10px; }",
    "#bit-rsvp-dialog .message-container { margin-bottom: 15px; }",
    "#bit-rsvp-dialog .publish-to-wall { display: block; margin: 15px 0px 10px 0px; color: #808080; }",
    "#bit-rsvp-dialog .publish-to-wall input.checkbox { margin-left: 0px; }",
    "#bit-rsvp-dialog .error-message { font-size: 12px; font-weight: bold; margin: auto auto 10px; padding: 10px; width: 440px; border: 1px solid #DD3C10; background-color: #FFEBE8; }",
    "#bit-rsvp-dialog .rsvp-loading { background: transparent url('http://static.bandsintown.com/images/facebook/ajax-loader.gif') center left no-repeat scroll; width: 16px; height: 25px; display: inline-block; margin: 0 10px; padding: 0; vertical-align: middle; }",
    "#bit-rsvp-dialog .wall-post { width: 462px; margin: auto; }",
    "#bit-rsvp-dialog .attachment { display: inline-block; padding-left: 16px; vertical-align: top; width: 350px; font-size: 11px;}",
    "#bit-rsvp-dialog .attachment .links { min-height: 30px; }",
    "#bit-rsvp-dialog .attachment .links div { color: #999999; padding-bottom: 3px; }",
    "#bit-rsvp-dialog .attachment .links div a { text-decoration: none; color: #3B5998; }",
    "#bit-rsvp-dialog .attachment .links a.attendance { color: #3B5998; padding: 0; cursor: pointer; text-decoration: none; }",
    "#bit-rsvp-dialog .attachment .links a.attendance:hover { text-decoration: underline; }",
    "#bit-rsvp-dialog .attachment .title { padding-bottom: 3px; font-weight: bold; }",
    "#bit-rsvp-dialog .attachment .title a { text-decoration: none; }",
    "#bit-rsvp-dialog .attachment .text { color: #808080; padding-bottom: 3px; }",
    "#bit-rsvp-dialog textarea { width: 450px; height: 45px; border: 1px solid #BBBBBB; display: block; margin: auto; padding: 5px; resize: none; }",
    "#bit-rsvp-dialog .grey-button-outer { display: block; border-bottom: 1px solid #d9d9d9; float: left; margin-right: 4px; }",
    "#bit-rsvp-dialog .grey-button-inner { border-color: #999999 #999999 #888888; border-width: 1px; border-style: solid; cursor: pointer; background: transparent url('/images/facebook/buttons_bg.png') repeat scroll 0 0; }",
    "#bit-rsvp-dialog input.grey-button { background: transparent; border: 1px solid transparent; color: #333333 !important; cursor: pointer; font-size:13px; font-weight:bold; padding: 1px 5px 2px 5px; margin: 0px; }",
    ".event-attendees { height: 69px; }",
    ".event-attendees .faces { padding: 0px 0px 6px 0px; margin: 0px; height: 33px; }",
    ".event-attendees .hidden { display: block; visibility: hidden; }",
    ".event-attendees .faces a { padding: 0px; margin: 0px; float: left; }",
    ".event-attendees .faces img { padding: 1px 1px 0px 0px; margin: 0px; }",
    ".event-attendees .rsvp-count { padding-bottom: 16px; color: #999999; }",
    ".event-attendees .loading { background: transparent url('http://static.bandsintown.com/images/facebook/ajax-loader.gif') no-repeat scroll center left; height: 72px; padding: 0px; margin: 0px; }",
    "* html #bit-rsvp-dialog .popup-border-inner  { display: none; }",
    "* html #rsvp-dialog-container { position: absolute; }",
    "#bit-rsvp-dialog .add-fan-app { padding: 12px; }",
    "#bit-rsvp-dialog .add-fan-app strong { font-size: 14px; font-weight: bold; color: #3f3f3f; }",
    "#bit-rsvp-dialog .add-fan-app .fbsuccessbox strong { font-size: 11px; }",
    "#bit-rsvp-dialog .add-fan-app .fberrorbox { font-size: 11px; text-align: center; }",
    "#bit-rsvp-dialog .add-fan-app .loading { background: transparent url(\"http://static.bandsintown.com/images/facebook/ajax-loader.gif\") center center no-repeat scroll; width: 16px; height: 11px; margin: auto; padding: 10px 0px; }",
    "#bit-rsvp-dialog .add-fan-app ul.icons { font-size: 12px; color: #666666; padding-top: 20px; }",
    "#bit-rsvp-dialog .add-fan-app .icons li { height: 35px; line-height: 35px; padding-left: 50px; }",
    "#bit-rsvp-dialog .add-fan-app .icons .notifications { background: transparent url(\"http://static.bandsintown.com/images/facebook/icons/notifications.png\") no-repeat scroll center left; }",
    "#bit-rsvp-dialog .add-fan-app .icons .local { background: transparent url(\"http://static.bandsintown.com/images/facebook/icons/local.png\") no-repeat scroll center left; }",
    "#bit-rsvp-dialog .add-fan-app .icons .track { background: transparent url(\"http://static.bandsintown.com/images/facebook/icons/track.png\") no-repeat scroll center left; }",
    "#bit-rsvp-dialog .rsvp-buttons a { font-size: 13px; font-weight: bold; line-height: 24px; padding: 0 15px; }",
    "#bit-rsvp-dialog .rsvp-buttons a.uiButton { padding: 2px 6px; }"
  ].join(""),

  iframe_height: "275px",
  iframe_width: "478px",
  
  show_event: function(artist_event) {
    if (!BIT.RSVPDialog.janrain_init_done) { BIT.RSVPDialog.janrain_init(); }
    BIT.RSVPDialog.artist_event = artist_event;
    
    var title = artist_event.title + " - " + BIT.Widget.formatted_date(artist_event);
    var tour_dates_url = BIT.RSVPDialog.widget.share_url;
    var image_url = artist_event.artists[0].image_url.replace("http://www.bandsintown.com", BIT.base_url);
    
    this.event_title.html("<a href=\"" + artist_event.facebook_event_url + "\" target=\"_blank\">" + BIT.Utils.Text.truncate(title, { length: 75 }) + "</a>");
    this.artist_pic.html("<img src=\"" + image_url + "\" width=\"90px\" />");
    this.message_links.html("<div class=\"link\">More Tour Dates: <a href=\"" + tour_dates_url + "\" target=\"blank\">" + BIT.Utils.URI.parse(tour_dates_url).host + "</a></div>");
      
    this.rsvp_status.val("");
    this.rsvp_message.val("");
    this.facebook_event_id.val(artist_event.facebook_event_id);
    
    this.load_rsvp_stats(artist_event.facebook_event_id);
    this.error_message.hide();
    this.show();
  },

  show: function() {
    this.container.style.display = 'block';
    this.show_rsvp_content();
  },

  hide: function() {
    if (!this.janrain_init_done) {
      this.hide_iframes();
      this.container.style.display = 'none';
    } else {
      $(this.container).hide();
      this.loading_image.hide();
      this.error_message.hide();
      this.rsvp_message.val("");
    }
  },

  rsvp: function(params) {
    var facebook_event_id = params.facebook_event_id;
    var rsvp_status = params.status;
    var wall_post = params.wall_post;
    var message = params.message;
    
    BIT.FB.rsvp_event(facebook_event_id, rsvp_status, function(rsvp_success) {
      if (rsvp_success) {
        BIT.Utils.Log("RSVP succeeded:", facebook_event_id, rsvp_status);
        if (wall_post) {
          BIT.FB.rsvp_wall_post({ 
            message: message, 
            rsvp_status: rsvp_status, 
            artist_event: BIT.RSVPDialog.artist_event,
            callback: function(response) { BIT.RSVPDialog.hide(); }
          });
        } else {
          BIT.RSVPDialog.hide();
        }
        
      // error handling for failed RSVP
      } else {
        BIT.Utils.Log("RSVP failed:", facebook_event_id, rsvp_status);
        BIT.RSVPDialog.error_message.show();
        BIT.RSVPDialog.loading_image.hide();
      }
    });
  },

  enable: function(rsvp_links) {
    $(rsvp_links).click(function(event) {
      if (!BIT.RSVPDialog.janrain_init_done) { BIT.RSVPDialog.janrain_init(); }

      var widget;
      var rsvp_link = $(event.target);
      var timestamp = /-(\d+)/.exec(rsvp_link.closest("table").attr("name"))[1];
      var event_id  = /\/event\/(\d+)/.exec(rsvp_link.attr("href"))[1];

      for (var i=0; i<BIT.widgets.length; i++) {
        widget = BIT.widgets[i];
        if (BIT.widgets[i].timestamp == timestamp) { break; }
      }

      for (var i=0; i<widget.artist_events.length; i++) {
        var artist_event = widget.artist_events[i];
        if (artist_event.id == event_id) {
          // show RSVP dialog if FB event exists
          if (artist_event.facebook_event_id) {
            event.preventDefault();
            event.stopPropagation();

            BIT.RSVPDialog.widget = widget;
            BIT.RSVPDialog.show_event(widget.artist_events[i]);
            break;
          }
          // else fall back to standalone RSVP if no FB event id
        }
      }
    });
  },

  insert_content: function() {
    if (BIT.RSVPDialog.content_added) { return; }
    var rsvp_dialog = document.createElement("div");
        rsvp_dialog.setAttribute("id", "bit-rsvp-dialog");
        rsvp_dialog.innerHTML = this.dialog_html;      
    document.getElementsByTagName("body")[0].appendChild(rsvp_dialog);
    BIT.RSVPDialog.content_added = true;
  },

  hide_iframes: function() {
    var iframes = this.container.getElementsByTagName("iframe");
    for (var i=0; i<iframes.length; i++) { 
      var iframe = iframes[i];
      iframe.onload = iframe.onreadystatechange = function() { 
        BIT.RSVPDialog.loaded_iframes[iframe.getAttribute("id")] = true; 
      }
      iframe.style.display = 'none';
    }
    BIT.RSVPDialog.dialog_loading.style.display = '';
  },

  show_iframe_and_event: function(params) {
    var iframe = params.iframe;
    var widget = params.widget;
    var bit_event_id = params.bit_event_id;

    if (this.loaded_iframes[iframe.getAttribute("id")]) {
      iframe.style.display = 'block';
      BIT.RSVPDialog.dialog_loading.style.display = 'none';
      this.show();
      var message = { "event_id": bit_event_id, "action": "show_event", "open_fb_event_after_rsvp": widget.open_fb_event_after_rsvp };
      BIT.Utils.XD.send(message, { target_window_name: iframe.name });
    } else {
      this.show();
      this.show_iframe_and_event_on_iframe_load(params);
    }
  },

  show_iframe_and_event_on_iframe_load: function(params) {
    var iframe = params.iframe;
    iframe.onload = iframe.onreadystatechange = function() {
      BIT.RSVPDialog.loaded_iframes[iframe.getAttribute("id")] = true;
      BIT.RSVPDialog.show_iframe_and_event(params);
    }
  },

  find_iframe_for_widget: function(widget_timestamp) {
    var iframes = BIT.RSVPDialog.container.getElementsByTagName("iframe");
    for (var i=0; i<iframes.length; i++) {
      if (iframes[i].getAttribute("id").match(widget_timestamp)) {
        return iframes[i];
      }
    }
  },

  show_fan_app_content: function() {
    this.content.style.display = 'none';
    this.buttons.style.display = 'none';
    this.fan_app_content.style.display = 'block';
    this.fan_app_buttons.style.display = 'block';
    this.title.innerHTML = "Stop Missing Shows!";
    this.center();
  },
  
  show_rsvp_content: function() {
    this.content.style.display = 'block';
    this.buttons.style.display = 'block';
    this.fan_app_content.style.display = 'none';
    this.fan_app_buttons.style.display = 'none';
    this.title.innerHTML = "RSVP";
    this.center();
  },

  min_dimensions_available: function() {
    var dimensions = BIT.Utils.Window.dimensions();
    return dimensions.width >= 505 && dimensions.height >= 400;
  },

  load_rsvp_stats: function(facebook_event_id) {
    var rsvp_stats_container = BIT.RSVPDialog.rsvp_stats_container;
    if (BIT.RSVPDialog.rsvp_stats[facebook_event_id] !== undefined) {
      rsvp_stats_container.html(BIT.RSVPDialog.rsvp_stats[facebook_event_id]);
      rsvp_stats_container.css("visibility", "visible");
      FB.XFBML.parse(document.getElementById(rsvp_stats_container.attr("id")), function() { BIT.RSVPDialog.center(); });
    } else {
      rsvp_stats_container.html("<div class=\"loading\"></div>");
      rsvp_stats_container.css("visibility", "visible");
      BIT.FB.event_attendees(facebook_event_id, function(fql_rows) {
        var rsvp_count = fql_rows.length;
        if (rsvp_count > 0) {
          var rsvp_count_text = "";
          if (rsvp_count == 1) {
            rsvp_count_text = "<div class=\"rsvp-count\">1 person going</div>";
          } else if (rsvp_count <= 998) {
            rsvp_count_text = "<div class=\"rsvp-count\">" + rsvp_count + " people going</div>";
          } else {
            rsvp_count_text = "<div class=\"rsvp-count\">1000+ people going</div>";
          }
          var fb_profile_pics = "";
          var max_pics = Math.min(10, rsvp_count);
          for (i=0; i<max_pics; i++) {
            fb_profile_pics += "<a target=\"_blank\" href=\"http://www.facebook.com/profile.php?id=" + fql_rows[i].uid + "\"><img src=\"http://graph.facebook.com/" + fql_rows[i].uid + "/picture?type=square\" height=\"32\" width=\"32\" /></a>";
          }
          rsvp_content = "<div class=\"faces\">" + fb_profile_pics + "</div>" + rsvp_count_text;
          BIT.RSVPDialog.rsvp_stats[facebook_event_id] = rsvp_content;
          rsvp_stats_container.html(rsvp_content);
          FB.XFBML.parse(document.getElementById(rsvp_stats_container.attr("id")), function() { BIT.RSVPDialog.center(); });
        } else {
          rsvp_stats_container.css("visibility", "hidden");
        }
      });
    }
  },

  // based on http://david-tang.net/plugins/fixedCenter/fixedCenter.html
  center: function() {
    if (BIT.RSVPDialog.container.style.display == 'none') { return; }

    var center_type = BIT.Utils.Window.is_iframe() ? "center_for_iframe" : "center_for_window";
    
    BIT.RSVPDialog[center_type]();
  },

  // center with fixed position based on window height and dialog height
  center_for_window: function() {
    var dialog_height = BIT.Utils.Element.get_outer_height(this.y_offset);
    var window_height = document.documentElement ? document.documentElement.clientHeight : window.innerHeight;
    var new_top = ( window_height/2 - dialog_height/2 );
    if (!BIT.Utils.Support.fixed_position()) { new_top += BIT.Utils.Element.get_scroll_top(); }
    this.y_offset.style.top = parseInt(new_top) + "px";
  },
  
  // center with absolute position based on the position of RSVP link that was clicked and dialog height
  center_for_iframe: function() {
    BIT.RSVPDialog.container.style.position = 'absolute';

    var dialog_height       = BIT.Utils.Element.get_outer_height(this.y_offset);
    var frame_scroll_offset = BIT.Utils.Element.get_scroll_top();
    var root_position       = BIT.Utils.Element.position(BIT.Utils.Element.get_parent(this.clicked_rsvp_link, "div"));
    var link_position       = BIT.Utils.Element.position(this.clicked_rsvp_link);

    // initial new top position is frame scroll offset + center of RSVP link - 1/2 dialog height
    var new_top = parseInt(frame_scroll_offset + link_position.vertical_center - (dialog_height / 2));

    // ensure the new top is not so low that part of the dialog is below the bottom of the widget
    var new_top_min = parseInt(root_position.bottom + frame_scroll_offset - dialog_height);
    new_top = Math.min(new_top, new_top_min);

    // ensure the new top is not hidden by being cut off at the top, at 0px the border is still cut off.  
    new_top = Math.max(3, new_top);

    this.y_offset.style.top = new_top + "px";
  },

  janrain_init: function() {
    if (this.janrain_init_done) { return; }

    // call base .init() if not already done
    if (!this.init_done) { this.init(); }
  
    // add RSVP form html
    this.content.innerHTML = BIT.Janrain.rsvp_form_html;
  
    // setup scope for dialog elements
    this.root = jQuery("#bit-rsvp-dialog");
    
    // error message, loading image
    this.loading_image = jQuery("#rsvp-loading", this.root);
    this.error_message = jQuery("#rsvp-error", this.root);

    // form inputs
    this.form              = jQuery("#rsvp-form", this.root);
    this.rsvp_message      = jQuery("#rsvp_message", this.form);
    this.rsvp_status       = jQuery("#rsvp_status", this.form);
    this.facebook_event_id = jQuery("#facebook_event_id", this.form);
    this.wall_post         = jQuery("#wall_post", this.form);

    // rsvp wall post preview
    this.artist_pic    = jQuery("#artist-pic", this.root);
    this.event_title   = jQuery("#event-title", this.root);
    this.message_links = jQuery("#message-links", this.root);

    // rsvp faces
    this.rsvp_stats = {};
    this.rsvp_stats_container = jQuery("#rsvp-stats", this.root);
    
    // prevent form submission
    this.form.submit(function(event) {
      event.preventDefault();
      event.stopPropagation();
    });
    
    // click handler for RSVP buttons
    jQuery("input[name=rsvp_type]", this.form).click(function(event) {
      BIT.RSVPDialog.loading_image.show();
      BIT.RSVPDialog.error_message.hide();

      BIT.Utils.Log("clearing Backplane subscribe");
      Backplane.unsubscribe(BIT.Janrain.bp_login_subscription);

      var status;
      var button = jQuery(event.target).val();
      if (button == "I'm Attending") { status = "attending"; }
      if (button == "No")            { status = "declined";  }
      if (button == "Maybe")         { status = "maybe";     }

      var wall_post = BIT.RSVPDialog.wall_post.attr("checked");
      var permissions = wall_post ? "rsvp_event,offline_access,publish_stream" : "rsvp_event,offline_access";
      
      var rsvp_params = {
        "facebook_event_id": BIT.RSVPDialog.facebook_event_id.val(),
        "status": status,
        "wall_post": wall_post,
        "message": BIT.RSVPDialog.rsvp_message.val()
      };
      
      BIT.FB.check_permissions(permissions, function(granted) {
        // all required permissions are granted, do the RSVP
        if (granted) {
          BIT.RSVPDialog.rsvp(rsvp_params);

        // RSVP links open the perms request window for Universal if not all permissions are granted
        } else {
          BIT.Janrain.request_facebook_permissions(permissions, function(granted) {
            if (granted) { BIT.RSVPDialog.rsvp(rsvp_params); }
          });
        }
      });
    });

    this.janrain_init_done = true;
  },

  init: function() {
    if (this.init_done) { return; }
    this.insert_content();
    this.loaded_iframes = {};
    this.clicked_rsvp_link = null;
    
    // visibility and positioning elements
    this.outer_container  = document.getElementById("bit-rsvp-dialog");
    this.container        = document.getElementById("bit-rsvp-dialog-container");
    this.border           = document.getElementById("bit-rsvp-dialog-border");
    this.y_offset         = document.getElementById("bit-rsvp-dialog-y-offset");
    this.content          = document.getElementById("bit-rsvp-dialog-content");
    this.dialog_loading   = document.getElementById("bit-rsvp-dialog-loading");
    this.buttons          = document.getElementById("bit-rsvp-dialog-buttons");
    this.fan_app_content  = document.getElementById("bit-fan-app-content");
    this.fan_app_buttons  = document.getElementById("bit-fan-app-buttons");
    this.title            = document.getElementById("bit-rsvp-dialog-title");
    
    // click callback function for cancel buttons
    var close_dialog = function(event) { BIT.RSVPDialog.hide(); }
    
    // RSVP cancel button
    this.cancel = document.getElementById("bit-rsvp-cancel");
    BIT.Utils.Event.observe("click", this.cancel, close_dialog);

    // fan app cancel button
    this.fan_app_cancel = document.getElementById("bit-fan-app-cancel");
    BIT.Utils.Event.observe("click", this.fan_app_cancel, close_dialog);
    
    // install fan app button
    this.fan_app_install = document.getElementById("bit-fan-app-install");
    BIT.Utils.Event.observe("click", this.fan_app_install, close_dialog);

    // center the dialog on window resize (only when the widget is not in an iframe)
    if (!BIT.Utils.Window.is_iframe()) {
      var resize_target = document.onresize ? document : window;
      BIT.Utils.Event.observe("resize", resize_target, function(event) { BIT.RSVPDialog.center() });  
    }

    // adjust iframe height and recenter after event data loads
    BIT.Utils.XD.listen(function(data) {
      if (data.action != "update_iframe_height") { return; }
      document.getElementById(data.iframe_id).style.height = data.height + "px";
      BIT.RSVPDialog.center();
    });

    // close RSVP dialog after successful RSVP
    BIT.Utils.XD.listen(function(data) {
      if (data.action != "close_rsvp_dialog") { return; }
      BIT.RSVPDialog.hide();
    });

    // show install fan app dialog after initial RSVP
    BIT.Utils.XD.listen(function(data) {
      if (data.action != "show_fan_app_dialog") { return; }
      BIT.RSVPDialog.show_fan_app_content();
    })
    
    // fix popup border issue in IE7
    this.border.style.display = 'block';

    this.init_done = true;
  }
};

BIT.Widget = function(options) {
  var self = this;

  this.unique_id = function(id_base) {
    return [id_base, this.timestamp].join("-");
  }

  this.artist            = options.artist;
  this.text_color        = options.text_color;
  this.link_color        = options.link_color;
  this.bg_color          = options.bg_color;
  this.separator_color   = options.separator_color   || '#E9E9E9';
  this.width             = options.width             || '100%';
  this.display_limit     = options.display_limit     || Number.MAX_VALUE;
  this.rsvp_links        = options.rsvp_links        != false;
  this.notify_me         = options.notify_me         != false;
  this.share_links       = options.share_links       != false;
  this.share_url         = options.share_url         || window.location.href;
  this.facebook_comments = options.facebook_comments != false;
  this.myspace_layout    = options.myspace_layout    || false;
  this.div_id            = options.div_id;
  this.rsvp_dialog       = options.rsvp_dialog       != false; 
  this.facebook_page_id  = String(options.facebook_page_id || '');
  this.event_data_links  = options.event_data_links  != false;
  this.open_fb_event_after_rsvp = options.open_fb_event_after_rsvp != false;

  this.source = options.source;
  if (typeof(this.source) == 'string' && this.source.toLowerCase() == 'iga' && options.janrain) {
    this.use_janrain_rendered_callback = true; 
    BIT.Janrain.echo_api_key           = options.janrain.echo_api_key;
    BIT.Janrain.capture_client_id      = options.janrain.capture_client_id;
    BIT.Janrain.capture_base_url       = options.janrain.capture_base_url;
  }
  
  this.prefix            = options.prefix                          || 'js';
  this.affil_code        = this.prefix + "_" + (options.affil_code || window.location.host);
  this.app_id            = this.prefix + "_" + (options.app_id     || window.location.host);
  
  this.timestamp         = new Date().getTime();
  this.unique_classname  = this.unique_id('bit');
  this.loader_div_id     = this.unique_id('bit-widget-loader');

  this.bandsintown_footer_link = options.bandsintown_footer_link != false;
  this.force_narrow_layout     = options.force_narrow_layout || false;
  
  this.local_events_loaded        = false;
  this.artist_events_loaded       = false;
  this.event_descriptions_hash    = {};
  this.rsvp_links_hash            = {};
  this.facebook_comments_xid_hash = {};
  this.ticket_types_hash          = {};
  this.pending_writes = [];    
  this.write_pending  = false;

  /*
   * The insert_events() function renders the widget:
   *
   *   1. A loading image is inserted.
   *   2. Bandsintown API requests are made with JSONP callbacks (1 request for local upcoming events, 1 for all upcoming events)
   *   3. When both requests have finished, the loading image is replaced with the event data.
   */
  this.insert_events = function() {
    if (this.artist) {
      this.write(this.loading_image());
      
      BIT.Utils.Log("Setting up local events JSONP");
      // load local events
      BIT.Utils.JSONP.getJSON({ 
        url: this.local_events_url(), 
        callback: this.local_events_callback, 
        callback_obj: this
      });

      BIT.Utils.Log("Setting up artist events JSONP");
      // load artist events
      BIT.Utils.JSONP.getJSON({ 
        url: this.artist_events_url(), 
        callback: this.artist_events_callback, 
        callback_obj: this
      });
    } else {
      this.write(this.no_artist_given());
    }
  }

  this.write = function(html, retry) {
    if (typeof(retry) == "undefined") { retry = false; }
    // If the widget was created without specifying a div_id, assume 
    // it should be written to the page in place using document.write.
    // Assign this.div_id to a unique id and create a div which will contain the widget.
    if (!this.div_id) {
      this.div_id = this.unique_id("widget-container");
      document.write("<div id=\"" + this.div_id + "\"></div>");
    }
  
    // If the div where the widget should be rendered exists and no content is pending
    if (document.getElementById(this.div_id) && !this.write_pending) {
      document.getElementById(this.div_id).innerHTML = html;

    // the widget div does not exist in the document yet, or content already has been queued
    } else {

      // if this is not a retry, html is new content and should be added to the pending_writes queue
      if (!retry) { this.pending_writes.push(html); }

      // if this is the first call to this.write, setup retries
      if (!this.write_pending) {
        this.write_pending = true;
        setTimeout(function() { self.retry_write(); }, 13);
      }
    }
  }

  // used when widget content loads before the div containing the widget is ready
  this.retry_write = function() {
    
    // div is ready, write the most recently added pending content
    if (document.getElementById(this.div_id)) {
      this.write_pending = false;
      this.write(this.pending_writes[this.pending_writes.length - 1], true);

    // div is still not ready, retry
    } else { 
      setTimeout(function() { self.retry_write(); }, 13);
    }
  }

  // called when widget content is ready to be written to the page (after Bandsintown API calls have finished)
  this.render = function() {
    this.write(this.to_html(this.artist_events, this.local_events));
    this.invert_description_link_colors();
    if (this.use_janrain_rendered_callback) { 
      BIT.Janrain.widget_rendered_callback(this);
    } else {
      this.rendered_callback();
    }
  }

  this.loading_image = function() {
    return [
      "<div id=\"" + this.loader_div_id + "\" style=\"text-align:center;\">",
        "<img src=\"http://static.bandsintown.com/images/widget-ajax-loader.gif\" />",
      "</div>"
    ].join("");
  }

  // returns the memoized root element of this widget 
  this.root_element = function() {
    if (this._root_element) { return this._root_element; }
    var classname_regex = new RegExp( this.unique_classname );
    var divs = document.getElementsByTagName('div');
    for ( var i=0; i<divs.length; i++ ) {
      var div = divs[i];
      if ( classname_regex.test(div.className) ) { this._root_element = div; }
    }
    return this._root_element;
  }

  /*
    Used for the links to expand/collapse event descriptions.
    For each link with a matching name that is a child element of this.root_element,
    the background color is set to the current text color, and the text color is set to
    either black or white based on how light or dark the new background color is.
  */
  this.invert_description_link_colors = function() {
    var links = BIT.Utils.Element.search("a.bit-event-description-link", this.root_element());
    for (i=0; i<links.length; i++) {
      var link = links[i];
      if (BIT.Utils.Element.get_parent(link, 'div') == this.root_element()) {
        var current_color = BIT.Utils.Element.get_computed_style(link, 'color');
        link.style.backgroundColor = current_color;
        var white_or_black = BIT.Utils.Color.image_color_for_background_color( current_color );
        var background_image = 'url("http://static.bandsintown.com/images/widget/plus_' + white_or_black + '.gif")';
        link.style.backgroundImage = background_image;
        link.style.backgroundPosition = 'center center';
        link.style.backgroundRepeat = 'no-repeat';
      }
    }
  }

  this.table_header = function(type) {
  
    var table_id    = "bit-" + type + "-events";
    var table_name  = this.unique_id("bit-" + type + "-events");
    var table_style = type == "local" ? "display:none;" : "";
  
    if (type == 'upcoming') {
      var event_links = "<span class='bit-header-links'>Upcoming | <a href='javascript:void(0);' onclick='return BIT.Widget.toggle_events(this);'>Local Dates</a></span>";
    } else {
      var event_links = "<span class='bit-header-links'><a href='javascript:void(0);' onclick='return BIT.Widget.toggle_events(this);'>Upcoming</a> | Local Dates</span>";
    }

    var bit_header_content = ["<div class='bit-header-overflow-fix'>", event_links, this.artist_share_links(), "</div>" ].join("");

    if (this.use_narrow_layout()) {
      return [
        "<table cellpadding='0' cellspacing='0' class='bit-events-narrow' style='" + table_style + "' id='" + table_id + "' name='" + table_name + "'><tbody>",
          "<tr class='bit-header-narrow'>",
            "<th colspan='" + this.adjusted_colspan(3) + "'>" + bit_header_content + "</th>",
          "</tr>"
      ].join("");
    } else {
      return [
        "<table cellpadding='0' cellspacing='0' class='bit-events' style='" + table_style + "' id='" + table_id + "' name='" + table_name + "'><tbody>",
          "<tr class='bit-header'>",
            "<th class='bit-description-links'>&nbsp;</th>",
            "<th colspan='" + this.adjusted_colspan(4) + "'>" + bit_header_content + "</th>",
          "</tr>"
		  /*,
          "<tr>",
            "<th class='bit-description-links'>&nbsp;</th>",
            "<th class='bit-date'>Date</th>",
            "<th class='bit-venue'>Venue</th>",
            "<th class='bit-location'>Location</th>",
            "<th class='bit-tickets' colspan='" + this.adjusted_colspan(1) + "'>Tickets</th>",
          "</tr>"*/
      ].join("");
    } 
  }

  this.table_footer = function(events, type) {
    var html = [];
    var colspan = this.adjusted_colspan( this.use_narrow_layout() ? 4 : 5 );

    if (this.display_limit < events.length) {
      html.push("<tr class='bit-bottom'><td colspan='" + colspan + "'><a href='javascript:void(0);' onClick='BIT.Widget.show_all_events(this)'>Show All Dates</a></td></tr>");
    }

    if (this.bandsintown_footer_link) {
      html.push("<tr class='bit-bottom'><td colspan='" + colspan + "'><a href='http://www.bandsintown.com/facebookapp?came_from=" + BIT.came_from_codes.footer + "' target='_blank' class='bit-logo' title='Bandsintown'></a><a href='http://www.bandsintown.com/facebookapp?came_from=" + BIT.came_from_codes.footer + "' target='_blank'>Bandsintown</a></td></tr>");
    }
  
    html.push("</tbody></table>");
    return html.join('\n');
  }

  this.to_html = function(upcoming_events, local_events) {
    // IE fix so the widget CSS works correctly:
    //   http://allofetechnical.wordpress.com/2010/05/21/ies-innerhtml-method-with-script-and-style-tags/
    var html = ["<div style=\"display:none\">&nbsp;</div>"];
    html.push(this.css());
    html.push("<div id='bit-events' name='bit-events' class='" + this.unique_classname + "'>");
  
    html.push(this.table_header('upcoming'));
    if (upcoming_events.length > 0) {
      for (var index = 0; index < upcoming_events.length; ++index) {
        var event = upcoming_events[index];
        var event_type = index < this.display_limit ? '' : 'bit-hidden';
        html.push(this.event_html(event, event_type)); 
      }
    } else {
      html.push(this.no_dates_message('upcoming'));
    }
    html.push(this.table_footer(upcoming_events, 'upcoming'));

    html.push(this.table_header('local'));
    if (local_events.length > 0) {
      for (var index = 0; index < local_events.length; ++index) {
        var event = local_events[index];
        var event_type = index < this.display_limit ? '' : 'bit-hidden';
        html.push(this.event_html(event, event_type)); 
      }
    } else {
      html.push(this.no_dates_message('local'));
    }
    html.push(this.table_footer(local_events, 'local'));

    html.push('</div>');
    return html.join('');
  }

  this.details_link_or_nbsp = function(event) {
    if (this.event_descriptions_hash[event.id]) {
      var layout_allows_comments = this.use_narrow_layout() ? this.myspace_layout : true;
      var onclick = this.show_facebook_comments() && layout_allows_comments ? this.toggle_event_details_js(event) : this.toggle_event_description_js(event);
      var id = "bit-event-description-link-" + event.id;
      return "<a href=\"javascript:void(0);\" class=\"bit-event-description-link\" onclick=\"" + onclick + "\" id=\"" + id + "\">&nbsp;</a>";
    } else {
      return "&nbsp;";
    }
  }

  this.adjusted_colspan = function(base_colspan) {
    var colspan = base_colspan;
    if (this.show_rsvp_links()) { colspan += 1; }
    if (this.show_facebook_comments()) { colspan += 1; }
    return colspan;
  }

  this.wide_event_html = function(event, event_type) {
    var html = [
      "<tr class='" + event_type + "' id='bit-event-" + event.id + "' >",
        "<td class='bit-description-links' style='height:78px;'>", 
		this.details_link_or_nbsp(event), 
		"</td>",
        "<td class='bit-date' style='width:57px;color:#000;'><span class=\"bit-event-data\">", //border-right:1px solid #8B281F;
		BIT.Widget.formatted_date(event), 
		"</span></td>",
        "<td class='bit-venue' class='infos' style='background:url(http://www.chimenebadi.fr/wordpress/wp-content/themes/chimene/images/backgrounds/grey.png);width:220px;'>",
		"<div style='border-right:1px solid #8B281F;'>",
		"<span class=\"bit-event-data\" >", 
		event.venue.name, 
		"</span></div></td>",
        "<td class='bit-location' style='background:url(http://www.chimenebadi.fr/wordpress/wp-content/themes/chimene/images/backgrounds/grey.png);padding-left:20px;'><span class=\"bit-event-data\">", 
		this.formatted_location(event), 
		"</span></td>",
        "<td class='bit-tickets' style='background:url(http://www.chimenebadi.fr/wordpress/wp-content/themes/chimene/images/backgrounds/grey.png);'>", 
		this.ticket_link(event), 
		"</td>",
       this.facebook_comments_td(event),
        this.wide_layout_rsvp_td(event),
      "</tr>"
    ];
  
    if (this.show_facebook_comments()) {
      html.push(this.event_details_row(event, event_type));
    } else if (this.event_descriptions_hash[event.id]) {
      html.push(this.event_description_row(event));
    }
    return html.join("");
  }

  this.event_details_row = function(event) {
    var colspan = this.use_narrow_layout() ? 5 : 7;
    var name    = this.unique_id("bit-event-details");
    var row_id  = "bit-event-details-" + event.id;
  
    if (this.event_descriptions_hash[event.id]) {
      var description_content = [
        "<div class='bit-details-title'>Event Details:</div>",
        "<div class='bit-details-text'>",
          "<div class='bit-details-description'>", this.truncate_and_process_description(this.event_descriptions_hash[event.id]), "</div>",
        "</div>"
      ].join("");
    } else {
      var description_content = [
        "<div class='bit-details-title' style='display:none;'></div>",
        "<div class='bit-details-text' style='display:none;'></div>"
      ].join("");
    }
  
    return [
      "<tr class='bit-event-details' name='" + name + "' " + "id='" + row_id + "' style='display:none;'>",
        "<td colspan='" + colspan + "' class='bit-details'>",
          description_content,
          "<div class='bit-details-comments'></div>",
        "</td>",
      "</tr>"
    ].join("");
  }

  this.event_description_row = function(event) {
    return [
      "<tr class='bit-event-description bit-dashed-border' style='display:none;'>",
        "<td class='bit-description-links'>&nbsp;</td>",
        "<td class='bit-date'>&nbsp;</td>",
        "<td colspan='" + this.adjusted_colspan(3) + "' class='bit-description'>" + this.process_description(this.event_descriptions_hash[event.id]) + "</td>",
      "</tr>"
    ].join("");
  }

  this.narrow_event_html = function(event, event_type) {
    var html = [
      "<tr class='" + event_type + "' id='bit-event-data-" + event.id + "'>",
        "<td class='bit-description-links'>", this.details_link_or_nbsp(event), "</td>",
        "<td class='bit-date'><span class=\"bit-event-data\">", BIT.Widget.formatted_date(event), "</span></td>",
        "<td class='bit-concert'>", this.narrow_layout_middle_column_html(event), "</td>",
        this.facebook_comments_td(event),
        this.narrow_layout_right_column_html(event),
      "</tr>"
    ];
  
    if (this.show_facebook_comments() && this.myspace_layout) {
      html.push(this.event_details_row(event, event_type));
    } else if (this.event_descriptions_hash[event.id]) {
      html.push(this.event_description_row(event));
    }
    return html.join("");
  }

  this.narrow_layout_right_column_html = function(event) {
    if (this.show_rsvp_links()) {
      return ["<td class='bit-rsvp'>", this.rsvp_link(event), "</td>"].join("");
    } else {
      return ["<td class='bit-tickets'>", this.ticket_link(event), "</td>"].join("");
    }
  }

  // the middle column in narrow layout contains venue name, location, and ticket link if RSVP is enabled and tix available, separated by line breaks.
  this.narrow_layout_middle_column_html = function(event) {
    var html = ["<span class=\"bit-event-data\">", event.venue.name, "<br/><strong>", this.formatted_location(event), "</strong></span>"].join("");
    if (this.show_rsvp_links()) {
      var ticket_link = this.ticket_link(event);
      if (ticket_link != '&nbsp;') { html += '<br/>' + ticket_link; }
    }
    return html;
  }

  this.rsvp_link = function(event) {
    return "<a target='_blank' class='bit-rsvp' href='" + this.rsvp_links_hash[event.id] + "&came_from=" + BIT.came_from_codes.rsvp + "'>RSVP</a>";
  }

  this.event_html = function(event, event_type) {
    return this.use_narrow_layout() ? this.narrow_event_html(event, event_type) : this.wide_event_html(event, event_type);    
  }

  this.formatted_location = function(event) {
    if (event.venue.country.toLowerCase() == 'united states') {
      return event.venue.city + ', ' + event.venue.region;
    } else {
      return event.venue.city + ', ' + event.venue.country;
    }
  }
  
  this.artist_param = function() {
    var param = this.api_artist || this.artist;
    param = param.replace(/\?/g, encodeURIComponent("?"));
    param = param.replace(/\//g, encodeURIComponent("/"));
    param = encodeURIComponent(param);
    param = param.replace(/'/g, escape("'"));
    param = param.replace(/"/g, escape("\""));
    return param;
  }

  this.facebook_page_id_param = function() {
    var all_digits = /^\d+$/;
    if (all_digits.test(this.facebook_page_id)) { 
      return 'fbid_' + this.facebook_page_id;
    } else {
      return null;
    }
  }

  this.ticket_link = function(event) {
    if (event.ticket_status == 'available') {
      // v2 responses have the artist param already appended to the ticket_url, v1 responses do not
      var artist_param = event.ticket_url.match(/artist=/) ? '' : ('?artist=' + this.artist_param());
      var href = event.ticket_url + artist_param + '&affil_code=' + encodeURIComponent(this.affil_code);
      return '<a target="_blank" class="bit-buy-tix" href="' + href + '">' + this.ticket_types_hash[event.id] + '</a>';
    } else {
      return '&nbsp;';
    }
  }

  this.wide_layout_rsvp_td = function(event) {
    if (this.show_rsvp_links()) {
      return '<td class="bit-rsvp">' + this.rsvp_link(event) + '</td>';
    } else {
      return '';
    }
  }

  this.facebook_comments_td = function(event) {
    var layout_allows_comments = this.use_narrow_layout() ? this.myspace_layout : true;  
    if (this.show_facebook_comments() && layout_allows_comments) {
      var name    = this.unique_id("bit-comments-button");
      var onclick = this.toggle_event_details_js(event);
      return [
        "<td class=\"bit-comment\">",
          "<a href=\"#\" class=\"bit-comment\" name=\"", name, "\" onclick=\"", onclick, "\"></a>",
        "</td>"
      ].join("");
    } else {
      return "";
    }
  }

  this.css = function() {
    var css = [
      ".bit-events, .bit-events-narrow {overflow: hidden;display: table;width:", this.width, ";font-family:arial,helvetica,sans-serif;font-size:13px;}",
      ".bit-events th, .bit-events td {width: auto;text-align: left; padding: 4px;vertical-align:middle;}",
      ".bit-events td {height: 36px; background:none;border-top: 1px solid " + this.separator_color + ";}",
      ".bit-events-narrow td {width:auto; height:57px; background:none;padding:4px; border-top: 1px solid " + this.separator_color + ";vertical-align:middle;}",
      (this.text_color ? "#bit-events td, #bit-events th { color: " + this.text_color + ";}" : ""),
      "#bit-events td.bit-tickets, #bit-events th.bit-tickets {width:55px; padding-right: 8px;}",
      "#bit-events td.bit-actions a, #bit-events td.bit-rsvp a { float: right; }",
      "#bit-events td.bit-rsvp { width: 42px; padding-right: 8px; }",
      "#bit-events td.bit-comment { width: 21px; padding-left: 8px; padding-right: 8px; }",
      "#bit-events td a.bit-rsvp { background:#EEEEEE url('http://static.bandsintown.com/images/widget/rsvp_bg.png') repeat 0 0; border:1px solid #999999; border-top-color:#888888; box-shadow:0 1px 0 rgba(0, 0, 0, .1); -moz-box-shadow:0 1px 0 rgba(0, 0, 0, .1);cursor:pointer; display:-moz-inline-box; display:inline-block; font-size:11px; font-weight:bold; line-height:normal !important; text-align:center; text-decoration:none; vertical-align:top; white-space:nowrap; font-family: 'Lucida Grande',Tahoma,Verdana,Arial,sans-serif; color: #333333; padding: 2px 6px 1px; height: 15px;}",
      "#bit-events td.bit-comment a.bit-comment { background: transparent url('http://static.bandsintown.com/images/facebook/comments_icon.gif') 0px 0px no-repeat; width: 15px; height: 16px; display: inline-block; margin-top: 2px; float: right; }",
      "#bit-events td.bit-comment a:hover, #bit-events td.bit-comment a.bit-comment-open { background-position: 0px -16px; transparent url('http://static.bandsintown.com/images/facebook/comments_icon.gif') -16px 0px no-repeat; }",
      "#bit-events td.bit-location {font-weight:bold;}",
      "#bit-events td.bit-description, #bit-events th.bit-description {font-size: 85%; left: 8px 4px; }",
      "#bit-events td.bit-description-links, #bit-events th.bit-description-links {padding-left: 8px; width: 6px;}",
      "#bit-events .bit-hidden {display:none;}",
      "#bit-events .bit-bottom td {padding-left:8px;height:36px;}",
      "#bit-events .bit-bottom td.concerts-by-bandsintown {text-align:right;}",
      "#bit-events .bit-bottom a { vertical-align: middle; border: none; display: inline-block; }",
      "#bit-events .bit-bottom a.bit-logo { background: transparent url('http://static.bandsintown.com/images/favicon.gif') no-repeat top left; height: 16px; width:16px; margin-right:4px; }",
      "#bit-events a { text-align: left; float: left; width:auto; }",
      "#bit-events a:hover { -webkit-transition: none; -moz-transition: none; -o-transition: none; transition: none; }",
      "#bit-events td.bit-description a { float: none; }",
      "#bit-events a.bit-event-description-link { text-decoration: none; margin: 0; padding: 0; display: inline-block; height: 9px; width: 9px; line-height: 9px; font-size: 9px; text-align: center; vertical-align: middle; border: none;}",
      (this.link_color ? "#bit-events a { color: " + this.link_color + ";}" : ""),
      (this.bg_color ? "#bit-events td, #bit-events th { background-color: " + this.bg_color + ";}" : ""),
      ".bit-events tr.bit-dashed-border td, .bit-events-narrow tr.bit-dashed-border td.bit-description { border-top: 1px dashed " + this.separator_color + ";}",
      ".bit-events tr.bit-dashed-border td.bit-description-links, .bit-events tr.bit-dashed-border td.bit-date, .bit-events-narrow tr.bit-dashed-border td { border-top: 1px solid transparent;}",
      "td.bit-concert { }",
      "#bit-events td.bit-concert a { float: none; }",
      "td.bit-date { width: 45px; }",
      "tr.bit-header th, tr.bit-header-narrow { line-height: 26px; }",
      "#bit-events tr.bit-header a, #bit-events tr.bit-header-narrow a { float: none; font-weight: normal; }",
      "#bit-events tr.bit-header-narrow th { text-align: left; padding: 4px;}",
      "#bit-events .bit-events-narrow tr.no-dates td a { display: block; }",
      (this.myspace_layout ? "" : "#bit-events .bit-events-narrow tr.no-dates td span { display: block; }"),
      "#bit-events .bit-events td.no-dates td { padding: 20px 0px; }",
      "#bit-events .bit-header-links { margin-right: 15px; }",
      "#bit-events .bit-share-text { float:right; }",
      "#bit-events .bit-share-links { float: right; }",
      "#bit-events .bit-share-links a { display: inline-block; width: 26px; height: 26px; vertical-align: middle; }",
      "#bit-events .bit-fb-share { background: transparent url('http://static.bandsintown.com/images/facebook/icons/fb_share.gif') top left no-repeat; margin-left: 4px; display: inline-block; width: 26px; height:26px; vertical-align: middle;}",
      "#bit-events .bit-twitter-share { background: transparent url('http://static.bandsintown.com/images/facebook/icons/twitter_share.gif') top left no-repeat; display: inline-block; width: 26px; height:26px; vertical-align: middle;}",
      "#bit-events .bit-events-narrow tr.no-dates td { padding-bottom: 25px; padding-top: 20px; }",
      "#bit-events tr.no-dates td { padding-top: 10px; padding-bottom: 10px; }",
      "#bit-events tr.no-dates td a { float: none; margin-top: 15px; }",
      "#bit-events table { border-bottom: 1px solid " + this.separator_color + ";width:612px;}",
      ".bit-header-overflow-fix { height: 26px; overflow: hidden; }",
      "#bit-events iframe { border: none; }",
      "#bit-events .comments-title, #bit-events .description-title { color: #323232; font-size: 11px; font-weight: bold; margin: 0px 0px 4px 0px;}",
      "#bit-events .bit-event-details { color: #000000; }",
      "#bit-events .bit-details-title { background-color: #ffffff; font-weight: bold; padding: 4px 8px 0px 20px; color: #0e0e0e; font-size: 11px; }",
      "#bit-events .bit-details-title a { float: right; color: #8296cc; text-decoration: none; }",
      "#bit-events .bit-details-title a:hover { color: #ffffff; text-decoration: none; }",
      "#bit-events .bit-details-text { background-color: #ffffff; margin-bottom: 1px; padding: 5px 8px 5px 20px; color: #1A1A1A; }",
      "#bit-events .bit-details-comments { background: transparent; padding: 0px; margin: 0px; }",
      "#bit-events .bit-details-description { }",
      "#bit-events .bit-details-text a { color: #3857a0; float: none; }",
      "#bit-events a.bit-fb-event-link { font-weight: bold; display: block; text-decoration: none; margin: 4px 0px; }",
      "#bit-events tr td.bit-details { padding: 0px; }",
	  ".bit-events td.bit-venue a{border-right:1px solid red;}"
    ].join("");
    
    if (!BIT.RSVPDialog.css_added) {
      css += BIT.RSVPDialog.css;
      BIT.RSVPDialog.css_added = true;
    }
    return "<style type=\"text/css\">" + css + "</style>";
  }

  this.no_artist_given = function() {
    return [this.css(), "<div class='bit-no-upcoming-events'>No artist given.</div>"].join("\n");
  }

  this.process_description = function( description ) {
    var processed_description = BIT.Utils.Text.escape_html(description || '');
    if (processed_description != '') {
      processed_description = processed_description.replace(/(https?:\/\/[^\s]+)/g, "<a href=\"$1\">$1</a>"); // add auto links
      processed_description = processed_description.replace(/\n/g, "<br/>"); // add line breaks
    }
    return processed_description;
  }

  this.truncate_and_process_description = function ( description ) {
    var truncate_length = 100;
    var max_lines = 3;
    var max_lines_regexp = new RegExp("(?:.*\?\\n){" + max_lines + "}");
    var stripped = BIT.Utils.Text.strip(description || '');

    if (stripped.length < truncate_length && !stripped.match(max_lines_regexp)) {
      return [
        "<span>", 
          this.process_description(stripped),
        "</span>"
      ].join("");
    } else {
      var truncated = BIT.Utils.Text.truncate(stripped, { length : truncate_length, omission : '', max_lines : max_lines });
      var view_more_link = "<a href=\"#\" onclick=\"BIT.Widget.view_more(this); return false;\" class=\"bit-toggle-description\">view more</a>";
      var view_less_link = "<a href=\"#\" onclick=\"BIT.Widget.view_less(this); return false;\" class=\"bit-toggle-description\">view less</a>";
      return [
        "<span>",
          this.process_description(truncated), "...",
          view_more_link,
        "</span>",
        "<span style=\"display:none;\">",
          this.process_description(stripped), 
          "<br/>",
          view_less_link,
        "</span>"
      ].join(""); 
    }
  }

  this.no_dates_message = function(type) {
    if (this.notify_me) {
      var text = "No " + type + " dates. <a href='" + this.notify_me_link() + "'>Notify me when <span>" + (this.api_artist || this.artist) + "</span> comes to my area.</a>";
    } else {
      var text = "No " + type + " dates."
    }
  
    if (this.use_narrow_layout()) {
      return "<tr class='no-dates'><td colspan='4'>" + text + "</td></tr>";
    } else {
      return "<tr class='no-dates'><td class='bit-description-links'>&nbsp;</td><td colspan='" + this.adjusted_colspan(4) + "'>" + text + "</td></tr>";
    }
  }

  this.notify_me_link = function() {
    return ["http://www.bandsintown.com/track/", this.artist_param(), "?came_from=", BIT.came_from_codes.notify_me].join("");
  }

  this.toggle_event_details_js = function(event) {
    return [
      "return BIT.Widget.toggle_event_details({",
        "\'link\':this,",
        "\'artist\':\'", this.artist_param(), "\',",
        "\'event_id\':\'", event.id, "\',",
        "\'widget_timestamp\':\'", this.timestamp, "\'",
      "});"
    ].join("");
  }

  this.toggle_event_description_js = function(event) {
    return [
      "return BIT.Widget.toggle_event_description({",
        "'link':this,",
        "'artist':'", this.artist_param(), "',",
        "'event_id':'", event.id, "'",
      "});"
    ].join("");
  }


  // share link methods
  this.artist_share_links = function() {
    if (this.share_links) {
      return [
        "<span class='bit-share-links'>",
          "<a target='_blank' title='Share this on Facebook' class='bit-fb-share' href='" + this.fb_share_link() + "'></a>",
          "<a target='_blank' title='Share this on Twitter' class='bit-twitter-share' href='" + this.twitter_share_link() + "'></a>",
        "</span>",
        "<span class='bit-share-text'>Share:</span>"
      ].join("");
    } else {
      return "";
    }
  }

  this.bit_share_link = function(came_from) {
    return [BIT.base_url, "/", this.artist_param(), "/share?u=", encodeURIComponent(this.share_url), "&came_from=", came_from].join("");
  }

  this.fb_share_link = function() {
    var link         = this.bit_share_link(BIT.came_from_codes.fb_share);
    var app_id       = '123966167614127';
    var picture      = BIT.base_url + "/" + this.artist_param() + '/photo/medium.jpg';
    var name         = this.artist + ' - Tour Dates';
    var caption      = BIT.Utils.URI.parse(this.share_url).host;
    var properties   = "";
    var redirect_uri = BIT.base_url + '/redirect?u=' + encodeURIComponent(this.share_url);

    if (/^http:\/\/myspace\.com\/\d+/.test(this.share_url)) { name += ' on MySpace'; }

    if (this.artist_events.length == 0) {
      var description = '';
    } else if (this.artist_events.length == 1) {
      var description = '1 upcoming tour date';
    } else {
      var description = this.artist_events.length + ' upcoming tour dates';
    }

    return [
      "http://www.facebook.com/dialog/feed?", 
      "app_id=", app_id,
      "&link=", encodeURIComponent(link),
      "&picture=", picture,
      "&name=", encodeURIComponent(name).replace(/'/g, escape("'")),
      "&caption=", encodeURIComponent(caption),
      "&description=", encodeURIComponent(description),
      "&properties=", encodeURIComponent(properties),
      "&redirect_uri=", encodeURIComponent(redirect_uri)
    ].join("");
  }

  this.twitter_share_link = function () {
    return ["http://www.bandsintown.com/", this.artist_param(), "/twitter_share?u=", encodeURIComponent(this.share_url)].join("");
  }

  // boolean methods
  this.show_rsvp_links = function() {
    if (this._show_rsvp_links != null) { return this._show_rsvp_links; }
    this._show_rsvp_links = this.rsvp_links && this.artist_events != null && this.artist_events[0] != null && this.artist_events[0].facebook_rsvp_url != null;
    return this._show_rsvp_links;
  }

  this.show_facebook_comments = function() {
    if (this._show_facebook_comments != null) { return this._show_facebook_comments; }  
    if (this.facebook_comments && this.artist_events) {
      for (i=0; i<this.artist_events.length; i++) {
        if (this.artist_events[i].facebook_comments_xid) { this._show_facebook_comments = true; }
      }
    } 
    this._show_facebook_comments = this._show_facebook_comments || false;
    return this._show_facebook_comments;
  }

  this.use_narrow_layout = function() {
    if (this.force_narrow_layout == true) { return true; }
    if (this.width.match(/\d+px$/)) {
      return (parseInt(this.width) < 275);
    } else { 
      return false;
    }
  }

  // bandsintown API methods
  // url for all of the widget artist's events
  this.artist_events_url = function() {
    var artist_events_url = [BIT.api_base_url, '/artists/', this.artist_param(), '/events.json?api_version=2.0&extended=true&app_id=', this.app_id].join('');
    if (this.facebook_page_id_param()) { artist_events_url += "&artist_id=" + this.facebook_page_id_param(); }
    return artist_events_url;
  }

  // url for all events near the user's current location featuring the widget artist
  this.local_events_url = function() {
    var artists = "&artists[]=" + encodeURIComponent(this.artist);
    if (this.facebook_page_id_param()) { artists += "&artists[]=" + this.facebook_page_id_param(); }
    return [BIT.api_base_url, '/events/search.json?app_id=', this.app_id, artists, '&location=use_geoip&per_page=100'].join('');
  }

  // jsonp callback for Artist - Events response
  this.artist_events_callback = function(events) {
    this.artist_events_loaded = true;
    this.artist_events = events || [];
    for (i=0; i<this.artist_events.length; i++) {
      var event = this.artist_events[i];
      this.api_artist = event.artists[0].name;
      this.event_descriptions_hash[event.id] = event.description;
      this.rsvp_links_hash[event.id] = event.facebook_rsvp_url;
      this.facebook_comments_xid_hash[event.id] = event.facebook_comments_xid;
      this.ticket_types_hash[event.id] = event.ticket_type || 'Tickets';
    }

    if (this.local_events_loaded) { this.render(); }
  }

  // jsonp callback for Events - Search response 
  this.local_events_callback = function(events) {
    this.local_events_loaded = true;
    this.local_events = events;

    if (this.artist_events_loaded) { this.render(); }
  },

  this.insert_rsvp_iframe = function(target) {
    var iframe_id = this.unique_id("rsvp-iframe");
    var iframe = document.createElement("iframe");
    iframe.setAttribute("id", iframe_id);
    iframe.setAttribute("name", iframe_id);
    iframe.setAttribute("src", BIT.base_url + "/" + this.artist_param() + "/rsvp_iframe?iframe_id=" + iframe_id + "&parent_window_location=" + encodeURIComponent(window.location.href) + "#");
    iframe.setAttribute("width", BIT.RSVPDialog.iframe_width);
    iframe.setAttribute("height", BIT.RSVPDialog.iframe_height);
    iframe.setAttribute("frameBorder", "0"); // ie wants frameBorder not frameborder
    iframe.setAttribute("scrolling", "no");
    iframe.onload = iframe.onreadystatechange = function() { 
      BIT.RSVPDialog.loaded_iframes[iframe_id] = true; 
    }
    iframe.style.display = "none";
    target.appendChild(iframe);
    this.rsvp_iframe = document.getElementById(iframe_id);
  },

  this.rendered_callback = function() {
    // don't do anything if no events loaded
    if (this.artist_events.length == 0) { return; }

    // add event data links for date/venue/location columns
    if (this.event_data_links) { 
      var event_data_spans = BIT.Utils.Element.search("span.bit-event-data", this.root_element());
      for (var i=0; i<event_data_spans.length; i++) {
        var span = event_data_spans[i];
        var parent = span.parentNode;
        var event_id = BIT.Utils.Element.get_parent(span, "tr").getAttribute("id").match(/(\d+)/)[1];
        var link = document.createElement("a");
        link.setAttribute('href', this.rsvp_links_hash[event_id]);
        link.setAttribute('class', 'bit-event-data');
        link.setAttribute('target', '_blank');
        link.innerHTML = span.innerHTML;
        BIT.Utils.Element.copy_css({ target: link, source: span, except: ["cursor"] });
        link.style.cursor = 'pointer';
        parent.insertBefore(link, span);
        parent.removeChild(span);
      }
    }

    // RSVP dialog setup
    
    // return if RSVP dialog is disabled or viewing in IE6
    if (!BIT.rsvp_dialog_enabled || !this.rsvp_dialog || BIT.Utils.Browser.IE6) { return; }
    
    // return if the widget is in an iframe that is too small to display the RSVP dialog
    if (BIT.Utils.Window.is_iframe() && !BIT.RSVPDialog.min_dimensions_available()) { return; }
    
    BIT.RSVPDialog.init();
    this.insert_rsvp_iframe(BIT.RSVPDialog.content);

    // handle click events on RSVP links
    var rsvp_links = BIT.Utils.Element.search("a.bit-rsvp", this.root_element());
    BIT.Utils.Event.observe("click", rsvp_links, this.rsvp_link_clicked_callback);

    // handle click events on event data links
    var event_data_links = BIT.Utils.Element.search("a.bit-event-data", this.root_element());
    BIT.Utils.Event.observe("click", event_data_links, this.rsvp_link_clicked_callback);
  },

  this.rsvp_link_clicked_callback = function(event) {
    BIT.Utils.Event.prevent_default(event);
    var clicked = typeof event.target == "undefined" ? event.srcElement : event.target;
    if (clicked.tagName != "A") { clicked = BIT.Utils.Element.get_parent(clicked, "A"); }
    var bit_event_id = clicked.getAttribute("href").match(/\/event\/(\d+)\//)[1];
    BIT.RSVPDialog.hide_iframes();
    var iframe = BIT.RSVPDialog.find_iframe_for_widget(self.timestamp);
    BIT.RSVPDialog.clicked_rsvp_link = clicked;
    BIT.RSVPDialog.show_iframe_and_event({ "widget": self, "iframe": iframe, "bit_event_id": bit_event_id });
  }
  BIT.widgets.push(this);
}

// onclick handler for showing all upcoming or local events
BIT.Widget.show_all_events = function( link ) {
  var events_tbody = BIT.Utils.Element.get_parent(link, 'tbody');
  var skipped_tr_classnames = /bit-(local|dashed-border|event-description|bottom|header)/;

  for (var i=0; i<events_tbody.childNodes.length; i++) {
    var tr = events_tbody.childNodes[i];
    if (tr.className && !tr.className.match(skipped_tr_classnames)) { tr.className = ''; }
  }

  BIT.Utils.Element.get_parent(link, 'tr').style.display = 'none';
}

// onclick handler for switching between viewing upcoming or local events
BIT.Widget.toggle_events = function ( events_link ) {
  var events_table = BIT.Utils.Element.get_parent(events_link, 'table');
  events_table.style.display = 'none';
  var other_events_table = events_table.nextSibling || events_table.previousSibling;
  other_events_table.style.display = '';
  return false;
}

// onclick handler for expanding/collapsing description when FB comments are not enabled
BIT.Widget.toggle_event_description = function ( params ) {
  var event_row        = BIT.Utils.Element.get_parent(params.link, 'tr');
  var description_row  = event_row.nextSibling;
  var description_link = event_row.childNodes[0].childNodes[0];
  
  if (description_row.style.display == 'none') {
    description_row.style.display = '';
    description_link.style.backgroundImage = description_link.style.backgroundImage.replace("plus", "minus");
    
  } else {
    description_row.style.display = 'none';
    description_link.style.backgroundImage = description_link.style.backgroundImage.replace("minus", "plus");
  }
}

// onclick handler for expanding/collapsing description + FB comments
BIT.Widget.toggle_event_details = function ( params ) {
  var event_row    = BIT.Utils.Element.get_parent(params.link, 'tr');
  var events_table = BIT.Utils.Element.get_parent(event_row, 'table');
  
  var comments_added_id = params.widget_timestamp + "-" + events_table.id;
  BIT.Widget.initialize_comments_added(comments_added_id);
  
  var shown_event_key     = params.widget_timestamp + "-" + events_table.id;
  BIT.Widget.shown_events = BIT.Widget.shown_events || {};
  var shown_event_id      = BIT.Widget.shown_events[shown_event_key];
  var opening_details     = shown_event_id != params.event_id;

  var widget;
  for (var i=0; i<BIT.widgets.length; i++) {
    if (BIT.widgets[i].timestamp == params.widget_timestamp) {
      widget = BIT.widgets[i];
    }
  }

  var detail_rows     = BIT.Utils.Element.search("tr.bit-event-details", widget.root_element());
  var comment_buttons = BIT.Utils.Element.search("a.bit-comment", widget.root_element());
  
  for (i=0; i<detail_rows.length; i++) {
    // there will always be 1 comment button per event row so we can use the same index on these element arrays
    var details_row    = detail_rows[i];
    var comment_button = comment_buttons[i];
    
    // if we are opening details for this event, show the details row and add open state to comments button
    if (opening_details && details_row.id.match(params.event_id)) {
      details_row.style.display = '';
      details_row.previousSibling.className = details_row.previousSibling.className + ' bit-details-open';
      comment_button.className = comment_button.className + " bit-comment-open";
      // add comments iframe if not already added
      if (!BIT.Widget.comments_added[comments_added_id][params.event_id]) {
        BIT.Widget.comments_added[comments_added_id][params.event_id] = true;
        var comments_div = details_row.childNodes[0].childNodes[2];
        BIT.Widget.add_comments_iframe(comments_div, params);
      }
      
    // if we are not opening details for this event, hide the details and remove open state from comments button
    } else {
      details_row.style.display = 'none';
      details_row.previousSibling.className = details_row.previousSibling.className.replace(' details-open', '');
      comment_button.className = comment_button.className.replace(" bit-comment-open", "");
    }
  }

  var description_links = BIT.Utils.Element.search("a.bit-event-description-link", widget.root_element());

  for (i=0; i<description_links.length; i++) {
    var link = description_links[i];
    // if we are opening details for this event, set the description button to open state
    if (opening_details && link.id.match(params.event_id)) {
      link.style.backgroundImage = link.style.backgroundImage.replace("plus", "minus");

    // if we are not opening details for this event, remove the open state from description button
    } else {
      link.style.backgroundImage = link.style.backgroundImage.replace("minus", "plus");
    }    
  }

  BIT.Widget.shown_events[shown_event_key] = BIT.Widget.shown_events[shown_event_key] == params.event_id ? null : params.event_id;
  return false;
}

BIT.Widget.initialize_comments_added = function(comments_added_id) {
  BIT.Widget.comments_added = BIT.Widget.comments_added || {};
  BIT.Widget.comments_added[comments_added_id] = BIT.Widget.comments_added[comments_added_id] || {};
}

BIT.Widget.add_comments_iframe = function (element, params) {  
  var iframe_width = parseInt(BIT.Utils.Element.get_computed_style(element, "width"));
  // use clientWidth in case "auto" was returned (IE issue)
  if (isNaN(iframe_width)) { iframe_width = element.clientWidth; }
  
  var iframe_url    = BIT.base_url + "/event/" + params.event_id + "/fb_comments_iframe?artist=" + params.artist + "&width=" + iframe_width;
  element.innerHTML = "<iframe src='" + iframe_url + "' width='" + iframe_width + "' frameborder='0'></iframe>";
  element.childNodes[0].style.height = "350px";
}

BIT.Widget.view_more = function( element ) {
  element.parentNode.style.display = 'none';
  element.parentNode.nextSibling.style.display = '';
}

BIT.Widget.view_less = function( element ) {
  element.parentNode.style.display = 'none';
  element.parentNode.previousSibling.style.display = '';
}

BIT.Widget.formatted_date = function(event) {
  var months = {
   '01' : 'Jan',
   '02' : 'Feb',
   '03' : 'Mar',
   '04' : 'Apr',
   '05' : 'May',
   '06' : 'Jun',
   '07' : 'Jul',
   '08' : 'Aug',
   '09' : 'Sep',
   '10' : 'Oct',
   '11' : 'Nov',
   '12' : 'Dec'
  };
  
  /*<div class="date">	
		<div class="day">01</div>
		<div class="month">Dec</div>
	</div>*/
  var y_m_d = event.datetime.split('T')[0].split('-');
  return '<div class="date" style="margin-top:0px;color:#000000;"><div class="day">'+ y_m_d[2] + '</div><div class="month">' + months[y_m_d[1]]+ "</div>";
}

BIT.came_from_codes = {
  footer : 10,
  fb_share : 36,
  notify_me : 38,
  rsvp : 39,
  event_details : 46,
  iga_rsvp_wall_post : 72
};
