;-*- Mode:LISP; Package:SYSTEM-INTERNALS; Readtable:ZL; Base:10; Lowercase:T -*-

;;; Implements the mechanism by which advised functions operate.

(deff (:special-form advise-prog) #'(:special-form prog))
(deff (:special-form advise-setq) #'(:special-form setq))
(deff (:special-form advise-progn) #'(:special-form progn))
(deff (:special-form advise-multiple-value-list) #'(:special-form multiple-value-list))
(deff advise-return-list #'return-list)
(deff advise-apply #'apply)
(deff (:special-form advise-let) #'(:special-form let))
(deff advise-list* #'list*)

;;; NOTE!!  Each of the above must have an optimizer in QCOPT, to compile properly.

(defmacro advised-function (before after around inner-function-expression)
  "Expands into the code that executes advice in the proper order."
  (let ((default-cons-area background-cons-area))
    `(advise-prog (values)
		  (declare (special values))
	          (advise-setq values
			       (advise-multiple-value-list
				 (advise-progn
				   ,@before
				   ,(advise-merge-arounds around inner-function-expression))))
		  ,@after
		  (advise-return-list values))))

;;; Take the list of around advise and merge it together
;;; Producing a form which evaluates them around the body.

(defun advise-merge-arounds (advice-list inner-function-expression)
  (if (null advice-list)
      `(advise-apply ,inner-function-expression arglist)
    (cl:subst (advise-merge-arounds (cdr advice-list) inner-function-expression) ':do-it
	      (car advice-list) :test #'eq)))

(defun (:property advise encapsulation-grind-function) (function def width real-io untyo-p)
  (when def					; Print the advice as calls to advise.
    (if (typep def 'compiled-function)
	(setq def (cadr (assq 'interpreted-definition (debugging-info def)))))
    (let ((body (encapsulation-body def)))
      (when (eq (car (car body)) 'si:displaced)
	(setf (car body) (cadr (car body))))
      (grind-print-advice-slot (cadr (car body)) ':before
			       function width real-io untyo-p)
      (grind-print-advice-slot (caddr (car body)) ':after
			       function width real-io untyo-p)
      (grind-print-advice-slot (cadddr (car body)) ':around
			       function width real-io untyo-p))))

(defun grind-print-advice-slot (slot-contents slot-name function width real-io untyo-p)
  (do ((l slot-contents (cdr l)) (i 0 (1+ i)))  ((null l))
    (grind-top-level `(advise ,function ,slot-name ,(cadr (cadar l)) ,i . ,(cddar l))
		     width real-io untyo-p)))

(defvar *advised-functions* nil
  "List of all function specs that have been advised.")

;;; Make a specifed function into an advised function
;;; (with no advice, as yet) if it isn't one already.
;;; Undisplace the advised-function macro if it has displaced itself.
(defun advise-init (function-spec)
  (let ((default-cons-area background-cons-area)
	(spec1 (unencapsulate-function-spec function-spec 'advise)))
    (cond ((neq spec1 (unencapsulate-function-spec spec1 '(advise)))
	   (uncompile spec1 t)
	   (let ((body (encapsulation-body (fdefinition spec1))))
	     ;; (car body) looks like:
	     ;;        (advised-function nil nil nil encapsulated-function)
	     (when (eq (car (car body)) 'si:displaced)
	       (setf (car body) (cadr (car body))))))
	  (t
	   (push function-spec *advised-functions*)
	   (encapsulate spec1 function-spec 'advise
			`(advised-function nil nil nil ,encapsulated-function))))))

(defmacro advise (&optional function-spec class name position &body forms)
  "Put advice on FUNCTION-SPEC to perform FORMS.
CLASS is :BEFORE, :AFTER or :AROUND.
NAME is the name for this piece of advice;
any existing piece with the same name and class will be replaced.
POSITION says where to put this advice wrt others of same class;
it is a number, or the name of some other piece of advice to go after,
or NIL meaning put this one first.
If given no arguments, ADVISE returns a list of functions which are presently advised."
  (if (null function-spec)
      '*advised-functions*
      `(advise-1 ',function-spec ',class ',name ',position ',forms)))

(defun advise-1 (function-spec class name position forms)
  (setq function-spec (dwimify-arg-package function-spec 'function))
  (advise-init function-spec)
  (setq forms (rename-within-new-definition-maybe function-spec forms))
  (advise-update-list (advise-find-slot (unencapsulate-function-spec function-spec 'advise)
					class)
		      name position forms)
  (if compile-encapsulations-flag
      (compile-encapsulations function-spec 'advise))
  function-spec)

(defun advise-find-slot (function-spec class &aux body)
  (uncompile function-spec t)
  (setq body (encapsulation-body (fdefinition function-spec)))
  (nthcdr (ecase class
	    (:before 1)
	    (:after 2)
	    (:around 3))
	  (car body)))

(defun advise-update-list (slot-location name position forms)
  (let* ((default-cons-area background-cons-area)
	 preceding (new-unit `(progn ',name . ,forms)))
    (cond ((numberp position)
	   (or (setq preceding (nthcdr position (locf (car slot-location))))
	       (progn (setq preceding (locf (car slot-location)))
		      (do () ((null (cdr preceding)))
			(pop preceding)))))
	  ((and (null name) (null position)))
	  ((or (symbolp position) (null position))
	   (setq preceding (mem #'(lambda (p elt) (eq p (cadadr elt)))
				(or position name) (locf (car slot-location))))))
    ;; If the symbol isn't found, or no position is specified,
    ;; insert new advice at the beginning.
    (or preceding (setq preceding (locf (car slot-location))))
    (push new-unit (cdr preceding))
    ;; If we have a name, delete any old advice with that name.
    (and name
	 (do ((l (locf (car slot-location)) (cdr l))) ((null l))
	   (and (eq (cadadr (cadr l)) name)
		(neq (cadr l) new-unit)
		(return (rplacd l (cddr l))))))
    nil))

(defmacro unadvise (&optional function-spec class position)
  "Remove some or all advice from FUNCTION-SPEC, or from all functions.
With no arguments, all advice is removed.  This is a consequence of these rules:
If FUNCTION-SPEC is non-NIL, advice is removed from that function only.
 Otherwise, advice is removed from all functions if the other args match.
If CLASS is non-NIL, only advice of that class is removed.
If POSITION is non-NIL (a number or name), only advice with that position is removed."
  (if (null function-spec)
      `(dolist (fn *advised-functions*)
	 (unadvise-1 fn ',class ',position))
    `(unadvise-1 ',function-spec ',class ',position)))

(defun unadvise-1 (function-spec &optional class position)
  (setq function-spec (dwimify-arg-package function-spec 'function))
  (and (member-equal function-spec *advised-functions*) (advise-init function-spec))
  (check-type class (member nil :before :after :around))
  (check-type position (or symbol (integer 0)))
  (let* ((spec1 (unencapsulate-function-spec function-spec 'advise)))
    (dolist (slot-location
	      (if class (list (advise-find-slot spec1 class))
		(list (advise-find-slot spec1 ':before)
		      (advise-find-slot spec1 ':after)
		      (advise-find-slot spec1 ':around))))
      ;; For each slot we are supposed to operate on,
      ;; remove any advice that matches POSITION.
      (cond ((null position)
	     (if (consp slot-location)
		 (setf (car slot-location) nil)))
	    ((numberp position)
	     (let ((preceding (nthcdr position (locf (car slot-location)))))
	       (when (cdr preceding) (rplacd preceding (cddr preceding)))))
	    ((symbolp position)
	     (do ((l (locf (car slot-location)) (cdr l)))
		 ((null l))
	       (and (eq (cadadr (cadr l)) position)
		    (return (rplacd l (cddr l))))))))
    ;; Flush the encapsulation if there is no advice in it.
    (and (null (car (advise-find-slot spec1 ':before)))
	 (null (car (advise-find-slot spec1 ':after)))
	 (null (car (advise-find-slot spec1 ':around)))
	 (let ((olddef (fdefinition (unencapsulate-function-spec spec1 '(advise)))))
	   (cond ((eq (car (fdefinition spec1)) 'macro)
		  (setq olddef (cons 'macro olddef))))
	   (fdefine spec1 olddef)
	   (setq *advised-functions* (cl:delete function-spec *advised-functions*
						:test #'equal))))
    (if compile-encapsulations-flag
	(compile-encapsulations function-spec 'advise))
    nil))

;;;; ADVISE-WITHIN: advise one function but only when called from another specific one.
;;;    An alternative to advising (:within foo bar).

(defmacro advise-within (within-function-spec function-to-advise class name position &rest forms)
  "Advise FUNCTION-TO-ADVISE, but only when called directly from WITHIN-FUNCTION-SPEC.
This is like using ADVISE on (:WITHIN WITHIN-FUNCTION-SPEC FUNCTION-TO-ADVISE)."
  `(advise-within-1 ',within-function-spec ',function-to-advise
		    ',class ',name ',position ',forms))

(defun advise-within-1 (within-function-spec function-to-advise class name position forms)
  (advise-1 `(:within ,within-function-spec ,function-to-advise)
	    class name position forms))

(defmacro unadvise-within (within-function-spec &optional advised-function class position)
  "Remove advice placed on FUNCTION-TO-ADVISE for when called directly from WITHIN-FUNCTION-SPEC.
This is like using UNADVISE on (:WITHIN WITHIN-FUNCTION-SPEC FUNCTION-TO-ADVISE).
If only WITHIN-FUNCTION-SPEC is given, all advice on functions within that is removed.
With no argument, all advice placed on any function within another function is removed."
  `(unadvise-within-1 ',within-function-spec ',advised-function ',class ',position))

;; UNADVISE-WITHIN is not superfluous because if you specify
;; just the within-function-spec, or nothing at all,
;; it eliminates all advising of anything within that within-function-spec,
;; or all advising within anything.

(defun unadvise-within-1 (within-function-spec &optional advised-function class position)
  (if (and within-function-spec advised-function)
      (unadvise-1 `(:within ,within-function-spec ,advised-function) class position)
    (dolist (fn *advised-functions*)
      (when (and (eq (car-safe fn) ':within)
		 (or (null within-function-spec)
		     (eq within-function-spec (second fn)))
		 (or (null advised-function)
		     (eq advised-function (third fn))))
	(unadvise-1 fn class position)))))
