;; Copyright (c) 2009-2011 Alex Shinn.  All rights reserved.
;; BSD-style license: http://synthcode.com/license.txt

;;> A general, non-host-specific pathname library.

(define (string-scan c str . o)
  (let ((limit (string-length str)))
    (let lp ((i (if (pair? o) (car o) 0)))
      (cond ((>= i limit) #f)
            ((eqv? c (string-ref str i)) i)
            (else (lp (+ i 1)))))))

(define (string-scan-right c str . o)
  (let lp ((i (if (pair? o) (car o) (- (string-length str) 1))))
    (cond ((negative? i) #f)
          ((eqv? c (string-ref str i)) i)
          (else (lp (- i 1))))))

(define (string-skip c str . o)
  (let ((limit (string-length str)))
    (let lp ((i (if (pair? o) (car o) 0)))
      (cond ((>= i limit) #f)
            ((not (eqv? c (string-ref str i))) i)
            (else (lp (+ i 1)))))))

(define (string-skip-right c str . o)
  (let lp ((i (if (pair? o) (car o) (- (string-length str) 1))))
    (cond ((negative? i) #f)
          ((not (eqv? c (string-ref str i))) i)
          (else (lp (- i 1))))))

;; POSIX basename
;; (define (path-strip-directory path)
;;   (if (string=? path "")
;;       path
;;       (let ((end (string-skip-right #\/ path)))
;;         (if (not end)
;;             "/"
;;             (let ((start (string-scan-right #\/ path (- end 1))))
;;               (substring path (if start (+ start 1) 0) (+ end 1)))))))

;;> Returns just the basename of @var{path}, with any directory
;;> removed.  If @var{path} does not contain a directory separator,
;;> return the whole @var{path}.  If @var{path} ends in a directory
;;> separator (i.e. path is a directory) return the empty string.

;; GNU basename
(define (path-strip-directory path)
  (if (string=? path "")
      path
      (let ((len (string-length path)))
        (if (eqv? #\/ (string-ref path (- len 1)))
            ""
            (let ((slash (string-scan-right #\/ path)))
              (if (not slash)
                  path
                  (substring path (+ slash 1) len)))))))

;;> Returns just the directory of @var{path}.
;;> If @var{path} is relative, return @scheme{"."}.

(define (path-directory path)
  (if (string=? path "")
      "."
      (let ((end (string-skip-right #\/ path)))
        (if (not end)
            "/"
            (let ((start (string-scan-right #\/ path (- end 1))))
              (if (not start)
                  "."
                  (let ((start (string-skip-right #\/ path start)))
                    (if (not start) "/" (substring path 0 (+ start 1))))))))))

(define (path-extension-pos path) (string-scan-right #\. path))

;;> Returns the rightmost extension of @var{path}, not including
;;> the @scheme{"."}.  If there is no extension, returns @scheme{#f}.

(define (path-extension path)
  (let ((i (path-extension-pos path)))
    (and i
         (let ((start (+ i 1)) (end (string-length path)))
           (and (< start end) (substring path start end))))))

;;> Returns @var{path} with the extension, if any, removed,
;;> along with the @scheme{"."}.

(define (path-strip-extension path)
  (let ((i (path-extension-pos path)))
    (if (and i (< (+ i 1) (string-length path)))
        (substring path 0 i)
        path)))

;;> Returns @var{path} with the extension, if any, replaced
;;> with @var{ext}.

(define (path-replace-extension path ext)
  (string-append (path-strip-extension path) "." ext))

;;> Returns @scheme{#t} iff @var{path} is an absolute path.

(define (path-absolute? path)
  (and (not (string=? "" path)) (eqv? #\/ (string-ref path 0))))

;;> Returns @scheme{#t} iff @var{path} is a relative path.

(define (path-relative? path) (not (path-absolute? path)))

;; This looks big and hairy, but it's mutation-free and guarantees:
;;   (string=? s (path-normalize s))  <=>  (eq? s (path-normalize s))
;; i.e. fast and simple for already normalized paths.

;;> Returns a normalized version of path, with duplicate directory
;;> separators removed and "/./" and "x/../" references removed.
;;> Does not take symbolic links into account - this is meant to
;;> be abstract and applicable to paths on remote systems and in
;;> URIs.  Returns @var{path} itself if @var{path} is already
;;> normalized.

(define (path-normalize path)
  (let* ((len (string-length path)) (len-1 (- len 1)))
    (define (collect i j res)
      (if (>= i j) res (cons (substring path i j) res)))
    (define (finish i res)
      (if (zero? i)
          path
          (apply string-append (reverse (collect i len res)))))
    ;; loop invariants:
    ;;   - res is a list such that (string-concatenate-reverse res)
    ;;     is always the normalized string up to j
    ;;   - the tail of the string from j onward can be concatenated to
    ;;     the above value to get a partially normalized path referring
    ;;     to the same location as the original path
    (define (inside i j res)
      (if (>= j len)
          (finish i res)
          (if (eqv? #\/ (string-ref path j))
              (boundary i (+ j 1) res)
              (inside i (+ j 1) res))))
    (define (boundary i j res)
      (if (>= j len-1)
          (finish i res)
          (case (string-ref path j)
            ((#\.)
             (case (string-ref path (+ j 1))
               ((#\.)
                (if (or (>= j (- len 2)) (eqv? #\/ (string-ref path (+ j 2))))
                    (if (>= i (- j 1))
                        (if (null? res)
                            (backup j "" '())
                            (backup j (car res) (cdr res)))
                        (backup j (substring path i j) res))
                    (inside i (+ j 2) res)))
               ((#\/)
                (if (= i j)
                    (boundary (+ j 2) (+ j 2) res)
                    (let ((s (substring path i j)))
                      (boundary (+ j 2) (+ j 2) (cons s res)))))
               (else (inside i (+ j 1) res))))
            ((#\/) (boundary (+ j 1) (+ j 1) (collect i j res)))
            (else (inside i (+ j 1) res)))))
    (define (backup j s res)
      (let ((pos (+ j 3)))
        (cond
         ;; case 1: we're reduced to accumulating parents of the cwd
         ((or (string=? s "/..") (string=? s ".."))
          (boundary pos pos (cons "/.." (cons s res))))
         ;; case 2: the string isn't a component itself, skip it
         ((or (string=? s "") (string=? s ".") (string=? s "/"))
          (if (pair? res)
              (backup j (car res) (cdr res))
              (boundary pos pos (if (string=? s "/") '("/") '("..")))))
         ;; case3: just take the directory of the string
         (else
          (let ((d (path-directory s)))
            (cond
             ((string=? d "/")
              (boundary pos pos (if (null? res) '("/") res)))
             ((string=? d ".")
              (boundary pos pos res))
             (else (boundary pos pos (cons "/" (cons d res))))))))))
    ;; start with boundary if abs path, otherwise inside
    (if (zero? len)
        path
        ((if (eqv? #\/ (string-ref path 0)) boundary inside) 0 1 '()))))

;;> Return a new string representing the path where each of @var{args}
;;> is a path component, separated with the directory separator.

(define (make-path . args)
  (define (x->string x)
    (cond ((string? x) x)
          ((symbol? x) (symbol->string x))
          ((number? x) (number->string x))
          (else (error "not a valid path component" x))))
  (define (trim-trailing-slash s)
    (let ((i (string-skip-right #\/ s)))
      (if i (substring s 0 (+ i 1)) "")))
  (if (null? args)
      ""
      (let ((start (trim-trailing-slash (x->string (car args)))))
        (let lp ((ls (cdr args))
                 (res (if (string=? "" start) '() (list start))))
          (cond
           ((null? ls)
            (apply string-append (reverse res)))
           ((pair? (car ls))
            (lp (append (car ls) (cdr ls)) res))
           (else
            (let ((x (trim-trailing-slash (x->string (car ls)))))
              (lp (cdr ls)
                  (if (string=? x "") res (cons x (cons "/" res)))))))))))