AjaxIM = function(options, actions) {
    if(this instanceof AjaxIM) {
        var self = this;

        // === {{{ defaults }}} ===
        //
        // These are the available settings for Ajax IM, and the associated
        // defaults:
        //
        // * {{{pollServer}}} is the default URL to which all actions refer. It is
        // possible to specify certain action URLs separately (as is used with the
        // NodeJS server).
        // * {{{theme}}} is the name of the theme folder that defines the HTML and
        // CSS of the IM bar and chat boxes. Usually, themes are deposited in the
        // provided "themes" folder and specified by that path, e.g. {{{themes/default}}}.
        // Theme files within the theme folder must be named {{{theme.html}}} and
        // {{{theme.css}}}.
        var defaults = {
            pollServer: '',
            theme: 'themes/default'
        };

        // === {{{AjaxIM.}}}**{{{settings}}}** ===
        //
        // These are the settings for the IM. If particular options are not specified,
        // the defaults (see above) will be used. //These options will be defined
        // upon calling the initialization function, and not set directly.//
        this.settings = $.extend(defaults, options);

        // === {{{AjaxIM.}}}**{{{actions}}}** ===
        //
        // Each individual action that the IM engine can execute is predefined here.
        // By default, it merely appends the action onto the end of the {{{pollServer}}} url,
        // however, it is possible to define actions individually. //The alternative actions
        // will be defined upon calling the initialization function, and not set directly.//
        //
        // Should you define an action at a different URL, Ajax IM will determine whether
        // or not this URL is within the current domain. If it is within a subdomain of
        // the current domain, it will set the document.domain variable for you,
        // to match a broader hostname scope; the action will continue to use {{{$.post}}}
        // (the default AJAX method for Ajax IM).
        //
        // On the other hand, should you choose a URL outside the current domain
        // Ajax IM will switch to {{{$.getJSON}}} (a get request) to avoid
        // cross-domain scripting issues. This means that a server on a different
        // port or at a different address will need to be able to handle GET
        // requests rather than POST requests (such as how the Node.JS Ajax IM
        // server works).
        this.actions = $.extend({
            noop: this.settings.pollServer + '/app/noop',
            listen: this.settings.pollServer + '/app/listen',
            send: this.settings.pollServer + '/app/message',
            status: this.settings.pollServer + '/app/status',
            signoff: this.settings.pollServer + '/app/signoff'
        }, actions);

        // If Socket.IO is available, create a socket
        self.socket = null;
        $.getScript(this.settings.pollServer+'/socket.io/socket.io.js', function(){
            self.socket = io(self.settings.pollServer);
            self.socket.on('client', function(event) {
               event = $.extend(true, {}, event);
               self.dispatchEvent(event);
            });
            var event = {type: 'hello', from: this.username, sessionID: cookies.get('sessionid')};
            self.sendEvent(event);
        });

        // We load the theme dynamically based on the passed
        // settings. If the theme is set to false, we assume
        // that the user is going to load it himself.
        this.themeLoaded = false;
        if(this.settings.theme) {
            if(typeof document.createStyleSheet == 'function')
                document.createStyleSheet(this.settings.theme + '/theme.css');
            else
                $('body').append('<link rel="stylesheet" href="' +
                    this.settings.theme + '/theme.css" />');
            $('<div>').appendTo('body').load(this.settings.theme + '/theme.html #imjs-bar, .imjs-tooltip',
                function() {
                    self.themeLoaded = true;
                    self.setup();
                }
            );
        } else {
            this.themeLoaded = true;
            this.setup();
        }

        // Allow a chatbox to be minimized
        $(document).on('click', '.imjs-chatbox', function(e) {
            e.preventDefault();
            return false;
        });

        $(document).on('click', '.imjs-chatbox .imjs-minimize', function() {
            $(this).parents('.imjs-selected').click();
        });

        // Setup message sending for all chatboxes
        $(document).on('keydown', '.imjs-chatbox .imjs-input', function(event) {
            var obj = $(this);
//            if(event.keyCode == 13 && !($.browser.msie && $.browser.version < 8)) {
            if(event.keyCode == 13) {
                self.send(obj.parents('.imjs-chatbox').data('username'), obj.val());
            }
        }).on('keyup', '.imjs-chatbox .imjs-input', function(event) {
            if(event.keyCode == 13) {
 //               if($.browser.msie && $.browser.version < 8) {
                if(false) {
                    var obj = $(this);
                    self.send(obj.parents('.imjs-chatbox').data('username'), obj.val());
                }

                var obj = $(this);
                obj.val('');
                obj.height(obj.data('height'));
            }
        }).on('keypress', '.imjs-chatbox .imjs-input', function(e) {
            var obj = $(this);
            obj.height(0);
            if(true) obj.height(0);
            if(this.scrollHeight > obj.height() || this.scrollHeight < obj.height()) {
                obj.height(this.scrollHeight);
            }
        });

        $(document).on('click', '.imjs-msglog', function() {
            var chatbox = $(this).parents('.imjs-chatbox');
            chatbox.find('.imjs-input').focus();
        });

        // Create a chatbox when a buddylist item is clicked
        $(document).on('click', '.imjs-friend', function() {
            var chatbox = self._createChatbox($(this).data('friend'));

            if(chatbox.parents('.imjs-tab').data('state') != 'active') {
                chatbox.parents('.imjs-tab').click();
                store.set(self.username + '-activeTab', $(this).data('friend'));
            }

            chatbox.find('.imjs-input').focus();
            if(!(input = chatbox.find('.imjs-input')).data('height')) {
                // store the height for resizing later
                if (!input.height()) {
                   input.height(16);
                }
                input.data('height', input.height());
            }
        });

        // Setup and hide the scrollers
        $('.imjs-scroll').css('display', 'none');
        $(document).on('click', '#imjs-scroll-right', function() {
            var hiddenTab = $(this)
                .prevAll('#imjs-bar li.imjs-tab:hidden')
                .filter(function() {
                    return (
                        $(this).data('state') != 'closed' &&
                        $(this).prev('#imjs-bar li.imjs-tab:visible').length
                    );
                })
                .not('.imjs-default')
                .slice(-1)
                .css('display', '');

            if(hiddenTab.length) {
                $('#imjs-bar li.imjs-tab:visible').eq(0).css('display', 'none');
                $(this).html(parseInt($(this).html()) - 1);
                $('#imjs-scroll-left').html(parseInt($('#imjs-scroll-left').html()) + 1);
            }

            return false;
        });
        $(document).on('click', '#imjs-scroll-left', function() {
            var hiddenTab = $(this)
                .nextAll('#imjs-bar li.imjs-tab:hidden')
                .filter(function() {
                    return (
                        $(this).data('state') != 'closed' &&
                        $(this).next('#imjs-bar li.imjs-tab:visible').length
                    );
                })
                .not('.imjs-default')
                .slice(-1)
                .css('display', '');

            if(hiddenTab.length) {
                $('#imjs-bar li.imjs-tab:visible').slice(-1).css('display', 'none');
                $(this).html(parseInt($(this).html()) - 1);
                $('#imjs-scroll-right').html(parseInt($('#imjs-scroll-right').html()) + 1);
            }

            return false;
        });

        // Setup status buttons
        $(document).on('click', '#imjs-status-panel .imjs-button', function() {
            var status = this.id.split('-')[2];

            $('#imjs-away-message-text, #imjs-away-message-text-arrow').animate({
                opacity: (status == 'away' ? 'show' : 'hide'),
                height: (status == 'away' ? 'show' : 'hide')
            }, 50);
            
            $('#imjs-status-panel .imjs-button').removeClass('imjs-toggled');
            $(this).addClass('imjs-toggled');
            
            if(self.current_status[0] == 'away')
                self._last_status_message = $('#imjs-away-message-text').val();

            $('#imjs-away-message-text').val(status == 'away'
                                                ? self._last_status_message ||
                                                  AjaxIM.l10n.defaultAway
                                                : '');

            self.status(status, $('#imjs-away-message-text').val());
            return false;
        });
        
        // Allow status message to be changed
        $(document)
            .on('keyup', '#imjs-away-message-text', (function() {
                var msg_type_timer = null;
                
                return function() {
                    if(msg_type_timer) clearTimeout(msg_type_timer);
    
                    msg_type_timer = setTimeout(function() {
                        self._last_status_message = 
                        self.current_status[1] = $('#imjs-away-message-text')
                                                    .addClass('imjs-loading').val();
                        self.status.apply(self, self.current_status);
                    }, 250);
                };
            })());
        $(this).bind('changeStatusSuccessful changeStatusFailed', function() {
            $('#imjs-away-message-text').removeClass('imjs-loading');
        });
        
        // Setup reconnect button
        $(document).on('click', '#imjs-reconnect', function() {
            self.offline = false;
            store.remove(self.username + '-offline');
            $('#imjs-reconnect').hide();
            $('.imjs-input').attr('disabled', false);
            
            // Restore status to available
            $('#imjs-status-panel .imjs-button').removeClass('imjs-toggled');
            $('#imjs-button-available').addClass('imjs-toggled');
            $(self.statuses).each(function() {
                $('#imjs-friends').removeClass('imjs-' + this);
            });
            $('#imjs-friends').addClass('imjs-available');
            $('#imjs-away-message-text, #imjs-away-message-text-arrow')
                .css('display', 'none');
            
            // Set status
            self.current_status = ['available', ''];
            store.set(self.username + '-status', ['available', '']);
            self.status('available', '');
            
            // Reconnect
            self.storage();
            self.listen();
        });

        // Initialize the chatbox hash
        this.chats = {};

        // On window resize, check scroller visibility
        $(window).resize(function() {
            try {
                self._scrollers();
            } catch(e) {}
        });

        // Set up event handling
        this.onEvent('hello', this.onHello);
        this.onEvent('message', this.onMessage);
        this.onEvent('status', this.onStatus);
        this.onEvent('notice', this.onNotice);
        this.onEvent('goodbye', this.onGoodbye);
    } else {
        return AjaxIM.init(options);
    }
};

$.extend(AjaxIM.prototype, {
    // == Main ==
    setup: function() {
        var self = this;
        $(this).trigger('loadComplete');

        $('.imjs-scroll').css('display', 'none');
        this.initTabBar();
        this._scrollers();

        this.username = store.get('user');
        this._lastReconnect = 0;

        if(this.username && store.get(this.username + '-offline') == true) {
            this.offline = true;
            
            setTimeout(function() { self._showReconnect(); }, 0);
            return;
        }

        if(this.username)
            this.storage();

        setTimeout(function() { if (!self.socket) self.listen(); }, 2000);
    },

    // === {{{AjaxIM.}}}**{{{storage()}}}** ===
    //
    // Retrieves chat session data from whatever storage engine is enabled
    // (provided that one is enabled at all). If a page reloads, this function
    // is called to restore the user's chat state (existing conversations, active tab).
    // This function is called //automatically//, upon initialization of the IM engine.
    storage: function() {
        var self = this,
            chatstore = store.get(this.username + '-chats'),
            friends = store.get(this.username + '-friends'),
            status = store.get(this.username + '-status') || ['available', ''];

        this.chatstore = chatstore || {};
        this.friends = {};
        this.current_status = status;

        if(friends) {
            $.each(friends, function(friend, data) {
                self.addFriend(friend, data.status, data.group);
            });

            $('#imjs-friends').removeClass('imjs-not-connected')
                              .addClass('imjs-' + status[0]);
            
            $('#imjs-button-' + status[0]).addClass('imjs-toggled');
            if(status[0] == 'away') {
                setTimeout(function() {
                    $('#imjs-away-message-text, #imjs-away-message-text-arrow').show();
                }, 250);
                $('#imjs-away-message-text').val(this.current_status[1]);
            }
        }

        $.each(this.chatstore, function(username, convo) {
            if(!convo.length) return;

            var chatbox = self._createChatbox(username, true),
                msglog = chatbox.find('.imjs-msglog').empty();
            chatbox.data('lastDateStamp', null).css('display', 'none');

            if(typeof convo == 'string')
                convo = self.chatstore[username] = JSON.parse(convo);

            // Restore all messages, date stamps, and errors
            msglog.html(convo.join(''));

            $(self).trigger('chatRestored', [username, chatbox]);
        });

        var activeTab = store.get(this.username + '-activeTab');
        if(activeTab && activeTab in this.chats) {
            this.chats[activeTab].parents('.imjs-tab').click();
            var msglog = this.chats[activeTab].find('.imjs-msglog');
            msglog[0].scrollTop = msglog[0].scrollHeight;
        }
        
        // Set username in Friends list
        var header = $('#imjs-friends-panel .imjs-header');
        header.html(header.html().replace('{username}', this.username));
    },

    // === //private// {{{AjaxIM.}}}**{{{_clearSession()}}}** ===
    //
    // Clears all session data from the last known user.
    _clearSession: function() {
        var last_user = store.get('user');
        $.each(['friends', 'activeTab', 'chats', 'status', 'connected'],
               function(i, key) {
                   store.remove(last_user + '-' + key);
               });
        store.set('user', '');

        this.chats = {};
        this.friends = {};
        this.chatstore = {};
        this.current_status = ['available', ''];

        $('.imjs-tab').not('.imjs-tab.imjs-default').remove();
        $('.imjs-friend-group').not('.imjs-friend-group.imjs-default').remove();

        delete this.username;
    },

    // === {{{AjaxIM.}}}**{{{listen()}}}** ===
    //
    // Queries the server for new messages.
    listen: function() {
        if(this.offline) return;

        var self = this;
        AjaxIM.get(
            this.actions.listen,
            {},
            function(response) {
                if($.isArray(response)) {
                    $.each(response, function(key, value) {
                        self._parseMessage(value);
                    });
                }
                else if($.isPlainObject(response)) {
                    self._parseMessage(response);
                }

                if (!self.socket) {
                    setTimeout(function() { self.listen(); }, 0);
                }
            },
            function(error) {
                self._notConnected();
                $(self).trigger('pollFailed', ['not connected']);

                // Try reconnecting in n*2 seconds (max 16)
                self._reconnectIn = (self._lastReconnect < (new Date()) - 60000)
                                     ? 1000
                                     : Math.min(self._reconnectIn * 2, 16000);
                self._lastReconnect = new Date();
                if (!self.socket) {
                    setTimeout(function() { self.listen(); }, self._reconnectIn);
                }
            },
            this.actions.noop
        );
    },

    // === //private// {{{AjaxIM.}}}**{{{_parseMessages(messages)}}}** ===
    //
    _parseMessage: function(message) {
        this.triggerEvent(message);
    },

    onHello: function(message) {
        var self = this;
        this._clearSession();

        this.username = message.username;
        this.current_status = ['available', ''];
        store.set('user', message.username);
        store.set(this.username + '-status', this.current_status);

        $('#imjs-friends').attr('class', 'imjs-available');
        $.each(message.friends, function() {
            var friend;
            if(this.length == 2)
                friend = this;
            else
                friend = [this.toString(), ['offline', '']];
            self.addFriend(friend[0], friend[1], 'Friends');
        });
        store.set(this.username + '-friends', this.friends);

        // Set username in Friends list
        var header = $('#imjs-friends-panel .imjs-header');
        header.html(header.html().replace('{username}', this.username));

        // Set status available
        $('#imjs-away-message-text, #imjs-away-message-text-arrow').hide();
        $('#imjs-status-panel .imjs-button').removeClass('imjs-toggled');
        $('#imjs-button-available').addClass('imjs-toggled');
    },

    onMessage: function(event) {
         this.incoming(event.from, event.body);
    },

    onStatus: function(event) {
        this._friendUpdate(event.from, event.status, event.message);
        this._storeFriends();
    },

    onNotice: function(event) {
    },

    onGoodbye: function(event) {
        this._notConnected();
    },

    // === {{{AjaxIM.}}}**{{{incoming(from, message)}}}** ===
    //
    // Handles a new message from another user. If a chatbox for that
    // user does not yet exist, one is created. If it does exist, but
    // is minimized, the user is notified but the chatbox is not brought
    // to the front. This function also stores the message, if a storage
    // method is set.
    //
    // ==== Parameters ====
    // * {{{from}}} is the username of the sender.
    // * {{{message}}} is the body.
    incoming: function(from, message) {
        // check if IM exists, otherwise create new window
        // TODO: If friend is not on the buddylist,
        // should add them to a temp list?
        var chatbox = this._createChatbox(from),
            tab = chatbox.parents('.imjs-tab');

        if(!$('#imjs-bar .imjs-selected').length) {
            tab.click();
        } else if(tab.data('state') != 'active') {
            this.notification(tab);
        }

        this._store(from, this._addMessage('b', chatbox, from, message));
    },

    // === {{{AjaxIM.}}}**{{{addFriend(username, group)}}}** ===
    //
    // Inserts a new friend into the friends list. If the group specified
    // doesn't exist, it is created. If the friend is already in this group,
    // they aren't added again, however, the friend item is returned.
    //
    // ==== Parameters ====
    // * {{{username}}} is the username of the new friend.
    // * {{{status}}} is the current status of the friend.
    // * {{{group}}} is the user group to which the friend should be added.
    addFriend: function(username, status, group) {
        var group_id = 'imjs-group-' + md5.hex(group);

        if(!(group_item = $('#' + group_id)).length) {
            var group_item = $('.imjs-friend-group.imjs-default').clone()
                    .removeClass('imjs-default')
                    .attr('id', group_id)
                    .data('group', group)
                    .appendTo('#imjs-friends-list');

            var group_header = group_item.find('.imjs-friend-group-header');
            group_header.html(group_header.html().replace('{group}', group));
        }

        var user_id = 'imjs-friend-' + md5.hex(username + group);

        if(!$('#' + user_id).length) {
            var user_item = group_item.find('ul li.imjs-default').clone()
                    .removeClass('imjs-default')
                    .addClass('imjs-' + status[0])
                    .attr('id', user_id)
                    .data('friend', username)
                    .appendTo(group_item.find('ul'));
//            if(status[0] == 'offline')
//                user_item.hide();
            user_item.html(
                user_item.html()
                         .replace('{username}', username)
                         .replace('{status}', status[1])
            );
            user_item.find('.imjs-friend-status')
                     .attr('title', status[1]);
        }

        this.friends[username] = {status: status, group: group};
        this._updateFriendCount();
        return this.friends[username];
    },

    // === //private// {{{AjaxIM.}}}**{{{_updateFriendCount()}}}** ===
    //
    // Counts the number of online friends and updates the friends count
    // in the friend tab.
    _updateFriendCount: function() {
        var friendsLength = 0;
        $.each(this.friends, function(u, f) {
            if(f.status[0] != 'offline') friendsLength++;
        });
        $('#imjs-friends .imjs-tab-text span span').html(friendsLength);
    },

    // === //private// {{{AjaxIM.}}}**{{{_storeFriends()}}}** ===
    //
    // If a storage method is enabled, the current state of the
    // user's friends list is stored.
    _storeFriends: function() {
        store.set(this.username + '-friends', this.friends);
    },

    // === //private// {{{AjaxIM.}}}**{{{_createChatbox(username)}}}** ===
    //
    // Builds a chatbox based on the default chatbox HTML and CSS defined
    // in the current theme. Should a chatbox for this user already exist,
    // a new one is not created. Instead, it is either given focus (should
    // no other windows already have focus), or a notification is issued.
    //
    // As well, if the chatbox does not exist, an associated tab will be
    // created.
    //
    // ==== Parameters ====
    // * {{{username}}} is the name of the user for whom the chatbox is intended
    // for.
    // * {{{no_stamp}}} sets whther or not to add a date stamp to the chatbox
    // upon creation.
    //
    // //Note:// New chatboxes are given an automatically generated ID in the
    // format of {{{#imjs-[md5 of username]}}}.
    _createChatbox: function(username, no_stamp) {
        var self = this,
            chatbox_id = 'imjs-' + md5.hex(username);
        if(!(chatbox = $('#' + chatbox_id)).length) {
            // add a tab
            var tab = this.addTab(username, '#' + chatbox_id);
            var chatbox = tab.find('.imjs-chatbox');

            chatbox.attr('id', chatbox_id);

            // remove default items from the message log
            var message_log = chatbox.find('.imjs-msglog').empty();

            // setup the chatbox header
            var cb_header = chatbox.find('.imjs-header');
            cb_header.html(cb_header.html().replace('{username}', username));

            if(!no_stamp) {
                // add a date stamp
                this._store(username, this._addDateStamp(chatbox));
            }

            // associate the username with the object and vice-versa
            this.chats[username] = chatbox;
            chatbox.data('username', username);

            if(username in this.friends) {
                status = this.friends[username].status;
                tab.addClass('imjs-' + status);
            }

            setTimeout(function() { self._scrollers(); }, 0);
        } else if(chatbox.parents('.imjs-tab').data('state') == 'closed') {
            chatbox.find('.imjs-msglog > *').addClass('imjs-msg-old');

            var tab = chatbox.parents('.imjs-tab');
            if(tab.css('display') == 'none')
                tab.css('display', '').removeClass('imjs-selected')
                    .insertAfter('#imjs-scroll-left')
                    .data('state', 'minimized');

            if(!no_stamp) {
                // possibly add a date stamp
                this._store(username, this._addDateStamp(chatbox));
            }

            if(!$('#imjs-bar .imjs-selected').length) {
                tab.click();
            } else {
                this.notification(tab);
            }

            setTimeout(function() { self._scrollers() }, 0);
        }

        return chatbox;
    },

    // === //private// {{{AjaxIM.}}}**{{{_addDateStamp(chatbox)}}}** //
    //
    // Adds a date/time notifier to a chatbox. These are generally
    // inserted upon creation of a chatbox, or upon the date changing
    // since the last time a date stamp was added. If a date stamp for
    // the current date already exists, a new one will not be added.
    //
    // ==== Parameters ====
    // * {{{chatbox}}} refers to the jQuery-selected chatbox DOM element.
    // * {{{time}}} is the date/time the date stamp will show. It is specified
    // in milliseconds since the Unix Epoch. This is //only// defined when
    // date stamps are being restored from storage; if not specified, the
    // current computer time will be used.
    _addDateStamp: function(chatbox, time) {
        var message_log = $(chatbox).find('.imjs-msglog');
        if(!time)
           time = (new Date()).getTime();

        var date_stamp = $('.imjs-tab.imjs-default .imjs-chatbox .imjs-msglog .imjs-date').clone();
        var date_stamp_time = date_stamp.find('.imjs-msg-time');
        if(date_stamp_time.length)
            date_stamp_time.html(dateFormat(time, date_stamp_time.html()));

        var date_stamp_date = date_stamp.find('.imjs-date-date');
        var formatted_date = dateFormat(time, date_stamp_date.html());
        if(chatbox.data('lastDateStamp') != formatted_date) {
            if(date_stamp_date.length)
                date_stamp_date.html(dateFormat(time, date_stamp_date.html()));

            chatbox.data('lastDateStamp', formatted_date);
            date_stamp.appendTo(message_log);

            return {
                replace_last: false,
                html: jQuery('<div>').append(date_stamp.clone()).html()
            };
        } else {
            //$('<div></div>').appendTo(message_log);
            return {replace_last: false, html: ''};
        }
    },

    // === //private// {{{AjaxIM.}}}**{{{_addError(chatbox, error)}}}** //
    //
    // Adds an error to a chatbox. These are generally inserted after
    // a user sends a message unsuccessfully. If an error message
    // was already added, another one will be added anyway.
    //
    // ==== Parameters ====
    // * {{{chatbox}}} refers to the jQuery-selected chatbox DOM element.
    // * {{{error}}} is the error message string.
    // * {{{time}}} is the date/time the error occurred. It is specified in
    // milliseconds since the Unix Epoch. This is //only// defined when
    // errors are being restored from storage; if not specified, the current
    // computer time will be used.
    _addError: function(chatbox, error, time) {
        var message_log = $(chatbox).find('.imjs-msglog');

        var error_item =
            $('.imjs-tab.imjs-default .imjs-chatbox .imjs-msglog .imjs-error').clone();

        var error_item_time = error_item.find('.imjs-msg-time');
        if(error_item_time.length) {
            if(!time)
                time = (new Date()).getTime();
            error_item_time.html(dateFormat(time, error_item_time.html()));
        }

        error_item.find('.imjs-error-error').html(error);
        error_item.appendTo(message_log);

        message_log[0].scrollTop = message_log[0].scrollHeight;

        return {
            replace_last: false,
            html: jQuery('<div>').append(error_item.clone()).html()
        };
    },

    // === //private// {{{AjaxIM.}}}**{{{_addMessage(ab, chatbox, username, message, time)}}}** //
    //
    // Adds a message to a chatbox. Depending on the {{{ab}}} value,
    // the color of the username may change as a way of visually
    // identifying users (however, this depends on the theme's CSS).
    // A timestamp is added to the message, and the chatbox is scrolled
    // to the bottom, such that the new message is visible.
    //
    // Messages will be automatically tag-escaped, so as to prevent
    // any potential cross-site scripting problems. Additionally,
    // URLs will be automatically linked.
    //
    // ==== Parameters ====
    // * {{{ab}}} refers to whether the user is "a" or "b" in a conversation.
    // For the general case, "you" are "a" and "they" are "b".
    // * {{{chatbox}}} refers to the jQuery-selected chatbox DOM element.
    // * {{{username}}} is the username of the user who sent the message.
    // * {{{time}}} is the time the message was sent in milliseconds since
    // the Unix Epoch. This is //only// defined when messages are being
    // restored from storage. For new messages, the current computer
    // time is automatically used.
    _addMessage: function(ab, chatbox, username, message, time) {
        var last_message = chatbox.find('.imjs-msglog > *:last-child');
        if(last_message.hasClass('imjs-msg-' + ab)) {
            // Last message was from the same person, so let's just add another imjs-msg-*-msg
            var message_container = (last_message.hasClass('imjs-msg-' + ab + '-container')
                ? last_message
                : last_message.find('.imjs-msg-' + ab + '-container'));

            var single_message =
                $('.imjs-tab.imjs-default .imjs-chatbox .imjs-msglog .imjs-msg-' + ab + '-msg')
                .clone().appendTo(message_container);

            single_message.html(single_message.html().replace('{username}', username));
        } else if(!last_message.length || !last_message.hasClass('imjs-msg-' + ab)) {
            var message_group = $('.imjs-tab.imjs-default .imjs-chatbox .imjs-msg-' + ab)
                .clone().appendTo(chatbox.find('.imjs-msglog'));
            message_group.html(message_group.html().replace('{username}', username));

            var single_message = message_group.find('.imjs-msg-' + ab + '-msg');
        }

        // clean up the message
        message = message.toString().replace(/</g, '&lt;').replace(/>/g, '&gt;')
                    .replace(/(^|.*)\*([^*]+)\*(.*|$)/, '$1<strong>$2</strong>$3');

        // autolink URLs
        message = message.replace(
            new RegExp('([A-Za-z][A-Za-z0-9+.-]{1,120}:[A-Za-z0-9/]' +
            '(([A-Za-z0-9$_.+!*,;/?:@&~=-])|%[A-Fa-f0-9]{2}){1,333}' +
            '(#([a-zA-Z0-9][a-zA-Z0-9$_.+!*,;/?:@&~=%-]{0,1000}))?)', 'g'),
            '<a href="$1" target="_blank">$1</a>');

        // insert the message
        single_message.html(single_message.html().replace('{message}', message));

        // set the message time
        var msgtime = single_message.find('.imjs-msg-time');
        if(!time)
            time = new Date();

        if(typeof time != 'string')
            time = dateFormat(time, msgtime.html());

        msgtime.html(time);

        var msglog = chatbox.find('.imjs-msglog');
        msglog[0].scrollTop = msglog[0].scrollHeight;

        return {
            replace_last : !!message_container,
            html: jQuery('<div>').append(
                      message_container
                      ? last_message.clone()
                      : message_group.clone()
                  ).html()
        };
    },

    _store: function(username, msg) {
        if(!msg.html.length) return;
        if(!this.chatstore) this.chatstore = {};

        if(!(username in this.chatstore)) {
            this.chatstore[username] = [];
        } else if(this.chatstore[username].length > 300) {
            // If the chat store gets too long, it becomes slow to load.
            this.chatstore[username].shift();
        }

        if(msg.replace_last)
            this.chatstore[username].pop();

        this.chatstore[username].push(msg.html);

        store.set(this.username + '-chats', this.chatstore);
    },

    // === //private// {{{AjaxIM.}}}**{{{_friendUpdate(friend, status, statusMessage)}}}** ===
    //
    // Called when a friend's status is updated. This function will update all locations
    // where a status icon is displayed (chat tab, friends list), as well as insert
    // a notification, should a chatbox be open.
    //
    // ==== Parameters ====
    // * {{{friend}}} is the username of the friend.
    // * {{{status}}} is the new status code. See {{{AjaxIM.statuses}}} for a list of available
    // codes. //Note: If an invalid status is specified, no action will be taken.//
    // * {{{statusMessage}}} is a message that was, optionally, specified by the user. It will be
    // used should "you" send the user an IM while they are away, or if their status is viewed
    // in another way (such as via the friends list [**not yet implemented**]).
    _friendUpdate: function(friend, status, statusMessage) {
        if(this.chats[friend]) {
            var tab = this.chats[friend].parents('.imjs-tab');
            var tab_class = 'imjs-tab';
            if(tab.data('state') == 'active') tab_class += ' imjs-selected';
            tab_class += ' imjs-' + status;

            tab.attr('class', tab_class);

            // display the status in the chatbox
            var date_stamp =
                $('.imjs-tab.imjs-default .imjs-chatbox .imjs-msglog .imjs-date').clone();

            var date_stamp_time = date_stamp.find('.imjs-msg-time');
            if(date_stamp_time.length)
                date_stamp_time.html(dateFormat(date_stamp_time.html()));

            var date_stamp_date = date_stamp.find('.imjs-date-date').html(
                AjaxIM.l10n[
                    'chat' + status.toUpperCase().slice(0, 1) + status.slice(1)
                ].replace(/%s/g, friend));

            var msglog = this.chats[friend].find('.imjs-msglog');
            date_stamp.appendTo(msglog);
            msglog[0].scrollTop = msglog[0].scrollHeight;
        }

        if (!this.friends[friend]) {
            this.addFriend(friend, [status, statusMessage], 'Friends');
        }
        if(this.friends[friend]) {
            var friend_id = 'imjs-friend-' + md5.hex(friend + this.friends[friend].group);
            $('#' + friend_id).attr('class', 'imjs-friend imjs-' + status);
            $('#' + friend_id).find('.imjs-friend-status')
                              .html(statusMessage)
                              .attr('status', statusMessage);

            if(status == 'offline') {
                $('#' + friend_id + ':visible').slideUp();
                $('#' + friend_id + ':hidden').hide();
            } else if(!$('#' + friend_id + ':visible').length) {
                $('#' + friend_id).slideDown();
            }

            this.friends[friend].status = [status, statusMessage];
            this._updateFriendCount();
        }
    },

    // === //private// {{{AjaxIM.}}}**{{{_notConnected()}}}** ===
    //
    // Puts the user into a visible state of disconnection. Sets the
    // friends list to "not connected" and empties it; disallows new messages
    // to be sent.
    _notConnected: function() {
        $('#imjs-friends')
            .addClass('imjs-not-connected')
            .unbind('click', this.activateTab);
        if($('#imjs-friends').hasClass('imjs-selected'))
            this.activateTab($('#imjs-friends'));
    },
    
    _showReconnect: function() {
        $('#imjs-reconnect').show();
    },

    // === {{{AjaxIM.}}}**{{{send(to, message)}}}** ===
    //
    // Sends a message to another user. The message will be added
    // to the chatbox before it is actually sent, however, if an
    // error occurs during sending, that will be indicated immediately
    // afterward.
    //
    // After sending the message, one of three status codes should be
    // returned as a JSON object, e.g. {{{{r: 'code'}}}}:
    // * {{{ok}}} &mdash; Message was sent successfully.
    // * {{{offline}}} &mdash; The user is offline or unavailable to
    // receive messages.
    // * {{{error}}} &mdash; a problem occurred, unrelated to the user
    // being unavailable.
    //
    // ==== Parameters ====
    // * {{{to}}} is the username of the recipient.
    // * {{{message}}} is the content to be sent.
    send: function(username, body) {
        if(!body) return;
        var self = this;

        if(this.chats[username]) {
            // possibly add a datestamp
            this._store(username, this._addDateStamp(this.chats[username]));
            this._store(username,
                        this._addMessage('a', this.chats[username],
                                         this.username, body));
        }

        $(this).trigger('sendingMessage', [username, body]);

        var event = {type: 'message', from: this.username, to: username, body: body};
        this.sendEvent(event, function(result) {
                if(result._status.sent) {
                    $(self).trigger('sendMessageSuccessful', [username, body]);
                } else if(result.type == 'error') {
                    if(result.error == 'not online')
                        $(self).trigger('sendMessageFailed', ['offline', username, body]);
                    else
                        $(self).trigger('sendMessageFailed', [result.error, username, body]);
                }
        }, function(error) {
                self._notConnected();
                var error = self._addError(
                              self.chats[username],
                              'You are currently not connected or the ' +
                              'server is not available. Please ensure ' +
                              'that you are signed in and try again.');
                self._store(error);
                $(self).trigger('sendMessageFailed',
                                ['not connected', username, body]);
        });
    },

    // === {{{AjaxIM.}}}**{{{status(s, message)}}}** ===
    //
    // Sets the user's status and status message. It is possible to not
    // set a status message by setting it to an empty string. The status
    // will be sent to the server, where upon the server will broadcast
    // the update to all individuals with "you" on their friends list.
    //
    // ==== Parameters ====
    // * {{{s}}} is the status code, as defined by {{{AjaxIM.statuses}}}.
    // * {{{message}}} is the custom status message.
    status: function(value, message) {
        var self = this;

        // update status icon(s)
        if(!~this.statuses.indexOf(value))
            return;

        // check if selected before writing over the class!
        $(this.statuses).each(function() {
            $('#imjs-friends').removeClass('imjs-' + this);
        });
        $('#imjs-friends').addClass('imjs-' + value);

        $(this).trigger('changingStatus', [value, message]);

        if(value == 'offline') {
            self._notConnected();
            self._showReconnect();
            store.set(this.username + '-offline', true);
            self.offline = true;
            $('.imjs-input').attr('disabled', true);

            AjaxIM.post(
                this.actions.signoff,
                {},
                function(result) {
                    if(result.type == 'success')
                        $(self).trigger('changeStatusSuccessful',
                                        [value, null]);
                },
                function(error) {
                    $(self).trigger('changeStatusFailed',
                                    ['not connected', value, null]);
                }
            );
        } else {
           var event = {type: 'status', status: value, message: message};
           this.sendEvent(event, function(result) {
               if(result._status.send) {
                   $(self).trigger('sendMessageSuccessful', [username, body]);
               } else if(result.type == 'error') {
                   if(result.error == 'not online')
                       $(self).trigger('sendMessageFailed', ['offline', username, body]);
                   else
                       $(self).trigger('sendMessageFailed', [result.error, username, body]);
                   }
           }, function(error) {
               self._notConnected();
               var error = self._addError(
                                   self.chats[username],
                                   'You are currently not connected or the ' +
                                   'server is not available. Please ensure ' +
                                   'that you are signed in and try again.');
               self._store(error);
               $(self).trigger('sendMessageFailed',
                                     ['not connected', username, body]);
           });
        }
    },

    // === {{{AjaxIM.}}}**{{{statuses}}}** ===
    //
    // These are the available status codes and their associated identities:
    // * {{{offline}}} (0) &mdash; Only used when signing out/when another
    // user has signed out, as once this status is set, the user is removed
    // from the server and friends will be unable to contact the user.
    // * {{{available}}} (1) &mdash; The user is online and ready to be messaged.
    // * {{{away}}} (2) &mdash; The user is online but is not available. Others
    // may still contact this user, however, the user may not respond. Anyone
    // contacting an away user will receive a notice stating that the user is away,
    // and (if one is set) their custom status message.
    // * {{{invisible}}} (3; **not yet implemented**) &mdash; The user is online,
    // but other users are made unaware, and the user will be represented
    // as being offline. It is still possible to contact this user, and for this
    // user to contact others; no status message or notice will be sent to others
    // messaging this user.
    statuses: ['offline', 'available', 'away'],

    // === {{{AjaxIM.}}}**{{{initTabs()}}}** ===
    //
    // Setup the footer bar and enable tab actions. This function
    // uses {{{jQuery.live}}} to set hooks on any bar tabs created
    // in the future.
    initTabBar: function() {
        var self = this;

        // Set up your standard tab actions
        $(document)
            .on('click', '.imjs-tab', function() {
                return self.activateTab.call(self, $(this));
            });

        $(document)
            .on('click', '.imjs-tab .imjs-close', function() {
                return self.closeTab.call(self, $(this));
            });

        // Set up the friends list actions
        $(document).click(function(e) {
            if(~['imjs-friends'].indexOf(e.target.id) ||
                $(e.target).parents('#imjs-friends').length) {
                return;
            }

            if($('#imjs-friends').data('state') == 'active')
                self.activateTab.call(self, $('#imjs-friends'));
            else if($('#imjs-status').data('state') == 'active')
                self.activateTab.call(self, $('#imjs-status'));
        });

        $('#imjs-friends')
            .data('state', 'minimized')
            .click(function(e) {
                if(!$(this).hasClass('imjs-not-connected') &&
                    e.target.id != 'imjs-friends-panel' &&
                    !$(e.target).parents('#imjs-friends-panel').length)
                    self.activateTab.call(self, $(this));
            })
            .mouseenter(function() {
                if($(this).hasClass('imjs-not-connected')) {
                    $('.imjs-tooltip')
                        .css('display', 'block')
                        .find('p')
                        .html(AjaxIM.l10n.notConnectedTip);

                    var tip_left = $(this).offset().left -
                        $('.imjs-tooltip').outerWidth() +
                        ($(this).outerWidth() / 2);
                    var tip_top = $(this).offset().top -
                        $('.imjs-tooltip').outerHeight(true);

                    $('.imjs-tooltip').css({
                            left: tip_left,
                            top: tip_top
                        });
                }
            })
            .mouseleave(function() {
                if($(this).hasClass('imjs-not-connected')) {
                    $('.imjs-tooltip').css('display', '');
                }
            });

        $('#imjs-friends-panel').css('display', 'none');
    },

    // === {{{AjaxIM.}}}**{{{activateTab()}}}** ===
    //
    // Activate a tab by setting it to the 'active' state and
    // showing any related chatbox. If a chatbox is available
    // for this tab, also focus the input box.
    //
    // //Note:// {{{this}}}, here, refers to the tab DOM element.
    activateTab: function(tab) {
        var chatbox = tab.find('.imjs-chatbox') || false,
            input;

        if(tab.data('state') != 'active') {
            if(tab.attr('id') != 'imjs-friends') {
                $('#imjs-bar > li')
                    .not(tab)
                    .not('#imjs-friends, .imjs-scroll, .imjs-default')
                    .add(tab.attr('id') == 'imjs-status' ? '#imjs-friends' : '')
                    .removeClass('imjs-selected')
                    .each(function() {
                        var self = $(this);
                        if(self.data('state') != 'closed') {
                            self.data('state', 'minimized');
                            var chatbox = self.find('.imjs-chatbox');
                            if(chatbox.length)
                                chatbox.css('display', 'none');
                        }
                    });
            } else {
                $('#imjs-status')
                    .removeClass('imjs-selected')
                    .data('state', 'minimized')
                    .find('.imjs-chatbox')
                    .css('display', 'none');
            }

            if(chatbox && chatbox.css('display') == 'none')
                chatbox.css('display', '');

            // set the tab to active...
            tab.addClass('imjs-selected').data('state', 'active');

            // ...and hide and reset the notification icon
            tab.find('.imjs-notification').css('display', 'none')
                .data('count', 0);

            if(chatbox && (username = chatbox.data('username')))
                store.set(this.username + '-activeTab', username);

            $(this).trigger('tabToggled', ['activated', tab]);
        } else {
            tab.removeClass('imjs-selected').data('state', 'minimized');

            if(chatbox && chatbox.css('display') != 'none')
                chatbox.css('display', 'none');

            store.set(this.username + '-activeTab', '');

            $(this).trigger('tabToggled', ['minimized', tab]);
        }

        if(chatbox) {
            if((input = chatbox.find('.imjs-input')).length &&
                !input.data('height')) {
                input.height(0);
                if(input[0].scrollHeight > input.height() ||
                   input[0].scrollHeight < input.height()) {
                    input.height(input[0].scrollHeight);
                }

                // store the height for resizing later
                if (!input.height()) {
                   input.height(16);
                }
                input.data('height', input.height());
            }

            try {
                var msglog = chatbox.find('.imjs-msglog');
                msglog[0].scrollTop = msglog[0].scrollHeight;
            } catch(e) {}

            try { chatbox.find('.imjs-input').focus(); } catch(e) {}
        }
    },

    // === {{{AjaxIM.}}}**{{{closeTab()}}}** ===
    //
    // Close a tab and hide any related chatbox, such that
    // the chatbox can not be reopened without reinitializing
    // the tab.
    //
    // //Note:// {{{this}}}, here, refers to the tab DOM element.
    closeTab: function(tab) {
        tab = tab.parents('.imjs-tab');
        tab.css('display', 'none')
           .removeClass('imjs-selected')
           .data('state', 'closed');

        delete this.chatstore[tab.find('.imjs-chatbox').data('username')];
        store.set(this.username + '-chats', this.chatstore);

        $(this).trigger('tabToggled', ['closed', tab]);

        this._scrollers();

        return false;
    },

    // === {{{AjaxIM.}}}**{{{addTab(label, action, closable)}}}** ===
    //
    // Adds a tab to the tab bar, with the label {{{label}}}. When
    // clicked, it will call a callback function, {{{action}}}. If
    // {{{action}}} is a string, it is assumed that the string is
    // referring to a chatbox ID.
    //
    // ==== Parameters ====
    // * {{{label}}} is the text that will be displayed on the tab.\\
    // * {{{action}}} is the callback function, if it is a non-chatbox
    // tab, or a string if it //is// a chatbox tab.\\
    // * {{{closable}}} is a boolean value that determines whether or not
    // it is possible for a user to close this tab.
    //
    // //Note:// New tabs are given an automatically generated ID
    // in the format of {{{#imjs-tab-[md5 of label]}}}.
    addTab: function(label, action, closable) {
        var tab = $('.imjs-tab.imjs-default').clone().insertAfter('#imjs-scroll-left');
        tab.removeClass('imjs-default')
            .attr('id', 'imjs-tab-' + md5.hex(label))
            .html(tab.html().replace('{label}', label))
            .data('state', 'minimized');

        var notification = tab.find('.imjs-notification');
        notification.css('display', 'none')
            .data('count', 0)
            .data('default-text', notification.html())
            .html(notification.html().replace('{count}', '0'));

        if(closable === false)
            tab.find('.imjs-close').eq(0).remove();

        if(typeof action != 'string') {
            tab.find('.imjs-chatbox').remove();
            tab.click(action);
        }

        return tab;
    },

    // === {{{AjaxIM.}}}**{{{notification(tab)}}}** ===
    //
    // Displays a notification on a tab. Generally, this is called when
    // a tab is minimized to let the user know that there is an update
    // for them. The way the notification is displayed depends on the
    // theme CSS.
    //
    // ==== Parameters ====
    // * {{{tab}}} is the jQuery-selected tab DOM element.
    notification: function(tab) {
        var notify = tab.find('.imjs-notification');
        var notify_count = notify.data('count') + 1;

        notify.data('count', notify_count)
            .html(notify.data('default-text').replace('{count}', notify_count))
            .css('display', '');
    },

    // === //private// {{{AjaxIM.}}}**{{{_scrollers()}}}** ===
    //
    // Document me!
    _scrollers: function() {
        var needScrollers = false;
        $('#imjs-scroll-left').nextAll('.imjs-tab')
        .filter(function() {
            return $(this).data('state') != 'closed';
        })
        .each(function(i, tab) {
            tab = $(tab).css('display', '');

            var tab_pos = tab.position();
            if(tab_pos.top >= $('#imjs-bar').height() ||
               tab_pos.left < 0 ||
               tab_pos.right > $(document).width()) {
                $('.imjs-scroll').css('display', '');
                tab.css('display', 'none');
                needScrollers = true;
            }
        });

        if(!needScrollers) {
            $('.imjs-scroll').css('display', 'none');
        }

        if($('#imjs-scroll-left').css('display') != 'none' &&
            $('#imjs-scroll-right').position().top >= $('#imjs-bar').height()) {
            $('#imjs-bar li.imjs-tab:visible').slice(-1).css('display', 'none');
        }

        if($('#imjs-bar li.imjs-tab:visible').length) {
            while($('.imjs-selected').css('display') == 'none')
                $('#imjs-scroll-right').click();
        }

        this._scrollerIndex();
    },

    _scrollerIndex: function() {
        var hiddenRight = $('#imjs-bar li.imjs-tab:visible').slice(-1)
            .nextAll('#imjs-bar li.imjs-tab:hidden')
            .not('.imjs-default')
            .filter(function() {
                return $(this).data('state') != 'closed'
            }).length;

        var hiddenLeft = $('#imjs-bar li.imjs-tab:visible').eq(0)
            .prevAll('#imjs-bar li.imjs-tab:hidden')
            .not('.imjs-default')
            .filter(function() {
                return $(this).data('state') != 'closed'
            }).length;

        $('#imjs-scroll-left').html(hiddenLeft);
        $('#imjs-scroll-right').html(hiddenRight);
    },

    unconfirmedEvents: {},
    eventId: 1,

    createEvent: function() {
        var event = {};
        event.id = this.eventId++;;
        this.unconfirmedEvents[event.id] = evt;
    },

    sendEvent: function(event, successFunc, failureFunc) {
        event.id = this.eventId++;
        var evt = $.extend({}, event);
        evt['_status'] = {
            successFunc: successFunc,
            failureFunc: failureFunc
        };
        this.unconfirmedEvents[event.id] = evt;
        if (this.socket) {
            this.socket.emit('server', event);
        } else {
            var self = this;
            var url = null;
            switch (event.type) {
                case 'message':
                    url = this.actions.send;
                    break;
                case 'status':
                   url = this.actions.status;
                   break;
                case 'signoff':
                   url = this.actions.signoff;
                   break;
                default:
                   break;
            }

            AjaxIM.post(url, event,
                function(result) {
                    if (result) {
                        for (var e=0; e < result.length; ++e) {
                            self.dispatchEvent(events[e]);
                        }
                    }
                },
                function(error) {
                    if (self.unconfirmedEvents[event.id]) {
                        event = self.unconfirmedEvents[event.id];
                        event['_status']['sent'] = false;
                        self.dispatchEvent(event);
                    }
                }
            );
        }
    },

    dispatchEvent: function(event) {
       if (event.id && this.unconfirmedEvents[event.id]) {
           event['_status'] = $.extend({}, this.unconfirmedEvents[event.id]['_status'], event['_status']);
           delete this.unconfirmedEvents[event.id];
           if (event['_status']['sent']) {
              event['_status']['successFunc'](event);
           } else {
              event['_status']['failureFunc'](event);
           }
       } else {
           this.triggerEvent(event);
       }
    },
    
    // poor man's Backbone.js Events
    eventHandlers: {},

    /**
     * Add a callback to listen for an event type.
     */
    onEvent: function(eventType, callback) {
        if (!this.eventHandlers[eventType]) {
            this.eventHandlers[eventType] = [];
        }
        this.eventHandlers[eventType].push(callback);
    },

    /**
     * Trigger an event on all interested callbacks.
     */
    triggerEvent: function(event) {
        if (this.eventHandlers[event.type]) {
            for (var e=0; e < this.eventHandlers[event.type].length; ++e) {
                this.eventHandlers[event.type][e].call(this, event);
            }
        }
    }
})

// == Static functions and variables ==
//
// The following functions and variables are available outside of an initialized
// {{{AjaxIM}}} object.

// === {{{AjaxIM.}}}**{{{client}}}** ===
//
// Once {{{AjaxIM.init()}}} is called, this will be set to the active AjaxIM
// object. Only one AjaxIM object instance can exist at a time. This variable
// can and should be accessed directly.
AjaxIM.client = null;

// === {{{AjaxIM.}}}**{{{init(options, actions)}}}** ===
//
// Initialize the AjaxIM client object and engine. Here, you can define your
// options and actions as outlined at the top of this documentation.
//
// ==== Parameters ====
// * {{{options}}} is the hash of custom settings to initialize Ajax IM with.
// * {{{actions}}} is the hash of any custom action URLs.
AjaxIM.init = function(options, actions) {
    if(!AjaxIM.client)
        AjaxIM.client = new AjaxIM(options, actions);

    return AjaxIM.client;
};


// === {{{AjaxIM.}}}**{{{request(url, data, successFunc, failureFunc)}}}** ===
//
// Wrapper around {{{$.jsonp}}}, the JSON-P library for jQuery, and {{{$.ajax}}},
// jQuery's ajax library. Allows either function to be called, automatically,
// depending on the request's URL array (see {{{AjaxIM.actions}}}).
//
// ==== Parameters ====
// {{{url}}} is the URL of the request.
// {{{data}}} are any arguments that go along with the request.
// {{{success}}} is a callback function called when a request has completed
// without issue.
// {{{_ignore_}}} is simply to provide compatability with {{{$.post}}}.
// {{{failure}}} is a callback function called when a request hasn't not
// completed successfully.
AjaxIM.post = function(url, data, successFunc, failureFunc, urlnoop) {
    AjaxIM.request(url, 'POST', data, successFunc, failureFunc, urlnoop);
};

AjaxIM.get = function(url, data, successFunc, failureFunc, urlnoop) {
    AjaxIM.request(url, 'GET', data, successFunc, failureFunc, urlnoop);
};

AjaxIM.request = function(url, type, data, successFunc, failureFunc, noopurl) {
    var errorTypes = ['timeout', 'error', 'notmodified', 'parseerror'];
    if(typeof failureFunc != 'function')
        failureFunc = function(){};

    var jsonp = (url.substring(0, 1) !== '/');
    var success = false;
    data['sessionid'] = cookies.get('sessionid');
    $.ajax({
        url: url,
        data: data,
        dataType: jsonp? 'jsonp': 'json',
        type: type,
        cache: false,
        timeout: 299000
    }).done(function(data) {
       success = true;
       _dbg(JSON.stringify(data));
       successFunc(data);
    }).fail(function(jqXHR, textStatus) {
       _dbg(textStatus);
       failureFunc(textStatus);
    });

    if (jsonp) {
        setTimeout(function() {
            var failfn = function() {
                if (!success) {
                    var textStatus = 'error';
                    _dbg(textStatus);
                    failureFunc(textStatus);
                }
            };
            if (noopurl) {
                var noopfn = function() {
                    var noopdone = false;
                    var event = {type: 'noop'};
                    $.ajax({
                        url: noopurl,
                        data: event,
                        dataType: 'jsonp',
                        type: type,
                        cache: false,
                        timeout: 299000
                    }).done(function(data) {
                        noopdone = true;
                        if (!success) {
                           setTimeout(noopfn, 3000);
                        }
                    }).fail(function(jqXHR, textStatus) {
                        // since JSONP, never called
                    });
                    setTimeout(function() {
                        if (!noopdone) {
                            failfn();
                        }
                    }, 3000);
                };
                noopfn();
            } else {
                failfn();
            }
        }, 3000);
    }

    // This prevents Firefox from spinning indefinitely
    // while it waits for a response.
    /*
    if(url == 'jsonp' && $.browser.mozilla) {
        $.jsonp({
            'url': 'about:',
            timeout: 0
        });
    }
    */
};

// === {{{AjaxIM.}}}**{{{incoming(data)}}}** ===
//
// Never call this directly. It is used as a connecting function between
// client and server for Comet.
//
// //Note:// There are two {{{AjaxIM.incoming()}}} functions. This one is a
// static function called outside of the initialized AjaxIM object; the other
// is only called within the initalized AjaxIM object.
AjaxIM.incoming = function(data) {
    if(!AjaxIM.client)
        return false;

    if(data.length)
        AjaxIM.client._parseMessages(data);
};

AjaxIM.eventID = 1;

// === {{{AjaxIM.}}}**{{{l10n}}}** ===
//
// Text strings used by Ajax IM. Should you want to translate Ajax IM into
// another language, merely change these strings.
//
// {{{%s}}} denotes text that will be automatically replaced when the string is
// used.
AjaxIM._ = function(str) {
    if(str in AjaxIM.l10n) return AjaxIM.l10n[str];
    return str;
};

AjaxIM.l10n = {
    dayNames: [
        "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
        "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
    ],
    monthNames: [
        "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
        "January", "February", "March", "April", "May", "June", "July", "August", "September",
        "October", "November", "December"
    ],

    chatOffline: '%s signed off.',
    chatAvailable: '%s became available.',
    chatAway: '%s went away.',

    notConnected: 'You are currently not connected or the server is not available. ' +
                  'Please ensure that you are signed in and try again.',
    notConnectedTip: 'You are currently not connected.',
    
    defaultAway: 'I\'m away.'
};

AjaxIM.debug = true;
function _dbg(msg) {
    if(AjaxIM.debug && window.console) console.log(msg);
}

function uid(n){
   var chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', nn='';
   for(var c=0; c < n; c++){
      nn += chars.substr(0|Math.random() * chars.length, 1);
   }
   return nn;
}
