;;; slynk-fancy-inspector.lisp --- Fancy inspector for CLOS objects
;;
;; Author: Marco Baringer <mb@bese.it> and others
;; License: Public Domain
;;

(in-package :slynk)

(defmethod emacs-inspect ((symbol symbol))
  (let ((package (symbol-package symbol)))
    (multiple-value-bind (_symbol status)
        (and package (find-symbol (string symbol) package))
      (declare (ignore _symbol))
      (append
        (label-value-line "Its name is" (symbol-name symbol))
        ;;
        ;; Value
        (cond ((boundp symbol)
               (append
                (label-value-line (if (constantp symbol)
                                      "It is a constant of value"
                                      "It is a global variable bound to")
                                  (symbol-value symbol) :newline nil)
                ;; unbinding constants might be not a good idea, but
                ;; implementations usually provide a restart.
                `(" " (:action "[unbind]"
                               ,(lambda () (makunbound symbol))))
                '((:newline))))
              (t '("It is unbound." (:newline))))
        (docstring-ispec "Documentation" symbol 'variable)
        (multiple-value-bind (expansion definedp) (macroexpand symbol)
          (if definedp
              (label-value-line "It is a symbol macro with expansion"
                                expansion)))
        ;;
        ;; Function
        (if (fboundp symbol)
            (append (if (macro-function symbol)
                        `("It a macro with macro-function: "
                          (:value ,(macro-function symbol)))
                        `("It is a function: "
                          (:value ,(symbol-function symbol))))
                    `(" " (:action "[unbind]"
                                   ,(lambda () (fmakunbound symbol))))
                    `((:newline)))
            `("It has no function value." (:newline)))
        (docstring-ispec "Function documentation" symbol 'function)
        (when (compiler-macro-function symbol)
            (append
             (label-value-line "It also names the compiler macro"
                               (compiler-macro-function symbol) :newline nil)
             `(" " (:action "[remove]"
                            ,(lambda ()
                               (setf (compiler-macro-function symbol) nil)))
                   (:newline))))
        (docstring-ispec "Compiler macro documentation"
                         symbol 'compiler-macro)
        ;;
        ;; Package
        (if package
            `("It is " ,(string-downcase (string status))
                       " to the package: "
                       (:value ,package ,(package-name package))
                       ,@(if (eq :internal status)
                             `(" "
                               (:action "[export]"
                                        ,(lambda () (export symbol package)))))
                       " "
                       (:action "[unintern]"
                                ,(lambda () (unintern symbol package)))
                       (:newline))
            '("It is a non-interned symbol." (:newline)))
        ;;
        ;; Plist
        (label-value-line "Property list" (symbol-plist symbol))
        ;;
        ;; Class
        (if (find-class symbol nil)
            `("It names the class "
              (:value ,(find-class symbol) ,(string symbol))
              " "
              (:action "[remove]"
                       ,(lambda () (setf (find-class symbol) nil)))
              (:newline)))
        ;;
        ;; More package
        (if (find-package symbol)
            (label-value-line "It names the package" (find-package symbol)))
        (inspect-type-specifier symbol)))))

#-sbcl
(defun inspect-type-specifier (symbol)
  (declare (ignore symbol)))

#+sbcl
(defun inspect-type-specifier (symbol)
  (let* ((kind (sb-int:info :type :kind symbol))
         (fun (case kind
                (:defined
                 (or (sb-int:info :type :expander symbol) t))
                (:primitive
                 (or #.(if (slynk-sbcl::sbcl-version>= 1 3 1)
                           '(let ((x (sb-int:info :type :expander symbol)))
                             (if (consp x)
                                 (car x)
                                 x))
                           '(sb-int:info :type :translator symbol))
                     t)))))
    (when fun
      (append
       (list
        (format nil "It names a ~@[primitive~* ~]type-specifier."
                (eq kind :primitive))
        '(:newline))
       (docstring-ispec "Type-specifier documentation" symbol 'type)
       (unless (eq t fun)
         (let ((arglist (arglist fun)))
           (append
            `("Type-specifier lambda-list: "
              ;; Could use ~:s, but inspector-princ does a bit more,
              ;; and not all NILs in the arglist should be printed that way.
              ,(if arglist
                   (inspector-princ arglist)
                   "()")
              (:newline))
            (multiple-value-bind (expansion ok)
                (handler-case (sb-ext:typexpand-1 symbol)
                  (error () (values nil nil)))
              (when ok
                (list "Type-specifier expansion: "
                      (princ-to-string expansion)))))))))))

(defun docstring-ispec (label object kind)
  "Return a inspector spec if OBJECT has a docstring of kind KIND."
  (let ((docstring (documentation object kind)))
    (cond ((not docstring) nil)
          ((< (+ (length label) (length docstring))
              75)
           (list label ": " docstring '(:newline)))
          (t
           (list label ":" '(:newline) "  " docstring '(:newline))))))

(unless (find-method #'emacs-inspect '() (list (find-class 'function)) nil)
  (defmethod emacs-inspect ((f function))
    (inspect-function f)))

(defun inspect-function (f)
  (append
   (label-value-line "Name" (function-name f))
   `("Its argument list is: "
     ,(inspector-princ (arglist f)) (:newline))
   (docstring-ispec "Documentation" f t)
   (if (function-lambda-expression f)
       (label-value-line "Lambda Expression"
                         (function-lambda-expression f)))))

(defun method-specializers-for-inspect (method)
  "Return a \"pretty\" list of the method's specializers. Normal
  specializers are replaced by the name of the class, eql
  specializers are replaced by `(eql ,object)."
  (mapcar (lambda (spec)
            (typecase spec
              (slynk-mop:eql-specializer
               `(eql ,(slynk-mop:eql-specializer-object spec)))
              #-sbcl
              (t
               (slynk-mop:class-name spec))
              #+sbcl
              (t
               ;; SBCL has extended specializers
               (let ((gf (sb-mop:method-generic-function method)))
                 (cond (gf
                        (sb-pcl:unparse-specializer-using-class gf spec))
                       ((typep spec 'class)
                        (class-name spec))
                       (t
                        spec))))))
          (slynk-mop:method-specializers method)))

(defun method-for-inspect-value (method)
  "Returns a \"pretty\" list describing METHOD. The first element
  of the list is the name of generic-function method is
  specialiazed on, the second element is the method qualifiers,
  the rest of the list is the method's specialiazers (as per
  method-specializers-for-inspect)."
  (append (list (slynk-mop:generic-function-name
                 (slynk-mop:method-generic-function method)))
          (slynk-mop:method-qualifiers method)
          (method-specializers-for-inspect method)))

(defmethod emacs-inspect ((object standard-object))
  (let ((class (class-of object)))
            `("Class: " (:value ,class) (:newline)
              ,@(all-slots-for-inspector object))))

(defvar *gf-method-getter* 'methods-by-applicability
  "This function is called to get the methods of a generic function.
The default returns the method sorted by applicability.
See `methods-by-applicability'.")

(defun specializer< (specializer1 specializer2)
  "Return true if SPECIALIZER1 is more specific than SPECIALIZER2."
  (let ((s1 specializer1) (s2 specializer2) )
    (cond ((typep s1 'slynk-mop:eql-specializer)
           (not (typep s2 'slynk-mop:eql-specializer)))
          ((typep s1 'class)
           (flet ((cpl (class)
                    (and (slynk-mop:class-finalized-p class)
                         (slynk-mop:class-precedence-list class))))
             (member s2 (cpl s1)))))))

(defun methods-by-applicability (gf)
  "Return methods ordered by most specific argument types.

`method-specializer<' is used for sorting."
  ;; FIXME: argument-precedence-order and qualifiers are ignored.
  (labels ((method< (meth1 meth2)
             (loop for s1 in (slynk-mop:method-specializers meth1)
                   for s2 in (slynk-mop:method-specializers meth2)
                   do (cond ((specializer< s2 s1) (return nil))
                            ((specializer< s1 s2) (return t))))))
    (stable-sort (copy-seq (slynk-mop:generic-function-methods gf))
                 #'method<)))

(defun abbrev-doc (doc &optional (maxlen 80))
  "Return the first sentence of DOC, but not more than MAXLAN characters."
  (subseq doc 0 (min (1+ (or (position #\. doc) (1- maxlen)))
                     maxlen
                     (length doc))))

(defstruct (inspector-checklist (:conc-name checklist.)
                                 (:constructor %make-checklist (buttons)))
  (buttons nil :type (or null simple-vector))
  (count   0))

(defun make-checklist (n)
  (%make-checklist (make-array n :initial-element nil)))

(defun reinitialize-checklist (checklist)
  ;; Along this counter the buttons are created, so we have to
  ;; initialize it to 0 everytime the inspector page is redisplayed.
  (setf (checklist.count checklist) 0)
  checklist)

(defun make-checklist-button (checklist)
  (let ((buttons (checklist.buttons checklist))
        (i (checklist.count checklist)))
    (incf (checklist.count checklist))
    `(:action ,(if (svref buttons i)
                   "[X]"
                   "[ ]")
              ,#'(lambda ()
                   (setf (svref buttons i) (not (svref buttons i))))
              :refreshp t)))

(defmacro do-checklist ((idx checklist) &body body)
  "Iterate over all set buttons in CHECKLIST."
  (let ((buttons (gensym "buttons")))
    `(let ((,buttons (checklist.buttons ,checklist)))
       (dotimes (,idx (length ,buttons))
          (when (svref ,buttons ,idx)
            ,@body)))))

(defun box (thing) (cons :box thing))
(defun ref (box)
  (assert (eq (car box) :box))
  (cdr box))
(defun (setf ref) (value box)
  (assert (eq (car box) :box))
  (setf (cdr box) value))

(defvar *inspector-slots-default-order* :alphabetically
  "Accepted values: :alphabetically and :unsorted")

(defvar *inspector-slots-default-grouping* :all
  "Accepted values: :inheritance and :all")

(defgeneric all-slots-for-inspector (object))

(defmethod all-slots-for-inspector ((object standard-object))
  (let* ((class           (class-of object))
         (direct-slots    (slynk-mop:class-direct-slots class))
         (effective-slots (slynk-mop:class-slots class))
         (longest-slot-name-length
          (loop for slot :in effective-slots
                maximize (length (symbol-name
                                  (slynk-mop:slot-definition-name slot)))))
         (checklist
          (reinitialize-checklist
           (ensure-istate-metadata object :checklist
                                   (make-checklist (length effective-slots)))))
         (grouping-kind
          ;; We box the value so we can re-set it.
          (ensure-istate-metadata object :grouping-kind
                                  (box *inspector-slots-default-grouping*)))
         (sort-order
          (ensure-istate-metadata object :sort-order
                                  (box *inspector-slots-default-order*)))
         (sort-predicate (ecase (ref sort-order)
                           (:alphabetically #'string<)
                           (:unsorted (constantly nil))))
         (sorted-slots (sort (copy-seq effective-slots)
                             sort-predicate
                             :key #'slynk-mop:slot-definition-name))
         (effective-slots
          (ecase (ref grouping-kind)
            (:all sorted-slots)
            (:inheritance (stable-sort-by-inheritance sorted-slots
                                                      class sort-predicate)))))
    `("--------------------"
      (:newline)
      " Group slots by inheritance "
      (:action ,(ecase (ref grouping-kind)
                       (:all "[ ]")
                       (:inheritance "[X]"))
               ,(lambda ()
                        ;; We have to do this as the order of slots will
                        ;; be sorted differently.
                        (fill (checklist.buttons checklist) nil)
                        (setf (ref grouping-kind)
                              (ecase (ref grouping-kind)
                                (:all :inheritance)
                                (:inheritance :all))))
               :refreshp t)
      (:newline)
      " Sort slots alphabetically  "
      (:action ,(ecase (ref sort-order)
                       (:unsorted "[ ]")
                       (:alphabetically "[X]"))
               ,(lambda ()
                        (fill (checklist.buttons checklist) nil)
                        (setf (ref sort-order)
                              (ecase (ref sort-order)
                                (:unsorted :alphabetically)
                                (:alphabetically :unsorted))))
               :refreshp t)
      (:newline)
      ,@ (case (ref grouping-kind)
           (:all
            `((:newline)
              "All Slots:"
              (:newline)
              ,@(make-slot-listing checklist object class
                                   effective-slots direct-slots
                                   longest-slot-name-length)))
           (:inheritance
            (list-all-slots-by-inheritance checklist object class
                                           effective-slots direct-slots
                                           longest-slot-name-length)))
      (:newline)
      (:action "[set value]"
               ,(lambda ()
                        (do-checklist (idx checklist)
                          (query-and-set-slot class object
                                              (nth idx effective-slots))))
               :refreshp t)
      "  "
      (:action "[make unbound]"
               ,(lambda ()
                        (do-checklist (idx checklist)
                          (slynk-mop:slot-makunbound-using-class
                           class object (nth idx effective-slots))))
               :refreshp t)
      (:newline))))

(defun list-all-slots-by-inheritance (checklist object class effective-slots
                                      direct-slots longest-slot-name-length)
  (flet ((slot-home-class (slot)
           (slot-home-class-using-class slot class)))
    (let ((current-slots '()))
      (append
       (loop for slot in effective-slots
             for previous-home-class = (slot-home-class slot) then home-class
             for home-class = previous-home-class then (slot-home-class slot)
             if (eq home-class previous-home-class)
               do (push slot current-slots)
             else
               collect '(:newline)
               and collect (format nil "~A:" (class-name previous-home-class))
               and collect '(:newline)
               and append (make-slot-listing checklist object class
                                             (nreverse current-slots)
                                             direct-slots
                                             longest-slot-name-length)
               and do (setf current-slots (list slot)))
       (and current-slots
            `((:newline)
              ,(format nil "~A:"
                       (class-name (slot-home-class-using-class
                                    (car current-slots) class)))
              (:newline)
              ,@(make-slot-listing checklist object class
                                   (nreverse current-slots) direct-slots
                                   longest-slot-name-length)))))))

(defun make-slot-listing (checklist object class effective-slots direct-slots
                          longest-slot-name-length)
  (flet ((padding-for (slot-name)
           (make-string (- longest-slot-name-length (length slot-name))
                        :initial-element #\Space)))
    (loop
      for effective-slot :in effective-slots
      for direct-slot = (find (slynk-mop:slot-definition-name effective-slot)
                              direct-slots
                              :key #'slynk-mop:slot-definition-name)
      for slot-name   = (inspector-princ
                         (slynk-mop:slot-definition-name effective-slot))
      collect (make-checklist-button checklist)
      collect "  "
      collect `(:value ,(if direct-slot
                            (list direct-slot effective-slot)
                            effective-slot)
                       ,slot-name)
      collect (padding-for slot-name)
      collect " = "
      collect (slot-value-for-inspector class object effective-slot)
      collect '(:newline))))

(defgeneric slot-value-for-inspector (class object slot)
  (:method (class object slot)
    (let ((boundp (slynk-mop:slot-boundp-using-class class object slot)))
      (if boundp
          `(:value ,(slynk-mop:slot-value-using-class class object slot))
          "#<unbound>"))))

(defun slot-home-class-using-class (slot class)
  (let ((slot-name (slynk-mop:slot-definition-name slot)))
    (loop for class in (reverse (slynk-mop:class-precedence-list class))
          thereis (and (member slot-name (slynk-mop:class-direct-slots class)
                               :key #'slynk-mop:slot-definition-name
                               :test #'eq)
                       class))))

(defun stable-sort-by-inheritance (slots class predicate)
  (stable-sort slots predicate
               :key #'(lambda (s)
                        (class-name (slot-home-class-using-class s class)))))

(defun query-and-set-slot (class object slot)
  (let* ((slot-name (slynk-mop:slot-definition-name slot))
         (value-string (read-from-minibuffer-in-emacs
                        (format nil "Set slot ~S to (evaluated) : "
                                slot-name))))
    (when (and value-string (not (string= value-string "")))
      (with-simple-restart (abort "Abort setting slot ~S" slot-name)
        (setf (slynk-mop:slot-value-using-class class object slot)
              (eval (read-from-string value-string)))))))


(defmethod emacs-inspect ((gf standard-generic-function))
  (flet ((lv (label value) (label-value-line label value)))
    (append
      (lv "Name" (slynk-mop:generic-function-name gf))
      (lv "Arguments" (slynk-mop:generic-function-lambda-list gf))
      (docstring-ispec "Documentation" gf t)
      (lv "Method class" (slynk-mop:generic-function-method-class gf))
      (lv "Method combination"
          (slynk-mop:generic-function-method-combination gf))
      `("Methods: " (:newline))
      (loop for method in (funcall *gf-method-getter* gf) append
            `((:value ,method ,(inspector-princ
                               ;; drop the name of the GF
                               (cdr (method-for-inspect-value method))))
              " "
              (:action "[remove method]"
                       ,(let ((m method)) ; LOOP reassigns method
                          (lambda ()
                            (remove-method gf m))))
              (:newline)))
      `((:newline))
      (all-slots-for-inspector gf))))

(defmethod emacs-inspect ((method standard-method))
  `(,@(if (slynk-mop:method-generic-function method)
          `("Method defined on the generic function "
            (:value ,(slynk-mop:method-generic-function method)
                    ,(inspector-princ
                      (slynk-mop:generic-function-name
                       (slynk-mop:method-generic-function method)))))
          '("Method without a generic function"))
      (:newline)
      ,@(docstring-ispec "Documentation" method t)
      "Lambda List: " (:value ,(slynk-mop:method-lambda-list method))
      (:newline)
      "Specializers: " (:value ,(slynk-mop:method-specializers method)
                               ,(inspector-princ
                                 (method-specializers-for-inspect method)))
      (:newline)
      "Qualifiers: " (:value ,(slynk-mop:method-qualifiers method))
      (:newline)
      "Method function: " (:value ,(slynk-mop:method-function method))
      (:newline)
      ,@(all-slots-for-inspector method)))

(defun specializer-direct-methods (class)
  (sort (copy-seq (slynk-mop:specializer-direct-methods class))
        #'string<
        :key
        (lambda (x)
          (symbol-name
           (let ((name (slynk-mop::generic-function-name
                        (slynk-mop::method-generic-function x))))
             (if (symbolp name)
                 name
                 (second name)))))))

(defmethod emacs-inspect ((class standard-class))
  `("Name: "
    (:value ,(class-name class))
    (:newline)
    "Super classes: "
    ,@(common-seperated-spec (slynk-mop:class-direct-superclasses class))
    (:newline)
    "Direct Slots: "
    ,@(common-seperated-spec
       (slynk-mop:class-direct-slots class)
       (lambda (slot)
         `(:value ,slot ,(inspector-princ
                          (slynk-mop:slot-definition-name slot)))))
    (:newline)
    "Effective Slots: "
    ,@(if (slynk-mop:class-finalized-p class)
          (common-seperated-spec
           (slynk-mop:class-slots class)
           (lambda (slot)
             `(:value ,slot ,(inspector-princ
                              (slynk-mop:slot-definition-name slot)))))
          `("#<N/A (class not finalized)> "
            (:action "[finalize]"
                     ,(lambda () (slynk-mop:finalize-inheritance class)))))
    (:newline)
    ,@(let ((doc (documentation class t)))
        (when doc
          `("Documentation:" (:newline) ,(inspector-princ doc) (:newline))))
    "Sub classes: "
    ,@(common-seperated-spec (slynk-mop:class-direct-subclasses class)
                             (lambda (sub)
                               `(:value ,sub
                                        ,(inspector-princ (class-name sub)))))
    (:newline)
    "Precedence List: "
    ,@(if (slynk-mop:class-finalized-p class)
          (common-seperated-spec
           (slynk-mop:class-precedence-list class)
           (lambda (class)
             `(:value ,class ,(inspector-princ (class-name class)))))
          '("#<N/A (class not finalized)>"))
    (:newline)
    ,@(when (slynk-mop:specializer-direct-methods class)
        `("It is used as a direct specializer in the following methods:"
          (:newline)
          ,@(loop
              for method in (specializer-direct-methods class)
              collect "  "
              collect `(:value ,method
                               ,(inspector-princ
                                 (method-for-inspect-value method)))
              collect '(:newline)
              if (documentation method t)
              collect "    Documentation: " and
              collect (abbrev-doc (documentation method t)) and
              collect '(:newline))))
    "Prototype: " ,(if (slynk-mop:class-finalized-p class)
                       `(:value ,(slynk-mop:class-prototype class))
                       '"#<N/A (class not finalized)>")
    (:newline)
    ,@(all-slots-for-inspector class)))

(defmethod emacs-inspect ((slot slynk-mop:standard-slot-definition))
  `("Name: "
    (:value ,(slynk-mop:slot-definition-name slot))
    (:newline)
    ,@(when (slynk-mop:slot-definition-documentation slot)
        `("Documentation:" (:newline)
                           (:value ,(slynk-mop:slot-definition-documentation
                                     slot))
                           (:newline)))
    "Init args: "
    (:value ,(slynk-mop:slot-definition-initargs slot))
    (:newline)
    "Init form: "
    ,(if (slynk-mop:slot-definition-initfunction slot)
         `(:value ,(slynk-mop:slot-definition-initform slot))
         "#<unspecified>")
    (:newline)
    "Init function: "
    (:value ,(slynk-mop:slot-definition-initfunction slot))
    (:newline)
    ,@(all-slots-for-inspector slot)))


;; Wrapper structure over the list of symbols of a package that should
;; be displayed with their respective classification flags. This is
;; because we need a unique type to dispatch on in EMACS-INSPECT.
;; Used by the Inspector for packages.
(defstruct (%package-symbols-container
            (:conc-name   %container.)
            (:constructor %%make-package-symbols-container))
  title ;; A string; the title of the inspector page in Emacs.
  description ;; A list of renderable objects; used as description.
  symbols ;; A list of symbols. Supposed to be sorted alphabetically.
  grouping-kind) ;; Either :SYMBOL or :CLASSIFICATION. Cf. MAKE-SYMBOLS-LISTING


(defun %make-package-symbols-container (&key title description symbols)
  (%%make-package-symbols-container :title title :description description
                                    :symbols symbols :grouping-kind :symbol))

(defun symbol-classification-string (symbol)
  "Return a string in the form -f-c---- where each letter stands for
boundp fboundp generic-function class macro special-operator package"
  (let ((letters "bfgctmsp")
        (result (copy-seq "--------")))
    (flet ((flip (letter)
             (setf (char result (position letter letters))
                   letter)))
      (when (boundp symbol) (flip #\b))
      (when (fboundp symbol)
        (flip #\f)
        (when (typep (ignore-errors (fdefinition symbol))
                     'generic-function)
          (flip #\g)))
      (when (type-specifier-p symbol) (flip #\t))
      (when (find-class symbol nil)   (flip #\c) )
      (when (macro-function symbol)   (flip #\m))
      (when (special-operator-p symbol) (flip #\s))
      (when (find-package symbol)       (flip #\p))
      result)))

(defgeneric make-symbols-listing (grouping-kind symbols))

(defmethod make-symbols-listing ((grouping-kind (eql :symbol)) symbols)
  "Returns an object renderable by Emacs' inspector side that
alphabetically lists all the symbols in SYMBOLS together with a
concise string representation of what each symbol
represents (see SYMBOL-CLASSIFICATION-STRING)"
  (let ((max-length (loop for s in symbols
                          maximizing (length (symbol-name s))))
        (distance 10)) ; empty distance between name and classification
    (flet ((string-representations (symbol)
             (let* ((name (symbol-name symbol))
                    (length (length name))
                    (padding (- max-length length)))
               (values
                (concatenate 'string
                             name
                             (make-string (+ padding distance)
                                          :initial-element #\Space))
                (symbol-classification-string symbol)))))
      `(""                           ; 8 is (length "Symbols:")
        "Symbols:" ,(make-string (+ -8 max-length distance)
                                 :initial-element #\Space)
        "Flags:"
        (:newline)
        ,(concatenate 'string        ; underlining dashes
                      (make-string (+ max-length distance -1)
                                   :initial-element #\-)
                      " "
                      (symbol-classification-string '#:foo))
        (:newline)
        ,@(loop for symbol in symbols appending
               (multiple-value-bind (symbol-string classification-string)
                   (string-representations symbol)
                 `((:value ,symbol ,symbol-string) ,classification-string
                   (:newline)
                   )))))))

(defmethod make-symbols-listing ((grouping-kind (eql :classification)) symbols)
  "For each possible classification (cf. CLASSIFY-SYMBOL), group
all the symbols in SYMBOLS to all of their respective
classifications. (If a symbol is, for instance, boundp and a
generic-function, it'll appear both below the BOUNDP group and
the GENERIC-FUNCTION group.) As macros and special-operators are
specified to be FBOUNDP, there is no general FBOUNDP group,
instead there are the three explicit FUNCTION, MACRO and
SPECIAL-OPERATOR groups."
  (let ((table (make-hash-table :test #'eq))
        (+default-classification+ :misc))
    (flet ((normalize-classifications (classifications)
             (cond ((null classifications) `(,+default-classification+))
                   ;; Convert an :FBOUNDP in CLASSIFICATIONS to
                   ;; :FUNCTION if possible.
                   ((and (member :fboundp classifications)
                         (not (member :macro classifications))
                         (not (member :special-operator classifications)))
                    (substitute :function :fboundp classifications))
                   (t (remove :fboundp classifications)))))
      (loop for symbol in symbols do
            (loop for classification in
                  (normalize-classifications (classify-symbol symbol))
                  ;; SYMBOLS are supposed to be sorted alphabetically;
                  ;; this property is preserved here except for reversing.
                  do (push symbol (gethash classification table)))))
    (let* ((classifications (loop for k being each hash-key in table
                                  collect k))
           (classifications (sort classifications
                                  ;; Sort alphabetically, except
                                  ;; +DEFAULT-CLASSIFICATION+ which
                                  ;; sort to the end.
                                  (lambda (a b)
                                    (cond ((eql a +default-classification+)
                                           nil)
                                          ((eql b +default-classification+)
                                           t)
                                          (t (string< a b)))))))
      (loop for classification in classifications
            for symbols = (gethash classification table)
            appending`(,(symbol-name classification)
                       (:newline)
                       ,(make-string 64 :initial-element #\-)
                       (:newline)
                       ,@(mapcan (lambda (symbol)
                                   `((:value ,symbol ,(symbol-name symbol))
                                     (:newline)))
                                 ;; restore alphabetic order.
                                 (nreverse symbols))
                       (:newline))))))

(defmethod emacs-inspect ((%container %package-symbols-container))
  (with-struct (%container. title description symbols grouping-kind) %container
            `(,title (:newline) (:newline)
              ,@description
              (:newline)
              "  " ,(ecase grouping-kind
                           (:symbol
                            `(:action "[Group by classification]"
                                      ,(lambda ()
                                         (setf grouping-kind :classification))
                                      :refreshp t))
                           (:classification
                            `(:action "[Group by symbol]"
                                      ,(lambda () (setf grouping-kind :symbol))
                                      :refreshp t)))
              (:newline) (:newline)
              ,@(make-symbols-listing grouping-kind symbols))))

(defun display-link (type symbols length &key title description)
  (if (null symbols)
      (format nil "0 ~A symbols." type)
      `(:value ,(%make-package-symbols-container :title title
                                                 :description description
                                                 :symbols symbols)
               ,(format nil "~D ~A symbol~P." length type length))))

(defmacro do-symbols* ((var &optional (package '*package*) result-form)
                       &body body)
  "Just like do-symbols, but makes sure a symbol is visited only once."
  (let ((seen-ht (gensym "SEEN-HT")))
    `(let ((,seen-ht (make-hash-table :test #'eq)))
       (do-symbols (,var ,package ,result-form)
         (unless (gethash ,var ,seen-ht)
           (setf (gethash ,var ,seen-ht) t)
           (tagbody ,@body))))))

(defmethod emacs-inspect ((package package))
  (let ((package-name         (package-name package))
        (package-nicknames    (package-nicknames package))
        (package-use-list     (package-use-list package))
        (package-used-by-list (package-used-by-list package))
        (shadowed-symbols     (package-shadowing-symbols package))
        (present-symbols      '()) (present-symbols-length   0)
        (internal-symbols     '()) (internal-symbols-length  0)
        (inherited-symbols    '()) (inherited-symbols-length 0)
        (external-symbols     '()) (external-symbols-length  0))

    (do-symbols* (sym package)
      (let ((status (symbol-status sym package)))
        (when (eq status :inherited)
          (push sym inherited-symbols) (incf inherited-symbols-length)
          (go :continue))
        (push sym present-symbols) (incf present-symbols-length)
        (cond ((eq status :internal)
               (push sym internal-symbols) (incf internal-symbols-length))
              (t
               (push sym external-symbols) (incf external-symbols-length))))
      :continue)

    (setf package-nicknames    (sort (copy-list package-nicknames)
                                     #'string<)
          package-use-list     (sort (copy-list package-use-list)
                                     #'string< :key #'package-name)
          package-used-by-list (sort (copy-list package-used-by-list)
                                     #'string< :key #'package-name)
          shadowed-symbols     (sort (copy-list shadowed-symbols)
                                     #'string<))
    ;;; SORT + STRING-LESSP conses on at least SBCL 0.9.18.
    (setf present-symbols      (sort present-symbols  #'string<)
          internal-symbols     (sort internal-symbols #'string<)
          external-symbols     (sort external-symbols #'string<)
          inherited-symbols    (sort inherited-symbols #'string<))
    `("" ;; dummy to preserve indentation.
      "Name: " (:value ,package-name) (:newline)

      "Nick names: " ,@(common-seperated-spec package-nicknames) (:newline)

      ,@(when (documentation package t)
          `("Documentation:" (:newline)
                             ,(documentation package t) (:newline)))

      "Use list: " ,@(common-seperated-spec
                      package-use-list
                      (lambda (package)
                        `(:value ,package ,(package-name package))))
      (:newline)

      "Used by list: " ,@(common-seperated-spec
                          package-used-by-list
                          (lambda (package)
                            `(:value ,package ,(package-name package))))
      (:newline)

      ,(display-link "present" present-symbols  present-symbols-length
                     :title
                     (format nil "All present symbols of package \"~A\""
                             package-name)
                     :description
                     '("A symbol is considered present in a package if it's"
                       (:newline)
                       "\"accessible in that package directly, rather than"
                       (:newline)
                       "being inherited from another package.\""
                       (:newline)
                       "(CLHS glossary entry for `present')"
                       (:newline)))

      (:newline)
      ,(display-link "external" external-symbols external-symbols-length
                     :title
                     (format nil "All external symbols of package \"~A\""
                             package-name)
                     :description
                     '("A symbol is considered external of a package if it's"
                       (:newline)
                       "\"part of the `external interface' to the package and"
                       (:newline)
                       "[is] inherited by any other package that uses the"
                       (:newline)
                       "package.\" (CLHS glossary entry of `external')"
                       (:newline)))
      (:newline)
      ,(display-link "internal" internal-symbols internal-symbols-length
                     :title
                     (format nil "All internal symbols of package \"~A\""
                             package-name)
                     :description
                     '("A symbol is considered internal of a package if it's"
                       (:newline)
                       "present and not external---that is if the package is"
                       (:newline)
                       "the home package of the symbol, or if the symbol has"
                       (:newline)
                       "been explicitly imported into the package."
                       (:newline)
                       (:newline)
                       "Notice that inherited symbols will thus not be listed,"
                       (:newline)
                       "which deliberately deviates from the CLHS glossary"
                       (:newline)
                       "entry of `internal' because it's assumed to be more"
                       (:newline)
                       "useful this way."
                       (:newline)))
      (:newline)
      ,(display-link "inherited" inherited-symbols  inherited-symbols-length
                     :title
                     (format nil "All inherited symbols of package \"~A\""
                             package-name)
                     :description
                     '("A symbol is considered inherited in a package if it"
                       (:newline)
                       "was made accessible via USE-PACKAGE."
                       (:newline)))
      (:newline)
      ,(display-link "shadowed" shadowed-symbols (length shadowed-symbols)
                     :title
                     (format nil "All shadowed symbols of package \"~A\""
                             package-name)
                     :description nil))))


(defmethod emacs-inspect ((pathname pathname))
  `(,(if (wild-pathname-p pathname)
         "A wild pathname."
         "A pathname.")
     (:newline)
     ,@(label-value-line*
        ("Namestring" (namestring pathname))
        ("Host"       (pathname-host pathname))
        ("Device"     (pathname-device pathname))
        ("Directory"  (pathname-directory pathname))
        ("Name"       (pathname-name pathname))
        ("Type"       (pathname-type pathname))
        ("Version"    (pathname-version pathname)))
     ,@ (unless (or (wild-pathname-p pathname)
                    (not (probe-file pathname)))
          (label-value-line "Truename" (truename pathname)))))

(defmethod emacs-inspect ((pathname logical-pathname))
  (append
   (label-value-line*
    ("Namestring" (namestring pathname))
    ("Physical pathname: " (translate-logical-pathname pathname)))
   `("Host: "
     (:value ,(pathname-host pathname))
     " ("
     (:value ,(logical-pathname-translations
               (pathname-host pathname)))
     " other translations)"
     (:newline))
   (label-value-line*
    ("Directory" (pathname-directory pathname))
    ("Name" (pathname-name pathname))
    ("Type" (pathname-type pathname))
    ("Version" (pathname-version pathname))
    ("Truename" (if (not (wild-pathname-p pathname))
                    (probe-file pathname))))))

(defmethod emacs-inspect ((n number))
  `("Value: " ,(princ-to-string n)))

(defun format-iso8601-time (time-value &optional include-timezone-p)
  "Formats a universal time TIME-VALUE in ISO 8601 format, with
    the time zone included if INCLUDE-TIMEZONE-P is non-NIL"
  ;; Taken from http://www.pvv.ntnu.no/~nsaa/ISO8601.html
  ;; Thanks, Nikolai Sandved and Thomas Russ!
  (flet ((format-iso8601-timezone (zone)
           (if (zerop zone)
               "Z"
               (multiple-value-bind (h m) (truncate (abs zone) 1.0)
                 ;; Tricky.  Sign of time zone is reversed in ISO 8601
                 ;; relative to Common Lisp convention!
                 (format nil "~:[+~;-~]~2,'0D:~2,'0D"
                         (> zone 0) h (round (* 60 m)))))))
    (multiple-value-bind (second minute hour day month year dow dst zone)
        (decode-universal-time time-value)
      (declare (ignore dow))
      (format nil "~4,'0D-~2,'0D-~2,'0DT~2,'0D:~2,'0D:~2,'0D~:[~*~;~A~]"
              year month day hour minute second
              include-timezone-p (format-iso8601-timezone (if dst
                                                              (+ zone 1)
                                                              zone))))))

(defmethod emacs-inspect ((i integer))
  (append
   `(,(format nil "Value: ~D = #x~8,'0X = #o~O = #b~,,' ,8:B~@[ = ~E~]"
	      i i i i (ignore-errors (coerce i 'float)))
     (:newline))
   (when (< -1 i char-code-limit)
     (label-value-line "Code-char" (code-char i)))
   (label-value-line "Integer-length" (integer-length i))
   (ignore-errors
    (label-value-line "Universal-time" (format-iso8601-time i t)))))

(defmethod emacs-inspect ((c complex))
  (label-value-line*
   ("Real part" (realpart c))
   ("Imaginary part" (imagpart c))))

(defmethod emacs-inspect ((r ratio))
  (label-value-line*
   ("Numerator" (numerator r))
   ("Denominator" (denominator r))
   ("As float" (float r))))

(defmethod emacs-inspect ((f float))
  (cond
    ((float-nan-p f)
     ;; try NaN first because the next tests may perform operations
     ;; that are undefined for NaNs.
     (list "Not a Number."))
    ((not (float-infinity-p f))
     (multiple-value-bind (significand exponent sign) (decode-float f)
       (append
	`("Scientific: " ,(format nil "~E" f) (:newline)
			 "Decoded: "
			 (:value ,sign) " * "
			 (:value ,significand) " * "
			 (:value ,(float-radix f)) "^"
			 (:value ,exponent) (:newline))
	(label-value-line "Digits" (float-digits f))
	(label-value-line "Precision" (float-precision f)))))
    ((> f 0)
     (list "Positive infinity."))
    ((< f 0)
     (list "Negative infinity."))))

(defun make-pathname-ispec (pathname position)
  `("Pathname: "
    (:value ,pathname)
    (:newline) "  "
    ,@(when position
        `((:action "[visit file and show current position]"
                   ,(lambda ()
                      (ed-in-emacs `(,pathname :position ,position :bytep t)))
                   :refreshp nil)
          (:newline)))))

(defun make-file-stream-ispec (stream)
  ;; SBCL's socket stream are file-stream but are not associated to
  ;; any pathname.
  (let ((pathname (ignore-errors (pathname stream))))
    (when pathname
      (make-pathname-ispec pathname (and (open-stream-p stream)
                                         (file-position stream))))))

(defmethod emacs-inspect ((stream file-stream))
  (multiple-value-bind (content)
      (call-next-method)
    (append (make-file-stream-ispec stream) content)))

(defmethod emacs-inspect ((condition stream-error))
  (multiple-value-bind (content)
      (call-next-method)
    (let ((stream (stream-error-stream condition)))
      (append (when (typep stream 'file-stream)
                (make-file-stream-ispec stream))
              content))))

(defun common-seperated-spec (list &optional (callback (lambda (v)
                                                         `(:value ,v))))
  (butlast
   (loop
      for i in list
      collect (funcall callback i)
      collect ", ")))

(defun inspector-princ (list)
  "Like princ-to-string, but don't rewrite (function foo) as #'foo.
Do NOT pass circular lists to this function."
  (let ((*print-pprint-dispatch* (copy-pprint-dispatch)))
    (set-pprint-dispatch '(cons (member function)) nil)
    (princ-to-string list)))

(provide :slynk/fancy-inspector)
