;;;; LTD-READ: PRESERVES COMMENTS AND REMEMBERS FILE POSITIONS
(in-package :cl-user)

(defvar *eof* "eof")
(defvar *file-position-table* (make-hash-table :test #'eq))
(defvar *read-list* (get-macro-character #\())
(defvar *lisp-array* (get-dispatch-macro-character #\# #\A))
(defvar *lisp-readtable* *readtable*)
(defvar *buffer*
  (make-array 200 :element-type 'character :fill-pointer 0 :adjustable t))


(defparameter *ltd-readtable*
  (let ((table (copy-readtable *readtable*)))
    (set-macro-character #\; 'collect-comments nil table)
    (set-macro-character #\( 'ltd-read-list nil table)
    (set-dispatch-macro-character #\# #\| 'collect-comments table)
    (set-dispatch-macro-character #\# #\A 'read-array table)
    (set-dispatch-macro-character #\# #\a 'read-array table)
    table))

(defstruct (com (:predicate comment?))
  ;; We spell it COM because LispWorks has a class called comment
  (comment "") (code nil))

(defun add-comment (comment code)
  "Precede the code with a comment, or just return code if comment is null."
  (if comment (make-com :comment comment :code code) code))

(defmacro move-comment (place exp)
  ;; Strip the comment off of place, then eval exp and put comment there.
  (let ((comment (gensym)))
  `(let ((,comment (get-comment ,place)))
     (setf ,place (strip ,place))
     (add-comment ,comment ,exp))))

(defun get-comment (exp)
  (if (comment? exp) (com-comment exp) nil))

(defmacro record-file-positions (stream exp)
  ;; Caution: not hygienic
  `(let* ((start (file-position ,stream))
          (value ,exp)
          (end (file-position ,stream)))
    (when (or (consp value) (stringp value))
      (setf (gethash value *file-position-table*) (cons start end)))
    value))

(defun ltd-read-list (stream char)
  ;; Check for () and record positions
  (declare (ignore char))
  (case (peek-char t stream nil)
    (#\) (read-char stream) '|()|)
    (otherwise (record-file-positions
		stream (funcall *read-list* stream char)))))

(defun read-array (stream char &optional arg)
  "Read an array using the native lisp array reader."
  (let ((*readtable* *lisp-readtable*))
    (funcall *lisp-array* stream char arg)))

(defun collect-comments (stream char &optional arg comment-so-far)
  ;; Gather up comments, then either attach to following exp
  ;; or return no values (thus silently ignoring the comments).
  (declare (ignore arg))
  (let* ((comment
          (case char
            (#\; (loop while (read-char-if stream #\;)) ; Flush leading ;s
	     (read-line stream nil ""))
            (#\| (ltd-read-hash-comment stream))
	    (t "")))
         (comments (if comment-so-far
                       (format nil "~A~%~A" comment-so-far comment)
		     comment)))
    (case (peek-char t stream nil)
      ((#\. #\)) (values));; Ignore the comment
      ((#\;) (collect-comments stream #\; nil comments))
      ((#\#)
       ;; Deal with #|, #+, #-
       (read-char stream)
       (case (peek-char nil stream nil)
	 ((#\|) (read-char stream)
	  (collect-comments stream #\| nil comments))
	 ((#\+ #\-) (add-comments-to-conditional stream comments))
	 (t (unread-char #\# stream)
	    (add-comment comments (ltd-read stream)))))
      (otherwise (add-comment comments (ltd-read stream))))))

(defun add-comments-to-conditional (stream comments)
  ;; We've read # and peeked at + or -
  ;; Add comments to next expression, if it is in,
  ;; or else ignore it and pass comments back to collect-comments
  (let* ((fn (ecase (read-char stream) (#\+ 'identity) (#\- 'not)))
	 (*package* (find-package :keyword))
	 (feature (read stream)))
    (if (funcall fn (member feature *features*)) ;; should handle and/or/not
	(add-comment comments (ltd-read stream))
      (let ((*read-suppress* t))
	(read stream nil nil) ;; Ignore the next exp
	(collect-comments stream nil 0 comments)))))
	

(defun ltd-read-hash-comment (stream)
  (setf (fill-pointer *buffer*) 0)
  (let ((level 1))
    (loop
     (let ((char (read-char stream nil :eof t)))
       (case char
	 (:eof (warn "EOF during #| ... |# comment") (RETURN))
	 (#\# (cond ((read-char-if stream #\|)
		     (incf level))
		    (t (vector-push-extend #\# *buffer*))))
	 (#\| (cond ((read-char-if stream #\#)
		     (decf level)
		     (when (eql level 0)
		       (RETURN)))
		    (t (vector-push-extend #\| *buffer*))))
	 (otherwise (vector-push-extend char *buffer*))))))
  (coerce *buffer* 'string))


(defun ltd-read (&optional (stream *standard-input*))
  "Read a Lisp expression, preserving comments and file positions."
  (let* ((*readtable* *ltd-readtable*))
    (record-file-positions stream (read stream nil *eof*))))

(defun read-char-if (stream char)
  (if (eql (peek-char nil stream nil nil) char)
      (read-char stream)
      nil))
