var BIT = BIT || {};

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

BIT.Utils = {
  
  RGB_REGEX : /\(\s*(\d+)\W+(\d+)\W+(\d+)\s*\)/, // 'rgb(10,20,30)'
  HEX_REGEX : /[A-F0-9]+/i,                      // '#ffffff', '#000', 'ffffff', '000'
  
  get_computed_style : function(element, attribute) {
    var computed_style;
    /* handles IE (?) */
    if (typeof element.currentStyle != 'undefined') {
      computed_style = element.currentStyle; 
    } else { 
      computed_style = document.defaultView.getComputedStyle(element, null); 
    }
    return computed_style[attribute];
  },
  
  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';
  },

  // 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
   */
  get_elements_by_classname : 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;
  },

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

  // parseUri 1.2.2
  // (c) Steven Levithan <stevenlevithan.com>
  // MIT License
  parse_uri : function (str) {
    var  o   = this.parse_uri_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_uri_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*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
    }
  },

  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, "\\\"");
    }
  },

  Log: function(message) {
    if (!BIT.Utils.Log.enabled) { return; }
    if (!console) { return; }
    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);
    }
  },

  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 = elementTop === 100;
          container.removeChild(el);
          container.scrollTop = originalScrollTop;
          return isSupported;
        }
        return null;
      })();
      return this._support.fixed_position;
    }
  }
};

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.getSession() && FB.getSession().access_token) {
      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.setSession({ access_token: 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");
        } 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.getSession()) { 
        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(rsvp_links) {
    jQuery(document).ready(function() {
  
      // return without doing anything unless Backplane and FB are available
      if (!Backplane || !FB) { return; }
      
      // 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.setSession({ access_token: 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);
          }
        });
      }
    });
  }
};

BIT.RSVPDialog = {         
  
  dialog_html: "\
  <div id=\"bit-rsvp-dialog\">\
    <div class=\"generic_dialog pop_dialog\" style=\"display:none\" id=\"rsvp-dialog-container\">\
      <div class=\"generic_dialog_popup wide-popup\">\
        <div class=\"pop_container_advanced popup-border-outer\" id=\"rsvp-dialog-y-offset\">\
          <div class=\"pop_container_advanced popup-border-inner\"><\/div>\
          <div class=\"pop_content popup-content\">\
            <h2 class=\"dialog_title\"><span id=\"rsvp-dialog-title\">RSVP<\/span><\/h2>\
            <div class=\"dialog_content\">\
              <div class=\"dialog_body promote-event\" id=\"rsvp-dialog\">\
                <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>\
              <\/div>\
              <div class=\"dialog_buttons clearfix\">\
                <div class=\"rsvp-buttons\">\
                  <label class=\"uiButton uiButtonLarge uiButtonDefault\">\
                    <input type=\"button\" class=\"rsvp-cancel\" value=\"Cancel\" id=\"rsvp-cancel\" />\
                  <\/label>\
                <\/div>\
              <\/div>\
            <\/div>\
          <\/div>\
        <\/div>\
      <\/div>\
    <\/div>\
  <\/div>",
  
  show_event: function(artist_event) {
    if (!BIT.RSVPDialog.init_done) { BIT.RSVPDialog.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.parse_uri(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.container.show();
    this.center();
  },

  hide: function() { 
    this.container.hide();
    this.loading_image.hide();
    this.error_message.hide();
    this.rsvp_message.val("");
  },
  
  position_vertically : function(rsvp_link) {},

  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.init_done) { BIT.RSVPDialog.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_stylesheet: function() {
    if (!BIT.RSVPDialog.stylesheet_added) {
      jQuery("body").append("<link rel=\"stylesheet\" href=\"" + BIT.base_url + "/stylesheets/facebook/popup.css\" />");
      BIT.RSVPDialog.stylesheet_added = true;
    }
  },
  
  insert_content: function() {
    if (!BIT.RSVPDialog.content_added) {
      jQuery("body").append(this.dialog_html);
      BIT.RSVPDialog.content_added = true;
    }
  },

  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.css('display') == 'none') { return; }
    var element = BIT.RSVPDialog.y_offset;
    var elementHeight, windowHeight, Y2;
    elementHeight = element.outerHeight();
    windowHeight = jQuery(window).height();
    Y2 = (windowHeight/2 - elementHeight/2);
    if (!BIT.Utils.Support.fixed_position()) { Y2 += jQuery("html").scrollTop(); }
    jQuery(element).css({ 'top':Y2 + "px" });
  },

  init: function() {
    // insert css and html if not already loaded
    this.insert_stylesheet();
    this.insert_content();
    
    // scope for dialog elements
    this.root      = jQuery("#bit-rsvp-dialog");
    
    // visibility and positioning elements
    this.container = jQuery("#rsvp-dialog-container", this.root);
    this.y_offset  = jQuery("#rsvp-dialog-y-offset", this.root);
    this.buttons   = jQuery("#rsvp-buttons", this.root);
    
    // 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);
    
    // cancel button hides the dialog
    jQuery("#rsvp-cancel", this.root).click(function(event) {
      event.preventDefault();
      BIT.RSVPDialog.hide();
    });
    
    // 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,publish_stream" : "rsvp_event";
      
      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); }
          });
        }
      });
    });

    jQuery(window).resize(function() { BIT.RSVPDialog.center(); });
    
    this.init_done = true;
  }
};

BIT.Widget = function(options) {

  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.source = options.source;
  if (typeof(this.source) == 'string' && this.source.toLowerCase() == 'iga' && options.janrain) {
    this.rendered_callback        = BIT.Janrain.widget_rendered_callback; 
    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.event_description_links_name = this.unique_id('bit-event-description-link');
  
  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.write_html = function() {
    document.getElementById(this.loader_div_id).style.display = 'none';
    document.write(this.to_html(this.artist_events, this.local_events));
    this.invert_description_link_colors();
    if (this.rendered_callback) {
      this.rendered_callback(BIT.Utils.get_elements_by_classname("a.bit-rsvp", this.root_element())); 
    }
  }

  this.insert_events = function() {
    if (this.artist) {
      this.show_loader();
      document.write("<script type=\"text/javascript\" src=\"" + this.local_events_url() + "\"></script>");
      document.write("<script type=\"text/javascript\" src=\"" + this.artist_events_url() + "\"></script>");
    } else {
      document.write(this.no_artist_given());
    }
  }
  
  // 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 = document.getElementsByName(this.event_description_links_name);
    for (i=0; i<links.length; i++) {
      var link = links[i];
      if (BIT.Utils.get_parent(link, 'div') == this.root_element()) {
        var current_color = BIT.Utils.get_computed_style(link, 'color');
        link.style.backgroundColor = current_color;
        var white_or_black = BIT.Utils.image_color_for_background_color( current_color );
        var background_image = 'url("http://www.bandsintown.com/images/widget/plus_' + white_or_black + '.gif")';
        link.style.backgroundImage = background_image;
        link.style.backgroundPosition = 'center center';
        link.style.backgroundRepeat = 'no-repeat';
      }
    }
  }
  
  /* 
    Returns html for a table header row containing links to switch between upcoming and local events and optional share links.
    If the widget is using the wide layout, this includes a second row with column headers for date, venue etc.
    type should be passed as either 'upcoming' or 'local'.
  */ 
  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("");
    } 
  }

  /*
    Returns html for the bottom rows below all events.
    If there are more events to be displayed than the display_limit option there will be a row containing a link to show all events.
    If the bandsintown_footer_link option is true there will be an extra row with a link to BIT.
  */
  this.table_footer = function(events, type) {
    var html = [];

    if (this.use_narrow_layout()) {
      var colspan = this.adjusted_colspan(4);
    } else {
      var colspan = this.adjusted_colspan(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');
  }
  
  /*
    Returns all html for the widget which is basically:
    <wrapper div>
      <upcoming events table>
      <local events table (hidden)>
    </wrapper div>
  */
  this.to_html = function(upcoming_events, local_events) {
    var html = [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 name = this.event_description_links_name;
      var id = "bit-event-description-link-" + event.id;
      return "<a href=\"javascript:void(0);\" name=\"" + name + "\" 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 + "'>",
        "<td class='bit-description-links'>", this.details_link_or_nbsp(event), "</td>",
        "<td class='bit-date'>", BIT.Widget.formatted_date(event), "</td>",
        "<td class='bit-venue'>", event.venue.name, "</td>",
        "<td class='bit-location'>", this.formatted_location(event), "</td>",
        "<td class='bit-tickets'>", 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 + "'>",
        "<td class='bit-description-links'>", this.details_link_or_nbsp(event), "</td>",
        "<td class='bit-date'>", BIT.Widget.formatted_date(event), "</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 = [event.venue.name, "<br/><strong>", this.formatted_location(event), "</strong>"].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' || event.venue.country.toLowerCase() == 'canada') {
      return event.venue.city + ', ' + event.venue.region;
    } else {
      return event.venue.city + ', ' + event.venue.country;
    }
  }
    
  this.artist_param = function() {
    var param = 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.ticket_link = function(event) {
    if (event.ticket_status == 'available') {
      var href = event.ticket_url + '&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() {
    return [
      "<style type='text/css'>",
      ".bit-events, .bit-events-narrow {overflow: hidden;display: table;width:", this.width, ";}",
      ".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 dotted " + this.separator_color + ";}",
      ".bit-events-narrow td {width:auto; height:57px; background:none;padding:4px; border-top: 1px dotted " + 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://www.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://www.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://www.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://www.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 dotted " + 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 { }",
      "td.bit-concert span { float: left; padding: 0 6px; color: " + this.link_color + "; }",
      "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://www.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://www.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 dotted " + this.separator_color + ";}",
      ".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; }",
      "</style>"
    ].join('');
  }
  
  this.show_loader = function() {
    document.write('<div id="' + this.loader_div_id + '" style="text-align:center;"><img src="http://www.bandsintown.com/images/widget-ajax-loader.gif" /></div>');
  }

  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.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.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.parse_uri(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(callback) {
    var callback = callback || 'widget.artist_events_callback';
    return [BIT.api_base_url, '/artists/', this.artist_param(), '/events.json?api_version=2.0&extended=true&app_id=', this.app_id, '&callback=', callback].join('');
  }
  
  // url for all events near the user's current location featuring the widget artist
  this.local_events_url = function(callback) {
    var callback = callback || 'widget.local_events_callback';
    return [BIT.api_base_url, '/events/search.json?app_id=', this.app_id, '&callback=', callback, '&artists[]=', encodeURIComponent(this.artist), '&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.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.write_html(); 
    }
  }
  
  // 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.write_html(); }
  }  

  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.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.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.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.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.get_parent(params.link, 'tr');
  var events_table = BIT.Utils.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 detail_rows     = document.getElementsByName("bit-event-details-" + params.widget_timestamp);
  var comment_buttons = document.getElementsByName("bit-comments-button-" + params.widget_timestamp);
  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 (BIT.Utils.get_parent(details_row, 'table').getAttribute('name') == events_table.getAttribute('name')) {
      // 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 = document.getElementsByName("bit-event-description-link-" + params.widget_timestamp);
  for (i=0; i<description_links.length; i++) {
    var link = description_links[i];
    if (BIT.Utils.get_parent(link, 'table').getAttribute('name') == events_table.getAttribute('name')) {
      // 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.get_computed_style(element, 'width'));
  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'
  };
  var y_m_d = event.datetime.split('T')[0].split('-');
  return months[y_m_d[1]] + ' ' + y_m_d[2];
}

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