(module pref mzscheme

  (require mzlib/class
	   framework
	   mred
	   mzlib/list
	   mzlib/string
	   mzlib/etc
	   net/head)

  ;; IMPORTANT! All preferences operations outside this
  ;; file should go through the following exports.
  ;; DO NOT use preferences:... elsewhere.
  (provide get-pref put-pref
	   show-pref-dialog
	   add-preferences-menu-items)

  (define (string-or-false? x) (or (not x) (string? x)))
  (define (ip-string? x) (and (string? x)
			      (positive? (string-length x))))
  (define (abs-path-or-false? x) 
    (or (not x)
	(and (path? x) (absolute-path? x))))

  (define (un/marshall-path pref)
    (preferences:set-un/marshall pref
				 (lambda (x) 
                                   (if (path? x)
                                       (path->bytes x)
                                       x))
				 (lambda (x) 
                                   (cond
                                     [(bytes? x) (bytes->path x) ]
                                     [(not x) x]
                                     [else 'badvalue]))))
  
  ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  ;;  Preference Definitions                                 ;;
  ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


  (preferences:set-default 'sirmail:mail-from "SirMail User <sirmail@plt-scheme.org>" string?)
  (preferences:set-default 'sirmail:username "username" string?)
  (preferences:set-default 'sirmail:password #f string-or-false?)
  (preferences:set-default 'sirmail:default-to-domain "plt-scheme.org" ip-string?)

  (preferences:set-default 'sirmail:imap-server "imap.plt-scheme.org" ip-string?)
  (preferences:set-default 'sirmail:use-ssl? #f boolean?)
  (preferences:set-default 'sirmail:server-certificate #f abs-path-or-false?)
  (preferences:set-default 'sirmail:smtp-server "sendmail.plt-scheme.org" ip-string?)

  (preferences:set-default 'sirmail:local-directory 
			   (build-path (find-system-path 'home-dir)
				       "SirMail")
			   (lambda (x)
			     (and (path? x)
				  (absolute-path? x))))
  (un/marshall-path 'sirmail:local-directory)
  (preferences:set-default 'sirmail:sent-directory 
			   (build-path (find-system-path 'home-dir)
				       "SentMail")
			   (lambda (x)
			     (or (not x)
				 (and (path? x)
				      (absolute-path? x)))))
  (un/marshall-path 'sirmail:sent-directory)
  (preferences:set-default 'sirmail:root-mailbox-folder #f string-or-false?)

  (preferences:set-default 'sirmail:archive-mailbox-folder #f string-or-false?)
  
  (preferences:set-default 'sirmail:initial-sort 'id
			   (lambda (x) (memq x '(id date subject from))))
  (preferences:set-default 'sirmail:biff-delay
			   60
			   (lambda (x)
			     (or (not x)
				 (and (number? x)
				      (exact? x)
				      (integer? x)
				      (positive? x)))))
  (preferences:set-default 'sirmail:warn-download-size 32000
			   (lambda (x) (or (not x) (and (number? x) (real? x)))))
  (preferences:set-default 'sirmail:external-composer 'xemacs
			   (lambda (x) (memq x '(xemacs gnu-emacs))))
  (preferences:set-default 'sirmail:use-extenal-composer? #f boolean?)
  (preferences:set-default 'sirmail:show-urls? #t boolean?)
  (preferences:set-default 'sirmail:show-gc-icon #f boolean?)
  (preferences:set-default 'sirmail:always-happy #f boolean?)
  (preferences:set-default 'sirmail:wrap-lines #f boolean?)
  (preferences:set-default 'sirmail:prefer-text #t boolean?)

  (preferences:set-default 'sirmail:aliases-file
                           (build-path (find-system-path 'home-dir) ".sirmail.aliases")
			   abs-path-or-false?)
  (un/marshall-path 'sirmail:aliases-file)
  (preferences:set-default 'sirmail:auto-file-table-file (build-path (find-system-path 'home-dir) ".sirmail.auto-file")
			   abs-path-or-false?)
  (un/marshall-path 'sirmail:auto-file-table-file)

  (preferences:set-default 'sirmail:self-addresses null
			   (lambda (x) (and (list? x) (andmap string? x))))
  (preferences:set-default 'sirmail:fields-to-show '("From" "To" "CC" "Subject" "Date" "X-Mailer" "X-Uptime")
			   (lambda (x) (and (list? x) (andmap string? x))))
  (preferences:set-default 'sirmail:bcc #f
                           (λ (x) (or (not x) (string? x))))

  (let ([fw 560]
	[fh 600])
    (let-values ([(display-width display-height) (get-display-size)])
      (preferences:set-default 'sirmail:frame-width
			       (min display-height fh)
			       (lambda (x) (and (number? x) (<= 0 x 32768))))
      (preferences:set-default 'sirmail:frame-height 
			       (min display-width fw)
			       (lambda (x) (and (number? x) (<= 0 x 32768))))))

  (define (xywh-okay? n)
    (and (number? n)
         (<= 0 n 10000)))
  (preferences:set-default 'sirmail:folder-window-w 200 xywh-okay?)
  (preferences:set-default 'sirmail:folder-window-h 400 xywh-okay?)
  (preferences:set-default 'sirmail:folder-window-x 0 xywh-okay?)
  (preferences:set-default 'sirmail:folder-window-y 0 xywh-okay?)

  ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  ;;  Preference Manager                                     ;;
  ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

  (define prefs-eventspace (make-eventspace))

  (define (in-preferences-eventspace thunk)
    (let ([val #f]
	  [s (make-semaphore)])
      (parameterize ([current-eventspace prefs-eventspace])
	(queue-callback
	 (lambda ()
	   (with-handlers ([void (lambda (x)
				   ;; Assume all raised values are exns
				   (set! val x))])
	     (set! val (thunk)))
	   (semaphore-post s))))
      (semaphore-wait s)
      (if (exn? val)
	  (raise val)
	  val)))

  (define (get-pref id)
    (in-preferences-eventspace (lambda ()
				 (preferences:get id))))

  (define (put-pref id val)
    (in-preferences-eventspace (lambda ()
				 (preferences:set id val))))

  (define (add-preferences-menu-items edit-menu)
    (make-object separator-menu-item% edit-menu)
    (make-object menu-item% "Preferences" edit-menu
		 (lambda (x y) (in-preferences-eventspace preferences:show-dialog))))
  
  (define (show-pref-dialog)
    (in-preferences-eventspace 
     (lambda ()
       (preferences:show-dialog)
       (yield 'wait))))


  ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  ;;  Preference Dialog                                      ;;
  ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

  (define needs-check null)

  
  (define (set-hilite e on?)
    (send e change-style 
	  (send (make-object style-delta%) set-delta-background (if on? "yellow" "white"))
	  0 (send e last-position)))

  ;; make-text-field : string panel number symbol boolean
  ;;                   ((union #f string) (union #f top-level-window<%>) string -> boolean)
  ;;                   (any -> string)
  ;;                   (string -> any)
  ;;                   [ (union #f string) ]
  ;;                -> void
  ;; sets up a text field for a preference
  ;; The 3rd-to-last argument checks the validity of the field content.If
  ;;  a string is provided, then a top-level-window<%> is also provded, and
  ;;  the checking function should tell the user why the field-value string
  ;;  is bad if it is bad.
  ;; the last two arguments convert between the string representation (as shown in the text field)
  ;; and the preferences's actual Scheme value.
  (define make-text-field
    (opt-lambda (label panel width-num pref optional? check-value val->str str->val [post-label #f])
      (define p0 (and (or optional? post-label)
		      (instantiate horizontal-panel% (panel) [stretchable-height #f])))
      (define e (and optional?
		     (make-object check-box% label p0
				  (lambda (c e)
				    (let ([on? (send c get-value)])
				      (send t enable on?)
				      (if on?
					  (t-cb t e)
					  (begin
					    ;; remove all need-check registrations, if any:
					    (let loop ()
					      (let ([a (assq t needs-check)])
						(when a
						  (set! needs-check (remq a needs-check))
						  (loop))))
					    (preferences:set pref #f))))))))
      (define t-cb (lambda (t e)
		     (let* ([s (send t get-value)])
		       (if (check-value #f #f s)
			   (preferences:set pref (str->val s))
			   (begin
			     (set! needs-check (cons (list t label check-value) needs-check))
			     (set-hilite (send t get-editor) #t))))))
      (define t (make-object text-field% 
			     (if optional? #f label) 
			     (or p0 panel)
			     t-cb
			     (make-string width-num #\X)))
      (when post-label
	(send t stretchable-width #f)
	(make-object message% post-label p0))

      (send t set-value (let ([v (preferences:get pref)])
			  (if v
			      (val->str v)
			      "")))
      (when optional?
	(send e set-value (preferences:get pref)))
      (when e
	(send t enable (send e get-value)))
      (preferences:add-callback pref (lambda (name val)
				       (set-hilite (send t get-editor) #f)
				       (when e
					 (send e set-value val)
					 (send t enable val))
				       (when val
					 (let ([sval (val->str val)])
					   (unless (equal? sval (send t get-value))
					     (send t set-value sval))))))

      (or p0 t)))

  (define (check-unsaved-pref?)
    (and (andmap (lambda (a)
		   ((caddr a) (cadr a) (send (car a) get-top-level-window) (send (car a) get-value)))
		 needs-check)
	 (begin
	   (set! needs-check null)
	   #t)))
	     
  (define make-file/directory-button
    (lambda (dir? button-label parent pref enabler)
      (define p0 (and enabler
		      (instantiate horizontal-panel% (parent) [stretchable-height #f])))
      (define e (and enabler
		     (make-object check-box% enabler p0
				  (lambda (c e)
				    (let ([on? (send c get-value)])
				      (send p enable on?)
				      (preferences:set
				       pref
				       (and on?
					    (string->path (send field get-value)))))))))
      (define p (instantiate horizontal-panel% ((or p0 parent))
			     [stretchable-height #f]))
      (define (set-it v)
	(preferences:set pref v))
      (define field (make-object text-field% button-label p
				 ;; For now, just counteract edits:
				 (lambda (t e)
				   (send field set-value (path->string (preferences:get pref))))
                      (path->string
                       (or (preferences:get pref)
                           (current-directory)))))
      (when e
	(send e set-value (preferences:get pref))
	(send p enable (send e get-value)))
      (preferences:add-callback pref (lambda (name val)
				       (when e
					 (send e set-value val)
					 (send p enable val))
				       (when val
					 (send field set-value (path->string val)))))
      (make-object button% "Set..." p (lambda (b e)
					(let ([v ((if dir? get-directory get-file)
						  (or enabler button-label))])
					  (when v
					    (set-it v)))))
      p0))

  (define make-boolean
    (opt-lambda (label p pref [extra-action void])
      (define c
	(make-object check-box% label p (lambda (c e)
					  (let ([v (send c get-value)])
					    (extra-action v)
					    (preferences:set pref v)))))
      (send c set-value (preferences:get pref))
      (preferences:add-callback pref (lambda (name val)
				       (send c set-value val)))))

  (define (is-host-address? s)
    (regexp-match "^([-a-zA-Z0-9]+[.])*[-a-zA-Z0-9]+$" s))

  (define (is-host-address+port? s)
    (or (is-host-address? s)
	(let ([m (regexp-match "^(.*):([0-9]+)$" s)])
	  (and m
	       (<= 1 (string->number (caddr m)) 65535)
	       (is-host-address? (cadr m))))))

  (define (is-host-address+port+user? s)
    (or (is-host-address+port? s)
	(let ([m (regexp-match "^(?:[-+a-zA-Z0-9_.]+)@(.*)$" s)])
	  (and m
	       (is-host-address+port? (cadr m))))))

  (define (is-host-address+port+user+type? s)
    (or (is-host-address+port+user? s)
	(let ([m (regexp-match "^(?:ssl|tcp):(.*)$" s)])
	  (and m
	       (is-host-address+port+user? (cadr m))))))

  (define (is-host-address+port+user+type-list? s)
    (let ([l (regexp-split ", *" s)])
      (andmap is-host-address+port+user+type? l)))

  (define (check-address ok? who tl s port-ok? multi?)
    (or (ok? s)
	(begin
	  (when who
	    (message-box
	     "Preference Error"
	     (format (string-append
		      "The ~a value must be a~a host IP address~a~a~a.\n"
		      "An IP address is an string containing a combination of "
		      "period (.), dash (-), A-Z, a-Z, and 0-9. "
		      "Also the period cannot appear at the very beginning or end.\n"
		      "~a"
		      "You provided\n\n  ~a\n\nwhich is not legal.")
		     who 
		     (if multi? " comma-separated list of" "")
		     (if multi? " es" "")
		     (if (and multi? port-ok?) " each" "")
		     (if port-ok? " with an optional port number" "")
		     (if port-ok? 
			 (string-append
			  "An optional port number is specified by adding a "
			  "colon (:) followed by a number between 1 and 65535.\n")
			 "")
		     s)
	     tl
	     '(ok stop)))
	  #f)))
			
  (define (check-host-address who tl s)
    (check-address is-host-address? who tl s #f #f))
  (define (check-host-address/port who tl s)
    (check-address is-host-address+port? who tl s #t #f))
  (define (check-host-address/port/user/type/multi who tl s)
    (check-address is-host-address+port+user+type-list? who tl s #t #t))

  ;; check-biff-delay : (union #f string) (union #f parent) string -> boolean
  ;; checks to see if the string in the biff delay field makes
  ;; sense as an exact integer between 1 and 3600
  (define (check-biff-delay who tl s)
    (let ([n (string->number s)])
      (or (and (number? n)
	       (integer? n)
	       (exact? n)
	       (<= 1 n 3600))
	  (begin
	    (when who
	      (message-box
	       "Preference Error"
	       (format (string-append
			"The biff delay must be an exact integer between 1 and 3600.~n"
			"You provided:~n"
			"  ~a")
		       s)
	       tl
	       '(ok stop)))
	    #f))))

  
  ;; check-message-size : (union #f string) (union #f parent) string -> boolean
  ;; checks to see if the string in the download-max-size field makes
  ;; sense as an exact positive integer
  (define (check-message-size who tl s)
    (let ([n (string->number s)])
      (or (and (number? n)
	       (integer? n)
	       (exact? n)
	       (positive? n))
	  (begin
	    (when who
	      (message-box
	       "Preference Error"
	       (format (string-append
			"The message size must be an exact, positive integer.~n"
			"You provided:~n"
			"  ~a")
		       s)
	       tl
	       '(ok stop)))
	    #f))))
  
  (define (check-user-address who tl s)
    (with-handlers ([exn:fail? 
		     (lambda (x)
		       (when who
			 (message-box
			  "Preference Error"
			  (format "The ~a value you provided is not a legal mail address: ~a"
				  who s)
			  tl
			  '(ok stop)))
		       #f)])
      (unless (= 1 (length (extract-addresses s 'all)))
	(error "multiple addresses"))
      #t))

  (define (check-simple-user-address who tl s)
    (and (check-user-address who tl s)
	 (car (extract-addresses s 'address))))

  (define (check-id who tl s) #t)
      
  (define (make-text-list label parent pref check-item)
    (let ([p (make-object group-box-panel% label parent)])
      (define l (make-object list-box% #f (or (preferences:get pref) null) p
			     (lambda (l e)
			       (send delete enable (pair? (send l get-selections))))
			     '(multiple)))
      (define hp (instantiate horizontal-panel% (p) 
			      [stretchable-height #f]
			      [alignment '(center center)]))
      (define add (make-object button% "Add" hp (lambda (b e)
						  (let loop ([init ""])
						    (let ([v (get-text-from-user (format "Add to ~a" label)
										 (format "Add to ~a" label)
										 (send parent get-top-level-window)
										 init)])
						      (when v
							(let ([revised (check-item (format "item for ~a" label)
										   (send b get-top-level-window) v)])
							  (if revised
							      (begin
								(send l append (if (string? revised) revised v))
								(set-prefs))
							      (loop v)))))))))
      (define delete (make-object button% "Delete" hp (lambda (b e)
							(let ([d (send l get-selections)])
							  (for-each (lambda (i)
								      (send l delete i))
								    (sort d >))
							  (set-prefs)))))
      (define (set-prefs)
	(send delete enable (pair? (send l get-selections)))
	(preferences:set
	 pref
	 (let ([n (send l get-number)])
	   (let loop ([i 0])
	     (if (= i n)
		 null
		 (cons (send l get-string i)
		       (loop (add1 i))))))))
      (send delete enable #f)
      (preferences:add-callback pref (lambda (name val)
				       (send l clear)
				       (for-each (lambda (i)
						   (send l append i))
						 val)
				       (send delete enable (pair? (send l get-selections)))))))

  (define (make-addresses-preferences-panel parent)
    (let ([p (instantiate vertical-panel% (parent))])
      
      (make-text-field "Mail From" p 20 'sirmail:mail-from #f check-user-address (lambda (x) x) (lambda (x) x))
      (make-text-field "SMTP Server" p 20 'sirmail:smtp-server #f check-host-address/port/user/type/multi 
		       (lambda (x) x) (lambda (x) x))

      (make-file/directory-button #t #f p
				  'sirmail:sent-directory
				  "Save Sent Files")


      (make-text-field "Default \"To\" domain" p 20 'sirmail:default-to-domain #f check-host-address (lambda (x) x) (lambda (x) x))
      (make-text-field "BCC line" p 20 'sirmail:bcc #t void (lambda (x) x) (lambda (x) x))
      (make-file/directory-button #f #f p
				  'sirmail:aliases-file
				  "Aliases File")

      (make-text-list "Self Addresses" p 'sirmail:self-addresses check-simple-user-address)

      (make-boolean "Enable compose-with-Emacs" p 'sirmail:use-extenal-composer?)

      p))

  (define (make-mbox-preferences-panel parent)
    (let ([p (instantiate vertical-panel% (parent)
               (alignment '(left center)))])
      
      (make-text-field "Username" p 10 'sirmail:username #f check-id (lambda (x) x) (lambda (x) x))
      (let ([sp (instantiate group-box-panel% ("IMAP Server" p)
			     [alignment '(left center)])]
	    [cert #f])
	(make-text-field "Server" sp 20 'sirmail:imap-server #f check-host-address/port (lambda (x) x) (lambda (x) x))
	(make-boolean "Encrypt connection using SSL" sp 'sirmail:use-ssl?
		      (lambda (on?) (send cert enable on?)))
	(set! cert (make-file/directory-button #f #f sp
					       'sirmail:server-certificate
					       "Verify SSL with certificates"))
        (make-text-field "Archive folder" sp 20 'sirmail:archive-mailbox-folder #t void (lambda (x) x) (lambda (x) x))
	(make-text-field "Folder list root" sp 20 'sirmail:root-mailbox-folder #t void (lambda (x) x) (lambda (x) x))

	(send cert enable (preferences:get 'sirmail:use-ssl?)))

      (make-file/directory-button #t "Local directory" p
				  'sirmail:local-directory
				  #f)

      (make-text-field "Check mail every" p 5 'sirmail:biff-delay #t check-biff-delay number->string string->number
		       "seconds")

      (make-text-field "Verify download of messages larger than" p 10
		       'sirmail:warn-download-size #t 
		       check-message-size number->string string->number
		       "bytes")

      (make-file/directory-button #f #f p
				  'sirmail:auto-file-table-file
				  "Auto-file table file")
      
      (make-boolean "Show GC icon" p 'sirmail:show-gc-icon)
      (make-boolean "Always happy to get mail" p 'sirmail:always-happy)
      
      (make-text-list "Shown Header Fields" p 'sirmail:fields-to-show void)

      p))

  (in-preferences-eventspace
   (lambda ()
     (preferences:add-panel "Reading" make-mbox-preferences-panel)
     (preferences:add-panel "Sending" make-addresses-preferences-panel)
     (preferences:add-editor-checkbox-panel)
     (preferences:add-can-close-dialog-callback check-unsaved-pref?))))

