(require 'slime)
(require 'advice)
(require 'slime-compiler-notes-tree) ; FIXME: actually only uses the tree bits, so that should be a library.

(define-slime-contrib slime-references
  "Clickable references to documentation (SBCL only)."
  (:authors "Christophe Rhodes  <csr21@cantab.net>"
            "Luke Gorrie  <luke@bluetail.com>"
            "Tobias C. Rittweiler <tcr@freebits.de>")
  (:license "GPL")
  (:on-load
   (ad-enable-advice 'slime-note.message 'after 'slime-note.message+references)
   (ad-activate 'slime-note.message)
   (setq slime-tree-printer 'slime-tree-print-with-references)
   (add-hook 'sldb-extras-hooks 'sldb-maybe-insert-references))
  (:on-unload
   (ad-disable-advice 'slime-note.message 'after 'slime-note.message+references)
   (ad-deactivate 'slime-note.message)
   (setq slime-tree-printer 'slime-tree-default-printer)
   (remove-hook 'sldb-extras-hooks 'sldb-maybe-insert-references)))

(defcustom slime-sbcl-manual-root "http://www.sbcl.org/manual/"
  "*The base URL of the SBCL manual, for documentation lookup."
  :type '(choice (string :tag "HTML Documentation")
                 (const :tag "Info Documentation" :info))
  :group 'slime-mode)

(defface sldb-reference-face
  (list (list t '(:underline t)))
  "Face for references."
  :group 'slime-debugger)


;;;;; SBCL-style references

(defvar slime-references-local-keymap
  (let ((map (make-sparse-keymap "local keymap for slime references")))
    (define-key map [mouse-2] 'slime-lookup-reference-at-mouse)
    (define-key map [return] 'slime-lookup-reference-at-point)
    map))

(defun slime-reference-properties (reference)
  "Return the properties for a reference.
Only add clickability to properties we actually know how to lookup."
  (cl-destructuring-bind (where type what) reference
    (if (or (and (eq where :sbcl) (eq type :node))
            (and (eq where :ansi-cl)
                 (memq type '(:function :special-operator :macro
                                        :type :system-class
                                        :section :glossary :issue))))
        `(slime-reference ,reference
                          font-lock-face sldb-reference-face
                          follow-link t
                          mouse-face highlight
                          help-echo "mouse-2: visit documentation."
                          keymap ,slime-references-local-keymap))))

(defun slime-insert-reference (reference)
  "Insert documentation reference from a condition.
See SWANK-BACKEND:CONDITION-REFERENCES for the datatype."
  (cl-destructuring-bind (where type what) reference
    (insert "\n" (slime-format-reference-source where) ", ")
    (slime-insert-propertized (slime-reference-properties reference)
                              (slime-format-reference-node what))
    (insert (format " [%s]" type))))

(defun slime-insert-references (references)
  (when references
    (insert "\nSee also:")
    (slime-with-rigid-indentation 2
      (mapc #'slime-insert-reference references))))

(defun slime-format-reference-source (where)
  (cl-case where
    (:amop    "The Art of the Metaobject Protocol")
    (:ansi-cl "Common Lisp Hyperspec")
    (:sbcl    "SBCL Manual")
    (t        (format "%S" where))))

(defun slime-format-reference-node (what)
  (if (listp what)
      (mapconcat #'prin1-to-string what ".")
    what))

(defun slime-lookup-reference-at-point ()
  "Browse the documentation reference at point."
  (interactive)
  (let ((refs (get-text-property (point) 'slime-reference)))
    (if (null refs)
        (error "No references at point")
      (cl-destructuring-bind (where type what) refs
        (cl-case where
          (:ansi-cl
           (cl-case type
             (:section
              (browse-url (funcall common-lisp-hyperspec-section-fun what)))
             (:glossary
              (browse-url (funcall common-lisp-hyperspec-glossary-function what)))
             (:issue
              (browse-url (common-lisp-issuex what)))
             (:special-operator
              (browse-url (common-lisp-special-operator (downcase name))))
             (t
              (hyperspec-lookup what))))
          (t
           (case slime-sbcl-manual-root
             (:info
              (info (format "(sbcl)%s" what)))
             (t
              (browse-url
               (format "%s#%s" slime-sbcl-manual-root
                       (subst-char-in-string ?\  ?\- what)))))))))))

(defun slime-lookup-reference-at-mouse (event)
  "Invoke the action pointed at by the mouse."
  (interactive "e")
  (cl-destructuring-bind (mouse-1 (w pos . _) . _) event
    (save-excursion
      (goto-char pos)
      (slime-lookup-reference-at-point))))

;;;;; Hook into *SLIME COMPILATION*

(defun slime-note.references (note)
  (plist-get note :references))

;;; FIXME: `compilation-mode' will swallow the `mouse-face'
;;; etc. properties.
(defadvice slime-note.message (after slime-note.message+references)
  (setq ad-return-value
        (concat ad-return-value
                (with-temp-buffer
                  (slime-insert-references
                   (slime-note.references (ad-get-arg 0)))
                  (buffer-string)))))

;;;;; Hook into slime-compiler-notes-tree

(defun slime-tree-print-with-references (tree)
  ;; for SBCL-style references
  (slime-tree-default-printer tree)
  (let ((note (plist-get (slime-tree.plist tree) 'note)))
    (when note
      (let ((references (slime-note.references note)))
        (when references
          (terpri (current-buffer))
          (slime-insert-references references))))))

;;;;; Hook into SLDB

(defun sldb-maybe-insert-references (extra)
  (slime-dcase extra
    ((:references references) (slime-insert-references references) t)
    (t nil)))

(provide 'slime-references)
