;;; fuel-debug.el -- debugging factor code

;; Copyright (C) 2008, 2009, 2010 Jose Antonio Ortega Ruiz
;; See http://factorcode.org/license.txt for BSD license.

;; Author: Jose Antonio Ortega Ruiz <jao@gnu.org>
;; Keywords: languages, fuel, factor
;; Start date: Sun Dec 07, 2008 04:16

;;; Comentary:

;; A mode for displaying the results of run-file and evaluation, with
;; support for restarts.

;;; Code:

(require 'fuel-eval)
(require 'fuel-popup)
(require 'fuel-menu)
(require 'fuel-base)


;;; Customization:

;;;###autoload
(defgroup fuel-debug nil
  "Major mode for interaction with the Factor debugger."
  :group 'fuel)

(defcustom fuel-debug-mode-hook nil
  "Hook run after `fuel-debug-mode' activates."
  :group 'fuel-debug
  :type 'hook)

(defcustom fuel-debug-confirm-restarts-p t
  "Whether to ask for confimation before executing a restart in
the debugger."
  :group 'fuel-debug
  :type 'boolean)

(defcustom fuel-debug-show-short-help t
  "Whether to show short help on available keys in debugger."
  :group 'fuel-debug
  :type 'boolean)

(defface fuel-font-lock-debug-error '((t (:inherit font-lock-warning-face)))
  "highlighting errors"
  :group 'fuel-debug)

(defface fuel-font-lock-debug-line
  '((t (:inherit font-lock-variable-name-face)))
  "line numbers in errors/warnings"
  :group 'fuel-debug)

(defface fuel-font-lock-debug-column
  '((t (:inherit font-lock-variable-name-face)))
  "column numbers in errors/warnings"
  :group 'fuel-debug)

(defface fuel-font-lock-debug-info '((t (:inherit font-lock-comment-face)))
  "information headers"
  :group 'fuel-debug)

(defface fuel-font-lock-debug-restart-number
  '((t (:inherit font-lock-warning-face)))
  "restart numbers"
  :group 'fuel-debug)

(defface fuel-font-lock-debug-restart-name
  '((t (:inherit font-lock-function-name-face)))
  "restart names"
  :group 'fuel-debug)

(defface fuel-font-lock-debug-missing-vocab
  '((t (:inherit font-lock-warning-face)))
  "missing vocabulary names"
  :group 'fuel-debug)

(defface fuel-font-lock-debug-unneeded-vocab
  '((t (:inherit font-lock-warning-face)))
  "unneeded vocabulary names"
  :group 'fuel-debug)


;;; Font lock and other pattern matching:

(defconst fuel-debug--compiler-info-alist
  '((":warnings" . ?w) (":errors" . ?e) (":linkage" . ?l)))

(defconst fuel-debug--error-file-regex "^P\" \\([^\"]+\\)\"")
(defconst fuel-debug--error-line-regex "\\([0-9]+\\):")
(defconst fuel-debug--error-cont-regex "^ +\\(\\^\\)$")

(defconst fuel-debug--error-regex
  (format "%s\n%s"
          fuel-debug--error-file-regex
          fuel-debug--error-line-regex))

(defconst fuel-debug--compiler-info-regex
  (format "^\\(%s\\) "
          (regexp-opt (mapcar 'car fuel-debug--compiler-info-alist))))

(defconst fuel-debug--restart-regex "^:\\([0-9]+\\) \\(.+\\)")

(defconst fuel-debug--font-lock-keywords
  `((,fuel-debug--error-file-regex . 'fuel-font-lock-debug-error)
    (,fuel-debug--error-line-regex 1 'fuel-font-lock-debug-line)
    (,fuel-debug--error-cont-regex 1 'fuel-font-lock-debug-column)
    (,fuel-debug--restart-regex (1 'fuel-font-lock-debug-restart-number)
                                (2 'fuel-font-lock-debug-restart-name))
    (,fuel-debug--compiler-info-regex 1 'fuel-font-lock-debug-restart-number)
    ("^\\(Restarts?\\|Loading\\) .+$" . 'fuel-font-lock-debug-info)
    ("^Error: " . 'fuel-font-lock-debug-error)))


;;; Debug buffer:

(defun fuel-debug--buffer ()
  (or (get-buffer "*fuel debug*")
      (with-current-buffer (get-buffer-create "*fuel debug*")
        (fuel-debug-mode)
        (fuel-popup-mode)
        (current-buffer))))

(defvar-local fuel-debug--last-ret nil)

(defvar-local fuel-debug--file nil)

(defvar-local fuel-debug--uses nil)

(defun fuel-debug--prepare-compilation (file msg)
  (let ((inhibit-read-only t))
    (with-current-buffer (fuel-debug--buffer)
      (erase-buffer)
      (insert msg)
      (setq fuel-debug--file file))))

(defun fuel-debug--display-retort (ret &optional success-msg no-pop)
  (let ((err (fuel-eval--retort-error ret))
        (inhibit-read-only t))
    (with-current-buffer (fuel-debug--buffer)
      (erase-buffer)
      (fuel-debug--display-output ret)
      (delete-blank-lines)
      (newline)
      (cond
       ((and (not err) success-msg)
        (message "%s" success-msg)
        (insert "\n" success-msg "\n"))
       ((eq (car err) 'fuel-con-error)
        (fuel-debug--display-parse-error (second err)))
       (err
        (fuel-debug--display-restarts err)
        (delete-blank-lines)
        (newline)))
      (fuel-debug--display-uses ret)
      (let ((hstr (fuel-debug--help-string err fuel-debug--file)))
        (if fuel-debug-show-short-help
            (insert "-----------\n" hstr "\n")
          (message "%s" hstr)))
      (setq fuel-debug--last-ret ret)
      (goto-char (point-max))
      (font-lock-fontify-buffer)
      (when (and err (not no-pop)) (fuel-popup--display))
      (not err))))

(defun fuel-debug--uses (ret)
  (let ((uses (fuel-eval--retort-result ret)))
    (and (eq :uses (car uses))
         (cdr uses))))

(defun fuel-debug--insert-vlist (title vlist)
  (goto-char (point-max))
  (insert title "\n\n  ")
  (let ((i 0) (step 5))
    (dolist (v vlist)
      (setq i (1+ i))
      (insert v)
      (insert (if (zerop (mod i step)) "\n  " " ")))
    (unless (zerop (mod i step)) (newline))
    (newline)))

(defun fuel-debug--highlight-names (names ref face)
  (dolist (n names)
    (when (not (member n ref))
      (put-text-property 0 (length n) 'font-lock-face face n))))

(defun fuel-debug--insert-uses (uses)
  (let* ((file (or file fuel-debug--file))
         (old (with-current-buffer (find-file-noselect file)
                (sort (factor-find-usings t) 'string<)))
         (new (sort uses 'string<)))
    (when (not (cl-equalp old new))
      (fuel-debug--highlight-names old new 'fuel-font-lock-debug-unneeded-vocab)
      (newline)
      (fuel-debug--insert-vlist "Correct vocabulary list:" new)
      new)))

(defun fuel-debug--display-uses (ret)
  (when (setq fuel-debug--uses (fuel-debug--uses ret))
    (newline)
    (fuel-debug--highlight-names fuel-debug--uses
                                 nil 'fuel-font-lock-debug-missing-vocab)
    (fuel-debug--insert-vlist "Missing vocabularies:" fuel-debug--uses)
    (newline)))

(defun fuel-debug--display-output (ret)
  "Diplays the retort `ret' in fuels debug buffer."
  (let* ((last (fuel-eval--retort-output fuel-debug--last-ret))
         (current (fuel-eval--retort-output ret))
         (llen (length last))
         (clen (length current))
         (trail (and last (substring-no-properties last (/ llen 2))))
         (err (fuel-eval--retort-error ret))
         (p (point)))
    (when current (save-excursion (insert current)))
    (when (and (> clen llen) (> llen 0) (search-forward trail nil t))
      (delete-region p (point)))
    (goto-char (point-max))
    (when err
      (insert (format "\nError: %S\n\n" (fuel-eval--error-name err))))))

(defun fuel-debug--display-parse-error (str)
  (insert
   (format
    "FUEL failed to parse the connection response, displayed below:\n\n%s\n\n" str)))

(defun fuel-debug--display-restarts (err)
  (let* ((rs (fuel-eval--error-restarts err))
         (rsn (length rs)))
    (when rs
      (insert "Restarts:\n\n")
      (dotimes (n rsn)
        (insert (format ":%s %s\n" (1+ n) (nth n rs))))
      (newline))))

(defun fuel-debug--help-string (err &optional file)
  (format "Press %s%s%s%sq bury buffer"
          (if (or file (fuel-eval--error-file err)) "g go to file, " "")
          (let ((rsn (length (fuel-eval--error-restarts err))))
            (cond ((zerop rsn) "")
                  ((= 1 rsn) "1 invoke restart, ")
                  (t (format "1-%s invoke restarts, " rsn))))
          (let ((str ""))
            (dolist (ci fuel-debug--compiler-info-alist str)
              (save-excursion
                (goto-char (point-min))
                (when (search-forward (car ci) nil t)
                  (setq str (format "%c %s, %s" (cdr ci) (car ci) str))))))
          (if fuel-debug--uses "u to update USING:, " "")))

(defun fuel-debug--buffer-file ()
  (with-current-buffer (fuel-debug--buffer)
    (or fuel-debug--file
        (and fuel-debug--last-ret
             (fuel-eval--error-file
              (fuel-eval--retort-error fuel-debug--last-ret))))))

(defsubst fuel-debug--buffer-error ()
  (fuel-eval--retort-error fuel-debug--last-ret))

(defsubst fuel-debug--buffer-restarts ()
  (fuel-eval--error-restarts (fuel-debug--buffer-error)))


;;; Buffer navigation:

(defun fuel-debug-goto-error ()
  (interactive)
  (let* ((err (fuel-debug--buffer-error))
         (file (or (fuel-debug--buffer-file)
                   (error "No file associated with compilation")))
         (l/c (and err (fuel-eval--error-line/column err)))
         (line (or (car l/c) 1))
         (col (or (cdr l/c) 0)))
    (find-file-other-window file)
    (when line
      (goto-char (point-min))
      (forward-line (1- line))
      (when col (forward-char col)))))

(defun fuel-debug--read-restart-no ()
  (let ((rs (fuel-debug--buffer-restarts)))
    (unless rs (error "No restarts available"))
    (let* ((rsn (length rs))
           (prompt (format "Restart number? (1-%s): " rsn))
           (no 0))
      (while (or (> (setq no (read-number prompt)) rsn)
                 (< no 1)))
      no)))

(defun fuel-debug-exec-restart (&optional n confirm)
  (interactive (list (fuel-debug--read-restart-no)))
  (let ((n (or n 1))
        (rs (fuel-debug--buffer-restarts)))
    (when (zerop (length rs))
      (error "No restarts available"))
    (when (or (< n 1) (> n (length rs)))
      (error "Restart %s not available" n))
    (when (or (not confirm)
              (y-or-n-p (format "Invoke restart %s? " n)))
      (message "Invoking restart %s" n)
      (let* ((file (fuel-debug--buffer-file))
             (buffer (if file (find-file-noselect file) (current-buffer))))
        (with-current-buffer buffer
          (fuel-debug--display-retort
           (fuel-eval--send/wait `(:fuel ((:factor ,(format ":%s" n)))))
           (format "Restart %s (%s) successful" n (nth (1- n) rs))))))))

(defun fuel-debug-show--compiler-info (info)
  (save-excursion
    (goto-char (point-min))
    (unless (re-search-forward (format "^%s" info) nil t)
      (error "%s information not available" info))
    (message "Retrieving %s info ..." info)
    (unless (fuel-debug--display-retort
             (fuel-eval--send/wait `(:fuel ((:factor ,info)))) "")
      (error "Sorry, no %s info available" info))))

(defun fuel-debug--replace-usings (file uses)
  (pop-to-buffer (find-file-noselect file))
  (goto-char (point-min))
  (if (re-search-forward "^USING: " nil t)
      (let ((begin (point))
            (end (or (and (re-search-forward ";\\( \\|$\\)") (point))
                     (point))))
        (kill-region begin end))
    (re-search-forward "^IN: " nil t)
    (beginning-of-line)
    (open-line 2)
    (insert "USING: "))
  (let ((start (point)))
    (insert (mapconcat 'substring-no-properties uses " ") " ;")
    (fill-region start (point) nil)))

(defun fuel-debug-update-usings ()
  (interactive)
  (when (and fuel-debug--file fuel-debug--uses)
    (let* ((file fuel-debug--file)
           (old (with-current-buffer (find-file-noselect file)
                  (factor-find-usings t)))
           (uses (sort (append fuel-debug--uses old) 'string<)))
      (fuel-popup--quit)
      (fuel-debug--replace-usings file uses))))


;;; Fuel Debug mode:

;;;###autoload
(define-derived-mode fuel-debug-mode fundamental-mode "FUEL Debug"
  "A major mode for displaying Factor's compilation results and
invoking restarts as needed.
\\{fuel-debug-mode-map}"
  (buffer-disable-undo)

  (suppress-keymap fuel-debug-mode-map)
  (dotimes (n 9)
    (define-key fuel-debug-mode-map (vector (+ ?1 n))
      `(lambda () (interactive)
         (fuel-debug-exec-restart ,(1+ n) fuel-debug-confirm-restarts-p))))
  (dolist (ci fuel-debug--compiler-info-alist)
    (define-key fuel-debug-mode-map (vector (cdr ci))
      `(lambda () (interactive) (fuel-debug-show--compiler-info ,(car ci)))))

  (setq font-lock-defaults
        '(fuel-debug--font-lock-keywords t nil nil nil)))

(fuel-menu--defmenu fuel-debug fuel-debug-mode-map
  ("Go to error" ("g" "\C-c\C-c") fuel-debug-goto-error)
  ("Next line" "n" next-line)
  ("Previous line" "p" previous-line)
  ("Update USINGs" "u" fuel-debug-update-usings))


(provide 'fuel-debug)

;;; fuel-debug.el ends here
