;; -*- Mode:LISP; Readtable:CL; Base:10;
       Package:(SIMPLE-PATHNAMES :USE LISP :NICKNAMES (PATH)) -*-

;;;CL-PATHNAMES.LISP
;;;
;;;A structure-based pathname implementation.  Includes hosts and a minimal
;;;interface for the network software.

;;;Although designed to be portable in CommonLISP, a separate set of Lambdoid
;;;(ZetaLISP compatibility) features will be provided, in
;;;ZL-PATHNAME-COMPATIBILITY.
;;;
;;;A major goal in this implementation is to avoid the need for cluttering up
;;;the code with additional interfaces and special handling, e.g. for specific
;;;host types.

;;;The following CommonLISP interfaces are defined at the end of this file:

;;;Component accessors:
;;;
;;; $_PATHNAME-DEVICE
;;; $_PATHNAME-DIRECTORY
;;; $_PATHNAME-HOST
;;; $_PATHNAME-NAME
;;; $_PATHNAME-TYPE
;;; $_PATHNAME-VERSION
;;;
;;;Related functions:
;;;
;;; FILE-$_NAMESTRING
;;; DIRECTORY-$_NAMESTRING
;;; ENOUGH-$_NAMESTRING
;;; HOST-$_NAMESTRING
;;; MAKE-$_PATHNAME
;;; MERGE-$_PATHNAMES
;;; $_NAMESTRING
;;; PARSE-$_NAMESTRING
;;; $_PATHNAME
;;; $_PATHNAMEP
;;; $_TRUENAME
;;;

;;;Utility functions

;;;For compiling within this file only:

(eval-when (compile)

(defvar *path-debug* t)
(defmacro path-debug (&rest args)
  (if *path-debug* ` (format *trace-output* ,@args)))
)


;;;Published variables:

(defvar *$_pathname-host-list* NIL
  "List containing all hosts that can serve in pathnames.")

;;;NB: ZetaLISP does pathname defaulting in an incompatible manner.
;;;I've tried to provide both ZL and CL compatibility in this
;;;implementation.  See *DEFAULT-$_PATHNAME-DEFAULTS-LIST*, below.

(defvar *default-$_pathname-defaults* nil
  "If non-NIL, this is a pathname that supplies default pathname information.
MAKE-$_PATHNAME and others use this to fill in omitted information.
If NIL, the host-specific defaults list *DEFAULT-$_PATHNAME-DEFAULTS-LIST*
  is then checked for an entry for the specified pathname host, or for the 
  default host *DEFAULT-$_PATHNAME_HOST*.")

(defvar *default-$_pathname-defaults-list* nil
  "If *DEFAULT-$_PATHNAME-DEFAULTS* is NIL, and this is non-NIL, it
  supplies host-specific default pathname information.
Functions such as MAKE-$_PATHNAME fill in omitted information from this
  list, using default information for the specified pathname host, or
  for the default pathname host *DEFAULT-$_PATHNAME_HOST*.")

(defvar *default-$_pathname-host* nil
  "If non-NIL, this the default host from which to obtain host-specific 
  default pathname information.
See *DEFAULT-$_PATHNAME-DEFAULTS-LIST*.")

(defvar *local-host* :unbound "The host corresponding to the local machine.")


;;;;;; Structure definitions

;;; BASIC-PATHNAME -- no-frills, for internal use only:

(defstruct ($_basic-pathname
	     (:print-function print-basic-pathname)
	     (:conc-name $_pathname_)
	     :named)
  ;;The accessors are <not> the same as the CommonLISP interfaces!
  ;;This defines, e.g., $_PATHNAME_HOST, not PATHNAME-HOST.
  ;;
  ;;Except for the host component, all slots are initialized to NIL.
  (host (error-no-host-supplied))
  device directory name type version)

;;;Normal, parseable pathname with namestring:

(defstruct ($_pathname
	     :named
	     (:print-function print-pathname)
	     (:include $_basic-pathname)
	     (:conc-name $_pathname_)
	     (:constructor internal-make-$_pathname
			   (&optional host device directory name type version namestring)))
  ;;The standard string representation is parsed from the $_BASIC-PATHNAME slots:
  namestring
  ;;Flag whether this pathname has been parsed
  parsed
  ;;A place to hang properties; use GETF to access values.
  property-list
  ;;A "truename" is the translated from this form of pathname to another one:
  truename)

;;;Hosts

(defstruct ($_host
	     :named
	     (:constructor make-$_host))
  name
  nicknames
  force-case
  namestring
  parsed
  ;;Functions for parsing full pathnames and components
  (parser 'parse-basic-pathname)
  ;;Functions for "printing" pathnames and components
  (pathname-printer 'default-pathname-printer)
  sample-pathname)

(defstruct ($_network-host :named (:include $_host))
  software-type
  hardware-type
  network-addresses
  networks)

;;;Pathname internal utilities

(defun error-no-host-supplied (&optional namestring)
  (error "Pathname ~@[~S ~]was not defined with a pathname host" namestring))

(defun print-basic-pathname (pathname &optional (stream *standard-output*) print-level)
  (declare (ignore print-level))
  (format stream "#<$_BASIC-PATHNAME ~S ~S ~S ~S ~S ~S>"
	  (safe-host-from-pathname pathname)
	  ($_pathname_device pathname)
	  ($_pathname_directory pathname)
	  ($_pathname_name pathname)
	  ($_pathname_type pathname)
	  ($_pathname_version pathname)))

(defmacro with-$_pathname-components ((var pathname) &body body)
  `(let ((,var ,pathname))
     (let ((host      ($_pathname_host ,var))
	   (device    ($_pathname_device ,var))
	   (directory ($_pathname_directory ,var))
	   (name      ($_pathname_name ,var))
	   (type      ($_pathname_type ,var))
	   (version   ($_pathname_version ,var)))
       ,@body)))

;;;Printing / name strings:

(defun default-pathname-printer (pathname &optional stream print-level)
  (declare (ignore print-level))
  (check-type pathname $_PATHNAME)
  (with-$_pathname-components (path pathname)
    (check-type host $_host)
    (flet ((print-it (s)
		     (format s "\"~A:~@[ ~A:~]~@[ (~{~A~^ ~})~]~{~@[ ~A~]~}~@[ ~S~]\""
			     ($_host-name host)
			     device directory
			     (list name type) version)))
      (if stream (print-it stream)
	(with-output-to-string (str)
	  (print-it str))))))

(defun print-pathname (pathname &optional (stream *standard-output*) print-level)
  (declare (ignore print-level))
  (let ((*print-escape* nil))
    (format stream "#<~A ~A>"
	    (type-of pathname)
	    (funcall (or ($_host-pathname-printer ($_pathname_host pathname))
			 'default-pathname-printer)
		     pathname nil))))

(defun print-host (host &optional (stream *standard-output*) print-level)
  (declare (ignore print-level))
  (format stream "#(HOST ~A)"
	  (or ($_host-name host) "<unnamed>")))

;;;Host internal utilities

(defun sample-$_pathname (host)
  (check-type host $_host)
  (or ($_host-sample-pathname host)
      (internal-make-$_pathname host)))

(defun host-type (host)
  (type-of host))


;;;Parsing / validation:

(defun parse-basic-pathname (namestring &optional host)
  (check-type namestring string)
  (setq namestring (string-trim '(#\space #\tab #\return) namestring))
  (let ((start 0) end tem)
    (when (and (setq tem (search "#\<" namestring)) (zerop tem))
      (setq start 2)
      (setq end
	    (do ((i (1- (length namestring)) (1- i)))
		((or (<= i 2) (char-equal #\> (char namestring i)))
		 i))))
    (flet ((read-one-part ()
	    (with-input-from-string (in namestring :start start :end end :index start)
	      (read in))))
      (path-debug "~&Parsing a ~S pathname" (read-one-part))
      (make-$_pathname
	:host      (let ((inhost (read-one-part)))
		     (or host inhost))
	:device    (read-one-part)
	:directory (read-one-part)
	:name      (read-one-part)
	:type      (read-one-part)
	:version   (read-one-part)))))

(defun internal-parse-$_pathname (pathname &optional (host ($_pathname_host pathname))
				  &aux parsed)
  (setf ($_pathname_parsed pathname)
	(setq parsed (let ((parser ($_host-parser host)))
		       (if parser (funcall parser ($_pathname_namestring pathname) t)))))
  (values pathname parsed))

(defmacro fix-type (var types &optional error-p)
  `(progn (check-type ,var ,(if error-p `(or ,@types) `(or ,@types null)))
	  (or ,var
	      ,(null error-p)
	      (error "~S is not initialized in pathname" ',var))))
      
(defun check-$_pathname (pathname)
  "Set PARSED flag if all is well."
  (with-$_pathname-components (path pathname)
    (fix-type host      ($_host) :must-be-supplied)
    (fix-type device    (string))
    (fix-type directory (list))
    (fix-type name      (string))
    (fix-type type      (string))
    (fix-type version   (number keyword))
    (setf ($_pathname_parsed path) t)
    path))

;;;Pathname defaulting:

;;;+++This is a mess:
(defun default-host ()
  (if *default-$_pathname-defaults*
      ($_pathname_host *default-$_pathname-defaults*)
    *default-$_pathname-host*))

(defun set-default-host (host)
  (setq *default-$_pathname-host* host))

(defsetf default-host set-default-host)

(defun default-pathname (&optional (host *default-$_pathname-host*) (use-per-host-defaults-p t))
  (or *default-$_pathname-defaults*
      (and use-per-host-defaults-p
	   (setq *default-$_pathname-defaults* 
		 (or (cdr (assoc host *default-$_pathname-defaults-list*))
		     (cdr (assoc NIL *default-$_pathname-defaults-list*)))))))

(defun set-default-pathname (pathname &optional (host *default-$_pathname-host*))
  (if (assoc host *default-$_pathname-defaults*)
      (setf (first (assoc host *default-$_pathname-defaults*)) host)
    (push (cons host (sample-$_pathname host)) *default-$_pathname-defaults*))
  (when host (set-default-pathname pathname NIL))
  (setq *default-$_pathname-defaults* pathname))

(defun default-sample-pathname ()
  (let ((host (default-host)))
    (and host (sample-$_pathname host))))

;;;;;;Published (CommonLISP) interfaces

(defun merge-$_pathnames (pathname &optional
			  (defaults *DEFAULT-$_PATHNAME-DEFAULTS*)
			  (default-version :newest))
  "This is the function that most programs should call on supplied file names.
Unspecified (NIL) components of PATHNAME are filled in from DEFAULTS.
Returns a pathname."
    (if (null (or pathname defaults))
	(error "Both PATHNAME and DEFAULTS are NIL")
      (let ((pathname (and pathname ($_pathname pathname)))
	    (defaults (and defaults ($_pathname defaults))))
	(if (null (and pathname defaults))
	    (or pathname defaults)
	  ;;;Merge components
	  (check-$_pathname
	    (let (name type)
	      (internal-make-$_pathname
		(or ($_pathname_host pathname) ($_pathname_host defaults))
		(or ($_pathname_device pathname) ($_pathname_device defaults))
		(or ($_pathname_directory pathname) ($_pathname_directory defaults))
		;;;+++This is just for debugging, so far.
		;;;Version depends on name/type:
		(setq name (or ($_pathname_name pathname) ($_pathname_name defaults)))
		(setq type (or ($_pathname_type pathname) ($_pathname_type defaults)))
		(if (or name type)
		    (or ($_pathname_version pathname) ($_pathname_version defaults)
			default-version)))))))))

(defun make-$_pathname (&key host device directory name type version
			(defaults (default-sample-pathname)))	;;;+++Turn off per-host defaulting!!!
  "Create a pathname, specifying components as keyword arguments.
Missing components are merged in from DEFAULTS, which defaults to a sample
  (mostly NULL) pathname for the host specified by *DEFAULT-$_PATHNAME-DEFAULTS*."
  (check-$_pathname
    (merge-$_pathnames
      (internal-make-$_pathname
	(or host (and defaults ($_pathname-host defaults)))
	device directory name type version)
      defaults version)))

;;;@@@Unfortunately, the code below requires that streams (and Flavors!) be
;;;defined previously.  Suggested cure: redo with CLOS.

(DEFUN $_PATHNAME (OBJECT)
  "Convert OBJECT to a $_pathname.
If it's a pathname, it is unchanged.
If it's a stream, its :PATHNAME operation is invoked.
If it's a string or symbol, it is parsed into a pathname."
  (etypecase object
    ($_pathname (check-$_pathname object))
    (stream (funcall object :pathname))
    ((or symbol string) (parse-$_namestring object))))

(defun $_PATHNAMEP (OBJECT)
  "T if OBJECT is a pathname."
  (typep object '$_pathname))

(DEFUN $_TRUENAME (OBJECT)
  "Returns the truename of the file OBJECT, as a pathname, refers to."
  (typecase object
    (stream (funcall object :$_truename))
    (t  ($_pathname_truename ($_pathname object)))))

(DEFUN $_NAMESTRING (OBJECT)
  "Convert OBJECT to a pathname and return its namestring."
  ($_pathname_namestring ($_pathname object))) 

(DEFUN $_PATHNAME-HOST (pathname)
  "Returns the host component of PATHNAME."
  ($_pathname_host ($_pathname pathname)))

(DEFUN $_PATHNAME-DEVICE (pathname)
  "Returns the device component of PATHNAME."
  ($_pathname_device ($_pathname pathname)))

(DEFUN $_PATHNAME-DIRECTORY (pathname)
  "Returns the directory component of PATHNAME."
  ($_pathname_directory ($_pathname pathname)))

(DEFUN $_PATHNAME-NAME (pathname)
  "Returns the name component of PATHNAME."
  ($_pathname_name ($_pathname pathname)))

(DEFUN $_PATHNAME-TYPE (pathname)
  "Returns the type component of PATHNAME."
  ($_pathname_type ($_pathname pathname)))

(DEFUN $_PATHNAME-VERSION (pathname)
  "Returns the version component of PATHNAME."
  ($_pathname_version ($_pathname pathname)))

;;;Load-time initializations:

;;;Define local host:

(setq *local-host* (make-$_host :name "LOCAL"))

;;;Make local host the default host:

(set-default-host *local-host*)
