/*function deleteCookie(name) {
   	// Only deletes non-wildcard cookies
    document.cookie = name + "=;expires=Thu, 01-Jan-1970 00:00:01 GMT";
}
deleteCookie('compojure-session');
deleteCookie('login-token');
*/

var cache = {}
var PendingMessages = {}
var MessageContentCache = {}
var RawFavs = {}
var MaxImagePosts = 30

// todo: preload these. also, look into image sprites (no go on animating their sizes tho)
// css clipping perhaps?
Imgs = {
  "chatThumb": "/static/img/thumbs/smallheartfaved.gif",
  "chatThumbBig": "/static/img/thumbs/chatheartover.gif",
  "chatThumbOff": "/static/img/thumbs/smallheart.gif",
  "chatThumbDot": "/static/img/thumbs/smallheart.gif",
  "logThumb": "/static/img/thumbs/heartfaved.gif",
  "logThumbBig": "/static/img/thumbs/heartover.gif",
  "logThumbOff": "/static/img/thumbs/heart.gif"
}

Anim = {
    "chatThumbBig": {"width": "54px", "height": "54px", "right": "0px", "bottom": "2px"},
    "chatThumbTiny": {"width": "16px", "height": "16px", "right": "8px", "bottom": "8px"},
    "chatThumb": {"width": "16px", "height": "16px", "right": "4px", "bottom": "4px"},
    "logThumb": {"width": "27px", "height": "27px", "marginRight": "0px", "marginTop": "0px"},
    "logThumbBig": {"width": "64px", "height": "64px", "marginRight": "-2px", "marginTop": "-2px"}
}

// Utils

/*Object.size = function(obj) {
    var size = 0, key;
    for (key in obj) {
        if (obj.hasOwnProperty(key)) size++;
    }
    return size;
};*/

isEmptyObject = function(obj) {
    for (key in obj) {
        if (obj.hasOwnProperty(key)) return false;
    }
    return true
}

String.prototype.trim = function(){ return this.replace(/^\s+|\s+$/g,'') }


function isCSSPropertySupported(prop){ return prop in document.body.style }

function escapeHtml(txt) {
    if (!txt) { return ""; }
//    txt = annoyingCaps(txt)
    return $("<span>").text(txt).html()
}

var Log = {
    "Levels": ['info', 'warn', 'error'],
    "AjaxSubmitLevels": ['warn', 'error'],
    "AjaxSubmitPath": "/logerror",
    
    "SupplementalInfo": function() {
        return { 'user': UserInfo && UserInfo.nick };
    },

    "ajaxSubmit": function(level, component, msg) {
        var info = Log.SupplementalInfo();
        var data = { 'level': level, 'component': component, 'msg': msg };
        $.extend(info, data);
        
        $.ajax({type: 'POST',
                timeout: 5000,
                url: Log.AjaxSubmitPath,
                data: info
               });
    },
    
    "initialize": function() {
        $.each(Log.Levels, function(i, level) {
            Log[level] = function(component, msg) {
                if (window.console && window.console[level])
                    window.console[level](args);
                if (Log.AjaxSubmitLevels.indexOf(level) != -1)
                    Log.ajaxSubmit(level, args);
            };
        });
    }
};

Log.initialize();


URLRegex = /((\b(http\:\/\/|https\:\/\/|ftp\:\/\/)|(www\.))+(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?)/gi;
PicRegex = /\.(jpg|jpeg|png|gif|bmp|svg|fid)$/i;

function getImagesAsArray(text) {
  var imgs = []
  var urls = text.match(URLRegex)
  if (urls === null) return imgs
  for (var i = 0; i<urls.length; i++){
    var url = urls[i]
    var urlWithoutParams = url.replace(/\?.*$/i, "");
    if (PicRegex.test(urlWithoutParams))
      imgs.push(url)
  }
  return imgs
}

function linkify(text) {
    LastMsgContainsImage = false
    text = text.replace(URLRegex, linkReplace);
    return text
}

function annoyingCaps(text){
  var chunks = text.split(" ")
  for(var i=0; i<chunks.length; i++){
    var chunk = chunks[i]
    if (!chunk.length || chunk.substr(0,4) == 'http') continue;
    var letters=chunk.split("")
    for(var j = 0; j<letters.length; j++){
      if (j % 2) letters[j] = letters[j].toUpperCase()
      else letters[j] = letters[j].toLowerCase()
    }
    chunks[i] = letters.join("")
  } 
  return chunks.join(" ") 
}


function imgClickHandler() {
    // Ugly hack. Don't open new links in chat, only in logs.
    // Ugly hack mkII: ensure middle-click opens images in new tab
    // c.f. http://code.google.com/p/chromium/issues/detail?id=255
    if ($.browser.webkit && event.button != 0) {
	event.stopPropagation();
    } else {
	return $('#chatrap').length == 0;
    }
}

// durty hack to use a global to check this... but otherwise i'd have to rewrite the String.replace function? :/
var LastMsgContainsImage = false
function linkReplace(url) {
    var lowerurl = url.toLowerCase();
    if (lowerurl.indexOf('http://') == 0 || lowerurl.indexOf('https://') == 0 || lowerurl.indexOf('ftp://') == 0)
        linkUrl = url;
    else
        linkUrl = 'http://' + url;

    var uri = parseUri(url)
    var type = getUriType(uri)
    
    if (type == 'image') {
      LastMsgContainsImage = true;
      return "<a target='_blank' href='" + linkUrl + "' class='img-wrapper' onclick='return imgClickHandler()'><img src='" + linkUrl + "'></a>";
    } else if (type == 'youtube') {
      Youtube.startAnimation();
      return "<a target='_blank' class='youtube' href='" + linkUrl + "'>" +
             "<img class='youtube-thumb' width='130' height='97' src='"+Youtube.nextThumbUrl(uri.queryKey.v)+"'>" + 
             "<img class='youtube-controls' src='/static/img/youtube.controls.png'></a>"
    } else if (type == 'midi' || type == 'wav') {
      return '<embed src="'+linkUrl+'" loop="false" autostart="false" volume="80" width="150" height="20" style="vertical-align:bottom"> <a href="'+linkUrl+'">'+uri.file+'</a>'
    } else
        return "<a target='_blank' href='" + linkUrl + "'>" + url + "</a>";
    
}

Youtube = {
  "timer": 0,
  
  "startAnimation": function(){
    if (!Youtube.timer)
      Youtube.timer = setTimeout(Youtube.animate, 1000)
  },
  
  "animate": function(){
    var thumbs = $(".youtube-thumb")
    thumbs.each(Youtube.nextThumb)
    if (thumbs.length == 0){
      clearTimeout(Youtube.timer)
      Youtube.timer = 0
    } else Youtube.timer = setTimeout(Youtube.animate, 1000);
  },
  
  "nextThumb": function(){
    var img = $(this);
    // yt thumb url is http://i.ytimg.com/vi/0123456789A/1.jpg
    var v = img.attr("src").substr(22,11)
    var num = img.attr("src").charAt(34);
    img.attr("src", (Youtube.nextThumbUrl(v, num)))
  },
  
  "nextThumbUrl": function(v, num){
    if (!num) num = 0;
    num = (parseInt(num) % 3) + 1 // cycle over 1,2,3
    return "http://i.ytimg.com/vi/" + v + "/" + num + ".jpg"
  },
  
}


function getUriType(uri){
  if (PicRegex.test(uri.file.toLowerCase()))
    return "image";
    
  if (parseDomain(uri.host) == "youtube.com" && ('v' in uri.queryKey || uri.anchor.indexOf('v') != -1))
    return "youtube";
  
  if (uri.path.substr(-4) == ".mid" || uri.path.substr(-5) == ".midi")
    return "midi"

  if (uri.path.substr(-4) == ".wav")
    return "wav"

  
  return "link";
}

function linkifyWithoutImage(text) {
    LastMsgContainsImage = false
    return text.replace(URLRegex, linkReplaceWithoutImage);
}

function linkReplaceWithoutImage(url){
    var urlWithoutParams = url.replace(/\?.*$/i, "");
    linkUrl = url.indexOf('http://') == 0 ? url : 'http://' + url;

    return "<a target='_blank' href='" + linkUrl + "'>" + url + "</a>"
}

// Message Handling

var ImageMsgCount = 0
function removeOldMessages(){
    // don't count posts that are all text
    if (LastMsgContainsImage) ImageMsgCount += 1;
    while (ImageMsgCount > MaxImagePosts) {
        var imgMsg = $(".contains-image:first")
        if (imgMsg.length) {
            imgMsg.prevAll().remove() // remove all text messages before the image message
            imgMsg.remove()
        } else break;
        ImageMsgCount -= 1;
    }
}

function buildMsgContent(content) {
  if (content.substr(0,6) == "<safe>") return content.substr(6,content.length - 13)
  else return linkify(escapeHtml(content));
}

// todo:
// isLoading doesn't get passed the right thing by $.map in addMessages
function buildMessageDiv(msg, isLoading) {
    var nick = escapeHtml(msg.nick);
//    if (nick == '' && Nick != '') return;
    removeOldMessages()
    var msgId = ('msg_id' in msg) ? 'id="message-' + msg.msg_id + '"' : '';
    var loadingClass = isLoading ? ' loading' : '';
    var containsImageClass = LastMsgContainsImage ? ' contains-image' : '';
    return '<div class="msgDiv dump ' + loadingClass + containsImageClass + '" ' + msgId  + '>'
        + '<span class="nick"><b><a href="http://dump.fm/' + nick + ' ">' + nick + '</a></b>'
        + ' <img src="'+Imgs.chatThumbDot+'" class="chat-thumb" onclick="Tag.favorite(this)"> '
        + '</span>'
        + '<span class="content">' + buildMsgContent(msg.content) + '</span>'
        + '</div>';
}

function buildUserDiv(user) {
    if (user.avatar) {
        return '<div class="username">'
            + '<a href="http://dump.fm/' + escapeHtml(user.nick) + '" target="_blank">'
            + '<img src="' + user.avatar + '" width="50" height="50">'
            + escapeHtml(user.nick) + '</a></div>';
    } else {
        return '<div class="username">'
            + '<a href="/' + escapeHtml(user.nick) + '" target="_blank">'
            + '<img src="/static/img/noinfo.png" width="50" height="50">'
            + escapeHtml(user.nick) + '</a></div>';
    }
}

// Favs

function buildFav(f) {
    var h = '<div class="fav-note">'
        + '<img src="/static/img/thumbs/chatheartover.gif">'
        + '<a href="http://dump.fm/' + f.from + '">' + f.from + '</a>'
        + '&nbsp;<span>just faved you!</span>'
        + '</div>';
    return $(h);
}

function removeFavAndHideBox() {
    $(this).remove();
    if ($('#favbox').children().length == 0)
        $('#favbox').hide();
}

function showFav(f) {
    $('#favbox').show();
    buildFav(f).appendTo('#favbox').animate(
    {"opacity": 0},
    {"duration": 9000,
    "easing": "easeInExpo",
    "complete": removeFavAndHideBox
    })
  
}


function updateFavs(fs) {
    if (fs.length == 0)
        return;
    $('#favbox').show();
    $(fs).each(function(i, f) { showFav(f) });
}


// Growl

function buildGrowlDataAndPopDatShit(msg) {
    var nick = escapeHtml(msg.nick);
    nick = '<a href="http://dump.fm/' + nick + ' " style="color:pink">' + nick + '</a>:'
    var msg = buildMsgContent(msg.content)
    growl(nick, msg)
}

function growl(user, msg) {
    $.gritter.add({title: user, text: msg});
}

function handleMsgError(resp) {
    var respText = resp.responseText ? resp.responseText.trim() : false;
    if (respText  == 'MUST_LOGIN') {
        alert("Can't send message! Please login.");
    } else if (respText) {
        alert("Can't send message! " + respText);
    } else {
        alert("Can't send message!");
    }
}

// Messages

function invalidImageDomain(content) {
    var words = content.toLowerCase().split(' ');
    for (var i = 0; i < words.length; i++) {
        var w = words[i];
        if (PicRegex.test(w)) {
            for (var j = 0; j < InvalidDomains.length; j++) {
                var d = InvalidDomains[j];
                if (w.indexOf(d) != -1) {
                    return d;
                }
            }
        }
    }
}

function submitMessage() {
    var content = $.trim($('#msgInput').val());

    var invalidDomain = invalidImageDomain(content);
    if (invalidDomain) {
        $('#msgInput').blur(); // Remove focus to prevent FF alert loop
        alert("Sorry, cannot accept images from " + invalidDomain + ". Maybe host the image elsewhere?");
        return;
    }

    $('#msgInput').val('');
    if (content ==  '') { return; }
    if (content.length > 2468) {
      alert("POST TOO LONG DUDE!");
      return;
    } // this shouldn't just be client side :V
    PendingMessages[content] = true;

    var msg = { 'nick': Nick, 'content': content };
    var div = addNewMessage(msg, true);

    var onSuccess = function(json) {
	if (typeof pageTracker !== 'undefined') {
            pageTracker._trackEvent('Message', 'Submit',
                                    typeof Room !== 'undefined' ? Room : 'UnknownRoom');
        }
        div.attr('id', 'message-' + json)
            .removeClass('loading').addClass('loaded');
    };
    var onError = function(resp, textStatus, errorThrown) {
        div.remove();
        handleMsgError(resp);
    };

    $.ajax({
        type: 'POST',
        timeout: 15000,
        url: '/msg',
        data: { 'room': Room, 'content': content },
        cache: false,
        dataType: 'json',
        success: onSuccess,
        error: onError
    });
}

function ifEnter(fn) {
    return function(e) {
        if (e.keyCode == 13) { fn(); }
    };
}

function addNewMessages(msgs) {
    var msgStr = $.map(msgs, buildMessageDiv).join('');
    $('#messageList').append(msgStr);
}

function addNewMessage(msg, isLoading) {
    var msgStr = buildMessageDiv(msg, isLoading);
    var div = $(msgStr).appendTo('#messageList');
    return div;
}

function setUserList(users) {
    $("#userList").html($.map(users, buildUserDiv).join(''));
}

function flattenUserJson(users) {
    var s = "";
    $.map(users.sort(), function(user) {
        s += user.nick + user.avatar;
    });
    return s;
}

function updateUI(msgs, users, favs) {
  if (window['growlize'] && msgs && msgs.length > 0) {
      $.map(msgs, buildGrowlDataAndPopDatShit)
  } else if (msgs && msgs.length > 0) {
      addNewMessages(msgs);
  }
  if (users !== null) {
      var flattened = flattenUserJson(users);
      if (!('userlist' in cache) || flattened != cache.userlist) {
          $("#userList").html($.map(users.sort(sortUsersByAlpha), buildUserDiv).join(''));
      }
      cache.userlist = flattened
  }
  updateFavs(favs);
}

function sortUsersByAlpha(a, b){
    var nickA = a.nick.toLowerCase()
    var nickB = b.nick.toLowerCase()
    if (nickA > nickB) return 1
    else if (nickA < nickB) return -1
    return 0
}

function isDuplicateMessage(m) {
    if (m.nick == Nick && m.content in PendingMessages) {
        delete PendingMessages[m.content];
        return true;
    } else {
        return false;
    }
}

function refresh() {
    var onSuccess = function(json) {
        try {
           Timestamp = json.timestamp;
            
            $.map(json.messages, function(msg){ MessageContentCache[msg.msg_id.toString()] = msg.content })
            
            var messages = $.grep(
                json.messages,
                function(m) { return !isDuplicateMessage(m) });
            updateUI(messages, json.users, json.favs);
            if (!Away.HasFocus)
                Away.UnseenMsgCounter += messages.length;
        } catch(e) {
            if (IsAdmin && window.console) {
                console.error(e);
            }
        }
        setTimeout(refresh, 1000);
    };
    var onError = function(resp, textStatus, errorThrown) {
        var msg = $.trim(resp.responseText);
        if (msg == "UNKNOWN_ROOM")
            location.href = "http://dump.fm";
        if (IsAdmin && window.console) {
            console.error(resp, textStatus, errorThrown);
        }
        setTimeout(refresh, 4000);
    };

    $.ajax({
        type: 'GET',
        timeout: 5000,
        url: '/refresh',
        data: { 'room': Room, 'since': Timestamp },
        cache: false,
        dataType: 'json',
        success: onSuccess,
        error: onError
    });
}

function sendClicked(){
        if (typeof pageTracker !== 'undefined') {
            pageTracker._trackEvent('UI', 'SendButtonActuallyClicked',
                                    typeof Nick !== 'undefined' ? Nick : 'anon');
        }
        submitMessage()


}

function paletteClicked(){
        if (typeof pageTracker !== 'undefined') {
            pageTracker._trackEvent('UI', 'FavPaletteActuallyClicked',
                                    typeof Nick !== 'undefined' ? Nick : 'anon');
        }
        paletteToggle()


}


function initChat() {
  Search.init()
  
    $('.oldmsg').each(function() {
        var dump = $(this);
        var content = dump.find(".content")
        MessageContentCache[dump.attr("id").substr(8)] = content.text()
        content.html(buildMsgContent(content.text()));
    });

    $('#msgInput').keyup(ifEnter(submitMessage));
    $('#msgSubmit').click(sendClicked);
    $('#palette-button').click(paletteClicked);

    messageList = $("#messageList")[0]

    initChatThumb();

    scrollToEnd()
    scrollWatcher()
    
    // see /static/webcam/webcam.js
    if ('webcam' in window) webcam.init()
    
    startChatUpdater();
}

function startChatUpdater() {
    setTimeout(refresh, 1000);
}

function makePlainText() {
    var j = $(this);
    j.text(j.text());
}

function activateProfileEditable() {
    var onSubmit = function(attr, newVal, oldVal) {
        newVal = $.trim(newVal);
        if (newVal == oldVal) { return oldVal };
        
        $.ajax({
            type: "POST",
            timeout: 5000,
            url: "/update-profile",
            data: { 'attr': attr, 'val': newVal }
        });
        if (attr == 'avatar') {
            if (newVal != "") {
                var s = '<img id="avatarPic" src="' + newVal + '" width="150" />';
                $('#avatarPic').replaceWith(s).show();
            } else {
                $('#avatarPic').hide();
            }
        }
        return escapeHtml(newVal);
    };

    if ($('#avatar-editing').length > 0)
      setupUploadAvatar('uploadp');

    var textareaOpts = { 'default_text': 'Enter here!',
                         'callback': onSubmit,
                         'field_type': 'textarea',
                         'callbackShowErrors': false };
    $('#contact.editable, #bio.editable')
        .editInPlace(textareaOpts)
        .each(makePlainText);
}

function enableProfileEdit() {
    $('img#contact').replaceWith('<div id="contact" class="linkify"></div>');
    $('img#bio').replaceWith('<div id="bio" class="linkify"></div>');
    $('#contact, #bio, #avatar').addClass('editable');
    $('#avatar-editing').show();
    var resetPage = function() { location.reload() };
    $('#edit-toggle a').text('done editing').click(resetPage);
    activateProfileEditable();
}

function initProfile() {
  Search.init()
    $(".linkify").each(function() {
        var text = jQuery(this).text();
        jQuery(this).html(linkifyWithoutImage(text));
    });    

    $('#edit-toggle').click(enableProfileEdit);
    activateProfileEditable();

    $('.dash-dump .content').each(function() {
        var t = $(this);
        t.html(buildMsgContent(t.text()));
    });
};

function initLog() {
  Search.init()
    $('.logged-dump .content').each(function() {
        var t = $(this);
        t.html(buildMsgContent(t.text()));
    });
    initLogThumb(".logged-dump .thumb", '.dump');
}

function initLogThumb(selector, parentSelector) {
  $(selector).bind('mouseover mouseout',
    function(e) {
      var favorited = $(this).parents(parentSelector).hasClass("favorite") ? true : false;
      if (e.type == "mouseover") {
        if (favorited) {
          $(this).attr("src", Imgs.logThumbOff);
        } else {
          $(this).attr("src", Imgs.logThumbBig);
          $(this).stop().animate(Anim.logThumbBig, 'fast');
        }
      } else { // mouseout
        if (favorited) {
          $(this).attr("src", Imgs.logThumb);
          $(this).stop().animate(Anim.logThumb, 'fast');
        } else {
          $(this).attr("src", Imgs.logThumbOff);
          $(this).stop().animate(Anim.logThumb, 'fast');
        }
      }
  })
 }

function initChatThumb(){
  $(".chat-thumb").live('mouseover mouseout',
    function(e) {
      var favorited = $(this).parents(".dump").hasClass("favorite") ? true : false;
      if (e.type == "mouseover") {
        if (favorited) {
          $(this).attr("src", Imgs.chatThumbOff);
        } else {
          $(this).attr("src", Imgs.chatThumbBig);
          $(this).stop().animate(Anim.chatThumbBig, 'fast')
        }
      } else { // mouseout
        if (favorited) {
          $(this).attr("src", Imgs.chatThumb);
          $(this).stop().animate(Anim.chatThumb, 'fast');
        } else {
          $(this).delay(600).stop().animate(Anim.chatThumbTiny, 'fast', 'swing',
            function(){
              $(this).attr("src", Imgs.chatThumbDot)
              $(this).animate(Anim.chatThumb, 0)
            })
        }
      }
  })
}


function paletteToChat(img){
  var chatText = $("#msgInput").val()
  if (chatText.length && chatText[chatText.length - 1] != " ")
    chatText += " "
  chatText += $(img).attr("src") + " "
  $("#msgInput").val(chatText)
  $("#msgInput").focus().val($("#msgInput").val()) //http://stackoverflow.com/questions/1056359/
  paletteHide()
}

paletteImageCache = false
function paletteBuildImageThumbs(){
  if (paletteImageCache) {
    var imgs = paletteImageCache
  } else {
    var imgs = []
    var dupeFilter = {}
    for(fav in RawFavs){
      var parsedImgs = getImagesAsArray(RawFavs[fav])
      for (var i=0; i<parsedImgs.length; i++){
        var img = parsedImgs[i]
        if (!dupeFilter[img]) {
          imgs.push(img)
          dupeFilter[img] = true
        }
      }
    }
    paletteImageCache = imgs
  }
    
  for(var i=0; i<imgs.length; i++){
    $("#palette-thumbs").append("<img onclick='paletteToChat(this)' src='"+imgs[i]+"'>")
  }
}

function paletteShow(){
  $("#palette").css("display", "block")
  if (isEmptyObject(RawFavs)) {
    $('#palette-thumbs').html('<div style="width:300px;color:#000;">This is where all the stuff you FAV goes!<br><br>To FAV a post click the little heart <img src="/static/img/thumbs/smallheart.gif"> next to a users name.<br><br> Everything you fav gets saved to your profile.. Have fun!</div>');
  } else {
    paletteBuildImageThumbs();
  }
}
function paletteHide(){
  $("#palette").css("display", "none")
  $("#palette-thumbs").html("")
}

function paletteToggle(){
  if ($("#palette").css("display") == "none")
    paletteShow()
  else
    paletteHide()
}


function setupUpload(elementId, roomKey) {
    var onSubmit = function(file, ext) {
	if (!(ext && /^(jpg|png|jpeg|gif|bmp|svg)$/i.test(ext))) {
	    alert('SORRY, NOT AN IMAGE DUDE... ');
	    return false;
	}
    };
    var onComplete = function(file, response) {
        var r = $.trim(response);
        if (r.match(/FILE_TOO_BIG/)) {
            var maxSize = r.split(" ")[1] / 1024;
            alert("Sorry. Your file is just too fucking big. "
                  + maxSize + "KB or less please.");
            return;
        } else if (r.match(/FILE_NOT_IMAGE/)) {
            alert("What did you upload? Doesn't seem like an image. Sorry.");
            return;
        } else if (r.match(/INVALID_RESOLUTION/)) {
            var maxWidth = r.split(" ")[1];
            var maxHeight = r.split(" ")[2];
            alert("Sorry, the maximum image resolution is "
                  + maxWidth + "x" + maxHeight);
            return;
        } else if (r != "OK") {
            alert(r);
            return;
        }

    	if (typeof pageTracker !== 'undefined') {
            var r = typeof Room !== 'undefined' ? Room : 'UnknownRoom';
            pageTracker._trackEvent('Message', 'Upload', r);
        }
    }
    new AjaxUpload(elementId, {
        action: '/upload/message',
        autoSubmit: true,
        name: 'image',
        data: { room: roomKey },
	    onSubmit: onSubmit,
        onComplete: onComplete
    });
}

function setupUploadAvatar(elementId) {
    // NOTE: AjaxUpload responses aren't converted from JSON.
    var onSubmit = function(file, error) {
        $('#spinner').show();
    };
    var onComplete = function(file, resp) {
        $('#spinner').hide();
	var r = $.trim(resp);

        if (r == 'INVALID_REQUEST') {
            location.reload();
        } else if (r == 'NOT_LOGGED_IN') {
            location.reload();
        } else if (r == 'INVALID_IMAGE') {
            alert("Sorry, dump.fm can't deal with your image. Pick another :(");
            return;
        } else if (r.match(/FILE_TOO_BIG/)) {
	    var maxSize = r.split(" ")[1] / 1024;
	    alert("Sorry. Your avatar is just too fucking big. "
		  + maxSize + "KB or less please.");
	    return;
	} else if (r.match(/INVALID_RESOLUTION/)) {
            var maxWidth = r.split(" ")[1];
            var maxHeight = r.split(" ")[2];
            alert("Sorry, the maximum avatar resolution is "
                  + maxWidth + "x" + maxHeight);
            return;
        }
	
        var s = '<img id="dashavatarPic" src="' + r + '" />';
        $('#dashavatar').html(s).show();
    };
    new AjaxUpload(elementId, {
        action: '/upload/avatar',
        autoSubmit: true,
        name: 'image',
        onSubmit: onSubmit,
        onComplete: onComplete
    });
}


/*
  search adds a script to the page... the script will call either Search.searchResult() or Search.searchError()
  todo: clean this up. remove duplicated function names etc
*/

var Search = {
  
  'term': "",
  'imagesPerPage': 25,
  'images': [],
  'tokens': [],
  
  'init': function(){
    $("#search-query").val("search dump.fm")
    $("#search-query").focus(function(){
      if ($("#search-query").val() == 'search dump.fm')
        $("#search-query").val("")
    })
    $("#search-query").blur(function(){
      if ($("#search-query").val().trim() == '')
        $("#search-query").val("search dump.fm")
    })
    $("#search-query").keydown(ifEnter(Search.doSearch))
    $("#search-results-images a").live("mouseup", Search.click)
  },

  'addScript': function(term) {
    $("#search-script").remove()
    $("head").append("<scr"+"ipt src='/cmd/ghettosearch/"+term+"' id='search-script'></sc"+"ript>")
  },
  'setContent': function(x){
    $("#search-results-images").html(x)
  },
  'setMessage': function(x){
    $("#search-controls").css("display", "block")
    $("#search-control-text").html(x)
  },
  'searchError': function(error){
    Search.setContent("")
    $('#search-control-previous').css("visibility", "hidden")
    $('#search-control-next').css("visibility", "hidden")
    Search.setMessage(error)
  },

  'doSearch': function(){
    term = $("#search-query").val().trim().toLowerCase()
    var rawTokens = term.split(" ")
    Search.tokens = []
    for(var t = 0; t < rawTokens.length; t++) {
      if (rawTokens[t].length > 2)
        Search.tokens.push(rawTokens[t])
    }
    if (Search.tokens.length == 0) {
      Search.setMessage("search query too small")
    } else {
      Search.setMessage("searching for '"+Search.tokens.join(" and ")+"'")
      Search.addScript(Search.tokens.join("+"))
    }
  },

  'renderPage': function(num){
    $("#search-results-images").css("display", "block")
    $("#search-controls").css("display", "block")
    if (Search.images.length > 0)
        Search.setMessage("results for '"+Search.tokens.join(" and ")+"' (page " + (num + 1) + ")");
    var contentString = ''
    var start = num * Search.imagesPerPage
    var imageCounter = 0
    for(var i = start; i < Search.images.length; i++){
        if(imageCounter > Search.imagesPerPage) break;
        contentString += '<a href="'+Search.images[i]+'" target="_blank" onclick="return false"><img src="'+Search.images[i]+'"></a>'
        imageCounter += 1
    }
    contentString += '<br><br><div id="search-commands">'
    if(num > 0) {
      $('#search-control-previous').attr("href", 'javascript:Search.renderPage('+(num-1)+')')
      $('#search-control-previous').css("visibility", "visible")
    } else {
      $('#search-control-previous').attr("href", 'javascript:void()')
      $('#search-control-previous').css("visibility", "hidden")
    }
    
    if (Search.images.length > start + imageCounter) {
      $('#search-control-next').attr("href", 'javascript:Search.renderPage('+(num+1)+')')
      $('#search-control-next').css("visibility", "visible")
    } else {
      $('#search-control-next').attr("href", 'javascript:void()')
      $('#search-control-next').css("visibility", "hidden")
    }
    Search.setContent(contentString)
  },

  'click': function(e){
    if (e.which == 1) // left click
      if (Search.addToChatBoxIfPossible(this))
        window.open(this.href)
    else if (e.which == 2) // middle click
      window.open(this.href)
  },

  'addToChatBoxIfPossible': function(img){
    var chatBoxExists = $("#msgInput").length
    if (chatBoxExists) {
      var chatText = $("#msgInput").val()
      if (chatText.length && chatText[chatText.length - 1] != " ")
        chatText += " "
      chatText += $(img).attr("href") + " "
      $("#msgInput").val(chatText)
      $("#msgInput").focus().val($("#msgInput").val()) //http://stackoverflow.com/questions/1056359/
      return false
    } else return true
  },

  'searchResult': function(results){
    Search.images = []
    var alreadyGot = {}
    if(results === null || results.length == 0) {
        Search.setContent("")
        Search.setMessage("no results found")        
    } else {
        for(var r = 0; r<results.length; r++){
          var content = results[r]['content']
          if (content.substring(0,6) == "<safe>") continue; // skip html posts
          var imageUrls = getImagesAsArray(content);
          for (var i=0; i<imageUrls.length; i++){
	          var imageUrl = imageUrls[i];
	          if (imageUrl in alreadyGot) continue;
	          alreadyGot[imageUrl] = true
	          var validImage = true;
	          for(var t = 0; t<Search.tokens.length; t++){
	            if (imageUrl.toLowerCase().indexOf(Search.tokens[t]) == -1) {
		            validImage = false;
		            break;
	            }
	          }
	          if (validImage)
	            Search.images.push(imageUrl);
          }
        }
        if (Search.images.length == 0) {
            Search.setMessage("no results found")  
        }
        Search.renderPage(0)
    }
  },
  'close': function(){
    Search.setContent("")
    $('#search-control-previous').css("visibility", "hidden")
    $('#search-control-next').css("visibility", "hidden")
    $("#search-results-images").css("display", "none")
    $("#search-controls").css("display", "none")
  }
  

}


// scrolling stuff
// this code keeps the div scrolled to the bottom, but will also let the user scroll up, without jumping down

function isScrolledToBottom(){
    var threshold = 15;
    
    var containerHeight = messageList.style.pixelHeight || messageList.offsetHeight
    var currentHeight = (messageList.scrollHeight > 0) ? messageList.scrollHeight : 0

    var result = (currentHeight - messageList.scrollTop - containerHeight < threshold);

    return result;  
}

function scrollIfPossible(){
    if (lastScriptedScrolledPosition <= messageList.scrollTop || isScrolledToBottom())
        scrollToEnd()
}

var lastScriptedScrolledPosition = 0
function scrollToEnd(){
    messageList.scrollTop = messageList.scrollHeight
    lastScriptedScrolledPosition = messageList.scrollTop
}

function scrollWatcher(){
    scrollIfPossible()
    setTimeout(scrollWatcher, 500)
}

// well fuck webkit for not supporting {text-decoration: blink}

function blinkStart(){
    blinkTimer = setInterval(function(){
        $(".blink").removeClass("blink").addClass("blink-turning-off")
        $(".blink-off").removeClass("blink-off").addClass("blink")
        $(".blink-turning-off").removeClass("blink-turning-off").addClass("blink-off")
    },500);
}

function blinkStop(){
    clearInterval(blinkTimer);
}

function initDirectory() {
    $('.linkify').each(function() {
        var t = $(this);
        t.html(buildMsgContent(t.text()));
    });
    Search.init()
    initLogThumb('.dlogged-dump .thumb', '.dlogged-dump');
}

//big hand stuff
// TODO: replace this with simple pointer-events thing.
function initBigHand(id){
    var cursorId = "#cursor-big"
    var cursor = $(cursorId)[0]
    
    // jquery's reported element sizes are not exactly the same as the browser's 'mouseover' target sizes
    // so we'll allow a few pixels extra
    var fudgeFactor = 2

    $(id).addClass("no-cursor")

    // i have to do this weirdly bc putting the cursor image where the mouse cursor is causes problems with mouse events:
    // * it stops mousemove events on the image below the mouse cursor
    // * it fucks up mouseover/out and even mouseenter/leave events, as well as click
    
    // so i am doing this:
    // on mousing over the image:
    //    make cursor visible
    //    find image co-ords
    //    bind a global mousemove func
    //    bind cursor click event
    //    unbind mouseover
    // mousemove func:
    //    move image to mouse co-ords
    //    if mouse co-ords are outside the image co-ords:
    //        make cursor invisible
    //        unbind mousemove func
    //        unbind cursor click event

    var mousemove = function(e){
        var y = e.pageY, x = e.pageX, coords = initBigHand.coords
        
        cursor.style.top = y + "px"
        cursor.style.left = x - 32 + "px" // 32: (4 pixels * 8 pixels per big pixel) to line up pointy finger with cursor
        if (y < coords.top || 
            y > coords.bottom ||
            x < coords.left || 
            x > coords.right) {
            $(cursorId).addClass('invisible')
            $(cursorId).css({"top": 0, "left": 0 })
            $(cursorId).unbind('click', cursorClick)
            $('logo7').unbind('mousemove', mousemove)
            $(id).mouseover(imageMouseOver)
        }    
    }
    
    var cursorClick = function(){ $(id).click() }
    
    var imageMouseOver = function(){
        //console.log("moused over...")
        initBigHand.coords = {
            "left":   $(id).offset().left - fudgeFactor,
            "top":    $(id).offset().top - fudgeFactor,
            "right":  $(id).offset().left + $(id).width() + fudgeFactor,
            "bottom": $(id).offset().top + $(id).height() + fudgeFactor
        }
        $('body').mousemove(mousemove)
        $(cursorId).click(cursorClick)
        $(cursorId).removeClass('invisible')
        $(id).unbind('mouseover', imageMouseOver)
    }
    
    $(id).mouseover(imageMouseOver)
    
}

// grab message id etc from some element e that's inside a dump
// (messages have something like id="message-0001" class="dump" )
function getMessageInfo(e){
    var message = $(e).parents(".dump")
    var id = message.attr("id").substr(8) // cut "message-001" to "001"
    var nick = message.attr("nick")
    var link = "http://dump.fm/p/" + nick + "/" + id
    var content = message.find(".linkify")
    if (!content.length) content = message.find(".content")
    var rawContent = content.html()
    var img = content.find("img").attr("src")
    var via = "via " + nick + " on dump.fm"
    return {"nick": nick, "id": id, "link": encodeURIComponent(link), 
            "content": rawContent, "img": encodeURIComponent(img), 
            "via": encodeURIComponent(via)}
}

Share = {
  "openLink": function(url){
    window.open(url, "_blank")
  },
  "facebook": function(button){
    var message = getMessageInfo(button)
    var url = "http://www.facebook.com/share.php?u=" + message.img + "&t=" + message.via
    Share.openLink(url)
  },
  "tumblr": function(button){
    var message = getMessageInfo(button)
    var url = "http://www.tumblr.com/share?v=3&u=" + message.img + "&t=" + message.via
    Share.openLink(url)
  },
  "twitter": function(button){
    var message = getMessageInfo(button)
    var url = "http://twitter.com/home?status=" + message.img + encodeURIComponent(" ") + message.via
    Share.openLink(url)
  },
  "delicious": function(button){
    var message = getMessageInfo(button)
    var url = "http://delicious.com/save?url=" + message.img + "&title=" + message.img + "&notes=" + message.via
    Share.openLink(url)
  }
}

Tag = {
  "favorite": function(button){
    var message = getMessageInfo(button)
    var favorited = ($(button).parents(".dump").hasClass("favorite")) ? true : false
    if (favorited) {
      Tag.rm(message.id, "favorite")
      $(button).parents(".dump").removeClass("favorite")
      if (RawFavs[message.id]) {
        delete RawFavs[message.id]
        paletteImageCache = false
      }
    } else {
      Tag.add(message.id, "favorite")
      $(button).parents(".dump").addClass("favorite")
      if (RawFavs && MessageContentCache[message.id]) { // chat ui stuff
        if ($("#palette-button").css("display") == "none")
          paletteButtonShowAnim()
        RawFavs[message.id] = MessageContentCache[message.id]
        paletteImageCache = false
      }
    }
  },
  "add": function(message_id, tag){
    Tag.ajax("/cmd/tag/add", {"message_id": message_id, "tag": tag})
  },
  "rm": function(message_id, tag){
    Tag.ajax("/cmd/tag/rm", {"message_id": message_id, "tag": tag})
  },
  "ajax": function(url, data) {
    $.ajax({
        "type": 'POST',
        "timeout": 5000,
        "url": url,
        "data": data,
        "cache": false
    });
  }
}


// uhhh todo: move preload stuff into js:
// var nextImage = new Image();
// nextImage.src = "your-url/newImage.gif";

// mAcRoMeDiA sHiT
function MM_swapImgRestore() { //v3.0
    var i,x,a=document.MM_sr; for(i=0;a&&i<a.length&&(x=a[i])&&x.oSrc;i++) x.src=x.oSrc;
}

function MM_preloadImages() { //v3.0
    var d=document;if(d.images){ if(!d.MM_p) d.MM_p=new Array();var i,j=d.MM_p.length,a=MM_preloadImages.arguments; for(i=0; i<a.length; i++) if (a[i].indexOf("#")!=0){ d.MM_p[j]=new Image; d.MM_p[j++].src=a[i];}}
}

function MM_findObj(n, d) { //v4.01
  var p,i,x;  if(!d) d=document; if((p=n.indexOf("?"))>0&&parent.frames.length) {
    d=parent.frames[n.substring(p+1)].document; n=n.substring(0,p);}
  if(!(x=d[n])&&d.all) x=d.all[n]; for (i=0;!x&&i<d.forms.length;i++) x=d.forms[i][n];
  for(i=0;!x&&d.layers&&i<d.layers.length;i++) x=MM_findObj(n,d.layers[i].document);
  if(!x && d.getElementById) x=d.getElementById(n); return x;
}

function MM_swapImage() { //v3.0
  var i,j=0,x,a=MM_swapImage.arguments; document.MM_sr=new Array; for(i=0;i<(a.length-2);i+=3)
   if ((x=MM_findObj(a[i]))!=null){document.MM_sr[j++]=x; if(!x.oSrc) x.oSrc=x.src; x.src=a[i+2];}
}

function timeFunc(f){
  var start = new Date().getTime();
  var res = f();
  console.log((new Date().getTime()) - start + " msecs");
  return res;
}

// parseUri 1.2.2 from http://blog.stevenlevithan.com/archives/parseuri
// (c) Steven Levithan <stevenlevithan.com>, MIT License
function parseUri (str) {
	var	o   = parseUri.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;
};

parseUri.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*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
	}
};
// end parseUri

function parseDomain(host){
  return host.toLowerCase().replace(/^www\./, "")
}

var Away = {
    "UnseenMsgCounter": 0,
    "OrigTitle": "",
    "HasFocus": true,
    "UpdateFrequency": 3000,

    "onFocus": function() {
        Away.HasFocus = true;
        Away.UnseenMsgCounter = 0;
        // Courtesy http://stackoverflow.com/questions/2952384/changing-the-window-title-when-focussing-the-window-doesnt-work-in-chrome
        window.setTimeout(function () { $('title').text(Away.OrigTitle); }, 100);
    },
    "onBlur": function() {
        Away.HasFocus = false;
    },

    "updateTitle": function () {
        if (Away.UnseenMsgCounter > 0) {
            var plural = Away.UnseenMsgCounter > 1 ? 's' : '';
            $('title').text(Away.UnseenMsgCounter + ' new dump' + plural + '! | ' + Away.OrigTitle);
        }
        setTimeout(Away.updateTitle, Away.UpdateFrequency);
        
    },
    "startTitleUpdater": function() {
        Away.OrigTitle = $('title').text();
        $(window).blur(Away.onBlur);
        $(window).focus(Away.onFocus);
        setTimeout(Away.updateTitle, Away.UpdateFrequency);
    }
};

var imgZoomThreshhold = [125, 125];

function initChatMsgs() {
    $('.msgDiv .content').live('mouseenter', function(e) {
	$(this).addClass('msg-hover');
    });

    $('.msgDiv .content').live('mouseleave', function(e) {
	$(this).removeClass('msg-hover');
    });

    $('.msgDiv .content .img-wrapper').live('mouseenter', function(e) {
	var img = $(this).find('img');

	if (img.width() < imgZoomThreshhold[0] || img.height() < imgZoomThreshhold[1])
	    return;	
	
        var zoomlink = $('<a>')
	    .attr({'href': img.attr('src') })
	    .addClass('msg-image-zoom')
	    .append($('<img>').attr('src', 'http://dump.fm/static/img/zoom.gif')
		    .addClass('zoom-icon'))
	    .click(function() { window.open(img.attr('src')); return false; });
        $(this).append(zoomlink);
    });

    $('.msgDiv .content .img-wrapper').live('mouseleave', function(e) {
	$(this).find('.msg-image-zoom').remove();
    });
    

    $('.content').live('click', function(e) {
	var tagName = e.target.tagName;
	if (tagName == 'A' || tagName == 'EMBED' || $(e.target).hasClass('youtube-thumb')) {
            return true;
	}
	var msg = $(this).parent('.msgDiv');
	var wasFavorited = msg.hasClass("favorite");
	var button = msg.find('.chat-thumb');
	if (wasFavorited) {
            $(button).attr("src", Imgs.chatThumbOff);
	} else {
            $(button).attr("src", Imgs.chatThumbBig);
            $(button).stop().animate(Anim.chatThumbBig, 'fast').animate(Anim.chatThumb, 'fast', 'swing');
	}
	Tag.favorite(button);
	return false;
    });
}

// sha1.js

/*
 * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined
 * in FIPS 180-1
 * Version 2.2 Copyright Paul Johnston 2000 - 2009.
 * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
 * Distributed under the BSD License
 * See http://pajhome.org.uk/crypt/md5 for details.
 */

/*
 * Configurable variables. You may need to tweak these to be compatible with
 * the server-side, but the defaults work in most cases.
 */
var hexcase = 0;  /* hex output format. 0 - lowercase; 1 - uppercase        */
var b64pad  = ""; /* base-64 pad character. "=" for strict RFC compliance   */

/*
 * These are the functions you'll usually want to call
 * They take string arguments and return either hex or base-64 encoded strings
 */
function hex_sha1(s)    { return rstr2hex(rstr_sha1(str2rstr_utf8(s))); }
function b64_sha1(s)    { return rstr2b64(rstr_sha1(str2rstr_utf8(s))); }
function any_sha1(s, e) { return rstr2any(rstr_sha1(str2rstr_utf8(s)), e); }
function hex_hmac_sha1(k, d)
  { return rstr2hex(rstr_hmac_sha1(str2rstr_utf8(k), str2rstr_utf8(d))); }
function b64_hmac_sha1(k, d)
  { return rstr2b64(rstr_hmac_sha1(str2rstr_utf8(k), str2rstr_utf8(d))); }
function any_hmac_sha1(k, d, e)
  { return rstr2any(rstr_hmac_sha1(str2rstr_utf8(k), str2rstr_utf8(d)), e); }

/*
 * Perform a simple self-test to see if the VM is working
 */
function sha1_vm_test()
{
  return hex_sha1("abc").toLowerCase() == "a9993e364706816aba3e25717850c26c9cd0d89d";
}

/*
 * Calculate the SHA1 of a raw string
 */
function rstr_sha1(s)
{
  return binb2rstr(binb_sha1(rstr2binb(s), s.length * 8));
}

/*
 * Calculate the HMAC-SHA1 of a key and some data (raw strings)
 */
function rstr_hmac_sha1(key, data)
{
  var bkey = rstr2binb(key);
  if(bkey.length > 16) bkey = binb_sha1(bkey, key.length * 8);

  var ipad = Array(16), opad = Array(16);
  for(var i = 0; i < 16; i++)
  {
    ipad[i] = bkey[i] ^ 0x36363636;
    opad[i] = bkey[i] ^ 0x5C5C5C5C;
  }

  var hash = binb_sha1(ipad.concat(rstr2binb(data)), 512 + data.length * 8);
  return binb2rstr(binb_sha1(opad.concat(hash), 512 + 160));
}

/*
 * Convert a raw string to a hex string
 */
function rstr2hex(input)
{
  try { hexcase } catch(e) { hexcase=0; }
  var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
  var output = "";
  var x;
  for(var i = 0; i < input.length; i++)
  {
    x = input.charCodeAt(i);
    output += hex_tab.charAt((x >>> 4) & 0x0F)
           +  hex_tab.charAt( x        & 0x0F);
  }
  return output;
}

/*
 * Convert a raw string to a base-64 string
 */
function rstr2b64(input)
{
  try { b64pad } catch(e) { b64pad=''; }
  var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
  var output = "";
  var len = input.length;
  for(var i = 0; i < len; i += 3)
  {
    var triplet = (input.charCodeAt(i) << 16)
                | (i + 1 < len ? input.charCodeAt(i+1) << 8 : 0)
                | (i + 2 < len ? input.charCodeAt(i+2)      : 0);
    for(var j = 0; j < 4; j++)
    {
      if(i * 8 + j * 6 > input.length * 8) output += b64pad;
      else output += tab.charAt((triplet >>> 6*(3-j)) & 0x3F);
    }
  }
  return output;
}

/*
 * Convert a raw string to an arbitrary string encoding
 */
function rstr2any(input, encoding)
{
  var divisor = encoding.length;
  var remainders = Array();
  var i, q, x, quotient;

  /* Convert to an array of 16-bit big-endian values, forming the dividend */
  var dividend = Array(Math.ceil(input.length / 2));
  for(i = 0; i < dividend.length; i++)
  {
    dividend[i] = (input.charCodeAt(i * 2) << 8) | input.charCodeAt(i * 2 + 1);
  }

  /*
   * Repeatedly perform a long division. The binary array forms the dividend,
   * the length of the encoding is the divisor. Once computed, the quotient
   * forms the dividend for the next step. We stop when the dividend is zero.
   * All remainders are stored for later use.
   */
  while(dividend.length > 0)
  {
    quotient = Array();
    x = 0;
    for(i = 0; i < dividend.length; i++)
    {
      x = (x << 16) + dividend[i];
      q = Math.floor(x / divisor);
      x -= q * divisor;
      if(quotient.length > 0 || q > 0)
        quotient[quotient.length] = q;
    }
    remainders[remainders.length] = x;
    dividend = quotient;
  }

  /* Convert the remainders to the output string */
  var output = "";
  for(i = remainders.length - 1; i >= 0; i--)
    output += encoding.charAt(remainders[i]);

  /* Append leading zero equivalents */
  var full_length = Math.ceil(input.length * 8 /
                                    (Math.log(encoding.length) / Math.log(2)))
  for(i = output.length; i < full_length; i++)
    output = encoding[0] + output;

  return output;
}

/*
 * Encode a string as utf-8.
 * For efficiency, this assumes the input is valid utf-16.
 */
function str2rstr_utf8(input)
{
  var output = "";
  var i = -1;
  var x, y;

  while(++i < input.length)
  {
    /* Decode utf-16 surrogate pairs */
    x = input.charCodeAt(i);
    y = i + 1 < input.length ? input.charCodeAt(i + 1) : 0;
    if(0xD800 <= x && x <= 0xDBFF && 0xDC00 <= y && y <= 0xDFFF)
    {
      x = 0x10000 + ((x & 0x03FF) << 10) + (y & 0x03FF);
      i++;
    }

    /* Encode output as utf-8 */
    if(x <= 0x7F)
      output += String.fromCharCode(x);
    else if(x <= 0x7FF)
      output += String.fromCharCode(0xC0 | ((x >>> 6 ) & 0x1F),
                                    0x80 | ( x         & 0x3F));
    else if(x <= 0xFFFF)
      output += String.fromCharCode(0xE0 | ((x >>> 12) & 0x0F),
                                    0x80 | ((x >>> 6 ) & 0x3F),
                                    0x80 | ( x         & 0x3F));
    else if(x <= 0x1FFFFF)
      output += String.fromCharCode(0xF0 | ((x >>> 18) & 0x07),
                                    0x80 | ((x >>> 12) & 0x3F),
                                    0x80 | ((x >>> 6 ) & 0x3F),
                                    0x80 | ( x         & 0x3F));
  }
  return output;
}

/*
 * Encode a string as utf-16
 */
function str2rstr_utf16le(input)
{
  var output = "";
  for(var i = 0; i < input.length; i++)
    output += String.fromCharCode( input.charCodeAt(i)        & 0xFF,
                                  (input.charCodeAt(i) >>> 8) & 0xFF);
  return output;
}

function str2rstr_utf16be(input)
{
  var output = "";
  for(var i = 0; i < input.length; i++)
    output += String.fromCharCode((input.charCodeAt(i) >>> 8) & 0xFF,
                                   input.charCodeAt(i)        & 0xFF);
  return output;
}

/*
 * Convert a raw string to an array of big-endian words
 * Characters >255 have their high-byte silently ignored.
 */
function rstr2binb(input)
{
  var output = Array(input.length >> 2);
  for(var i = 0; i < output.length; i++)
    output[i] = 0;
  for(var i = 0; i < input.length * 8; i += 8)
    output[i>>5] |= (input.charCodeAt(i / 8) & 0xFF) << (24 - i % 32);
  return output;
}

/*
 * Convert an array of big-endian words to a string
 */
function binb2rstr(input)
{
  var output = "";
  for(var i = 0; i < input.length * 32; i += 8)
    output += String.fromCharCode((input[i>>5] >>> (24 - i % 32)) & 0xFF);
  return output;
}

/*
 * Calculate the SHA-1 of an array of big-endian words, and a bit length
 */
function binb_sha1(x, len)
{
  /* append padding */
  x[len >> 5] |= 0x80 << (24 - len % 32);
  x[((len + 64 >> 9) << 4) + 15] = len;

  var w = Array(80);
  var a =  1732584193;
  var b = -271733879;
  var c = -1732584194;
  var d =  271733878;
  var e = -1009589776;

  for(var i = 0; i < x.length; i += 16)
  {
    var olda = a;
    var oldb = b;
    var oldc = c;
    var oldd = d;
    var olde = e;

    for(var j = 0; j < 80; j++)
    {
      if(j < 16) w[j] = x[i + j];
      else w[j] = bit_rol(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1);
      var t = safe_add(safe_add(bit_rol(a, 5), sha1_ft(j, b, c, d)),
                       safe_add(safe_add(e, w[j]), sha1_kt(j)));
      e = d;
      d = c;
      c = bit_rol(b, 30);
      b = a;
      a = t;
    }

    a = safe_add(a, olda);
    b = safe_add(b, oldb);
    c = safe_add(c, oldc);
    d = safe_add(d, oldd);
    e = safe_add(e, olde);
  }
  return Array(a, b, c, d, e);

}

/*
 * Perform the appropriate triplet combination function for the current
 * iteration
 */
function sha1_ft(t, b, c, d)
{
  if(t < 20) return (b & c) | ((~b) & d);
  if(t < 40) return b ^ c ^ d;
  if(t < 60) return (b & c) | (b & d) | (c & d);
  return b ^ c ^ d;
}

/*
 * Determine the appropriate additive constant for the current iteration
 */
function sha1_kt(t)
{
  return (t < 20) ?  1518500249 : (t < 40) ?  1859775393 :
         (t < 60) ? -1894007588 : -899497514;
}

/*
 * Add integers, wrapping at 2^32. This uses 16-bit operations internally
 * to work around bugs in some JS interpreters.
 */
function safe_add(x, y)
{
  var lsw = (x & 0xFFFF) + (y & 0xFFFF);
  var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
  return (msw << 16) | (lsw & 0xFFFF);
}

/*
 * Bitwise rotate a 32-bit number to the left.
 */
function bit_rol(num, cnt)
{
  return (num << cnt) | (num >>> (32 - cnt));
}
