function runWhen(callback, test) {
  if (test()) {
    callback();
  } else {
    window.setTimeout(function() {
      runWhen(callback, test);
    }, 100);
  }
}

function voteClick(me, shortType, longType, id, direction) {
  var vote_url = $(me).attr("href");
  if (typeof is_auth == "undefined") {
    alert("Somebody go tell a programmer that is_auth isn't defined.");
  } else if (is_auth) {
    if (id) {
      var data = {
        model: longType,
        direction: direction,
        object_id: id,
        template_object_name: shortType,
        allow_xmlhttprequest: 'True'};
      var args = {
        type: 'POST',
        url: vote_url,
        data: data,
        success: function() {
          $('#voting' + id).html('Thank you for your vote');
        }};
      $.ajax(args);
    }
  } else {
    var html = "<a href='" + auth_login + "'>Login</a> to Vote";
    $('div#voting' + id).html(html);
  }
  return false;
}

function protocol(url) {
  var found = url.match(/^(\w+):\/\//);
  if (found) {
    return found[1].toLowerCase();
  }
  return "";
}

function isExternal(url) {
  // Admin links counts as external because it's weird to have the cms admin
  // show up in the iframe.
  var isAdmin = url.match("(https?://" + window.location.host + ")?/admin/");
  var p = protocol(url);
  return isAdmin || (p && (p != window.location.protocol.replace(":", "") ||
                           url.indexOf(window.location.host) == -1));
}

function stripHtml(text) {
  var tags = /<(script|style).*?<\/(script|style)>|<[^>]*>/gi;
  return text.replace(tags, '');
}

/* Repeat from util.js. See that for extensive comments. */
jQuery.fn.strippedHtml = function(text, maxLength) {
  // Nobody uses this, I'm just doing it to be consistent with jQuery.
  if (typeof text == "undefined") {
    return stripHtml(this.html());
  }
  text = stripHtml(text);
  var dotsText = "...";
  if (maxLength && text.length > maxLength) {
    if (maxLength <= dotsText.length) {
      return this.text(text.substring(0, maxLength));
    }
    var toggled = false;
    var visible = $("<span/>");
    visible.html(text.substring(0, maxLength - dotsText.length));
    var dots = $("<span/>").html(dotsText);
    var remaining = $("<span/>").css({display: "none"});
    remaining.html(text.substring(maxLength - dotsText.length, text.length));
    var setup = function(e) {
      e.css({cursor: "pointer"}).click(function() {
        if (toggled) {
          dots.css({display: "inline"});
          remaining.css({display: "none"});
        } else {
          remaining.css({display: "inline"});
          dots.css({display: "none"});
        }
        toggled = !toggled;
      });
    };
    setup(visible);
    setup(dots);
    setup(remaining);
    return this.empty().append(visible).append(dots).append(remaining);
  } else {
    return this.html(text);
  }
};

jQuery.fn.truncateToHeight = function(maxHeight, set_text) {
  this.each(function() {
    var element = $(this);
    var text = String(set_text || element.text());
    var tags = /<[^>]*>/gi;
    text = text.replace(tags, '');

    var characters = text.length;
    var step = text.length / 2;
    var newText = text;
    while (step > 0) {
      element.html(newText);
      if (element.outerHeight() <= maxHeight) {
        if (text.length == newText.length) {
          step = 0;
        } else {
          characters += step;
          newText = text.substring(0, characters);
        }
      } else {
        characters -= step;
        newText = newText.substring(0, characters);
      }
      step = parseInt(step / 2);
    }
    if (text.length > newText.length) {
      element.html(newText + "...");
      while (element.outerHeight() > maxHeight && newText.length >= 1) {
        newText = newText.substring(0, newText.length - 1);
        element.html(newText + "...");
      }
    }
  });
  return this;
};

/* dumps turns dates into yyyy-mm-ddThh:mm:ss which is great for sorting
 * dates while they're still strings. If you need to manipulate dates and
 * times, use this function. */
function dateFromString(str, addHours) {
  addHours = addHours ? addHours : 0;
  var d = new Date();
  var match = str.match(/^(\d{4})-(\d\d)-(\d\d)T(\d\d:\d\d:\d\d)([+Z][:\d]+)?$/);
  if (match) {
    str = [match[2], match[3], match[1]].join("/") + " " + match[4];
  } else {
    match = str.match(/^(\d{4})-(\d\d)-(\d\d)$/);
    if (match) {
      str = [match[2], match[3], match[1]].join("/") + " 00:00:00";
    }
  }
  d.setTime(Date.parse(str) + addHours * 1000 * 60 * 60);
  return d;
}

$(document).ready(function() {
  $("a").each(function(i) {
    var a = $(this);
    if (a.attr("href") && isExternal(a.attr("href")) && !a.attr("target")) {
      a.attr("target", "_blank");
      a.addClass('external-link');
    }
  });
  if ((typeof item_pk != "undefined" && item_pk) &&
      (typeof item_content_type != "undefined" && item_content_type)) {
    $.get("/api/most/view/" + item_content_type + "/" + item_pk + "/");
  }
  jQuery("a#logout").click(function(e) {
    e.preventDefault();
    $.ajax({url: "/accounts/logout/", cache: false, success: function(data) {
      var finishLogout = function() {
        $(".login").html("<span>you have been logged out</span>");
        document.location.reload();
      };
      if (typeof FB != "undefined" && FB.getSession()) {
        FB.logout(finishLogout);
      } else {
        finishLogout();
      }
    }});
  });
  $('div.ad_container').each(function (i) {
    if ($(this).children(':not(script):not(p):not(span):not(h4)').length < 1) {
      $(this).css('display','none');
    }
  });
  $('ul li:first-child').addClass('first');
  $('ul li:last-child').addClass('last');
});

/* from  http://getfirebug.com/firebug/firebugx.js */
if (!window.console) {
  var _names = ["log", "debug", "info", "warn", "error", "assert", "dir",
    "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace",
    "profile", "profileEnd"];

  window.console = {};
  for (var i = 0; i < _names.length; ++i) {
    window.console[_names[i]] = function() {};
  }
}

function IE() {
  return jQuery.browser.msie;
}

function IE6() {
  return IE() && jQuery.browser.version.match(/6/);
}

function embed_video(video, id, width, height) {
  embed_video_helper(video, id, width, height, "", "", "");
}

function embed_video_helper(
  video,
  id,
  width,
  height,
  thumbnail,
  extra_vars,
  player_path) {
  // IE6 is sensitive to modifying the dom before it's done loading. Here, we
  // add a div to a parent element before we hit the closing tag of the parent.
  // Hence, $(document).ready
  $(document).ready(function() {
    var url_jpg = thumbnail ? thumbnail : video.replace(/\.[a-z]+$/, ".png");
    var flashvars = false;
    var flashStuff = "&showfsbutton=true&stretching=exactfit" + extra_vars;
    var params = {
      wmode: "transparent",
      allowscriptaccess: "always",
      showfsbutton: "true",
      allowfullscreen: "true",
      flashvars: "file=" + video + flashStuff + "&image=" + url_jpg};
    var new_id = "videoplayer_" + id;
    var attributes = {
      id: new_id,
      name: new_id};
    var buttons = buttons_div(new_id + "_buttons", $("#" + new_id));
    buttons.addClass("video clearfix");
    var vpp = videoPlayerPath();
    embed_code_helper(width, height, params, vpp, buttons, "Video");
    swfobject.embedSWF(player_path || videoPlayerPath(), new_id, width, height,
        "9.0.0","", flashvars, params, attributes);
  });
};

function buttons_div(buttons_id, insertAfter) {
  var div = $("<div/>").attr("id", buttons_id);
  return div.insertAfter(insertAfter).addClass("media_buttons clearfix");
}

function embed_code_helper(width, height, params,
    player_path, wrapper, videoOrAudio) {
  var embed_code = $("<embed/>").attr("src", player_path);
  embed_code.attr("width", width).attr("height", height);
  for (param in params) {
    embed_code.attr(param, params[param]);
  }
  var fieldSet = $("<fieldset/>").addClass("embedthis").hide();
  var fieldSetHidden = true;
  var opener = $("<a/>").addClass("Embed").addClass("button").attr("title", "Embed");
  opener.attr("href", "#").click(function(event) {
    event.preventDefault();
    fieldSetHidden = !fieldSetHidden;
    if (fieldSetHidden) {
      fieldSet.hide();
    } else {
      fieldSet.show();
    }
  });
  opener.text("Embed");
  opener.appendTo(wrapper);
  fieldSet.appendTo(wrapper);
  fieldSet.append($("<legend/>").text("Embed This " + videoOrAudio));
  fieldSet.append($("<p/>").html("Copy and paste the HTML below to embed this "
      + videoOrAudio.toLowerCase() + " onto your web page. Find out more "
      + "<a href=\"/audio/help/\">here</a>."));
  var embed_id = wrapper.attr("id") + "_code";
  var labelText = "WNYC " + videoOrAudio.toLowerCase() + " player code:";
  $("<label/>").text(labelText).attr("for", embed_id).appendTo(fieldSet);
  fieldSet.append($("<br/>"));
  var textArea = $("<textarea/>").attr("id", embed_id).attr("readonly", "true");
  textArea.attr("rows", 6).attr("cols", 44);
  var innerHtml = $("<p/>").append(embed_code).html();
  var replaceWith = "$1http://" + window.location.host + "/";
  innerHtml = innerHtml.replace(/(=|")\//g, replaceWith);
  // We need to make sure the embed tag is closed for sites running in strict
  // mode.
  if (!innerHtml.match(/<\/embed>/i)) {
    innerHtml = innerHtml.replace("<EMBED", "<embed");
    innerHtml += "</embed>";
  }

  /* http://www.longtailvideo.com/support/forum/Bug-Reports/9709/Javascript-error-with-embed
   * IE all versions throws all these javascript errors when you unload the
   * page with the JW Player embed. Wrapping the embed tag in an object tag
   * with appropriate params and attributes didn't do the trick. This little
   * bit of script here essentially changes the offending
   * function __flash__removeCallback(instance, name) {
   *   instance[name] = null;
   * }
   * into
   * function __flash__removeCallback(instance, name) {
   *   if (instance) instance[name] = null;
   * }*/
  innerHtml += '<script type="text/javascript">(function(){var s=function()' +
      '{__flash__removeCallback=function(i,n){if(i)i[n]=null;};' +
      'window.setTimeout(s,10);};s();})();</script>';
  innerHtml = innerHtml.replace(/</g, "&lt;").replace(/>/g, "&gt;");
  textArea.html(innerHtml).appendTo(fieldSet).click(function() {
    this.select();
  });
  fieldSet.append('<a href="#" class="embed-close-button">Close</a>');
  $('.embed-close-button').click(function(event) {
      fieldSet.hide();
      event.preventDefault();
  });
}

function create_bookmark(user, pk, content_type, bookmark_url, can_bookmark) {
  if (user != "" && content_type != "" && pk != "" && can_bookmark == true) {
    var data = {user:user, content_type: content_type, pk: pk};
    var args = {type:"POST",
        url: bookmark_url,
        data: data,
        success: function(msg) {
          $('li.bookmark').html("You Recommend");
        }};
    $.ajax(args);
  } else if (user != "" && can_bookmark == false) {
  } else if (user == "") {
  }
  return false;
}

function delete_bookmark(bookmark_id, delete_bookmark_url) {
  if (bookmark_id) {
    var data = {bookmark_id: bookmark_id};
    var args = {type: "POST",
        url: delete_bookmark_url,
        data: data,
        success: function() {
          $('li#bookmark_' + bookmark_id).remove();
        }};
    $.ajax(args);
  } else {
    return false;
  }
}

/* This code was originally in article_tools.js. As far as I can tell,
 * addthis_(sendto|close|open) aren't actually defined anywhere. In
 * case they're pulled in from an external javascript file however,
 * I'll leave this in for now. */
$(document).ready(function() {
  if ("undefined" != typeof addthis_close) {
    $('#emailthislink').click(function() {
      return loadEmailForm(item_ctype, item_pk);
    });
    $('li.share > a.addthis_share').hover(function() {
      return addthis_open(this, '', '[URL]', '[TITLE]');
    }, addthis_close).click(addthis_sendto);
    $('li.print > a').click(function() {
      window.print();
    });
    $('li.bookmark > a').click(function() { return false; });

    if (typeof item_pk == "undefined" || item_pk == null) {
       $('li.bookmark').html(" ");
    } else {
       if (can_bookmark) {
          $('li.bookmark > a.bookmark_item').bind('click', add_bookmark);
       } else if (!can_bookmark && is_auth) {
           $('li.bookmark').html("You Recommend");
       } else {
          $('li.bookmark > a.bookmark_item').bind('click', suggest_login);
       }
    }
  }
});

function loadEmailForm(ctype, pk) {
  console.log('in loadEmailForm');
  var place = $('#emailthiswrapper');
  if (!place.length) {
    place = $('<div id="emailthiswrapper"></div>');
    $('body').append(place);
  }
  console.log("we've found emailthiswrapper");
  place.load('/emailthis/' +  ctype + '/' + pk + '/');
  var pos = $('#emailthislink').position();
  place.css({'top': pos.top+20, 'left': pos.left}).fadeIn('slow');
  return false;
}

function cancelEmail() {
  $('#emailthiswrapper').fadeOut(1000, function() {
     $(this).html('');
  });
}

function encodeURI(unencoded) {
  if (typeof(encodeURIComponent) != "undefined") {
    return encodeURIComponent(unencoded).replace(/\'/g, '%27');
  } else {
    var returnValue = escape(unencoded).replace(/\+/g, '%2B');
    return returnValue.replace(/\"/g,'%22').rval.replace(/\'/g, '%27');
  }
}

$(function() {
  $("input.clearonfocus").bind("focus", function() {
    if (this.defaultValue == this.value) this.value = '';
  });
  jQuery('input.resetonblur').bind('blur', function() {
    if (this.value == '') this.value = this.defaultValue;
  });
  $(".hide_at_first").hide().css("position", "static");
});

function localize_npr_links() {
  $('a').each(function(idx) {
    var href=this.href;
    if (href && href.match(/^http\:\/\/(www\.) ?npr\.org/)
        && (!href.match(/force_localization |\/dmg\//))) {
      var newhref = 'http://www.npr.org/stations/force/force_localization.php?'
          + 'station=WNYC_FM&url=' + href;
      this.setAttribute('href', newhref);
    }
  });
}
$(localize_npr_links);

function embed_audio_height() {
  return 29;
}

function embed_audio_width() {
  return 300;
}

function override_audio_width() {
}

/* This code relates to popping open the popup player. It makes sense to have
 * it in whats_on_now.js, except that thetakeaway.org and the other single-show
 * sites don't have the Whats On Now module but do have embedded audio and
 * buttons. */
function openStation(slug) {
  openPopupPlayer({station: slug});
  return false;
}

function audioUrlForPopup(audio, title, thumbnail, download) {
  popArgs = [];
  var push = function(key, value) {
    if (value) {
      popArgs.push(key + "=" + value);
    }
  };
  push("title", title);
  push("thumbnail", thumbnail);
  push("download", download);
  return audio + (popArgs.length ? "?" + popArgs.join("&") : "");
}

function embedAudioObjectHelper(id, path, width, height, params, attributes, popupSkin, inlineSkin) {
  var skinPrefix = "skin=/media/audioplayer/skins/";
  var skin = "";
  if ("undefined" != typeof inPopupPlayer && inPopupPlayer) {
    if (popupSkin) {
      skin = skinPrefix + popupSkin;
    }
  } else if (inlineSkin) {
    skin = skinPrefix + inlineSkin;
  }
  if (skin) {
    if (params["flashvars"]) {
      skin = params["flashvars"] + "&" + skin;
    }
    params["flashvars"] = skin;
  }
  var flashvars = false;
  swfobject.embedSWF(path, id, width, height,
      "9.0.0","", flashvars, params, attributes);
}

var embedAudioObject = embedAudioObjectHelper;

/* player.swf calls playerReady(obj) automatically when it finishes loading
 * except for IE6 using Flash 9, and FF3 on Linux, where the javascript API
 * is broken and unavailable. Hence, always degrade as gracefully as possible
 * if we can't get a player object.  */
var loadedPlayers = [];
var onPlayerReady = null;
function playerReady(obj) {
  // /media/audioplayer/wnyc_newscast.swf calls this function with
  // version: 4.1.60, id: undefined, client: FLASH WIN 10,1,53,64
  if (obj.id) {
    var newPlayer = document.getElementById(obj.id);
    loadedPlayers.push(newPlayer);
    if (onPlayerReady) {
      onPlayerReady(newPlayer);
    }
  }
}

/* TODO: when we update to JW Player verion 5 do nextPlayer.pause(). This is
 * mostly here so the popup player can pause the opener window players so we
 * don't have competing audio. The popup player reloads players in a variety
 * of circumstances. Just remove empty ones here. */
window.stopPlayers = function() {
  var tempPlayers = [];
  for (var playerI = 0; playerI < loadedPlayers.length; playerI++) {
    var player = loadedPlayers[playerI];
    if (player.sendEvent) {
      player.sendEvent("STOP", true);
      tempPlayers.push(player);
    }
  }
  loadedPlayers = tempPlayers;
};

function popup_player_width() {
  return 620;
}

function popup_player_height() {
  return 310;
}

function openPopupPlayer(parameters) {
  var paramList = [];
  for (key in parameters) {
    paramList.push(escape(key) + "=" + escape(parameters[key]));
  }
  var link = '/popup_player/' + playerDisplayParamString() + '#' +
      paramList.join("&");
  // WQXR doesn't really have a popup player any more, so in that case we
  // go to WNYC.
  if (window.popupPlayerRoot) {
    link = window.popupPlayerRoot + link;
  }
  var options = "'toolbar=no,location=no,status=no,menubar=no,scrollbars=no," +
      "resizable=yes,left=0,top=0,width=" + popup_player_width() + ",height=" +
      popup_player_height() + "'";
  var newWindow = window.open(link, "popup_player", options);
  if (newWindow.focus) {
    newWindow.focus();
  }
}

function queryStringLookup() {
  return parametersFromQuery(window.location.search);
}

var embedded = false;

function playerDisplayParamString() {
  var allParams = queryStringLookup();
  var displayParams = {};
  for (var key in allParams) {
    if (key.match(/^(stations|logo|hide_tabs)$/)) {
      displayParams[key] = allParams[key];
    }
  }
  if (embedded) {
    displayParams["embedded"] = true;
  }
  return parametersToQuery(displayParams);
}

function parametersFromQuery(url) {
  url = url.replace(/^[#?]/, "");
  var hashParts = url.split("&");
  var variables = {};
  for (var i = 0; i < hashParts.length; i++) {
    var subParts = hashParts[i].split("=");
    if (subParts.length > 1) {
      // We store unsafe values, so we must avoid XSS during display
      variables[unescape(subParts[0])] = unescape(subParts[1]);
    }
  }
  return variables;
}

function parametersToQuery(params) {
  var paramList = [];
  for (var key in params) {
    paramList.push(escape(key) + "=" + escape(params[key]));
  }
  var paramString = "";
  if (paramList.length) {
    paramString = "?" + paramList.join("&");
  }
  return paramString;
}

$(function() {
  $(".slideshow-container").each(function() {
    initSlideshow($(this));
  });
});

function initSlideshow(slideshowWrapper) {
  var storyslides = slideshowWrapper.find('.storyslides');
  // CSS by default has set height to load elegantly before slideshow is
  // available
  storyslides.css({overflow: 'visible', height:'auto'});

  var onAfter = function(curr, next, opts) {
    var upcomingSlide = $(next);
    var container = upcomingSlide.parent();
    var slideInfo = container.find('.storyslide-info');
    var find = '.storyslide-navigation .storyslide-count';
    var slideCountElement = container.parent().find(find);
    var slidecount = (opts.currSlide + 1) + ' of ' + opts.slideCount;
    slideCountElement.html(slidecount);
    slideInfo.show(0, function() { // Show Caption and Resize Container
      var h = upcomingSlide.height();
      var min_height = 380;
      container.height(h > min_height ? h : min_height);
    });
  }

  var onBefore = function(curr, next, opts) {
    $(curr).parent().find('.storyslide-info').hide();
  }

  storyslides.cycle({
    fx: 'scrollHorz',
    speed: 500,
    timeout: 0,
    next: slideshowWrapper.find('.storyslide-image, .storyslide-next'),
    prev: slideshowWrapper.find('.storyslide-prev'),
    containerResize: 0,
    after: onAfter,
    before: onBefore
  });
}

function format_feeds(defaultTitle) {
  var target = $('.feed-list');
  if (!target.length) {
    return;
  }
  var header = target.attr('title') || defaultTitle || 'Newsfeeds';
  var htmlbuff = ['<ul>'];
  var rsslinks=$('head link[rel="alternate"][type="application/rss+xml"]');
  rsslinks.each(function(idx) {
    var title = this.getAttribute('title').replace(/\s*\(RSS\)\s*$/, '');
    var rsslink = this.getAttribute('href');
    if (rsslink.match(/^\/feeds/)) {
      var atomlink = rsslink.replace(/^\/feeds/, '/atomfeeds');
    } else {
      var atomlink = '';
    }
    var safetitle = title.replace(/[^a-zA-Z0-9_ -]/, '');
    htmlbuff.push('<li><a class="feed-link" title="' + safetitle + '" href="' +
        rsslink + '">' + title + '</a>');
    if (atomlink) {
      htmlbuff.push('&nbsp;&nbsp;(<a class="atom-link" title="' + safetitle +
          '" href="' + atomlink + '">Atom</a>)');
    }
    htmlbuff.push('</li>');
  });
  htmlbuff.push('</ul>');
  $('.feed-list').html(htmlbuff.join('\n'));
}

/* Sliders */

$(document).ready(function() {
  function jcarousel_itemVisible(carousel, item, idx, state) {
    bindRollClick($(item));
  }

  function bindRollClick(item) {
    item.bind({
      mouseover: function() {
        $(this).addClass('active');
      },
      mouseout: function() {
        $(this).removeClass('active');
      },
      click: function(event) {
        var target = $(event.target);
        if (target.is(".edit-tout")) {
        } else {
          window.location = $(this).find('a').eq(0).attr('href');
          return false;
        }
      }
    });
  }

  if ($.jcarousel) {
    // jcarousel clones slides verbatim when circular wrapping is on. Hence,
    // if javasscript tries to reference a specific element in the carousel,
    // it'll run into trouble because of all the duplicates. My quick and
    // dirty fix is to turn off circular when there are html ids.
    var jCarouselOnThese = $(".siteslider, .showslider").not(".noslider");
    var wrap = jCarouselOnThese.find("*[id!='']").length ? null : 'circular';
    // Used blank intead of null to prevent text selection on double click
    jCarouselOnThese.jcarousel({
      scroll: 1,
      wrap: wrap,
      buttonNextHTML: blank,
      buttonPrevHTML: blank,
      itemVisibleInCallback: jcarousel_itemVisible,
      itemFallbackDimension: 620
    });
    bindRollClick($(".noslider ul li, #video-list ul li"));
  }
});

var blank = '<div><img src="/media/img/blank.gif" width="120" height="120" alt=""/></div>';

function revealcomment(id){
  var data = {'comment_id' : id};
  $.getJSON('/comments/getjsoncomment',
            data,
            function(data) {
              $('div#comment'+id).html(data);
            });
};

function audioHelpPath() {
  return "/audio/help/";
}

// We will rip these out completely when we rework embed links for audio.
// Leave them in for now so we can keep that feature limping along with
// JWPlayer.
function inlinePlayerPath() {
  return "/media/audioplayer/red_progress_player_no_pop.swf";
}

function videoPlayerPath() {
  return "/media/videoplayer/mediaplayer.swf";
}

function anchorFoot() { // Anchors The Footer to the Bottom of Page (NYPR and American Icons)
$(document).ready(function() {

  // Position The Footer to Anchor Bottom (Currently NYPR and American Icons)

  var foot = $('#footer');
  var originalPosition = foot.offset().top; // Distance to foot
  positionFoot();

  $(window).bind("resize", function(){ // Trigger Position on Resize
    positionFoot();
  });

  function positionFoot(){ // Position Foot Function
    var footPosition = foot.offset().top; // get foot and window info
    var footHeight = foot.height();
    var windowHeight = $(window).height();

    // Determine New and Current Position
    var newPosition = (parseInt(windowHeight)) - (parseInt(footHeight));

    // Fix Footer to Bottom
    if(originalPosition < windowHeight) {
      foot.addClass('fixed'); // Add Fixed Class
     }

    // Or, Do Not Fix Footer to Bottom
    if(newPosition <= originalPosition){
      foot.removeClass('fixed'); // Removed Fixed Class
    }
  }
});
}

function setupOpenIdLoginForm() {
  var form = $("form[action*='openid']");
  if (form.length) {
    var providerInput = form.find("input[type='text']");
    var submit = form.find("input[type='submit']");
    $(".openId_provider a.pick_openid").each(function() {
      var a = $(this);
      var url = a.attr("data-url");
      var html_id = a.attr("data-html_id");
      var getUsername = $("div." + html_id);
      var button = getUsername.find("button").click(function() {
        var username = getUsername.find("input[type='text']").attr("value");
        providerInput.attr("value", url.replace(/username/, username));
        submit.click();
      });
      a.click(function(event) {
        event.preventDefault();
        $(".get_openid_username").not("." + html_id).hide();
        if (getUsername.length) {
          getUsername.show();
        } else {
          providerInput.attr("value", url);
          submit.click();
        }
      });
    });
    $("a.cancel_openid").click(function(event) {
      $(".get_openid_username").hide();
      event.preventDefault();
    });
  }
}
$(setupOpenIdLoginForm);

function showFullPageSplash(cookie) {
  // The homepage calls this function. Hash navigation always loads the
  // homepage in the outer frame. Hence, only run this function when on the
  // homepage. This happens if there's no iframe or the iframe has the
  // homepage in it.
  if (useHashNavigation && !isIframe() && !onHomepage()) {
    return;
  }
  if (!jQuery.cookie(cookie)) {
    var expires = new Date((new Date()).getTime() + 6 * 60 * 60 * 1000);
    jQuery.cookie(cookie, "1", {expires: expires});
    window.location = "/splash/";
  }
}

function onHomepage() {
  return isHomepage(stripPound(window.location.hash));
}

function isAdSlotFilled(adID,container) {
  if ($(adID).height() < 2)
    $(container).hide();
};

function modalPopup (popupId) {
  $(popupId).animate({
    opacity: 'toggle'
  }, 150);
  var overlay = $('.modalOverlay')
  if (overlay.length) {
    overlay.remove();
  } else {
    $('body').prepend('<div class="modalOverlay"></div>');
  };
}

function initTwitterBoxes() {
  if (typeof twitterBoxes != "undefined") {
    for (var i = 0; i < twitterBoxes.length; i++) {
      var options = twitterBoxes[i];
      var wrapperId = options.twitterTarget;
      var wrapper = $("#" + wrapperId);
      if ("undefined" != typeof getTwitters && wrapper.length) {
        var cached = jQuery.cookie(twitterCookieKey(options));
        try {
          eval("cached = " + cached + ";");
        } catch(err) {
          cached = null;
        }
        if (cached) {
          oldRenderTwitters(cached, options);
        } else {
          getTwitters(wrapperId, options);
        }
      }
    }
  }
}

$(initTwitterBoxes);

var oldRenderTwitters = window.renderTwitters;
window.renderTwitters = function(obj, options) {
  if (obj.length) {
    // We need to cache Twitter feeds because Twitter has rate limiting at
    // 150 requests per client per hour. As they don't document how it works,
    // we cache so that a whole office with a single ip won't have problems.
    toCache = [];
    // The API will return many entries, so we don't want to cache all of them
    // for fear of hitting the storage limit on cookies.
    for (var i = 0; i < obj.length && toCache.length < options.count; i++) {
      var isReply = obj[i]["in_reply_to_status_id"] ||
                    obj[i]["in_reply_to_user_id"];
      if (!isReply || !options.ignoreReplies) {
        // obj is pretty out of control. Just cache the properties we need.
        toCache.push({
          text: obj[i].text,
          created_at: obj[i].created_at, // Used deep in twitter.min.js
          time: obj[i].time,
          source: obj[i].source,
          user: {
            screen_name: obj[i].user.screen_name,
            name: obj[i].user.name}
          });
      }
    }
    oldRenderTwitters(toCache, options);
  }
  var expires = new Date();
  expires.setTime(expires.getTime() + 30 * 60 * 1000);
  jQuery.cookie(
    twitterCookieKey(options),
    serialize(toCache),
    {expires: expires});
}

function twitterCookieKey(options) {
  var key = "__" + options.id;
  if (options.list) {
    key += "_" + options.list;
  }
  return key;
}

function serialize(obj) {
  /* Return the obj as a json string. */
  var type = typeof obj;
  if ("string" == type) {
    return '"' + obj.replace(/"/g, '\\"') + '"';
  } else if ("number" == type || "boolean" == type) {
    return "" + obj;
  } else if ("undefined" == type || "function" == type || null == obj) {
    return null;
  }
  var yepArray = isArray(obj);
  var subTerms = [];
  if (yepArray) {
    var ends = ["[", "]"]
    for (var i = 0; i < obj.length; i++) {
      subTerms.push(serialize(obj[i]));
    }
  } else {
    var ends = ["{", "}"];
    for (var i in obj) {
      var toPush = serialize(obj[i]);
      if (toPush) {
        // i doesn't ever contain '"' at this time
        subTerms.push(i + ':' + toPush);
      }
    }
  }
  return ends[0] + subTerms.join(",") + ends[1];
}

function isArray(obj) {
  return obj.constructor.toString().indexOf("Array") >= 0;
}

// -1 not started. 0 done. > 0 number of newsletter sign ups still waiting. We
// must wait for all the newsletter sign ups to complete or the browser may
// cancel the requests.
var newsletterStatus = -1;
function wireUpNewsletterSignups() {
  /* django-socialregistration handles the facebook, yahoo, etc... logins in
   * its views, so wire up a separate request for newsletter sign ups. */
  var opt_ins = $(".newsletter_opt_in");
  if (opt_ins.length) {
    var form = $(opt_ins.closest("form")[0]);
    form.submit(function(event) {
      var checked = $(".newsletter_opt_in:checked");
      if (-1 == newsletterStatus && checked.length) {
        event.preventDefault();
        newsletterStatus = checked.length;
        checked.each(function() {
          newsletterSignup($(this).attr("id"), form, function() {
            newsletterStatus--;
            if (0 == newsletterStatus) {
              form.submit();
            }
          });
        });
        return false;
      } else {
        return true;
      }
    });
  }
}

$(wireUpNewsletterSignups);

/* Broken out for testing.
 * newsletterSignup("WNYC", $($(".newsletter_opt_in").closest("form")[0]), function() {});
 * We used to POST, but would get empty posts every couple of days,
 * which makes me think some browsers didn't support it.
 */
function newsletterSignup(newsletter_slug, form, callback) {
  var url = "/users/newsletter_sign_up/?newsletter_slug=" +
            newsletter_slug +
            "&email=" +
            encodeURI(form.find("input[name=email]").attr("value"));
  $.get(url, callback);
}

function wireUpFormatsPopup() {
  if ($('.popdown_ad').height() > 2) {
    $('#formats_popup').addClass('yes_popdown_ad');
  } else {
    $('#formats_popup').addClass('no_popdown_ad');
  }
  $('.formats_opener').click(function(event) {
    event.preventDefault();
    $('#formats_popup').toggleClass('hidden');
  });
}

$(wireUpFormatsPopup);

function cloneObject(cloneMe) {
  var clone = {};
  for (key in cloneMe) {
    clone[key] = cloneMe[key];
  }
  return clone;
}

/* Hash navigation. See
http://wiki.wnyc.org/mediawiki/index.php/Hash_Navigation#How_it_works */
function isHome() {
  return "/" == window.location.pathname;
}

function isIframe() {
  return parent != window;
}

function allUrl() {
  return window.location.pathname + window.location.search;
}

var useHashNavigation = false;
function headInitHashNavigation() {
  /* This should be run in the html head tag. It doesn't depend on the contents
   * of the page and should run as soon as it can. */
  if (window.doNotUseHashNavigation) {
    return;
  }
  useHashNavigation = true;
  resizePermanentHeaderSpace();
  if (isIframe()) {
    var newUrl = allUrl();
    if (isHomepage(newUrl)) {
      newUrl = "/" + homepageTrail(newUrl);
    }
    parent.setHash(newUrl);
    parent.setTitle($("title").html());
  } else if (!isHome()) {
    window.location = "/#" + allUrl();
  }
}

function setTitle(title) {
  document.title = title;
}

function setLogin(html) {
  $("#login").html(html);
  $("#login a").each(function() {
    setAHrefForHash($(this));
  });
}

function handUpLogin() {
  var login = $("ul#login"); // In case there's a login FORM somewhere.
  if (login.length) { // In case a page doesn't have the login ul.
    parent.setLogin(login.html());
  }
}

function bodyInitHashNavigation() {
  /* This needs to run inside the body because it depends on elements in the
   * body. */
  if (window.doNotUseHashNavigation) {
    return;
  }
  if (isIframe()) {
    // The permanent header is on the homepage, but this homepage here is in an
    // iframe which is on the homepage. We have to hide the iframe permanent
    // header or we'd see 2 of them.
    if (isHomepage(window.location.pathname)) {
      $(".permanent_header_part").hide();
    }
    runUntilPageLoad(fixIFrameHomePageLinks);
    $(handUpLogin);
  } else {
    if (isHome()) {
      var hash = stripPound(window.location.hash);
      if (!hash || isHomepage(hash) || hashIsPlayStream()) {
        // This is for the special yet most common use case when a user loads
        // the home page. No iFrame exists yet, so if they click a link we have
        // to create the iFrame. After that point, every link will be within
        // the iFrame and redirect the iFrame, so no hashes necessary. In case
        // a user clicks an <a/> beforre the page loads, just keep setting them
        // until it does.
        runUntilPageLoad(setAHrefsForHash);
      }
      checkHash();
    }
  }
}

function isHomepage(url) {
  /* This isn't smart about urls that start with a period such as ../ */
  if (!url || url.charAt(0) == "?" || url.charAt(0) == "#") {
    return isHomepage(window.location.pathname);
  }
  /* return typeof homepageTrail(url) == "string"; is also valid. */
  return homepageTrail(url) !== false;
}

function homepageTrail(url) {
  /* If the url is for the homepage, return the query string and/or hash as a
   * string. Otherwise return false. Examples:
   * "/" -> ""
   * "#asdf" -> "#asdf"
   * "a#asdf" -> false
   * "/not/homepage?does=notmatter" -> false
   * "/homepage/#a=b" -> "#a=b"
   * "/_=_" -> "" This one comes from some wacky oauth redirect. */
  var got = url.match(/^(\/(homepage\/)?)?([#?].*)?$/);
  if (got) {
    return got[3] || "";
  } else if (url.match(/_=_/)) {
    // This is a special case from Facebook login.
    return "";
  }
  return false;
}

function fixIFrameHomePageLinks() {
  /* Browsers don't let you open an iframe to the same url. However, there are
   * exceptions, such as when you click in the iframe to that url, or there's a
   * hash or something. We'll just keep it consistent by making the inner
   * iFrame never go to / but rather /homepage/ */
  $("a").each(function() {
    var a = $(this);
    var href = a.attr("href");
    if (href) {
      var station = a.attr("href").match(/#station=(.+)/);
      /* Producers want to be able to put links on the site that fire up the
       * player. We use #station=wqxr. */
      if (station) {
        if (!a.attr("data-set")) {
          a.attr("data-set", "true");
          a.click(function() {
            parent.playStation(station[1]);
          });
        }
      } else if (isHomepage(href)) {
        a.attr("href", "/homepage/" + homepageTrail(href));
      }
    }
  });
}

function setHashNavFrame(url) {
  if (isHomepage(url)) {
    url = "/homepage/" + homepageTrail(url);
  }
  var main_frame = $("#main_frame");
  if (main_frame.length) {
    main_frame.attr("src", url);
  } else {
    $("body").css({margin: 0, padding: 0});
    // IE needs the capital B in frameBorder
    var iStr = "<iframe id='main_frame' width='100%' frameBorder='0' />";
    $("body").append($(iStr).attr("src", url));
    runUntilPageLoad(function() {
      // We can't hide #sm2-container otherwise soundManager stops working.
      var toHide = $("body").children().not(
        ".permanent_header_part,#main_frame,#sm2-container");
      toHide.hide();
      // Chrome and IE are known to display youtube iframes. For the life of
      // me, this is the only way I could get them to go away.
      toHide.find("iframe[src*='youtube']").remove();
    });
    resizeIFrame();
    setHash(url);
  }
}

function runUntilPageLoad(callback) {
  callback(); // Make sure it runs at least once.
  var interval = setInterval(callback, 100);
  $(function() {
    clearInterval(interval);
    callback(); // It should run one last time now that the DOM is loaded.
  });
}

function setAHrefForHash(a, url) {
  /* The url is optional. We'll use the href if no url is specified. */
  var href = typeof url == "string" ? url : a.attr("href");
  if (!href || href.charAt(0) == "#" || a.attr("target") == "_blank") {
    return;
  }
  href = absolutePath(href);
  if (isExternal(href)) {
    a.attr("target", "_blank");
  } else if (useHashNavigation && !isIframe()) {
    a.attr("href", "#" + href);
  } else if (window.inPopupPlayer) {
    a.click(function(evt) {
      evt.preventDefault();
      popupPlayerOpenUrl(href);
    });
  } else {
    a.attr("href", href);
  }
}

function setAHrefsForHash() {
  $("a").each(function() {
    setAHrefForHash($(this));
  });
}

function windowLocationPathname() {
  var hash = window.location.hash;
  if (hash.match(/^#?\//)) {
    return hash;
  }
  return window.location.pathname;
}

function absolutePath(href) {
  var prefix = window.location.protocol + "//" + window.location.host
  href = href.replace(prefix, "");
  if (href.charAt(0) == "/" || isExternal(href)) {
    return href;
  }
  var pathParts = windowLocationPathname().split("/");
  pathParts.pop();
  while (href.match(/^\.?\.\//)) {
    while (href.match(/^\.\.\//)) {
      pathParts.pop();
      href = href.slice("../".length);
    }
    while (href.match(/^\.\//)) {
      href = href.slice("./".length);
    }
  }
  pathParts.push(href);
  return pathParts.join("/");
}

// In IE a slim band appears between the iframe and the bottom of the page,
// causing unwanted scroll bars on the outer page. I can't find why.
// I tried marginheight, marginHeight, and vspace attributes on the iframe.
// I tried margin and padding css.
var offset = IE() ? -3 : 0;
function resizeIFrame() {
  var height = $(window).height() - $("#permanent_header").height() + offset;
  $("#main_frame").height(height);
  window.setTimeout(resizeIFrame, 100);
}

function resizePermanentHeaderSpace() {
  $("#permanent_header_space").height($("#permanent_header").height());
  window.setTimeout(resizePermanentHeaderSpace, 100);
}

var lastHash = "";
function setHash(newHash) {
  lastHash = stripPound(newHash);
  window.location.hash = lastHash;
}

function hashIsPlayStream() {
  var rawHash = stripPound(window.location.hash);
  var streamFinder = rawHash.match(/^station=([-_\w]+)$/);
  if (streamFinder) {
    return streamFinder[1];
  }
}

function checkHash() {
  /* #station=wqxr will play wqxr when the top player is ready.
   * Otherwise, #/what/er/ will load the appropriate page. */
  if (hashIsPlayStream()) {
    var slug = hashIsPlayStream();
    window.location.hash = lastHash;
    if (stations[slug]) {
      runWhenTopPlayerReady(function() {
        playStation(slug);
        topPlayer.playClick();
      });
    } else {
      alert("That's not a valid station.");
    }
  } else {
    var newHash = absolutePath(stripPound(window.location.hash));
    var newHomepage = isHomepage(newHash);
    if (unescape(lastHash) != unescape(newHash) &&
        !(isHomepage(lastHash) && newHomepage)) {
      lastHash = newHash;
      if (isExternal(lastHash)) {
        lastHash = "/";
      }
      // Since #main_frame is on /, we can't set it to /. Browsers such as
      // Firefox understandably don't let you do that because that is usually
      // a recursive error.
      var url = lastHash;
      if (isHomepage(url)) {
        url = "/homepage/" + homepageTrail(url);
      }
      setHashNavFrame(url);
    }
  }
  window.setTimeout(checkHash, 100);
}

function stripPound(hash) {
  return hash.replace(/^#/, "");
}
/* End hash navigation */

/* simpler cycle option for small carousels */

function loadSimpleCycle(list, fx, timeout, next, prev) {
  $(list).cycle({
    fx: fx,
    timeout: timeout,
    next: next,
    prev: prev,
    pause: true,
    pauseOnPagerHover: true
  });
}

function callAt(func, when) {
  return window.setTimeout(
    func,
    when.getTime() - (new Date()).getTime() + 1000);
}

function loadMobileAppAd() {
  var userAgent = navigator.userAgent;
  var isApple = userAgent.match(/(iPhone|iPod|iPad)/);
  var isAndroid = userAgent.match(/Android/);
  if (isApple) {
    // || isAndroid
    var app_name = "iPhone";
    var app_url = iPhoneAppUrl();
    /*
    if (isAndroid) {
      var app_name = "Android";
      var app_url = androidAppUrl();
    }
    */
    var html = '<a href="' + app_url + '" id="mobile-app-ad-inner">Download our free ' + app_name + ' App</a>';
    $("#mobile-app-ad").html(html);
  }
}


function loadSectionStories(sectionslug){
  var url = "/api/v1/list/section_stories/" + sectionslug + "/?limit=3";
  var src = $("#handlebars-stories").html();
  var tmpl = Handlebars.compile(src);
  $.getJSON(url,
            [],
            function(data) {
                $("#handlebars-section").html(tmpl(data));
            });
}

