(require 'slime)
(require 'slime-repl)
(require 'cl-lib)

(define-slime-contrib slime-editing-commands
  "Editing commands without server interaction."
  (:authors "Thomas F. Burdick  <tfb@OCF.Berkeley.EDU>"
            "Luke Gorrie  <luke@synap.se>"
            "Bill Clementson <billclem@gmail.com>"
            "Tobias C. Rittweiler <tcr@freebits.de>")
  (:license "GPL")
  (:on-load
   (define-key slime-mode-map "\M-\C-a"  'slime-beginning-of-defun)
   (define-key slime-mode-map "\M-\C-e"  'slime-end-of-defun)
   (define-key slime-mode-map "\C-c\M-q" 'slime-reindent-defun)
   (define-key slime-mode-map "\C-c\C-]" 'slime-close-all-parens-in-sexp)))

(defun slime-beginning-of-defun ()
  (interactive)
  (if (and (boundp 'slime-repl-input-start-mark)
           slime-repl-input-start-mark)
      (slime-repl-beginning-of-defun)
    (let ((this-command 'beginning-of-defun)) ; needed for push-mark
      (call-interactively 'beginning-of-defun))))

(defun slime-end-of-defun ()
  (interactive)
  (if (eq major-mode 'slime-repl-mode)
      (slime-repl-end-of-defun)
      (end-of-defun)))

(defvar slime-comment-start-regexp
  "\\(\\(^\\|[^\n\\\\]\\)\\([\\\\][\\\\]\\)*\\);+[ \t]*"
  "Regexp to match the start of a comment.")

(defun slime-beginning-of-comment ()
  "Move point to beginning of comment.
If point is inside a comment move to beginning of comment and return point.
Otherwise leave point unchanged and return NIL."
  (let ((boundary (point)))
    (beginning-of-line)
    (cond ((re-search-forward slime-comment-start-regexp boundary t)
           (point))
          (t (goto-char boundary) 
             nil))))

(defvar slime-close-parens-limit nil
  "Maxmimum parens for `slime-close-all-sexp' to insert. NIL
means to insert as many parentheses as necessary to correctly
close the form.")

(defun slime-close-all-parens-in-sexp (&optional region)
  "Balance parentheses of open s-expressions at point.
Insert enough right parentheses to balance unmatched left parentheses.
Delete extra left parentheses.  Reformat trailing parentheses 
Lisp-stylishly.

If REGION is true, operate on the region. Otherwise operate on
the top-level sexp before point."
  (interactive "P")
  (let ((sexp-level 0)
        point)
    (save-excursion
      (save-restriction
        (when region
          (narrow-to-region (region-beginning) (region-end))
          (goto-char (point-max)))
        ;; skip over closing parens, but not into comment
        (skip-chars-backward ") \t\n")
        (when (slime-beginning-of-comment)
          (forward-line)
          (skip-chars-forward " \t"))
        (setq point (point))
        ;; count sexps until either '(' or comment is found at first column
        (while (and (not (looking-at "^[(;]"))
		    (ignore-errors (backward-up-list 1) t))
          (cl-incf sexp-level))))
    (when (> sexp-level 0)
      ;; insert correct number of right parens
      (goto-char point)
      (dotimes (i sexp-level) (insert ")"))
      ;; delete extra right parens
      (setq point (point))
      (skip-chars-forward " \t\n)")
      (skip-chars-backward " \t\n")
      (let* ((deleted-region     (delete-and-extract-region point (point)))
             (deleted-text       (substring-no-properties deleted-region))
             (prior-parens-count (cl-count ?\) deleted-text)))
        ;; Remember: we always insert as many parentheses as necessary
        ;; and only afterwards delete the superfluously-added parens.
        (when slime-close-parens-limit
          (let ((missing-parens (- sexp-level prior-parens-count
                                   slime-close-parens-limit)))
            (dotimes (i (max 0 missing-parens))
              (delete-char -1))))))))

(defun slime-insert-balanced-comments (arg)
  "Insert a set of balanced comments around the s-expression
containing the point.  If this command is invoked repeatedly
\(without any other command occurring between invocations), the
comment progressively moves outward over enclosing expressions.
If invoked with a positive prefix argument, the s-expression arg
expressions out is enclosed in a set of balanced comments."
  (interactive "*p")
  (save-excursion
    (when (eq last-command this-command)
      (when (search-backward "#|" nil t)
        (save-excursion
          (delete-char 2)
          (while (and (< (point) (point-max)) (not (looking-at " *|#")))
            (forward-sexp))
          (replace-match ""))))
    (while (> arg 0)
      (backward-char 1)
      (cond ((looking-at ")") (cl-incf arg))
            ((looking-at "(") (cl-decf arg))))
    (insert "#|")
    (forward-sexp)
    (insert "|#")))

(defun slime-remove-balanced-comments ()
  "Remove a set of balanced comments enclosing point."
  (interactive "*")
  (save-excursion
    (when (search-backward "#|" nil t)
      (delete-char 2)
      (while (and (< (point) (point-max)) (not (looking-at " *|#")))
      (forward-sexp))
      (replace-match ""))))


;; SLIME-CLOSE-PARENS-AT-POINT is obsolete:

;; It doesn't work correctly on the REPL, because there
;; BEGINNING-OF-DEFUN-FUNCTION and END-OF-DEFUN-FUNCTION is bound to
;; SLIME-REPL-MODE-BEGINNING-OF-DEFUN (and
;; SLIME-REPL-MODE-END-OF-DEFUN respectively) which compromises the
;; way how they're expect to work (i.e. END-OF-DEFUN does not signal
;; an UNBOUND-PARENTHESES error.)

;; Use SLIME-CLOSE-ALL-PARENS-IN-SEXP instead.

;; (defun slime-close-parens-at-point ()
;;   "Close parenthesis at point to complete the top-level-form.  Simply
;; inserts ')' characters at point until `beginning-of-defun' and
;; `end-of-defun' execute without errors, or `slime-close-parens-limit'
;; is exceeded."
;;   (interactive)
;;   (loop for i from 1 to slime-close-parens-limit
;;         until (save-excursion
;;                 (slime-beginning-of-defun)
;;                 (ignore-errors (slime-end-of-defun) t))
;;         do (insert ")")))

(defun slime-reindent-defun (&optional force-text-fill)
  "Reindent the current defun, or refill the current paragraph.
If point is inside a comment block, the text around point will be
treated as a paragraph and will be filled with `fill-paragraph'.
Otherwise, it will be treated as Lisp code, and the current defun
will be reindented.  If the current defun has unbalanced parens,
an attempt will be made to fix it before reindenting.

When given a prefix argument, the text around point will always
be treated as a paragraph.  This is useful for filling docstrings."
  (interactive "P")
  (save-excursion
    (if (or force-text-fill (slime-beginning-of-comment))
        (fill-paragraph nil)
      (let ((start (progn (unless (or (and (zerop (current-column))
                                           (eq ?\( (char-after)))
                                      (and slime-repl-input-start-mark
                                           (slime-repl-at-prompt-start-p)))
                            (slime-beginning-of-defun))
                          (point)))
            (end (ignore-errors (slime-end-of-defun) (point))))
        (unless end
          (forward-paragraph)
          (slime-close-all-parens-in-sexp)
          (slime-end-of-defun)
          (setf end (point)))
        (indent-region start end nil)))))

(provide 'slime-editing-commands)
