;;; alchemist-report.el --- Run command in a process and handles buffer of it

;; Copyright © 2015 Samuel Tonini

;; Author: Samuel Tonini <tonini.samuel@gmail.com

;; This file is not part of GNU Emacs.

;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:

;; Run command in a process and handles buffer output and display

;;; Code:

(require 'ansi-color)
(require 'alchemist-project)

(defgroup alchemist-report nil
  "Run command in a process and handles buffer output and display"
  :prefix "alchemist-report-"
  :group 'alchemist)

(defvar alchemist-report-on-exit nil)
(defvar alchemist-report-on-exit-function nil)
(defvar alchemist-report-on-render nil)
(defvar alchemist-report-on-render-function nil)
(defvar alchemist-report--last-run-status nil)
(defvar alchemist-report-mode-name nil)

(defun alchemist-report--kill-process (process)
  "Interrupt and kill the running report PROCESS."
  (when process
    (let ((mode-name (replace-regexp-in-string ":.+$" "" mode-name)))
      (if (or (not (eq (process-status process) 'run))
              (eq (process-query-on-exit-flag process) nil)
              (yes-or-no-p
               (format "A %s process already running; kill it? "
                       mode-name)))
          (condition-case ()
              (progn
                (interrupt-process process)
                (sit-for 1)
                (delete-process process))
            (error nil))
        (error "Cannot have two processes in `%s' at once"
               (buffer-name))))))

(defun alchemist-report--sentinel (process status)
  "Sentinel for test report buffer."
  (if (memq (process-status process) '(exit signal))
      (let ((buffer (process-buffer process)))
        (if (null (buffer-name buffer))
            (set-process-buffer process nil)
          (progn
            (alchemist-report--render-report buffer)
            (alchemist-report--handle-exit status buffer)
            (alchemist-report-update-mode-name process)
            (delete-process process))))))

(defun alchemist-report--render-report (buffer)
  "Call the defined render functions for the BUFFER."
  (when alchemist-report-on-render-function
    (funcall alchemist-report-on-render-function buffer)))

(defun alchemist-report--handle-exit (status buffer)
  "Call the defined exit function specified in `alchemist-report-on-exit-function'.
Argument for the exit function is the STATUS and BUFFER of the finished process."
  (alchemist-report--store-process-status status)
  (when alchemist-report-on-exit-function
    (funcall alchemist-report-on-exit-function status buffer)))

(defun alchemist-report--store-process-status (status)
  "Store STATUS of the last finished process."
  (setq alchemist-report--last-run-status status))

(defun alchemist-report--last-run-successful-p ()
  "Return non-nil if the last process successfully finished."
  (when (string-prefix-p "finished" alchemist-report--last-run-status) t))

(defun alchemist-report-filter (process output)
  "Process filter for report buffers."
  (with-current-buffer (process-buffer process)
    (let* ((buffer-read-only nil)
           (output (if (string= (process-name process) alchemist-test-report-process-name)
                       (alchemist-test-clean-compilation-output output)
                     output))
           (moving (= (point) (process-mark process))))
      (save-excursion
        (goto-char (process-mark process))
        (insert output)
        (set-marker (process-mark process) (point))
        (ansi-color-apply-on-region (point-min) (point-max)))
      (if moving (goto-char (process-mark process))))))

(defun alchemist-report-update-mode-name (process)
  "Update the `mode-name' with the status of PROCESS."
  (with-current-buffer (process-buffer process)
    (setq-local mode-name (format "%s:%s"
                                  (replace-regexp-in-string ":.+$" "" mode-name)
                                  (process-status process)))))

(defun alchemist-report-interrupt-current-process ()
  "Interrupt the current running report process."
  (interactive)
  (let ((buffer (current-buffer))
        (name (replace-regexp-in-string ":.+" "" mode-name)))
    (if (get-buffer-process buffer)
        (interrupt-process (get-buffer-process buffer))
      (error "The [%s] process is not running" (downcase name)))))

(defun alchemist-report-cleanup-process-buffer (buffer)
  "Clean the content BUFFER of process.
If there is already a running process, ask for interrupting it."
  (with-current-buffer buffer
    (let ((inhibit-read-only t)
          (process (get-buffer-process buffer)))
      (erase-buffer))))

(defun alchemist-report-display-buffer (buffer)
  "Display the BUFFER."
  (display-buffer buffer))

(defun alchemist-report-activate-mode (mode buffer)
  "Enable MODE inside BUFFER."
  (with-current-buffer buffer
    (funcall mode)
    (setq-local truncate-lines t) ;; Do not display continuation lines.
    (setq-local window-point-insertion-type t)))

(defun alchemist-report-run (command process-name buffer-name mode &optional on-exit hidden)
  "Run COMMAND in a new process called PROCESS-NAME.
The output of PROCESS-NAME will be displayed in BUFFER-NAME.
After displaying BUFFER-NAME, the MODE function will be called within.

Optional ON-EXIT and HIDDEN functions could be defined.
The function ON-EXIT will be called when PROCESS-NAME is finished.
The HIDDEN variable defines if PROCESS-NAME should run in the background."
  (let* ((buffer (get-buffer-create buffer-name))
         (default-directory (alchemist-project-root-or-default-dir)))
    (alchemist-report-cleanup-process-buffer buffer)
    (alchemist-report--kill-process (get-buffer-process buffer))
    (start-process-shell-command process-name buffer command)
    (when on-exit
      (setq alchemist-report-on-exit-function on-exit))
    (set-process-sentinel (get-buffer-process buffer) 'alchemist-report--sentinel)
    (set-process-filter (get-buffer-process buffer) 'alchemist-report-filter)
    (alchemist-report-activate-mode mode buffer)
    (if (not hidden)
        (alchemist-report-display-buffer buffer))
    (alchemist-report-update-mode-name (get-buffer-process buffer))))

(provide 'alchemist-report)

;;; alchemist-report.el ends here
