;;; -*- Mode:LISP; Package:USER; Base:10; Readtable:CL -*-

;;; WRITE-BLOCKED-TAPE.LISP

;;; Routines for writing raw tapes in blocked and variable format.

;;; See MAKE-BLOCKED-TAPE and MAKE-VMS-COPY-TAPE, below.

;;; Still to do:
;;;  -- allow to specify recursive pathnames
;;;  -- give usage examples

(defconstant *max-buffer-size* 4096)

(defparameter *write-blocked-tape-verbosely* t
  "If non-NIL, informative information is printed while writing tape.")

(defconstant *write-blocked-tape-lispm-chars*
	     (list #\tab #\page #\return #\line))

;;; Utility functions

(defun string-ascii-translate(string)
  "Destructively modifies STRING to translate LISPM control characters
 (contained in the list *WRITE-BLOCKED-TAPE-LISPM-CHARS*) to their
ASCII equivalents."
  (dotimes(indx (string-length string))
    (when (member (aref string indx) *write-blocked-tape-lispm-chars*)
      (setf (aref string indx) (- (aref string indx) #o200)))))

(defun paths-and-streams(paths-and-streams)
  (unless (listp paths-and-streams)
    (setq paths-and-streams (ncons paths-and-streams)))
  (loop for elt in paths-and-streams
	append
	(etypecase elt
	  ((or pathname string)
	   (directory elt))
	  (stream (ncons elt)))))

;;; VMS special stuff

(defun vms-type(pathname)
  (let((type (send (pathname pathname) :canonical-type)))
    (or (cadr(assoc :vms (getf fs:canonical-types type)))
	type)))

(defun vms-filename-fix(name)
  (substitute-if-not
    #\$
    #'(lambda(char)
	(or (member char (list #\_ #\$ #\.))	;VMS special chars
	    (alphanumericp char)))
    (substitute
      #\_ #\- name)))
    
(defun vms-names(paths)
  (loop for path in paths
	as name = (vms-filename-fix (pathname-name path))
	as ltype = (pathname-type path)
	as vms-type = (vms-type path)
	collect (string-append name #\. vms-type)))

;;; Main I/O routines

;;; Write from stream to tape

(defun write-blocked-stream(input output
			    record-size blocking-factor
			    &optional (ascii-translation t)
			    &aux buffer-size block-size)
  "Copy records from INPUT stream, to OUTPUT, which is either:
  1. The tape device, e.g. TAPE:*SELECTED-DEVICE*, or
  2. An output stream such as a file or window.
Records are blocked according to RECORD-SIZE and BLOCKING-FACTOR;
a RECORD-SIZE of NIL and BLOCKING-FACTOR of one means
variable-length record/blocks."
  (if record-size
      (setq buffer-size (* record-size blocking-factor)
	    block-size buffer-size)
    (setq buffer-size *max-buffer-size*))
  (using-resource (buffer-block si:dma-buffer
				(ceiling (quotient buffer-size 1024.0)))
    (let((record-within-buffer 0)
	 (record-within-input 0)
	 (buffers-to-output 0)
	 (buffer (si:dma-buffer-string buffer-block)))
      (fill buffer #\space)
      ;;"Generic" I/O methods (knows about tape device vs. standard streams)
      (flet((putout()
		   (etypecase output
		     (tape:tapemaster-device
		      (send output :write-block buffer-block block-size))
		     ((or si:output-stream tv:sheet)
		      (send output :line-out buffer 0 block-size)))
		   (incf buffers-to-output))
	    (puteof()
		   (etypecase output
		     (tape:tapemaster-device
		      (send output :write-filemark))
		     ((or si:output-stream tv:sheet)
		      (send output :fresh-line)))))
	;;Main I/O loop
	(do ((line (read-line input nil :eof)
		   (read-line input nil :eof)))
	    ((equal line :eof))
	  (incf record-within-input)
	  (if ascii-translation
	      (string-ascii-translate line))
	  ;;Do we have a full buffer to output?
	  (when (= record-within-buffer blocking-factor)
	    (putout)				;output buffer
	    (fill buffer #\space)		;blank out buffer
	    (setq record-within-buffer 0))
	  ;;Null lines need at least one blank
	  (when (zerop (string-length line))
	      (setq line " "))
	  ;;Set blocksize for variable length
	  (unless record-size
	    (setq block-size (string-length line)))
	  ;;Insert input record into output buffer
	  (replace buffer line
		   :start1
		   (if record-size (* record-within-buffer record-size)
		     0))
	  (incf record-within-buffer))
	;;Final buffer pending?
	(unless (zerop record-within-buffer)
	  (putout))
	(puteof))
      ;;Done I/O
      (when *write-blocked-tape-verbosely*
	  (format t "~%---Wrote ~d record~:P~:[~;, ~d block~:P~]"
		  record-within-input (> blocking-factor 1) buffers-to-output)
	  (format t "~&   from ~s~&   to   ~s"
		  (or (send-if-handles input :name)  input)
		  (or (send-if-handles output :name) output)))
      record-within-input)
    ))

;;; Write a collection of streams and/or files to tape

(defun write-blocked-tape(paths &optional record-size (blocking-factor 1))
  "Write file(s) and/or open streams as specified by the list PATHS
to the selected tape device in blocked format, formatted
according to RECORD-SIZE and BLOCKING-FACTOR."
  (let((tape tape:*selected-device*)
       (tape:*selected-format* (make-instance 'tape:raw-format)))
    (when (null (and record-size blocking-factor))
      (setq blocking-factor 1))
    (when *write-blocked-tape-verbosely*
      (format t "~&+++Write blocked tape:")
      (format t " record size is ~:[variable~;~:*~d~]" record-size)
      (when (> blocking-factor 1)
	(format t ", blocking factor is ~d" blocking-factor)))
    (dolist (path-or-stream paths)
      (typecase path-or-stream
	(pathname
	 (with-open-file(in path-or-stream :direction :input)
	   (write-blocked-stream
	     in tape
	     record-size blocking-factor)))
	(stream
	 (write-blocked-stream
	   path-or-stream tape
	   record-size blocking-factor))))))

;;; Complete user interfaces

;;; MAKE-BLOCKED-TAPE is generalized; given a pathname or list of files,
;;; write them to tape using the specified record size and blocking factor.

(defun make-blocked-tape (paths-and-streams &optional record-size blocking-factor &aux paths)
  "Make a complete blocked record-format tape suitable for reading, e.g., by
  VAX/VMS COPY or UNIX dd.
PATHS-AND-STREAMS can be:
    1) a pathname (object or string), passed to DIRECTORY to
       get the list of files to write; or
    2) an open stream (gets copied to the tape; not closed!), or
    2) a list of pathnames and/or open streams.
BLOCKING-FACTOR is a number of records to write as one block; default
  is NIL, or 1 record per block.
RECORD-SIZE can be NIL, in which case variable-length record blocks are
  written (BLOCKING-FACTOR is set to 1).
The tape is finished off with 2 EOFs."
  (setq paths (paths-and-streams paths-and-streams))
  (when (null paths)
    (error "No file(s) selected by pathname(s)"))
  (write-blocked-tape paths record-size blocking-factor)
  (dotimes(i 2)
    (send tape:*selected-device* :write-filemark))
  (send tape:*selected-device* :search-filemark-reverse 1))

;;; MAKE-VMS-COPY-TAPE is specific to the VAX/VMS COPY format.  It puts a
;;; command file at the head of the tape which can be used to restore the
;;; blocked files. Modifications to the generated COPY-LISPM.COM file may be
;;; needed, e.g.  to use other COPY options.

;;; To restore a tape made with this utility, do, e.g. (if your tape drive is
;;; called MSA0:)

;;;   $ assign msa0: mt:
;;;   $ mount/for mt:
;;;   $ copy mt: temp.com
;;;   $ @temp

(defun make-vms-copy-tape(pathname-list &aux paths)
  "Interface like MAKE-BLOCKED-TAPE, specifically for VAX/VMS; see it for info.
Make a VAX/VMS COPY (text file) tape; the first file on the tape is a VMS
.COM file, which when restored and run, will restore all the files on the tape
with appropriate VMS names.
The tape is finished off with 2 EOFs."
  (setq paths (paths-and-streams pathname-list))
  (when (null paths)
    (error "No file(s) selected by pathname(s)"))
  (let((vms-pathstrings
	 (cons "TEMP.COM"
	       (vms-names paths))))
    (tape:rewind)
    (with-input-from-string
      (comfile
	(apply #'(lambda(&rest strings)
		   (apply #'string-append
			  (loop for str in strings
				collect (string-append "$ " str #\return))))
	       "! Machine-generated, tape-specific command file for"
	       "! restoring a VMS COPY-format source tape created on"
	       "! a Lambda LISP machine with the utility"
	       "! WRITE-VMS-COPY-TAPE."
	       "set noon"
	       "DISMOUNT/NOUNLOAD MT:"
	       "MOUNT/FOR MT: NOLABEL MYTAPE:"
	       (loop for path in vms-pathstrings
		     collect (string-append "COPY/LO MYTAPE: " path))))
      (write-blocked-tape (ncons comfile))
      (write-blocked-tape paths)))
  (dotimes(i 2)
    (send tape:*selected-device* :write-filemark))
  (send tape:*selected-device* :search-filemark-reverse 1))
