;;; gnus-virt.el --- framework for "virtual" newsgroups

;; Copyright (C) 1993, 1994 Jamie Zawinski <jwz@lucid.com>

;; This file is part of GNU Emacs.

;; GNU Emacs 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 2, or (at your option)
;; any later version.

;; GNU Emacs 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 GNU Emacs; see the file COPYING.  If not, write to
;; the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.

;; The basic idea here is this: given a set of message identifiers (either
;; message-id strings, or newsgroup/message-number pairs), display a standard
;; GNUS *Summary* buffer in which all the usual commands "just work," as if
;; all of these articles appeared in the same newsgroup (even though they
;; don't, necessarily.)
;;
;; This file does not implement the method of getting the message identifiers,
;; but it implements the substrate necessary to navigate them.

;; This requires NNTP, because though GNUS makes it easy to define entirely
;; new access methods, it doesn't provide an easy way of subtly modifying the
;; behavior of article-access in ways that shouldn't require knowledge of the
;; underlying method.


(require 'gnus)
(require 'nntp)

(defvar gnus-virt-message-numbers nil
  "A vector mapping pseudo-message-id-numbers to message-id strings
or to conses of (\"newsgroup-name\" . \"article-number\").")

(or (fboundp 'gnus-virt-orig-nntp-request-article)
    (fset 'gnus-virt-orig-nntp-request-article
	  (symbol-function 'nntp-request-article)))

(defun nntp-request-article (id)
  "Select article by message ID (or number)."
  (if (and (numberp id) gnus-virt-message-numbers)
      (setq id (aref gnus-virt-message-numbers id)))
  (if (consp id)
      (progn
	(gnus-request-group (car id))
	(setq id (cdr id))))
  (gnus-virt-orig-nntp-request-article id))


(defun gnus-virt-cleanup ()
  (setq gnus-virt-message-numbers nil))

(add-hook 'gnus-exit-group-hook 'gnus-virt-cleanup)


(defun gnus-virt-canonicalize-message-id (id)
  "C-News has screwy notions about case sensitivity."
  (if (string-match "@[^@]+\\'" id)
      (concat (substring id 0 (match-beginning 0))
	      (downcase (substring id (match-beginning 0))))
    id))

(defun gnus-virt-retrieve-headers (sequence)
  "Return list of article headers specified by SEQUENCE of article id.
The article ids may be message-id strings, or conses of the form
  (\"newsgroup-name\" . \"article-number\").
The format of the returned list is
 `([NUMBER SUBJECT FROM XREF LINES DATE MESSAGE-ID REFERENCES] ...)'.
If there is no References: field, In-Reply-To: field is used instead.
Reader macros for the vector are defined as `nntp-header-FIELD'.
Writer macros for the vector are defined as `nntp-set-header-FIELD'."
  (setq gnus-virt-message-numbers (apply 'vector sequence))
  (let* ((nmessages (length gnus-virt-message-numbers))
	 (i 0)
	 (last-group nil)
	 (rest sequence)
	 (per-group-queue nil)
	 (mid nil)
	 (messages (make-vector nmessages nil))
	 ;; local function to snarf the headers from what's on
	 ;; per-group-queue and slightly munge the result.
	 (grab-headers
	  (function
	   (lambda ()
	     (if last-group (gnus-request-group last-group))
	     (setq per-group-queue (nreverse per-group-queue))
	     (let* ((new (gnus-retrieve-headers-by-id per-group-queue))
		    (new-rest new)
		    (ids-rest per-group-queue)
		    )
	       (while new-rest
		 ;; Find the appropriate slot in `messages' for this message.
		 ;; Note that message-IDs are not completely case insensitive!
		 ;; The NNTP server downcases the part after the "@" in the
		 ;; message ID on the "221" reply line, but doesn't touch the
		 ;; Message-ID: field in the headers, so the two don't match.
		 (while (not (equal (gnus-virt-canonicalize-message-id
				     (aref gnus-virt-message-numbers i))
				    (gnus-virt-canonicalize-message-id
				     (nntp-header-id (car new-rest)))))
		   (setq i (1+ i))
		   (if (>= i nmessages)
		       (error "couldn't find %s"
			      (nntp-header-id (car new-rest))))
		   )
		 (aset messages i (car new-rest))

		 ;; For newgroup/number pairs, add an entry to the Xref field
		 ;; if there's nothing there already (meaning it was a single
		 ;; post and not a crosspost.)  Since we'll be reading this in
		 ;; a virtual group, all posts are corossposts as far as GNUS
		 ;; is concerned.
		 (or (nntp-header-xref (car new-rest))
		     (nntp-set-header-xref
		      (car new-rest)
		      (concat (system-name) " " last-group ":"
			      (nntp-header-number (car new-rest)))))
		 ;; NNTP HEAD command run on message-ids returns 0 as message
		 ;; number; so store some unique number there, and store the
		 ;; the message descriptor in a variable.  (We don't just
		 ;; store the descriptor in the header because things expect
		 ;; it to be numeric, dammit...)
		 (nntp-set-header-number (car new-rest) i)
		 ;; Possibly replace conses of (group . n) with message-ids.
		 (aset gnus-virt-message-numbers i
		       (nntp-header-id (car new-rest)))
		 (setq ids-rest (cdr ids-rest)
		       new-rest (cdr new-rest)))
	       )
	     (setq last-group nil
		   per-group-queue nil)))))
    (while rest
      (setq mid (car rest))
       (cond ((consp mid)		; newsgroup/article-number pair
	     (if (equal last-group (car mid))
		 ;; the queue has other article-numbers in this group; add.
		 (setq per-group-queue (cons (cdr mid) per-group-queue))
	       ;; else the queue has article-numbers in another group, or
	       ;; it has raw message-ids.  Flush the queue, and restart it.
	       (if per-group-queue (funcall grab-headers))
	       (setq last-group (car mid)
		     per-group-queue (cons (cdr mid) nil))))

	    (t				; message-id
	     (if last-group
		 ;; the queue has article-numbers of some group on it;
		 ;; flush and restart it.
		 (funcall grab-headers))
	     ;; in this case the queue has message-ids (in no group.)
	     (setq per-group-queue (cons mid per-group-queue))))
      (setq rest (cdr rest)))
    ;;
    ;; at the end, if there is still stuff on the queue, flush it.
    (if per-group-queue (funcall grab-headers))
    ;;
    ;; Now notice messages we weren't able to get (why?) and flag them.
    (setq i 0)
    (while (< i nmessages)
      (if (null (aref messages i))
	  (let ((id (format "%s" (aref gnus-virt-message-numbers i))))
	    (aset messages i
		  (vector i id "EXPIRED?" nil 0 "" id nil))))
      (setq i (1+ i)))
    (append messages nil) ; coerce vector to list
    ))

(defun gnus-virt-select-newsgroup (name message-ids)
  "Select a \"virtual\" newsgroup consisting of the given message-ids.
The message ids may be actual message-id strings, or conses of the form
  (\"newsgroup-name\" . \"article-number\")."
  ;; much of this copied from gnus-select-newsgroup
  (gnus-start-news-server)
  (setq gnus-newsgroup-name name)
;;  (setq gnus-newsgroup-unreads ...)
  (setq gnus-newsgroup-unselected nil)

  ;; Get headers list.
  (setq gnus-newsgroup-headers (gnus-virt-retrieve-headers message-ids))

  (setq gnus-newsgroup-unreads
	(mapcar 'gnus-header-number gnus-newsgroup-headers))

  ;; ## do something about expired articles here?
  ;; ## do something about marked articles here?

  ;; First and last article in this newsgroup.
  (setq gnus-newsgroup-begin
	(if gnus-newsgroup-headers
	    (nntp-header-number (car gnus-newsgroup-headers))
	  0
	  ))
  (setq gnus-newsgroup-end
	(if gnus-newsgroup-headers
	    (nntp-header-number
	     (gnus-last-element gnus-newsgroup-headers))
	  0
	  ))
  ;; File name that an article was saved last.
  (setq gnus-newsgroup-last-rmail nil)
  (setq gnus-newsgroup-last-mail nil)
  (setq gnus-newsgroup-last-folder nil)
  (setq gnus-newsgroup-last-file nil)
  ;; Reset article pointer etc.
  (setq gnus-current-article nil)
  (setq gnus-current-headers nil)
  (setq gnus-current-history nil)
  (setq gnus-have-all-headers nil)
  (setq gnus-last-article nil)
  ;; Clear old hash tables for the variable gnus-newsgroup-headers.
  (gnus-clear-hashtables-for-newsgroup-headers)
  ;; GROUP is successfully selected.
  t
  )

(defun gnus-virt-summary-read-group (group message-ids &optional no-article)
  "Start reading news in a \"virtual\" newsgroup of the given message-ids.
The message ids may be actual message-id strings, or conses of the form
  (\"newsgroup-name\" . \"article-number\").
If NO-ARTICLE is non-nil, no article is selected initially."

  (setq message-ids (mapcar 'gnus-virt-canonicalize-message-id message-ids))

  ;; delete duplicate message ids
  (let ((rest message-ids))
    (while rest
      (if (member (car rest) (cdr rest))
	  (setcar rest nil))
      (setq rest (cdr rest)))
    (setq message-ids (delq nil message-ids)))

  ;; make there be a newsgroup to prevent error when quitting...
  (or (gnus-gethash group gnus-newsrc-hashtb)
      (gnus-sethash group (list group nil) gnus-newsrc-hashtb))

  ;; this is a real kludge; we need to cause gnus-virt-select-newsgroup to
  ;; be called instead of gnus-select-newsgroup so we temporarily redefine
  ;; it...  We could do this by advising gnus-select-newsgroup and using
  ;; some special bindings for communication, but that's not really much
  ;; less gross than this.
  (let ((old (symbol-function 'gnus-select-newsgroup)))
    (unwind-protect
	(progn
	  (fset 'gnus-select-newsgroup
		(function (lambda (group show-all)
			    (gnus-virt-select-newsgroup group message-ids))))
	  (gnus-summary-read-group group nil no-article))
      ;; protected
      (fset 'gnus-select-newsgroup old))))

;;; A simple user-level interface; this could be greatly improved.
;;; In fact, doing that is what all of the interesting problems here
;;; are about.

(defun gnus-virt-read-merged-groups (virtual-name newsgroup-names
				     &optional all no-article)
  "Read news in the given newsgroup as if they were one group.
VIRTUAL-NAME is the name to assign to this new group;
NEWSGROUP-NAMES is a list of the names of the newsgroups to combine.
If argument ALL is non-nil, already read articles become readable.
If optional argument NO-ARTICLE is non-nil, no article body is displayed."
  (let ((articles nil)
	(rest (reverse newsgroup-names)))
    (while rest
      (let* ((group (car rest))
	     (numbers (gnus-uncompress-sequence
		       (nthcdr 2 (gnus-gethash group
					       (if all
						   gnus-active-hashtb
						 gnus-unread-hashtb))))))
	(setq articles (nconc (mapcar '(lambda (x) (cons group x)) numbers)
			      articles)))
      (setq rest (cdr rest)))
    ;; ## We should compute gnus-newsgroup-unreads and gnus-newsgroup-marked
    ;;    as the union of the unread/marked articles of all of these groups.
    (gnus-virt-summary-read-group virtual-name articles no-article)))


(provide 'gnus-virt)
