
(module utilr mzscheme
  (require mzlib/unit
	   mzlib/class
	   (lib "mred-sig.ss" "mred")
	   net/qp-sig
	   net/base64-sig
	   (prefix unihead: net/unihead)
	   mzlib/etc
	   mzlib/string)

  (require "sirmails.ss")

  (provide util@)
  (define-unit util@
      (import mred^
	      base64^
	      qp^)
      (export sirmail:utils^)

      ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
      ;;  Utilities                                              ;;
      ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

      (define crlf (string #\return #\linefeed))

      (define (split s re)
	(regexp-split re s))

      (define (drop-last-if-empty orig-l)
	(let loop ([l orig-l][accum null])
	  (cond
	   [(null? l) orig-l]
	   [(null? (cdr l))
	    (if (equal? #"" (car l))
		(reverse accum)
		orig-l)]
	   [else (loop (cdr l) (cons (car l) accum))])))

      (define (splice l sep)
	(if (null? l)
	    #""
	    (let ([p (open-output-bytes)])
	      (let loop ([l l])
		(write-bytes (car l) p)
		(unless (null? (cdr l))
		  (display sep p)
		  (loop (cdr l))))
	      (get-output-bytes p))))
      
      (define (split-crlf/preserve-last s)
	(split s #rx#"\r\n"))

      (define (split-crlf s)
	(drop-last-if-empty (split-crlf/preserve-last s)))

      (define (split-lf s)
	(drop-last-if-empty (split s #rx#"\n")))

      (define (crlf->lf s)
	(splice (split-crlf s) #"\n"))

      (define (crlf->lf/preserve-last s)
	(splice (split-crlf/preserve-last s) #"\n"))

      (define (lf->crlf s)
	(splice (split-lf s) #"\r\n"))

      (define (string-crlf->lf s)
	(regexp-replace* #rx"\r\n" s "\n"))

      (define (string-lf->crlf s)
	(regexp-replace* #rx"\n" s "\r\n"))

      (define (header->lines s)
	(regexp-split #rx"\r\n" 
		      ;; We don't want the extra empty line at the end:
		      (substring s 0 (- (string-length s) 2))))

      (define (enumerate n)
	(let loop ([n n][a null])
	  (if (zero? n)
	      a
	      (loop (sub1 n) (cons n a)))))

      (define (find i l)
	(let loop ([l l][pos 0])
	  (if (null? l)
	      #f
	      (if (eq? (car l) i)
		  pos
		  (loop (cdr l) (add1 pos))))))

      (define (string->regexp s)
	(regexp-quote s))

      ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

      (define (show-error-message-box x main-frame)
	(let ([sp (open-output-string)])
	  ;; use error display handler in case
	  ;; errortrace (or something else) is
	  ;; installed
	  (parameterize ([current-output-port sp]
			 [current-error-port sp])
	    ((error-display-handler)
	     (if (exn? x)
		 (exn-message x)
		 (format "uncaught exn: ~s" x))
	     x))
	  (message-box "Error" 
		       (get-output-string sp)
		       main-frame
		       '(ok stop))))

      ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

      (define (break-really-hard? set-d!)
	(let* ([d (make-object dialog% "Danger")]
	       [p (make-object vertical-pane% d)]
	       [bp (make-object horizontal-pane% d)]
	       [result #f])
	  (send bp stretchable-width #f)
	  (send bp stretchable-height #f)
	  (make-object message% "Breaking now is dangerous." p)
	  (make-object message% "It requires killing the window." p)
	  (make-object message% "" p)
	  (make-object message% "Are you sure you want to kill?" p)
	  (make-object button% "&Kill" bp (lambda (b e)
					    (set! result #t)
					    (send d show #f)))
	  (make-object button% "Cancel" bp (lambda (b e) (send d show #f)))
	  (set-d! d)
	  (send d show #t)
	  result))

      ;; as-background: (bool window<%> braek-thunk ->)
      ;;                (break-ok-thunk break-not-ok-thunk -> X) 
      ;;                (-> Y) -> X
      ;; Runs a task in the background.
      ;; The `enable' function is called with #f as the first argument
      ;;  near the start of the background task. The `break-thunk' can be
      ;;  called to interrupt the task. The `enable' function might
      ;;  not get called at all if the task is fast enough.
      ;; The `go' function performs the task (in a thread created by
      ;;   as-background); it receives thunks that it can call to
      ;;   indicate when breaks are "safe". If the user tries to break
      ;;   at a non-safe point, the user is warned; if the user
      ;;   proceeds, things are killed and `exit' is called. If
      ;;   the user breaks at a safe point, a break signal is sent
      ;;   to the thread for the background task.
      ;; The `pre-kill' thunk is called before things are killed
      ;;   for a non-"safe" break.
      (define (as-background enable go pre-kill)
	(let* ([v #f]
	       [exn #f]
	       [break-ok? #f]
	       [breaking-dialog #f]
	       [adjust-break (make-semaphore 1)]
	       [change-break-ok (lambda (ok?)
				  (lambda ()
				    (semaphore-wait adjust-break)
				    (set! break-ok? ok?)
				    (let ([waiting? (and ok? breaking-dialog)])
				      (when waiting?
					(send breaking-dialog show #f)
					(set! breaking-dialog #f))
				      (semaphore-post adjust-break)
				      (when waiting?
					(break-thread (current-thread))))))]
	       [s (make-semaphore 0)]
	       [t (thread (lambda ()
			    (with-handlers ([void (lambda (x) 
						    (set! exn x))])
			      (set! v (call-with-values 
					  (lambda () (go (change-break-ok #f)
							 (change-break-ok #t)))
					list))
			      ((change-break-ok #f)))
			    (when breaking-dialog
			      (send breaking-dialog show #f))
			    (semaphore-post s)))])
	  ;; If the operation is fast enough, no need to disable then yield then enable,
	  ;; which makes the screen flash and causes events to get dropped. 1/4 second
	  ;; seems "fast enough".
	  (unless (sync/timeout 0.25 s)
	    (let ([v (enable #f #f
			     (lambda ()
			       (semaphore-wait adjust-break)
			       (if break-ok?
				   (break-thread t)
				   (let ([v (break-really-hard? (lambda (d) 
								  (set! breaking-dialog d)
								  (semaphore-post adjust-break)))])
				     (semaphore-wait adjust-break)
				     (set! breaking-dialog #f)
				     (semaphore-post adjust-break)
				     (when v
				       (pre-kill)
				       (kill-thread t)
				       (exit))))))])
	      (yield s)
	      (enable #t v void)))
	  (if exn
	      (raise exn)
	      (apply values v))))

      ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

      ;; the actual fixed width font is already set by
      ;; the editor:standard-style-list-mixin
      (define (make-fixed-width c e wrap? wrap-bm)
	(let ([s (send (send e get-style-list)
		       find-named-style "Standard")])
	  (send e set-tabs null 8 #f)
	  (let ([font (send s get-font)]
		[dc (send c get-dc)]
		[wbox (box 0)]
		[hbox (box 0)])
	    (send e get-view-size wbox hbox)
	    (let-values ([(w h) (send c get-size)]
			 [(1w 1h d a) (send dc get-text-extent "X" font)])
	      (let ([80chars (+ (* 1w 80)
				2 ; +2 for caret
				(if wrap-bm 
				    (send wrap-bm get-width) 
				    0))])
		(when wrap? 
		  (when wrap-bm 
		    (send e set-autowrap-bitmap wrap-bm))
		  (send e set-max-width 80chars))
		(send c min-width 
		      (inexact->exact (round (+ 80chars (- w (unbox wbox)))))))))))

      ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

      (define confirm-box 
	(opt-lambda (title message [parent #f] [style null])
	  (if (= 1 (message-box/custom
		    title
		    message
		    "&Yes"
		    "&No"
		    #f
		    parent
		    (append (if (memq 'app style) null '(caution)) 
			    '(default=1))
		    2))
	      'yes
	      'no)))

      ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

      (define (get-pw-from-user username parent)
	(get-text-from-user "Password" 
			    (format "Password for ~a:" username)
			    parent
			    ""
			    '(password)))

      ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
      ;;  Decoding `from' names                                  ;;
      ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

      (define generalize-encoding unihead:generalize-encoding)
      (define parse-encoded unihead:decode-for-header)
      (define encode-for-header unihead:encode-for-header)))
