;;  -*- lexical-binding: t -*-
;;
;; Copyright (c) 2017 David Christiansen.
;; Released under Apache 2.0 license as described in the file LICENSE.
;;
;; Author: David Christiansen
;;
;;; Commentary:
;; This is an interface to Lean's support for holes.
;;
;; The interface consists of two components: the hole command, which
;; collects the list of completions and the message, and a handler,
;; which selects a completion.
;;
;; For now, the only handler uses completing-read, but handlers using
;; helm or company's interface would be a good addition.
;;
;;; Code:
(require 'lean4-server)

(defun lean4-hole-handler-completing-read (alternatives)
  "Pick a hole replacement from ALTERNATIVES with `completing-read'."
  (let* ((choices (cl-loop for alt in alternatives
                           collect (cons (concat (plist-get alt :code)
                                                 " — "
                                                 (plist-get alt :description))
                                         (plist-get alt :code))))
         (selection (let ((this-command 'lean4-hole))
                      (completing-read
                       "Response: "
                       choices
                       nil t nil nil nil t)))
         (code (assoc selection choices)))
    (if code
        (cdr code)
      (error "Didn't select a hole completion"))))

(defvar lean4-hole-handler-function 'lean4-hole-handler-completing-read)

(defun lean4-hole--line-col->pos (line col)
  "Compute the position corresponding to LINE and COL."
  (save-restriction
    (widen)
    (save-excursion
      (goto-char (point-min))
      (forward-line (1- line))
      (forward-char col)
      (point))))

(defun lean4-hole ()
  "Ask Lean for a list of holes, then ask the user which to use."
  (interactive)
  (with-demoted-errors "lean hole: %s"
    (lean4-server-send-command
     'hole_commands (list :file_name (buffer-file-name)
                          :line (line-number-at-pos)
                          :column (lean4-line-offset))
     (cl-function
      (lambda (&key start end results)
        (let ((start-pos (lean4-hole--line-col->pos (plist-get start :line)
                                                   (plist-get start :column)))
              (end-pos (lean4-hole--line-col->pos (plist-get end :line)
                                                 (plist-get end :column))))
          (let ((start-marker (make-marker))
                (end-marker (make-marker)))
            (set-marker start-marker start-pos (current-buffer))
            (set-marker end-marker end-pos (current-buffer))
            (let* ((choices
                    (cl-loop for res in results
                             collect (cons (concat (plist-get res :name)
                                                   " — "
                                                   (plist-get res :description))
                                           (plist-get res :name))))
                   (selection (let ((this-command 'lean4-hole))
                                (completing-read
                                 "Hole command: "
                                 choices
                                 nil t nil nil nil t)))
                   (code (assoc selection choices)))
              (if code
                  (lean4-hole--command (cdr code) start-marker end-marker)
                (error "Didn't select a hole completion"))))))))))

;; This uses markers to ensure that if the hole moves while the
;; command is running, it is still updated.
(defun lean4-hole--command (command start-marker end-marker)
  "Execute COMMAND in the hole between START-MARKER and END-MARKER."
  (interactive)
  (with-demoted-errors "lean hole: %s"
    (lean4-server-send-command
     'hole (list :action command
                 :file_name (buffer-file-name)
                 :line (line-number-at-pos start-marker)
                 :column (lean4-line-offset start-marker))
     (cl-function
      (lambda (&key message replacements)
        (let ((replacement-count (length (plist-get replacements :alternatives))))
          (let ((selected-code
                 (cond ((= replacement-count 0)
                        nil)
                       ((= replacement-count 1)
                        (plist-get (car (plist-get replacements :alternatives)) :code))
                       (t
                        (lean4-hole-handler-completing-read
                         (plist-get replacements :alternatives))))))
            (when selected-code
              (save-excursion
                (goto-char start-marker)
                (delete-region start-marker end-marker)
                (insert selected-code)))))
        (when message
          (message "%s" (s-trim message)))
        (set-marker start-marker nil)
        (set-marker end-marker nil))))))

(defun lean4-hole-right-click ()
  "Ask Lean for a list of hole commands, then ask the user which to use."
  (interactive)
  (let ((buf (current-buffer)))
    (ignore-errors
      (list
       'hole_commands
       (list :file_name (buffer-file-name)
             :line (line-number-at-pos)
             :column (lean4-line-offset))
       (cl-function
        (lambda (&key start end results)
          (when (and start end)
            (with-current-buffer buf
              (let ((start-pos (lean4-hole--line-col->pos (plist-get start :line)
                                                         (plist-get start :column)))
                    (end-pos (lean4-hole--line-col->pos (plist-get end :line)
                                                       (plist-get end :column))))
                (let ((start-marker (make-marker))
                      (end-marker (make-marker)))
                  (set-marker start-marker start-pos (current-buffer))
                  (set-marker end-marker (1+ end-pos) (current-buffer))
                  (mapcar (lambda (res)
                            (let ((item-name (plist-get res :name))
                                  (item-desc (plist-get res :description)))
                              (list :name
                                    (concat item-name " — " item-desc)
                                    :action
                                    (lambda ()
                                      (lean4-hole--command
                                       item-name
                                       start-marker end-marker)))))
                          results)))))))))))

(provide 'lean4-hole)
;;; lean4-hole.el ends here
