;;; filling/indenting stream
;;;
;;; Copyright (C) 2004-2008 by Sam Steingold
;;; Copyright (C) 2004 by Bruno Haible
;;; This is Free Software, distributed under the GNU GPL v2+
;;; See http://www.gnu.org/copyleft/gpl.html

(in-package "EXT")
(export '(custom::*fill-indent-sexp*) "CUSTOM")
(export '(fill-stream with-fill-stream custom:*fill-indent-sexp*))
(import '(fill-stream with-fill-stream) "SYS")
(in-package "SYSTEM")

(defvar *fill-indent-sexp* #'1+
  "The default indentation of new FILL-STREAMs inside SEXPs.
This should be a number (the actual indentation),
a function of one argument (the text indentation),
NIL (no indentation) or
T (same indentation as the text, i.e., the same effect as #'IDENTITY).")

(declaim (inline right-margin))
(defun right-margin () (or *print-right-margin* sys::*prin-linelength*))

(defclass fill-stream (fundamental-character-output-stream)
  ((target-stream :initarg :stream :type stream)
   (buffer :type string :initform
           (make-array (right-margin) :element-type 'character
                       :fill-pointer 0 :adjustable t))
   (inside-sexp :initform nil :type boolean)
   (sexp-indent :initarg :sexp-indent :initform *fill-indent-sexp*
                :type (or symbol integer function))
   ;; the indentation level variable or number:
   (indent-var :initarg :text-indent :initform 0 :type (or symbol integer))
   (pending-space :initform nil :type boolean)
   (current-indent :initform 0 :type integer) ; current line indentation
   (pending-indent :initform nil :type (or null integer))))

(defun fill-stream-line-position (fill-stream)
  (with-slots (target-stream buffer pending-space) fill-stream
    (let ((pos (sys::line-position target-stream)))
      (if pos
          (+ pos (if pending-space 1 0) (string-width buffer))
          nil))))

(defun fill-stream-text-indent (stream)
  (let ((text-indent-raw (slot-value stream 'indent-var)))
    (etypecase text-indent-raw
      (number text-indent-raw)
      (symbol (symbol-value text-indent-raw)))))

(defun fill-stream-sexp-indent (stream)
  (let* ((sexp-indent-raw (slot-value stream 'sexp-indent))
         (text-indent (fill-stream-text-indent stream))
         (sexp-indent-value
          (etypecase sexp-indent-raw
            (number sexp-indent-raw)
            (symbol (symbol-value sexp-indent-raw))
            (function (funcall sexp-indent-raw text-indent)))))
    (case sexp-indent-value
      ((nil) 0)
      ((t) text-indent)
      (t sexp-indent-value))))

;; SEXP is either a single line, then it is formatted inline as a word,
;; or it takes several lines, then it is formatted as an indented block

;; flush the buffer and print a newline (when NEWLINE-P is non-NIL)
(defun fill-stream-flush-buffer (stream newline-p &aux sexp-block-p)
  (with-slots (target-stream buffer pending-indent current-indent
               pending-space inside-sexp)
      stream
    (flet ((newline ()          ; terpri
             (setq current-indent (fill-stream-text-indent stream)
                   pending-indent current-indent)
             (terpri target-stream)))
      (when (plusp (length buffer)) ; something in the buffer to flush
        ;; fill: if the buffer does not fit on the line, TERPRI
        (let ((pos (fill-stream-line-position stream)))
          (when (and pos (<= (right-margin) pos)) ; does not fit on this line
            (setq sexp-block-p (find #\newline buffer)) ; only inside sexp
            (unless sexp-block-p (newline))
            (when inside-sexp ; just finished an S-expression
              (setq newline-p (or newline-p sexp-block-p)))))
        (unless sexp-block-p ; S-expression in a block
          (cond (pending-indent      ; do the indent
                 (sys::write-spaces pending-indent target-stream)
                 (setq pending-indent nil))
                (pending-space
                 (write-char #\Space target-stream))))
        (setq pending-space nil)
        (if sexp-block-p
          (do* ((indent (fill-stream-sexp-indent stream))
                (beg 0 (1+ end))
                (end (position #\Newline buffer)
                     (position #\Newline buffer :start beg)))
               ((null end)
                (write-char-sequence buffer target-stream :start beg))
            (write-char-sequence buffer target-stream :start beg :end end)
            (terpri target-stream)
            (sys::write-spaces indent target-stream))
          (write-char-sequence buffer target-stream))
        (setf (fill-pointer buffer) 0))
      (when newline-p (newline)))))

(progn
(defmethod stream-write-char ((stream fill-stream) ch)
  (with-slots #1=(buffer pending-space inside-sexp) stream
    #2=
    (if inside-sexp
        (vector-push-extend ch buffer)
        (case ch
          (#\Newline (fill-stream-flush-buffer stream t))
          ((#\Space #\Tab)
           (when (plusp (length buffer))
             (fill-stream-flush-buffer stream nil))
           (setq pending-space t))
          (t (vector-push-extend ch buffer))))))
(defmethod stream-write-char-sequence ((stream fill-stream) sequence
                                       &optional (start 0) (end nil))
  (with-slots #1# stream
    ;; make sure that the buffer can accommodate the sequence
    (let ((new-size (+ (length sequence) (length buffer))))
      (when (> new-size (array-dimension buffer 0))
        (adjust-array buffer new-size)))
    ;; Same body as in stream-write-char.
    (count-if (lambda (ch) #2#) sequence :start start :end end))
  sequence))

(defmethod stream-line-column ((stream fill-stream))
  (let ((pos (fill-stream-line-position stream)))
    (if pos (max (- pos (slot-value stream 'current-indent)) 0) nil)))
(defmethod stream-start-line-p ((stream fill-stream))
  (let ((pos (fill-stream-line-position stream)))
    (if pos (<= pos (slot-value stream 'current-indent)) nil)))
(defmethod stream-finish-output ((stream fill-stream))
  (fill-stream-flush-buffer stream nil)
  (finish-output (slot-value stream 'target-stream)))
(defmethod stream-force-output ((stream fill-stream))
  (fill-stream-flush-buffer stream nil)
  (force-output (slot-value stream 'target-stream)))
(defmethod stream-clear-output ((stream fill-stream))
  (with-slots (target-stream buffer pending-indent pending-space) stream
    (setq pending-indent nil pending-space nil)
    (setf (fill-pointer buffer) 0)
    (clear-output target-stream)))

(defmacro with-fill-stream ((stream-var target-stream &rest opts) &body body)
  (multiple-value-bind (body-rest declarations) (parse-body body)
    `(LET ((,stream-var (MAKE-INSTANCE 'fill-stream :STREAM ,target-stream
                                       ,@opts)))
       (DECLARE (READ-ONLY ,stream-var) ,@declarations)
       (UNWIND-PROTECT (PROGN ,@body-rest)
         (FORCE-OUTPUT ,stream-var)))))

;;; for format, see `format-s-expression'
(fmakunbound 'stream-start-s-expression)
(fmakunbound 'stream-end-s-expression)
(defgeneric stream-start-s-expression (stream)
  (:documentation "Return the new binding for *PRINT-RIGHT-MARGIN*.")
  (:method ((stream t)) (declare (ignore stream)) *print-right-margin*)
  (:method ((stream fill-stream))
    (fill-stream-flush-buffer stream nil)
    (setf (slot-value stream 'inside-sexp) t)
    (- (right-margin) (fill-stream-sexp-indent stream))))
(defgeneric stream-end-s-expression (stream)
  (:method ((stream t)) (declare (ignore stream)))
  (:method ((stream fill-stream))
    (fill-stream-flush-buffer stream nil)
    (setf (slot-value stream 'inside-sexp) nil)))

(defmacro with-stream-s-expression ((stream) &body body)
  `(let ((*print-right-margin* (stream-start-s-expression ,stream)))
     ,@body
     (stream-end-s-expression ,stream)))
