;;; ob-m4.el --- Babel Functions for m4 scripts    -*- lexical-binding: t; -*-

;; Copyright (C) 2015-2018 Free Software Foundation, Inc.

;; Author: Brad Knotwell
;; Keywords: literate programming, reproducible research
;; Version: 0.1.0

;; This file is part of GNU Emacs.

;; GNU Emacs 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.

;; GNU Emacs 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 GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; Provides a way to evaluate m4 scripts in Org mode.

;;; Usage:

;; Add to your Emacs config:

;; (org-babel-do-load-languages
;;  'org-babel-load-languages
;;  '((m4 . t)))

;; In addition to the normal header arguments, ob-m4 also provides
;; two specific options:
;;  :cmd-line -- allows a user to append arguments to the call
;;  :prefix-buildins -- calls with the -P option to automatically
;;       prefix all built-in macros with 'm4_'
;; Note:  it is a bad idea to explicitly pass the "-P" option in
;; :cmd-line to enable prefixing as any arguments specified via
;; :var NAME=value will be prefixed incorrectly.
;;
;; Explaining this with a bit more detail, all :var NAME=value arguments are
;; prepended to the body as definitions:
;;     define(NAME,value)
;;            or
;;     m4_define(NAME,value) iff :prefix-builtins is set
;;
;; Note:  the :prefix-builtin option is incompatible with Posix
;;        compliant m4 implementations.  This is fine for two reasons:
;;     * anyone using a Posix compliant utility is fine by default as
;;       the -P option is omitted
;;     * GNU's m4 implementation is (essentially) standard at this point
(require 'ob)

(defvar org-babel-m4-command "m4"
  "Name of the m4 executable command.")

(defvar org-babel-tangle-lang-exts)
(add-to-list 'org-babel-tangle-lang-exts '("m4" . "m4"))

(defconst org-babel-header-args:m4
  '((:cmd-line . :any)
    (:prefix-builtins))
  "M4 specific header arguments.")

(defvar org-babel-default-header-args:m4 '()
  "Default arguments for evaluating a m4 source block.")

;; build the passed-in macro definitions as a single string to prepend to the body
;; the hocus-pocus with :prefix-builtins is necessary for the -P option to work

(defun __org-babel-m4-prefix (params) (if (assq :prefix-builtins params) "m4_"))
(defun org-babel--variable-assignment:m4_generic (params varname values)
  (concat (__org-babel-m4-prefix params)
	  "define(" (format "%s" varname) "," (format "%s" values) ")"
	  (__org-babel-m4-prefix params)
	  "dnl\n"))

(defun org-babel--variable-assignment:m4_list (params varname values)
  (concat (__org-babel-m4-prefix params)
	  "define(" (format "%s,[" varname)
	  (mapconcat (lambda (value)
		       (if (= (length value) 1) (format "%s" (car value))
		       (concat "["
			       (mapconcat (lambda (x) (format "%s" x)) value ",")
			       "]"))) values ",")
	  "])" (__org-babel-m4-prefix params) "dnl\n"))

(defun org-babel--variable-assignments:m4 (params varnames values)
  "Represent the parameters as m4 definitions"
  (pcase values
    (`(,_ . ,_) (org-babel--variable-assignment:m4_list params varnames values))
    (_ (org-babel--variable-assignment:m4_generic params varnames values))))

(defun org-babel-variable-assignments:m4 (params)
  (apply 'concat (mapcar (lambda (pair) (org-babel--variable-assignments:m4
					 params (car pair) (cdr pair)))
			 (org-babel--get-vars params))))

;; required to make tangling work
;; the final "\n" is required to make m4 work as the body doesn't end in a newline
(defun org-babel-expand-body:m4 (body params)
  "Expand BODY according to PARAMS, return the expanded body."
  (concat (org-babel-variable-assignments:m4 params) body "\n"))

(defun org-babel-execute:m4 (body params)
  "Execute a block of m4 code with Org Babel.
BODY is the source inside a m4 source block and PARAMS is an
association list over the source block configurations.  This
function is called by `org-babel-execute-src-block'."
  (message "executing m4 source code block")
  (let* ((result-params (cdr (assq :result-params params)))
         (cmd-line (cdr (assq :cmd-line params)))
	 (prefix-builtins (assq :prefix-builtins params))
	 (code-file (let ((file (org-babel-temp-file "m4-")))
                      (with-temp-file file
			(insert (org-babel-expand-body:m4 body params) file)) file))
	 (stdin (let ((stdin (cdr (assq :stdin params))))
		   (when stdin
		     (let ((tmp (org-babel-temp-file "m4-stdin-"))
			   (res (org-babel-ref-resolve stdin)))
		       (with-temp-file tmp
			 (insert res))
		       tmp))))
         (cmd (mapconcat #'identity
			 (remq nil
			       (list org-babel-m4-command
				     cmd-line (if prefix-builtins "-P") "<" code-file))
			 " ")))
    (org-babel-reassemble-table
     (let ((results
            (cond
             (stdin (with-temp-buffer
                      (call-process-shell-command cmd stdin (current-buffer))
                      (buffer-string)))
             (t (org-babel-eval cmd "")))))
       (when results
         (org-babel-result-cond result-params
	   results
	   (let ((tmp (org-babel-temp-file "m4-results-")))
	     (with-temp-file tmp (insert results))
	     (org-babel-import-elisp-from-file tmp)))))
     (org-babel-pick-name
      (cdr (assq :colname-names params)) (cdr (assq :colnames params)))
     (org-babel-pick-name
      (cdr (assq :rowname-names params)) (cdr (assq :rownames params))))))

(provide 'ob-m4)
;;; ob-m4.el ends here

;; Test examples below:
;;
;;
;; #+begin_src m4 :results output :var XXXX='hello' :var YYYY='world'
;; define(this1, one will expand)dnl
;; m4_define(this2,one will not expand)m4_dnl
;; this1
;; this2
;; XXXX
;; YYYY
;; ZZZZ
;; #+end_src

;; #+RESULTS:
;; : m4_define(this2,one will not expand)m4_dnl
;; : one will expand
;; : this2
;; : hello
;; : world
;; : ZZZZ

;; #+begin_src m4 :prefix-builtins :results output :var XXXX='hello' :var YYYY='world'
;; define(this1, one will not expand)dnl
;; m4_define(this2,one will expand)m4_dnl
;; this1
;; this2
;; XXXX
;; YYYY
;; ZZZZ
;; #+end_src

;; #+RESULTS:
;; : define(this1, one will not expand)dnl
;; : this1
;; : one will expand
;; : hello
;; : world
;; : ZZZZ
