;;; flex-compiler.el --- User interactive interface to compilers  -*- lexical-binding: t; -*-

;; Copyright (C) 2015 - 2025 Paul Landes

;; Author: Paul Landes
;; Maintainer: Paul Landes
;; Keywords: compilation integration processes
;; URL: https://github.com/plandes/flex-compile
;; Package-Requires: ((emacs "26") (dash "2.13.0") (buffer-manage "0.8"))

;; 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 2, 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, write to the Free Software
;; Foundation, Inc., 51 Franklin Street, Fifth Floor,
;; Boston, MA 02110-1301, USA.

;;; Commentary:

;; Run, evaluate and compile functionality for a variety of different languages
;; and modes.  The specific "compilation" method is different across each
;; add-on library.  For example, for ESS and Clojure you can evaluate a
;; specific file and/or evaluate a specfic expression via a REPL.  For running
;; a script or starting a `make' an async process is started.
;;
;; For more information see https://github.com/plandes/flex-compile

;;; Code:

(require 'cl-lib)
(require 'flex-compile-manage)

(defvar flex-compiler-read-history nil
  "History variable for `flex-compiler-read'.")

(defun flex-compiler-by-name (name)
  "Convenience function to get a compiler by it's NAME."
  (config-manager-entry flex-compile-manage-inst name))

(defun flex-compiler-active ()
  "Return the currently activated compiler."
  (flex-compile-manager-active flex-compile-manage-inst))

(defun flex-compiler-read (last-compiler-p)
  "Read a flexible compiler to use.

LAST-COMPILER-P, if non-nil, use the last chosen compiler."
  (or (if last-compiler-p
	  (let ((arg (cl-second flex-compiler-read-history)))
	    (setq flex-compiler-read-history
		  (append (cons arg nil)
			  flex-compiler-read-history))
	    arg))
      (let* ((this flex-compile-manage-inst)
	     (names (config-manager-entry-names this)))
	(choice-program-complete
	 "Compiler" names t t nil
	 'flex-compiler-read-history
	 (cl-second flex-compiler-read-history)
	 nil t t))))

(defun flex-compiler-manage-read-form (no-input-p)
  "Read the compilation query form from the user.

If NO-INPUT-P is t, use the default witout getting it from the user.

This invokes the `flex-compiler-query-read-form method' on the
currently activated compiler."
  (let* ((mgr flex-compile-manage-inst)
	 (this (flex-compile-manager-active mgr)))
    (if (not (child-of-class-p (eieio-object-class this)
			       'repl-flex-compiler))
	(error "Compiler `%s' has no ability evaluation expressions"
	       (config-entry-name this))
      (flex-compile-manager-assert-ready mgr)
      (flex-compiler-query-read-form this no-input-p))))

;;;###autoload
(defun flex-compiler-config-save ()
  "Save all compiler and manager configuration."
  (interactive)
  (config-persistable-save flex-compile-manage-inst))

;;;###autoload
(defun flex-compiler-config-load ()
  "Load all compiler and manager configuration."
  (interactive)
  (condition-case err
      (config-persistable-load flex-compile-manage-inst)
    (invalid-slot-name
     (error "Could not unpersist: %S, repair or delete %s"
	    err (slot-value flex-compile-manage-inst 'file)))))

;;;###autoload
(defun flex-compiler-list ()
  "Display the flex compiler list."
  (interactive)
  (let ((this flex-compile-manage-inst))
    (->> (format "*%s*" (capitalize (config-manager-name this)))
	 (config-manager-list-entries-buffer this))))

;;;###autoload
(defun flex-compiler-reset-configuration ()
  "Reset every compiler's configuration."
  (interactive)
  (when (y-or-n-p "This will wipe all compiler configuration.  Are you sure? ")
    (if (file-exists-p flex-compile-manage-persistency-file-name)
	(delete-file flex-compile-manage-persistency-file-name))
    (flex-compile-clear flex-compile-manage-inst)
    (flex-compiler-config-save)
    (message "Configuration reset")))

;;;###autoload
(defun flex-compiler-doc-show ()
  "Create markdown documentation on all compilers and their meta data."
  (interactive)
  (config-persistent-doc flex-compile-manage-inst))

;;;###autoload
(defun flex-compiler-show-configuration ()
  "Create a buffer with the configuration of the current compiler."
  (interactive)
  (let* ((this flex-compile-manage-inst)
	 (active (flex-compile-manager-active this))
	 (buf (config-prop-entry-show-configuration active))
	 (window-config (current-window-configuration)))
    (with-current-buffer buf
      (and (fboundp 'markdown-mode) (markdown-mode))
      ;; add a quit key to only this buffer
      (let ((local-map (make-sparse-keymap)))
	(set-keymap-parent local-map (current-local-map))
	(define-key local-map (kbd "q")
		    (lambda ()
		      (interactive)
		      (kill-buffer (current-buffer))
		      (set-window-configuration window-config)))
	(use-local-map local-map))
      ;; massage the configuration text into markdown
      (goto-char (point-min))
      (delete-region (line-beginning-position) (line-beginning-position 2))
      (config-persistent-doc active 1)
      (insert "\n\n# Configuration\n\n")
      (while (re-search-forward "^\\([^:]+\\): \\(.*\\)$" nil t)
	(replace-match "* `\\1`: \\2"))
      (insert "\n\n**Press q to quit**")
      (setq buffer-read-only t))
    (set-window-configuration window-config)
    (pop-to-buffer buf)))

;;;###autoload
(defun flex-compiler-do-activate (compiler-name)
  "Activate/select a compiler.

COMPILER-NAME the name of the compiler to activate."
  (interactive
   (list (if (equal current-prefix-arg 1)
	     (progn (flex-compiler-show-configuration) nil)
	   (flex-compiler-read current-prefix-arg))))
  (when compiler-name
    (let ((this flex-compile-manage-inst))
      (config-manager-activate this compiler-name))))

;;;###autoload
(defun flex-compiler-do-compile (config-options)
  "Invoke compilation polymorphically.

CONFIG-OPTIONS, if non-nil invoke the configuration options for the compiler
before invoking the compilation.  By default CONFIG-OPTIONS is only detected
config-options by the \\[universal-argument] but some compilers use the numeric
argument as well.  This creates the need for an awkward key combination of:

  \\[digital-argument] \\[universal-argument] \\[flex-compiler-compile]

to invoke this command with full configuration support."
  (interactive
   (list (cond ((null current-prefix-arg) 'compile)
	       ;; universal arg
	       ((equal '(4) current-prefix-arg) nil)
	       (t (1- current-prefix-arg)))))
  (let* ((this flex-compile-manage-inst)
	 (active (flex-compile-manager-active this)))
    (flex-compile-manager-assert-ready this)
    (if (eq config-options 'compile)
	(let (comp-def)
	  (let ((display-buffer-alist
		 (flex-compiler-display-buffer-alist active)))
	    (setq comp-def (flex-compiler-compile active)))
	  (flex-compiler-display-buffer active comp-def))
      (condition-case err
	  (config-prop-entry-configure active config-options)
	(cl-no-applicable-method
	 (message "Compiler %s is not configurable: no method: %S"
		  (config-entry-name active)
		  (cl-second err)))))))

;;;###autoload
(defun flex-compiler-do-run-or-set-config (action)
  "This either invokes the `run' compilation functionality or it configures it.

ACTION is the interactive argument given by the read function."
  (interactive
   (list (cond ((null current-prefix-arg) 'run)
	       ((eq 2 current-prefix-arg) 'force-show)
	       ;; universal arg
	       ((equal '(4) current-prefix-arg) 'find)
	       ((eq 1 current-prefix-arg) 'set-config)
	       (t (error "Unknown prefix state: %s" current-prefix-arg)))))
  (let* ((this flex-compile-manage-inst)
	 (active (flex-compile-manager-active this)))
    (flex-compile-manager-assert-ready this)
    (condition-case err
	(cl-case action
	  (run (let (compile-def)
		 (let ((display-buffer-alist
			(flex-compiler-display-buffer-alist active)))
		   (setq compile-def (flex-compiler-run active)))
		 (flex-compiler-display-buffer active compile-def)))
	  (force-show (let (compile-def)
			(let ((display-buffer-alist
			       (flex-compiler-display-buffer-alist active)))
			  (setq compile-def (append (flex-compiler-run active)
						    '((force-show . t)))))
		 (flex-compiler-display-buffer active compile-def)))
	  (find (flex-compiler-find active))
	  (set-config (config-prop-entry-configure active 'immediate))
	  (t (error "Unknown action: %S" action)))
      (cl-no-applicable-method
       (message "Unsupported action `%S' on compiler %s: no method %S"
		action
		(config-entry-name active)
		(cl-second err))))))

;;;###autoload
(defun flex-compiler-do-eval (&optional form)
  "Evaluate the current form for the \(usually REPL based compiler).
FORM is the form to evaluate \(if implemented).  If called with
\\[universal-argument] then prompt the user with the from to evaluation."
  (interactive (list (flex-compiler-manage-read-form (not current-prefix-arg))))
  (let* ((mgr flex-compile-manage-inst)
	 (this (flex-compile-manager-active mgr)))
    (flex-compile-manager-assert-ready mgr)
    (let ((res (flex-compiler-evaluate-form this form)))
      (when (and res (called-interactively-p 'interactive))
	(kill-new res)
	(message "%s" res))
      res)))

;;;###autoload
(defun flex-compiler-do-clean (allp)
  "Invoke the clean functionality of the compiler.
if ALLP is non-nil, then invoke a more destructive cleaning when supported."
  (interactive "P")
  (let* ((this flex-compile-manage-inst)
	 (active (flex-compile-manager-active this)))
    (condition-case nil
	(progn
	  (flex-compile-manager-assert-ready this)
	  (let (compile-def)
	    (let ((display-buffer-alist
		   (flex-compiler-display-buffer-alist active)))
	      (setq compile-def (flex-compiler-clean active allp)))
	    (flex-compiler-display-buffer active compile-def)))
      (cl-no-applicable-method
       (message "Compiler `%s' has no ability to clean"
		(config-entry-name active))))))

(defmacro with-flex-compiler (spec &rest body)
  "Eval BODY forms like `progn' and set `this' to a flex compiler.
During the evaluation of BODY the specified compiler is activated.

SPEC is either the compiler string or a form (<compiler name> <options>).
The options, if given, is a plist form of the following:
    `:slots': a list of slots to using `with-slots'
    `:activate': leave the compiler activated afterward

Functions `compile', `run' and `clean' are mapped to (flex-compiler-* this)
respectively to provide convenience to mapping.

Examples:

  Return the `config-file' slot value from the `script-flex-compiler' compiler:
    (with-flex-compiler (\"script\" :slots (config-file))
      config-file)

  Configure the compiler and leave it activated:
    (with-flex-compiler (\"script\" :slots (config-file) :activate t)
      (setq config-file \"~/test.sh\"))

  Run `script-flex-compiler' on a script in a path, then restore configuration:
    (with-flex-compiler (\"script\")
      (cl-letf (((oref this :config-file) \"~/test.sh\")
		((oref this :start-directory) \"~/dir1\"))
	(compile)))"
  (let* ((config
	  (cond ((consp spec) (cons (car spec) (cdr spec)))
		((stringp spec) (cons spec nil))
		(t (-> "Expecting: <compiler> or (<compiler> (option plist))"
		       error))))
	 (compiler-name (car config))
	 (opts (cdr config))
	 (slots (plist-get opts :slots))
	 (activatep (plist-get opts :activate))
	 (mng flex-compile-manage-inst)
	 (org-active (flex-compile-manager-active mng)))
    `(unwind-protect
	 (let ((this (config-manager-activate ,mng ,compiler-name)))
	   (cl-labels ((compile () (flex-compiler-compile this))
		       (run () (flex-compiler-run this))
		       (clean () (flex-compiler-run this)))
	     (with-slots ,slots this
	       (progn ,@body))))
       (unless ,activatep
	 (config-manager-activate ,mng ,org-active)))))

(put 'with-flex-compiler 'lisp-indent-function 1)

(provide 'flex-compiler)

;;; flex-compiler.el ends here
