/* ------------------------------------------------------------------------
 * campfire.js
 * Copyright (c) 2006-2007 37signals, LLC. All rights reserved.
 * ------------------------------------------------------------------------ */

var Campfire = {};
Campfire.UnreadMessageCounter = /^\((\d+)\) /;
Campfire.Responders = [
  'TimestampManager',
  'Transcript',
  'Poller',
  'Speaker',
  'WindowManager',
  'SoundManager',
  'Addresser',
  'Uploader',
  'RequestWatchdog'
];

/*--------------------------------------------------------------------------*/

Campfire.Chat = Class.create();
Campfire.Chat.prototype = {
  initialize: function(options) {
    Object.extend(this, options);
    
    if (this.debug)
      alert('Chatting in debug mode!');

    if (/MSIE/.test(navigator.userAgent)) {
      this.IE  = true;
      if (/MSIE 7/.test(navigator.userAgent))
        this.IE7 = true;
    }

    this.register.apply(this, Campfire.Responders);
  },
  
  register: function() {
    this.events = {};
    this.listeners = $A(arguments).map(function(klass) {
      return this[klass.toLowerCase()] = new Campfire[klass](this);
    }.bind(this));
  },
  
  cacheEventsFor: function(event) {
    var callback = ('on-' + event).camelize();
    this.events[event] = this.listeners.inject([], 
      function(callbacks, listener) {
        if (listener[callback]) {
          var __method = listener[callback].bind(listener);
          __method.listener = listener;
          callbacks.push(__method);
        }
        return callbacks;
      });
  },
  
  dispatch: function() {
    var args = $A(arguments), event = args.shift();
    
    if (!this.events[event])
      this.cacheEventsFor(event);

    try {
      this.events[event].each(function(callback) {
        callback.apply(callback.listener, args);
      });
    } catch (e) {
      if (this.debug)
        alert('Error in event ' + event + ': ' + e);
    }
  },
  
  redirectTo: function(url, disableAndDelay) {
    this.poller.stop();
    
    if (disableAndDelay) {
      this.disableAndDelayRedirectTo(window.location.href);
    } else {
      window.location.href = url;
    }
  },
  
  disableAndDelayRedirectTo: function(url) {
    $(document.body).addClassName("disabled");
    $("input", "term", "send").invoke("disable");

    var delay = parseInt(Math.random() * 11) * 5 + 5; // 5 to 60 seconds from now
    function showDelay() {
      var text = delay.toString() + " second" + (delay == 1 ? "" : "s");
      $("timeout_text").update("Campfire will be back in " + text);
    }

    var interval = window.setInterval(function() {
      if (delay) {
        showDelay();
        delay -= 1;
      } else {
        $("timeout_text").update("Campfire is restarting&hellip;");
        window.clearInterval(interval);
        window.location.href = url;
      }
    }, 1000);

    showDelay();
  }
}

/*--------------------------------------------------------------------------*/

Campfire.WindowManager = Class.create();
Campfire.WindowManager.prototype = {
  initialize: function(chat) {
    this.chat = chat;
    this.unreadCount = 0;
    this.reset();
    
    if (this.chat.IE) {
      Event.observe(window, 'load', this.reset.bind(this));
    } else {
      this.startForcefullyAutoscrolling();
    }
    
    Event.observe(window, 'load', this.registerCallbacks.bind(this));
  },
  
  registerCallbacks: function() {
    Event.observe(window, 'scroll', this.onScroll.bind(this));
    Event.observe(window, 'resize', this.onResize.bind(this));
    Event.observe(window, 'blur',   this.onBlur.bind(this));
    Event.observe(window, 'focus',  this.onFocus.bind(this));
  },
  
  startForcefullyAutoscrolling: function() {
    if (this.chat.scrollToBottom)
      this.interval = window.setInterval(this.scrollToBottom.bind(this), 50);
  },
  
  stopForcefullyAutoscrolling: function() {
    if (this.interval) {
      window.clearInterval(this.interval);
      delete this.interval;
    }
  },
    
  reset: function() {
    this.layout();
    if (this.chat.scrollToBottom)
      this.scrollToBottom();
    if (this.chat.speaker)
      window.setTimeout(this.chat.speaker.focus.bind(this.chat.speaker), 10);
  },
  
  getPageHeight: function() {
    return Math.max(document.documentElement.offsetHeight, 
      document.body.scrollHeight);
  },
  
  getWindowHeight: function() {
    return window.innerHeight || document.body.clientHeight;
  },
  
  getScrollOffset: function() {
    return Math.max(document.documentElement.scrollTop,
      document.body.scrollTop);
  },
  
  isScrolledToBottom: function() {
    return this.getScrollOffset() + this.getWindowHeight() >= 
      this.getPageHeight();
  },
  
  getChatViewportWidth: function() {
    var element = this.chat.transcript.element.parentNode.parentNode;
    return Element.getDimensions(element).width;
  },
  
  getChatAuthorColumnWidth: function() {
    var element = this.chat.transcript.element.getElementsByTagName('td')[1];
    if (!element) return 0;
    return Position.cumulativeOffset(element)[0] -
      Position.cumulativeOffset(this.chat.transcript.element)[0];
  },
  
  adjustChatMessageColumnWidth: function() {
    if (this.chat.IE && !this.chat.IE7) return false;
    var viewportWidth = this.getChatViewportWidth();
    var authorColumnWidth = this.getChatAuthorColumnWidth();
    var messageColumnWidth = viewportWidth - authorColumnWidth - 10;
    
    var stylesheet = $A(document.styleSheets).last();
    var rules = stylesheet.cssRules || stylesheet.rules;
    var style = rules[rules.length - 1].style;
    if (style) style.width = messageColumnWidth + 'px';
  },
  
  adjustChatControls: function() {
    if ((this.chat.IE && !this.chat.IE7) || !this.chat.speaker) return false;
    var controlsWidth = Element.getDimensions(this.chat.speaker.controls).width;
    this.chat.speaker.input.style.width = 
      this.getChatViewportWidth() - controlsWidth - 10 + 'px';
  },
  
  layout: function() {
    this.adjustChatMessageColumnWidth();
    this.adjustChatControls();
  },
  
  scrollToBottom: function() {
    if (this.scrollingToBottom) return;
    this.scrollingToBottom = true;
    
    if (this.chat.IE) {
      $('last_message').scrollIntoView(true);
    } else {
      window.scrollTo(0, this.getPageHeight() + this.getWindowHeight() + 100);
    }
    
    this.scrollingToBottom = false;
    this.scrolledToBottom = true;
  },

  onScroll: function(event) {
    if (this.scrollingToBottom) return;
    this.stopForcefullyAutoscrolling();
    this.scrolledToBottom = this.isScrolledToBottom();
  },
  
  onResize: function(event) {
    this.layout();
    if (!this.chat.IE && this.scrolledToBottom && !this.isScrolledToBottom())
      this.scrollToBottom();
  },
  
  onBlur: function(event) {
    this.blurred = true;
  },
  
  onFocus: function(event) {
    this.blurred = false;
    this.resetUnreadCount();
  },
  
  onMessagesInsertedBeforeDisplay: function() {
    this.stopForcefullyAutoscrolling();
    this.scrolledToBottom = this.isScrolledToBottom();
  },
  
  onMessagesInserted: function(messages) {
    if (this.blurred) {
      var count = 0;
      for (var i = 0; i < messages.length; i++)
        if (messages[i].actsLikeTextMessage()) count++;
      this.incrementUnreadCountBy(count);
    }
    
    this.stopForcefullyAutoscrolling();
    this.adjustChatMessageColumnWidth();
    if (this.scrolledToBottom)
      this.scrollToBottom();
  },
  
  incrementUnreadCountBy: function(count) {
    this.unreadCount += count;
    this.updateUnreadCounter();
  },
  
  resetUnreadCount: function() {
    this.unreadCount = 0;
    this.updateUnreadCounter();
  },
  
  updateUnreadCounter: function() {
    if (this.unreadCount) {
      var match = document.title.match(Campfire.UnreadMessageCounter);
      if (match) {
        document.title = document.title.replace(Campfire.UnreadMessageCounter,
          "(" + this.unreadCount + ") ");
      } else {
        document.title = "(" + this.unreadCount + ") " + document.title;
      }
    } else {
      document.title = document.title.replace(Campfire.UnreadMessageCounter, "");
    }
  },
  
  onMessageSpoken: function() {
    this.scrollToBottom();
    this.chat.speaker.focus();
  },
  
  onMessageBodyUpdated: function() {
    this.scrollToBottom();
  }
}

/*--------------------------------------------------------------------------*/

Campfire.Transcript = Class.create();
Campfire.Transcript.prototype = {
  initialize: function(chat) {
    this.chat     = chat;
    this.element  = $(chat.transcriptElement);
    this.template = new Template(chat.messageTemplate || '');
    this.findMessages();
  },
  
  findMessages: function() {
    var elements = this.element.select('.message');
    this.messages = elements.map(function(element) {
      var message = new Campfire.Message(element);
      if (message.kind == 'timestamp') this.lastTimestampMessage = message;
      return message;
    }.bind(this));
    this.updateTranscriptLink();
  },
  
  bodyForPendingMessage: function(message) {
    return MessageTransformers.applyFirst(message);  
  },
  
  insertPendingMessage: function(message, template, options) {
    options = Object.extend(Object.extend({}, options || {}), {
      id: 'pending', body: this.bodyForPendingMessage(message)
    });
    
    var html = (template || this.template).evaluate(options);
    var element = this.insertMessages(html, 'pending').first();
    element.element.id = '';
    return element;
  },
  
  insertMessages: function() {
    var ids = $A(arguments), html = ids.shift();
    new Insertion.Bottom(this.element, html);
    var messages = ids.map(this.getMessageById.bind(this));
    this.chat.dispatch('messagesInsertedBeforeDisplay', messages);
    messages.pluck('element').each(Element.show);
    this.chat.dispatch('messagesInserted', messages);
    return messages;
  },
  
  queueMessage: function(message, id) {
    this.messageQueue = this.messageQueue || [];
    this.messageQueue.push([message, id]);
  },
  
  trimHistoryBy: function(size) {
    var adjustment = this.messages.length - this.chat.messageHistory + size;
    if (adjustment > 0) {
      adjustment.times(function() {
        Element.remove(this.messages.shift().element);
      }.bind(this));
      for (var i = 0, message; i < this.messages.length; i++)
        if ((message = this.messages[i]).actsLikeTextMessage())
          return message.setAuthorVisibilityInRelationTo(null);
    }
  },
  
  updateTranscriptLink: function() {
    if (this.messages.length >= this.chat.messageHistory) {
      if (!$('todays_transcript')) return;
      Element.show('todays_transcript');
      var link = $('todays_transcript_link');
      link.href = link.href.replace(/\/\d+$/, 
        '/' + this.messages.first().id());
    }
  },
  
  onMessagesInsertedBeforeDisplay: function(messages) {
    for (var i = 0; i < messages.length; i++) {
      var message = messages[i];
      if (message.kind == 'timestamp') {
        if (this.lastTimestampMessage)
          message.setAuthorVisibilityInRelationTo(this.lastTimestampMessage);
        this.lastTimestampMessage = message;
      } else {
        message.setAuthorVisibilityInRelationTo(this.messages.last());
      }
      if (Element.hasClassName(message.element, 'user_' + this.chat.userID))
        Element.addClassName(message.element, 'you');
      this.messages.push(message);
    }

    this.updateTranscriptLink();
    this.trimHistoryBy(messages.length);
  },
  
  onMessageAccepted: function(message, id) {
    var element = message.element;
    element.id = 'message_' + id;
    Element.removeClassName(element, 'pending');
  },
  
  onMessageBodyUpdated: function(message, body) {
    message.updateBody(body);
  },
  
  onPollCompleted: function() {
    if (!this.messageQueue) return;
    var args = [''], message;
    for (var i = 0; i < this.messageQueue.length; i++) {
      message = this.messageQueue[i];
      args[0] += message[0];
      args.push(message[1]);
    }
    delete this.messageQueue;
    this.insertMessages.apply(this, args);
  },
  
  getRows: function() {
    return this.element.getElementsByTagName('tr');
  },
  
  getMessage: function(element) {
    return new Campfire.Message(element);
  },
  
  getMessageById: function(id) {
    return this.getMessage($('message_' + id));
  }
}

/*--------------------------------------------------------------------------*/

Campfire.TimestampManager = Class.create();
Campfire.TimestampManager.prototype = {
  initialize: function(chat) {
    this.chat       = chat;
    this.transcript = chat.transcript;
  },
  
  removePendingTimestamp: function() {
    if (this.pendingTimestamp) {
      this.chat.transcript.messages = 
        this.chat.transcript.messages.without(this.pendingTimestamp);
      Element.remove(this.pendingTimestamp.element);
      this.pendingTimestamp = null;
    }
  },
  
  hidePendingTimestamp: function() {
    if (this.pendingTimestamp)
      Element.addClassName(this.pendingTimestamp.element, 'hidden');
  },
  
  showPendingTimestamp: function() {
    if (this.pendingTimestamp)
      Element.removeClassName(this.pendingTimestamp.element, 'hidden');
  },
  
  onMessagesInserted: function(messages) {
    var firstMessage = messages[0];
    if (messages.length == 1 && firstMessage.kind == 'timestamp') {
      this.removePendingTimestamp();
      this.pendingTimestamp = firstMessage;
      this.hidePendingTimestamp();
    } else if (this.pendingTimestamp) {
      if (firstMessage.kind == 'timestamp') {
        this.removePendingTimestamp();
      } else {
        this.showPendingTimestamp();
        this.pendingTimestamp = null;
      }
    }
  }
}

/*--------------------------------------------------------------------------*/

Campfire.Message = Class.create();
Campfire.Message.prototype = {
  initialize: function(element) {
    this.element = element;
    
    var children    = element.getElementsByTagName('td');
    this.authorCell = children[0];
    this.bodyCell   = children[1];

    this.kind = (element.className.match(/\s*(\w+)_message\s*/) || [])[1];
  },
  
  id: function() {
    return parseInt(this.element.id.match(/\d+/)) || 0;
  },
  
  pending: function() {
    return Element.hasClassName(this.element, 'pending');
  },
  
  innerElement: function(name) {
    var node = this[name + 'Cell'];
    if (this.actsLikeTextMessage() || this.kind == 'timestamp')
      return node.childNodes[0];
    return node;    
  },
  
  authorElement: function() {
    return this.innerElement('author');
  },
  
  bodyElement: function() {
    return this.innerElement('body');
  },
  
  author: function() {
    return this.authorElement().innerHTML;
  },
  
  actsLikeTextMessage: function() {
    return this.kind == 'text' || this.kind == 'upload' || this.kind == 'paste' || this.kind == 'sound';
  },

  hideDateForTimestamp: function(lastTimestamp) {
    return !(!lastTimestamp || this.author() != lastTimestamp.author());
  },
  
  hideAuthorForMessage: function(lastMessage) {
    if (!lastMessage) return;
    if (lastMessage.kind == 'timestamp')
      return this.hideDateForTimestamp(lastMessage);
    return !(this.author() != lastMessage.author() ||
      !this.actsLikeTextMessage() || !lastMessage.actsLikeTextMessage());
  },
  
  setKind: function(kind) {
    Element.removeClassName(this.element, this.kind + '_message');
    this.kind = kind;
    Element.addClassName(this.element, this.kind + '_message');
  },
  
  setAuthorVisibilityInRelationTo: function(message) {
    Element[this.hideAuthorForMessage(message) ? 'hide' : 'show'](this.authorElement());
  },
  
  updateBody: function(body) {
    Element.update(this.bodyElement(), body);
  },
  
  getSound: function() {
    var element = $(this.bodyCell);
    if (!element.hasAttribute("data-sound")) return "";
    return element.readAttribute("data-sound");
  },
  
  inspect: function() {
    return $H(this).merge({id: this.id(), pending: this.pending()}).inspect();
  }
}

/*--------------------------------------------------------------------------*/

Campfire.Poller = Class.create();
Campfire.Poller.prototype = {
  initialize: function(chat) {
    this.chat     = chat;
    this.url      = chat.pollURL;
    this.defaultInterval = chat.pollInterval || 3;
    this.interval = this.defaultInterval;
    this.lastCacheID = this.chat.lastCacheID;
    this.timestamp = chat.timestamp;
    this.start();
  },
  
  start: function() {
    this.stopped = this.request = false;
    this.registerTimer();
  },
  
  stop: function() {
    this.stopped = true;
  },
  
  registerTimer: function() {
    window.setTimeout(this.poll.bind(this), this.interval * 1000);
  },
  
  parametersForRequest: function() {
    return $H({
      l: this.lastCacheID,          // l: the last cache fragment id
      m: this.chat.membershipKey,   // m: the user's membership key
      t: $T(),                      // t: the timestamp of the current request, 
                                    //    used only to defeat caching
      s: this.timestamp             // s: the server timestamp of the last 
                                    //    refresh from this client
    });
  },
  
  poll: function() {
    if (this.stopped || this.request) {
      if (this.chat.debug)
        alert('Polling is stopped! stopped=' + this.stopped + ', request=' + this.request);
      return;
    }
    this.request = new Ajax.Request(this.url, {
      parameters: this.parametersForRequest().toQueryString(),
      onComplete: this.onComplete.bind(this),
      onException: this.onException.bind(this)
    });
    this.chat.dispatch('pollStarted');
  },

  onComplete: function() {
    this.interval = this.defaultInterval;
    this.chat.dispatch('pollCompleted');
  },

  onPollCompleted: function() {
    this.request = false;
    this.registerTimer();
  },

  onException: function(exception) {
    if (this.chat.debug)
      alert('Polling exception on ' + this.url + ' at ' + this.interval + 'sec interval: ' + exception);
    this.interval = 2 * this.interval;
    if (this.interval > 60)
      this.interval = 60;
    this.chat.dispatch('pollCompleted');
  }
}

/*--------------------------------------------------------------------------*/

Campfire.Speaker = Class.create();
Campfire.Speaker.prototype = {
  initialize: function(chat) {
    this.chat     = chat;
    this.url      = chat.speakURL;
    this.input    = $(chat.speakElement);
    this.form     = Form.forElement(this.input);
    this.controls = $('chat_controls');
    this.filters  = Campfire.Speaker.Filters.toArray();
    this.registerCallbacks();
    this.focus();
  },
  
  registerCallbacks: function() {
    Event.observe(this.input, 'keypress', this.onKeyPress.bind(this));
    Event.observe(this.input, 'keyup', this.onKeyUp.bind(this));
    Event.observe(this.form, 'submit', this.onSubmit.bind(this));
  },
  
  focus: function() {
    window.setTimeout(Field.focus.bind(Field, this.input), 10);
  },
  
  speak: function(message, kind) {
    if (!(message = this.filterMessage(message))) return;
    if (kind === true) kind = "paste"; // support the old 'sendAsPaste' argument
    
    this.chat.dispatch('preparationForMessageSpoken', message);
    var parameters = {message: message, kind: kind, t: $T()}, element;
    
    if (kind == "paste") {
      element = this.chat.transcript.insertPendingMessage('');
      element.setKind('paste');
      element.updateBody('<span class="pasting">Pasting...</span>');

    } else if (kind == "sound") {
      (function() { this.chat.soundmanager.play(message) }).bind(this).defer();
      element = this.chat.transcript.insertPendingMessage(message, 
        new Template(this.chat.soundTemplate), 
        { description: this.chat.sounds[message] }
      );
            
    } else {
      element = this.chat.transcript.insertPendingMessage(message);
    }
    
    new Ajax.Request(this.url, {
      parameters: $H(parameters).toQueryString(),
      onComplete: this.onComplete.bind(this, element)
    });
    
    this.chat.dispatch('messageSpoken', element);
  },

  send: function(forcePaste) {
    var value = $F(this.input);
    if (value.blank()) return;
    var pasting = forcePaste || value.match(/\r|\n/);
    this.speak(value, pasting ? "paste" : null);
    this.input.value = '';
  },
  
  onSubmit: function(event) {
    this.send();
    Event.stop(event);
    this.focus();
  },
  
  onKeyPress: function(event) {
    this.chat.dispatch('preparationForKeyPress', event);
    switch (event.keyCode) {
      case Event.KEY_RETURN:
      case 3: /* safari sends ASCII 3 for the enter key */
        if (event.shiftKey) {
          return;
        } else if (event.ctrlKey || event.metaKey) {
          this.send(true);
        } else {
          this.send();
        }
        Event.stop(event);
    }
  },
  
  onKeyUp: function(event) {
    this.chat.dispatch('keyPressed', event);
  },
  
  onComplete: function(element, request, messageID) {
    this.chat.dispatch('messageAccepted', element, messageID);
    var body = request.responseText.strip();
    if (body) this.chat.dispatch('messageBodyUpdated', element, body);
  },
  
  filterMessage: function(message) {
    for (var i = 0; i < this.filters.length; i++)
      if (!(message = this.filters[i].call(this, message)))
        return false;
    return message;
  }
}

Campfire.evaluator = function(script) { return eval(script) };

Campfire.Speaker.Filters = [
  function(message) {
    var match;
    if (match = message.match(/^\/me +(.*)/)) {
      if ((match = match[1].toString()).blank()) return false;
      return '*' + match + '*';
    } else {
      return message;
    }
  },
  
  function(message) {
    var matches, sound, description;
    if (matches = message.match(/^\/play +([^ ]+)/)) {
      sound = matches[1].toString();
      if (this.chat.sounds[sound]) {
        this.chat.speaker.speak(sound, "sound");
      }
      return false;
    } else {
      return message;
    }
  },
  
  function(message) {
    var match;
    if (match = message.match(/^\/eval +(.*)/)) {
      if ((match = match[1].toString()).blank()) return false;
      var query = ">>> " + match, result;
      var body  = '<code>' + query.escapeHTML() + '</code>';
      try {
        result = Campfire.evaluator.call(null, match);
        if (typeof result != "undefined")
          result = Object.inspect(result);
      } catch (e) {
        result = e.toString();
      }
      
      if (typeof result != "undefined")
        body += '<br /><pre><code>' + result.escapeHTML() + '</code></pre>';
        
      var element = this.chat.transcript.insertPendingMessage('');
      element.setKind('paste');
      element.updateBody(body);
      
      this.chat.windowmanager.scrollToBottom();
      
    } else {
      return message;
    }
  }
];

/*--------------------------------------------------------------------------*/

Campfire.Addresser = Class.create();
Campfire.Addresser.Pattern = /^(?:\/\/|@)([^\s.:;,-]+)([\s.:;,-]+|$)/;
Campfire.Addresser.prototype = {
  initialize: function(chat) {
    this.chat = chat;
    this.element = $('tooltip');
  },
  
  onMessagesInserted: function() {
    this._participants = null;
  },
  
  onPreparationForKeyPress: function(event) {
    if (this.addressee && event.keyCode == Event.KEY_RETURN)
      this.complete(Event.element(event));
  },
  
  onKeyPressed: function(event) {
    var address;
    if (address = this.extractAddress(Event.element(event)))
      if (this.addressee = this.findAddressee(address))
        return this.showTooltip();

    this.cancelTooltip();
  },
  
  extractAddress: function(element) {
    return $F(element).match(Campfire.Addresser.Pattern);
  },
  
  findAddressee: function(address) {
    var abbreviation = address[1], punctuation = address[2];
    if (!/\S/.test(punctuation)) punctuation = ": ";
    
    var initials    = new RegExp("^" + abbreviation.split("").join(".*\\W"), "i");
    var succession  = new RegExp(abbreviation.split("").join(".*"), "i");
    var participant = this.findParticipantBy(initials) ||
                      this.findParticipantBy(succession);
    
    if (participant) 
      return { participant: participant, punctuation: punctuation };
  },
  
  findParticipantBy: function(pattern) {
    var match;
    if (match = this.participants().grep(pattern))
      if (match.length == 1)
        return match[0].replace(/ \(guest\)$/, "");
  },
  
  complete: function(element) {
    element.value = element.value.replace(
      Campfire.Addresser.Pattern,
      this.addressee.participant + this.addressee.punctuation
    );
    this.cancelTooltip();
  },
  
  showTooltip: function(match) {
    if (!this.element.visible()) 
      this.element.visualEffect("appear", { duration: 0.15 });
    this.element.update(this.addressee.participant + this.addressee.punctuation);
  },

  cancelTooltip: function() {
    if (this.element.visible())
      this.element.visualEffect("fade", { duration: 0.15 });
    this.addressee = null;
  },
  
  participants: function() {
    var elements = $(this.chat.participantList).select("span.name");
    return this._participants = this._participants ||
      elements.pluck("innerHTML").invoke("unescapeHTML");
  }
}

/*--------------------------------------------------------------------------*/

Campfire.Uploader = Class.create();
Campfire.Uploader.prototype = {
  initialize: function(chat) {
    this.chat = chat;
  },
  
  chooseFile: function() {
    Element.show('upload_form_contents');
    Element.hide('upload_form_progress');
  },
  
  start: function() {
    if (this.value()) {
      this.showProgress();
      $('upload_form_tag').submit();
      this.chat.speaker.focus();
    }
  },
  
  reset: function() {
    Element.update('upload_form_tag', $('upload_form_tag').innerHTML);
  },
  
  showProgress: function() {
    Element.show('upload_form_progress');
    Element.hide('upload_form_contents');
    Element.update('upload_form_status', 
      'Uploading <strong>' + this.filename() + '</strong>&hellip;');
  },
  
  waitForMessage: function(id) {
    this.pending = id;
    if (this.messageExists(id))
      this.finish();
    else
      Element.update('upload_form_status', 'Finishing upload&hellip;');
  },
  
  messageExists: function() {
    return !!$('message_' + this.pending);
  },

  finish: function() {
    if (!this.pending) return;
    this.reset();
    this.chooseFile();
    $('uploader').showHide.hide();
    delete this.pending;
  },
  
  value: function() {
    return $('upload').value;
  },
  
  filename: function() {
    var value = this.value();
    return (value.match(/([^:\\\/]+)$/) || [null, value])[1];
  },
  
  onMessagesInserted: function(messages) {
    if (!this.messageExists(this.pending)) return;
    this.finish();
  },

  enableUploads: function(flag) {
    Element[flag ? 'show' : 'hide']('upload_file_link');
  }
}

/*--------------------------------------------------------------------------*/

Campfire.SoundManager = Class.create();
Campfire.SoundManager.prototype = {
  initialize: function(chat) {
    this.chat    = chat;
    this.enabled = chat.soundsEnabled;
    this.sounds  = {};

    if ($('sounds')) {
      $('sounds').hide();
      window.soundManager.onerror = (function() {
        this.enabled = false;
      }).bind(this);
      window.soundManager.onload = function() {
        $('sounds').show();
        window.soundManager.onerror = Prototype.emptyFunction;
      }
    }
  },
  
  getURLForSound: function(sound) {
    return '/sounds/' + sound + '.mp3';
  },
  
  getSound: function(sound) {
    return this.sounds[sound] = this.sounds[sound] || soundManager.createSound({
      id:  sound,
      url: this.getURLForSound(sound)
    });
  },

  play: function(sound, force) {
    if (!force && this.isMuted()) return;
    this.getSound(sound).play();
    this.chat.dispatch('soundPlayed', this.getURLForSound(sound));
  },
  
  playMessage: function(element) {
    var message = new Campfire.Message(element.up("tr"));
    this.play(message.getSound(), true);
    if (this.chat.speaker) this.chat.speaker.focus();
  },

  onMessagesInserted: function(messages) {
    var sound;
    
    for (var i = 0; i < messages.length; i++) {
      if (messages[i].kind == 'sound')
        sound = messages[i].getSound();
      else if (!sound && messages[i].actsLikeTextMessage())
        sound = 'incoming';
    }
    
    if (sound) this.play(sound);
  },
  
  onPreparationForMessageSpoken: function() {
    this.speaking = true;
  },
  
  onMessageSpoken: function() {
    this.speaking = false;
  },
  
  isMuted: function() {
    return !this.enabled || this.speaking;
  }
}

/*--------------------------------------------------------------------------*/

if (/Safari/.test(navigator.userAgent)) {
  // Safari doesn't fire window.onfocus/window.onblur events for tabs, but
  // it *does* pause Flash movies in background tabs.  Sneaky!
  
  Campfire.Responders.push('TabFocusDetector');

  Campfire.TabFocusDetector = Class.create();
  Campfire.TabFocusDetector.prototype = {
    initialize: function(chat) {
      this.chat = chat;
      this.createFlashProxy();
      Event.observe(window, 'load', this.start.bind(this));
    },
  
    createFlashProxy: function() {
      var uid   = 'flash_tab_focus_detector';
      var proxy = new FlashProxy(uid, '/movies/javascript_flash_gateway.swf');
      var tag   = new FlashTag('/movies/sound_player.swf', 1, 1);

      tag.setFlashvars($H({ lcId: uid, mp3Url: '/sounds/incoming.mp3' }).toQueryString());
      $(document.body).insert(tag.toString());
      
      this.proxy = proxy;
    },
  
    start: function() {
      if (!this.interval && this.proxy) {
        this.registerCallback();
        this.interval = window.setInterval(this.tick.bind(this), 100);
        this.ping();
      }
    },
  
    stop: function() {
      if (this.interval)
        window.clearInterval(this.interval);
    },
  
    ping: function() {
      this.time = $T();
      if (this.paused) {
        this.paused = false;
        this.chat.windowmanager.onFocus();
      }
    },
    
    tick: function() {
      var paused = $T() - this.time > 1250;
      if (paused && !this.paused) {
        this.paused = true;
        this.chat.windowmanager.onBlur();
      }
    },
    
    registerCallback: function() {
      this.callbackName = 'tabFocusDetectorCallback_' + $T();
      window[this.callbackName] = this.ping.bind(this);
      this.proxy.call('startTimer', this.callbackName);
    }
  }
}

/*--------------------------------------------------------------------------*/

Campfire.RequestWatchdog = Class.create();
Campfire.RequestWatchdog.Threshold = 5;
Campfire.RequestWatchdog.prototype = {
  initialize: function(chat) {
    this.chat = chat;
    this.requests = [];
    Ajax.Responders.register(this);
  },
  
  onCreate: function(request) {
    request.timestamp = $T();
    this.requests.push(request);
    this.setTimeoutForRequest(request);
  },
  
  onComplete: function(request) {
    this.requests = this.requests.without(request);
  },
  
  onRequestTimeout: function(request) {
    if (this.requests.include(request)) {
      this.log(request);
      request.transport.abort();
      this.chat.poller.stop();
      this.onComplete(request);
      this.chat.poller.start();
    }
  },
  
  setTimeoutForRequest: function(request, timeout) {
    return window.setTimeout(this.onRequestTimeout.bind(this, request),
      (timeout || Campfire.RequestWatchdog.Threshold) * 1000);
  },
  
  log: function(request) {
    var params = $H({ 
      timestamp:  request.timestamp,
      location:   request.url,
      readyState: request.transport.readyState
    }).toQueryString();
    
    var image = $(document.body.appendChild(document.createElement("img")));
    image.setStyle({ position: "absolute", left: 0, top: 0, width: "1px", height: "1px" });
    image.src = window.location.protocol + "//123.campfirenow.com/images/jslog.gif?" + params;
    image.onload = function() { image.remove() };
  }
};

/*--------------------------------------------------------------------------*/

if (window.fluid) {
  Object.extend(Campfire.WindowManager.prototype, {
    updateUnreadCounter: function() {
      fluid.setDockBadge((this.unreadCount || "").toString());
    }
  });
  
  Campfire.GrowlNotifier = Class.create({
    initialize: function(chat) {
      this.chat = chat;
      this.pattern = new RegExp("^" + RegExp.escape(this.chat.username));
    },
    
    onMessagesInserted: function(messages) {
      for (var i = 0; i < messages.length; i++) {
        var message = messages[i];
        if (message.kind == "text") {
          var bodyElement = message.bodyElement();
          var body = bodyElement.innerHTML.unescapeHTML();

          if (body.match(this.pattern)) {
            window.fluid.showGrowlNotification({
              title: document.title,
              description: "(" + message.author() + ") " + body,
              priority: 1,
              sticky: false,
              onclick: function() {
                bodyElement.visualEffect("highlight", { duration: 2 });
              }
            });
          }
        }
      }
    }
  });
  
  Campfire.Responders.push("GrowlNotifier");
}
