;;
;; SCSH process form notation
;;
;; See http://www.scsh.net/docu/html/man-Z-H-3.html#node_chap_2
;;
;; Some minor changes due to Chicken- and R7RS-incompatible identifiers:
;; | was changed to pipe, |+ was changed to pipe+
;;
;; || wasn't changed, but it's really the zero-length symbol
;;
;; BIG FAT WARNING: Don't mix this with threading, or Bad Things will happen

(module scsh-process
  ((& fit-pipes) (run fit-pipes) (exec-epf fit-pipes)
   exec-path)

(import chicken scheme data-structures)

(use posix)

(define-syntax &
  (syntax-rules ()
    ((_ ?epf ...)
     (process-fork (lambda ()
                     (handle-exceptions exn
                       ;; TODO: Figure out how SCSH does this.  It shows the error
                       ;; on stderr in the REPL, but then still quits it.
                       ;; If we just invoke current-handler, it'll get a second REPL
                       (begin (print-error-message exn) (exit 1))
                       (exec-epf ?epf ...)))))))

(define-syntax run
  (syntax-rules ()
    ((_ ?epf ...)
     ;; We reorder the values as they make more sense this way for SCSH compat:
     ;; scsh returns just the exit code, and conveniently we allow MV in single
     ;; value continuations, which makes it compatible.
     (receive (pid normal-exit? exit-status)
       (process-wait (& ?epf ...))
       (values exit-status normal-exit? pid)))))

(define (maybe-symbol->string s)
  (if (symbol? s) (symbol->string s) s))

;; Perhaps this should really be a procedure?
(define-syntax setup-redirection
  (syntax-rules (< > << >> = - stdports)
    ((_ (< ?file-name)) (setup-redirection (< 0 ?file-name)))
    ((_ (> ?file-name)) (setup-redirection (> 1 ?file-name)))
    ((_ (<< ?object)) (setup-redirection (<< 0 ?object)))
    ((_ (>> ?object)) (setup-redirection (>> 1 ?object)))
    ((_ (< ?fd ?file-name))
     (duplicate-fileno (file-open (maybe-symbol->string `?file-name)
                                  open/rdonly)
                       `?fd))
    ((_ (> ?fd ?file-name))
     (duplicate-fileno (file-open (maybe-symbol->string `?file-name)
                                  (fx+ open/wronly open/creat))
                       `?fd))
    ((_ (<< ?fd ?object)) (error "<< currently not implemented"))
    ((_ (>> ?fd ?object)) (error ">> currently not implemented"))
    ((_ (= ?fd-from ?fd/port-to))
     (let* ((fd/port-to ?fd/port-to)    ; Evaluate once
            (fd-to (if (port? fd/port-to)
                       (port->fileno fd/port-to)
                       fd/port-to)))
       (duplicate-fileno ?fd-from fd-to)))
    ((_ (- ?fd/port))
     (let ((o `?fd/port))
       (cond
        ((fixnum? ?fd/port) (file-close o))
        ((output-port? ?fd/port) (close-output-port o))
        ((input-port? ?fd/port) (close-input-port o))
        (else (error "Can only close i/o-ports and file descriptor numbers" o)))))
    ((_ stdports)
     (begin (setup-redirection (= 0 (current-input-port)))
            (setup-redirection (= 1 (current-output-port)))
            (setup-redirection (= 2 (current-error-port)))))
    ((_ ?arg0 ...)
     (syntax-error "Invalid redirection pattern: " `?arg0 ...))))

(define (fit-pipes from-fds to-fds progs)
  (define (make-pipes) (map (lambda _ (receive (create-pipe))) to-fds))
  (when (null? progs) (error "Can't fit a pipeline between zero programs"))
  (let ((initial-pairs (make-pipes)))
    ;; Close sending ends of these pipes; they're unused.
    (for-each (lambda (p) (file-close (cadr p))) initial-pairs)
    (let lp ((input-pairs initial-pairs)
             (progs progs))
      (if (null? (cdr progs))
          (begin
            ;; Fit the final input pipes to their respective fds.
            (for-each (lambda (to-fd p)
                        (duplicate-fileno (car p) to-fd)
                        (file-close (car p)))
                      to-fds input-pairs)
            ((car progs)))
          ;; Subprocess output goes into fds in output-pairs.  Their matching
          ;; input fds are used as input for the next process in the pipeline.
          (let ((output-pairs (make-pipes)))
            (process-fork (lambda ()
                            ;; Close receiving end in child and set up linkage
                            ;; from the output descriptors to the created pipes.
                            (for-each (lambda (p from-fds-for-this-p)
                                        (file-close (car p))
                                        (for-each (lambda (from-fd)
                                                    (duplicate-fileno (cadr p) from-fd))
                                                  from-fds-for-this-p))
                                      output-pairs from-fds)
                            ;; Set up input descriptors
                            (for-each (lambda (to-fd p)
                                        (duplicate-fileno (car p) to-fd)
                                        (file-close (car p)))
                                      to-fds input-pairs)
                            ((car progs))))
            ;; Close sending ends of the output pairs in the parent.
            (for-each (lambda (op) (file-close (cadr op))) output-pairs)
            (lp output-pairs (cdr progs)))))))

;; The most "core" syntax form
(define-syntax exec-epf
  ;; The nested let-syntaxes exist to let us pre-empt the fallthrough
  ;; whenever we see one of the recognised special rules so we don't end up
  ;; with the generic one if we happen to make a small mistake
  (syntax-rules (pipe pipe+ begin epf)
    ((_ (pipe ?pf0 ?pf1 ...))
     (exec-epf (pipe+ ((1 2 0)) ?pf0 ?pf1 ...)))
    ((_ (pipe+ ?args ...))
     (let-syntax
         ((exec-pipe+
           (syntax-rules ___ ()
             ((_ ((?from0 ?from1 ___ ?to) ___) (?prog0 ?arg0 ___) ___)
              (fit-pipes `((?from0 ?from1 ___) ___)
                         `(?to ___)
                         (list (lambda () (exec-path `?prog0 `?arg0 ___)) ___))))))
       (exec-pipe+ ?args ...)))
    ((_ (begin ?expr0 ?expr1 ...))
     (begin (setup-redirection (= 0 (current-input-port)))
            (setup-redirection (= 1 (current-output-port)))
            (setup-redirection (= 2 (current-error-port)))
            ?expr0 ?expr1 ...))
    ;; epf can be used if you happen to have a program called
    ;; "epf", "begin", "pipe", etc which you'd like to run.
    ((_ (epf ?args ...))
     (let-syntax ((exec-epf
                   (syntax-rules ___ ()
                     ((_ (?prog ?arg0 ___) ?redir0 ___)
                      (begin
                        (setup-redirection ?redir0) ___
                        (exec-path `?prog `?arg0 ___))))))
       (exec-epf ?args ...)))
    ;; This is purely for convenience, so you don't need the (epf ...) wrapper
    ((_ (?prog ?arg0 ...) ?redir0 ...)
     (exec-epf (epf (?prog ?arg0 ...) ?redir0 ...)))))

;; TODO: Perhaps expose environment, and mess around with the path so that
;; execve can be used in a sensible way?  Scsh has its own PATH, so we could
;; use something similar to that, but it's more work.
(define (exec-path prog . args)
  (process-execute (maybe-symbol->string prog) (map maybe-symbol->string args)))
)