;;; custom.scm: Customization support
;;;
;;; Copyright (c) 2003-2008 uim Project http://code.google.com/p/uim/
;;;
;;; All rights reserved.
;;;
;;; Redistribution and use in source and binary forms, with or without
;;; modification, are permitted provided that the following conditions
;;; are met:
;;; 1. Redistributions of source code must retain the above copyright
;;;    notice, this list of conditions and the following disclaimer.
;;; 2. Redistributions in binary form must reproduce the above copyright
;;;    notice, this list of conditions and the following disclaimer in the
;;;    documentation and/or other materials provided with the distribution.
;;; 3. Neither the name of authors nor the names of its contributors
;;;    may be used to endorse or promote products derived from this software
;;;    without specific prior written permission.
;;;
;;; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND
;;; ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
;;; IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
;;; ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE
;;; FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
;;; DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
;;; OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
;;; HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
;;; LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
;;; OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
;;; SUCH DAMAGE.
;;;;

;; There are complex definitions to experiment the customization
;; mechanism. Will be simplified once the requirement is cleared up.
;; -- YamaKen

(require "i18n.scm")
(require "util.scm")
(require "key.scm")

;; config
(define key-list->gui-key-list 'key-list-export-as-basic)
(define gui-key-list->key-list 'key-list-import-as-basic)
;;(define key-list->gui-key-list 'key-list-export-as-traditional)
;;(define gui-key-list->key-list 'key-list-import-as-traditional)

;; public
(define custom-activity-hooks ())
(define custom-get-hooks ())
(define custom-set-hooks ())
(define custom-literalize-hooks ())
(define custom-update-hooks ())
(define custom-group-update-hooks ())
(define custom-group-list-update-hooks ())

;; private
(define custom-full-featured? #t)
(define custom-rec-alist ())
(define custom-group-rec-alist ())
(define custom-subgroup-alist ())

(define custom-validator-alist
  '((boolean      . custom-boolean?)
    (integer      . custom-integer?)
    (string       . custom-string?)
    (pathname     . custom-pathname?)
    (choice       . custom-valid-choice?)
    (ordered-list . custom-ordered-list?)
    (key          . custom-key?)))

(define anything?
  (lambda (x)
    #t))

(define custom-boolean?
  (lambda (x)
    #t))

(define custom-integer?
  (lambda (x min max)
    (and (integer? x)
	 (<= min x)
	 (<= x max))))

(define custom-string?
  (lambda (x regex)
    (string? x)))

(define custom-pathname?
  (lambda (str type)
    (and (string? str)
	 (symbol? type)
	 (memq type '(regular-file directory)))))

(define custom-valid-choice?
  (lambda arg
    (let ((sym (car arg))
	  (choice-rec-alist (cdr arg)))
      (and (symbol? sym)
	   (assq sym choice-rec-alist)
	   #t))))

(define custom-ordered-list?
  (lambda arg
    (let ((syms (car arg))
	  (choice-rec-alist (cdr arg)))
      (and (list? syms)
	   (every (lambda (sym)
		    (apply custom-valid-choice? (cons sym choice-rec-alist)))
		  syms)))))

(define custom-key?
  (lambda (key-repls)
    (and (list? key-repls)
	 (every (lambda (key)
		  (or (and (string? key)  ;; "<Control>a"
			   ;;(valid-strict-key-str? key)
			   (valid-key-str? key))  ;; acceps translators
		      (and (symbol? key)  ;; 'generic-cancel-key
			   (custom-exist? key 'key))))
		key-repls))))

(define custom-pathname-type
  (lambda (custom-sym)
    (car (custom-type-attrs custom-sym))))

(define custom-expand-key-references
  (lambda (key)
    (cond
     ((string? key)
      (list (key-str->gui-key-str key)))
     ((list? key)
      (append-map custom-expand-key-references key))
     ((and (symbol? key)
	   (custom-exist? key 'key))
      (custom-expand-key-references (custom-value key)))
     (else
      ()))))

;; TODO
(define custom-key-advanced-editor?
  (lambda (custom-sym)
    #f))

(define reversed-tag-prefix-alist
  (map (lambda (pair)
	 (cons (cdr pair)
	       (car pair)))
       tag-prefix-alist))

;; TODO: write test
;; (key-str->key-list "<Control><Shift><IgnoreRegularShift>return")
;;   -> (Control_key Shift_key IgnoreRegularShift "return")
;; (key-str->key-list "C-M-a")
;;   -> (Control_key Meta_key "a")
(define key-str->key-list
  (lambda (key-str)
    (unfold (compose not car parse-key-prefix)
	    (compose car parse-key-prefix)
	    (compose cdr parse-key-prefix)
	    key-str
	    (compose list cdr parse-key-prefix))))

;; TODO: write test
(define key-list->key-str
  (lambda (key-list)
    (string-append-map
     (lambda (elem)
       (if (symbol? elem)
	   (let ((mod (cdr (assq elem reversed-tag-prefix-alist))))
	     (string-append "<" mod ">"))
	   elem))
     key-list)))

;; TODO: write test
(define map-key-list-body
  (lambda (body-mapper key-list)
    (map (lambda (elem)
	   (if (string? elem)
	       (body-mapper elem)
	       elem))
	 key-list)))

;; TODO: write test
(define map-key-list-letter
  (lambda (letter-mapper key-list)
    (let ((letter (string->alphabetic-ichar (find string? key-list))))
      (map-key-list-body (lambda (elem)
			   (if letter
			       (charcode->string (letter-mapper letter))
			       elem))
			 key-list))))

;; TODO: write test
(define map-key-str
  (lambda (key-list-mapper key-str)
    (if (string? key-str)
	(let ((key-list (key-str->key-list key-str)))
	  (key-list->key-str (key-list-mapper key-list)))
	key-str)))

;; TODO: write test
(define key-list-upcase
  (lambda (key-list)
    (map-key-list-letter ichar-upcase key-list)))

;; TODO: write test
(define key-list-downcase
  (lambda (key-list)
    (map-key-list-letter ichar-downcase key-list)))

;; TODO: write test
(define key-list-visualize-space
  (lambda (key-list)
    (map-key-list-body (lambda (elem)
			 (if (string=? elem " ")
			     "space"
			     elem))
		      key-list)))

;; TODO: write test
(define key-list-characterize-space
  (lambda (key-list)
    (map-key-list-body (lambda (elem)
			 (if (string=? elem "space")
			     " "
			     elem))
		      key-list)))

;; TODO: write test
(define key-list-encode-shift
  (lambda (key-list)
    (let* ((has-shift? (memq 'Shift_key key-list))
	   (str (find string? key-list))
	   (printable (string->printable-ichar str))
	   (letter (string->alphabetic-ichar str)))
      (filter-map (lambda (elem)
		    (cond
		     ((and (eq? elem 'Shift_key)
			   (ichar-graphic? printable))
		      #f)
		     ((and (string? elem)
			   has-shift?
			   letter)
		      (charcode->string (ichar-upcase letter)))
		     ((and (string? elem)
			   has-shift?
			   (ichar-graphic? printable))
		      str)
		     (else
		      elem)))
		  key-list))))

;; TODO: write test
(define key-list-decode-shift
  (lambda (key-list)
    (let* ((letter (string->alphabetic-ichar (find string? key-list)))
	   (upper-case? (and letter
			     (ichar-upper-case? letter)))
	   (has-shift? (memq 'Shift_key key-list))
	   (stripped (key-list-downcase key-list)))
      (if (and (not has-shift?)
	       upper-case?)
	  (cons 'Shift_key stripped)
	  stripped))))

;; TODO: write test
(define key-list-ignore-regular-shift
  (lambda (key-list)
    (let ((printable (string->printable-ichar (find string? key-list))))
      (if (ichar-graphic? printable)
	  (cons 'IgnoreRegularShift key-list)
	  key-list))))

;; TODO: write test
(define key-list-ignore-letter-shift
  (lambda (key-list)
    (let ((letter (string->alphabetic-ichar (find string? key-list))))
      (if letter
	  (cons 'IgnoreShift key-list)
	  key-list))))

;; TODO: write test
(define key-list-ignore-punct-numeric-shift
  (lambda (key-list)
    (let* ((str (find string? key-list))
	   (c (string->printable-ichar str)))
      (if (and (ichar-graphic? c)
	       (not (ichar-alphabetic? c)))
	  (cons 'IgnoreShift key-list)
	  key-list))))

;; TODO: write test
(define key-list-ignore-case
  (lambda (key-list)
    (let ((letter (string->alphabetic-ichar (find string? key-list))))
      (if letter
	  (cons 'IgnoreCase key-list)
 	  key-list))))

;; TODO: write test
(define key-list-strip-shift
  (lambda (key-list)
    (delete 'Shift_key key-list eq?)))

;; TODO: write test
(define key-list-strip-regular-shift
  (lambda (key-list)
    (let* ((str (find string? key-list))
	   (printable (string->printable-ichar str)))
      (if (ichar-graphic? printable)
	  (key-list-strip-shift key-list)
	  key-list))))

;; TODO: write test
(define key-list-strip-translators
  (lambda (key-list)
    (remove translator-prefix? key-list)))

;; TODO: write test
(define key-list-export-as-basic (compose key-list-visualize-space
					  key-list-encode-shift
					  key-list-strip-translators))

;; TODO: write test
(define key-list-import-as-basic (compose key-list-characterize-space
					  key-list-ignore-punct-numeric-shift
					  key-list-ignore-case
					  key-list-decode-shift
					  key-list-strip-regular-shift))

;; TODO: write test
(define key-list-export-as-traditional (compose key-list-visualize-space
						key-list-strip-translators))

;; TODO: write test
(define key-list-import-as-traditional (compose key-list-characterize-space
						key-list-ignore-regular-shift))

;; TODO: write test
(define key-str->gui-key-str
  (lambda (key-str)
    (map-key-str (symbol-value key-list->gui-key-list)
		 key-str)))

;; TODO: write test
(define gui-key-str->key-str
  (lambda (key-str)
    (map-key-str (symbol-value gui-key-list->key-list)
		 key-str)))

(define custom-choice-label
  (lambda (custom-sym val-sym)
    (let* ((sym-rec-alist (custom-type-attrs custom-sym))
	   (srec (assq val-sym sym-rec-alist)))
      (if srec
	  (custom-choice-rec-label srec)
	  (symbol->string val-sym)))))

(define custom-choice-desc
  (lambda (custom-sym val-sym)
    (let* ((sym-rec-alist (custom-type-attrs custom-sym))
	   (srec (assq val-sym sym-rec-alist)))
      (if srec
	  (custom-choice-rec-desc srec)
	  (symbol->string val-sym)))))

(define custom-choice-range-reflect-olist-val
  (lambda (dst-sym src-sym indication-alist)
    (custom-set-type-info!
     dst-sym
     (cons (custom-type dst-sym)
	   (action-id-list->choice (custom-value src-sym)
				   indication-alist)))))

(define-record 'custom-group-rec
  '((sym   #f)
    (label "")
    (desc  "")))

(define define-custom-group
  (lambda (gsym label desc)
    (let ((grec (custom-group-rec-new gsym label desc)))
      (if (not (custom-group-rec gsym))
	  (begin
	    (set! custom-group-rec-alist (cons grec custom-group-rec-alist))
	    (custom-call-hook-procs 'global
				    custom-group-list-update-hooks))))))

(define custom-group-rec
  (lambda (gsym)
    (assq gsym custom-group-rec-alist)))

;; API
(define custom-group-label
  (lambda (gsym)
    (custom-group-rec-label (custom-group-rec gsym))))

;; API
(define custom-group-desc
  (lambda (gsym)
    (custom-group-rec-desc (custom-group-rec gsym))))

;; API
(define custom-group-subgroups
  (lambda (gsym)
    (let ((groups (filter-map (lambda (pair)
				(let ((primary-grp (car pair))
				      (subgrp (cdr pair)))
				  (and (eq? gsym primary-grp)
				       subgrp)))
			      custom-subgroup-alist)))
      (reverse groups))))

;; API
(define custom-list-groups
  (lambda ()
    (let ((groups (map custom-group-rec-sym custom-group-rec-alist)))
      (reverse groups))))

;; API
(define custom-list-primary-groups
  (lambda ()
    (let ((groups (filter-map
		   (lambda (grec)
		     (let ((grp (custom-group-rec-sym grec)))
		       (and (assq grp custom-subgroup-alist)
			    grp)))
		   custom-group-rec-alist)))
      (reverse groups))))

;; TODO: rewrite test for 'AND' expression
;; API
;; #f means 'any group'
(define custom-collect-by-group
  (lambda groups
    (reverse
     (filter-map (lambda (crec)
		   (let ((custom-groups (custom-rec-groups crec)))
		     (and (or (not (car groups))
			      (every (lambda (group)
				       (memq group custom-groups))
				     groups))
			  (custom-rec-sym crec))))
		 custom-rec-alist))))

;; API
(define custom-add-hook
  (lambda (custom-sym hook-sym proc)
    (set-symbol-value! hook-sym (cons (cons custom-sym proc)
				      (symbol-value hook-sym)))))

;; #f for custom-sym means 'any entries'
(define custom-remove-hook
  (lambda (custom-sym hook-sym)
    (let ((removed (if custom-sym
		       (alist-delete custom-sym (symbol-value hook-sym) eq?)
		       ()))
	  (removed? (if custom-sym
			(assq custom-sym (symbol-value hook-sym))
			(not (null? (symbol-value hook-sym))))))
      (set-symbol-value! hook-sym removed)
      removed?)))

(define custom-hook-procs
  (lambda (sym hook)
    (let* ((filter (lambda (pair)
		     (let ((custom (car pair))
			   (proc (cdr pair)))
		       (and (eq? sym custom)
			    proc))))
	   (procs (filter-map filter
			      hook)))
      procs)))

(define custom-call-hook-procs
  (lambda (sym hook)
    (let ((procs (custom-hook-procs sym hook)))
      (map (lambda (proc)
	     (proc))
	   procs))))

(define-record 'custom-rec
  '((sym     #f)
    (default #f)
    (groups  ())
    (type    ())
    (label   "")
    (desc    "")))

(define custom-rec
  (lambda (sym)
    (assq sym custom-rec-alist)))

;; TODO: rewrite test for overwriting and 'main' subgroup
;; API
(define define-custom
  (lambda (sym default groups type label desc)
    (let* ((primary-grp (car groups))
	   (subgrps (if (null? (cdr groups))
			'(main)
			(cdr groups)))
	   (modified-groups (cons primary-grp subgrps))
	   (crec (custom-rec-new sym default modified-groups type label desc)))
      ;; See also require-custom for error handling TODO
      (for-each (lambda (gsym)
		  (or (custom-group-rec gsym)
		      (error (string-append "undefined group '"
					    (symbol->string gsym)
					    "' is referred by "
					    (symbol->string sym)))))
		modified-groups)
      (set! custom-rec-alist (alist-replace crec custom-rec-alist))
      (custom-call-hook-procs primary-grp custom-group-update-hooks)
      (if (not (symbol-bound? sym))
	  (let ((quoted-default (if (or (symbol? default)
					(list? default))
				    (list 'quote default)
				    default)))
	    (eval (list 'define sym quoted-default)
		  (interaction-environment))
	    (custom-set-value! sym default)))  ;; to apply hooks
      (for-each (lambda (subgrp)
		  (let ((registered (custom-group-subgroups primary-grp)))
		    (if (not (memq subgrp registered))
			(set! custom-subgroup-alist
			      (cons (cons primary-grp subgrp)
				    custom-subgroup-alist)))))
		subgrps))))

;; #f as type means 'any type'
(define custom-exist?
  (lambda (sym type)
    (and (assq sym custom-rec-alist)
	 (or (not type)
	     (eq? type
		  (custom-type sym))))))

;; API
(define custom-valid?
  (lambda (sym val)
    (let* ((type-name (custom-type sym))
	   (type-attrs (custom-type-attrs sym))
	   (valid? (symbol-value (cdr (assq type-name
					    custom-validator-alist)))))
      (apply valid? (cons val type-attrs)))))

;; API
(define custom-value
  (lambda (sym)
    (custom-call-hook-procs sym custom-get-hooks)
    (let ((val (symbol-value sym)))
      (if (custom-valid? sym val)
	  val
	  (custom-default-value sym)))))

;; TODO: rewrite test
;; API
(define custom-set-value!
  (lambda (sym val)
    (and (custom-valid? sym val)
	 (let* ((custom-syms (custom-collect-by-group #f))
		(map-activities (lambda ()
				  (map (lambda (pair)
					 ((cdr pair)))
				       custom-activity-hooks)))
		(pre-activities (map-activities)))
	   (set-symbol-value! sym val)
	   (if (eq? (custom-type sym)
		    'key)
	       (let ((key-val (custom-modify-key-predicate-names val)))
		 (eval (list 'define (symbol-append sym '?)
			     (list 'make-key-predicate (list 'quote key-val)))
		       (interaction-environment))))
	   (custom-call-hook-procs sym custom-set-hooks)
	   (custom-call-hook-procs sym custom-update-hooks)
	   (let ((post-activities (map-activities)))
	     (for-each (lambda (another-sym pre post)
			 (if (and (not (eq? another-sym sym))
				  (not (eq? (not pre)     ;; normalize bool
					    (not post)))) ;; normalize bool
			     (custom-call-hook-procs another-sym
						     custom-update-hooks)))
		       (map car custom-activity-hooks)
		       pre-activities
		       post-activities)
	     #t)))))

;; API
(define custom-touch-value!
  (lambda (sym)
    (custom-set-value! sym
		       (custom-value sym))))

(define custom-active?
  (lambda (sym)
    (let* ((procs (custom-hook-procs sym custom-activity-hooks))
	   (activities (map (lambda (proc)
			      (proc))
			    procs))
	   (active? (apply proc-and activities)))
      active?)))

;; API
(define custom-default?
  (lambda (sym)
    (equal? (symbol-value sym)
	    (custom-default-value sym))))

;; API
(define custom-default-value
  (lambda (sym)
    (custom-rec-default (custom-rec sym))))

;; API
(define custom-groups
  (lambda (sym)
    (custom-rec-groups (custom-rec sym))))

;; API
(define custom-type
  (lambda (sym)
    (car (custom-rec-type (custom-rec sym)))))

(define custom-type-attrs
  (lambda (sym)
    (let* ((crec (custom-rec sym))
	   (typedef (custom-rec-type crec)))
      (cdr typedef))))

;; TODO: write test
;; API for temporary solution
(define custom-type-info
  (lambda (sym)
    (custom-rec-type (custom-rec sym))))

;; TODO: write test
;; API for temporary solution
(define custom-set-type-info!
  (lambda (sym info)
    (custom-rec-set-type! (custom-rec sym)
			  info)
    (custom-call-hook-procs sym custom-update-hooks)))

;; API
(define custom-range
  (lambda (sym)
    (let* ((type (custom-type sym))
	   (attrs (custom-type-attrs sym)))
      (case type
	((choice ordered-list)
	 (map custom-choice-rec-sym attrs))
	((integer string)
	 attrs)
	(else
	 ())))))

;; API
(define custom-label
  (lambda (sym)
    (custom-rec-label (custom-rec sym))))

;; API
(define custom-desc
  (lambda (sym)
    (custom-rec-desc (custom-rec sym))))

(define custom-list-as-literal
  (lambda (lst)
    (let ((canonicalized (map (lambda (elem)
				(cond
				 ((symbol? elem)
				  (symbol->string elem))
				 ((string? elem)
				  (string-escape elem))
				 (else
				  "")))
			      lst)))
      (string-append "'(" (string-join canonicalized " ") ")"))))

;; API
(define custom-value-as-literal
  (lambda (sym)
    (let ((val (custom-value sym))
	  (type (custom-type sym)))
      (cond
       ((eq? type 'integer)
	(number->string val))
       ((eq? type 'string)
	(string-escape val))
       ((eq? type 'pathname)
	(string-escape val))
       ((eq? type 'choice)
	(string-append "'" (symbol->string val)))
       ((or (eq? type 'ordered-list)
	    (eq? type 'key))
	(custom-list-as-literal val))
       ((or (eq? val #f)
	    (eq? type 'boolean))
	(if (eq? val #f)
	    "#f"
	    "#t"))))))

;; Don't invoke this from a literalize-hook. It will cause infinite loop
(define custom-definition-as-literal
  (lambda (sym)
    (let ((var (symbol->string sym))
	  (val (custom-value-as-literal sym))
	  (hooked (custom-call-hook-procs sym custom-literalize-hooks)))
      (if (not (null? hooked))
	  (string-join hooked "\n")
	  (apply string-append
		 (append
		  (list "(define " var " " val ")")
		  (if (eq? (custom-type sym)
			   'key)
		      (let ((key-val (custom-list-as-literal
				      (custom-modify-key-predicate-names
				       (custom-value sym)))))
			(list "\n(define " var "? "
			      "(make-key-predicate " key-val "))"))
		      ())))))))

;; API
;; TODO: implement after uim 0.4.6 depending on scm-nested-eval
(define custom-broadcast-custom
  (lambda (sym)
  #f))

;; API
;; #f means 'any group'
;; TODO: support "AND" expression
(define custom-broadcast-customs
  (lambda (group)
    (let ((custom-syms (custom-collect-by-group group)))
      (for-each custom-broadcast-custom custom-syms))))

(define custom-register-cb
  (lambda (hook valid? custom-sym ptr gate-func func)
    (and (valid? custom-sym)
	 (let ((cb (lambda () (gate-func func ptr custom-sym))))
	   (custom-add-hook custom-sym hook cb)))))

;;
;; predefined subgroups
;;

(define-custom-group 'main
		     (N_ "-")
		     (N_ "Main settings of this group"))

(define-custom-group 'hidden
		     (N_ "Hidden settings")
		     (N_ "Hidden settings of this group. This group is invisible from uim_custom clients. Exists for internal variable management."))


(prealloc-heaps-for-heavy-job)
(custom-reload-customs)
