
(module process mzscheme
  (provide process
	   process*
	   process/ports
	   process*/ports
	   system
	   system*
	   system/exit-code
	   system*/exit-code)

  (require mzlib/port)

  ;; Helpers: ----------------------------------------

  (define (shell-path/args who argstr)
    (case (system-type)
      ((unix macosx) (append '("/bin/sh" "-c") (list argstr)))
      ((windows) (let ([cmd
			(let ([d (find-system-path 'sys-dir)])
			  (let ([cmd (build-path d "cmd.exe")])
			    (if (file-exists? cmd)
				cmd
				(let ([cmd (build-path d "command.com")])
				  (if (file-exists? cmd)
				      cmd
				      ;; One last try: up a dir
				      (build-path d 'up "command.com"))))))])
		   (list cmd
			 'exact
			 (format "~a /c \"~a\"" (path->string cmd) argstr))))
      (else (raise-mismatch-error 
	     who
	     (format "~a: don't know what shell to use for platform: " who)
	     (system-type)))))

  (define (if-stream-out p)
    (if (or (not p) (file-stream-port? p))
	p
	(if (output-port? p)
	    #f
	    (raise-type-error
	     'subprocess
	     "output port"
	     p))))

  (define (if-stream-in p)
    (if (or (not p) (file-stream-port? p))
	p
	(if (input-port? p)
	    #f
	    (raise-type-error
	     'subprocess
	     "input port"
	     p))))

  (define (streamify-in cin in get-thread? ready-for-break)
    (if (and cin (not (file-stream-port? cin)))
	(let ([t (thread (lambda () 
			   (dynamic-wind
			       void
			       (lambda ()
				 (with-handlers ([exn:break? void])
				   (ready-for-break #t)
                                   (copy-port cin in)
				   (ready-for-break #f)))
			       (lambda () (close-output-port in)))
			   (ready-for-break #t)))])
	  (and get-thread? t))
	in))

  (define (streamify-out cout out get-thread?)
    (if (and cout (not (file-stream-port? cout)))
	(let ([t (thread (lambda () 
                           (dynamic-wind
                               void
                               (lambda () (copy-port out cout))
                               (lambda () (close-input-port out)))))])
	  (and get-thread? t))
	out))

  ;; Old-style functions: ----------------------------------------

  (define (process*/ports cout cin cerr exe . args)
    (let-values ([(subp out in err) (apply subprocess 
					   (if-stream-out cout)
					   (if-stream-in cin)
					   (if-stream-out cerr)
					   exe args)]
		 [(it-ready) (make-semaphore)])
      (let ([so (streamify-out cout out #t)]
	    [si (streamify-in cin in #t (lambda (ok?)
					  (if ok?
					      (semaphore-post it-ready)
					      (semaphore-wait it-ready))))]
	    [se (streamify-out cerr err #t)]
	    [aport (lambda (x)
		     (and (port? x) x))])
	(when (thread? si)
	  ;; Wait for process to end, then stop copying input:
	  (thread (lambda ()
		    (sync subp si)
		    (semaphore-wait it-ready)
		    (break-thread si))))
	(let ([threads-still-going?
	       (lambda ()
		 (ormap (lambda (s)
			  (and (thread? s)
			       (thread-running? s)))
			(list so si se)))])
	  (list (aport so)
		(aport si)
		(subprocess-pid subp)
		(aport se)
		(letrec ((control
			  (lambda (m)
			    (case m
			      ((status) (let ((s (subprocess-status subp)))
					  (cond ((or (not (integer? s))
						     (threads-still-going?))
						 'running)
						((zero? s) 'done-ok)
						(else 'done-error))))
			      ((exit-code) (if (threads-still-going?)
					       #f
					       (let ((s (subprocess-status subp)))
						 (and (integer? s) s))))
			      ((wait) 
			       (subprocess-wait subp)
			       (let ([twait (lambda (t)
					      (when (thread? t)
						(thread-wait t)))])
				 (twait so)
				 (twait si)
				 (twait se)))
			      ((interrupt) (subprocess-kill subp #f))
			      ((kill) (subprocess-kill subp #t))
			      (else
			       (raise-type-error 'control-process
						 "'status, 'exit-code, 'wait, 'interrupt, or 'kill" m))))))
		  control))))))

  (define (process/ports out in err str)
    (apply process*/ports out in err (shell-path/args "process/ports" str)))

  (define (process* exe . args)
    (apply process*/ports #f #f #f exe args))

  (define (process str)
    (apply process* (shell-path/args "process" str)))

  ;; Note: these always use current ports
  (define (system*/exit-code exe . args)
    (if (eq? (system-type) 'macos)
	(if (null? args)
          (raise-mismatch-error
           'system*/exit-code "command-line arguments not supported for MacOS" args)
	  (subprocess #f #f #f exe))
	(let ([cout (current-output-port)]
	      [cin (current-input-port)]
	      [cerr (current-error-port)]
	      [it-ready (make-semaphore)])
	  (let-values ([(subp out in err)
			(apply
			 subprocess
			 (if-stream-out cout)
			 (if-stream-in cin)
			 (if-stream-out cerr)
			 exe args)])
	    (let ([ot (streamify-out cout out #t)]
		  [it (streamify-in cin in #t (lambda (ok?)
						(if ok?
						    (semaphore-post it-ready)
						    (semaphore-wait it-ready))))]
		  [et (streamify-out cerr err #t)])
	      (subprocess-wait subp)
	      (when it
		;; stop piping output to subprocess
		(semaphore-wait it-ready)
		(break-thread it))
	      ;; wait for other pipes to run dry:
	      (when (thread? ot)
		(thread-wait ot))
	      (when (thread? et)
		(thread-wait et))
	      (when err
		(close-input-port err))
	      (when out
		(close-input-port out))
	      (when in
		(close-output-port in)))
	    (subprocess-status subp)))))

  (define (system* exe . args)
    (if (eq? (system-type) 'macos)
	(if (null? args)
          (raise-mismatch-error
           'system* "command-line arguments not supported for MacOS" args)
	  (subprocess #f #f #f exe))
	(zero? (apply system*/exit-code exe args))))

  (define (system str)
    (if (eq? (system-type) 'macos)
	(subprocess #f #f #f "by-id" str)
	(apply system* (shell-path/args "system" str))))

  (define (system/exit-code str)
    (if (eq? (system-type) 'macos)
	(subprocess #f #f #f "by-id" str)
	(apply system*/exit-code (shell-path/args "system" str)))))
