;;; print.lisp
;;;
;;; Copyright (C) 2004-2006 Peter Graves
;;; $Id$
;;;
;;; 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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
;;;
;;; As a special exception, the copyright holders of this library give you
;;; permission to link this library with independent modules to produce an
;;; executable, regardless of the license terms of these independent
;;; modules, and to copy and distribute the resulting executable under
;;; terms of your choice, provided that you also meet, for each linked
;;; independent module, the terms and conditions of the license of that
;;; module.  An independent module is a module which is not derived from
;;; or based on this library.  If you modify this library, you may extend
;;; this exception to your version of the library, but you are not
;;; obligated to do so.  If you do not wish to do so, delete this
;;; exception statement from your version.

;;; Adapted from SBCL.

(in-package #:system)

;;; Can this object contain other objects?
(defun compound-object-p (x)
  (or (consp x)
      (typep x 'structure-object)
      (typep x 'standard-object)
      (typep x '(array t *))))

;;; Punt if INDEX is equal or larger then *PRINT-LENGTH* (and
;;; *PRINT-READABLY* is NIL) by outputting \"...\" and returning from
;;; the block named NIL.
(defmacro punt-print-if-too-long (index stream)
  `(when (and (not *print-readably*)
              *print-length*
              (>= ,index *print-length*))
     (write-string "..." ,stream)
     (return)))

(defun output-integer (integer stream)
;;   (%output-object integer stream))
  (if (xp::xp-structure-p stream)
      (let ((s (sys::%write-to-string integer)))
        (xp::write-string++ s stream 0 (length s)))
      (%output-object integer stream)))

(defun output-list (list stream)
  (cond ((and (null *print-readably*)
              *print-level*
              (>= *current-print-level* *print-level*))
         (write-char #\# stream))
        (t
         (let ((*current-print-level* (1+ *current-print-level*)))
                (write-char #\( stream)
                (let ((*current-print-length* 0)
                      (list list))
                  (loop
                    (punt-print-if-too-long *current-print-length* stream)
                    (output-object (pop list) stream)
                    (unless list
                      (return))
                    (when (or (atom list)
                              (check-for-circularity list))
                      (write-string " . " stream)
                      (output-object list stream)
                      (return))
                    (write-char #\space stream)
                    (incf *current-print-length*)))
                (write-char #\) stream))))
  list)

;;; Output the abbreviated #< form of an array.
(defun output-terse-array (array stream)
  (let ((*print-level* nil)
        (*print-length* nil))
    (print-unreadable-object (array stream :type t :identity t))))

(defun array-readably-printable-p (array)
  (and (eq (array-element-type array) t)
       (let ((zero (position 0 (array-dimensions array)))
             (number (position 0 (array-dimensions array)
                               :test (complement #'eql)
                               :from-end t)))
         (or (null zero) (null number) (> zero number)))))

(defun output-vector (vector stream)
  (declare (vector vector))
  (cond ((stringp vector)
         (assert nil)
         (sys::%output-object vector stream))
        ((not (or *print-array* *print-readably*))
         (output-terse-array vector stream))
        ((bit-vector-p vector)
         (assert nil)
         (sys::%output-object vector stream))
        (t
         (when (and *print-readably*
                    (not (array-readably-printable-p vector)))
           (error 'print-not-readable :object vector))
         (cond ((and (null *print-readably*)
                     *print-level*
                     (>= *current-print-level* *print-level*))
                (write-char #\# stream))
               (t
                (let ((*current-print-level* (1+ *current-print-level*)))
                  (write-string "#(" stream)
                  (dotimes (i (length vector))
                    (unless (zerop i)
                      (write-char #\space stream))
                    (punt-print-if-too-long i stream)
                    (output-object (aref vector i) stream))
                  (write-string ")" stream))))))
  vector)

(defun output-ugly-object (object stream)
  (cond ((consp object)
         (output-list object stream))
        ((and (vectorp object)
              (not (stringp object))
              (not (bit-vector-p object)))
         (output-vector object stream))
        ((structure-object-p object)
         (cond
           ((and (null *print-readably*)
                 *print-level*
                 (>= *current-print-level* *print-level*))
            (write-char #\# stream))
           (t
            (print-object object stream))))
        ((standard-object-p object)
         (print-object object stream))
        ((java::java-object-p object)
         (print-object object stream))
        ((xp::xp-structure-p stream)
         (let ((s (sys::%write-to-string object)))
           (xp::write-string++ s stream 0 (length s))))
        ((functionp object)
          (print-object object stream))
        (t
         (%output-object object stream))))


;;;; circularity detection stuff

;;; When *PRINT-CIRCLE* is T, this gets bound to a hash table that
;;; (eventually) ends up with entries for every object printed. When
;;; we are initially looking for circularities, we enter a T when we
;;; find an object for the first time, and a 0 when we encounter an
;;; object a second time around. When we are actually printing, the 0
;;; entries get changed to the actual marker value when they are first
;;; printed.
(defvar *circularity-hash-table* nil)

;;; When NIL, we are just looking for circularities. After we have
;;; found them all, this gets bound to 0. Then whenever we need a new
;;; marker, it is incremented.
(defvar *circularity-counter* nil)

;;; Check to see whether OBJECT is a circular reference, and return
;;; something non-NIL if it is. If ASSIGN is T, then the number to use
;;; in the #n= and #n# noise is assigned at this time.
;;; If ASSIGN is true, reference bookkeeping will only be done for
;;; existing entries, no new references will be recorded!
;;;
;;; Note: CHECK-FOR-CIRCULARITY must be called *exactly* once with
;;; ASSIGN true, or the circularity detection noise will get confused
;;; about when to use #n= and when to use #n#. If this returns non-NIL
;;; when ASSIGN is true, then you must call HANDLE-CIRCULARITY on it.
;;; If CHECK-FOR-CIRCULARITY returns :INITIATE as the second value,
;;; you need to initiate the circularity detection noise, e.g. bind
;;; *CIRCULARITY-HASH-TABLE* and *CIRCULARITY-COUNTER* to suitable values
;;; (see #'OUTPUT-OBJECT for an example).
(defun check-for-circularity (object &optional assign)
  (cond ((null *print-circle*)
         ;; Don't bother, nobody cares.
         nil)
        ((null *circularity-hash-table*)
         (values nil :initiate))
        ((null *circularity-counter*)
         (ecase (gethash object *circularity-hash-table*)
           ((nil)
            ;; first encounter
            (setf (gethash object *circularity-hash-table*) t)
            ;; We need to keep looking.
            nil)
           ((t)
            ;; second encounter
            (setf (gethash object *circularity-hash-table*) 0)
            ;; It's a circular reference.
            t)
           (0
            ;; It's a circular reference.
            t)))
        (t
         (let ((value (gethash object *circularity-hash-table*)))
           (case value
             ((nil t)
              ;; If NIL, we found an object that wasn't there the
              ;; first time around. If T, this object appears exactly
              ;; once. Either way, just print the thing without any
              ;; special processing. Note: you might argue that
              ;; finding a new object means that something is broken,
              ;; but this can happen. If someone uses the ~@<...~:>
              ;; format directive, it conses a new list each time
              ;; though format (i.e. the &REST list), so we will have
              ;; different cdrs.
              nil)
             (0
              (if assign
                  (let ((value (incf *circularity-counter*)))
                    ;; first occurrence of this object: Set the counter.
                    (setf (gethash object *circularity-hash-table*) value)
                    value)
                  t))
             (t
              ;; second or later occurrence
              (- value)))))))

;;; Handle the results of CHECK-FOR-CIRCULARITY. If this returns T then
;;; you should go ahead and print the object. If it returns NIL, then
;;; you should blow it off.
(defun handle-circularity (marker stream)
  (case marker
    (:initiate
     ;; Someone forgot to initiate circularity detection.
     (let ((*print-circle* nil))
       (error "trying to use CHECK-FOR-CIRCULARITY when ~
       circularity checking isn't initiated")))
    ((t)
     ;; It's a second (or later) reference to the object while we are
     ;; just looking. So don't bother groveling it again.
     nil)
    (t
;;      (write-char #\# stream)
;;      (let ((*print-base* 10)
;;            (*print-radix* nil))
       (cond ((minusp marker)
;;            (output-integer (- marker) stream)
;;            (write-char #\# stream)
              (print-reference marker stream)
              nil)
             (t
;;            (output-integer marker stream)
;;            (write-char #\= stream)
              (print-label marker stream)
              t)))))

(defun print-label (marker stream)
  (write-char #\# stream)
  (let ((*print-base* 10)
        (*print-radix* nil))
    (output-integer marker stream))
  (write-char #\= stream))

(defun print-reference (marker stream)
  (write-char #\# stream)
  (let ((*print-base* 10)
        (*print-radix* nil))
    (output-integer (- marker) stream))
  (write-char #\# stream))

;;;; OUTPUT-OBJECT -- the main entry point

;; Objects whose print representation identifies them EQLly don't need to be
;; checked for circularity.
(defun uniquely-identified-by-print-p (x)
  (or (numberp x)
      (characterp x)
      (and (symbolp x)
           (symbol-package x))))

(defun %print-object (object stream)
  (if *print-pretty*
      (xp::output-pretty-object object stream)
      (output-ugly-object object stream)))

(defun %check-object (object stream)
  (multiple-value-bind (marker initiate)
      (check-for-circularity object t)
    (if (eq initiate :initiate)
        ;; Initialize circularity detection.
        (let ((*circularity-hash-table* (make-hash-table :test 'eq)))
          (%check-object object (make-broadcast-stream))
          (let ((*circularity-counter* 0))
            (%check-object object stream)))
        ;; Otherwise...
        (if marker
            (when (handle-circularity marker stream)
              (%print-object object stream))
            (%print-object object stream)))))

;;; Output OBJECT to STREAM observing all printer control variables.
(defun output-object (object stream)
  (cond ((or (not *print-circle*)
             (uniquely-identified-by-print-p object))
         (%print-object object stream))
        ;; If we have already started circularity detection, this object might
        ;; be a shared reference. If we have not, then if it is a compound
        ;; object, it might contain a circular reference to itself or multiple
        ;; shared references.
        ((or *circularity-hash-table*
             (compound-object-p object))
         (%check-object object stream))
        (t
         (%print-object object stream)))
  object)

(provide "PRINT")
