;;; syd-prelude.el -*- lexical-binding: t; -*-

(eval-when-compile (require 'cl-lib))

(require 'syd-constants)

(use-package dash)

(require 'dash)

(cl-defmacro syd-define-stub
    (name &key (desc "implement me!") interactive)
  (let ((todo (format "%s: TODO: %s" name desc)))
    `(defun ,name (&rest _)
       ,@(if interactive (list '(interactive)) nil)
       ,todo
       (error ,todo))))

;; FIXME: When `arg-list' contains nils, things break.
(cl-defun syd-parse-rest-and-keys (arg-list)
  "The default behaviour of `cl-defun' makes combining &rest with &keys pretty
useless.  This function will partition ARG-LIST by returning a pair (REST
. KEYS), where REST is the list of ARGS that belong to no key-value pair, and
KEYS is an alist of the parsed keywords."
  ;; Ugh.
  (let (parsed-rest parsed-keys)
    (cl-loop for (lead lag) on arg-list by (lambda (x) (-drop 2 x))
             do (if (keywordp lead)
                    (push (cons lead lag) parsed-keys)
                  ;; Push in reverse order; we reverse the whole list as a
                  ;; post-processing step.
                  (push lead parsed-rest)
                  (when lag
                    (push lag parsed-rest))))
    (cons (reverse parsed-rest) parsed-keys)))

(cl-defun syd-lift-lambdas (&key with-each with-all forms)
  ;; Call the continuation if non-nil.  Wraps the return value in a singleton
  ;; list for "affine" use with unquote-splicing.
  (-let ((call-cont (lambda (cont arg)
                      (if cont
                          (list (funcall cont arg))
                        nil)))
         names)
    `(progn
       ,@(cl-loop
          for form in forms
          appending (cond ((syd-hform-symbol form)
                           (let ((name (nth 1 form)))
                             (push name names)
                             (funcall call-cont with-each name)))
                          ((syd-hform-defun form)
                           (let ((name (nth 1 form)))
                             (push name names)
                             `(,form
                               ,@(funcall call-cont with-each name))))
                          ((syd-hform-lambda form)
                           (let ((name (gensym "lifted-lambda")))
                             (push name names)
                             `((defun ,name (&rest args)
                                 (,form args))
                               ,@(funcall call-cont with-each name))))
                          (t (error "IDK!"))))
       ,@(funcall call-cont with-all names))))

(defun syd-hform-symbol (hform)
  (and (listp hform)
       (= 2 (length hform))
       (symbolp (nth 1 hform))
       (memq (nth 0 hform) '(quote function))))

(defun syd-hform-defun (hform)
  "If HFORM is a defun form, return the defun's name.  Otherwise, return nil"
  (when-let* ((sym (car-safe hform)))
    (and (symbolp sym)
         (eq sym 'defun)
         (nth 1 hform))))

(defun syd-hform-lambda (hform)
  "If HFORM is a lambda, return non-nil."
  (when-let* ((sym (car-safe hform)))
    (and (symbolp sym)
         (eq sym 'lambda))))

(defmacro comment (&rest _)
  "Ignore each argument, and expand to nil."
  nil)

(defmacro with-transient-after (hook-or-function &rest forms)
  (declare (indent defun))
  (let ((hook-name (gensym "transient-hook"))
        (hook-or-function* (gensym "hook-or-function")))
    `(let* ((,hook-or-function* ,hook-or-function))
       (defun ,hook-name (&rest _)
         "Transient hook defined by `with-transient-after'."
         (cond ((functionp ,hook-or-function*)
                (advice-remove ,hook-or-function* #',hook-name))
               ((symbolp ,hook-or-function*)
                (remove-hook ,hook-or-function* #',hook-name)))
         ,@forms)
       (cond ((functionp ,hook-or-function*)
              (advice-add ,hook-or-function* :before #',hook-name))
             ((symbolp ,hook-or-function*)
              ;; https://www.gnu.org/software/emacs/manual/html_node/elisp/Setting-Hooks.html#Setting-Hooks-1
              (put ',hook-name 'permanent-local-hook t)
              (add-hook ,hook-or-function* #',hook-name))))))

(defun syd-plist-put (plist prop new-val)
  "Immutably update a single property of PLIST.  Like `plist-put', but PLIST is
not mutated; a new plist is returned."
  (cl-loop for (prop* old-val) on plist by #'cddr
           appending (if (eq prop prop*)
                         (list prop* new-val)
                       (list prop* old-val))))

(defmacro syd-add-hook (hooks &rest hforms)
  (declare (indent defun))
  (syd-lift-lambdas
   :forms hforms
   :with-all (lambda (fns)
               (let ((fn* (gensym "fn"))
                     (fns* (gensym "fns"))
                     (hook* (gensym "hook")))
                 `(let ((,fns* (list ,@(--map `(function ,it)
                                              fns))))
                    (dolist (,hook* (ensure-list ,hooks))
                      (dolist (,fn* ,fns*)
                        (add-hook ,hook* ,fn*))))))))

(defmacro syd-silently (&rest body)
  `(error "TODO: syd-silently"))

(defmacro syd-quietly (&rest body)
  "Evaluate BODY without generating any output.

This silences calls to `message', `load', `write-region' and anything that
writes to `standard-output'.  In interactive sessions this inhibits output to
the echo-area, but not to *Messages*.  Return value is that of BODY's final
form."
  `(if init-file-debug
       (progn ,@body)
     ,(if noninteractive
          `(syd-silently ,@body)
        `(let ((inhibit-message t)
               (save-silently t))
           (prog1 (progn ,@body)
             (message ""))))))

(defun syd--parse-defadvice-args (arg-list)
  "Parses the docstring and keywords provided to `syd-defadvice'."
  (let (docstring
        advice)
    (when (stringp (car-safe arg-list))
      (setq docstring (pop arg-list)))
    (while (and (length> arg-list 2)
                (keywordp (car arg-list)))
      (let ((how (pop arg-list))
            (sym (pop arg-list)))
        (push (cons how sym) advice)))
    ;; What's left of `arg-list' is the body of the defun.
    (list docstring advice arg-list)))

(defmacro syd-defadvice (name params &rest args)
  "Define a function and add it as advice."
  (declare (indent defun))
  (-let (((docstring advice body) (syd--parse-defadvice-args args)))
    `(progn (defun ,name ,params
              ,@(-some-> docstring list)
              ,@body)
            ,@(-map (lambda (arg)
                      (-let (((how . sym) arg))
                        `(advice-add ,sym ,how #',name)))
                    advice))))

(syd-defadvice syd-lsp-install-server-a ()
  :override #'lsp-install-server
  (user-error (concat "Ignoring a call to `lsp-install-server'"
                      " — tell the caller to use Nix!")))

(defun syd-str (sep &rest strs)
  (mapconcat #'identity strs sep))

(provide 'syd-prelude)
