;;; syd-handle-lookup.el -*- lexical-binding: t; -*-

(require 'syd-text)

(use-package better-jumper)

(require 'better-jumper)

(defvar syd-lookup-online-documentation-backends
  `(("Kagi" . "https://kagi.com/search?q=%s")
    ("DuckDuckGo" . "https://duckduckgo.com/?q=%s")
    ("Nixpkgs" . "https://search.nixos.org/packages?query=%s")
    ("Hackage" . "https://hackage.haskell.org/packages/search?terms=%s")
    ("The Free Dictionary" . "https://www.thefreedictionary.com/%s")
    ("The Free Thesaurus" . "https://www.freethesaurus.com/%s"))
  "A list of pairs (NAME . BACKEND) describing the various backends
`syd-lookup-online-documentation' may delegate to.

NAME is a string used when speaking to the user about BACKEND.

If BACKEND is an interactive command, it will be called interactively.

If BACKEND is a procedure, it will be called with the search string as the lone
argument.

If BACKEND is a string, the user's browser will be opened to the URL returned by
(format BACKEND QUERY), where QUERY is the appropriately-encoded search
string.")

(defvar syd-lookup-documentation-handlers '(syd-lookup-online-documentation)
  "A list of lookup handlers used to find documentation.  A lookup handler
receives an identifier, and is expected to return nil on failure, and non-nil on
success.  When a handler returns a marker, the marker will be jumped to.")

(defun syd-lookup--prompt-for-online-backend ()
  (assoc-string
   (completing-read "Search with: "
                    (mapcar #'car syd-lookup-online-documentation-backends)
                    nil
                    t)
   syd-lookup-online-documentation-backends))

(cl-defun syd-lookup--call-online-backend (backend &key query-string)
  (pcase-let ((`(,name . ,backend-fn) backend))
    (cond ((functionp backend-fn) (if (commandp backend-fn)
                                      (call-interactively backend-fn)
                                    (funcall backend-fn query-string)))
          ((stringp backend-fn)
           (browse-url (format backend-fn
                               (url-encode-url
                                (read-string (format "Search %s for: "
                                                     name)
                                             query-string)))))
          (t (signal 'wrong-type-argument `("backend" ,backend-fn))))))

;;;###autoload
(cl-defun syd-lookup-online-documentation (backend &key query-string)
  (interactive (list (syd-lookup--prompt-for-online-backend)
                     :query-string (when (use-region-p)
                                     (syd-thing-at-point-or-region))))
  (syd-lookup--call-online-backend backend
                                   :query-string query-string))

;;;###autoload
(defun syd-lookup-documentation (identifier)
  "Try to find documentation on IDENTIFIER, and "
  (interactive (list (syd-thing-at-point-or-region)))
  (or (syd-lookup--jump-to 'documentation identifier
                           :display-fn #'display-buffer)
      (user-error "Couldn't find documentation on %S"
                  (substring-no-properties identifier))))

(defvar syd-lookup--handlers-by-category
  '((documentation . syd-lookup-documentation-handlers)))

(cl-defun syd-lookup--jump-to
    (category identifier &key (display-fn #'switch-to-buffer))
  (let* ((handlers (alist-get category syd-lookup--handlers-by-category))
         (origin (point-marker))
         ;; TODO: If called with a prefix argument, prompt the user to select a
         ;; handler.
         (result (run-hook-wrapped handlers #'syd-lookup--run-handler
                                   identifier origin)))
    (unwind-protect
        (when (cond ((null result)
                     (message "No lookup handler could find %S" identifier)
                     nil)
                    ((markerp result)
                     (let ((b (marker-buffer result)))
                       (funcall display-fn b)
                       (when (eq (current-buffer) b)
                         (goto-char result)))
                     result)
                    (result))
          (with-current-buffer (marker-buffer origin)
            (better-jumper-set-jump (marker-position origin)))
          result))
    (set-marker origin nil)))

(defun syd-lookup--run-handler (handler identifier origin)
  (condition-case-unless-debug e
      (let ((wconf (current-window-configuration))
            (result (condition-case-unless-debug e
                        (if (commandp handler)
                            (call-interactively handler)
                          (funcall handler identifier))
                      (error
                       (message "Lookup handler %S threw an error: %s" handler e)
                       'fail))))
        (cond ((eq result 'fail)
               (set-window-configuration wconf)
               nil)
              ((or (get handler 'syd-lookup-async)
                   (eq result 'deferred)))
              ((bufferp result)
               (with-current-buffer result
                 (point-marker)))
              ((or result
                   (null origin)
                   (/= (point-marker) origin))
               (prog1 (point-marker)
                 (set-window-configuration wconf)))))
    ((error user-error)
     (message "Lookup handler %S: %s" handler e)
     nil)))

(general-def
  :states 'normal
  "K" #'syd-lookup-documentation)

(provide 'syd-handle-lookup)
;;; syd-handle-lookup.el ends here
