;;; log4e.el --- provide logging framework for elisp

;; Copyright (C) 2013  Hiroaki Otsu

;; Author: Hiroaki Otsu <ootsuhiroaki@gmail.com>
;; Keywords: log
;; URL: https://github.com/aki2o/log4e
;; Version: 0.4.1

;; 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 file 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:
;;
;; This extension provides logging framework for elisp.
;;
;; For detail, see <https://github.com/aki2o/log4e/blob/master/README.md>
;;
;; Enjoy!!!

;;; Code:

(require 'cl-lib)
(require 'rx)


(defconst log4e-log-level-alist '((fatal . 6)
                                  (error . 5)
                                  (warn  . 4)
                                  (info  . 3)
                                  (debug . 2)
                                  (trace . 1))
  "Alist of log level value.")

(defconst log4e-default-logging-function-name-alist '((fatal . "log-fatal")
                                                      (error . "log-error")
                                                      (warn  . "log-warn")
                                                      (info  . "log-info")
                                                      (debug . "log-debug")
                                                      (trace . "log-trace"))
  "Alist of logging function name at default.")


(defmacro log4e--def-symmaker (symnm)
  `(progn
     (defsubst ,(intern (concat "log4e--make-symbol-" symnm)) (prefix)
       (intern (concat ,(format "log4e--%s-" symnm) prefix)))))

(log4e--def-symmaker "log-buffer")
(log4e--def-symmaker "msg-buffer")
(log4e--def-symmaker "log-template")
(log4e--def-symmaker "time-template")
(log4e--def-symmaker "min-level")
(log4e--def-symmaker "max-level")
(log4e--def-symmaker "toggle-logging")
(log4e--def-symmaker "toggle-debugging")
(log4e--def-symmaker "buffer-coding-system")
(log4e--def-symmaker "author-mail-address")

(defmacro log4e--def-level-logger (prefix suffix level)
  (let ((argform (if suffix
                     '(msg &rest msgargs)
                   '(level msg &rest msgargs)))
        (buff (log4e--make-symbol-log-buffer prefix))
        (msgbuff (log4e--make-symbol-msg-buffer prefix))
        (codsys (log4e--make-symbol-buffer-coding-system prefix))
        (logtmpl (log4e--make-symbol-log-template prefix))
        (timetmpl (log4e--make-symbol-time-template prefix))
        (minlvl (log4e--make-symbol-min-level prefix))
        (maxlvl (log4e--make-symbol-max-level prefix))
        (logging-p (log4e--make-symbol-toggle-logging prefix)))
    `(progn

       ;; Define logging function
       (defun ,(intern (concat prefix "--" (or suffix "log"))) ,argform
         ,(format "Do logging for %s level log.
%sMSG/MSGARGS are passed to `format'."
                  (if suffix level "any")
                  (if suffix "" "LEVEL is symbol that is one of trace debug info warn error fatal.\n"))
         (let ((log4e--current-msg-buffer ,msgbuff))
           ,(if suffix
                `(apply 'log4e--logging ,buff ,codsys ,logtmpl ,timetmpl ,minlvl ,maxlvl ,logging-p ',level msg msgargs)
              `(apply 'log4e--logging ,buff ,codsys ,logtmpl ,timetmpl ,minlvl ,maxlvl ,logging-p level msg msgargs))))

       ;; Define logging macro
       (defmacro ,(intern (concat prefix "--" (or suffix "log") "*")) ,argform
         ,(format "Do logging for %s level log.
%sMSG/MSGARGS are passed to `format'.
Evaluation of MSGARGS is invoked only if %s level log should be printed."
                  (if suffix level "any")
                  (if suffix "" "LEVEL is symbol that is one of trace debug info warn error fatal.\n")
                  (if suffix level "the"))
         (let (,@(if suffix (list `(level ',level)) '())
               (buff (log4e--make-symbol-log-buffer ,prefix))
               (msgbuff (log4e--make-symbol-msg-buffer ,prefix))
               (codsys (log4e--make-symbol-buffer-coding-system ,prefix))
               (logtmpl (log4e--make-symbol-log-template ,prefix))
               (timetmpl (log4e--make-symbol-time-template ,prefix))
               (minlvl (log4e--make-symbol-min-level ,prefix))
               (maxlvl (log4e--make-symbol-max-level ,prefix))
               (logging-p (log4e--make-symbol-toggle-logging ,prefix)))
           `(let ((log4e--current-msg-buffer ,msgbuff))
              (when (and ,logging-p
                         (log4e--logging-level-p ,minlvl ,maxlvl ',level))
                (log4e--logging ,buff ,codsys ,logtmpl ,timetmpl ,minlvl ,maxlvl ,logging-p ',level ,msg ,@msgargs)))))
       
       )))

(defsubst log4e--logging-level-p (minlevel maxlevel currlevel)
  (let ((minlvlvalue (or (assoc-default minlevel log4e-log-level-alist)
                         1))
        (maxlvlvalue (or (assoc-default maxlevel log4e-log-level-alist)
                         6))
        (currlvlvalue (or (assoc-default currlevel log4e-log-level-alist)
                          0)))
    (and (>= currlvlvalue minlvlvalue)
         (<= currlvlvalue maxlvlvalue))))

(defsubst log4e--get-or-create-log-buffer (buffnm &optional codesys)
  (or (get-buffer buffnm)
      (let ((buff (get-buffer-create buffnm)))
        (with-current-buffer buff
          (log4e-mode)
          (when codesys
            (setq buffer-file-coding-system codesys)))
        buff)))

(defvar log4e--regexp-msg-format
  (rx-to-string `(and "%"
                      (* (any "+#-0"))        ; flags
                      (* (any "0-9"))         ; width
                      (? "." (+ (any "0-9"))) ; precision
                      (any "a-zA-Z"))))

(defsubst log4e--insert-log (logtmpl timetmpl level msg msgargs propertize-p)
  (let ((timetext (format-time-string timetmpl))
        (lvltext (format "%-05s" (upcase (symbol-name level))))
        (buffer-read-only nil))
    (when propertize-p
      (put-text-property 0 (length timetext) 'face 'font-lock-doc-face timetext)
      (put-text-property 0 (length lvltext) 'face 'font-lock-keyword-face lvltext))
    (let* ((logtext logtmpl)
           (logtext (replace-regexp-in-string "%t" timetext logtext))
           (logtext (replace-regexp-in-string "%l" lvltext logtext))
           (logtext (replace-regexp-in-string "%m" msg logtext))
           (begin (point)))
      (insert logtext "\n")
      (when propertize-p
        (put-text-property begin (+ begin 1) 'log4e--level level))
      (cl-loop initially (goto-char begin)
               while (and msgargs
                          (re-search-forward log4e--regexp-msg-format nil t))
               do (let* ((currtype (match-string-no-properties 0))
                         (currarg (pop msgargs))
                         (failfmt nil)
                         (currtext (condition-case e
                                       (format currtype currarg)
                                     (error (setq failfmt t)
                                            (format "=%s=" (error-message-string e))))))
                    (save-match-data
                      (when propertize-p
                        (ignore-errors
                          (cond (failfmt (put-text-property 0 (length currtext) 'face 'font-lock-warning-face currtext))
                                (t       (put-text-property 0 (length currtext) 'face 'font-lock-string-face currtext))))))
                    (replace-match currtext t t)))
      (goto-char begin))))

(defvar log4e--current-msg-buffer nil)

;; We needs this signature be stay for other compiled plugins using old version
(defun log4e--logging (buffnm codsys logtmpl timetmpl minlevel maxlevel logging-p level msg &rest msgargs)
  (when (and logging-p
             (log4e--logging-level-p minlevel maxlevel level))
    (save-match-data
      (with-current-buffer (log4e--get-or-create-log-buffer buffnm codsys)
        (goto-char (point-max))
        (let* ((buffer-read-only nil)
               (begin (point))
               (currlog (progn
                          (log4e--insert-log logtmpl timetmpl level msg msgargs t)
                          (goto-char (point-max))
                          (buffer-substring-no-properties begin (point))))
               (msgbuf (or (when (and log4e--current-msg-buffer
                                      (not (eq log4e--current-msg-buffer t)))
                             (ignore-errors (get-buffer log4e--current-msg-buffer)))
                           log4e--current-msg-buffer)))
          (when msgbuf
            (let ((standard-output (if (buffer-live-p msgbuf)
                                       msgbuf
                                     standard-output)))
              (princ currlog))))
        nil))))

(defun log4e--get-current-log-line-level ()
  (save-excursion
    (beginning-of-line)
    (get-text-property (point) 'log4e--level)))

;; We needs this signature be stay for other plugins compiled with this old version
(defun log4e--clear-log (buffnm)
  (with-current-buffer (log4e--get-or-create-log-buffer buffnm)
    (setq buffer-read-only nil)
    (erase-buffer)))

;; We needs this signature be stay for other plugins compiled with this old version
(defun log4e--open-log (buffnm)
  (let* ((buff (get-buffer buffnm)))
    (if (not (buffer-live-p buff))
        (message "[Log4E] Not exist log buffer.")
      (with-current-buffer buff
        (setq buffer-read-only t))
      (pop-to-buffer buff))))

;; We needs this signature be stay for other plugins compiled with this old version
(defun log4e--open-log-if-debug (buffnm dbg)
  (when dbg
    (log4e--open-log buffnm)))

;; (defun log4e--send-report-if-not-debug (buffnm dbg addr prefix)
;;   (let* ((buff (get-buffer buffnm)))
;;     (when (and (not dbg)
;;                (stringp addr)
;;                (buffer-live-p buff))
;;       (reporter-submit-bug-report addr prefix nil nil nil nil))))


(defmacro log4e:deflogger (prefix msgtmpl timetmpl &optional log-function-name-custom-alist)
  "Define the functions of logging for your elisp.

Specification:
 The following features are provided by evaluating this.
 - do logging for each log level.
 - set max and min log level.
 - switch logging.
 - switch debugging.
 - open and clear log buffer.
 - send bug report for you.
 For details, see Functions section.

Argument:
 - PREFIX is string as your elisp prefix.
 - MSGTMPL is string as format of log.
   - %t : Replaced with time string. About it, see TIMETMPL argument.
   - %l : Replaced with log level that's TRACE, DEBUG, INFO, WARN, ERROR, FATAL.
   - %m : Replaced with log message that passed by you.
 - TIMETMPL is string as format of time that's passed to `format-time-string'.
 - LOG-FUNCTION-NAME-CUSTOM-ALIST is alist as the function name of logging.
   - If this value is nil, define the following functions.
      yourprefix--log-trace
      yourprefix--log-debug
      ...
      yourprefix--log-fatal
   - If you want to custom the name of them, give like the following value.
      \\='((fatal . \"fatal\")
        (error . \"error\")
        (warn  . \"warn\")
        (info  . \"info\")
        (debug . \"debug\")
        (trace . \"trace\"))
     Then, define the following functions.
      yourprefix--trace
      yourprefix--debug
      ...
      yourprefix--fatal

Functions:
 List all functions defined below. PREFIX is your prefix.
 - PREFIX--log-fatal    ... #1
 - PREFIX--log-error    ... #1
 - PREFIX--log-warn     ... #1
 - PREFIX--log-info     ... #1
 - PREFIX--log-debug    ... #1
 - PREFIX--log-trace    ... #1
 - PREFIX--log-fatal*   ... #2
 - PREFIX--log-error*   ... #2
 - PREFIX--log-warn*    ... #2
 - PREFIX--log-info*    ... #2
 - PREFIX--log-debug*   ... #2
 - PREFIX--log-trace*   ... #2
 - PREFIX--log
 - PREFIX--log-set-level
 - PREFIX--log-enable-logging            ... #3
 - PREFIX--log-disable-logging           ... #3
 - PREFIX--log-enable-messaging          ... #3
 - PREFIX--log-disable-messaging         ... #3
 - PREFIX--log-enable-debugging          ... #3
 - PREFIX--log-disable-debugging         ... #3
 - PREFIX--log-debugging-p
 - PREFIX--log-set-coding-system
 - PREFIX--log-set-author-mail-address
 - PREFIX--log-clear-log                 ... #3
 - PREFIX--log-open-log                  ... #3
 - PREFIX--log-open-log-if-debug

 #1 : You can customize this name
 #2 : Name is a #1 name + \"*\"
 #3 : This is command

Example:
;; If you develop elisp that has prefix \"hoge\",
;; write and eval the following sexp in your elisp file.

 (require \\='log4e)
 (log4e:deflogger \"hoge\" \"%t [%l] %m\" \"%H:%M:%S\")

;; Eval the following
 (hoge--log-enable-logging)

;; Then, write the following

 (defun hoge-do-hoge (hoge)
   (if (not (stringp hoge))
       (hoge--log-fatal \"failed do hoge : hoge is %s\" hoge)
     (hoge--log-debug \"start do hoge about %s\" hoge)
     (message \"hoge!\")
     (hoge--log-info \"done hoge about %s\" hoge)))

;; Eval the following
 (hoge-do-hoge \"HOGEGE\")

;; Do M-x hoge--log-open-log
;; Open the buffer which name is \" *log4e-hoge*\". The buffer string is below
12:34:56 [INFO ] done hoge about HOGEGE

;; Eval the following
 (hoge--log-set-level \\='trace)
 (hoge-do-hoge \"FUGAGA\")

;; Do M-x hoge--log-open-log
;; Open the buffer. its string is below
12:34:56 [INFO ] done hoge about HOGEGE
12:35:43 [DEBUG] start do hoge about FUGAGA
12:35:43 [INFO ] done hoge about FUGAGA
 
"
  (declare (indent 0))
  (if (or (not (stringp prefix))   (string= prefix "")
          (not (stringp msgtmpl))  (string= msgtmpl "")
          (not (stringp timetmpl)) (string= timetmpl ""))
      (message "[LOG4E] invalid argument of deflogger")
    (let* ((bufsym (log4e--make-symbol-log-buffer prefix))
           (msgbufsym (log4e--make-symbol-msg-buffer prefix))
           (logtmplsym (log4e--make-symbol-log-template prefix))
           (timetmplsym (log4e--make-symbol-time-template prefix))
           (minlvlsym (log4e--make-symbol-min-level prefix))
           (maxlvlsym (log4e--make-symbol-max-level prefix))
           (tglsym (log4e--make-symbol-toggle-logging prefix))
           (dbgsym (log4e--make-symbol-toggle-debugging prefix))
           (codsyssym (log4e--make-symbol-buffer-coding-system prefix))
           (addrsym (log4e--make-symbol-author-mail-address prefix))
           (funcnm-alist (cl-loop with custom-alist = (car (cdr log-function-name-custom-alist))
                                  for lvl in '(fatal error warn info debug trace)
                                  for lvlpair = (assq lvl custom-alist)
                                  for fname = (or (cdr-safe lvlpair) "")
                                  collect (or (if (string-match "\*" fname)
                                                  (progn
                                                    (message "[LOG4E] ignore %s level name in log-function-name-custom-alist. can't use '*' for the name." lvl)
                                                    nil)
                                                lvlpair)
                                              (assq lvl log4e-default-logging-function-name-alist)))))
      `(progn

         ;; Define variable for prefix
         (defvar ,bufsym (format " *log4e-%s*" ,prefix))
         (defvar ,logtmplsym ,msgtmpl)
         (defvar ,timetmplsym ,timetmpl)
         (defvar ,minlvlsym 'info)
         (defvar ,maxlvlsym 'fatal)
         (defvar ,tglsym nil)
         (defvar ,msgbufsym nil)
         (defvar ,dbgsym nil)
         (defvar ,codsyssym nil)
         (defvar ,addrsym nil)

         ;; Define level set function
         (defun ,(intern (concat prefix "--log-set-level")) (minlevel &optional maxlevel)
           "Set range for doing logging.

MINLEVEL is symbol of lowest level for doing logging. its default is \\='info.
MAXLEVEL is symbol of highest level for doing logging. its default is \\='fatal."
           (setq ,minlvlsym minlevel)
           (setq ,maxlvlsym maxlevel))

         ;; Define logging toggle function
         (defun ,(intern (concat prefix "--log-enable-logging")) ()
           "Enable logging by logging functions."
           (interactive)
           (setq ,tglsym t))
         (defun ,(intern (concat prefix "--log-disable-logging")) ()
           "Disable logging by logging functions."
           (interactive)
           (setq ,tglsym nil))

         ;; Define messaging toggle function
         (defun ,(intern (concat prefix "--log-enable-messaging")) (&optional buffer)
           "Enable dump the log into other buffer by logging functions.

BUFFER is a buffer dumped log into. nil means *Messages* buffer."
           (interactive)
           (setq ,msgbufsym (or buffer t)))
         (defun ,(intern (concat prefix "--log-disable-messaging")) ()
           "Disable dump the log into other buffer by logging functions."
           (interactive)
           (setq ,msgbufsym nil))

         ;; Define debugging toggle function
         (defun ,(intern (concat prefix "--log-enable-debugging")) ()
           "Enable debugging and logging.

`PREFIX--log-debugging-p' will return t."
           (interactive)
           (setq ,tglsym t)
           (setq ,dbgsym t))
         (defun ,(intern (concat prefix "--log-disable-debugging")) ()
           "Disable debugging.

`PREFIX--log-debugging-p' will return nil."
           (interactive)
           (setq ,dbgsym nil))
         (defun ,(intern (concat prefix "--log-debugging-p")) ()
           ,dbgsym)

         ;; Define coding system set funtion
         (defun ,(intern (concat prefix "--log-set-coding-system")) (coding-system)
           "Set charset and linefeed of LOG-BUFFER.

CODING-SYSTEM is symbol for setting to `buffer-file-coding-system'.
LOG-BUFFER is a buffer which name is \" *log4e-PREFIX*\"."
           (setq ,codsyssym coding-system))

         ;;          ;; Define author mail set function
         ;;          (defun ,(intern (concat prefix "--log-set-author-mail-address")) (before-atmark after-atmark)
         ;;            "Set mail address of author for elisp that has PREFIX. This value is used SEND-REPORT.

         ;; BEFORE-ATMARK is string as part of mail address. If your address is \"hoge@example.co.jp\", it is \"hoge\".
         ;; AFTER-ATMARK is string as part of mail address. If your address is \"hoge@example.co.jp\", it is \"example.co.jp\".
         ;; SEND-REPORT is `PREFIX--log-send-report-if-not-debug'."
         ;;            (setq ,addrsym (concat before-atmark "@" after-atmark)))

         ;; Define log buffer handle function
         (defun ,(intern (concat prefix "--log-clear-log")) ()
           "Clear buffer string of buffer which name is \" *log4e-PREFIX*\"."
           (interactive)
           (log4e--clear-log ,bufsym))
         (defun ,(intern (concat prefix "--log-open-log")) ()
           "Open buffer which name is \" *log4e-PREFIX*\"."
           (interactive)
           (log4e--open-log ,bufsym))
         (defun ,(intern (concat prefix "--log-open-log-if-debug")) ()
           "Open buffer which name is \" *log4e-PREFIX*\" if debugging is enabled."
           (log4e--open-log-if-debug ,bufsym ,dbgsym))

         ;;          ;; Define report send function
         ;;          (defun ,(intern (concat prefix "--log-send-report-if-not-debug")) ()
         ;;            "Send bug report to author if debugging is disabled.

         ;; The author mailaddress is set by `PREFIX--log-set-author-mail-address'.
         ;; About the way of sending bug report, see `reporter-submit-bug-report'."
         ;;            (log4e--send-report-if-not-debug ,bufsym ,dbgsym ,addrsym ,prefix))

         ;; Define each level logging function
         (log4e--def-level-logger ,prefix nil nil)
         (log4e--def-level-logger ,prefix ,(assoc-default 'fatal funcnm-alist) fatal)
         (log4e--def-level-logger ,prefix ,(assoc-default 'error funcnm-alist) error)
         (log4e--def-level-logger ,prefix ,(assoc-default 'warn  funcnm-alist) warn)
         (log4e--def-level-logger ,prefix ,(assoc-default 'info  funcnm-alist) info)
         (log4e--def-level-logger ,prefix ,(assoc-default 'debug funcnm-alist) debug)
         (log4e--def-level-logger ,prefix ,(assoc-default 'trace funcnm-alist) trace)
         
         ))))


;;;###autoload
(define-derived-mode log4e-mode view-mode "Log4E"
  "Major mode for browsing a buffer made by log4e.

\\<log4e-mode-map>
\\{log4e-mode-map}"
  (define-key log4e-mode-map (kbd "J") 'log4e:next-log)
  (define-key log4e-mode-map (kbd "K") 'log4e:previous-log))

(defun log4e:next-log ()
  "Move to start of next log on log4e-mode."
  (interactive)
  (let* ((level))
    (while (and (not level)
                (< (point) (point-max)))
      (forward-line 1)
      (setq level (log4e--get-current-log-line-level)))
    level))

(defun log4e:previous-log ()
  "Move to start of previous log on log4e-mode."
  (interactive)
  (let* ((level))
    (while (and (not level)
                (> (point) (point-min)))
      (forward-line -1)
      (setq level (log4e--get-current-log-line-level)))
    level))

;;;###autoload
(defun log4e:insert-start-log-quickly ()
  "Insert logging statment for trace level log at start of current function/macro."
  (interactive)
  (let* ((fstartpt (when (re-search-backward "(\\(?:defun\\|defmacro\\|defsubst\\)\\*? +\\([^ ]+\\) +(\\([^)]*\\))" nil t)
                     (point)))
         (fncnm (when fstartpt (match-string-no-properties 1)))
         (argtext (when fstartpt (match-string-no-properties 2)))
         (prefix (save-excursion
                   (goto-char (point-min))
                   (cl-loop while (re-search-forward "(log4e:deflogger[ \n]+\"\\([^\"]+\\)\"" nil t)
                            for prefix = (match-string-no-properties 1)
                            for currface = (get-text-property (match-beginning 0) 'face)
                            if (not (eq currface 'font-lock-comment-face))
                            return prefix))))
    (when (and fstartpt prefix)
      (let* ((fncnm (replace-regexp-in-string (concat "\\`" prefix "[^a-zA-Z0-9]+") "" fncnm))
             (fncnm (replace-regexp-in-string "-" " " fncnm))
             (argtext (replace-regexp-in-string "\n" " " argtext))
             (argtext (replace-regexp-in-string "^ +" "" argtext))
             (argtext (replace-regexp-in-string " +$" "" argtext))
             (args (split-string argtext " +"))
             (args (cl-loop for arg in args
                            if (and (not (string= arg ""))
                                    (not (string-match "\\`&" arg)))
                            collect arg))
             (logtext (cl-loop with ret = (format "start %s." fncnm)
                               for arg in args
                               do (setq ret (concat ret " " arg "[%s]"))
                               finally return ret))
             (sexpformat (cl-loop with ret = "(%s--log 'trace \"%s\""
                                  for arg in args
                                  do (setq ret (concat ret " %s"))
                                  finally return (concat ret ")")))
             (inserttext (apply 'format sexpformat prefix logtext args)))
        (forward-char)
        (forward-sexp 3)
        (when (re-search-forward "\\=[ \n]+\"" nil t)
          (forward-char -1)
          (forward-sexp))
        (newline-and-indent)
        (insert inserttext)))))


(provide 'log4e)
;;; log4e.el ends here
