;;; -*- Mode:LISP; Package:USER; Readtable:CL; Base:10; Fonts:(MEDFNT) -*-

;;; Intelligent database interface utilities, for ObjectLISP objects.
;;; Really 3 separate concepts that are useful in combination:

;;; [1] Handy interface for generating object-contained edit (choose
;;; variable values) windows; see DEFCLASSMENU.  [Also illustrates one
;;; way to create a CHOOSE-VARIABLE-VALUES variable type that displays a
;;; message on the menu that can't be overwritten by the user; see
;;; PASS-BACK-ERROR-MSG].

;;; [2] Interface for constructing validation routines; see VALID-P.

;;; [3] Combined MAKE-EDIT-VALIDATE function for "enhancing" an object
;;; with a new object class, editing, and validating it.

(export '(defclassmenu
	   menu-edit
	   menu-error-msg
	   valid-p
	   validate
	   make-edit-validate))

;;; -*- [1] -*-
;;;
;;; Menu-editing objects.  A DEFKIND that include the class
;;; MENU-EDITABLE will inherit menu creation methods.  Call
;;; DEFCLASSMENU to set up the menu.  (See example on class
;;; SIMPLE-ANIMAL at bottom.) 
;;

(makunbound 'menu-editable)	 ;Kill the class.

(defkind menu-editable)		 ;Define the menu-editing object class.

(definstancevars menu-editable	 ;Things no menu should be without
  (menu-error-msg "OK")		 ;Current status msg
  (menu-title "Edit menu")	 ;Settable menu title
  (menu-alist			 ;Alist for TV:CHOOSE-VARIABLE-VALUES
    '((menu-error-msg
	"Status" :pass-back-error-msg)))
  (menu-edit-function		 ;Will be closure; this is safe.
    #'(lambda()nil))
  )

(defobfun (add-menu-alist menu-editable) ;This adds new menu elements
	  (alist)		 ;Takes a menu alist
  (loop for elt in alist
	with newalist = menu-alist
	as newvar = (if (listp elt)
			(car elt)
		      elt)
	do (setq newalist
		 (if (listp elt)
		     (remove newvar newalist :key #'car-safe)
		   (remove newvar newalist)))
	finally (have 'menu-alist
		      (append newalist alist)))
  menu-alist)

(defobfun (add-menu-choice menu-editable)
	  (choice-list)
  (format t "~&Adding menu choice ~s" choice-list)
  (setq menu-alist (append menu-alist (ncons choice-list)))
  (make-menu))

(defobfun (make-menu menu-editable) ())	 ;No-op here.

(defobfun (exist menu-editable)	 ;When creating new menu object,
	  (&rest ignore)
  (shadowed-exist)
  (make-menu))			 ;...make the menu

(defobfun (expand-alist menu-editable)	 ;This creates the edit-function
	  ()
  (let* ((vars (remove nil (mapcar #'car-safe menu-alist)))
	 (svars (mapcar #'string vars))
	 (lvars (mapcar #'make-symbol svars)))
	  (list 'let
		(mapcar #'(lambda(v i)
			    (list v
				  (list 'ask
					'(current-obj)
					i)))
			    lvars vars)
		`(declare(special ,@vars))
		`(declare(special ,@lvars))
		`(catch 'abort
		   (tv:choose-variable-values
		     ',(loop for old in vars
			     for new in lvars
			     with l = menu-alist
			     do (setf l (subst new old l))
			     finally (return l))
		     :margin-choices
		     '("Exit"
		       ("Abort" (throw 'abort (values nil :abort))))
		     :near-mode '(:point 400 400)
		     :label (ask (current-obj) menu-title))
		   ,@(mapcar #'(lambda (var val)
				 (list 'ask '(current-obj)
				       (list 'have
					     (list 'quote var)
					     val)))
			     vars lvars)
		   t)		 ;catching 'abort
		)))

(defmacro defclassmenu(class title alist)
  "Does all the work of defining MENU-EDITABLE obfun's for class CLASS
(which really should inherit from the MAKE-EDITABLE class).

 -  TITLE is the initial menu heading title.
 -  ALIST is the (unquoted) variables/options list, suitable for
      passing to TV:CHOOSE-VARIABLE-VALUES.

An example is:

  (defkind myclass)
  (definstancevars myvar anothervar)
  (DEFCLASSMENU myclass \"My menu\"
     ((myvar \"My var\" :number)
      (anothervar \"Another variable\" :string)))
  "
    (once-only(class)
    `(progn
       'compile
	 (defobfun (make-menu ,class) () ;Build the menu
	   (shadowed-make-menu)
	   (have 'menu-title ,title)
	   (add-menu-alist ',alist)
	   (have 'menu-edit-function
		 (eval `(lambda() ,(expand-alist)))))
	 (defobfun (menu-edit ,class) () ;User interface
	   (funcall menu-edit-function))
	 )))

;;; The data is already set up, but we need the obfun's:

(defclassmenu menu-editable "Edit menu"
  ((menu-error-msg "Status" :pass-back-error-msg)
   "------"))

;;; Utility routines for LMI window system

;;; Set-up a special keyword for TV:CHOOSE-VARIABLE-VALUES menus.

;; If the TYPE on a variable list is :PASS-BACK-ERROR-MSG, then that
;; variable (the current object's instance variable MENU-ERROR-MSG) will be
;; (re)displayed, when selected, with the value of the current object's
;; MENU-ERROR-MSG. 

;; This just means that the status line for the error message
;; can be dealt with dynamically, and won't be user-overwritable.

;; Usage: in the menu alist in a call to TV:CHOOSE-VARIABLE-VALUES,
;;      '(...(menu-error-msg "Status" :pass-back-error-msg)...)

;; This defines the function to be used when a menu (re)displays the
;; MENU-ERROR-MSG:

(defun tv:pass-back-error-msg(&rest ignore)
"CHOOSE-VARIABLE-VALUES type for handling an object variable MENU-ERROR-MSG."
  (declare(special menu-error-msg))
  (ask (current-obj) menu-error-msg))

;; This tells the window s/w to handle the keyword :PASS-BACK-ERROR-MSG

(defprop :pass-back-error-msg
  	 (princ					  ;Print w/out quotes
	   tv:pass-back-error-msg		  ;See above
	   nil nil nil				  ;Crufty TV stuff
	   "Status message")			  ;Who-line doc msg
  tv:choose-variable-values-keyword)

;;; -*- [2] -*-
;;;
;;; Self-validating objects.
;;;
;;; All About object validation:

;;; A class obfun VALID-P as used here is expected to return two values:
;;; VALIDITY and MENU-ERROR-MSG. VALIDITY should be non-NIL for an
;;; object that is "valid" -- e.g., its instance vars don't violate the
;;; internal "rules" for that object class, etc.  If VALIDITY is NIL,
;;; MENU-ERROR-MSG should be a string indicating the reason for the
;;; validity exception. (MENU-ERROR-MSG should also be a variable
;;; binding for the object being validated, as with a
;;; MENU-EDITABLE.)

;;; The obfun VALIDATE as used here calls VALID-P and if the result is
;;; non-NIL, proceeds to call a routine that allows the user to correct
;;; the error condition.

;;; For the sake of completeness, we define validity functions for the
;;; "global object" (what is not prohibited, is permitted):

(defobfun valid-p (&rest ignore) t)

(defobfun validate (&rest ignore)
  (multiple-value-bind (validity msg) (valid-p)
    (or validity
      (format t
	      "~&>> Invalid object signaled by (VALIDATE): ~s~%"
	      msg))))

;;; -*- [3] -*-
;;;
;;; How to turn an object of one class into one that takes on the
;;; bindings, etc., of another class... then edit its (presumably
;;; enlarged) menu, and validate it.

(defmacro transform-thing(obj class)
  `(let((newclass (oneof ,class)))
     (ask newclass (exist))
     (make-obj ,obj newclass)))

(defmacro make-edit-validate (obj class
			      &optional
			      (edit-fun (intern 'menu-edit))
			      (validate-fun (intern 'validate)))
  "Returns object from OBJ \"transformed\" into another CLASS;
preserves existing bindings, etc., and edits, and validates it.
EDIT-FUN can be a specified object function binding that will let 'edit'
the object, VALIDATE-FUN one that validates it; EDIT-FUN default to the
name created by DEFCLASSMENU, and VALIDATE-FUN defaults to VALIDATE."
  `(progn 'compile
	  (let((newobj (transform-thing ,obj ,class)))
	    (and
	      (ask newobj (make-menu))
	      (ask newobj (,edit-fun))
	      (ask newobj (,validate-fun))
	      newobj))))

;;; Testing and example code:

(defkind unknown-thing menu-editable)
(defkind typed-thing unknown-thing)
(defkind animal-thing typed-thing)
(defkind mineral-thing typed-thing)
(defkind vegetable-thing typed-thing)
(defkind small-thing unknown-thing)
(defkind big-thing unknown-thing)
(defkind known-thing)

(defobfun (exist unknown-thing) (&rest ignore)
  (shadowed-exist))

(definstancevars unknown-thing
  (type 'animal))

(definstancevars known-thing guess)

(definstancevars typed-thing
  bigthing)

(defclassmenu unknown-thing "Tell me about the thing"
  ((type "Is it animal, mineral, or vegetable?"
	 :choose (animal vegetable mineral))))

(defclassmenu typed-thing "Tell me more..."
  ((type "It's")
   ""
   (bigthing "Is it bigger than a bread-box?" :boolean)))

(defclassmenu known-thing "My guess is it's one of..."
  ((guess :sexp)))

(defun what-is-it?()
  (let((thing (oneof unknown-thing)))
    (ask thing (menu-edit))
    (setq thing (make-edit-validate thing typed-thing))
    (setq thing
	  (if (ask thing bigthing)
	      (transform-thing thing big-thing)
	    (transform-thing thing small-thing)))
    (case (ask thing type)
      (animal (setq thing (transform-thing thing animal-thing)))
      (mineral (setq thing (transform-thing thing mineral-thing)))
      (vegetable (setq thing (transform-thing thing vegetable-thing))))
    (let*((possibles '((animal (elephant giraffe bear)
			      (mouse chipmunk frog))
		      (mineral (mountain sea-shore menhir)
			       (diamond pebble grain-of-sand))
		      (vegetable (tree corn-stalk greensward)
				 (flower carrot blade-of-grass))))
	 (choices (assoc (ask thing type) possibles))
	 (mychoice (list 'guess :choose
			 (if (ask thing bigthing)
		       (car choices)
		     (cadr choices)))))
      (declare(special mychoice))
      (setq thing (transform-thing thing known-thing))
      (ask thing (show))
      (ask thing (have 'guess (cddr mychoice)))
      (ask thing (add-menu-choice mychoice))
      (ask thing (menu-edit)))
    thing
    ))
       