;;; rudel.el --- A collaborative editing framework for Emacs
;;
;; Copyright (C) 2008, 2009 Jan Moringen
;;
;; Author: Jan Moringen <scymtym@users.sourceforge.net>
;; Keywords: rudel, collaboration
;; URL: http://rudel.sourceforge.net/
;; X-RCS: $Id:$
;;
;; This file is part of Rudel.
;;
;; Rudel is free software: you can redistribute it and/or modify it
;; under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;;
;; Rudel is distributed in the hope that it will be useful, but
;; WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
;; General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with rudel. If not, see <http://www.gnu.org/licenses>.


;;; Commentary:
;;
;; Rudel is a framework for collaborative editing in Emacs.  Its
;; architecture allows communication with arbitrary collaborative
;; editors.


;;; History:
;;
;; 0.1 - Initial revision.


;;; Code:
;;

(eval-when-compile
  (require 'cl))

(require 'eieio)
(require 'eieio-base)

(require 'eieio-speedbar) ;; TODO required for now

(require 'rudel-util)
(require 'rudel-backend)
(require 'rudel-session-initiation)
(require 'rudel-operations)
(require 'rudel-operators)
(require 'rudel-overlay)
(require 'rudel-hooks)
(require 'rudel-interactive) ;; for `rudel-read-backend',
			     ;; `rudel-read-document',
			     ;; `rudel-read-session'
(require 'rudel-icons)
(require 'rudel-compat) ;; for `read-color' replacement


;;; Global variables
;;

(defconst rudel-version '(0 3)
  "Version of the Rudel framework.")

(defvar rudel-current-session nil
  "Global object representing the current Rudel session.
nil if there is no active session.")

(defvar rudel-buffer-document nil
  "Buffer-local variable which holds the Rudel document associated with the buffer.")
(make-variable-buffer-local 'rudel-buffer-document)
(put 'rudel-buffer-document 'permanent-local t)

(defvar rudel-buffer-change-workaround-data nil
  "Buffer-local variable which holds change data that could not be accessed otherwise.
It would be nice to find another way to do this.")
(make-variable-buffer-local 'rudel-buffer-change-workaround-data)
(put 'rudel-buffer-change-workaround-data 'permanent-local t)


;;; Customization
;;

(defgroup rudel nil
  "Rudel collaborative editing framework."
  :group 'applications)

(defcustom rudel-allocate-buffer-function
  'rudel-allocate-buffer-clear-existing
  "A function used to find or create buffers to associate to documents.
The function is called with the document name as the sole
argument and has to return a buffer object which will be attached
to the document in question."
  :group   'rudel
  :type    '(choice (const :tag "Clear content of existing buffer"
			   rudel-allocate-buffer-clear-existing )
		    (const :tag "Create a new uniquely named buffer"
			   rudel-allocate-buffer-make-unique )
		    (function :tag "Other function"))
  :require 'rudel-interactive)

(defcustom rudel-default-username (user-login-name)
  "*"
  :group 'rudel
  :type  '(string))


;;; Class rudel-session
;;

(defclass rudel-session (rudel-hook-object)
  ((backend              :initarg  :backend
			 :type     rudel-backend-child
			 :documentation
			 "The backend used by this session.")
   (users                :initarg  :users
			 :type     list
			 :initform nil
			 :documentation
			 "The list of users participating in this
session.")
   (documents            :initarg  :documents
			 :type     list
			 :initform nil
			 :documentation
			 "This list of documents available in
this session.")
   ;; Hooks
   (end-hook             :initarg  :end-hook
			 :type     list
			 :initform nil
			 :documentation
			 "")
   (add-user-hook        :initarg  :add-user-hook
			 :type     list
			 :initform nil
			 :documentation
			 "This hook is run when a user gets added
to the session.
The arguments are the session and the user object.")
   (remove-user-hook     :initarg  :remove-user-hook
			 :type     list
			 :initform nil
			 :documentation
			 "This hook is run when a user gets
removed from the session.
The arguments are the session and the user object.")
   (add-document-hook    :initarg  :add-document-hook
			 :type     list
			 :initform nil
			 :documentation
			 "This hook is run when a document gets
added to the session.
The arguments are the session and the document object.")
   (remove-document-hook :initarg  :remove-document-hook
			 :type     list
			 :initform nil
			 :documentation
			 "This hook is run when a document gets
removed from the session.
The arguments are the session and the document object."))
  "This class serves as a base class for rudel-client-session and
rudel-server-session. Consequently, it consists of slots common
to client and server sessions."
  :abstract t)

(defmethod rudel-end ((this rudel-session))
  "Terminate THIS session performing all necessary cleanup."
  ;; Run the hook.
  (object-run-hook-with-args this 'end-hook))

(defmethod rudel-add-user ((this rudel-session) user)
  "Add USER to the user list of THIS session.

Runs object hook (see `rudel-hook-object') `add-user-hook' with
arguments THIS and USER."
  ;; Add USER to list.
  (object-add-to-list this :users user)

  ;; Run the hook.
  (object-run-hook-with-args this 'add-user-hook user))

(defmethod rudel-remove-user ((this rudel-session) user)
  "Remove USER from the user list of THIS session.

Runs object hook (see `rudel-hook-object') `remove-user-hook'
with arguments THIS and USER."
  ;; Remove USER from list.
  (object-remove-from-list this :users user)

  ;; Run the hook.
  (object-run-hook-with-args this 'remove-user-hook user))

(defmethod rudel-find-user ((this rudel-session)
			    which &optional test key)
  "Find user WHICH in the user list.
WHICH is compared to the result of KEY using TEST."
  (unless test
    (setq test #'string=))
  (unless key
    (setq key #'object-name-string))
  (with-slots (users) this
    (find which users :key key :test test))
  )

(defmethod rudel-add-document ((this rudel-session) document)
  ""
  (unless (slot-boundp document :session)
    (oset document :session this))

  ;; Add DOCUMENT to the list of documents in THIS session.
  (object-add-to-list this :documents document)

  ;; Run the hook.
  (object-run-hook-with-args this 'add-document-hook document))

(defmethod rudel-remove-document ((this rudel-session) document)
  "Remove DOCUMENT from THIS session, detaching it if necessary."
  ;; Detach document from its buffer when necessary.
  (when (rudel-attached-p document)
    (rudel-detach-from-buffer document))

  ;; Remove DOCUMENT from the list of documents in THIS session.
  (object-remove-from-list this :documents document)

  ;; Run the hook.
  (object-run-hook-with-args this 'remove-document-hook document))

(defmethod rudel-find-document ((this rudel-session)
				which &optional test key)
  "Find document WHICH in the document list.
WHICH is compared to the result of KEY using TEST."
  (unless test
    (setq test #'string=))
  (unless key
    (setq key #'object-name-string))
  (with-slots (documents) this
    (find which documents :key key :test test))
  )

(defmethod rudel-unsubscribed-documents ((this rudel-session))
  ""
  (unless (slot-boundp this :self)
    (error "Cannot find unsubscribed documents unless slot self is bound"))
  (with-slots (documents self) this
    (remove-if
     (lambda (document)
       (with-slots (subscribed) document
	 (memq self subscribed)))
       documents))
  )


;;; Class rudel-client-session
;;
(defclass rudel-client-session (rudel-session)
  ((connection :initarg  :connection
	       :type     (or null rudel-connection-child)
	       :initform nil
	       :documentation
	       "The connection used for communication by this
session.")
   (self       :initarg  :self
	       :type     rudel-user-child
	       :documentation
	       "Points into USERS to the user object representing
the local user"))
  "Objects represent a collaborative editing session from a
client perspective.")

(defmethod rudel-end ((this rudel-client-session))
  ;; Clean everything up
  (with-slots (connection users documents) this
    ;; Detach all documents from their buffers
    (mapc #'rudel-detach-from-buffer documents)

    ;; Terminate the connection
    (when connection
      (condition-case nil
	  (rudel-disconnect connection)
	(error nil))))

  ;;
  (when (next-method-p)
    (call-next-method))
  )


;;; Class rudel-server-session
;;

(defclass rudel-server-session (rudel-session)
  ()
  "Class rudel-server-session "
  :abstract t)


;;; Class rudel-connection
;;

(defclass rudel-connection ()
  ((session :initarg :session
	    :type    rudel-session-child
	    :documentation
	    ""))
  "This abstract class defines the interface implementations of
client protocols have to obey."
  :abstract t)

(defgeneric rudel-disconnect ((this rudel-connection))
  "Close the connection.")

(defgeneric rudel-change-color- ((this rudel-connection) color) ;; TODO name
  "")

(defgeneric rudel-publish ((this rudel-connection) document)
  "")

(defgeneric rudel-subscribe-to ((this rudel-connection) document)
  "")

(defgeneric rudel-unsubscribe-from ((this rudel-connection) document) ; TODO name should be rudel-unsubscribe
  "")

(defgeneric rudel-local-insert ((this rudel-connection))
  "")

(defgeneric rudel-local-delete ((this rudel-connection))
  "")

(defgeneric rudel-remote-insert ((this rudel-connection))
  "")

(defgeneric rudel-remote-delete ((this rudel-connection))
  "")


;;; Class rudel-user
;;

(defclass rudel-user (eieio-named
		      eieio-speedbar-file-button
		      rudel-hook-object)
  ((color       :initarg  :color
		:accessor rudel-color
		:documentation
		"Color used to indicate ownership or authorship
by the user. Examples includes text written by the user or the
user name itself.")
   (change-hook :initarg  :change-hook
		:type     list
		:initform nil
		:documentation
		"This hook is run when this user object
changes."))
  "Objects of this class represent users participating in
collaborative editing session. Note that a participating user
does not have to be connected to the session at any given time."
  :abstract t)

(defmethod rudel-display-string ((this rudel-user)
				 &optional use-images align)
  "Return a textual representation of THIS for user interface stuff."
  (with-slots ((name :object-name) color) this
    (propertize
     (concat
      (when use-images
	(propertize "*" 'display rudel-icon-person))
      name)
     'face (list :background color)))
  )


;;; Class rudel-document
;;

(defclass rudel-document (eieio-named
			  eieio-speedbar-file-button
			  rudel-hook-object)
  ((session          :initarg  :session
		     :type     rudel-session
		     :documentation
		     "")
   (buffer           :initarg  :buffer
		     :type     (or null buffer)
		     :initform nil
		     :documentation
		     "")
   (subscribed       :initarg  :subscribed
		     :type     list
		     :initform nil
		     :documentation
		     "")
   ;; Hooks
   (subscribe-hook   :initarg  :subscribe-hook
		     :type     list
		     :initform nil
		     :documentation
		     "This hook is run when a user subscribes to
this document object.")
   (unsubscribe-hook :initarg  :unsubscribe-hook
		     :type     list
		     :initform nil
		     :documentation
		     "This hook is run when a user unsubscribes
from this document object.")
   (attach-hook      :initarg  :attach-hook
		     :type     list
		     :initform nil
		     :documentation
		     "This hook is run when a buffer is attached
to this document object.")
   (detach-hook      :initarg  :detach-hook
		     :type     list
		     :initform nil
		     :documentation
		     "This hook is run when the attached buffer
is detached from this document object."))
  "This class represents a document, which participants of a
collaborative editing session can subscribe to."
  :abstract t)

(defmethod rudel-unique-name ((this rudel-document))
  "Returns a suggested name for the buffer attached to THIS document."
  (object-name-string this))

(defmethod rudel-suggested-buffer-name ((this rudel-document))
  "Returns a suggested name for the buffer attached to THIS document."
  (rudel-unique-name this))

(defmethod rudel-attached-p ((this rudel-document))
  (with-slots (buffer) this
    buffer))

(defmethod rudel-attach-to-buffer ((this rudel-document) buffer)
  "Attach THIS document to BUFFER"
  (with-slots ((doc-buffer :buffer)) this
    ;; Set buffer slot of THIS to BUFFER and associated THIS with
    ;; BUFFER.
    (setq doc-buffer buffer)
    (rudel-set-buffer-document this buffer)

    (with-current-buffer doc-buffer
      ;; Add the handler function for buffer changes to the buffer's
      ;; change hook.
      (add-hook 'after-change-functions
		#'rudel-handle-buffer-change
		nil t)

      ;; Store change data before the change a done. This is necessary
      ;; because the number of bytes (not characters) cannot otherwise
      ;; be recovered after a deletion.
      (add-hook 'before-change-functions
		#'rudel-buffer-change-workaround
		nil t)

      ;; Add a handler to the kill-buffer hook to unsubscribe from the
      ;; document when the buffer gets killed.
      (add-hook 'kill-buffer-hook
		#'rudel-unpublish-buffer
		nil t)

      ;;
      (add-hook 'change-major-mode-hook
		#'rudel-handle-major-mode-change
		nil t))

    ;; Run the hook.
    (object-run-hook-with-args this 'attach-hook doc-buffer))
  )

(defmethod rudel-detach-from-buffer ((this rudel-document))
  "Detach document THIS from its buffer.
Do nothing, if THIS is not attached to any buffer."
  (with-slots (buffer) this
    (let ((buffer-save buffer))

      ;; Only try to detach from BUFFER, if it is non-nil. BUFFER can
      ;; be nil, if the user did not subscribe to the document, or
      ;; unsubscribed after subscribing.
      (when buffer

	(with-current-buffer buffer
	  ;; Remove our handler function from the kill-buffer hook.
	  (remove-hook 'kill-buffer-hook
		       #'rudel-unpublish-buffer
		       t)

	  ;; Remove our handler function from the after-change hook.
	  (remove-hook 'after-change-functions
		       #'rudel-handle-buffer-change
		       t)

	  ;; Remove our handler function from the before-change hook.
	  (remove-hook 'before-change-functions
		       #'rudel-buffer-change-workaround
		       t)

	  ;; Remove all overlays.
	  (rudel-overlays-remove-all)

	  ;; Remove the major mode change handler.
	  (remove-hook 'change-major-mode-hook
		       #'rudel-handle-major-mode-change
		       t))

	;; Unset buffer slot of THIS and delete association of THIS with
	;; BUFFER.
	(rudel-set-buffer-document nil buffer)
	(setq buffer nil))

      ;; Run the hook.
      (object-run-hook-with-args this 'detach-hook buffer-save)))
  )

(defmethod rudel-add-user ((this rudel-document) user)
  "Add USER to the list of subscribed users of THIS.

Runs object hook (see `rudel-hook-object') `subscribe-hook' with
arguments THIS and USER."
  ;; Add USER to list.
  (object-add-to-list this :subscribed user)

  ;; Run the hook.
  (object-run-hook-with-args this 'subscribe-hook user))

(defmethod rudel-remove-user ((this rudel-document) user)
  "Remove USER from the list of subscribed users of THIS.

Runs object hook (see `rudel-hook-object') `unsubscribe-hook'
with arguments THIS and USER."
  ;; Remove USER from list.
  (object-remove-from-list document :subscribed user)

  ;; Run the hook.
  (object-run-hook-with-args this 'unsubscribe-hook user))

(defmethod rudel-insert ((this rudel-document) position data)
  "Insert DATA at POSITION into the buffer attached to THIS.
When POSITION is nil `point-max' is used to determine the
insertion position.
Modification hooks are disabled during the insertion."
  (with-slots (buffer) this
    (save-excursion
      (set-buffer buffer)

      (unless position
	(setq position (- (point-max) 1)))

      (let ((inhibit-modification-hooks t))
	(goto-char (+ position 1))
	(insert data))))
  )

(defmethod rudel-delete ((this rudel-document) position length)
  "Delete a region of LENGTH character at POSITION from the buffer attached to THIS.
Modification hooks are disabled during the insertion."
  (with-slots (buffer) this
    (save-excursion
      (set-buffer buffer)

      (let ((inhibit-modification-hooks t))
	(delete-region (+ position 1) (+ position length 1)))))
  )

(defmethod rudel-local-operation ((this rudel-document) operation)
  "Apply the local operation OPERATION to THIS."
  (with-slots (session buffer) this
    (with-slots (connection (user :self)) session
      (dolist (operators (list

			   ;; Update overlays
			   (rudel-overlay-operators
			    "overlay-operators"
			    :document this
			    :user     user)

			   ;; Notify connection
			   (rudel-connection-operators
			    "connection-operators"
			    :connection connection
			    :document   this)))

	;; Apply the operation using each set of operators
	(rudel-apply operation operators))))
  )

(defmethod rudel-remote-operation ((this rudel-document) user operation)
  "Apply the remote operation OPERATION performed by USER to THIS."
  (dolist (operators (append

		       ;; Update buffer contents
		       (list (rudel-document-operators
			      "document-operators"
			      :document this))

		       ;; Update overlays
		       (when user
			 (list (rudel-overlay-operators
				"overlay-operators"
				:document this
				:user     user)))))

    ;; Apply the operation using each set of operators
    (rudel-apply operation operators))
  )

(defmethod rudel-chunks ((this rudel-document))
  "Return a list of text chunks of the associated buffer.
Each element in the chunk is a list structured like this (START
END AUTHOR). START and END are numbers, AUTHOR is of type (or
null rudel-user-child)."
  (with-slots (buffer) this
    ;; Extract buffer string and a list of chunks partitioning the
    ;; string according to the respective author (or nil).
    (with-current-buffer buffer
      (let ((string         (buffer-string)) ;; TODO no-properties?
	    (overlay-chunks (mapcar
			     (lambda (overlay)
			       (list (- (overlay-start overlay) 1)
				     (- (overlay-end   overlay) 1)
				     (rudel-overlay-user overlay)))
			     (sort* (rudel-author-overlays)
				    '< :key 'overlay-start)))
	    (last)
	    (augmented-chunks))

	;; Iterate through the list of chunks to find gaps between
	;; chunks (also before the first) and insert entries with
	;; author nil accordingly.
	(dolist (chunk overlay-chunks)
	  (when (or (and (not last)
			 (> (nth 0 chunk) 0))
		    (and last
			 (/= (nth 1 last)
			     (nth 0 chunk))))
	    (push (list (if last (nth 1 last) 0)
			(nth 0 chunk)
			nil)
		  augmented-chunks))
	  (push chunk augmented-chunks)
	  (setq last chunk))

	;; If there is text after the last chunk, create another one
	;; with author nil. If there were no chunks at all, this chunk
	;; can also cover the whole buffer string.
	(when (or (and (not last)
		       (/= (length string) 0))
		  (and last
		       (/= (nth 1 last) (length string))))
	  (push (list (if last (nth 1 last) 0)
		      (length string)
		      nil)
		augmented-chunks))

	;; Sort chunks according to the start position.
	(sort* augmented-chunks '< :key 'car))))
  )


;;; Buffer-related functions
;;

(defun rudel-buffer-has-document-p (&optional buffer)
  "Return non-nil if a document object is attached to BUFFER.
If BUFFER is nil, use the current buffer."
  (unless buffer
    (setq buffer (current-buffer)))

  (buffer-local-value 'rudel-buffer-document buffer))

(defun rudel-buffer-document (&optional buffer)
  "Return the document object attached to BUFFER.
If BUFFER is nil, use the current buffer."
  (unless buffer
    (setq buffer (current-buffer)))

  (buffer-local-value 'rudel-buffer-document buffer))

(defun rudel-set-buffer-document (document &optional buffer)
  "Associate BUFFER to DOCUMENT.
If DOCUMENT is nil, make it not associated to any buffer.
If BUFFER is nil, use the current buffer."
  (unless buffer
    (setq buffer (current-buffer)))

  (with-current-buffer buffer
    (setq rudel-buffer-document document)))

(defun rudel-handle-buffer-change (from to length)
  "Handle buffer change at range FROM to TO with length LENGTH by relaying them to the document object of the buffer.
See after-change-functions for more information."
  (when (rudel-buffer-has-document-p)
    (let ((document (rudel-buffer-document))
	  (text)) ; TODO with-rudel-buffer-document?
      (cond
       ;; The change was an insert
       ((and (/= from to)
	     (zerop length))
	(with-slots (buffer) document
	  (with-current-buffer buffer
	    (setq text (buffer-substring-no-properties from to)))
	  (rudel-local-operation document
				 (rudel-insert-op
				  "insert"
				  :from (- from 1)
				  :data text))))

       ;; The change was a delete
       ((and (= from to)
	     (not (zerop length)))
	(rudel-local-operation document
			       (rudel-delete-op
				"delete"
				:from   (- from 1)
				:length length)))

       ;; The operation was neither an insert nor a delete. This seems
       ;; to mean that the region has changed arbitrarily. The only
       ;; option we have is sending a delete and corresponding insert
       ;; message that emulate the change.
       (t
	(with-slots (buffer) document
	  (with-current-buffer buffer
	    (setq text (buffer-substring-no-properties from to)))
	  (rudel-local-operation document
				 (rudel-delete-op
				  "delete"
				  :from   (- from 1)
				  :length length))
	  (rudel-local-operation document
				 (rudel-insert-op
				  "insert"
				  :from (- from 1)
				  :data text)))))))
  )

(defun rudel-buffer-change-workaround (from to)
  (when (/= from to)
    (setq rudel-buffer-change-workaround-data
	  (list from to
		(buffer-substring-no-properties from to)))))


;;; Protection against major mode changes
;;

(defvar rudel-mode-changed-buffers nil
  "List of buffers that may need to be repaired after a major
  mode change.")

(defun rudel-handle-major-mode-change ()
  "Store the current buffer to repair damage done by major mode change.

Note: The way this works is inspired by mode-local.el by David
Ponce and Eric M. Ludlam."
  ;; Store the buffer for later repair.
  (add-to-list 'rudel-mode-changed-buffers (current-buffer))

  ;; Schedule `rudel-after-major-mode-change' to run after the
  ;; command, that caused the major mode change.
  (add-hook 'post-command-hook
	    #'rudel-after-major-mode-change)
  )

(defun rudel-after-major-mode-change ()
  "Repair damage done by major mode changes.
As a function in `post-command-hook', this is run after there was
a `major-mode' change.

Note: The way this works is inspired by mode-local.el by David
Ponce and Eric M. Ludlam."
  ;; Remove this function from `post-command-hook'.
  (remove-hook 'post-command-hook
	       #'rudel-after-major-mode-change)

  ;; Repair all buffers affected by the major mode change.
  (dolist (buffer rudel-mode-changed-buffers)
    (let ((document (buffer-local-value 'rudel-buffer-document
					buffer)))
      (rudel-attach-to-buffer document buffer)))
  )


;;; Interactive functions
;;

;;;###autoload
(defun rudel-join-session (info)
  "Join the collaborative editing session described by INFO.
INFO is a property list that describes the collaborative editing
session in terms of properties like :host, :port
and :encryption. The particular properties and their respective
meanings depend on the used backend.

When called interactively, all data required to join a session
will be prompted for."
  (interactive
   ;; Try the discover method of session initiation backends to find
   ;; available sessions.
   (list
    (let ((info)
	  (session-initiation-backend))
      (while (not info)
	(message "Discovering Sessions ...")
	(let* ((sessions   (rudel-session-initiation-discover
			    session-initiation-backend))
	       (maybe-info (if (= (length sessions) 1)
				 (car sessions)
			       (rudel-read-session
				sessions "Choose Session: " 'object))))
	  (if (rudel-backend-cons-p maybe-info)
	      (setq session-initiation-backend (car maybe-info))
	    (setq info maybe-info))))
    info)))

  ;; First, create the session object.
  (let* ((backend    (cdr (plist-get info :backend)))
	 (session    (rudel-client-session
		      (plist-get info :name)
		      :backend backend))
	 (connection))
    ;; Give the backend a chance to collect remaining connect
    ;; info. For session initiation methods like Zeroconf, we have the
    ;; _connection_ info, but are still missing the username and
    ;; stuff.
    (setq info (rudel-ask-connect-info backend info))

    ;; Add the session object to the connect information.
    (plist-put info :session session)

    ;; Ask BACKEND to connect using INFO. Do not catch errors since
    ;; the error messages are probably the best feedback we can give.
    (setq connection (rudel-connect backend info))

    ;; Set the connection slot of the session object and store it
    ;; globally.
    (oset session :connection connection)
    (setq rudel-current-session session)

    ;; Reset the global session variable when the session ends.
    (object-add-hook session 'end-hook
		     (lambda (session)
		       (setq rudel-current-session nil)))

    ;; Run the hook.
    (run-hook-with-args 'rudel-session-start-hook session))
  )

;;;###autoload
(defun rudel-host-session ()
  "Host a collaborative editing session.
All data required to host a session will be prompted for
interactively."
  (interactive)
  ;; If necessary, ask the user for the backend we should use.
  (let* ((backend (cdr (rudel-backend-choose
			'protocol
			(lambda (backend)
			  (rudel-capable-of-p backend 'host)))))
	 (info    (rudel-ask-host-info backend))
	 (server))

    ;; Try to create the server
    (condition-case error-data
	(setq server (rudel-host backend info))
      ('error
       (error "Could not host session using backend `%s' with %s: %s"
	      (object-name-string backend)
	      info
	      (car error-data))))
    server))

;;;###autoload
(defun rudel-end-session ()
  "End the current collaborative editing session."
  (interactive)
  (unless rudel-current-session
    (error "No active Rudel session"))

  ;; Actually end the session.
  (rudel-end rudel-current-session)
  )

;;;###autoload
(defun rudel-change-color ()
  "Change the color associated with the local user.
Not all backends support this operation."
  (interactive)
  ;; Make sure we have a session.
  (unless rudel-current-session
    (error "No active Rudel session"))

  (with-slots (backend connection self) rudel-current-session
    ;; Make sure the backend can change colors.
    (unless (rudel-capable-of-p backend 'change-color)
      (error "Backend `%s' cannot change colors"
	     (object-name-string backend)))

    (with-slots ((name :object-name) color) self
      ;; Ask the user for a new color.
      (setq color (read-color "New Color: " t))

      ;; Tell the connection to announce the change and change it in
      ;; our user object.
      (rudel-change-color- connection color)

      ;; Run the change hook.
      (object-run-hook-with-args self 'change-hook)

      ;; Update overlay color.
      (rudel-overlay-set-face-attributes
       (rudel-overlay-make-face-symbol 'author name)
       color)))
  )

;;;###autoload
(defun rudel-subscribe (document)
  "Subscribe to DOCUMENT offered by a peer in a collaborative editing session.
When called interactively, DOCUMENT is prompted for interactively."
  (interactive
   (list (progn
	   ;; We have to retrieve the document list from an active
	   ;; session.
	   (unless rudel-current-session
	     (error "No active Rudel session"))
	   ;; Select unsubscribed documents.
	   (let ((documents (rudel-unsubscribed-documents
			     rudel-current-session)))
	     ;; Already subscribed to all documents. This is an error.
	     (when (null documents)
	       (error "No unsubscribed documents"))
	     ;; Read an unsubscribed document.
	     (rudel-read-document documents nil 'object)))))

  ;; Make sure we have a session.
  (unless rudel-current-session
    (error "No active Rudel session"))

  ;; Create a new buffer and attach the document to it.
  (let* ((name   (rudel-suggested-buffer-name document))
	 (buffer (funcall rudel-allocate-buffer-function name)))
    (rudel-attach-to-buffer document buffer)

    (let ((connection (oref (oref document :session) :connection)))
      (rudel-subscribe-to connection document))

    ;; Show the new buffer.
    (set-window-buffer nil buffer))
  )

;;;###autoload
(defun rudel-publish-buffer (&optional buffer)
  "Make the BUFFER available for subscription to peers in a collaborative editing session.
If BUFFER is nil, the current buffer is used."
  (interactive (list nil))

  ;; Make sure we have a session.
  (unless rudel-current-session
    (error "No active Rudel session"))

  (unless buffer
    (setq buffer (current-buffer)))

  (with-current-buffer buffer
    (when (rudel-buffer-has-document-p)
      (error "Buffer already published or subscribed"))) ; TODO keep this?

  ;;
  (with-slots (backend connection self) rudel-current-session
    (let ((document (rudel-make-document backend
					 (buffer-name buffer)
					 rudel-current-session)))
      (rudel-add-document rudel-current-session document)

      (rudel-attach-to-buffer document buffer)
      (object-add-to-list document :subscribed self)

      (rudel-publish connection document)))
  )

;;;###autoload
(defun rudel-unpublish-buffer (&optional buffer)
  "Deny peers access to BUFFER in a collaborative editing session.
If BUFFER is nil, the current is used."
  (interactive)

  ;; Make sure we have a session.
  (unless rudel-current-session
    (error "No active Rudel session"))

  (unless buffer
    (setq buffer (current-buffer)))

  (with-current-buffer buffer
    (unless (rudel-buffer-has-document-p)
      (error "Buffer is not published")))

  ;;
  (with-slots (connection) rudel-current-session
    (let ((document (rudel-buffer-document buffer)))
      (rudel-detach-from-buffer document)

      (rudel-unsubscribe-from connection document)))
  )

(provide 'rudel)
;;; rudel.el ends here