;;!emacs
;; $Id: 
;;
;; FILE:         hpath.el
;; SUMMARY:      Hyperbole support routines for handling UNIX paths.  
;; USAGE:        GNU Emacs Lisp Library
;;
;; AUTHOR:       Bob Weiner
;; ORG:          Brown U.
;;
;; ORIG-DATE:     1-Nov-91 at 00:44:23
;; LAST-MOD:     18-May-93 at 15:07:32 by Bob Weiner
;;
;; This file is part of Hyperbole.
;; 
;; Copyright (C) 1991, Brown University, Providence, RI
;; Developed with support from Motorola Inc.
;; 
;; Permission to use, modify and redistribute this software and its
;; documentation for any purpose other than its incorporation into a
;; commercial product is hereby granted without fee.  A distribution fee
;; may be charged with any redistribution.  Any distribution requires
;; that the above copyright notice appear in all copies, that both that
;; copyright notice and this permission notice appear in supporting
;; documentation, and that neither the name of Brown University nor the
;; author's name be used in advertising or publicity pertaining to
;; distribution of the software without specific, written prior permission.
;; 
;; Brown University makes no representations about the suitability of this
;; software for any purpose.  It is provided "as is" without express or
;; implied warranty.
;;
;;
;; DESCRIPTION:  
;; DESCRIP-END.

;;; ************************************************************************
;;; Public functions
;;; ************************************************************************

(defun hpath:absolute-to (path &optional default-dirs)
  "Returns PATH as an absolute path relative to one directory from optional DEFAULT-DIRS or 'default-directory'.
Returns PATH unchanged when it is not a valid path or when DEFAULT-DIRS
is invalid.  DEFAULT-DIRS when non-nil may be a single directory or a list of
directories.  The first one in which PATH is found is used."
  (if (not (stringp path))
      path
    (if (not (cond ((null default-dirs)
		    (setq default-dirs (cons default-directory nil)))
		   ((stringp default-dirs)
		    (setq default-dirs (cons default-dirs nil)))
		   ((listp default-dirs))
		   (t nil)))
	path
      (let ((rtn) dir)
	(while (and default-dirs (null rtn))
	  (setq dir (expand-file-name
		     (file-name-as-directory (car default-dirs)))
		rtn (expand-file-name path dir)
		default-dirs (cdr default-dirs))
	  (or (file-exists-p rtn) (setq rtn nil)))
	(or rtn path)))))

(defun hpath:at-p (&optional type non-exist)
  "Returns delimited path or non-delimited ange-ftp path at point, if any.
Delimiters may be:  double quotes, open and close single quote, or
Texinfo file references.
If optional TYPE is the symbol 'file or 'directory, then only that path type is
accepted as a match.  Only locally reachable paths are checked for existence.
With optional NON-EXIST, nonexistent local paths are allowed.
Absolute pathnames must begin with a '/' or '~'.  Relative pathnames
must begin with a './' or '../' to be recognized."
  (or (hpath:ange-ftp-p)
      (hpath:is-p (or (hargs:delimited "\"" "\"") 
		      ;; Filenames in Info docs
		      (hargs:delimited "\`" "\'")
		      ;; Filenames in TexInfo docs
		      (hargs:delimited "@file{" "}"))
		  type non-exist)))

(defun hpath:ange-ftp-p ()
  "Returns an ange-ftp pathname that point is within or nil.
See the 'ange-ftp' Elisp package for pathname format details.
Always returns nil if the ange-ftp package has not been loaded."
  (if (featurep 'ange-ftp)
      (let ((user (if (stringp ange-ftp-default-user)
		      ange-ftp-default-user "anonymous"))
	    path)
	(setq path
	(save-excursion
	  (skip-chars-backward "^[ \t\n\"`'\(\{")
	  (cond
	   ;; user, domain and path
	   ((looking-at "/?[^@/:]+@[^/:@ \t\n\^M\"`']+:[^]@ \t\n\^M\"`'\)\}]*")
	    (buffer-substring (match-beginning 0) (match-end 0)))
	   ;; @domain and path
	   ((looking-at "@[^/:@ \t\n\^M\"`']+:[^]@ \t\n\^M\"`'\)\}]*")
	    (concat "/" user (buffer-substring
			      (match-beginning 0) (match-end 0))))
	   ;; domain and path
	   ((and (looking-at
  "/?\\(\\([^/:@ \t\n\^M\"`']+\\):[^]@:, \t\n\^M\"`'\)\}]*\\)[] \t\n\^M,.\"`'\)\}]")
		 (setq path (buffer-substring
			     (match-beginning 1) (match-end 1)))
		 (string-match "[^.]\\.[^.]"
			       (buffer-substring (match-beginning 2)
						 (match-end 2))))
	    (concat "/" user "@" path)))))
	(if (and path (= ?. (aref path (1- (length path)))))
	    (substring path 0 -1)
	  path))))

(defun hpath:find (filename)
  "Edit file FILENAME using program from hpath:find-alist if available.
Switch to a buffer visiting file FILENAME, creating one if none already
exists.  Return non-nil iff file is displayed within a buffer."
  (interactive "FFind file: ")
  (setq filename (hpath:substitute-value filename))
  (hpath:validate filename)
  (let ((find-program (hpath:find-program filename)))
    (if find-program
	(progn (hact 'exec-window-cmd (concat find-program " " filename))
	       nil)
      (switch-to-buffer (find-file-noselect filename))
      t)))

(defun hpath:find-other-window (filename)
  "Edit file FILENAME, in another window or using program from hpath:find-alist.
May create a new window, or reuse an existing one; see the function display-buffer.
Return non-nil iff file is displayed within a buffer."
  (interactive "FFind file in other window: ")
  (setq filename (hpath:substitute-value filename))
  (hpath:validate filename)
  (let ((find-program (hpath:find-program filename)))
    (if find-program
	(progn (hact 'exec-window-cmd (concat find-program " " filename))
	       nil)
      (switch-to-buffer-other-window (find-file-noselect filename))
      t)))

(defun hpath:is-p (path &optional type non-exist)
  "Returns PATH if PATH is a Unix path, else nil.
If optional TYPE is the symbol 'file or 'directory, then only that path type
is accepted as a match.  The existence of the path is checked only for
locally reachable paths (Info paths are not checked).  Single spaces are
permitted in middle of pathnames, but not at the start.  Tabs and newlines
are converted to space before the pathname is checked, this normalized path
form is what is returned for PATH.  With optional NON-EXIST, nonexistent
local paths are allowed."
  (and (stringp path) (not (or (string= path "")
			       (string-match "\\`\\s \\|\\s \\'" path)))
       ;; Convert tabs and newlines to space.
       (setq path (hbut:key-to-label (hbut:label-to-key path)))
       (or (not (string-match "[()]" path))
	   (string-match "\\`([^\)]+)[ A-Za-z0-9]" path))
       (if (string-match "\\${[^}]+}" path)
	   (setq path (hpath:substitute-value path))
	 t)
       (not (string-match "[\t\n\^M\"`'{}|\\]" path))
       (let ((remote-path (string-match "@.+:\\|.+:/" path)))
	 (cond (remote-path
		(cond ((eq type 'file)
		       (if (not (string= "/" (substring path -1))) path))
		      ((eq type 'directory)
		       (if (string= "/" (substring path -1)) path))
		      (path)))
	       ((or non-exist (file-exists-p path))
		(cond ((eq type 'file)
		       (and (not (file-directory-p path)) path))
		      ((eq type 'directory)
		       (and (file-directory-p path) path))
		      (path)))))))

(defun hpath:relative-to (path &optional default-dir)
  "Returns PATH relative to optional DEFAULT-DIR or 'default-directory'.
Returns PATH unchanged when it is not a valid path."
  (if (not (and (stringp path) (file-exists-p path)))
      path
    (setq default-dir
	  (expand-file-name
	   (file-name-as-directory (or default-dir default-directory)))
	  path (expand-file-name path))
    (and path default-dir
	 (let ((end-dir (min (length path) (length default-dir))))
	   (if (string= (substring path 0 end-dir) default-dir)
	       (concat "./" (substring path end-dir)) path)))))

(defun hpath:substitute-value (path)
  "Substitutes matching value for Emacs Lisp variables and environment variables in PATH.
Returns path with variable values substituted."
  (substitute-in-file-name
    (hypb:replace-match-string
      "\\${[^}]+}"
      path
      (function
	(lambda (str)
	  (let* ((var-group (substring path match start))
		 (var-name (substring path (+ match 2) (1- start)))
		 (rest-of-path (substring path start))
		 (sym (intern-soft var-name)))
	    (if (file-name-absolute-p rest-of-path)
		(setq rest-of-path (substring rest-of-path 1)))
	    (if (and sym (boundp sym))
		(directory-file-name (hpath:substitute-dir var-name rest-of-path))
	      var-group))))
      t)))

(defun hpath:substitute-var (path)
  "Replaces up to one match in PATH with first matching variable from 'hpath:variables'."
  (if (not (and (stringp path) (string-match "/" path)))
      path
    (setq path (hpath:symlink-referent path))
    (let ((new-path)
	  (vars hpath:variables)	  
	  result var val)
      (while (and vars (null new-path))
	(setq var (car vars) vars (cdr vars))
	(if (boundp var)
	    (progn (setq val (symbol-value var))
		   (cond ((stringp val)
			  (if (setq result
				    (hpath:substitute-var-name var val path))
			      (setq new-path result)))
			 ((null val))
			 ((listp val)
			  (while (and val (null new-path))
			    (if (setq result
				    (hpath:substitute-var-name var (car val) path))
				(setq new-path result))
			    (setq val (cdr val))))
			 (t (error "(hpath:substitute-var): '%s' has invalid value for hpath:variables" var))))))
      (or new-path path)
      )))

;;
;; The following function recursively resolves all UNIX links to their
;; final referents.
;; Works with Apollo's variant and other strange links like:
;; /usr/local -> $(SERVER_LOCAL)/usr/local, /usr/bin ->
;; ../$(SYSTYPE)/usr/bin and /tmp -> `node_data/tmp.  It also handles
;; relative links properly as in /usr/local/emacs -> gnu/emacs which must
;; be resolved relative to the '/usr/local' directory.
;; It will fail on Apollos if the '../' notation is used to move just
;; above the '/' directory level.  This is fairly uncommon and so the
;; problem has not been fixed.
;;
(defun hpath:symlink-referent (linkname)
  "Returns expanded file or directory referent of LINKNAME.
LINKNAME should not end with a directory delimiter.
Returns nil if LINKNAME is not a string.
Returns expanded LINKNAME if it is not a symbolic link but is a pathname."
  (if (stringp linkname)
      (progn
	(let ((referent))
	  (while (setq referent (file-symlink-p linkname))
	    (setq linkname (hpath:symlink-expand
			    referent (file-name-directory linkname)))))
	(hpath:symlink-expand linkname (file-name-directory linkname)))))

(defun hpath:symlink-expand (referent dirname)
  "Returns expanded file or directory REFERENT relative to DIRNAME."
  (let ((var-link)
	(dir dirname))
    (while (string-match "\\$(\\([^\)]*\\))" referent)
      (setq var-link (getenv (substring referent (match-beginning 1)
					(match-end 1)))
	    referent (concat (substring referent 0 (match-beginning 0))
			     var-link
			     (substring referent (match-end 0)))))
    ;; If referent is not an absolute path
    (let ((nd-abbrev (string-match "`node_data" referent)))
      (if (and nd-abbrev (= nd-abbrev 0))
	  (setq referent (concat
			   ;; Prepend node name given in dirname, if any
			   (and (string-match "^//[^/]+" dirname)
				(substring dirname 0 (match-end 0)))
			   "/sys/" (substring referent 1)))))
    (while (string-match "\\.\\." referent)
      ;; Match to "//.." or "/.." at the start of link referent
      (while (string-match "^\\(//\\.\\.\\|/\\.\\.\\)\\(/\\|$\\)" referent)
	(setq referent (substring referent (match-end 1))))
      ;; Match to "../" or ".." at the start of link referent
      (while (string-match "^\\.\\.\\(/\\|$\\)" referent)
	(setq dir (file-name-directory (directory-file-name dir))
	      referent (concat dir (substring referent (match-end 0)))))
      ;; Match to rest of "../" in link referent
      (while (string-match "[^/]+/\\.\\./" referent)
	(setq referent (concat (substring referent 0 (match-beginning 0))
			       (substring referent (match-end 0))))))
    (and (/= (aref referent 0) ?~)
	 (/= (aref referent 0) ?/)
	 (setq referent (expand-file-name referent dirname))))
  referent)

(defun hpath:validate (path)
  "Returns PATH if PATH is a valid, readable path, else signals error.
Default-directory should be equal to current Hyperbole button source
directory when called so that PATH is expanded relative to it." 
  (if (and (stringp path) (file-readable-p path))
      path
    (error "(hpath:validate): \"%s\" is not readable." path)))

;;; ************************************************************************
;;; Private functions
;;; ************************************************************************

(defun hpath:find-program (filename)
  (let ((regexp-alist hpath:find-alist)
	(prog)
	elt)
    (while (and (not prog) regexp-alist)
      (if (string-match (car (setq elt (car regexp-alist))) filename)
	  (setq prog (cdr elt)))
      (setq regexp-alist (cdr regexp-alist)))
    prog))

(defun hpath:substitute-dir (var-name rest-of-path)
  "Returns a dir for VAR-NAME using REST-OF-PATH to find match or triggers an error when no match.
VAR-NAME's value may be a directory or a list of directories.  If it is a
list, the first directory prepended to REST-OF-PATH which produces a valid
local pathname is returned."
  (let (sym val)
    (cond ((not (stringp var-name))
	   (error "(hpath:substitute-dir): VAR-NAME arg, '%s', must be a string" var-name))
	  ((not (and (setq sym (intern-soft var-name))
		     (boundp sym)))
	   (error "(hpath:substitute-dir): VAR-NAME arg, \"%s\", is not a bound variable"
		  var-name))
	  ((stringp (setq val (symbol-value sym)))
	   (if (hpath:validate (expand-file-name rest-of-path val))
	       val))
	  ((listp val)
	   (let ((dir))
	     (while (and val (not dir))
	       (setq dir (car val) val (cdr val))
	       (or (and (file-name-absolute-p dir)
			(file-readable-p (expand-file-name rest-of-path dir)))
		   (setq dir nil)))
	     (if dir (hpath:validate (directory-file-name dir))
	       (error "(hpath:substitute-dir): Can't find match for \"%s\""
		      (concat "${" var-name "}/" rest-of-path))
	       )))
	  (t (error "(hpath:substitute-dir): Value of VAR-NAME, \"%s\", must be a string or list" var-name))
	  )))

(defun hpath:substitute-var-name (var-symbol var-dir-val path)
  "Replaces with VAR-SYMBOL any occurrences of VAR-DIR-VAL in PATH.
If PATH is modified, returns PATH, otherwise returns nil."
  (let ((new-path (hypb:replace-match-string
		    (regexp-quote (file-name-as-directory var-dir-val))
		    path (concat "${" (symbol-name var-symbol) "}/")
		    t)))
    (if (equal new-path path) nil new-path)))

(provide 'hpath)
