// http://plugins.jquery.com/files/jquery.cookie.js.txt
jQuery.cookie=function(name,value,options){if(typeof value!='undefined'){options=options||{};if(value===null){value='';options.expires=-1;}
var expires='';if(options.expires&&(typeof options.expires=='number'||options.expires.toUTCString)){var date;if(typeof options.expires=='number'){date=new Date();date.setTime(date.getTime()+(options.expires*24*60*60*1000));}else{date=options.expires;}
expires='; expires='+date.toUTCString();}
var path=options.path?'; path='+(options.path):'';var domain=options.domain?'; domain='+(options.domain):'';var secure=options.secure?'; secure':'';document.cookie=[name,'=',encodeURIComponent(value),expires,path,domain,secure].join('');}else{var cookieValue=null;if(document.cookie&&document.cookie!=''){var cookies=document.cookie.split(';');for(var i=0;i<cookies.length;i++){var cookie=jQuery.trim(cookies[i]);if(cookie.substring(0,name.length+1)==(name+'=')){cookieValue=decodeURIComponent(cookie.substring(name.length+1));break;}}}
return cookieValue;}};

// The root domain is used so that subdomains don't result in
// spurious extra urls (e.g. both dump.fm/nick and sub.dump.fm/nick)
var RootDomain = location.href.match(/http:\/\/(\w)+\./)
    ? 'http://dump.fm/' : '/';

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

emptyFunc = function(){};
falseFunc = function(){ return false };

isImgBroken = function(img){
  return (img.height == 0 || img.width == 0 || img.height == NaN || img.width == NaN)
}

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 track(group, name) {
    if (typeof pageTracker !== 'undefined') {
        pageTracker._trackEvent(group, name,
                                typeof Nick !== 'undefined' ? Nick : 'anon');
    }
}

var Preferences = {
    "Domain": '.dump.fm',
    
    "getProperty": function(prop, defaultValue) {
        var value = $.cookie(prop);
        return (value !== null) ? value : defaultValue;
    },
    
    "setProperty": function(prop, val) {
        $.cookie(prop, val, { domain: Preferences.Domain, path: '/' });
    },
    
    "delProperty": function(prop) {
        $.cookie(prop, null, { domain: Preferences.Domain, path: '/' });
    }
};

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;
RecipRegex = /(^|\s)@\w+/g;
TopicRegex = /(^|\s)#\w+/g;


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 topicReplace(text) {
    text = $.trim(text).toLowerCase();
    var topicLabel = text.substring(1);
    return ' <a target="_blank" href="'+ RootDomain + 't/' + topicLabel + '">' + text + '</a> ';
}

function recipientReplace(atText, recips) {
    recips = recips || [];

    var space = '';    
    if (atText[0] == ' ') {
        atText = atText.slice(1);
        space = ' ';
    }
    
    var nick = atText.slice(1).toLowerCase();
    var matchedRecip;
    for (var i = 0; i < recips.length; i++) {
        if (recips[i].toLowerCase() == nick) {
            matchedRecip = recips[i];
            break;
        }
    }
    
    if (matchedRecip) {
        return space + '<a target="_blank" href="' + RootDomain + matchedRecip + '">@' + matchedRecip + '</a>';
    } else {
        return space + atText;
    }
}

function linkify(text, recips) {
    LastMsgContainsImage = false;
    var recipWrapper = function(text) { return recipientReplace(text, recips); };
    return text
        .replace(URLRegex, linkReplace)
        .replace(RecipRegex, recipWrapper)
        .replace(TopicRegex, topicReplace);
}

// use this in escapeHtml to turn everyone's text lIkE tHiS
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";
  
  var domain = parseDomain(uri.host)
  
  if (domain == "gstatic" && uri.path == "/images" && 'q' in uri.queryKey)
    return "image"; 
  
 // actual image url = uri.queryKey['q'].split(":").slice(2).join(":") but often the original image is broken...


  if (domain == "youtube" && ('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;
    }
}

var TextEnabled = Preferences.getProperty("chat.textEnabled", "true") == "none";
var ImgsEnabled = Preferences.getProperty("chat.imgsEnabled", "true") == "true";

function muteSelector() {
    var muted = [];
    for (nick in MUTES) {
        muted.push(".nick_" + nick);
    }
    return muted.join(",");
}
function setTextEnable() {
    var muted = muteSelector();
    if ($(this).attr('checked')) {
        TextEnabled = true;
        Preferences.setProperty("chat.textEnabled", "false");
        track('UI', 'TextEnabled');
        $('.dump').not('.contains-image,'+muted).show();
    } else {
        TextEnabled = false;
        Preferences.setProperty("chat.textEnabled", "false");
        track('UI', 'TextDisabled');
        $('.dump').not('.contains-image').hide()
    }
};

function setImgsEnable() {
    var muted = muteSelector();
    if ($(this).attr('checked')) {
        ImgsEnabled = true;
        Preferences.setProperty("chat.imgsEnabled", "true");
        track('UI', 'ImgsEnabled');
        $('.contains-image').not(muted).show();
    } else {
        ImgsEnabled = false;
        Preferences.setProperty("chat.imgsEnabled", "false");
        track('UI', 'ImgsDisabled');
        $('.contains-image').hide();
    }
};

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

// todo:
// isLoading doesn't get passed the right thing by $.map in addMessages
function buildMessageDiv(msg, opts) {
    var opts = opts || {};
    var nick = escapeHtml(msg.nick);
    removeOldMessages();

    var builtContent = buildMsgContent(msg.content, msg.recips);
    var msgId = ('msg_id' in msg) ? 'id="message-' + msg.msg_id + '"' : '';
    var loadingClass = opts.isLoading ? ' loading' : '';
    var containsImageClass = LastMsgContainsImage ? ' contains-image' : '';
    var displayStyle = ((ImgsEnabled && LastMsgContainsImage) || (TextEnabled && !LastMsgContainsImage)) ? '' : ' style="display: none"';
    if (displayStyle === '' && MUTES[nick])
        displayStyle = ' style="display: none"';

    return '<div class="msgDiv dump ' + loadingClass + containsImageClass + " nick_" + nick + '" ' + msgId + displayStyle + '>'
        + '<span class="nick"><b><a href="' + RootDomain + nick + ' ">' + nick + '</a></b>'
        + ' <img src="'+Imgs.chatThumbDot+'" class="chat-thumb" onclick="Tag.favorite(this)"> '
        + '</span>'
        + '<span class="content">' + builtContent + '</span>'
        + '</div>';
}

var MUTES = {};
$(".mute").live("click", function(){
    $(this).removeClass("mute");
    $(this).addClass("unmute");
    $(this).html("o");
    var nick = $(this).parent().children("a").html().replace(/<img[^>]+>/,"");
    $(".nick_" + nick).hide();
    MUTES[nick] = true;
});
$(".unmute").live("click", function(){
    $(this).removeClass("unmute");
    $(this).addClass("mute");
    $(this).html("x");
    var nick = $(this).parent().children("a").html().replace(/<img[^>]+>/,"");
    $(".nick_" + nick).show();
    delete MUTES[nick];
});

function buildUserDiv(user) {
    var muted = MUTES[user.nick] ? '<span class="unmute">o</span>' : '<span class="mute">x</span>';
    if (user.avatar) {
        return '<div class="username">'
            + muted
            + '<a href="' + RootDomain + escapeHtml(user.nick) + '" target="_blank">'
            + '<img src="' + user.avatar + '" width="50" height="50">'
            + escapeHtml(user.nick)
            + '</a>'
            + '</div>';
    } else {
        return '<div class="username">'
            + muted
            + '<a href="' + RootDomain + escapeHtml(user.nick) + '" target="_blank">'
            + '<img src="' + RootDomain + '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="' + RootDomain + 'static/img/thumbs/chatheartover.gif">'
        + '<a href="' + RootDomain + 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,
         "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="' + RootDomain + 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 clearMessages(){
  track('UI', 'ClearScreen');
  $('.dump').remove();
}

function submitMessage() {
    var content = $.trim($('#msgInput').val());
    
    if (content == "/clear") {
        clearMessages()
        $('#msgInput').val('');
        return;
    }
    
    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;
    }
    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.msgid)
            .removeClass('loading').addClass('loaded');
        div.find('.content').html(buildMsgContent(content, json.recips));
    };
    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: true });
    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, 1500);
    };
    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(){
    track('UI', 'SendButtonActuallyClicked');
    submitMessage();
}

function paletteClicked(){
    track('UI', 'FavPaletteActuallyClicked');
    paletteToggle();
}


function initChat() {
  Search.initInpage()

  $('#textbutton input').attr('checked', TextEnabled).change(setTextEnable);
  $('#imgbutton input').attr('checked', ImgsEnabled).change(setImgsEnable);
/*  $('#clearbutton input').click(function() {
    track('UI', 'ClearScreen');
    $('.dump').remove();
    $(this).removeAttr('checked');
    return false;
  });
*/
  
    $('.oldmsg').each(function() {
        var dump = $(this);
        var content = dump.find(".content")
        MessageContentCache[dump.attr("id").substr(8)] = content.text()
        content.html(buildMsgContent(content.text(), Recips));

        if ((ImgsEnabled && dump.hasClass('contains-image')) || (TextEnabled && !dump.hasClass('contains-image')))
            dump.show();            
        else
            dump.hide();
    });

    $('#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(recips) {
    Search.initInpage();
    $(".linkify-text").each(function() {
        var text = jQuery(this).text();
        jQuery(this).html(linkifyWithoutImage(text));
    });

    $(".linkify-full").each(function() {
        $(this).html(buildMsgContent($(this).text(), recips));
    });   

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

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

function initLog(recips) {
    Search.initInpage();
    $('.logged-dump .content').each(function() {
        var t = $(this);
        t.html(buildMsgContent(t.text(), recips));
    });
    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 darn 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();
        $('#dashtotal').css('background-image', 'url(' + r + ')');
    };
    new AjaxUpload(elementId, {
        action: '/upload/avatar',
        autoSubmit: true,
        name: 'image',
        onSubmit: onSubmit,
        onComplete: onComplete
    });
}




// 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.initInpage()
    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");
        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
        });
    },
    "animated_fav": function(button, scoreClass, bigTextSize, smallTextSize) {
        if (!Nick) { return; }
        Tag.favorite(button);
        var $dump = $(button).parents(".dump");
        var isAdding = $dump.hasClass('favorite');

        // Frontpage-favs can be triggered by clicking score number,
        // so manually sync heart-thumb visual state.
        if ($(button).hasClass('hallscore')) {
            $dump.find('.thumb').attr('src', isAdding ? Imgs.logThumb : Imgs.logThumbOff);
        }

        if ($dump.attr('nick') == Nick)
            return;
        
        var $score = $dump.find(scoreClass);
        var inc = isAdding ? 1 : -1;
        var oldScore = parseInt($score.text(), 10);
        $score.text(oldScore + inc);
        $score.stop().animate({ 'font-size': bigTextSize }, 250, function() {
            $score.animate({ 'font-size': smallTextSize }, 250);
        });
        
        if (isAdding) {
            var link = $('<a>')
                .attr('href', Domain + '/' + Nick + '/popular')
                .append($('<b>').text(Nick))
                .append(" ");
            $dump.find('.faver-list').append(link);
        } else {
            $dump.find('.faver-list b').filter(function() { return $(this).text() == Nick }).parent().remove();
        }
    }
}

/*
 timb:
 the ImgCache manages loading images and keeping track of image sizes...
 it can be passed a bunch of urls to load and a callback that gets called when more images are ready
 image loading can also be paused and started again.
 there can be separate ImageCaches, eg, one for search result images, one for chat images (but they all share the actual image cache)
 In theory it should also avoid a few http requests bc we can just dup DOM nodes for images that are already loaded that don't have cache headers (not sure tho, browsers probably pretty aggressive with that already)
*/

var ImgCache = {
  "imgs": {}, // <img> nodes indexed by url
  "caches": {},
  
  "init": function(name){
    // don't clear callback
    var callback = emptyFunc
    if (name in ImgCache.caches)
      callback = ImgCache.caches[name].onImgsLoaded
    delete ImgCache.caches[name]
    
    ImgCache.caches[name] = {
      "loadAtATime": 10,
      "urlsToLoad": [],
      "imgsLoading": {},
      "imgsLoadingCounter" : 0, // a hack so i don't have to iterate over the object to always get its size...
      "imgsLoaded": {},
      "onImgsLoaded": callback,
      "paused": false
    }
  },
  
  "add": function(name, urls){
    if (!(name in ImgCache.caches)) ImgCache.init(name)
    if (!$.isArray(urls)) urls = [urls];
    
    var cache = ImgCache.caches[name]
    
    urls.forEach(function(url){
      cache.urlsToLoad.push(url)
    })
    
  },
  
  "config": function(name, cfg){
    if (!(name in ImgCache.caches)) ImgCache.init(name)
    var cache = ImgCache.caches[name]
    for(var key in cfg)
      cache[key] = cfg[key]
  },
  
  "clear": function(name){ ImgCache.init(name) },
  "pause": function(name){ ImgCache.caches[name].paused = true },
  "unpause": function(name){ ImgCache.caches[name].paused = false },
  
  "loadImages": function(cache){
    if (cache.paused) return;
    
    while(cache.urlsToLoad.length && cache.imgsLoadingCounter < cache.loadAtATime) {
      var url = cache.urlsToLoad.shift()
      if (url in ImgCache.imgs) { // already loading this image
        var img = ImgCache.imgs[url]
        if (img.complete) {
          cache.imgsLoaded[url] = ImgCache.imgs[url]
        } else if (!(url in cache.imgsLoading)) {
          cache.imgsLoading[url] = ImgCache.imgs[url]
          cache.imgsLoadingCounter += 1
        }
      } else {
        var img = new Image()
        img.src = url
        img.animated = (parseUri(url)["file"].toLowerCase().substr(-3) == "gif") ? true : false;
        ImgCache.imgs[url] = img
        cache.imgsLoading[url] = img
        cache.imgsLoadingCounter += 1
      }
    }
  },
  "processLoadingImages": function(cache){
    for (var url in cache.imgsLoading) {
      var img = cache.imgsLoading[url]
      if (img.complete) {
        cache.imgsLoaded[url] = img
        delete cache.imgsLoading[url]
        cache.imgsLoadingCounter -= 1
      }
    }
  },
  
  "loader": function(){
    for (name in ImgCache.caches){
      var cache = ImgCache.caches[name]
      ImgCache.processLoadingImages(cache) // move images from imgsLoading into imgsLoaded
      ImgCache.loadImages(cache) // put new images in imgsLoading/imgsLoaded from urlsToLoad
      for (var url in cache.imgsLoaded) {
        cache.onImgsLoaded(cache.imgsLoaded) // only call if new images actually loaded
        delete cache.imgsLoaded
        cache.imgsLoaded = {}
        break;
      }
      
    }
    setTimeout(ImgCache.loader, 500)
  }
}

ImgCache.loader()


var Search = {
  
  'term': "",
  'images': [],
  'tokens': [],
  'closed': true,
  
  'initFullpage': function(){
    Search.type = "fullpage"
    Search.init()
    Search.initSpaceFill = Search.initSpaceFillFullpage
  },
  
  'initInpage': function(){
    Search.type = "inpage"
    Search.init()
    Search.initSpaceFill = Search.initSpaceFillInpage
  },
  
  'init': function(){
    ImgCache.config("search", {"onImgsLoaded": Search.imgsLoaded})

    $('#search-results-images a').live('hover', Search.resultsHover)

    var input = Search.$input = $("#search-query")
    var label = "search dump.fm"
    Search.$container = $("#search-results-images")
    
    input.val(label)
    input.focus(function(){
      if (input.val() == label) input.val("")
    })
    input.blur(function(){
      if (input.val().trim() == '') input.val(label)
    })
    input.keydown(ifEnter(Search.doSearch))
    $("#search-results-images a").live("mouseup", Search.click)
  },
  
  'initSpaceFillFullpage': function() {
      SpaceFill.init({
        "container": "#search-results-images",
        "width": $(document).width(),
        "height": $(document).height(),
        "type": "columns",
        "spacing": "justify",
        "minMargin": 16,
        "columnWidth": 250
        })
    },

  'initSpaceFillInpage': function() {
      SpaceFill.init({
        "container": "#search-results-images",
        "width": $(document).width() * 0.93,
        "height": $(document).height(),
        "type": "columns",
        "spacing": "justify",
        "minMargin": 8,
        "columnWidth": 120
        })
    },
  
  "resultsHover": function(e){
    if (e.type == 'mouseover') {
      var img = ImgCache.imgs[this.href]
      if (img.animated) {
        img.width = img.adjWidth
        img.height = img.adjHeight
        $(this).addClass("animating")
        $(this).append(img)
      }
    } else {
      var img = ImgCache.imgs[this.href]
      if (img.animated) {
        $(this).removeClass("animating")
        this.removeChild(img)
      }
    }
  },
  
  "imgsLoaded": function(imgs){
    //if (ColumnFill.isSpaceFilled()) return;
    
    if (Search.closed) return;
    
    if (Search.$container[0].style.display != "block") {
      Search.$container.css("display", "block")
      $("#userList").css("display", "none")
      Search.setMessage("results for '"+Search.tokens.join(" and ")+"'")
    }
    
    for (var url in imgs){
      var img = imgs[url]
      if (isImgBroken(img)) continue;
      var width = img.width
      var height = img.height
    
      var maxWidth = SpaceFill.config.columnWidth
      var maxHeight = Math.floor(SpaceFill.config.columnWidth * 1.2)

      if (width > maxWidth) {
        scaleFactor = maxWidth / width
        width = maxWidth
        height = Math.floor(height * scaleFactor)
      } else if (height > maxHeight) {
        scaleFactor = maxHeight / height
        height = maxHeight
        width = Math.floor(width * scaleFactor)
      }
      img.adjWidth = width
      img.adjHeight = height
    
      var c = document.createElement("canvas")
      c.width = width
      c.height = height
      var ctx = c.getContext('2d');
      ctx.drawImage(img, 0, 0, c.width, c.height)

      var a = document.createElement("a")
      a.onclick = falseFunc
      a.href = img.src
      a.style.width = width + "px"
      a.style.height = height + "px"

      a.appendChild(c)
    
      SpaceFill.add(a)
    }    
  },
  
  'setContent': function(x){
    $("#search-results-images").html(x)
  },
  
  'setMessage': function(x){
    $("#search-controls").css("display", "block")
    $("#search-message").html(x)
  },
  
  'searchError': function(error){
    Search.setContent("")
    Search.setMessage(error)
  },

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

  'doAjax': function(term) {
    if (Domain == "http://dump.fm") {
      $.ajax({
        "dataType": "json",
        "url": "/cmd/search/" + term,
        "success": Search.results,
        "error": Search.error,
        "timeout": 20000,
      })
    } else { // search main site via jsonp
      $("#search-script").remove()
      $("head").append("<s"+"cript src='http://dump.fm/cmd/search/"+term+"?callback=Search.results' id='search-script'></s"+"cript>")
    }
  },

  '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
  },

  'results': function(results){
    Search.resultsClear()
    if(results === null || results.length == 0) {
        Search.setMessage("no results found")        
    } else {
      Search.initSpaceFill()
      var urls = []
      results.forEach(function(r){ 
        var url = r.url
        if (url.charAt(0) == '/')
          url = 'http://dump.fm/images' + url
        else
          url = 'http://' + url
        urls.push(url) 
      })
      ImgCache.add("search", urls)
    }
  },
  
  'resultsClear': function(){
    $("#search-results-images").html("")
    //ImgCache.pause("search")
    ImgCache.clear("search")
  },
  
  'close': function(){
    Search.resultsClear()
    Search.closed = true
    Search.$container.css("display", "none")
    $("#search-controls").css("display", "none")
    $("#userList").css("display", "block")
  }
  
}

var ColumnFill = {
  "init": function(){
    var cfg = SpaceFill.config
    var numColumns = ColumnFill.calcColumns()
    ColumnFill.columns = []
    for (var i = 0; i < numColumns; i++) {
        ColumnFill.columns.push({"height": 0})
    }

    cfg.marginWidth = cfg.marginHeight = cfg.minMargin

    if (cfg.spacing == "center") {
      cfg.columnSpacingAmt = (cfg.width - (numColumns * (cfg.columnWidth + cfg.marginWidth) + cfg.marginWidth)) / 2
    } else if (cfg.spacing == "justify") {
      cfg.marginWidth = (cfg.width - (numColumns * cfg.columnWidth)) / (numColumns + 1)
    }


  },
  "add": function(obj){
    var cfg = SpaceFill.config
    var colIndex = ColumnFill.shortestColumn()
    var col = ColumnFill.columns[colIndex]

    if (cfg.spacing == "center") {
      var colLeft = colIndex * (cfg.marginWidth + cfg.columnWidth) + cfg.columnSpacingAmt
      var imgLeft = Math.floor((cfg.marginWidth / 2) + (cfg.columnWidth / 2) - (parseInt(obj.style.width) / 2)) + colLeft + "px"
    } else if (cfg.spacing == "justify") {
      var colLeft = (colIndex * (cfg.marginWidth + cfg.columnWidth))
      var imgLeft = Math.floor((cfg.marginWidth / 2) + (cfg.columnWidth / 2) - (parseInt(obj.style.width) / 2)) + colLeft + "px"
    }

    obj.style.position = 'absolute'
    obj.style.top = col.height + cfg.marginHeight + "px"
    obj.style.left = imgLeft

    col.height += cfg.marginHeight + parseInt(obj.style.height)

    $(cfg.container).append(obj)

  },
  "calcColumns": function(){
    var cfg = SpaceFill.config
    var numColumns = 0
    var width = cfg.width - cfg.minMargin
    var columnSub = cfg.columnWidth + cfg.minMargin
    while (width > columnSub) {
      width -= columnSub
      numColumns++
    }
    return numColumns
  },
  
  "shortestColumn": function(){
    var min = Infinity
    var mindex = 0
    for(var i = 0; i< ColumnFill.columns.length; i++){
      var col = ColumnFill.columns[i]
      if ( min > col.height) {
        min = col.height
        mindex = i
      }
    }
    return mindex
  },
  
  "isSpaceFilled": function(){
    var config = SpaceFill.config
    var colIndex = ColumnFill.shortestColumn()
    var col = ColumnFill.columns[colIndex]

    if (col.height > 4 * config.height) return true;
    else return false;
  }

}

var SpaceFill = {
  "init": function(config){
    config.type = "columns"
    SpaceFill.config = config

    SpaceFill.types[config.type].init()
    SpaceFill.add = SpaceFill.types[config.type].add
  },
  "types": { "columns": ColumnFill }
}

// 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
// timb: todo: this can't deal with @s in urls correctly. ex: http://www.classicbattletech.com/images/gallery/Combat_Operations_Cover@1280x960.jpg
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

// this doesn't properly deal with eg, .gov.uk .co.ck etc
function parseDomain(host){
  var chunks = host.split(".")
  if (chunks.length == 1) return chunks[0]
  else return chunks[chunks.length - 2]
}

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

/* SHA1.js (timb: compressed this)
 * Version 2.2 Copyright Paul Johnston 2000 - 2009.
 * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
 * Distributed under the BSD License
 * from http://pajhome.org.uk/crypt/md5/sha1.html
 */
var SHA1 = {
"hexcase": 0,
"b64pad": "",
"hex": function(s)    { return SHA1.rstr2hex(SHA1.rstr(SHA1.str2rstr_utf8(s))); },
"b64": function(s)    { return SHA1.rstr2b64(SHA1.rstr(SHA1.str2rstr_utf8(s))); },
"any": function(s, e) { return SHA1.rstr2any(SHA1.rstr(SHA1.str2rstr_utf8(s)), e); },
"hex_hmac": function(k, d){ return SHA1.rstr2hex(SHA1.rstr_hmac(SHA1.str2rstr_utf8(k), SHA1.str2rstr_utf8(d))); },
"b64_hmac": function(k, d){ return SHA1.rstr2b64(SHA1.rstr_hmac(SHA1.str2rstr_utf8(k), SHA1.str2rstr_utf8(d))); },
"any_hmac": function(k, d, e){ return SHA1.rstr2any(SHA1.rstr_hmac(SHA1.str2rstr_utf8(k), SHA1.str2rstr_utf8(d)), e); },
"rstr": function(s) { return SHA1.binb2rstr(SHA1.binb(SHA1.rstr2binb(s), s.length * 8)); },
"rstr_hmac": function(key, data){
  var bkey = SHA1.rstr2binb(key);
  if(bkey.length > 16) bkey = SHA1.binb(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 = SHA1.binb(ipad.concat(SHA1.rstr2binb(data)), 512 + data.length * 8);
  return SHA1.binb2rstr(SHA1.binb(opad.concat(hash), 512 + 160));
},
"rstr2hex": function(input){
  try { SHA1.hexcase } catch(e) { SHA1.hexcase=0; }
  var hex_tab = SHA1.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;
},
"rstr2b64": function(input){
  try { SHA1.b64pad } catch(e) { SHA1.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 += SHA1.b64pad;
      else output += tab.charAt((triplet >>> 6*(3-j)) & 0x3F);
    }
  }
  return output;
},
"rstr2any": function(input, encoding){
  var divisor = encoding.length;
  var remainders = Array();
  var i, q, x, quotient;
  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);
  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;
  }
  var output = "";
  for(i = remainders.length - 1; i >= 0; i--)
    output += encoding.charAt(remainders[i]);
  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;
},
"str2rstr_utf8": function(input){
  var output = "";
  var i = -1;
  var x, y;
  while(++i < input.length){
    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++;
    }
    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;
},
"str2rstr_utf16le": function(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;
},
"str2rstr_utf16be": function(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;
},
"rstr2binb": function(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;
},
"binb2rstr": function(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;
},
"binb": function(x, len){
  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] = SHA1.bit_rol(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1);
      var t = SHA1.safe_add(SHA1.safe_add(SHA1.bit_rol(a, 5), SHA1.ft(j, b, c, d)),
                       SHA1.safe_add(SHA1.safe_add(e, w[j]), SHA1.kt(j)));
      e = d;
      d = c;
      c = SHA1.bit_rol(b, 30);
      b = a;
      a = t;
    }
    a = SHA1.safe_add(a, olda);
    b = SHA1.safe_add(b, oldb);
    c = SHA1.safe_add(c, oldc);
    d = SHA1.safe_add(d, oldd);
    e = SHA1.safe_add(e, olde);
  }
  return Array(a, b, c, d, e);
},
"ft": function(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;
},
"kt": function(t){
  return (t < 20) ?  1518500249 : (t < 40) ?  1859775393 :
    (t < 60) ? -1894007588 : -899497514;
},
"safe_add": function(x, y){
 var lsw = (x & 0xFFFF) + (y & 0xFFFF);
 var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
 return (msw << 16) | (lsw & 0xFFFF);
},
"bit_rol": function(num, cnt) { return (num << cnt) | (num >>> (32 - cnt)) }
}

