// Chat client code
// Some of this code is modified with thanks from code provided by Anant Garg
// Which is Copyright (c) 2009 Anant Garg (anantgarg.com | inscripts.com)
// Used under single domain licence for torn.com

// Other parts of this code are based on code provided by Friendfeed
// Which is licences under the Apache 2.0 licence

// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
//	   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.

// Finally, much of this code relies on jQuery, which is GPL (and ace).
// Donation has been made to jQuery.

var chat = {};

jQuery = jQuery.noConflict();

function startChat() {
    chat.start();
}

// some helper functions

// setup an ajax channel in a double-inner iframe, calls callback
// with a channel function that can be used to do an Ajax request on a
// subdomain
function setupChannel(callback) {
    // create trampoline that sets it document.domain to be
    // able to talk to the actual channel iframe
    var frameName = 'f' + (Math.random() + "").substring(2, 8);
    jQuery('<iframe src="/js/channel/trampoline.html" name="' + frameName + '" style="position:absolute;left:-10000px"></iframe>')
        .load(function () { window.frames[frameName].setup(callback); })
        .appendTo(document.body);

    return frameName;
}

// make Ajax request on channel with options, transfering useful cookies
function channeledAjax(channel, options) {
    var cookies = {};
    cookies['PHPSESSID'] = jQuery.cookie('PHPSESSID');
    cookies['secret'] = jQuery.cookie('secret');

    var abort = channel(options, cookies);
    
    // pack abort in nicely, we can't return the real xhr object
    // since it's in the (inaccessible) iframe
    return { abort: abort };
}

(function () {
    // if we got a parent with the real chat in there, just override
    // our local copy with that
    if (top.chat && window != top) {
        console.log("Warning: chat.js included twice (in top and in iframe)");
        chat = top.chat;
        return;
    }
        
    // A server is a long-polling host we can connect to in order to
    // get room messages, etc. We keep track of them so they're easier
    // to manage.
    var servers = {};

    // Each room is represented by some Javascript state and a DOM
    // element.
    var rooms = {};

    // setup the chat API
    chat.start = startChat;
    chat.stop = stopChat;
    chat.addRoom = addChatRoom;
    chat.setRoomStatus = setChatRoomStatus;
    chat.restartPollers = restartPollers;
    chat.rooms = rooms;
    chat.servers = servers;
    chat.users = {}; // user id -> our local idea of online status
    chat.ownUserId = null; // global variable set by PHP script that includes us
    chat.USER_STATUS_UNKNOWN = -1;
    chat.USER_STATUS_OFFLINE = 0;
    chat.USER_STATUS_ONLINE = 1;
    chat.running = false;
    chat.stopWithButton = function () {
        chat.stop();
        setupReenableChatButton();
    };
    chat.setupRoom = function (userids) {
        var data = { users: jQuery.toJSON(jQuery.makeArray(userids)) };
        try {
            channeledAjax(servers[statusServerId].channel, {
                type: "POST",
                url: "/torncity/chat/status/setuproom",
                data: data,
                timeout: 10 * 1000
            });
        }
        catch (err) {
        }
    }
    chat.scheduleUserStatusPoll = function (millisecs) {
        if (chat.running) {
            whenChannelUp(statusServerId, function () {
                schedulePoll(statusServerId, millisecs);
            });
        }
    }; 
    // these callbacks are one-shot, receiver must reconnect upon
    // being called (helps with garbage collection)
    chat.listenForRoomChanges = function (key, cb) {
        callbacks.rooms[key] = { callback: cb };
    };
    chat.listenForUserChanges = function (key, users, cb) {
        callbacks.users[key] = {
            users: users,
            callback: cb
        };
    };
    chat.removeCallback = function (type, key) {
        delete callbacks[type][key];
    };
    chat.userActivityOccurred = function () {
        lastUserActivity = new Date();
    }
    
    // and the implementation
    
    var maxMessagesPerRoom = 40;
    var maxInactivityTime = 30 * 60;
    var longPollTimeout = 5 * 60;
    var monitorInterval = null;
    var monitorIntervalTime = 6 * 60;
    var maxMessagesToStoreLocally = 10;
    var lastUserActivity = new Date();
    var statusServerId = "status";
    var holdOffChatRoomRestructuring = 0; // semaphore, > 0 means true
    var restoringStateFromLocalStorage = 0; // semaphore, > 0 means true
    var callbacks = {
        rooms: {},
        users: {}
    }

    var statusMap = { 0: 'open', 1: 'minimized', 2: 'closed' };

    function callCallbacks(type, filter) {
        var tmp = callbacks[type];
        jQuery.each(tmp, function (k, o) {
            if (filter && !filter(o))
                return;

            // clear it out before we call callback
            delete callbacks[type][k];
            
            o.callback();
        });
    }
    
    // a couple of helpers for querying the servers/rooms dicts

    function getRoomsWithStatus(status) {
        var res = [];
        for (var id in rooms) {
            if (rooms[id].status == status)
                res.push(rooms[id]);
        }
        return res;
    }

    function getRoomsForServer(serverId) {
        var res = [];
        for (var id in rooms) {
            if (rooms[id].server == serverId)
                res.push(rooms[id]);
        }
        return res;
    }

    function getUserIdsInRoom(roomId) {
        var res = [];
        for (var uid in rooms[roomId].participants)
            res.push(uid);
        return res;
    }

    function getUserNamesInRoom(roomId, userIds) {
        var res = [];
        for (var uid in rooms[roomId].participants)
            res.push(rooms[roomId].participants[uid]);
        return res;
    }
    
    // chatbox DOM manipulation

    // take list of names, return "name1, name2 and 5 more"
    function abbreviatedList(users, maxNames) {
        var tmp = users.slice(0, maxNames);

        var missing = users.length - tmp.length;
        if (missing > 0)
            tmp.push(missing + " more");

        var res = "";
        for (var i = 0; i < tmp.length; ++i) {
            if (i > 0) {
                if (i == tmp.length - 1)
                    res += " and ";
                else
                    res += ", ";
            }
            
            res += tmp[i];
        }
        return res;
    }
    
    function restructureChatBoxes() {
        console.log ("Restructuring chat boxes");
        
        // counters for right margins
        var open = getRoomsWithStatus("open");
        var minimized = getRoomsWithStatus("minimized");
        
        var openCount = open.length - 1;
        var minCount = minimized.length - 1;

        var adhocRooms = [], otherRooms = [];
        jQuery.each(rooms, function (i, room) {
            if (room.participants)
                adhocRooms.push(room);
            else
                otherRooms.push(room);
        });

        function sortByName(a, b) {
            if (a.name < b.name)
                return -1;
            else if (a.name > b.name)
                return 1;
            return 0;
        }
        
        adhocRooms.sort(sortByName);
        otherRooms.sort(sortByName);

        jQuery.each(otherRooms.concat(adhocRooms), function(i, room) {
            var offset, bottom, showTime = 0; 

            if (room.status == "open") {
                offset = 7;
                if (openCount > 0) {
                    offset += openCount*(225+7);
                    openCount--;
                }

                bottom = 0;
                if (minimized.length > 0)
                    bottom = 38;

                room.chatbox
                    .css('right', offset)
                    .css('bottom', bottom)
                    .css('width','225px');

                room.chatbox.find('.chatboxcontent')
                    .css('display','block')
                    .css('width','209px');

                room.chatbox.find('.chatboxinput').css('display', 'block');
                room.chatbox.find('.chatboxhead').css('width', '209px');
                room.chatbox.find('.chatboxoptions').show();
                room.chatbox.find('.chatboxtitle').css('width', '189px');
                
                if (jQuery.browser.msie) {
                    // compensate for different box model in IE quirks mode
                    room.chatbox.find('.chatboxhead').css('width', '225px');
                    room.chatbox.find('.chatboxcontent').css('width','225px');
                }
                if (room.chatbox.is(':hidden'))
                    room.chatbox.fadeIn(showTime);
                
                // scroll after (possibly) showing to ensure we have a scroll height
                room.chatbox.find('.chatboxcontent').scrollTop(room.chatbox.find('.chatboxcontent').get(0).scrollHeight);
            }
            else if (room.status == "minimized") {
                offset = 7;
                if (minCount > 0) {
                    offset += minCount*(116+7);
                    minCount--;
                }
                
                room.chatbox
                    .css('width','116px')
                    .css('right', offset)
                    .css('bottom', '0px');
                
                room.chatbox.find('.chatboxcontent').hide();
                room.chatbox.find('.chatboxinput').hide();
                
                room.chatbox.find('.chatboxhead').css('width', '100px');
                room.chatbox.find('.chatboxoptions').hide();
                room.chatbox.find('.chatboxtitle').css('width', '100px');
                
                if (jQuery.browser.msie) {
                    // compensate for different box model in IE quirks mode
                    room.chatbox.find('.chatboxhead').css('width', '116px');
                }

                if (room.chatbox.is(':hidden'))
                    room.chatbox.fadeIn(showTime);
            }
            else { // closed
                room.chatbox.fadeOut(200, function () { jQuery(this).hide(); });
            }
        });
    }

    function createChatBox(roomId) {
        console.log("Creating chat box for", roomId)
        
        // create DOM elements
        var chatbox = jQuery("<div />" ).attr("id","chatbox_"+roomId)
            .addClass("chatbox")
            .css('display', 'none') // start off hidden, restructuring will show it
            .html('<div class="chatbox_menu" id="mb_'+roomId+'" align="left"><a href="#">close&nbsp;'+roomId+'</a></div><div id="bh_'+roomId+'" class="chatboxhead"><div class="chatboxtitle" title="'+rooms[roomId].name+'">'+rooms[roomId].name+'</div><div class="chatboxoptions"><a class="close" href="">X</a></div><div clear="all"></div></div><div class="chatboxcontent"></div><div class="chatboxinput"><div class="chatboxnotice"></div><textarea class="chatboxtextarea"></textarea></div>')
            .appendTo(jQuery("body"));

        rooms[roomId].chatbox = chatbox;

        // remove menu
        jQuery('.chatbox_menu').css('display', 'none');

        // setup event handling

        // clicking anywhere within takes us to textarea
        chatbox.click(function() {
            if (rooms[roomId].status == "open") {
                chatbox.find(".chatboxtextarea").focus();
            }
        });

        // minimize/maximize
        chatbox.find(".chatboxhead").click(function() {
            var room = rooms[roomId];
            if (!room)
                return;

            var newStatus = room.status == "minimized" ? "open" : "minimized";
            if (newStatus == "open") {    
                room.chatbox.find(".chatboxhead").css("background-color","#999999");
                //jQuery(".chatboxtextarea").removeClass("chatboxtextareaselected");
                room.chatbox.find(".chatboxtextarea").focus();
                // scroll to bottom
                room.chatbox.find(".chatboxcontent").scrollTop(room.chatbox.find(".chatboxcontent").get(0).scrollHeight);
            }

            setChatRoomStatus(roomId, newStatus);
            // no need to update server-side when we change minimized/open
            // FIMXE: setChatRoomStatus(roomId, newStatus, true);
        });

        if (!rooms[roomId].participants)
            chatbox.find(".chatboxhead .close").hide();
        
        // close
        chatbox.find(".chatboxhead .close").click(function (e) {
            e.preventDefault();
            e.stopPropagation();
            
            setChatRoomStatus(roomId, "closed");
        });
        
        // textarea events
        chatbox.find(".chatboxtextarea").blur(function(){
            rooms[roomId].focused = false;
            chatbox.find(".chatboxtextarea").removeClass('chatboxtextareaselected');
            chatbox.find(".chatboxhead").css('background-color', '#999999');
        }).focus(function() {
            rooms[roomId].focused = true;
            chatbox.find('.chatboxhead').removeClass('chatboxblink');
            chatbox.find(".chatboxtextarea").addClass('chatboxtextareaselected');
            chatbox.find(".chatboxhead").css('background-color', '#999999');
        }).keypress(function() {
            var t = "";
            if (rooms[roomId].initializedParticipantStatus && rooms[roomId].participants) {
                var foundOnline = false;
                for (var uid in rooms[roomId].participantStatus)
                    if (rooms[roomId].participantStatus[uid] == chat.USER_STATUS_ONLINE) {
                        foundOnline = true;
                        break;
                    }

                if (!foundOnline)
                    t = "Warning: nobody else online in room";
            }
            chatbox.find('.chatboxnotice').text(t);
            
            chatbox.find(".chatboxhead").css('background-color', '#999999');
        }).keydown(function (event) {
            if (event.which == 13 && event.shiftKey == 0) {
                event.preventDefault();

                var textarea = jQuery(this);
                
                var message = textarea.val();
                message = message.replace(/^\s+|\s+$/g,"");

                textarea
                    .val("")
                    .css('height','44px')
                    .css('overflow','hidden');

                if (message != '')
                    newMessage(message, roomId);
            }
        }).keyup(function (event) {
            possiblyEnlargeChatBoxTextArea(jQuery(this));
        });
        
        jQuery(".chatboxtextarea").removeClass('chatboxtextareaselected');
    }

    function possiblyEnlargeChatBoxTextArea(textarea) {
        var maxHeight = 94;

        var h = textarea.get(0).clientHeight,
            scrollHeight = textarea.get(0).scrollHeight;

        if (scrollHeight && scrollHeight > h && h < maxHeight) {
            h = scrollHeight;
            if (h > maxHeight) {
                textarea.css('overflow','auto');
                h = maxHeight;
            }

            // need some extra to account for border and padding
            h += 10;
            
            textarea.height(h);
        }
    }

    function addHtmlToChatBox(roomId, html) {
        var c = rooms[roomId].chatbox.find(".chatboxcontent");

        var scrolledToBottom = c.get(0).clientHeight + c.scrollTop() >= c.get(0).scrollHeight - 5;
        
        var node = jQuery(html).appendTo(c);

        // make sure we haven't got too many messages
        c.children().slice(0, -maxMessagesPerRoom).remove();

        if (scrolledToBottom) {
            //var d = c.get(0).scrollHeight - c.get(0).clientHeight + c.scrollTop();
            c.scrollTop(c.get(0).scrollHeight);
        }
        
        return node;
    }

    function setErrorForServer(serverId, text) {
        var disablement = text ? "disabled" : "";
        jQuery.each(getRoomsForServer(serverId), function (i, r) {
            r.chatbox.find('.chatboxnotice').text(text);
            r.chatbox.find('.chatboxtextarea').attr('disabled', disablement);
        });
    }

    // attract attention to box
    function newsInChatBox(roomId) {
        if (roomId != 'Trade' && roomId != 'Global' && roomId != 'New_Players'
            && rooms[roomId].status != "closed") {
            rooms[roomId].chatbox.find(".chatboxhead").css('background-color', '#0033CC');
        }
    }

    function showMessageInChatBox(roomId, message, alreadyUpdated) {
        var room = rooms[roomId];
        var chatboxcontent = room.chatbox.find('.chatboxcontent');
        
        var existing = chatboxcontent.find("#m" + message.id);
        if (existing.length > 0)
            return;

        chatboxcontent.find('.timestamp').remove();

        var html = message.html;
        if (message.date)
            html += "<div class=\"timestamp\" style=\"color:#666666; display:block; margin:0\"><i>Last message:</i> " + message.date + " - TCT</div>";
        
        var node = addHtmlToChatBox(roomId, html);

        // add timestamp of the last message
        if (message.date)
            node.eq(0).attr('title', message.date);

        if (message.fromid != chat.ownUserId && !alreadyUpdated)
            newsInChatBox(roomId);
    }

    
    // server communication

    function createServer(serverId, callback) {
        console.log("Creating server channel", serverId);
        servers[serverId] = {};
        servers[serverId].failHappened = null; // when did last error occur
        servers[serverId].scheduleTimeout = null; // timer for scheduling new poll
        servers[serverId].lastActivity = new Date();
        servers[serverId].poll = null; // the Ajax poll, used for abortion
        servers[serverId].channel = null; // iframe Ajax channel
        servers[serverId].waitingForChannelUp = []; // list of callbacks
        if (callback)
            servers[serverId].waitingForChannelUp.push(callback);
        servers[serverId].frameName = setupChannel(function (channel) {
            if (!servers[serverId])
                return;
            
            servers[serverId].channel = channel;
	    console.log("Created channel to server", serverId);

            jQuery.each(servers[serverId].waitingForChannelUp, function (i, cb) {
                cb(serverId);
            });
            
            servers[serverId].waitingForChannelUp = null;
        });
    }

    // call callback when we're connected, this might be immediately,
    // or after some time if we need to wait for the server being created
    function whenChannelUp(serverId, callback) {
        if (servers[serverId]) {
            if (servers[serverId].channel != null)
                callback(serverId);
            else
                servers[serverId].waitingForChannelUp.push(callback);
        }
        else
            createServer(serverId, callback);
    }

    function updateServerSideRoomStatus(room) {
        var data = {
            "room": room.id,
            "_xsrf": getCookie("_xsrf")
        };

        if (room.status == "open")
            data["status"] = 0;
        else if (room.status == "minimized")
            data["status"] = 1;
        else // closed
            data["status"] = 2;
        
        try {
            channeledAjax(servers[statusServerId].channel, {
                type: "POST",
                url: "/torncity/chat/status/updateroom",
                data: data,
                timeout: 10 * 1000
            });
        }
        catch (err) {
        }
    }
    
    function pollServer(serverId) {
        if (!servers[serverId] || !servers[serverId].channel)
            return;
        
        if (servers[serverId].poll) {
             // abort() doesn't trigger error handler so this doesn't
             // get us into trouble
            servers[serverId].poll.abort();
            servers[serverId].poll = null;
        }

        // if we just had a real error, we lower the timeout so we can
        // detect faster whether everything is OK now (if we get an
        // error, no harm is done, the timeout didn't matter; if we
        // get a timeout, then we assume everything is fine and go
        // back to the usual long timeout)
        var timeout = servers[serverId].failHappened != null ? 30 : longPollTimeout;
        // randomize to avoid lemming effect
        timeout *= 0.7 + 0.6 * Math.random();

        var data = {}, url = null;
        
        if (serverId == statusServerId) {
            url = "/torncity/chat/status/updates";

            // rooms
            var roomStatus = {};
            jQuery.each(rooms, function (id, r) {
                if (r.status == "open")
                    roomStatus[id] = 0;
                else if (r.status == "minimized")
                    roomStatus[id] = 1;
                else 
                    roomStatus[id] = 2;
            });
            
            data["rooms"] = jQuery.toJSON(roomStatus);

            // online users
            var userStatus = {};
            jQuery.each(callbacks.users, function(i, o) {
                for (var i = 0; i < o.users.length; ++i) {
                    var u = o.users[i], s = chat.users[u];
                    if (s == null)
                        s = chat.USER_STATUS_UNKNOWN;
                    userStatus[u] = s;
                }
            });
            data["users"] = jQuery.toJSON(userStatus);
        }
        else {
            url = "/torncity/chat/message/" + serverId + "/updates";
            
            var rs = getRoomsForServer(serverId);
            // we only want active rooms
            rs = jQuery.grep(rs, function (r) {
                // special-case global rooms
                if ((r.id == "Global" || r.id == "Trade" || r.id == "New_Players")
                    && r.status == "minimized")
                    return false;
                return r.status != "closed";
            });
            if (rs.length == 0)
                return;

            var cursors = {};
            jQuery.each(rs, function (i, r) {
                cursors[r.id] = r.cursor;
            });

            data['_xsrf'] = getCookie("_xsrf");
            data['cursors'] = jQuery.toJSON(cursors);
        }

        servers[serverId].poll = channeledAjax(servers[serverId].channel, {
            url: url,
            type: "POST",
            cache: false,
            dataType: "json",
            data: data,
            success: function (res) { onPollSuccess(serverId, res); },
            error: function (xml, reason, err) { onPollError(serverId, xml, reason, err); },
            timeout: timeout * 1000
        });
    }

    function gotMessages(data) {
        var scheduled = {};
        var roomsToSave = {};
        
        var msgs = data.messages, updated = data.updated_rooms;
        for (var i = 0; i < msgs.length; i++) {
            var roomId = msgs[i].room, m = msgs[i];

            if (!rooms[roomId])
                continue;

            rooms[roomId].cursor = m.id;

            if (!rooms[roomId].initializedParticipantStatus && !scheduled[roomId]) {
                // got the messages for room, so we can now fetch the
                // online statuses of the users
                addUserStatusListener(roomId);
                chat.scheduleUserStatusPoll(200);
                scheduled[roomId] = true;
            }

            rooms[roomId].lastMessages.push(m);
            while (rooms[roomId].lastMessages.length > maxMessagesToStoreLocally)
                rooms[roomId].lastMessages.shift();
            roomsToSave[roomId] = true;
            
            showMessageInChatBox(m.room, m, jQuery.inArray(m.room, updated) != -1);
        }

        if (!restoringStateFromLocalStorage)
            for (var roomId in roomsToSave)
                jQuery.jStorage.set('roommessages' + roomId, rooms[roomId].lastMessages);
    }

    function gotStatus(data) {
        if (data.rooms != null) {
            if (!restoringStateFromLocalStorage)
                jQuery.jStorage.set('statusrooms', data.rooms);

            ++holdOffChatRoomRestructuring;
            
            jQuery.each(data.rooms, function(i, room) {
                var id = room.id, status = statusMap[+room.status];

                // we always start up new rooms as minimized
                // regardless of what server thinks
                /*FIXMEif (status == "open")
                    status = "minimized";*/
                
                if (!rooms[id]) {
                    // maybe we got a new room
                    addChatRoom(id, room.server, room.name, status, room.participants);
                    return;
                }

                var normalizedStatus;
                /*FIXMEif (rooms[id].status == "open")
                    normalizedStatus = "minimized";
                else*/
                    normalizedStatus = rooms[id].status;
                
                if (status != normalizedStatus) {
                    // or new status
                    setChatRoomStatus(id, status, true);
                }
                    
                if (room.server != rooms[id].server) {
                    // or new server
                    rooms[id].server = room.server;
                    whenChannelUp(server, function () {
                        schedulePoll(rooms[id].server, 200 + Math.random() * 200);
                    });
                }
            });

            // check that we didn't delete some rooms
            jQuery.each(rooms, function (i, room) {
                var found = false;
                for (var j = 0; j < data.rooms.length; ++j) {
                    if (data.rooms[j].id == room.id) {
                        found = true;
                        break;
                    }
                }

                if (!found) {
                    removeChatRoom(room.id);
                    jQuery.jStorage.deleteKey('roommessages' + room.id);
                }
            });
            
            --holdOffChatRoomRestructuring;
            restructureChatBoxes();
            
            callCallbacks("rooms");

            // fix wierd problem in IE6 where the chat box doesn't
            // show up immediately
            jQuery("body").focus();
        }

        if (data.users != null) {
            if (!restoringStateFromLocalStorage)
                jQuery.jStorage.set('statususers', data.users);
            for (var u in data.users)
                chat.users[u] = data.users[u];

            // call callbacks of subscribers to the users we've got
            // news about
            callCallbacks("users", function (o) {
                for (var u in data.users)
                    if (jQuery.inArray(u, o.users) != -1)
                        return true;
                
                return false;
            });
        }
    }
    
    function onPollSuccess(serverId, data) {
        if (!servers[serverId])
            return;

        servers[serverId].lastActivity = new Date();
        setErrorForServer(serverId, "");
        servers[serverId].failHappened = null;
        servers[serverId].poll = null;

        console.log ("Poll request returns success, server", serverId);

        try {
            if (serverId == statusServerId)
                gotStatus(data);
            else
                gotMessages(data);
        }
        catch (err) {
            console.log ("Error when receiving new messages or status", err);
            onPollError(serverId, null, "error", err);
            throw err;
            return;
        }

        var baseTimeout = 1500;
        if (serverId == statusServerId) {
            // we are more gentle with the status server, when it
            // returns a new room, it can take some time before we
            // have it setup, thus with a short timeout, we can bug it
            // a couple of times before the state is up to date;
            // besides status updates are less time critical than new
            // messages
            baseTimeout = 2500;
        }
        
        schedulePoll(serverId, baseTimeout * (0.75 + Math.random() * 0.5));
    }

    function getRescheduleTime(failHappened) {
        var o = {};
        
        o.secondsSince = ((new Date()).getTime() - failHappened.getTime()) / 1000;
        o.nextTry = o.secondsSince;
        o.nextTry = Math.max(4, Math.min(4 * 60, o.nextTry)); // clamp
        // randomize a bit to avoid lemming effect
        o.nextTry *= 0.7 + Math.random() * 0.6;

        return o;
    }
    
    function onPollError(serverId, XMLHttpRequest, status, errorThrown) {
        if (!servers[serverId])
            return;
        
        servers[serverId].lastActivity = new Date();
        servers[serverId].poll = null;
        
        // possible reasons for error: timeout, lost connection, navigating away

        if (status == "timeout") {
            // timeout generally means we're good, there just wasn't
            // anything to report in the other end, so just reschedule
            console.log("Timeout, reconnecting");
            servers[serverId].failHappened = null;
            setErrorForServer(serverId, "");
            schedulePoll(serverId, 200);
            return;
        }
        
        if (servers[serverId].failHappened == null)
            servers[serverId].failHappened = new Date();

        var t = getRescheduleTime(servers[serverId].failHappened);
        if (t.secondsSince > 60)
            setErrorForServer(serverId, "Lost server connection");
        else
            setErrorForServer(serverId, "");

        console.log("Error", status, errorThrown, ", next try in " + t.nextTry.toFixed(1) + " seconds");

        schedulePoll(serverId, t.nextTry * 1000);
    }

    function schedulePoll(serverId, millisecs) {
	console.log("Scheduling poll on", serverId);
        // make sure we're the only one running
        if (servers[serverId].scheduleTimeout)
            clearTimeout(servers[serverId].scheduleTimeout);
        
        servers[serverId].scheduleTimeout = setTimeout(function () {
            if (!servers[serverId])
                return;
            
            servers[serverId].scheduleTimeout = null;
            
            pollServer(serverId);
        }, millisecs);
    }
        
    
    function newMessage(message, roomId) {
        if (!rooms[roomId])
            return;
            
        var serverId = rooms[roomId].server;
        if (!servers[serverId])
            return;
        
        console.log ("New message", message);
        rooms[roomId].chatbox.find(".chatboxtextarea").attr('disabled','disabled');
        
        if (message == '')
            message = ' ';

        function onError(xhr, status, error) {
            if (!rooms[roomId])
                return;
                
            console.log ("Error trying to submit");
            addHtmlToChatBox(roomId, '<div class="error">Sorry, failed to send message...</div>');
            // add back the text so it doesn't disappear completely
            rooms[roomId].chatbox.find(".chatboxtextarea").val(message).attr('disabled','').focus();
            possiblyEnlargeChatBoxTextArea(rooms[roomId].chatbox.find(".chatboxtextarea"));
        }
        
        try {
            whenChannelUp(serverId, function () {
                channeledAjax(servers[serverId].channel, {
                    type: "POST",
                    url: "/torncity/chat/message/" + serverId + "/new",
                    data: { body: message, room: roomId },
                    timeout: 10 * 1000,
                    success: function(msg) {
                        if (!rooms[roomId])
                            return;
                        
                        rooms[roomId].chatbox.find(".chatboxtextarea").attr('disabled','').focus();
                        console.log ("Submitted OK");
                    },
                    error: onError
                });
            });
        }
        catch (err) {
            onError(null, "exception", err);
        }
    }

    function addChatRoom(roomId, serverId, displayName, status, participants, callback) {
        if (rooms[roomId])
            return;

        rooms[roomId] = {};
        rooms[roomId].id = roomId;
        rooms[roomId].name = displayName;
        rooms[roomId].server = serverId;
        rooms[roomId].status = status;
        rooms[roomId].cursor = "";
        rooms[roomId].lastMessages = [];
        rooms[roomId].focused = false;
        rooms[roomId].initializedParticipantStatus = true;
        rooms[roomId].participantStatus = {};
        if (participants) {
            // make participants into a map of uid -> playername
            rooms[roomId].participants = {};
            for (var i = 0; i < participants.length; ++i)
                rooms[roomId].participants[participants[i][0]] = participants[i][1];
            rooms[roomId].initializedParticipantStatus = false;
            
            // fix up room name
            var l = abbreviatedList(getUserNamesInRoom(roomId), 3);
            if (l)
                rooms[roomId].name = l;
        }
        else
            rooms[roomId].participants = null;

        createChatBox(roomId);
        if (!holdOffChatRoomRestructuring)
	    restructureChatBoxes();
        
        if (callback)
            callback(roomId);

        callCallbacks("rooms");

        var storedMessages = jQuery.jStorage.get('roommessages' + roomId);
        if (storedMessages) {
            ++restoringStateFromLocalStorage;
            // set current room as updated as these are old messages
            gotMessages({ messages: storedMessages, updated_rooms: [ roomId ] });
            --restoringStateFromLocalStorage;
        }
        
        whenChannelUp(serverId, function () {
            schedulePoll(serverId, 200);
        });
    }

    function removeChatRoom(roomId, keepUI) {
        console.log("Removing room", roomId);
        var s = rooms[roomId].server;
        
        if (rooms[roomId].chatbox) {
	    if (!keepUI)
		rooms[roomId].chatbox.remove();
            rooms[roomId].chatbox = null;
        }
        delete rooms[roomId];

        if (getRoomsForServer(s).length == 0
            && s != statusServerId
            && servers[s])
            removeServer(s);
    }

    function removeServer(serverId) {
        var s = servers[serverId];
        delete servers[serverId];
            
        if (s.poll)
            s.poll.abort();

        if (s.frameName)
            jQuery("body > iframe[name=" + s.frameName + "]").remove();
    }
    
    function setChatRoomStatus(roomId, status, skipServerUpdate) {
        var room = rooms[roomId];
        if (!room)
            return;

         // allow both textual and numeric status
        if (!isNaN(+status))
            status = statusMap[+status];

        var prev = room.status;
        
        room.status = status;
        if (!skipServerUpdate)
            updateServerSideRoomStatus(room);
        if (!holdOffChatRoomRestructuring)
            restructureChatBoxes();

        // if we just close it, it's OK, the poll will just timeout
        // and stop; however, if we're opening the box, we need to
        // react; for global rooms where we cut the connection on
        // minimization, we also need to react on an open
        if (prev == "closed" && status != "closed"
            || ((roomId == "Global" || roomId == "Trade" || roomId == "New_Players")
                && prev == "minimized" && status == "open"))
            whenChannelUp(room.server, function () {
                schedulePoll(room.server, 200);
            });
    }

    // subscribe to changes in online status
    function addUserStatusListener(roomId) {
        if (!rooms[roomId])
            return;
        
        chat.listenForUserChanges(
            "participants:" + roomId, getUserIdsInRoom(roomId),
            function () { userStatusChangesForRoom(roomId); });
    }
    
    function userStatusChangesForRoom(roomId) {
        var room = rooms[roomId];
        if (!room)
            return;
        
        addUserStatusListener(roomId);

        var users = getUserIdsInRoom(roomId);
        
        if (!room.initializedParticipantStatus) {
            room.initializedParticipantStatus = true;

            var online =
                jQuery.grep(users, function (uid) {
                    return chat.users[uid] == chat.USER_STATUS_ONLINE;
                });

            var s = "";
            if (online.length == 0) {
                s = "You are the only one online";
            }
            else if (online.length == 1) {
                s = room.participants[online[0]] + " is online";
            }
            else {
                s = abbreviatedList(jQuery.map(online, function (uid) {
                    return room.participants[uid];
                }), 3);

                s += " are online";
            }
            addHtmlToChatBox(roomId, '<div><i>' + s + '</i></div>');
        }
        else {
            var wentOnline = [], wentOffline = [];
            
            jQuery.each(users, function (i, uid) {
                var from = room.participantStatus[uid], to = chat.users[uid];
                if (from != to) {
                    if (to == chat.USER_STATUS_OFFLINE)
                        wentOffline.push(room.participants[uid]);
                    else if (to == chat.USER_STATUS_ONLINE)
                        wentOnline.push(room.participants[uid]);
                }
            });

            var html = [];
            if (wentOffline.length > 0)
                html.push(abbreviatedList(wentOffline, 3) + " went offline");
            if (wentOnline.length > 0)
                html.push(abbreviatedList(wentOnline, 3) + " comes online");

            if (wentOffline.length + wentOnline.length > 0)
                addHtmlToChatBox(roomId, '<div><i>' + html.join(", ") + '</i></div>');
        }

        // update the rooms view
        jQuery.each(users, function (i, uid) {
            room.participantStatus[uid] = chat.users[uid];
        });
    }
    
    function restartPollers() {
        for (var id in servers)
            pollServer(id);
    }
    
    function stopChat(keepUI) {
        chat.running = false;

        console.log ("Stopping chat");

        stopListeningForUserActivity();
        
        clearInterval(monitorInterval);
        monitorInterval = null;
        
        var id;

        // whack rooms
        for (id in rooms)
            removeChatRoom(id, keepUI);

        // whack servers
        for (id in servers)
            removeServer(id);
    }

    function startChat() {
        jQuery("#reenableChatButton").remove();
        
        if (chat.running) {
            console.log ("Chat already running");
            return;
        }

	// clean up server connections before we leave the page,
	// however keep the UI intact so the page transition appears
	// seamless
        window.onbeforeunload = function() {
            stopChat(true);
        };
        
        try {
            chat.running = true;
            listenForUserActivity();
            if (!monitorInterval)
                monitorInterval = setInterval(monitor, monitorIntervalTime * 1000);
            var storedData = {
                rooms: jQuery.jStorage.get("statusrooms"),
                users: jQuery.jStorage.get("statususers")
            }
            if (storedData.rooms) {
                console.log("got status from local storage")
                ++restoringStateFromLocalStorage;
                gotStatus(storedData);
                --restoringStateFromLocalStorage;
            }
            whenChannelUp(statusServerId, function (serverId) {
                schedulePoll(statusServerId, 100);
            });
        } catch (err) {
            console.log("Catched exception, error", err);
            stopChat();
        }
    }

    function setupReenableChatButton() {
        if (jQuery("#reenableChatButton").length > 0)
            return;

        jQuery('<div id="reenableChatButton">Reenable chat</div>')
            .appendTo(document.body)
            .click(function () {
                jQuery("#reenableChatButton").remove();
        
                if (!chat.running)
                    startChat();
            });
    }

    // monitor chat health and user activity
    function monitor() {
        if (!chat.running)
            return;

        function secondsSince(t) {
            return (new Date().getTime() - t.getTime()) / 1000;
        }
        
        var inactivity = secondsSince(lastUserActivity);
        console.log("User active in", inactivity, "seconds");
        if (inactivity > maxInactivityTime) {
            console.log("Disabling chat due to inactivity in", inactivity.toFixed(1), "seconds");
            stopChat();
            setupReenableChatButton();
            return;
        }
        
        console.log("Monitoring health of long-pollers");
        
        var checkServers = { };
        checkServers[statusServerId] = true;
        
        for (var r in rooms)
            if (rooms[r].status != "closed")
                checkServers[rooms[r].server] = true;

        for (var serverId in checkServers)
            if (servers[serverId] &&
                secondsSince(servers[serverId].lastActivity) > longPollTimeout * 2)
                schedulePoll(serverId, 200 + Math.random() * 200);
    }

    // listen for events and update user activity timestamp accordingly
    function listenForUserActivity() {
        jQuery(document.body)
            .keypress(chat.userActivityOccurred)
            .click(chat.userActivityOccurred);
    }

    function stopListeningForUserActivity() {
        jQuery(document.body)
            .unbind('keypress')
            .unbind('click');
    }
})();


// new json encode plugin
// http://code.google.com/p/jquery-json/

(function(jQuery){jQuery.toJSON=function(o)
{if(typeof(JSON)=='object'&&JSON.stringify)
return JSON.stringify(o);var type=typeof(o);if(o===null)
return"null";if(type=="undefined")
return undefined;if(type=="number"||type=="boolean")
return o+"";if(type=="string")
return jQuery.quoteString(o);if(type=='object')
{if(typeof o.toJSON=="function")
return jQuery.toJSON(o.toJSON());if(o.constructor===Date)
{var month=o.getUTCMonth()+1;if(month<10)month='0'+month;var day=o.getUTCDate();if(day<10)day='0'+day;var year=o.getUTCFullYear();var hours=o.getUTCHours();if(hours<10)hours='0'+hours;var minutes=o.getUTCMinutes();if(minutes<10)minutes='0'+minutes;var seconds=o.getUTCSeconds();if(seconds<10)seconds='0'+seconds;var milli=o.getUTCMilliseconds();if(milli<100)milli='0'+milli;if(milli<10)milli='0'+milli;return'"'+year+'-'+month+'-'+day+'T'+
hours+':'+minutes+':'+seconds+'.'+milli+'Z"';}
if(o.constructor===Array)
{var ret=[];for(var i=0;i<o.length;i++)
ret.push(jQuery.toJSON(o[i])||"null");return"["+ret.join(",")+"]";}
var pairs=[];for(var k in o){var name;var type=typeof k;if(type=="number")
name='"'+k+'"';else if(type=="string")
name=jQuery.quoteString(k);else
continue;if(typeof o[k]=="function")
continue;var val=jQuery.toJSON(o[k]);pairs.push(name+":"+val);}
return"{"+pairs.join(", ")+"}";}};jQuery.evalJSON=function(src)
{if(typeof(JSON)=='object'&&JSON.parse)
return JSON.parse(src);return eval("("+src+")");};jQuery.secureEvalJSON=function(src)
{if(typeof(JSON)=='object'&&JSON.parse)
return JSON.parse(src);var filtered=src;filtered=filtered.replace(/\\["\\\/bfnrtu]/g,'@');filtered=filtered.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,']');filtered=filtered.replace(/(?:^|:|,)(?:\s*\[)+/g,'');if(/^[\],:{}\s]*$/.test(filtered))
return eval("("+src+")");else
throw new SyntaxError("Error parsing JSON, source is not valid.");};jQuery.quoteString=function(string)
{if(string.match(_escapeable))
{return'"'+string.replace(_escapeable,function(a)
{var c=_meta[a];if(typeof c==='string')return c;c=a.charCodeAt();return'\\u00'+Math.floor(c/16).toString(16)+(c%16).toString(16);})+'"';}
return'"'+string+'"';};var _escapeable=/["\\\x00-\x1f\x7f-\x9f]/g;var _meta={'\b':'\\b','\t':'\\t','\n':'\\n','\f':'\\f','\r':'\\r','"':'\\"','\\':'\\\\'};})(jQuery);

// jQuery cookie plugin http://plugins.jquery.com/files/jquery.cookie.js.txt
jQuery.cookie=function(B,I,L){if(typeof I!="undefined"){L=L||{};if(I===null){I="";L.expires=-1}var E="";if(L.expires&&(typeof L.expires=="number"||L.expires.toUTCString)){var F;if(typeof L.expires=="number"){F=new Date();F.setTime(F.getTime()+(L.expires*24*60*60*1000))}else{F=L.expires}E="; expires="+F.toUTCString()}var K=L.path?"; path="+(L.path):"";var G=L.domain?"; domain="+(L.domain):"";var A=L.secure?"; secure":"";document.cookie=[B,"=",encodeURIComponent(I),E,K,G,A].join("")}else{var D=null;if(document.cookie&&document.cookie!=""){var J=document.cookie.split(";");for(var H=0;H<J.length;H++){var C=jQuery.trim(J[H]);if(C.substring(0,B.length+1)==(B+"=")){D=decodeURIComponent(C.substring(B.length+1));break}}}return D}};
