#!/usr/bin/guile \
--debug -e %main -s
!#

;; utils

(use-modules ((srfi srfi-19)
              #:renamer (symbol-prefix-proc 'srfi-19:))
             ((srfi srfi-1)
              #:select (fold))
             ((srfi srfi-13)
              #:select (string-join)))

(define (ensure-fdes port mode)
  (or (false-if-exception (fileno port))
      (open-fdes *null-device* mode)))

(define (move-fdes from to)
  (cond ((not (= from to))
         (dup2 from to)
         (close-fdes from))))

(define (port-eos? port)
  (and (char-ready? port)
       (eof-object? (peek-char port))))

(define (chars-to-port fromport toport)
  (if (and (char-ready? fromport)
           (not (port-eos? fromport)))
      (begin
        (write-char (read-char fromport) toport)
        (chars-to-port fromport toport))))

(define (%spawnlp filename . args)
  (let ((outpipe (pipe))
        (errpipe (pipe)))
    (let ((pid (primitive-fork)))
      (if (= pid 0)
          (let ((in-fdes (ensure-fdes (current-input-port) O_RDONLY))
                (out-fdes (fileno (cdr outpipe)))
                (err-fdes (fileno (cdr errpipe))))

            (port-for-each (lambda (pt-entry)
                             (false-if-exception
                              (let ((pt-fileno (fileno pt-entry)))
                                (if (not (or (= pt-fileno in-fdes)
                                             (= pt-fileno out-fdes)
                                             (= pt-fileno err-fdes)))
                                    (close-fdes pt-fileno))))))

            (move-fdes in-fdes 0)
            (move-fdes out-fdes 1)
            (move-fdes err-fdes 2)
            (apply execlp filename filename args))
          (let* ((ports (list (cons (car outpipe) (current-output-port))
                              (cons (car errpipe) (current-error-port))))
                 (select (lambda ()
                           (select (map car ports) '() (map car ports)))))
            
            (for-each close-port (map cdr (list outpipe errpipe)))
            (let lp ((sret (select)))
              (for-each
               (lambda (port)
                 (cond ((assq-ref ports port)
                        => (lambda (to) (chars-to-port port to)))
                       (else
                        (throw 'cant-happen))))
               (car sret))
              (cond ((and (null? (caddr sret))
                          (not (fold (lambda (x knil)
                                       (and knil (port-eos? x)))
                                     #t (car sret))))
                     (lp (select)))
                    (else
                     (let ((waitval (waitpid pid)))
                       (for-each close-port (map car ports))
                       waitval)))))))))

(define (spawnlp filename . args)
  (let ((exit-val (status:exit-val
                   (cdr (apply %spawnlp filename args)))))
    (eq? exit-val 0)))

(define (null-output-port)
  (open-output-file *null-device*))

(define (with-output-to-null proc)
  (with-output-to-port (null-output-port) proc))

(define (with-error-to-null proc)
  (with-error-to-port (null-output-port) proc))

(define (with-output-to-error proc)
  (with-output-to-port (current-error-port) proc))

(define (with-output-and-error-to-port port proc)
  (with-output-to-port port (lambda () (with-error-to-port port proc))))

(define (with-output-and-error-to-null proc)
  (with-output-and-error-to-port (null-output-port) proc))

(define (string->date str)
  (let ((str* (with-output-to-string
                (lambda () (spawnlp "date" "-d" str "+%F %T")))))
    (srfi-19:string->date str* "~Y~m~d~H~M~S")))

(define (date->string date)
  (srfi-19:date->string date "~Y-~m-~d ~H:~M:~S"))

(define (make-command-line . args)
  (string-join (map (lambda (x) (string-append "\"" x "\"")) args) " "))

(define (debug . args)
  (for-each display args)
  (newline))

(define (date-difference d1 d2)
  (srfi-19:time-difference (srfi-19:date->time-monotonic d1)
                           (srfi-19:date->time-monotonic d2)))
  
(define (date-add-seconds d seconds)
  (srfi-19:time-utc->date
   (srfi-19:add-duration
    (srfi-19:date->time-utc d) 
    (srfi-19:make-time srfi-19:time-duration 0 seconds))))

(define (date<=? d1 d2)
  (srfi-19:time<=? (srfi-19:date->time-monotonic d1)
                   (srfi-19:date->time-monotonic d2)))
  
(define (date-difference-sufficiently-small? stop start)
  (srfi-19:time<=? (date-difference stop start)
                   (srfi-19:make-time srfi-19:time-duration 0 3600)))

(define (date-mean start stop)
  (let ((diff (date-difference stop start)))
    (date-add-seconds
     start (inexact->exact (floor (/ (srfi-19:time-second diff) 2))))))

;; code

(define (cvs . args)
  (or (apply spawnlp "cvs" args)
      (throw 'cvs-error args)))

(define (with-nonfatal-cvs-errors proc)
  (catch 'cvs-error proc (lambda args #f)))

(define (set-cvs-date! date)
  (debug "* Updating tree to " (date->string date) "...")
  (with-output-and-error-to-null
   (lambda ()
     (cvs "update" "-d" "-P" "-D" (date->string date)))))

(define (in-cvs-tree?)
  (with-output-and-error-to-null 
   (lambda ()
     (with-nonfatal-cvs-errors
      (lambda () (cvs "status"))))))

(define (assert-in-cvs-tree!)
  (debug "* Making sure we're in a CVS tree...")
  (if (not (in-cvs-tree?))
      (with-output-to-error
       (lambda ()
         (display "Error: ")
         (display (car (program-arguments)))
         (display " needs to be run from within a CVS checkout.\n")
         (exit 1)))))

(define (run-check command args)
  (debug "* Running check")
  (let ((ret (apply spawnlp command args)))
    (debug "* Check " (if ret "passed" "failed"))
    ret))

(define (run-check-for-date date command args)
  (set-cvs-date! date)
  (run-check command args))

(define (assert-check-run-on-date! date command args expected)
  (debug "* Checking that the check " (if expected "succeeds" "fails")
         " on " (date->string date) "...")
  (let* ((port (open-output-string))
         (res (with-output-and-error-to-port
               (or port (current-output-port))
               (lambda () (run-check-for-date date command args)))))
    (cond ((not (eq? res expected))
           (debug "* Expected a " (if expected "passing" "failing")
                  " result on " (date->string date) ",")
           (debug "  but command unexpectedly "
                  (if expected "failed" "passed") ".")
           (debug)
           (debug "Command: " (apply make-command-line command args))
           (debug)
           (debug "Failed output:")
           (debug)
           (debug (get-output-string port))
           (exit 1)))))
 
(define (run-bisection start-date stop-date command args)
  (cond
   ((date-difference-sufficiently-small? stop-date start-date)
    (debug "* Bisection stopped.")
    (debug)
    (debug "Command:")
    (debug "  " (apply make-command-line command args))
    (debug "started to fail somewhere between " (date->string start-date)
           " and " (date->string stop-date) ".")
    (exit 0))
   (else
    (let ((mean (date-mean start-date stop-date)))
      (if (run-check-for-date mean command args)
          (run-bisection mean stop-date command args)
          (run-bisection start-date mean command args))))))

(define (main start-date-str stop-date-str command . args)
  (let ((start-date (string->date start-date-str))
        (stop-date (string->date stop-date-str)))
    (catch 'cvs-error
           (lambda ()
             (assert-in-cvs-tree!)
             (assert-check-run-on-date! start-date command args #t)
             (assert-check-run-on-date! stop-date command args #f)
             (run-bisection start-date stop-date command args))
           (lambda (key args)
             (format (current-error-port)
                     "Error runnng ~A with arguments ~S, exiting.\n"
                     "cvs" args)
             (exit 1)))))

;; invocation

(define (usage)
  (format #t "usage: ~a START-DATE STOP-DATE COMMAND ARGS...\n"
          (car (command-line))))

(define (%main args)
  (if (not (>= (length args) 4))
      (begin
        (with-output-to-error usage)
        (exit 1)))
  (apply main (cdr args)))
