;;; vm-thread.el ---  Thread support for VM
;;
;; Copyright (C) 1994, 2001 Kyle E. Jones
;; Copyright (C) 2003-2006 Robert Widhopf-Fenk
;;
;; This program 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 of the License, or
;; (at your option) any later version.
;;
;; This program 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 this program; if not, write to the Free Software Foundation, Inc.,
;; 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

;;; Code:

;;;###autoload
(defun vm-toggle-threads-display ()
  "Toggle the threads display on and off.
When the threads display is on, the folder will be sorted by
thread and thread indentation (via the %I summary format specifier)
will be visible."
  (interactive)
  (vm-select-folder-buffer)
  (vm-check-for-killed-summary)
  ;; get numbering of new messages done now
  ;; so that the sort code only has to worry about the
  ;; changes it needs to make.
  (vm-update-summary-and-mode-line)
  (vm-set-summary-redo-start-point t)
  (setq vm-summary-show-threads (not vm-summary-show-threads))
  (if vm-summary-show-threads
      (vm-sort-messages "thread")
    (vm-sort-messages "physical-order")))

;;;###autoload
(defun vm-build-threads (message-list)
  (if (not (vectorp vm-thread-obarray))
      (setq vm-thread-obarray (make-vector 641 0)
	    vm-thread-subject-obarray (make-vector 641 0)))
  (let ((mp (or message-list vm-message-list))
	(n 0)
	;; Just for laughs, make the update interval vary.
	(modulus (+ (% (vm-abs (random)) 11) 40))
	;; no need to schedule reindents of reparented messages
	;; unless there were already messages present.
	(schedule-reindents message-list)
	m parent parent-sym id id-sym date refs old-parent-sym)
    (while mp
      (setq m (car mp)
	    parent (vm-th-parent m)
	    id (vm-su-message-id m)
	    id-sym (intern id vm-thread-obarray)
	    date (vm-so-sortable-datestring m))
      (put id-sym 'messages (cons m (get id-sym 'messages)))
      (put id-sym 'date date)
      (if (and (null (cdr (get id-sym 'messages)))
	       schedule-reindents)
	  (vm-thread-mark-for-summary-update (get id-sym 'children)))
      (if parent
	  (progn
	    (setq parent-sym (intern parent vm-thread-obarray))
	    (cond ((or (not (boundp id-sym))
		       (null (symbol-value id-sym))
		       (eq (symbol-value id-sym) parent-sym))
		   (set id-sym parent-sym))
		  (t
		   (setq old-parent-sym (symbol-value id-sym))
		   (put old-parent-sym 'children
			(let ((kids (get old-parent-sym 'children))
			      (msgs (get id-sym 'messages)))
			  (while msgs
			    (setq kids (delq (car msgs) kids)
				  msgs (cdr msgs)))
			  kids ))
		   (set id-sym parent-sym)
		   (if schedule-reindents
		       (vm-thread-mark-for-summary-update
			(get id-sym 'messages)))))
	    (put parent-sym 'children
		 (cons m (get parent-sym 'children))))
	(if (not (boundp id-sym))
	    (set id-sym nil)))
      ;; use the references header to set parenting information
      ;; for ancestors of this message.  This does not override
      ;; a parent pointer for a message if it already exists.
      (if (cdr (setq refs (vm-th-references m)))
	  (let (parent-sym id-sym msgs)
	    (setq parent-sym (intern (car refs) vm-thread-obarray)
		  refs (cdr refs))
	    (while refs
	      (setq id-sym (intern (car refs) vm-thread-obarray))
	      (if (and (boundp id-sym) (symbol-value id-sym))
		  nil
		(set id-sym parent-sym)
		(if (setq msgs (get id-sym 'messages))
		    (put parent-sym 'children
			 (append msgs (get parent-sym 'children))))
		(if schedule-reindents
		    (vm-thread-mark-for-summary-update msgs)))
	      (setq parent-sym id-sym
		    refs (cdr refs)))))
      (setq mp (cdr mp) n (1+ n))
      (if (zerop (% n modulus))
	  (message "Building threads (by reference)... %d" n)))
    (if vm-thread-using-subject
	(progn
	  (setq n 0 mp (or message-list vm-message-list))
	  (while mp
	    (setq m (car mp)
		  parent (vm-th-parent m)
		  id (vm-su-message-id m)
		  id-sym (intern id vm-thread-obarray)
		  date (vm-so-sortable-datestring m))
	    ;; inhibit-quit because we need to make sure the asets
	    ;; below are an atomic group.
	    (let* ((inhibit-quit t)
		   (subject (vm-so-sortable-subject m))
		   (subject-sym (intern subject vm-thread-subject-obarray)))
	      ;; if this subject was never seen before create the
	      ;; information vector.
	      (if (not (boundp subject-sym))
		  (set subject-sym
		       (vector id-sym date
			       nil (list m)))
		;; this subject seen before 
		(aset (symbol-value subject-sym) 3
		      (cons m (aref (symbol-value subject-sym) 3)))
		(if (string< date (aref (symbol-value subject-sym) 1))
		    (let* ((vect (symbol-value subject-sym))
			   (i-sym (aref vect 0)))
		      ;; optimization: if we know that this message
		      ;; already has a parent, then don't bother
		      ;; adding it to the list of child messages
		      ;; since we know that it will be threaded and
		      ;; unthreaded using the parent information.
		      (if (or (not (boundp i-sym))
			      (null (symbol-value i-sym)))
			  (aset vect 2 (append (get i-sym 'messages)
					       (aref vect 2))))
		      (aset vect 0 id-sym)
		      (aset vect 1 date)
		      ;; this loops _and_ recurses and I'm worried
		      ;; about it going into a spin someday.  So I
		      ;; unblock interrupts here.  It's not critical
		      ;; that it finish... the summary will just be out
		      ;; of sync.
		      (if schedule-reindents
			  (let ((inhibit-quit nil))
			    (vm-thread-mark-for-summary-update (aref vect 2)))))
		  ;; optimization: if we know that this message
		  ;; already has a parent, then don't bother adding
		  ;; it to the list of child messages, since we
		  ;; know that it will be threaded and unthreaded
		  ;; using the parent information.
		  (if (null parent)
		      (aset (symbol-value subject-sym) 2
			    (cons m (aref (symbol-value subject-sym) 2)))))))
	    (setq mp (cdr mp) n (1+ n))
	    (if (zerop (% n modulus))
		(message "Building threads (by subject)... %d" n)))))
    (if (> n modulus)
	(message "Building threads... done"))))

;; used by the thread sort code.
;;
;; vm-th-thread-list initializes the oldest-date property on
;; the message-id symbols.  Since this property is used as an
;; ordering key by the thread sort the oldest-date properties
;; must be computed before the sort begins, not during it.
;; Otherwise the sort won't be stable and there will be chaos.

;;;###autoload
(defun vm-build-thread-lists ()
  (let ((mp vm-message-list))
    (while mp
      (vm-th-thread-list (car mp))
      (setq mp (cdr mp)))))

(defun vm-thread-mark-for-summary-update (message-list)
  (let (m)
    (while message-list
      (setq m (car message-list))
      ;; if thread-list is null then we've already marked this
      ;; message, or it doesn't need marking.
      (if (null (vm-thread-list-of m))
	  nil
	(vm-mark-for-summary-update m t)
	(vm-set-thread-list-of m nil)
	(vm-set-thread-indentation-of m nil)
	(vm-thread-mark-for-summary-update
	 (get (intern (vm-su-message-id m) vm-thread-obarray)
	      'children)))
      (setq message-list (cdr message-list)))))

(defun vm-thread-list (message)
  (let ((done nil)
	(m message)
	(loop-recovery-point nil)
	(date (vm-so-sortable-datestring message))
	thread-list id-sym subject-sym loop-sym root-date youngest-date)
    (save-excursion
      (set-buffer (vm-buffer-of m))
      (fillarray vm-thread-loop-obarray 0)
      (setq id-sym (intern (vm-su-message-id m) vm-thread-obarray)
	    thread-list (list id-sym))
      (set (intern (symbol-name id-sym) vm-thread-loop-obarray) t)
      (while (not done)
	;; save the date of the oldest message in this thread
	(setq root-date (get id-sym 'oldest-date))
	(if (or (null root-date)
		(string< date root-date))
	    (put id-sym 'oldest-date date))
	;; save the date of the youngest message in this thread
	(setq youngest-date (get id-sym 'youngest-date))
	(if (or (null root-date)
		(string< youngest-date date))
	    (put id-sym 'youngest-date date))
	(if (and (boundp id-sym) (symbol-value id-sym))
	    (progn
	      (setq id-sym (symbol-value id-sym)
		    loop-sym (intern (symbol-name id-sym)
				     vm-thread-loop-obarray))
	      (if (boundp loop-sym)
		  ;; loop detected, bail...
		  (setq done t
			thread-list (or loop-recovery-point thread-list))
		(set loop-sym t)
		(setq thread-list (cons id-sym thread-list)
		      m (car (get id-sym 'messages)))))
	  (if (null m)
	      (setq done t)
	    (if (null vm-thread-using-subject)
		(setq done t)
	      (setq subject-sym
		    (intern (vm-so-sortable-subject m)
			    vm-thread-subject-obarray))
	      (if (or (not (boundp subject-sym))
		      (eq (aref (symbol-value subject-sym) 0) id-sym))
		  (setq done t)
		(setq id-sym (aref (symbol-value subject-sym) 0)
;; seems to cause more trouble than it fixes
;; revisit this later.
;;		      loop-recovery-point (or loop-recovery-point
;;					      thread-list)
		      loop-sym (intern (symbol-name id-sym)
				       vm-thread-loop-obarray))
		(if (boundp loop-sym)
		    ;; loop detected, bail...
		    (setq done t
			  thread-list (or loop-recovery-point thread-list))
		  (set loop-sym t)
		  (setq thread-list (cons id-sym thread-list)
			m (car (get id-sym 'messages)))))))))
      thread-list )))

;; remove message struct from thread data.
;;
;; optional second arg non-nil means forget information that
;; might be different if the message contents changed.
;;
;; message must be a real (non-virtual) message

;;;###autoload
(defun vm-unthread-message (message &optional message-changing)
  (save-excursion
    (let ((mp (cons message (vm-virtual-messages-of message)))
	  m id-sym subject-sym vect p-sym)
      (while mp
	(setq m (car mp))
	(set-buffer (vm-buffer-of m))
	(if (not (vectorp vm-thread-obarray))
	    nil
	  (let ((inhibit-quit t))
	    (vm-set-thread-list-of m nil)
	    (vm-set-thread-indentation-of m nil)
	    (setq id-sym (intern (vm-su-message-id m) vm-thread-obarray)
		  subject-sym (intern (vm-so-sortable-subject m)
				      vm-thread-subject-obarray))
	    (if (boundp id-sym)
		(progn
		  (put id-sym 'messages (delq m (get id-sym 'messages)))
		  (vm-thread-mark-for-summary-update (get id-sym 'children))
		  (setq p-sym (symbol-value id-sym))
		  (and p-sym (put p-sym 'children
				  (delq m (get p-sym 'children))))
		  (if message-changing
		      (set id-sym nil))))
	    (if (and (boundp subject-sym) (setq vect (symbol-value subject-sym)))
		(if (not (eq id-sym (aref vect 0)))
		    (aset vect 2 (delq m (aref vect 2)))
		  (if message-changing
		      (if (null (cdr (aref vect 3)))
			  (makunbound subject-sym)
			(let ((p (aref vect 3))
			      oldest-msg oldest-date children)
			  (setq oldest-msg (car p)
				oldest-date (vm-so-sortable-datestring (car p))
				p (cdr p))
			  (while p
			    (if (and (string-lessp (vm-so-sortable-datestring (car p))
						   oldest-date)
				     (not (eq m (car p))))
				(setq oldest-msg (car p)
				      oldest-date (vm-so-sortable-datestring (car p))))
			    (setq p (cdr p)))
			  (aset vect 0 (intern (vm-su-message-id oldest-msg)
					       vm-thread-obarray))
			  (aset vect 1 oldest-date)
			  (setq children (delq oldest-msg (aref vect 2)))
			  (aset vect 2 children)
			  (aset vect 3 (delq m (aref vect 3)))
			  ;; I'm not sure there aren't situations
			  ;; where this might loop forever.
			  (let ((inhibit-quit nil))
			    (vm-thread-mark-for-summary-update children)))))))))
	  (setq mp (cdr mp))))))

(defun vm-th-references (m)
  (or (vm-references-of m)
      (vm-set-references-of
       m
       (let (references)
	 (setq references (vm-get-header-contents m "References:" " "))
	 (and references (vm-parse references "[^<]*\\(<[^>]+>\\)"))))))

(defun vm-th-parent (m)
  (or (vm-parent-of m)
      (vm-set-parent-of
       m
       (or (car (vm-last (vm-th-references m)))
	   (let (in-reply-to ids id)
	     (setq in-reply-to (vm-get-header-contents m "In-Reply-To:" " ")
		   ids (and in-reply-to (vm-parse in-reply-to
						  "[^<]*\\(<[^>]+>\\)")))
	     (while ids
	       (if (< (length id) (length (car ids)))
		   (setq id (car ids)))
	       (setq ids (cdr ids)))
	     (and id (vm-set-references-of m (list id)))
	     id )))))

;;;###autoload
(defun vm-th-thread-indentation (m)
  (or (vm-thread-indentation-of m)
      (let ((p (vm-th-thread-list m)))
	(while (and p (null (get (car p) 'messages)))
	  (setq p (cdr p)))
	(vm-set-thread-indentation-of m (1- (length p)))
	(vm-thread-indentation-of m))))

;;;###autoload
(defun vm-th-thread-list (m)
  (or (vm-thread-list-of m)
      (progn
	(vm-set-thread-list-of m (vm-thread-list m))
	(vm-thread-list-of m))))

(provide 'vm-thread)

;;; vm-thread.el ends here
