#!/bin/sh
":"; exec emacs --quick --script "$0" -- "$@" # -*- mode: emacs-lisp; lexical-binding: t; -*-
(pop argv)

;; TODO auto open browser
;; TODO auto refresh browser? (with localserver & iframe)
;; TODO remove id names?
;; TODO space at japanese-english boundary (e.g. "あaあ" => "あ a あ", "、 a" => "、a", "あ(aい)あ" => "あ (aい) あ")
;; TODO add convenient macros (--nice-macros)
;; TODO read file names from stdin (-), or read file contents from stdin (--stdin)

;; Note on the shebang (and the next 2 lines):
;; https://gist.github.com/ctarbide/99b0ac9f7d6bef19cdd3e9f71b4cbcf7

(require 'cl-lib)

(defvar org-html-cli-the-file nil "File being exported.")

(defun configure-default-org ()
  (mapc 'require '(org ox ox-html))

  ;; options
  (setq org-export-time-stamp-file      nil
        org-export-with-section-numbers nil
        org-export-with-author          nil
        org-export-with-date            t
        org-export-with-title           t
        org-export-with-toc             nil
        org-export-with-broken-links    t

        ;; html5
        org-html-html5-fancy            t
        org-html-doctype                "html5"

        ;; my-org-html-body-only               t
        org-html-head-include-default-style nil
        org-html-head-include-scripts       nil
        org-html-htmlize-output-type    nil
        org-html-validation-link        nil

        )

  ;; no babel eval confirmation
  (setq org-confirm-babel-evaluate nil)

  ;; default title to be buffer name (with extension removed)
  (add-to-list 'org-export-filter-options-functions
               '(lambda (pl _)
                  (unless (plist-get pl :title)
                    (let* ((bufname (plist-get pl :input-buffer))
                           (title   (replace-regexp-in-string "\\.org$" "" bufname)))
                      (plist-put pl :title title)))
                  pl))

  )


;; No backup
(setq make-backup-files nil)


;; Helpers

(defun wait-file-change (files)
  (let ((echo-files-command (concat
                             "echo -e '"
                             (mapconcat (lambda (file) file) files "\\n")
                             "'"
                             )))
    ;; For some reason "{ echo x; echo y; ... } | entr" does not work correctly (it does not wait)
    ;; So combine to single echo command
    (shell-command (format "%s | entr -npz true" echo-files-command) "*scratch*")))

(defun do-export (file embed-mode)
  (setq org-html-cli-the-file file)
  (if embed-mode
      (do-export-embed file)
    (find-file file)
    (org-html-export-to-html nil nil nil (bound-and-true-p my-org-html-body-only))
    (kill-buffer)))

(defun do-export-embed (file)
  (cl-labels ((get-input-alist
               (file)
               ;; If file content is:

               ;;   * not org
               ;;   BEGIN_ORG
               ;;   * yes org
               ;;   END_ORG
               ;;   * not org

               ;; then get-input-alist returns:

               ;;   '((nil . "* not org\n")
               ;;     (org . "* yes org\n")
               ;;     (nil . "* not org\n"))

               (let ((l     nil)
                     (org   nil)
                     (p     (buffer-end -1))
                     (break nil))
                 (find-file file)
                 (goto-char p)
                 (while (and (not break) (search-forward "BEGIN_ORG" nil t))
                   (let ((p1 (line-beginning-position))
                         (p2 (line-beginning-position 2)))
                     (if (search-forward "END_ORG" nil t)
                         (let ((p3 (line-beginning-position))
                               (p4 (line-beginning-position 2)))
                           (push `(nil . ,(buffer-substring p  p1)) l)
                           (push `(org . ,(buffer-substring p2 p3)) l)
                           (setq p p4))
                       (setq break t))))
                 (push `(nil . ,(buffer-substring p (buffer-end 1))) l)
                 (kill-buffer)
                 (nreverse l)))
              (org-to-html
               (str)
               (with-temp-buffer
                 (insert str)
                 (org-html-export-as-html nil nil nil t)
                 (switch-to-buffer "*Org HTML Export*")
                 (buffer-string)))
              (write-to-file
               (file str)
               (with-temp-file file
                 (insert str))))
    (let* ((input-list    (get-input-alist file))
           (output-list   (mapcar (lambda (pair)
                                    (if (eq (car pair) 'org)
                                        (org-to-html (cdr pair))
                                      (cdr pair)))
                                  input-list))
           (output-string (apply 'concat output-list))
           (output-file   (if (string-suffix-p ".html" file)
                              (format "%s.html" file)
                            (replace-regexp-in-string "\\.[^.]*$" ".html" file))))
      ;; (print input-list) (print output-file)
      (write-to-file output-file output-string))))

(defun error-abort (format-string &rest args)
  (apply 'message (concat "ERROR: " format-string) args)
  (print-help)
  (kill-emacs 1))

(defun parse-args (args opts-without-value &optional opts-with-value)
  ;; TODO: long opt as alias like: opts-with-value = '((v version) (q quiet))
  "Parse command-line arguments and return an alist.
Examples:
(parse-args (split-string \"-x --longopt\")
            '(x longopt))
  ==> ((x . t) (longopt . t))

(parse-args (split-string \"-x -v val\")
            '(x) '(v))
  ==> ((x . t) (v . \"val\"))

(parse-args (split-string \"-xyzv val\")
           '(x y z) '(v))
  ==> ((x . t) (y . t) (z . t) (v . \"val\"))

(parse-args (split-string \"-v val hello world\")
            nil '(v))
  ==> ((v . \"val\") (0 . \"hello\") (1 . \"world\"))"

  (let ((result nil)
        (i      0)) ;; index of unnamed argument

    (while args
      (let* ((a       (pop args))
             (optname (cond
                       ((string-prefix-p "--" a) (intern (substring a 2)))
                       ((string-prefix-p "-" a)  (progn
                                                   ;; add "a" and "b" in "-abc"
                                                   (dolist (c (string-to-list (substring a 1 -1)))
                                                     (let ((optname (intern (string c))))
                                                       (unless (memq optname opts-without-value)
                                                         (error-abort "unknown option: -%s" optname))
                                                       (push (cons optname t)
                                                             result)))
                                                   ;; then return "c" (as symbol)
                                                   (intern (substring a -1))))
                       (t                        nil)))
             (pair (cond
                    ;; unnamed argument
                    ((not optname)                  (prog1 (cons i a) (setq i (1+ i))))
                    ;; option that must have an argument
                    ((memq optname opts-with-value) (if (and (car args)
                                                             (not (string-prefix-p "-" (car args))))
                                                        (cons optname (pop args))
                                                      (error-abort "-%s requires an argument" optname)))
                    ;; option that must not have an argument
                    (t                              (if (memq optname opts-without-value)
                                                        (cons optname t)
                                                      (error-abort "unknown option: -%s" optname))))))
        (push pair result)))

    (reverse result)))

(defun print-help ()
  (message "Usage: ./org-html.el [-hmqw] [-e LISPCODE] [-l LISPFILE] <file> ...
Options:
  -h            Help.
  -e LISPCODE   Eval LISPCODE before exporting.
  -l LISPFILE   Load LISPFILE before exporting.
  -m            Embedded mode: only parse lines between BEGIN_ORG and END_ORG.
  -q            Quiet.
  -w            Watch file changes (using \"entr\").
Note:
  1. Within -e and -c, currently processed file's name can be referenced as \"org-html-cli-the-file\""))

(defun nop (&rest args))

;; Main

(let* ((argc           (length argv))
       (arg-alist      (parse-args argv '(h m q w) '(c e)))
       (arg-help       (alist-get 'h arg-alist))
       (arg-embed-mode (alist-get 'm arg-alist))
       (arg-quiet      (alist-get 'q arg-alist))
       (arg-watch      (alist-get 'w arg-alist))
       (lisp-file      (alist-get 'l arg-alist))
       (lisp-code      (alist-get 'e arg-alist))
       (input-files    (cl-loop for (i . file) in arg-alist if (integerp i) collect file))
       (export         (lambda ()
                         (dolist (f input-files) (do-export f arg-embed-mode))
                         (message "Done."))))

  ;; (message "config-file:  %s\ninput-files:  %s\nquiet: %s" config-file input-files arg-quiet)

  (when (or (= 0 argc) arg-help)
    (print-help)
    (kill-emacs))

  (when arg-quiet (advice-add 'message :override 'nop))

  (configure-default-org)

  (when lisp-file (load (expand-file-name lisp-file)))
  (when lisp-code (eval (car (read-from-string (concat "(progn\n" lisp-code "\n)")))))

  (if arg-watch
      (if (executable-find "entr")
          (while t
            (funcall export)
            (wait-file-change input-files))
        (message "\"entr\" command is not available!")
        (kill-emacs 1))
    (funcall export)
    (kill-emacs)))

;; vim: ft=lisp
