(in-package :slynk)

(defvar *application-hints-tables* '()
  "A list of hash tables mapping symbols to indentation hints (lists 
of symbols and numbers as per cl-indent.el). Applications can add hash 
tables to the list to change the auto indentation sly sends to 
emacs.")

(defun has-application-indentation-hint-p (symbol)
  (let ((default (load-time-value (gensym))))
    (dolist (table *application-hints-tables*)
      (let ((indentation (gethash symbol table default)))
        (unless (eq default indentation)
          (return-from has-application-indentation-hint-p
            (values indentation t))))))
  (values nil nil))

(defun application-indentation-hint (symbol)
  (let ((indentation (has-application-indentation-hint-p symbol)))
    (labels ((walk (indentation-spec)
               (etypecase indentation-spec
                 (null nil)
                 (number indentation-spec)
                 (symbol (string-downcase indentation-spec))
                 (cons (cons (walk (car indentation-spec))
                             (walk (cdr indentation-spec)))))))
      (walk indentation))))

;;; override slynk version of this function
(defun symbol-indentation (symbol)
  "Return a form describing the indentation of SYMBOL. 

The form is to be used as the `sly-common-lisp-indent-function' property
in Emacs."
  (cond
    ((has-application-indentation-hint-p symbol)
     (application-indentation-hint symbol))
    ((and (macro-function symbol)
             (not (known-to-emacs-p symbol)))
     (let ((arglist (arglist symbol)))
       (etypecase arglist
         ((member :not-available)
          nil)
         (list
          (macro-indentation arglist)))))
    (t nil)))

;;; More complex version.
(defun macro-indentation (arglist)
  (labels ((frob (list &optional base)
             (if (every (lambda (x)
                          (member x '(nil "&rest") :test #'equal))
                        list)
                 ;; If there was nothing interesting, don't return anything.
                 nil
                 ;; Otherwise substitute leading NIL's with 4 or 1.
                 (let ((ok t))
                   (substitute-if (if base
                                      4
                                      1)
                                  (lambda (x)
                                    (if (and ok (not x))
                                        t
                                        (setf ok nil)))
                                  list))))
           (walk (list level &optional firstp)
             (when (consp list)
               (let ((head (car list)))
                 (if (consp head)
                     (let ((indent (frob (walk head (+ level 1) t))))
                       (cons (list* "&whole" (if (zerop level)
                                                 4
                                                 1)
                                    indent) (walk (cdr list) level)))
                     (case head
                       ;; &BODY is &BODY, this is clear.
                       (&body
                        '("&body"))
                       ;; &KEY is tricksy. If it's at the base level, we want
                       ;; to indent them normally:
                       ;;
                       ;;  (foo bar quux
                       ;;       :quux t
                       ;;       :zot nil)
                       ;;
                       ;; If it's at a destructuring level, we want indent of 1:
                       ;;
                       ;;  (with-foo (var arg
                       ;;             :foo t
                       ;;             :quux nil)
                       ;;     ...)
                       (&key
                        (if (zerop level)
                            '("&rest" nil)
                            '("&rest" 1)))
                       ;; &REST is tricksy. If it's at the front of
                       ;; destructuring, we want to indent by 1, otherwise
                       ;; normally:
                       ;;
                       ;;  (foo (bar quux
                       ;;        zot)
                       ;;    ...)
                       ;;
                       ;; but
                       ;;
                       ;;  (foo bar quux
                       ;;       zot)
                       (&rest
                        (if (and (plusp level) firstp)
                            '("&rest" 1)
                            '("&rest" nil)))
                       ;; &WHOLE and &ENVIRONMENT are skipped as if they weren't there
                       ;; at all.
                       ((&whole &environment)
                        (walk (cddr list) level firstp))
                       ;; &OPTIONAL is indented normally -- and the &OPTIONAL marker
                       ;; itself is not counted.
                       (&optional
                        (walk (cdr list) level))
                       ;; Indent normally, walk the tail -- but
                       ;; unknown lambda-list keywords terminate the walk.
                       (otherwise
                        (unless (member head lambda-list-keywords)
                          (cons nil (walk (cdr list) level))))))))))
    (frob (walk arglist 0 t) t)))

#+nil
(progn
  (assert (equal '(4 4 ("&whole" 4 "&rest" 1) "&body")
                 (macro-indentation '(bar quux (&rest slots) &body body))))
  (assert (equal nil
                 (macro-indentation '(a b c &rest more))))
  (assert (equal '(4 4 4 "&body")
                 (macro-indentation '(a b c &body more))))
  (assert (equal '(("&whole" 4 1 1 "&rest" 1) "&body")
                 (macro-indentation '((name zot &key foo bar) &body body))))
  (assert (equal nil
                 (macro-indentation '(x y &key z)))))

(provide :slynk/indentation)