/*
 * @package AJAX_Chat
 * @author Sebastian Tschan
 * @copyright (c) Sebastian Tschan
 * @license GNU Affero General Public License
 * @link https://blueimp.net/ajax/
 *
 * The SELFHTML documentation has been used throughout this project:
 * http://selfhtml.org
 *
 * Stylesheet and cookie methods have been inspired by Paul Sowden (A List Apart):
 * http://www.alistapart.com/stories/alternate/
 */

// AJAX Chat client side logic:
var ajaxChat = {

  settingsInitiated: null,
  styleInitiated: null,
  initializeFunction: null,
  finalizeFunction: null,
  loginChannelID: null,
  loginChannelName: null,
  timerRate: null,
  timer: null,
  ajaxURL: null,
  baseURL: null,
  regExpMediaUrl: null,
  dirs: null,
  startChatOnLoad: null,
  chatStarted: null,
  domIDs: null,
  dom: null,
  settings: null,
  nonPersistentSettings: null,
  unusedSettings: null,
  bbCodeTags: null,
  colorCodes: null,
  emoticonCodes: null,
  emoticonFiles: null,
  soundFiles: null,
  sounds: null,
  soundTransform: null,
  sessionName: null,
  cookieExpiration: null,
  cookiePath: null,
  cookieDomain: null,
  cookieSecure: null,
  chatBotName: null,
  chatBotID: null,
  allowUserMessageDelete: null,
  inactiveTimeout: null,
  privateChannelDiff: null,
  privateMessageDiff: null,
  showChannelMessages: null,
  messageTextMaxLength: null,
  socketServerEnabled: null,
  socketServerHost: null,
  socketServerPort: null,
  socketServerChatID: null,
  socket: null,
  socketIsConnected: null,
  socketTimerRate: null,
  socketReconnectTimer: null,
  socketRegistrationID: null,
  userID: null,
  userName: null,
  userRole: null,
  channelID: null,
  channelName: null,
  channelSwitch: null,
  usersList: null,
  userNamesList: null,
  userMenuCounter: null,
  encodedUserName: null,
  userNodeString: null,
  ignoredUserNames: null,
  lastID: null,
  localID: null,
  lang: null,
  langCode: null,
  baseDirection: null,
  originalDocumentTitle: null,
  blinkInterval: null,
  httpRequest: null,
  retryTimer:null,
  retryTimerDelay:null,
  DOMbuffering: null,
  DOMbuffer: null,
  DOMbufferRowClass: 'rowOdd',

  init: function(config, lang, initSettings, initStyle, initialize, initializeFunction, finalizeFunction) {
    this.httpRequest    = new Object();
    this.usersList      = new Array();
    this.userNamesList    = new Array();
    this.userMenuCounter  = 0;
    this.lastID        = 0;
    this.localID      = 0;
    this.lang        = lang;
    this.initConfig(config);
    this.initDirectories();
    if(initSettings) {
      this.initSettings();
    }
    if(initStyle) {
      this.initStyle();
    }
    this.initializeFunction = initializeFunction;
    this.finalizeFunction = finalizeFunction;
    if(initialize) {
      this.setLoadHandler();
    }
  },

  initConfig: function(config) {
    this.loginChannelID      = config['loginChannelID'];
    this.loginChannelName    = config['loginChannelName'];
    this.timerRate        = config['timerRate'];
    this.ajaxURL        = config['ajaxURL'];
    this.baseURL        = config['baseURL'];
    this.regExpMediaUrl      = config['regExpMediaUrl'];
    this.startChatOnLoad    = config['startChatOnLoad'];
    this.domIDs          = config['domIDs'];
    this.settings        = config['settings'];
    this.nonPersistentSettings  = config['nonPersistentSettings'];
    this.bbCodeTags        = config['bbCodeTags'];
    this.colorCodes        = config['colorCodes'];
    this.emoticonCodes      = config['emoticonCodes'];
    this.emoticonFiles      = config['emoticonFiles'];
    this.soundFiles        = config['soundFiles'];
    this.sessionName      = config['sessionName'];
    this.cookieExpiration    = config['cookieExpiration'];
    this.cookiePath        = config['cookiePath'];
    this.cookieDomain      = config['cookieDomain'];
    this.cookieSecure      = config['cookieSecure'];
    this.chatBotName      = config['chatBotName'];
    this.chatBotID        = config['chatBotID'];
    this.allowUserMessageDelete  = config['allowUserMessageDelete'];
    this.inactiveTimeout    = config['inactiveTimeout'];
    this.privateChannelDiff    = config['privateChannelDiff'];
    this.privateMessageDiff    = config['privateMessageDiff'];
    this.showChannelMessages  = config['showChannelMessages'];
    this.messageTextMaxLength  = config['messageTextMaxLength'];
    this.socketServerEnabled  = config['socketServerEnabled'];
    this.socketServerHost    = config['socketServerHost'];
    this.socketServerPort    = config['socketServerPort'];
    this.socketServerChatID    = config['socketServerChatID'];
    this.DOMbuffering      = false;
    this.DOMbuffer        = "";
    this.retryTimerDelay = this.timerRate + (((this.inactiveTimeout*6000) - this.timerRate)/4);
  },

  initDirectories: function() {
    this.dirs = new Object();
    this.dirs['emoticons']   = this.baseURL+'img/emoticons/';
    this.dirs['sounds']    = this.baseURL+'sounds/';
    this.dirs['flash']    = this.baseURL+'flash/';
  },

  initSettings: function() {
    this.settingsInitiated = true;
    this.unusedSettings = new Object();
    var cookie = this.readCookie(this.sessionName + '_settings');
    if(cookie) {
      var settingsArray = cookie.split('&');
      var setting,key,value,number;
      for(var i=0; i<settingsArray.length; i++) {
        setting = settingsArray[i].split('=');
        if(setting.length == 2) {
          key = setting[0];
          value = this.decodeText(setting[1]);
          switch(value) {
            case 'true':
              value = true;
              break;
            case 'false':
              value = false;
              break;
            case 'null':
              value = null;
              break;
            default:
              number = parseFloat(value);
              if(!isNaN(number)) {
                if(parseInt(number) == number) {
                  value = parseInt(number);
                } else {
                  value = number;
                }
              }
          }
          if(this.inArray(this.nonPersistentSettings, key)) {
            // The setting is not used, store it for the persistSettings method:
            this.unusedSettings[key] = value;
          } else {
            this.settings[key] = value;
          }
        }
      }
    }
  },

  persistSettings: function() {
    if(this.settingsInitiated) {
      var settingsArray = new Array();
      for(var property in this.settings) {
        if(this.inArray(this.nonPersistentSettings, property)) {
          if(this.unusedSettings && this.unusedSettings[property]) {
            // Store the unusedSetting previously stored:
            this.settings[property] = this.unusedSettings[property];
          } else {
            continue;
          }
        }
        settingsArray.push(property + '=' + this.encodeText(this.settings[property]));
      }
      this.createCookie(this.sessionName + '_settings', settingsArray.join('&'), this.cookieExpiration);
    }
  },

  getSettings: function() {
    return this.settings;
  },

  getSetting: function(key) {
    // Only return null if setting is null or undefined, not if it is false:
    for(var property in this.settings) {
      if(property == key) {
        return this.settings[key];
      }
    }
    return null;
  },

  setSetting: function(key, value) {
    this.settings[key] = value;
  },

  initializeSettings: function() {
    if(this.settings['persistFontColor'] && this.settings['fontColor']) {
      // Set the inputField font color to the font color:
      if(this.dom['inputField']) {
        this.dom['inputField'].style.color = this.settings['fontColor'];
      }
    }
  },

  initialize: function() {
    this.setUnloadHandler();
    this.initializeDocumentNodes();
    this.loadPageAttributes();
    this.initEmoticons();
    this.initColorCodes();
    this.initializeSettings();
    this.setSelectedStyle();
    this.customInitialize();
    //preload the Alert icon (it can't display if there's no connection unless it's cached!)
    this.setStatus('Alert');
    if(typeof this.initializeFunction == 'function') {
      this.initializeFunction();
    }
    if(!this.isCookieEnabled()) {
      this.addChatBotMessageToChatList('/error CookiesRequired');
    } else {
      if(this.startChatOnLoad) {
        this.startChat();
      } else {
        this.setStartChatHandler();
        this.requestTeaserContent();
      }
    }
  },

  requestTeaserContent: function() {
    var params = '&view=teaser';
    params += '&getInfos=' + this.encodeText('userID,userName,userRole');
    if(!isNaN(parseInt(this.loginChannelID))) {
      params += '&channelID='+this.loginChannelID;
    } else if(this.loginChannelName !== null) {
      params += '&channelName='+this.encodeText(this.loginChannelName);
    }
    this.updateChat(params);
  },

  setStartChatHandler: function() {
    if(this.dom['inputField']) {
      this.dom['inputField'].onfocus = function() {
        ajaxChat.startChat();
        // Reset the onfocus event on first call:
        ajaxChat.dom['inputField'].onfocus = '';
      }
    }
  },

  startChat: function() {
    this.chatStarted = true;
    if(this.dom['inputField'] && this.settings['autoFocus']) {
      this.dom['inputField'].focus();
    }
    this.loadFlashInterface();
    this.startChatUpdate();
  },

  loadPageAttributes: function() {
    var htmlTag      = document.getElementsByTagName('html')[0];
    this.langCode    = htmlTag.getAttribute('lang')  ? htmlTag.getAttribute('lang')  : 'en';
    this.baseDirection  = htmlTag.getAttribute('dir')  ? htmlTag.getAttribute('dir')  : 'ltr';
  },

  setLoadHandler: function() {
    // Make sure initialize() is called on page load:
      var onload = window.onload;
    if(typeof onload != 'function') {
      window.onload = function() {
        ajaxChat.initialize();
      }
    } else {
      window.onload = function() {
        onload();
        ajaxChat.initialize();
      }
    }
  },

  setUnloadHandler: function() {
    // Make sure finalize() is called on page unload:
      var onunload = window.onunload;
    if(typeof onunload != 'function') {
      window.onunload = function() {
        ajaxChat.finalize();
      }
    } else {
      window.onunload = function() {
        ajaxChat.finalize();
        onunload();
      }
    }
  },

  updateDOM: function(id, str, prepend, overwrite) {
    var domNode = this.dom[id] ? this.dom[id] : document.getElementById(id);
    if(!domNode) {
      return;
    }
    try {
      // Test for validity before adding the string to the DOM:
      domNode.cloneNode(false).innerHTML = str;
      if(overwrite) {
        domNode.innerHTML = str;
      } else if(prepend) {
        domNode.innerHTML = str + domNode.innerHTML;
      } else {
        domNode.innerHTML += str;
      }
    } catch(e) {
      this.addChatBotMessageToChatList('/error DOMSyntax '+id);
      this.updateChatlistView();
    }
  },

  initializeDocumentNodes: function() {
    this.dom = new Object();
    for(var key in this.domIDs) {
      this.dom[key] = document.getElementById(this.domIDs[key]);
    }
  },

  initEmoticons: function() {
    this.DOMbuffer = "";
    for(var i=0; i<this.emoticonCodes.length; i++) {
      // Replace specials characters in emoticon codes:
      this.emoticonCodes[i] = this.encodeSpecialChars(this.emoticonCodes[i]);
      this.DOMbuffer = this.DOMbuffer
            + '<a href="javascript:ajaxChat.insertText(\''
            + this.scriptLinkEncode(this.emoticonCodes[i])
            + '\');"><img src="'
            + this.dirs['emoticons']
            + this.emoticonFiles[i]
            + '" alt="'
            + this.emoticonCodes[i]
            + '" title="'
            + this.emoticonCodes[i]
            + '"/></a>';
      }
    if(this.dom['emoticonsContainer']) {
       this.updateDOM('emoticonsContainer', this.DOMbuffer);
     }
     this.DOMbuffer = "";
  },

  initColorCodes: function() {
    if(this.dom['colorCodesContainer']) {
      this.DOMbuffer = "";
      for(var i=0; i<this.colorCodes.length; i++) {
        this.DOMbuffer = this.DOMbuffer
          + '<a href="javascript:ajaxChat.setFontColor(\''
          + this.colorCodes[i]
          + '\');" style="background-color:'
          + this.colorCodes[i]
          + ';" title="'
          + this.colorCodes[i]
          + '"></a>'
          + "\n"
      }
      this.updateDOM('colorCodesContainer', this.DOMbuffer);
       this.DOMbuffer = "";
    }
  },

  setStatus: function(currentStatus) {
    //Make sure the status container div exists before changing its class.
    if (document.getElementById('statusIconContainer') != null ) {
      //currentStatus options are: Off for green, On for orange, and Alert for red.
      document.getElementById('statusIconContainer').className = 'statusContainer' + currentStatus;
    }
  },

  startChatUpdate: function() {
    // Start the chat update and retrieve current user and channel info and set the login channel:
    var infos = 'userID,userName,userRole,channelID,channelName';
    if(this.socketServerEnabled) {
      infos += ',socketRegistrationID';
    }
    var params = '&getInfos=' + this.encodeText(infos);
    if(!isNaN(parseInt(this.loginChannelID))) {
      params += '&channelID='+this.loginChannelID;
    } else if(this.loginChannelName !== null) {
      params += '&channelName='+this.encodeText(this.loginChannelName);
    }
    this.updateChat(params);
  },

  updateChat: function(paramString) {
    var requestUrl = this.ajaxURL
            + '&lastID='
            + this.lastID;
    if(paramString) {
      requestUrl += paramString;
    }
    this.makeRequest(requestUrl,'GET',null);
  },

  loadFlashInterface: function() {
    if(this.dom['flashInterfaceContainer']) {
      this.updateDOM(
        'flashInterfaceContainer',
        '<object id="ajaxChatFlashInterface" style="position:absolute; left:-100px;" '
        +'classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" '
        +'codebase="'
        + window.location.protocol
        +'//download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0" '
        +'height="1" width="1">'
        +'<param name="flashvars" value="bridgeName=ajaxChat"/>'
        +'<param name="src" value="'+this.dirs['flash']+'FABridge.swf"/>'
        +'<embed name="ajaxChatFlashInterface" pluginspage="'
        + window.location.protocol
        +'//www.macromedia.com/go/getflashplayer" '
        +'src="'+this.dirs['flash']+'FABridge.swf" height="1" width="1" flashvars="bridgeName=ajaxChat"/>'
        +'</object>'
      );
      FABridge.addInitializationCallback('ajaxChat', this.flashInterfaceLoadCompleteHandler);
    }
  },

  flashInterfaceLoadCompleteHandler: function() {
    ajaxChat.initializeFlashInterface();
  },

  initializeFlashInterface: function() {
    if(this.socketServerEnabled) {
      this.socketTimerRate = (this.inactiveTimeout-1)*60*1000;
      this.socketConnect();
    }
    this.loadSounds();
    this.initializeCustomFlashInterface();
  },

  socketConnect: function() {
    if(!this.socketIsConnected) {
      try {
        if(!this.socket && FABridge.ajaxChat) {
          this.socket = FABridge.ajaxChat.create('flash.net.XMLSocket');
          this.socket.addEventListener('connect', this.socketConnectHandler);
          this.socket.addEventListener('close', this.socketCloseHandler);
          this.socket.addEventListener('data', this.socketDataHandler);
          this.socket.addEventListener('ioError', this.socketIOErrorHandler);
          this.socket.addEventListener('securityError', this.socketSecurityErrorHandler);
        }
        this.socket.connect(this.socketServerHost, this.socketServerPort);
      } catch(e) {
        //alert(e);
      }
    }
    clearTimeout(this.socketReconnectTimer);
    this.socketReconnectTimer = null;
  },

  socketConnectHandler: function(event) {
    ajaxChat.socketIsConnected = true;
    // setTimeout is needed to avoid calling the flash interface recursively:
    setTimeout('ajaxChat.socketRegister()', 0);
  },

  socketCloseHandler: function(event) {
    ajaxChat.socketIsConnected = false;
    if(ajaxChat.socket) {
      clearTimeout(ajaxChat.timer);
      ajaxChat.updateChat(null);
    }
  },

  socketDataHandler: function(event) {
    ajaxChat.socketUpdate(event.getData());
  },

  socketIOErrorHandler: function(event) {
    // setTimeout is needed to avoid calling the flash interface recursively (e.g. sound on new messages):
    setTimeout('ajaxChat.addChatBotMessageToChatList(\'/error SocketIO\')', 0);
    setTimeout('ajaxChat.updateChatlistView()', 1);
  },

  socketSecurityErrorHandler: function(event) {
    // setTimeout is needed to avoid calling the flash interface recursively (e.g. sound on new messages):
    // setTimeout('ajaxChat.addChatBotMessageToChatList(\'/error SocketSecurity\')', 0);
    setTimeout('ajaxChat.updateChatlistView()', 1);
  },

  socketRegister: function() {
    if(this.socket && this.socketIsConnected) {
      try {
        this.socket.send(
          '<register chatID="'
          +this.socketServerChatID
          +'" userID="'
          +this.userID
          +'" regID="'
          +this.socketRegistrationID
          +'"/>'
        );
      } catch(e) {
        //alert(e);
      }
    }
  },

  loadXML: function(str) {
    if(!arguments.callee.parser) {
      try {
        // DOMParser native implementation (Mozilla, Opera):
        arguments.callee.parser = new DOMParser();
      } catch(e) {
        var customDOMParser = function() {}
        if(navigator.appName == 'Microsoft Internet Explorer') {
          // IE implementation:
          customDOMParser.prototype.parseFromString = function(str, contentType) {
            if(!arguments.callee.XMLDOM) {
              arguments.callee.XMLDOM = new ActiveXObject('Microsoft.XMLDOM');
            }
            arguments.callee.XMLDOM.loadXML(str);
            return arguments.callee.XMLDOM;
          }
        } else {
          // Safari, Konqueror:
          customDOMParser.prototype.parseFromString = function(str, contentType) {
            if(!arguments.callee.httpRequest) {
              arguments.callee.httpRequest = new XMLHttpRequest();
            }
            arguments.callee.httpRequest.open(
              'GET',
              'data:text/xml;charset=utf-8,'+encodeURIComponent(str),
              false
            );
            arguments.callee.httpRequest.send(null);
            return arguments.callee.httpRequest.responseXML;
          }
        }
        arguments.callee.parser = new customDOMParser();
      }
    }
    return arguments.callee.parser.parseFromString(str, 'text/xml');
  },

  socketUpdate: function(data) {
    var xmlDoc = this.loadXML(data);
    if(xmlDoc) {
      this.handleOnlineUsers(xmlDoc.getElementsByTagName('user'));
      // If the root node has the attribute "mode" set to "1" it is a channel message:
      if((this.showChannelMessages || xmlDoc.firstChild.getAttribute('mode') != '1') && !this.channelSwitch) {
        var channelID = xmlDoc.firstChild.getAttribute('channelID');
        if(channelID == this.channelID ||
          parseInt(channelID) == parseInt(this.userID)+this.privateMessageDiff
          ) {
          this.handleChatMessages(xmlDoc.getElementsByTagName('message'));
        }
      }
    }
  },

  setAudioVolume: function(volume) {
    volume = parseFloat(volume);
    if(!isNaN(volume)) {
      if(volume < 0) {
        volume = 0.0;
      } else if(volume > 1) {
        volume = 1.0;
      }
      this.settings['audioVolume'] = volume;
      try {
        if(!this.soundTransform) {
          this.soundTransform = FABridge.ajaxChat.create('flash.media.SoundTransform');
        }
        this.soundTransform.setVolume(volume);
      } catch(e) {
        //alert(e);
      }
    }
  },

  loadSounds: function() {
    try {
      this.setAudioVolume(this.settings['audioVolume']);
      this.sounds = new Object();
      var sound,urlRequest;
      for(var key in this.soundFiles) {
        sound = FABridge.ajaxChat.create('flash.media.Sound');
        sound.addEventListener('complete', this.soundLoadCompleteHandler);
        sound.addEventListener('ioError', this.soundIOErrorHandler);
        urlRequest = FABridge.ajaxChat.create('flash.net.URLRequest');
        urlRequest.setUrl(this.dirs['sounds']+this.soundFiles[key]);
        sound.load(urlRequest);
      }
    } catch(e) {
      alert(e);
    }
  },

  soundLoadCompleteHandler: function(event) {
    var sound = event.getTarget();
    for(var key in ajaxChat.soundFiles) {
      // Get the sound key by matching the sound URL with the sound filename:
      if((new RegExp(ajaxChat.soundFiles[key])).test(sound.getUrl())) {
        // Add the loaded sound to the sounds list:
        ajaxChat.sounds[key] = sound;
      }
    }
  },

  soundIOErrorHandler: function(event) {
    // setTimeout is needed to avoid calling the flash interface recursively (e.g. sound on new messages):
    setTimeout('ajaxChat.addChatBotMessageToChatList(\'/error SoundIO\')', 0);
    setTimeout('ajaxChat.updateChatlistView()', 1);
  },

  soundPlayCompleteHandler: function(event) {
    // soundChannel event 'soundComplete'
  },

  playSound: function(soundID) {
    if(this.sounds && this.sounds[soundID]) {
      try {
        // play() parameters are
        // startTime:Number (default = 0),
        // loops:int (default = 0) and
        // sndTransform:SoundTransform  (default = null)
        return this.sounds[soundID].play(0, 0, this.soundTransform);
      } catch(e) {
        //alert(e);
      }
    }
    return null;
  },

  playSoundOnNewMessage: function(dateObject, userID, userName, userRole, messageID, messageText, channelID, ip) {
    if(this.settings['audio'] && this.sounds && this.lastID && !this.channelSwitch) {
      switch(userID) {
        case this.chatBotID:
          var messageParts = messageText.split(' ', 1);
          switch(messageParts[0]) {
            case '/login':
            case '/channelEnter':
              this.playSound(this.settings['soundEnter']);
              break;
            case '/logout':
            case '/channelLeave':
            case '/kick':
              this.playSound(this.settings['soundLeave']);
              break;
            case '/error':
              this.playSound(this.settings['soundError']);
              break;
            default:
              this.playSound(this.settings['soundChatBot']);
          }
          break;
        case this.userID:
          this.playSound(this.settings['soundSend']);
          break;
        default:
          this.playSound(this.settings['soundReceive']);
          break;
      }
    }
  },

  fillSoundSelection: function(selectionID, selectedSound) {
    var selection = document.getElementById(selectionID);
    // Skip the first, empty selection:
    var i = 1;
    for(var key in this.soundFiles) {
      selection.options[i] = new Option(key, key);
      if(key == selectedSound){
        selection.options[i].selected = true;
      }
      i++;
    }
  },

  getHttpRequest: function(identifier) {
    if(!this.httpRequest[identifier]) {
      if (window.XMLHttpRequest) {
        this.httpRequest[identifier] = new XMLHttpRequest();
        if (this.httpRequest[identifier].overrideMimeType) {
          this.httpRequest[identifier].overrideMimeType('text/xml');
        }
      } else if (window.ActiveXObject) {
        try {
          this.httpRequest[identifier] = new ActiveXObject("Msxml2.XMLHTTP");
        } catch (e) {
          try {
            this.httpRequest[identifier] = new ActiveXObject("Microsoft.XMLHTTP");
          } catch (e) {
          }
        }
      }
    }
    return this.httpRequest[identifier];
  },

  makeRequest: function(url, method, data) {
    ajaxChat.setStatus('On');
    ajaxChat.retryTimer = setTimeout("ajaxChat.updateChat(null); ajaxChat.setStatus('Alert');", this.retryTimerDelay);
    try {
      var identifier;
      if(data) {
        // Create up to 50 HTTPRequest objects:
        if(!arguments.callee.identifier || arguments.callee.identifier > 50) {
          arguments.callee.identifier = 1;
        } else {
          arguments.callee.identifier++;
        }
        identifier = arguments.callee.identifier;
      } else {
        identifier = 0;
      }
      this.getHttpRequest(identifier).open(method, url, true);
      this.getHttpRequest(identifier).onreadystatechange = function() {
        try {
          ajaxChat.handleResponse(identifier);
        } catch(e) {
          try {
            clearTimeout(ajaxChat.timer);
          } catch(e) {
            //alert(e);
          }
          try {
            if(data) {
              ajaxChat.addChatBotMessageToChatList('/error ConnectionTimeout');
              ajaxChat.setStatus('Alert');
              ajaxChat.updateChatlistView();
            }
          } catch(e) {
            //alert(e);
          }
          try {
            ajaxChat.timer = setTimeout('ajaxChat.updateChat(null);', ajaxChat.timerRate);
          } catch(e) {
            //alert(e);
          }
        }
      };
      if(method == 'POST') {
        this.getHttpRequest(identifier).setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
      }
      this.getHttpRequest(identifier).send(data);
    } catch(e) {
      clearTimeout(this.timer);
      if(data) {
        this.addChatBotMessageToChatList('/error ConnectionTimeout');
        ajaxChat.setStatus('Alert');
        this.updateChatlistView();
      }
      this.timer = setTimeout('ajaxChat.updateChat(null);', this.timerRate);
    }
  },

  handleResponse: function(identifier) {
    if (this.getHttpRequest(identifier).readyState == 4) {
      if (this.getHttpRequest(identifier).status == 200) {
        clearTimeout(ajaxChat.retryTimer);
        var xmlDoc = this.getHttpRequest(identifier).responseXML;
        ajaxChat.setStatus('Off');
      } else {
        // Connection status 0 can be ignored.
        if (this.getHttpRequest(identifier).status == 0) {
          ajaxChat.setStatus('On');
          this.updateChatlistView();
          return false;
        } else {
          this.addChatBotMessageToChatList('/error ConnectionStatus '+this.getHttpRequest(identifier).status);
          ajaxChat.setStatus('Alert');
          this.updateChatlistView();
          return false;
        }
      }
    }
    if(!xmlDoc) {
      return false;
    }
    this.handleXML(xmlDoc);
    return true;
  },

  handleXML: function(xmlDoc) {
    this.handleInfoMessages(xmlDoc.getElementsByTagName('info'));
    this.handleOnlineUsers(xmlDoc.getElementsByTagName('user'));
    this.handleChatMessages(xmlDoc.getElementsByTagName('message'));
    this.channelSwitch = null;
    this.setChatUpdateTimer();
  },

  setChatUpdateTimer: function() {
    clearTimeout(this.timer);
    if(this.chatStarted) {
      var timeout;
      if(this.socketIsConnected) {
        timeout = this.socketTimerRate;
      } else {
        timeout = this.timerRate;
        if(this.socketServerEnabled && !this.socketReconnectTimer) {
          // If the socket connection fails try to reconnect once in a minute:
          this.socketReconnectTimer = setTimeout('ajaxChat.socketConnect();', 60000);
        }
      }
      this.timer = setTimeout('ajaxChat.updateChat(null);', timeout);
    }
  },

  handleInfoMessages: function(infoNodes) {
    var infoType, infoData;
    for(var i=0; i<infoNodes.length; i++) {
      infoType = infoNodes[i].getAttribute('type');
      infoData = infoNodes[i].firstChild ? infoNodes[i].firstChild.nodeValue : '';
      this.handleInfoMessage(infoType, infoData);
    }
  },

  handleInfoMessage: function(infoType, infoData) {
    switch(infoType) {
      case 'channelSwitch':
        this.clearChatList();
        this.clearOnlineUsersList();
        this.setSelectedChannel(infoData);
        this.channelName = infoData;
        this.channelSwitch = true;
        break;
      case 'channelName':
        this.setSelectedChannel(infoData);
        this.channelName = infoData;
        break;
      case 'channelID':
        this.channelID = infoData;
        break;
      case 'userID':
        this.userID = infoData;
        break;
      case 'userName':
        this.userName = infoData;
        this.encodedUserName = this.scriptLinkEncode(this.userName);
        this.userNodeString = null;
        break;
      case 'userRole':
        this.userRole = infoData;
        break;
      case 'logout':
        this.handleLogout(infoData);
        return;
      case 'socketRegistrationID':
        this.socketRegistrationID = infoData;
        this.socketRegister();
      default:
        this.handleCustomInfoMessage(infoType, infoData);
    }
  },

  handleOnlineUsers: function(userNodes) {
    if(userNodes.length) {
      var index,userID,userName,userRole;
      var onlineUsers = new Array();
      for(var i=0; i<userNodes.length; i++) {
        userID = userNodes[i].getAttribute('userID');
        userName = userNodes[i].firstChild ? userNodes[i].firstChild.nodeValue : '';
        userRole = userNodes[i].getAttribute('userRole');
        onlineUsers.push(userID);
        index = this.arraySearch(userID, this.usersList);
        if(index === false) {
          this.addUserToOnlineList(
            userID,
            userName,
            userRole
          );
        } else if(this.userNamesList[index] != userName) {
          this.removeUserFromOnlineList(userID, index);
          this.addUserToOnlineList(
            userID,
            userName,
            userRole
          );
        }
      }
      // Clear the offline users from the online users list:
      for(var i=0; i<this.usersList.length; i++) {
        if(!this.inArray(onlineUsers, this.usersList[i])) {
          this.removeUserFromOnlineList(this.usersList[i], i);
        }
      }
      this.setOnlineListRowClasses();
    }
  },

  handleChatMessages: function(messageNodes) {
    if(messageNodes.length) {
      var userNode,userName,textNode,messageText;
      for(var i=0; i<messageNodes.length; i++) {
        this.DOMbuffering = true;
        userNode = messageNodes[i].getElementsByTagName('username')[0];
        userName = userNode.firstChild ? userNode.firstChild.nodeValue : '';
        textNode = messageNodes[i].getElementsByTagName('text')[0];
        messageText = textNode.firstChild ? textNode.firstChild.nodeValue : '';
        if (i == (messageNodes.length - 1)) {this.DOMbuffering = false;}
        this.addMessageToChatList(
            new Date(messageNodes[i].getAttribute('dateTime')),
            messageNodes[i].getAttribute('userID'),
            userName,
            messageNodes[i].getAttribute('userRole'),
            messageNodes[i].getAttribute('id'),
            messageText,
            messageNodes[i].getAttribute('channelID'),
            messageNodes[i].getAttribute('ip')
        );
      }
      this.DOMbuffering = false;
      this.updateChatlistView();
      this.lastID = messageNodes[messageNodes.length-1].getAttribute('id');
    }
  },

  setSelectedChannel: function(channel) {
    if(this.dom['channelSelection']) {
      // Replace the entities in the channel name with their character equivalent:
      channel = this.decodeSpecialChars(channel);
      var channelSelected = false;
      for(var i=0; i<this.dom['channelSelection'].options.length; i++) {
        if(this.dom['channelSelection'].options[i].value == channel) {
          this.dom['channelSelection'].options[i].selected = true;
          channelSelected = true;
          break;
        }
      }
      // The given channel is not in the list, add it:
      if(!channelSelected) {
        var option = document.createElement('option');
        var text = document.createTextNode(channel);
        option.appendChild(text);
        option.setAttribute('value', channel);
        option.setAttribute('selected', 'selected');
        this.dom['channelSelection'].appendChild(option);
      }
    }
  },

  removeUserFromOnlineList: function(userID, index) {
    this.usersList.splice(index, 1);
    this.userNamesList.splice(index, 1);
    if(this.dom['onlineList']) {
      this.dom['onlineList'].removeChild(this.getUserNode(userID));
    }
  },

  addUserToOnlineList: function(userID, userName, userRole) {
    this.usersList.push(userID);
    this.userNamesList.push(userName);
    if(this.dom['onlineList']) {
      this.updateDOM(
        'onlineList',
        this.getUserNodeString(userID, userName, userRole),
        (this.userID == userID)
      );
    }
  },

  getUserNodeString: function(userID, userName, userRole) {
    if(this.userNodeString && userID == this.userID) {
      return this.userNodeString;
    } else {
      var encodedUserName = this.scriptLinkEncode(userName);
      var str  = '<div id="'
          + this.getUserDocumentID(userID)
          + '"><a href="javascript:ajaxChat.toggleUserMenu(\''
          + this.getUserMenuDocumentID(userID)
          + '\', \''
          + encodedUserName
          + '\', '
          + userID
          + ');" class="'
          + this.getRoleClass(userRole)
          + '" title="'
          + this.lang['toggleUserMenu'].replace(/%s/, userName)
          + '">'
          + userName
          + '</a>'
          + '<ul class="userMenu" id="'
          + this.getUserMenuDocumentID(userID)
          + '"'
          + ((userID == this.userID) ?
            '>'+this.getUserNodeStringItems(encodedUserName, userID, false) :
            ' style="display:none;">')
          + '</ul>'
          +'</div>';
      if(userID == this.userID) {
        this.userNodeString = str;
      }
      return str;
    }
  },

  toggleUserMenu: function(menuID, userName, userID) {
    // If the menu is empty, fill it with user node menu items before toggling it.
    var isInline = false;
    if (menuID.indexOf('ium') >= 0 ) {
      isInline = true;
    }
    if(!document.getElementById(menuID).firstChild) {
      this.updateDOM(
        menuID,
        this.getUserNodeStringItems(
          this.encodeText(this.addSlashes(this.getScriptLinkValue(userName))),
          userID,
          isInline
        ),
        false,
        true
      )
    }
    this.showHide(menuID);
    this.dom['chatList'].scrollTop = this.dom['chatList'].scrollHeight;
  },

  getUserNodeStringItems: function(encodedUserName, userID, isInline) {
    var menu;
    if(encodedUserName != this.encodedUserName) {
      menu   = '<li><a href="javascript:ajaxChat.insertMessageWrapper(\'/msg '
          + encodedUserName
          + ' \');">'
          + this.lang['userMenuSendPrivateMessage']
          + '</a></li>'
          + '<li><a href="javascript:ajaxChat.insertMessageWrapper(\'/describe '
          + encodedUserName
          + ' \');">'
          + this.lang['userMenuDescribe']
          + '</a></li>'
          + '<li><a href="javascript:ajaxChat.sendMessageWrapper(\'/query '
          + encodedUserName
          + '\');">'
          + this.lang['userMenuOpenPrivateChannel']
          + '</a></li>'
          + '<li><a href="javascript:ajaxChat.sendMessageWrapper(\'/query\');">'
          + this.lang['userMenuClosePrivateChannel']
          + '</a></li>'
          + '<li><a href="javascript:ajaxChat.sendMessageWrapper(\'/ignore '
          + encodedUserName
          + '\');">'
          + this.lang['userMenuIgnore']
          + '</a></li>';
      if (isInline) {
        menu  += '<li><a href="javascript:ajaxChat.sendMessageWrapper(\'/invite '
          + encodedUserName
          + '\');">'
          + this.lang['userMenuInvite']
          + '</a></li>'
          + '<li><a href="javascript:ajaxChat.sendMessageWrapper(\'/uninvite '
          + encodedUserName
          + '\');">'
          + this.lang['userMenuUninvite']
          + '</a></li>'
          + '<li><a href="javascript:ajaxChat.sendMessageWrapper(\'/whereis '
          + encodedUserName
          + '\');">'
          + this.lang['userMenuWhereis']
          + '</a></li>';
      }
      if(this.userRole == 2 || this.userRole == 3) {
        menu  += '<li><a href="javascript:ajaxChat.insertMessageWrapper(\'/kick '
            + encodedUserName
            + ' \');">'
            + this.lang['userMenuKick']
            + '</a></li>'
            + '<li><a href="javascript:ajaxChat.sendMessageWrapper(\'/whois '
            + encodedUserName
            + '\');">'
            + this.lang['userMenuWhois']
            + '</a></li>';
      }
    } else {
      menu   = '<li><a href="javascript:ajaxChat.sendMessageWrapper(\'/quit\');">'
          + this.lang['userMenuLogout']
          + '</a></li>'
          + '<li><a href="javascript:ajaxChat.sendMessageWrapper(\'/who\');">'
          + this.lang['userMenuWho']
          + '</a></li>'
          + '<li><a href="javascript:ajaxChat.sendMessageWrapper(\'/ignore\');">'
          + this.lang['userMenuIgnoreList']
          + '</a></li>'
          + '<li><a href="javascript:ajaxChat.sendMessageWrapper(\'/list\');">'
          + this.lang['userMenuList']
          + '</a></li>'
          + '<li><a href="javascript:ajaxChat.insertMessageWrapper(\'/action \');">'
          + this.lang['userMenuAction']
          + '</a></li>'
          + '<li><a href="javascript:ajaxChat.insertMessageWrapper(\'/roll \');">'
          + this.lang['userMenuRoll']
          + '</a></li>'
          + '<li><a href="javascript:ajaxChat.insertMessageWrapper(\'/nick \');">'
          + this.lang['userMenuNick']
          + '</a></li>';
      if(this.userRole == 1 || this.userRole == 2 || this.userRole == 3) {
        menu  += '<li><a href="javascript:ajaxChat.sendMessageWrapper(\'/join\');">'
            + this.lang['userMenuEnterPrivateRoom']
            + '</a></li>';
        if(this.userRole == 2 || this.userRole == 3) {
          menu  += '<li><a href="javascript:ajaxChat.sendMessageWrapper(\'/bans\');">'
              + this.lang['userMenuBans']
              + '</a></li>';
        }
      }
    }
    menu += this.getCustomUserMenuItems(encodedUserName, userID);
    return menu;
  },

  setOnlineListRowClasses: function() {
    if(this.dom['onlineList']) {
      var node = this.dom['onlineList'].firstChild;
      var rowEven = false;
      while(node) {
        this.setClass(node, (rowEven ? 'rowEven' : 'rowOdd'))
        node = node.nextSibling;
        rowEven = !rowEven;
      }
    }
  },

  clearChatList: function() {
    while(this.dom['chatList'].hasChildNodes()) {
      this.dom['chatList'].removeChild(this.dom['chatList'].firstChild);
    }
  },

  clearOnlineUsersList: function() {
    this.usersList = new Array();
    this.userNamesList = new Array();
    if(this.dom['onlineList']) {
      while(this.dom['onlineList'].hasChildNodes()) {
        this.dom['onlineList'].removeChild(this.dom['onlineList'].firstChild);
      }
    }
  },

  getEncodedChatBotName: function() {
    if(typeof arguments.callee.encodedChatBotName == 'undefined') {
      arguments.callee.encodedChatBotName = this.encodeSpecialChars(this.chatBotName);
    }
    return arguments.callee.encodedChatBotName;
  },

  addChatBotMessageToChatList: function(messageText) {
    this.addMessageToChatList(
      new Date(),
      this.chatBotID,
      this.getEncodedChatBotName(),
      4,
      null,
      messageText,
      null
    );
  },

  addMessageToChatList: function(dateObject, userID, userName, userRole, messageID, messageText, channelID, ip) {
    // Prevent adding the same message twice:
    if(this.getMessageNode(messageID)) {
      return;
    }
    if(!this.onNewMessage(dateObject, userID, userName, userRole, messageID, messageText, channelID, ip)) {
      return;
    }
    this.DOMbufferRowClass = this.DOMbufferRowClass == 'rowEven' ? 'rowOdd' : 'rowEven';
    this.DOMbuffer = this.DOMbuffer +
      this.getChatListMessageString(
        dateObject, userID, userName, userRole, messageID, messageText, channelID, ip
      );
    if(!this.DOMbuffering){
       this.updateDOM('chatList', this.DOMbuffer)
       this.DOMbuffer = "";
     }
  },

  getChatListMessageString: function(dateObject, userID, userName, userRole, messageID, messageText, channelID, ip) {
    var rowClass = this.DOMbufferRowClass;
    var userClass = this.getRoleClass(userRole);
    var colon;
    if(messageText.indexOf('/action') == 0 || messageText.indexOf('/me') == 0 || messageText.indexOf('/privaction') == 0) {
      userClass += ' action';
      colon = ' ';
    } else {
      colon = ': ';
    }
    var dateTime = this.settings['dateFormat'] ? '<span class="dateTime">'
            + this.formatDate(this.settings['dateFormat'], dateObject) + '</span> ' : '';
    return  '<div id="'
        + this.getMessageDocumentID(messageID)
        + '" class="'
        + rowClass
        + '">'
        + this.getDeletionLink(messageID, userID, userRole, channelID)
        + dateTime
        + '<span class="'
        + userClass
        + '"'
        + this.getChatListUserNameTitle(userID, userName, userRole, ip)
        + ' dir="'
        + this.baseDirection
        + '" onclick="ajaxChat.insertText(this.firstChild.nodeValue);">'
        + userName
        + '</span>'
        + colon
        + this.replaceText(messageText)
        + '</div>';
  },

  getChatListUserNameTitle: function(userID, userName, userRole, ip) {
    return (ip != null) ? ' title="IP: ' + ip + '"' : '';
  },

  getMessageDocumentID: function(messageID) {
    return ((messageID === null) ? 'ajaxChat_lm_'+(this.localID++) : 'ajaxChat_m_'+messageID);
  },

  getMessageNode: function(messageID) {
    return ((messageID === null) ? null : document.getElementById(this.getMessageDocumentID(messageID)));
  },

  getUserDocumentID: function(userID) {
    return 'ajaxChat_u_'+userID;
  },

  getUserNode: function(userID) {
    return document.getElementById(this.getUserDocumentID(userID));
  },

  getUserMenuDocumentID: function(userID) {
    return 'ajaxChat_um_'+userID;
  },

  getInlineUserMenuDocumentID: function(menuID, index) {
    return 'ajaxChat_ium_'+menuID+'_'+index;
  },

  getDeletionLink: function(messageID, userID, userRole, channelID) {
    if(messageID !== null && this.isAllowedToDeleteMessage(messageID, userID, userRole, channelID)) {
      if(!arguments.callee.deleteMessage) {
        arguments.callee.deleteMessage = this.encodeSpecialChars(this.lang['deleteMessage']);
      }
      return  '<a class="delete" title="'
          + arguments.callee.deleteMessage
          + '" href="javascript:ajaxChat.deleteMessage('
          + messageID
          + ');"> </a>' // Adding a space - without any content Opera messes up the chatlist display
    }
    return '';
  },

  isAllowedToDeleteMessage: function(messageID, userID, userRole, channelID) {
    if((((this.userRole == 1 && this.allowUserMessageDelete && (userID == this.userID ||
      parseInt(channelID) == parseInt(this.userID)+this.privateMessageDiff ||
      parseInt(channelID) == parseInt(this.userID)+this.privateChannelDiff)) ||
      this.userRole == 2) && userRole != 3 && userRole != 4) || this.userRole == 3) {
      return true;
    }
    return false;
  },

  onNewMessage: function(dateObject, userID, userName, userRole, messageID, messageText, channelID, ip) {
    if(!this.customOnNewMessage(dateObject, userID, userName, userRole, messageID, messageText, channelID, ip)) {
      return false;
    }
    if(this.ignoreMessage(dateObject, userID, userName, userRole, messageID, messageText, channelID, ip)) {
      return false;
    }
    if(this.parseDeleteMessageCommand(messageText)) {
      return false;
    }
    this.blinkOnNewMessage(dateObject, userID, userName, userRole, messageID, messageText, channelID, ip);
    this.playSoundOnNewMessage(dateObject, userID, userName, userRole, messageID, messageText, channelID, ip);
    return true;
  },

  parseDeleteMessageCommand: function(messageText) {
    if(messageText.indexOf('/delete') == 0) {
      var messageID = messageText.substr(8);
      var messageNode = this.getMessageNode(messageID);
      if(messageNode) {
        var nextSibling = messageNode.nextSibling;
        try {
          this.dom['chatList'].removeChild(messageNode);
          if(nextSibling) {
            this.updateChatListRowClasses(nextSibling);
          }
        } catch(e) {
        }
      }
      return true;
    }
    return false;
  },

  blinkOnNewMessage: function(dateObject, userID, userName, userRole, messageID, messageText, channelID, ip) {
    if(this.settings['blink'] && this.lastID && !this.channelSwitch && userID != this.userID) {
      clearInterval(this.blinkInterval);
      this.blinkInterval = setInterval(
        'ajaxChat.blinkUpdate(\''+this.addSlashes(this.decodeSpecialChars(userName))+'\')',
        this.settings['blinkInterval']
      );
    }
  },

  blinkUpdate: function(blinkStr) {
    if(!this.originalDocumentTitle) {
      this.originalDocumentTitle = document.title;
    }
    if(!arguments.callee.blink) {
      document.title = '[@ ] '+blinkStr+' - '+this.originalDocumentTitle;
      arguments.callee.blink = 1;
    } else if(arguments.callee.blink > this.settings['blinkIntervalNumber']) {
      clearInterval(this.blinkInterval);
      document.title = this.originalDocumentTitle;
      arguments.callee.blink = 0;
    } else {
      if(arguments.callee.blink % 2 != 0) {
        document.title = '[@ ] '+blinkStr+' - '+this.originalDocumentTitle;
      } else {
        document.title = '[ @] '+blinkStr+' - '+this.originalDocumentTitle;
      }
      arguments.callee.blink++;
    }
  },

  updateChatlistView: function() {
    if(this.dom['chatList'].childNodes && this.settings['maxMessages']) {
      while(this.dom['chatList'].childNodes.length > this.settings['maxMessages']) {
        this.dom['chatList'].removeChild(this.dom['chatList'].firstChild);
      }
    }

    if(this.settings['autoScroll']) {
      this.dom['chatList'].scrollTop = this.dom['chatList'].scrollHeight;
    }
  },

  encodeText: function(text) {
    return encodeURIComponent(text);
  },

  decodeText: function(text) {
    return decodeURIComponent(text);
  },

  utf8Encode: function(plainText) {
    var utf8Text = '';
    for(var i=0; i<plainText.length; i++) {
      var c=plainText.charCodeAt(i);
      if(c<128) {
        utf8Text += String.fromCharCode(c);
      } else if((c>127) && (c<2048)) {
        utf8Text += String.fromCharCode((c>>6)|192);
        utf8Text += String.fromCharCode((c&63)|128);
      } else {
        utf8Text += String.fromCharCode((c>>12)|224);
        utf8Text += String.fromCharCode(((c>>6)&63)|128);
        utf8Text += String.fromCharCode((c&63)|128);
      }
    }
    return utf8Text;
  },

  utf8Decode: function(utf8Text) {
    var plainText = '';
    var c,c2,c3;
    var i=0;
    while(i<utf8Text.length) {
      c = utf8Text.charCodeAt(i);
      if(c<128) {
        plainText += String.fromCharCode(c);
        i++;
      } else if((c>191) && (c<224)) {
        c2 = utf8Text.charCodeAt(i+1);
        plainText += String.fromCharCode(((c&31)<<6) | (c2&63));
        i+=2;
      } else {
        c2 = utf8Text.charCodeAt(i+1);
        c3 = utf8Text.charCodeAt(i+2);
        plainText += String.fromCharCode(((c&15)<<12) | ((c2&63)<<6) | (c3&63));
        i+=3;
      }
    }
    return plainText;
  },

  encodeSpecialChars: function(text) {
    if (!arguments.callee.regExp) {
      arguments.callee.regExp = new RegExp('[&<>\'"]', 'g');
    }

    return text.replace(
      arguments.callee.regExp,
      this.encodeSpecialCharsCallback
    );
  },

  encodeSpecialCharsCallback: function(str) {
    switch(str) {
      case '&':
        return '&amp;';
      case '<':
        return '&lt;';
      case '>':
        return '&gt;';
      case '\'':
        // As &apos; is not supported by IE, we use &#39; as replacement for ('):
        return '&#39;';
      case '"':
        return '&quot;';
      default:
        return str;
    }
  },

  decodeSpecialChars: function(text) {
    if (!arguments.callee.regExp) {
      arguments.callee.regExp = new RegExp('(&amp;)|(&lt;)|(&gt;)|(&#39;)|(&quot;)', 'g');
    }

    return text.replace(
      arguments.callee.regExp,
      this.decodeSpecialCharsCallback
    );
  },

  decodeSpecialCharsCallback: function(str) {
    switch(str) {
      case '&amp;':
        return '&';
      case '&lt;':
        return '<';
      case '&gt;':
        return '>';
      case '&#39;':
        return '\'';
      case '&quot;':
        return '"';
      default:
        return str;
    }
  },

  inArray: function(haystack, needle) {
    var i = haystack.length;
    while(i--) {
      if(haystack[i] === needle) {
        return true;
      }
    }
    return false;
  },

  arraySearch: function(needle, haystack) {
    var i = haystack.length;
    while(i--) {
      if(haystack[i] === needle) {
        return i;
      }
    }
      return false;
  },

  stripTags: function(str) {
    if (!arguments.callee.regExp) {
      arguments.callee.regExp = new RegExp('<\\/?[^>]+?>', 'g');
    }

    return str.replace(arguments.callee.regExp, '');
  },

  stripBBCodeTags: function(str) {
    if (!arguments.callee.regExp) {
      arguments.callee.regExp = new RegExp('\\[\\/?[^\\]]+?\\]', 'g');
    }

    return str.replace(arguments.callee.regExp, '');
  },

  escapeRegExp: function(text) {
    if (!arguments.callee.regExp) {
      var specials = new Array(
        '^', '$', '*', '+', '?', '.', '|', '/',
        '(', ')', '[', ']', '{', '}', '\\'
      );
      arguments.callee.regExp = new RegExp(
        '(\\' + specials.join('|\\') + ')', 'g'
      );
    }
    return text.replace(arguments.callee.regExp, '\\$1');
  },

  addSlashes: function(text) {
    // Adding slashes in front of apostrophs and backslashes to ensure a valid JavaScript expression:
    return text.replace(/\\/g, '\\\\').replace(/\'/g, '\\\'');
  },

  removeSlashes: function(text) {
    // Removing slashes added by calling addSlashes(text) previously:
    return text.replace(/\\\\/g, '\\').replace(/\\\'/g, '\'');
  },

  formatDate: function(format, date) {
    date = (date == null) ? new date() : date;

    return format
    .replace(/%Y/g, date.getFullYear())
    .replace(/%m/g, this.addLeadingZero(date.getMonth()+1))
    .replace(/%d/g, this.addLeadingZero(date.getDate()))
    .replace(/%H/g, this.addLeadingZero(date.getHours()))
    .replace(/%i/g, this.addLeadingZero(date.getMinutes()))
    .replace(/%s/g, this.addLeadingZero(date.getSeconds()));
  },

  addLeadingZero: function(number) {
    number = number.toString();
    if(number.length < 2) {
      number = '0'+number;
    }
    return number;
  },

  getUserIDFromUserName: function(userName) {
    var index = this.arraySearch(userName, this.userNamesList);
    if(index !== false) {
      return this.usersList[index];
    }
    return null;
  },

  getUserNameFromUserID: function(userID) {
    var index = this.arraySearch(userID, this.usersList);
    if(index !== false) {
      return this.userNamesList[index];
    }
    return null;
  },

  getRoleClass: function(roleID) {
    switch(parseInt(roleID)) {
      case 0:
        return 'guest';
      case 1:
        return 'user';
      case 2:
        return 'moderator';
      case 3:
        return 'admin';
      case 4:
        return 'chatBot';
      default:
        return 'default';
    }
  },

  handleInputFieldKeyPress: function(event) {
    if(event.keyCode == 13 && !event.shiftKey) {
      this.sendMessage();
      try {
        event.preventDefault();
      } catch(e) {
        event.returnValue = false; // IE
      }
      return false;
    }
    return true;
  },

  handleInputFieldKeyUp: function(event) {
    this.updateMessageLengthCounter();
  },

  updateMessageLengthCounter: function() {
    if(this.dom['messageLengthCounter']) {
      this.updateDOM(
        'messageLengthCounter',
        this.dom['inputField'].value.length  + '/' + this.messageTextMaxLength,
        false,
        true
      )
    }
  },

  sendMessage: function(text) {
    text = text ? text : this.dom['inputField'].value;
    if(!text) {
      return;
    }
    text = this.parseInputMessage(text);
    if(text) {
      clearTimeout(this.timer);
      var message =   'lastID='
              + this.lastID
              + '&text='
              + this.encodeText(text);
      this.makeRequest(this.ajaxURL,'POST',message);
    }
    this.dom['inputField'].value = '';
    this.dom['inputField'].focus();
    this.updateMessageLengthCounter();
  },

  parseInputMessage: function(text) {
    if(text.charAt(0) == '/') {
      var textParts = text.split(' ');
      switch(textParts[0]) {
        case '/ignore':
          text = this.parseIgnoreInputCommand(text, textParts);
          break;
        default:
          text = this.parseCustomInputCommand(text, textParts);
      }
      if(text && this.settings['persistFontColor'] && this.settings['fontColor']) {
        text = this.assignFontColorToCommandMessage(text, textParts);
      }
    } else {
      text = this.parseCustomInputMessage(text);
      if(text && this.settings['persistFontColor'] && this.settings['fontColor']) {
        text = this.assignFontColorToMessage(text);
      }
    }
    return text;
  },

  assignFontColorToMessage: function(text) {
    return '[color='+this.settings['fontColor']+']'+text+'[/color]';
  },

  assignFontColorToCommandMessage: function(text, textParts) {
    switch(textParts[0]) {
      case '/msg':
      case '/describe':
        if(textParts.length > 2) {
          return  textParts[0]+' '+textParts[1]+' '
              + '[color='+this.settings['fontColor']+']'
              + textParts.slice(2).join(' ')
              + '[/color]';
        }
        break;
      case '/me':
      case '/action':
        if(textParts.length > 1) {
          return  textParts[0]+' '
              + '[color='+this.settings['fontColor']+']'
              + textParts.slice(1).join(' ')
              + '[/color]';
        }
        break;
    }
    return text;
  },

  parseIgnoreInputCommand: function(text, textParts) {
    var ignoredUserNames = this.getIgnoredUserNames();
    if(textParts.length > 1) {
      var userName = this.encodeSpecialChars(textParts[1]);
      // Prevent adding the chatBot or current user to the list:
      if(userName == this.userName || userName == this.getEncodedChatBotName()) {
        // Display the list of ignored users instead:
        return this.parseIgnoreInputCommand(null, new Array('/ignore'));
      }
      if(ignoredUserNames.length > 0) {
        var i = ignoredUserNames.length;
        while(i--) {
          if(ignoredUserNames[i] === userName) {
            ignoredUserNames.splice(i,1);
            this.addChatBotMessageToChatList('/ignoreRemoved '+userName);
            this.setIgnoredUserNames(ignoredUserNames);
            this.updateChatlistView();
            return null;
          }
        }
      }
      ignoredUserNames.push(userName);
      this.addChatBotMessageToChatList('/ignoreAdded '+userName);
      this.setIgnoredUserNames(ignoredUserNames);
    } else {
      if(ignoredUserNames.length == 0) {
        this.addChatBotMessageToChatList('/ignoreListEmpty -');
      } else {
        this.addChatBotMessageToChatList('/ignoreList '+ignoredUserNames.join(' '));
      }
    }
    this.updateChatlistView();
    return null;
  },

  getIgnoredUserNames: function() {
    if(!this.ignoredUserNames) {
      var ignoredUserNamesString = this.getSetting('ignoredUserNames');
      if(ignoredUserNamesString) {
        this.ignoredUserNames = ignoredUserNamesString.split(' ');
      } else {
        this.ignoredUserNames = new Array();
      }
    }
    return this.ignoredUserNames;
  },

  setIgnoredUserNames: function(ignoredUserNames) {
    this.ignoredUserNames = ignoredUserNames;
    this.setSetting('ignoredUserNames', ignoredUserNames.join(' '));
  },

  ignoreMessage: function(dateObject, userID, userName, userRole, messageID, messageText, channelID, ip) {
    if(userID == this.chatBotID && messageText.charAt(0) == '/') {
      var textParts = messageText.split(' ');
      if(textParts.length > 1) {
        switch(textParts[0]) {
          case '/invite':
          case '/uninvite':
          case '/roll':
            userName = textParts[1];
            break;
        }
      }
    }
    if(this.inArray(this.getIgnoredUserNames(), userName)) {
      return true;
    }
    return false;
  },

  deleteMessage: function(messageID) {
    var messageNode = this.getMessageNode(messageID);
    if(messageNode) {
      var originalClass = this.getClass(messageNode);
      this.setClass(messageNode, originalClass+' deleteSelected');
      if(confirm(this.lang['deleteMessageConfirm'])) {
        var nextSibling = messageNode.nextSibling;
        try {
          this.dom['chatList'].removeChild(messageNode);
          if(nextSibling) {
            this.updateChatListRowClasses(nextSibling);
          }
          this.updateChat('&delete='+messageID);
        } catch(e) {
          this.setClass(messageNode, originalClass);
        }
      } else {
        this.setClass(messageNode, originalClass);
      }
    }
  },

  updateChatListRowClasses: function(node) {
    if(!node) {
      node = this.dom['chatList'].firstChild;
    }
    if(node) {
      var previousNode = node.previousSibling;
      var rowEven = (previousNode && this.getClass(previousNode) == 'rowOdd') ? true : false;
      while(node) {
        this.setClass(node, (rowEven ? 'rowEven' : 'rowOdd'))
        node = node.nextSibling;
        rowEven = !rowEven;
      }
    }
  },

  getClass: function(node) {
    if(typeof node.className != 'undefined') {
      return node.className; // IE
    } else {
      return node.getAttribute('class');
    }
  },

  setClass: function(node, className) {
    if(typeof node.className != 'undefined') {
      node.className = className; // IE
    } else {
      node.setAttribute('class', className);
    }
  },

  scriptLinkEncode: function(text) {
    return this.encodeText(this.addSlashes(this.decodeSpecialChars(text)));
  },

  scriptLinkDecode: function(text) {
    return this.encodeSpecialChars(this.removeSlashes(this.decodeText(text)));
  },

  getScriptLinkValue: function(value) {
    // This method returns plainText encoded values from javascript links
    // The value has to be utf8Decoded for MSIE and Opera:
    if(typeof arguments.callee.utf8Decode == 'undefined') {
      switch(navigator.appName) {
        case 'Microsoft Internet Explorer':
        case 'Opera':
          arguments.callee.utf8Decode = true;
          return this.utf8Decode(value);
        default:
          arguments.callee.utf8Decode = false;
          return value;
      }
    } else if(arguments.callee.utf8Decode) {
      return this.utf8Decode(value);
    } else {
      return value;
    }
  },

  sendMessageWrapper: function(text) {
    this.sendMessage(this.getScriptLinkValue(text));
  },

  insertMessageWrapper: function(text) {
    this.insertText(this.getScriptLinkValue(text), true);
  },

  switchChannel: function(channel) {
    if(!this.chatStarted) {
      this.clearChatList();
      this.channelSwitch = true;
      this.loginChannelID = null;
      this.loginChannelName = channel;
      this.requestTeaserContent();
      return;
    }
    clearTimeout(this.timer);
    var message =   'lastID='
            + this.lastID
            + '&channelName='
            + this.encodeText(channel);
    this.makeRequest(this.ajaxURL,'POST',message);
    if(this.dom['inputField'] && this.settings['autoFocus']) {
      this.dom['inputField'].focus();
    }
  },

  logout: function() {
    clearTimeout(this.timer);
    var message = 'logout=true';
    this.makeRequest(this.ajaxURL,'POST',message);
  },

  handleLogout: function(url) {
    window.location.href = url;
  },

  toggleSetting: function(setting, buttonID) {
    this.setSetting(setting, !this.getSetting(setting));
    if(buttonID) {
      this.updateButton(setting, buttonID);
    }
  },

  updateButton: function(setting, buttonID) {
    var node = document.getElementById(buttonID);
    if(node) {
      this.setClass(node, (this.getSetting(setting) ? 'button' : 'button off'))
    }
  },

  showHide: function(id, styleDisplay, displayInline) {
    var node = document.getElementById(id);
    if(node) {
      if(styleDisplay) {
        node.style.display = styleDisplay;
      } else {
        if(node.style.display == 'none') {
          node.style.display = (displayInline ? 'inline' : 'block');
        } else {
          node.style.display = 'none';
        }
      }
    }
  },

  setPersistFontColor: function(bool) {
    this.settings['persistFontColor'] = bool;
    if(!this.settings['persistFontColor']) {
      this.settings['fontColor'] = null;
      if(this.dom['inputField']) {
        this.dom['inputField'].style.color = '';
      }
    }
  },

  setFontColor: function(color) {
    if(this.settings['persistFontColor']) {
      this.settings['fontColor'] = color;
      if(this.dom['inputField']) {
        this.dom['inputField'].style.color = color;
      }
      if(this.dom['colorCodesContainer']) {
        this.dom['colorCodesContainer'].style.display = 'none';
        if(this.dom['inputField']) {
          this.dom['inputField'].focus();
        }
      }
    } else {
      this.insert('[color=' + color + ']', '[/color]');
    }
  },

  insertText: function(text, clearInputField) {
    if(clearInputField) {
      this.dom['inputField'].value = '';
    }
    this.insert(text, '');
  },

  insertBBCode: function(bbCode) {
    switch(bbCode) {
      case 'url':
        var url = prompt(this.lang['urlDialog'], 'http://');
        if(url)
          this.insert('[url=' + url + ']', '[/url]');
        else
          this.dom['inputField'].focus();
        break;
      default:
        this.insert('[' + bbCode + ']', '[/' + bbCode + ']');
    }
  },

  insert: function(startTag, endTag) {
    this.dom['inputField'].focus();
    // Internet Explorer:
    if(typeof document.selection != 'undefined') {
      // Insert the tags:
      var range = document.selection.createRange();
      var insText = range.text;
      range.text = startTag + insText + endTag;
      // Adjust the cursor position:
      range = document.selection.createRange();
      if (insText.length == 0) {
        range.move('character', -endTag.length);
      } else {
        range.moveStart('character', startTag.length + insText.length + endTag.length);
      }
      range.select();
    }
    // Firefox, etc. (Gecko based browsers):
    else if(typeof this.dom['inputField'].selectionStart != 'undefined') {
      // Insert the tags:
      var start = this.dom['inputField'].selectionStart;
      var end = this.dom['inputField'].selectionEnd;
      var insText = this.dom['inputField'].value.substring(start, end);
      this.dom['inputField'].value =   this.dom['inputField'].value.substr(0, start)
                      + startTag
                      + insText
                      + endTag
                      + this.dom['inputField'].value.substr(end);
      // Adjust the cursor position:
      var pos;
      if (insText.length == 0) {
        pos = start + startTag.length;
      } else {
        pos = start + startTag.length + insText.length + endTag.length;
      }
      this.dom['inputField'].selectionStart = pos;
      this.dom['inputField'].selectionEnd = pos;
    }
    // Other browsers:
    else {
      var pos = this.dom['inputField'].value.length;
      this.dom['inputField'].value =   this.dom['inputField'].value.substr(0, pos)
                      + startTag
                      + endTag
                      + this.dom['inputField'].value.substr(pos);
    }
  },

  replaceText: function(text) {
    try{
      text = this.replaceLineBreaks(text);
      if(text.charAt(0) == '/') {
        text = this.replaceCommands(text);
      } else {
        text = this.replaceBBCode(text);
        text = this.replaceHyperLinks(text);
        text = this.replaceEmoticons(text);
      }
      text = this.breakLongWords(text);
      text = this.replaceCustomText(text);
    } catch(e){
      //alert(e);
    }
    return text;
  },

  replaceCommands: function(text) {
    try {
      if(text.charAt(0) != '/') {
        return text;
      }
      var textParts = text.split(' ');
      switch(textParts[0]) {
        case '/login':
          return this.replaceCommandLogin(textParts);
        case '/logout':
          return this.replaceCommandLogout(textParts);
        case '/channelEnter':
          return this.replaceCommandChannelEnter(textParts);
        case '/channelLeave':
          return this.replaceCommandChannelLeave(textParts);
        case '/privmsg':
          return this.replaceCommandPrivMsg(textParts);
        case '/privmsgto':
          return this.replaceCommandPrivMsgTo(textParts);
        case '/privaction':
          return this.replaceCommandPrivAction(textParts);
        case '/privactionto':
          return this.replaceCommandPrivActionTo(textParts);
        case '/me':
        case '/action':
          return this.replaceCommandAction(textParts);
        case '/invite':
          return this.replaceCommandInvite(textParts);
        case '/inviteto':
          return this.replaceCommandInviteTo(textParts);
        case '/uninvite':
          return this.replaceCommandUninvite(textParts);
        case '/uninviteto':
          return this.replaceCommandUninviteTo(textParts);
        case '/queryOpen':
          return this.replaceCommandQueryOpen(textParts);
        case '/queryClose':
          return this.replaceCommandQueryClose(textParts);
        case '/ignoreAdded':
          return this.replaceCommandIgnoreAdded(textParts);
        case '/ignoreRemoved':
          return this.replaceCommandIgnoreRemoved(textParts);
        case '/ignoreList':
          return this.replaceCommandIgnoreList(textParts);
        case '/ignoreListEmpty':
          return this.replaceCommandIgnoreListEmpty(textParts);
        case '/kick':
          return this.replaceCommandKick(textParts);
        case '/who':
          return this.replaceCommandWho(textParts);
        case '/whoChannel':
          return this.replaceCommandWhoChannel(textParts);
        case '/whoEmpty':
          return this.replaceCommandWhoEmpty(textParts);
        case '/list':
          return this.replaceCommandList(textParts);
        case '/bans':
          return this.replaceCommandBans(textParts);
        case '/bansEmpty':
          return this.replaceCommandBansEmpty(textParts);
        case '/unban':
          return this.replaceCommandUnban(textParts);
        case '/whois':
          return this.replaceCommandWhois(textParts);
        case '/whereis':
          return this.replaceCommandWhereis(textParts);
        case '/roll':
          return this.replaceCommandRoll(textParts);
        case '/nick':
          return this.replaceCommandNick(textParts);
        case '/error':
          return this.replaceCommandError(textParts);
        default:
          return this.replaceCustomCommands(text, textParts);
      }
    } catch(e) {
      //alert(e);
    }
    return text;
  },

  replaceCommandLogin: function(textParts) {
    return  '<span class="chatBotMessage">'
        + this.lang['login'].replace(/%s/, textParts[1])
        + '</span>';
  },

  replaceCommandLogout: function(textParts) {
    var type = '';
    if(textParts.length == 3)
      type = textParts[2];
    return  '<span class="chatBotMessage">'
        + this.lang['logout' + type].replace(/%s/, textParts[1])
        + '</span>';
  },

  replaceCommandChannelEnter: function(textParts) {
    return  '<span class="chatBotMessage">'
        + this.lang['channelEnter'].replace(/%s/, textParts[1])
        + '</span>';
  },

  replaceCommandChannelLeave: function(textParts) {
    return  '<span class="chatBotMessage">'
        + this.lang['channelLeave'].replace(/%s/, textParts[1])
        + '</span>';
  },

  replaceCommandPrivMsg: function(textParts) {
    var privMsgText = textParts.slice(1).join(' ');
    privMsgText = this.replaceBBCode(privMsgText);
    privMsgText = this.replaceHyperLinks(privMsgText);
    privMsgText = this.replaceEmoticons(privMsgText);
    return  '<span class="privmsg">'
        + this.lang['privmsg']
        + '</span> '
        + privMsgText;
  },

  replaceCommandPrivMsgTo: function(textParts) {
    var privMsgText = textParts.slice(2).join(' ');
    privMsgText = this.replaceBBCode(privMsgText);
    privMsgText = this.replaceHyperLinks(privMsgText);
    privMsgText = this.replaceEmoticons(privMsgText);
    return  '<span class="privmsg">'
        + this.lang['privmsgto'].replace(/%s/, textParts[1])
        + '</span> '
        + privMsgText;
  },

  replaceCommandPrivAction: function(textParts) {
    var privActionText = textParts.slice(1).join(' ');
    privActionText = this.replaceBBCode(privActionText);
    privActionText = this.replaceHyperLinks(privActionText);
    privActionText = this.replaceEmoticons(privActionText);
    return  '<span class="action">'
        + privActionText
        + '</span> <span class="privmsg">'
        + this.lang['privmsg']
        + '</span> ';
  },

  replaceCommandPrivActionTo: function(textParts) {
    var privActionText = textParts.slice(2).join(' ');
    privActionText = this.replaceBBCode(privActionText);
    privActionText = this.replaceHyperLinks(privActionText);
    privActionText = this.replaceEmoticons(privActionText);
    return  '<span class="action">'
        + privActionText
        + '</span> <span class="privmsg">'
        + this.lang['privmsgto'].replace(/%s/, textParts[1])
        + '</span> ';
  },

  replaceCommandAction: function(textParts) {
    var actionText = textParts.slice(1).join(' ');
    actionText = this.replaceBBCode(actionText);
    actionText = this.replaceHyperLinks(actionText);
    actionText = this.replaceEmoticons(actionText);
    return  '<span class="action">'
        + actionText
        + '</span>';
  },

  replaceCommandInvite: function(textParts) {
    var inviteText = this.lang['invite']
              .replace(/%s/, textParts[1])
              .replace(
                /%s/,
                '<a href="javascript:ajaxChat.sendMessageWrapper(\'/join '
                + this.scriptLinkEncode(textParts[2])
                + '\');" title="'
                + this.lang['joinChannel'].replace(/%s/, textParts[2])
                + '">'
                + textParts[2]
                + '</a>'
              );
    return  '<span class="chatBotMessage">'
        + inviteText
        + '</span>';
  },

  replaceCommandInviteTo: function(textParts) {
    var inviteText = this.lang['inviteto']
              .replace(/%s/, textParts[1])
              .replace(/%s/, textParts[2]);
    return  '<span class="chatBotMessage">'
        + inviteText
        + '</span>';
  },

  replaceCommandUninvite: function(textParts) {
    var uninviteText = this.lang['uninvite']
              .replace(/%s/, textParts[1])
              .replace(/%s/, textParts[2]);
    return  '<span class="chatBotMessage">'
        + uninviteText
        + '</span>';
  },

  replaceCommandUninviteTo: function(textParts) {
    var uninviteText = this.lang['uninviteto']
              .replace(/%s/, textParts[1])
              .replace(/%s/, textParts[2]);
    return  '<span class="chatBotMessage">'
        + uninviteText
        + '</span>';
  },

  replaceCommandQueryOpen: function(textParts) {
    return  '<span class="chatBotMessage">'
        + this.lang['queryOpen'].replace(/%s/, textParts[1])
        + '</span>';
  },

  replaceCommandQueryClose: function(textParts) {
    return  '<span class="chatBotMessage">'
        + this.lang['queryClose'].replace(/%s/, textParts[1])
        + '</span>';
  },

  replaceCommandIgnoreAdded: function(textParts) {
    return  '<span class="chatBotMessage">'
        + this.lang['ignoreAdded'].replace(/%s/, textParts[1])
        + '</span>';
  },

  replaceCommandIgnoreRemoved: function(textParts) {
    return  '<span class="chatBotMessage">'
        + this.lang['ignoreRemoved'].replace(/%s/, textParts[1])
        + '</span>';
  },

  replaceCommandIgnoreList: function(textParts) {
    return  '<span class="chatBotMessage">'
        + this.lang['ignoreList'] + ' '
        + this.getInlineUserMenu(textParts.slice(1))
        + '</span>';
  },

  replaceCommandIgnoreListEmpty: function(textParts) {
    return  '<span class="chatBotMessage">'
        + this.lang['ignoreListEmpty']
        + '</span>';
  },

  replaceCommandKick: function(textParts) {
    return  '<span class="chatBotMessage">'
        + this.lang['logoutKicked'].replace(/%s/, textParts[1])
        + '</span>';
  },

  replaceCommandWho: function(textParts) {
    return  '<span class="chatBotMessage">'
        + this.lang['who'] + ' '
        + this.getInlineUserMenu(textParts.slice(1))
        + '</span>';
  },

  replaceCommandWhoChannel: function(textParts) {
    return  '<span class="chatBotMessage">'
        + this.lang['whoChannel'].replace(/%s/, textParts[1]) + ' '
        + this.getInlineUserMenu(textParts.slice(2))
        + '</span>';
  },

  replaceCommandWhoEmpty: function(textParts) {
    return  '<span class="chatBotMessage">'
        + this.lang['whoEmpty']
        + '</span>';
  },

  replaceCommandList: function(textParts) {
    var channels = textParts.slice(1);
    var listChannels = new Array();
    var channelName;
    for(var i=0; i<channels.length; i++) {
      channelName = (channels[i] == this.channelName) ? '<b>'+channels[i]+'</b>' : channels[i];
      listChannels.push(
        '<a href="javascript:ajaxChat.sendMessageWrapper(\'/join '
        + this.scriptLinkEncode(channels[i])
        + '\');" title="'
        + this.lang['joinChannel'].replace(/%s/, channels[i])
        + '">'
        + channelName
        + '</a>'
      );
    }
    return  '<span class="chatBotMessage">'
        + this.lang['list'] + ' '
        + listChannels.join(', ')
        + '</span>';
  },

  replaceCommandBans: function(textParts) {
    var users = textParts.slice(1);
    var listUsers = new Array();
    for(var i=0; i<users.length; i++) {
      listUsers.push(
        '<a href="javascript:ajaxChat.sendMessageWrapper(\'/unban '
        + this.scriptLinkEncode(users[i])
        + '\');" title="'
        + this.lang['unbanUser'].replace(/%s/, users[i])
        + '">'
        + users[i]
        + '</a>'
      );
    }
    return  '<span class="chatBotMessage">'
        + this.lang['bans'] + ' '
        + listUsers.join(', ')
        + '</span>';
  },

  replaceCommandBansEmpty: function(textParts) {
    return  '<span class="chatBotMessage">'
        + this.lang['bansEmpty']
        + '</span>';
  },

  replaceCommandUnban: function(textParts) {
    return  '<span class="chatBotMessage">'
        + this.lang['unban'].replace(/%s/, textParts[1])
        + '</span>';
  },

  replaceCommandWhois: function(textParts) {
    return  '<span class="chatBotMessage">'
        + this.lang['whois'].replace(/%s/, textParts[1]) + ' '
        + textParts[2]
        + '</span>';
  },

  replaceCommandWhereis: function(textParts) {
    return  '<span class="chatBotMessage">'
        + this.lang['whereis'].replace(/%s/, textParts[1]).replace(
                /%s/,
                '<a href="javascript:ajaxChat.sendMessageWrapper(\'/join '
                + this.scriptLinkEncode(textParts[2])
                + '\');" title="'
                + this.lang['joinChannel'].replace(/%s/, textParts[2])
                + '">'
                + textParts[2]
                + '</a>'
              )
        + '</span>';
  },

  replaceCommandRoll: function(textParts) {
    var rollText = this.lang['roll'].replace(/%s/, textParts[1]);
    rollText = rollText.replace(/%s/, textParts[2]);
    rollText = rollText.replace(/%s/, textParts[3]);
    return  '<span class="chatBotMessage">'
        + rollText
        + '</span>';
  },

  replaceCommandNick: function(textParts) {
    return  '<span class="chatBotMessage">'
        + this.lang['nick'].replace(/%s/, textParts[1]).replace(/%s/, textParts[2])
        + '</span>';
  },

  replaceCommandError: function(textParts) {
    var errorMessage = this.lang['error'+textParts[1]];
    if(!errorMessage) {
      errorMessage = 'Error: Unknown.';
    } else if(textParts.length > 2) {
      errorMessage = errorMessage.replace(/%s/, textParts.slice(2).join(' '));
    }
    return  '<span class="chatBotErrorMessage">'
        + errorMessage
        + '</span>';
  },

  getInlineUserMenu: function(users) {
    var menu = '';
    for(var i=0; i<users.length; i++) {
      if(i>0) {
        menu += ', ';
      }
      menu  += '<a href="javascript:ajaxChat.toggleUserMenu(\''
          + this.getInlineUserMenuDocumentID(this.userMenuCounter, i)
          + '\', \''
          + this.scriptLinkEncode(users[i])
          + '\', null);" title="'
          + this.lang['toggleUserMenu'].replace(/%s/, users[i])
          + '" dir="'
          + this.baseDirection
          + '">'
          + ((users[i] == this.userName) ? '<b>'+users[i]+'</b>' : users[i])
          + '</a>'
          + '<ul class="inlineUserMenu" id="'
          + this.getInlineUserMenuDocumentID(this.userMenuCounter, i)
          + '" style="display:none;">'
          + '</ul>';
    }
    this.userMenuCounter++;
    return menu;
  },

  containsUnclosedTags: function(str) {
    if (!arguments.callee.regExpOpenTags || !arguments.callee.regExpCloseTags) {
      arguments.callee.regExpOpenTags    = new RegExp('<[^>\\/]+?>', 'gm');
      arguments.callee.regExpCloseTags  = new RegExp('<\\/[^>]+?>', 'gm');
    }
    var openTags  = str.match(arguments.callee.regExpOpenTags);
    var closeTags  = str.match(arguments.callee.regExpCloseTags);
    // Return true if the number of tags doesn't match:
    if((!openTags && closeTags) ||
      (openTags && !closeTags) ||
      (openTags && closeTags && (openTags.length != closeTags.length))) {
      return true;
    }
    return false;
  },

  breakLongWords: function(text) {
    if(!this.settings['wordWrap'])
      return text;
    var newText = '';
    var charCounter = 0;
    var currentChar, withinTag, withinEntity;

    for(var i=0; i<text.length; i++) {
      currentChar = text.charAt(i);

      // Check if we are within a tag or entity:
      if(currentChar == '<') {
        withinTag = true;
        // Reset the charCounter after newline tags (<br/>):
        if(i>5 && text.substr(i-5,4) == '<br/')
          charCounter = 0;
      } else if(withinTag && i>0 && text.charAt(i-1) == '>') {
        withinTag = false;
        // Reset the charCounter after newline tags (<br/>):
        if(i>4 && text.substr(i-5,4) == '<br/')
          charCounter = 0;
      } else if(currentChar == '&') {
        withinEntity = true;
      } else if(withinEntity && i>0 && text.charAt(i-1) == ';') {
        withinEntity = false;
        // We only increase the charCounter once for the whole entiy:
        charCounter++;
      }

      if(!withinTag && !withinEntity) {
        // Reset the charCounter if we encounter a word boundary:
        if(currentChar == ' ' || currentChar == '\n' || currentChar == '\t') {
          charCounter = 0;
        } else {
          // We are not within a tag or entity, increase the charCounter:
          charCounter++;
        }
        if(charCounter > this.settings['maxWordLength']) {
          // maxWordLength has been reached, break here and reset the charCounter:
          newText += this.getBreakString();
          charCounter = 0;
        }
      }
      // Add the current char to the text:
      newText += currentChar;
    }

    return newText;
  },

  getBreakString: function() {
    // Returns the character sequence used to wrap long words
    if(typeof arguments.callee.breakString == 'undefined') {
      arguments.callee.breakString = '&#8203;';
    }
    return arguments.callee.breakString;
  },

  replaceBBCode: function(text) {
    if(!this.settings['bbCode']) {
      // If BBCode is disabled, just strip the text from BBCode tags:
      if (!arguments.callee.regExpStripBBCode) {
        arguments.callee.regExpStripBBCode = new RegExp(
          '\\[(?:\\/)?(\\w+)(?:=([^<>]*?))?\\]',
          'gm'
        );
      }
      return text.replace(
        arguments.callee.regExpStripBBCode,
        ''
      );
    }
    // Remove the BBCode tags:
    if (!arguments.callee.regExp) {
      arguments.callee.regExp = new RegExp(
        '\\[(\\w+)(?:=([^<>]*?))?\\](.+?)\\[\\/\\1\\]',
        'gm'
      );
    }
    return text.replace(
      arguments.callee.regExp,
      this.replaceBBCodeCallback
    );
  },

  replaceBBCodeCallback: function(str, p1, p2, p3) {
    // Only replace predefined BBCode tags:
    if(!ajaxChat.inArray(ajaxChat.bbCodeTags, p1)) {
      return str;
    }
    // Avoid invalid XHTML (unclosed tags):
    if(ajaxChat.containsUnclosedTags(p3)) {
      return str;
    }
    switch(p1) {
      case 'color':
        return ajaxChat.replaceBBCodeColor(p3, p2);
      case 'url':
        return ajaxChat.replaceBBCodeUrl(p3, p2);
      case 'img':
        return ajaxChat.replaceBBCodeImage(p3);
      case 'quote':
        return ajaxChat.replaceBBCodeQuote(p3, p2);
      case 'code':
        return ajaxChat.replaceBBCodeCode(p3);
      case 'u':
        return ajaxChat.replaceBBCodeUnderline(p3);
      default:
        return ajaxChat.replaceCustomBBCode(p1, p2, p3);
    }
  },

  replaceBBCodeColor: function(content, attribute) {
    if(this.settings['bbCodeColors']) {
      // Only allow predefined color codes:
      if(!attribute || !this.inArray(ajaxChat.colorCodes, attribute))
        return content;
      return   '<span style="color:'
          + attribute + ';">'
          + this.replaceBBCode(content)
          + '</span>';
    }
    return content;
  },

  replaceBBCodeUrl: function(content, attribute) {
    var url;
    if(attribute)
      url = attribute.replace(/\s/gm, this.encodeText(' '));
    else
      url = this.stripBBCodeTags(content.replace(/\s/gm, this.encodeText(' ')));
    if (!arguments.callee.regExpUrl) {
      arguments.callee.regExpUrl = new RegExp(
        '^(?:(?:http)|(?:https)|(?:ftp)|(?:irc)):\\/\\/',
        ''
      );
    }
    if(!url || !url.match(arguments.callee.regExpUrl))
      return content;
    return   '<a href="'
        + url
        + '" onclick="window.open(this.href); return false;">'
        + this.replaceBBCode(content)
        + '</a>';
  },

  replaceBBCodeImage: function(url) {
    if(this.settings['bbCodeImages']) {
      if (!arguments.callee.regExpUrl) {
        arguments.callee.regExpUrl = new RegExp(
          this.regExpMediaUrl,
          ''
        );
      }
      if(!url || !url.match(arguments.callee.regExpUrl))
        return url;
      url = url.replace(/\s/gm, this.encodeText(' '));
      var maxWidth = this.dom['chatList'].offsetWidth-50;
      var maxHeight = this.dom['chatList'].offsetHeight-50;
      return  '<a href="'
          +url
          +'" onclick="window.open(this.href); return false;">'
          +'<img class="bbCodeImage" style="max-width:'
          +maxWidth
          +'px; max-height:'
          +maxHeight
          +'px;" src="'
          +url
          +'" alt="" onload="ajaxChat.updateChatlistView();"/></a>';
    }
    return url;
  },

  replaceBBCodeQuote: function(content, attribute) {
    if(attribute)
      return  '<span class="quote"><cite>'
          + this.lang['cite'].replace(/%s/, attribute)
          + '</cite><q>'
          + this.replaceBBCode(content)
          + '</q></span>';
    return   '<span class="quote"><q>'
        + this.replaceBBCode(content)
        + '</q></span>';
  },

  replaceBBCodeCode: function(content) {
    // Replace vertical tabs and multiple spaces with two non-breaking space characters:
    return   '<code>'
        + this.replaceBBCode(content.replace(/\t|(?:  )/gm, '&#160;&#160;'))
        + '</code>';
  },

  replaceBBCodeUnderline: function(content) {
    return   '<span style="text-decoration:underline;">'
        + this.replaceBBCode(content)
        + '</span>';
  },

  replaceHyperLinks: function(text) {
    if(!this.settings['hyperLinks']) {
      return text;
    }
    if(!arguments.callee.regExp) {
      arguments.callee.regExp = new RegExp(
        '(^|\\s|>)((?:(?:http)|(?:https)|(?:ftp)|(?:irc)):\\/\\/[^\\s<>]+)(<\\/a>)?',
        'gm'
      );
    }
    return text.replace(
      arguments.callee.regExp,
      // Specifying an anonymous function as second parameter:
      function(str, p1, p2, p3) {
        // Do not replace URL's inside URL's:
        if(p3) {
          return str;
        }
        return   p1
            + '<a href="'
            + p2
            + '" onclick="window.open(this.href); return false;">'
            + p2
            + '</a>';
      }
    );
  },

  replaceLineBreaks: function(text) {
    if (!arguments.callee.regExp) {
      arguments.callee.regExp = new RegExp('\\n',  'g');
    }
    if(!this.settings['lineBreaks']) {
      return text.replace(arguments.callee.regExp, ' ');
    } else {
      return text.replace(arguments.callee.regExp, '<br/>');
    }
  },

  replaceEmoticons: function(text) {
    if(!this.settings['emoticons']) {
      return text;
    }
    if(!arguments.callee.regExp) {
      var regExpStr = '^(.*)(';
      for(var i=0; i<this.emoticonCodes.length; i++) {
        if(i!=0)
          regExpStr += '|';
        regExpStr += '(?:' + this.escapeRegExp(this.emoticonCodes[i]) + ')';
      }
      regExpStr += ')(.*)$';
      arguments.callee.regExp = new RegExp(regExpStr, 'gm');
    }
    return text.replace(
      arguments.callee.regExp,
      this.replaceEmoticonsCallback
    );
  },

  replaceEmoticonsCallback: function(str, p1, p2, p3) {
    if (!arguments.callee.regExp) {
      arguments.callee.regExp = new RegExp('(="[^"]*$)|(&[^;]*$)', '');
    }
    // Avoid replacing emoticons in tag attributes or XHTML entities:
    if(p1.match(arguments.callee.regExp)) {
      return str;
    }
    if(p2) {
      var index = ajaxChat.arraySearch(p2, ajaxChat.emoticonCodes);
      return   ajaxChat.replaceEmoticons(p1)
        +  '<img src="'
        +  ajaxChat.dirs['emoticons']
        +  ajaxChat.emoticonFiles[index]
        +  '" alt="'
        +  p2
        +  '" />'
        +   ajaxChat.replaceEmoticons(p3);
    }
    return str;
  },

  getActiveStyle: function() {
    var cookie = this.readCookie(this.sessionName + '_style');
    var style = cookie ? cookie : this.getPreferredStyleSheet();
    return style;
  },

  initStyle: function() {
    this.styleInitiated = true;
    this.setActiveStyleSheet(this.getActiveStyle());
  },

  persistStyle: function() {
    if(this.styleInitiated) {
      this.createCookie(this.sessionName + '_style', this.getActiveStyleSheet(), this.cookieExpiration);
    }
  },

  setSelectedStyle: function() {
    if(this.dom['styleSelection']) {
      var style = this.getActiveStyle();
      var styleOptions = this.dom['styleSelection'].getElementsByTagName('option');
      for(var i=0; i<styleOptions.length; i++) {
        if(styleOptions[i].value == style) {
          styleOptions[i].selected = true;
          break;
        }
      }
    }
  },

  getSelectedStyle: function() {
    var styleOptions = this.dom['styleSelection'].getElementsByTagName('option');
    if(this.dom['styleSelection'].selectedIndex == -1) {
      return styleOptions[0].value;
    } else {
      return styleOptions[this.dom['styleSelection'].selectedIndex].value;
    }
  },

  setActiveStyleSheet: function(title) {
    var i, a, main;
    var titleFound = false;
    for(i=0; (a = document.getElementsByTagName('link')[i]); i++) {
      if(a.getAttribute('rel').indexOf('style') != -1 && a.getAttribute('title')) {
        a.disabled = true;
        if(a.getAttribute('title') == title) {
                  a.disabled = false;
                  titleFound = true;
        }
      }
    }
    if(!titleFound && title != null) {
       this.setActiveStyleSheet(this.getPreferredStyleSheet());
    }
  },

  getActiveStyleSheet: function() {
    var i, a;
    for(i=0; (a = document.getElementsByTagName('link')[i]); i++) {
      if(a.getAttribute('rel').indexOf('style') != -1 && a.getAttribute('title') && !a.disabled) {
        return a.getAttribute('title');
      }
    }
    return null;
  },

  getPreferredStyleSheet: function() {
    var i,a;
    for(i=0; (a = document.getElementsByTagName('link')[i]); i++) {
      if(a.getAttribute('rel').indexOf('style') != -1
        && a.getAttribute('rel').indexOf('alt') == -1
        && a.getAttribute('title')
        ) {
        return a.getAttribute('title');
      }
    }
    return null;
  },

  switchLanguage: function(langCode) {
    window.location.search = '?lang='+langCode;
  },

  createCookie: function(name,value,days) {
    var expires = '';
    if(days) {
      var date = new Date();
      date.setTime(date.getTime()+(days*24*60*60*1000));
      expires = '; expires='+date.toGMTString();
    }
    var path = '; path='+this.cookiePath;
    var domain = this.cookieDomain ? '; domain='+this.cookieDomain : '';
    var secure = this.cookieSecure ? '; secure' : '';
    document.cookie = name+'='+encodeURIComponent(value)+expires+path+domain+secure;
  },

  readCookie: function(name) {
    if(!document.cookie)
       return null;
    var nameEQ = name + '=';
    var ca = document.cookie.split(';');
    for(var i=0; i<ca.length; i++) {
      var c = ca[i];
      while(c.charAt(0) == ' ') {
        c = c.substring(1, c.length);
      }
      if(c.indexOf(nameEQ) == 0) {
        return decodeURIComponent(c.substring(nameEQ.length, c.length));
      }
    }
    return null;
  },

  isCookieEnabled: function() {
    this.createCookie(this.sessionName + '_cookie_test', true, 1);
    var cookie = this.readCookie(this.sessionName + '_cookie_test');
    if(cookie) {
      // Unset the test cookie:
      this.createCookie(this.sessionName + '_cookie_test', true, -1);
      // Cookie test successfull, return true:
      return true;
    }
    return false;
  },

  finalize: function() {
    if(typeof this.finalizeFunction == 'function') {
      this.finalizeFunction();
    }
    // Ensure the socket connection is closed on unload:
    if(this.socket) {
      try {
        this.socket.close();
        this.socket = null;
      } catch(e) {
        //alert(e);
      }
    }
    this.persistSettings();
    this.persistStyle();
    this.customFinalize();
  },

  // Override to perform custom actions on flash initialization:
  initializeCustomFlashInterface: function() {
  },

  // Override to handle custom info messages
  handleCustomInfoMessage: function(infoType, infoData) {
  },

  // Override to add custom initialization code
  // This method is called on page load
  customInitialize: function() {
  },

  // Override to add custom finalization code
  // This method is called on page unload
  customFinalize: function() {
  },

  // Override to add custom user menu items:
  // Return a string with list items ( <li>menuItem</li> )
  // encodedUserName contains the userName ready to be used for javascript links
  // userID is only available for the online users menu - not for the inline user menu
  // use (encodedUserName == this.encodedUserName) to check for the current user
  getCustomUserMenuItems: function(encodedUserName, userID) {
    return '';
  },

  // Override to parse custom input messages:
  // Return replaced text
  // text contains the whole message
  parseCustomInputMessage: function(text) {
    return text;
  },

  // Override to parse custom input commands:
  // Return parsed text
  // text contains the whole message, textParts the message split up as words array
  parseCustomInputCommand: function(text, textParts) {
    return text;
  },

  // Override to replace custom text:
  // Return replaced text
  // text contains the whole message
  replaceCustomText: function(text) {
    return text;
  },

  // Override to replace custom commands:
  // Return replaced text for custom commands
  // text contains the whole message, textParts the message split up as words array
  replaceCustomCommands: function(text, textParts) {
    return text;
  },

  // Override to replace custom BBCodes:
  // Return replaced text and call replaceBBCode recursively for the content text
  // tag contains the BBCode tag, attribute the BBCode attribute and content the content text
  // This method is only called for BBCode tags which are in the bbCodeTags list
  replaceCustomBBCode: function(tag, attribute, content) {
    return '<' + tag + '>' + this.replaceBBCode(content) + '</' + tag + '>';
  },

  // Override to perform custom actions on new messages:
  // Return true if message is to be added to the chatList, else false
  customOnNewMessage: function(dateObject, userID, userName, userRole, messageID, messageText, channelID, ip) {
    return true;
  }

}
