;;; doxymin.el --- Create doxygen style docs the easy way -*- lexical-binding: t -*-
;;
;; URL: https://gitlab.com/L0ren2/doxymin
;; Package-Version: 0.0.4
;; Package-Revision: v0.0.4-0-g1de66de7bf4b
;; Package-Requires: ((emacs "28.1"))
;; Keywords: (abbref convenience docs)
;;
;; Copyright (C) 2025 Lorenz Schwab
;;
;; Author: Lorenz Schwab <lorenz_schwab at web dot de>
;; Created: 2025-08-03
;; Keywords: doxygen documentation
;;
;; 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, write to the Free Software
;; Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
;;

;;; Commentary:
;;
;; Minor mode for creating Doxygen documentation.
;; Doxymin allows you to generate doxygen style comments for your C functions.
;; It also works for C++ and it might work for Java and C#. Technically should
;; work for all functions of languages using curly braces as delimiters.
;;
;; - Invoke doxymin-mode with M-x doxymin-mode.  To have doxymin-mode
;;   invoked automatically when in C mode, put
;;
;;   (add-hook 'c-mode-common-hook 'doxymin-mode)
;;
;;   in your .emacs.
;;
;; - Default key bindings are:
;;   - C-x C-d f will insert a Doxygen comment for the next function.
;;   - C-x C-d i will insert a Doxygen comment for the current file.
;;
;; Doxymin has been tested on and works with:
;; - GNU Emacs 30.1
;;
;; To submit a problem report, request a feature or get support, please
;; open an issue at https://gitlab.com/L0ren2/doxymin

;;; Code:

(provide 'doxymin)

(require 'cl-lib)
(require 'custom)
(require 'tempo)

(defconst doxymin-version "0.0.3"
  "Doxymin version number.")
(defun doxymin-version ()
  "Report the current version of doxymin in the minibuffer."
  (interactive)
  (message "Using doxymin version %s" doxymin-version))

(defgroup doxymin nil
  "Create Doxygen comments."
  :group 'tools)

(defcustom doxymin-doxygen-style
  "JavaDoc"
  "The style of comments to insert into code.
See http://www.stack.nl/~dimitri/doxygen/docblocks.html#docblocks for examples
of the various styles.

Must be one of \"JavaDoc\", \"Qt\", \"C++\", \"C++!\" or \"Fortran\".
Setting this variable to anything else will generate errors."
  :type '(radio (const :tag "JavaDoc" "JavaDoc")
                (const :tag "Qt" "Qt")
                (const :tag "C++" "C++")
                (const :tag "C++!" "C++!")
                (const :tag "Fortran" "Fortran"))
  :group 'doxymin)

(defcustom doxymin-command-character
  nil
  "The character to use to introduce Doxygen commands when inserting comments.
If nil, then use the default dictated by `doxymin-doxygen-style'.  Otherwise,
must be one of \"@\" or \"\\\"."
  :type '(choice (const :tag "None" nil)
                 string)
  :group 'doxymin)

(defcustom doxymin-file-comment-template
  nil
  "A tempo template to insert for `doxymin-insert-file-comment'.
If nil, then a default template based on the current style as indicated
by `doxymin-doxygen-style' will be used.

For help with tempo templates, see http://www.lysator.liu.se/~davidk/elisp/"
  :type '(sexp)
  :group 'doxymin)

(defcustom doxymin-function-comment-template
  nil
  "A tempo template to insert for \=`doxymin-insert-function-comment'\=.
If nil, then a default template based on the current style as
indicated by \=`doxymin-doxygen-style'\= will be used.  Note that the
function \=`doxymin-find-next-func'\= is available to you... it returns
an assoc list with the function's name, argument list (BUG: may be
incorrect for parameters that require parentheses), thrown exceptions and return
value:

\(cdr (assoc (quote func) (doxymin-find-next-func)))
  -> function name (string).
\(cdr (assoc (quote args) (doxymin-find-next-func)))
  -> list of arguments.
\(cdr (assoc (quote throws) (doxymin-find-next-func)))
  -> list of thrown exceptions.
\(cdr (assoc (quote return) (doxymin-find-next-func)))
  -> return type (string).

The argument list is a list of strings.

For help with tempo templates, see http://www.lysator.liu.se/~davidk/elisp/"
  :type '(sexp)
  :group 'doxymin)

(defcustom doxymin-void-types
  "void"
  "String with void types.  Extend this string if there are typedefs of void.
Example: \"void tVOID\"."
  :type '(string)
  :group 'doxymin)


;; Minor mode implementation

;;;###autoload
(define-minor-mode doxymin-mode
  "Minor mode for creating Doxygen documentation.
To submit a problem report, request a feature or get support, please
open an issue at https://gitlab.com/L0ren2/doxymin

To see what version of doxymin you are running, enter
`\\[doxymin-version]'.

Key bindings:
\\{doxymin-mode-map}"
  :dummy nil

  (when (boundp 'filladapt-token-table)
    ;; add tokens to filladapt to match doxygen markup
    (let ((bullet-regexp (concat "[@\\]"
                                 "\\(param\\(?:\\s-*"
                                 "\\[\\(?:in\\|out\\|in,out\\)\\]\\)?"
                                 "\\s-+\\sw+"
                                 "\\|tparam\\s-+\\sw+"
                                 "\\|return\\|attention\\|note"
                                 "\\|brief\\|li\\|arg\\|remarks"
                                 "\\|invariant\\|post\\|pre"
                                 "\\|todo\\|warning\\|bug"
                                 "\\|deprecated\\|since\\|test\\)")))
      (unless (assoc bullet-regexp filladapt-token-table)
        (setq filladapt-token-table
              (append filladapt-token-table
                      (list (list bullet-regexp 'bullet))))))))

;; Keymap

(defvar doxymin-mode-map
  (let ((km (make-sparse-keymap)))
    (define-key km (kbd "C-x C-d f") #'doxymin-insert-function-comment)
    (define-key km (kbd "C-x C-d i") #'doxymin-insert-file-comment)
    km)
  "Keymap for doxymin minor mode.")

;;;###autoload
(or (assoc 'doxymin-mode minor-mode-alist)
    (setq minor-mode-alist
          (cons '(doxymin-mode " doxy") minor-mode-alist)))

(or (assoc 'doxymin-mode minor-mode-map-alist)
    (setq minor-mode-map-alist
          (cons (cons 'doxymin-mode doxymin-mode-map)
                minor-mode-map-alist)))


;; Default templates

(defconst doxymin-Fortran-blank-multiline-comment-template
  '("!>" p > n "!!" p > n "!!" > n "!!" > n)
  "Default Fortran-style template for a blank multiline doxygen comment.")

(defconst doxymin-JavaDoc-blank-multiline-comment-template
  '("/**" > n "* " p > n "*" > n "*/" > n)
  "Default JavaDoc-style template for a blank multiline doxygen comment.")

(defconst doxymin-Qt-blank-multiline-comment-template
  '("//! " p > n "/*!" > n > n "*/" > n)
  "Default Qt-style template for a blank multiline doxygen comment.")

(defconst doxymin-C++-blank-multiline-comment-template
  '("///" > n "/// " p > n "///" > n)
  "Default C++-style template for a blank multiline doxygen comment.")

(defconst doxymin-C++!-blank-multiline-comment-template
  '("//!" > n "//! " p > n "//!" > n)
  "Default C++!-style template for a blank multiline doxygen comment.")

(defconst doxymin-Fortran-blank-singleline-comment-template
  '("!> " > p)
  "Default Fortran-style template for a blank single line doxygen comment.")

(defconst doxymin-JavaDoc-blank-singleline-comment-template
  '("/// " > p)
  "Default JavaDoc-style template for a blank single line doxygen comment.")

(defconst doxymin-Qt-blank-singleline-comment-template
  '("//! " > p)
  "Default Qt-style template for a blank single line doxygen comment.")

(defconst doxymin-C++-blank-singleline-comment-template
  '("/// " > p)
  "Default C++-style template for a blank single line doxygen comment.")

(defconst doxymin-C++!-blank-singleline-comment-template
  '("//! " > p)
  "Default C++!-style template for a blank single line doxygen comment.")

(defun doxymin-doxygen-command-char ()
  "Return the doxygen command character for the selected doxygen style."
  (cond
   (doxymin-command-character doxymin-command-character)
   ((string= doxymin-doxygen-style "JavaDoc") "@")
   ((string= doxymin-doxygen-style "Qt") "\\")
   ((string= doxymin-doxygen-style "C++") "@")
   ((string= doxymin-doxygen-style "C++!") "\\")
   ((string= doxymin-doxygen-style "Fortran") "@")
   (t "@")))

(defun doxymin--user-mail-address ()
  "Return the user's email address."
  (or
   (and (and (fboundp 'user-mail-address) (user-mail-address))
        (list 'l "<" (user-mail-address) ">"))
   (and (and (boundp 'user-mail-address) user-mail-address)
        (list 'l "<" user-mail-address ">"))))

(defun doxymin--spaced-user-name ()
  "Append a space to the user's name if it is not empty."
  (concat (user-full-name)
          (if (string= "" (user-full-name)) "" " ")))

(defconst doxymin-Fortran-file-comment-template
  '("!> " (doxymin-doxygen-command-char) "file "
    (if (buffer-file-name)
        (file-name-nondirectory (buffer-file-name))
      "") > p > n
    "!! " (doxymin-doxygen-command-char) "author " (doxymin--spaced-user-name)
    (doxymin--user-mail-address)
    > n
    "!! " (doxymin-doxygen-command-char) "date " (current-time-string) > n
    "!!" > n
    "!! " (doxymin-doxygen-command-char) "brief " (p "Brief description of this file: ") > n
    "!! " p > n)
  ;; "!<" > n)
  "Default Fortran-style template for file documentation.")

(defconst doxymin-JavaDoc-file-comment-template
  '("/**" > n
    " * " (doxymin-doxygen-command-char) "file "
    (if (buffer-file-name)
        (file-name-nondirectory (buffer-file-name))
      "") > n
    " * " (doxymin-doxygen-command-char) "author " (doxymin--spaced-user-name)
    (doxymin--user-mail-address)
    > n
    " * " (doxymin-doxygen-command-char) "date " (current-time-string) > n
    " *" > n
    " * " (doxymin-doxygen-command-char) "brief " (p "Brief description of this file: ") > n
    " *" > n
    " * " p > n
    " */" > n)
  "Default JavaDoc-style template for file documentation.")

(defconst doxymin-Qt-file-comment-template
  '("/*!" > n
    " " (doxymin-doxygen-command-char) "file "
    (if (buffer-file-name)
        (file-name-nondirectory (buffer-file-name))
      "") > n
    " " (doxymin-doxygen-command-char) "author " (doxymin--spaced-user-name)
    (doxymin--user-mail-address)
    > n
    " " (doxymin-doxygen-command-char) "date " (current-time-string) > n
    "" > n
    " " (doxymin-doxygen-command-char) "brief " (p "Brief description of this file: ") > n
    "" > n
    " " p > n
    "*/" > n)
  "Default Qt-style template for file documentation.")

(defconst doxymin-C++-file-comment-template
  '("///" > n
    "/// " (doxymin-doxygen-command-char) "file "
    (if (buffer-file-name)
        (file-name-nondirectory (buffer-file-name))
      "") > n
    "/// " (doxymin-doxygen-command-char) "author " (doxymin--spaced-user-name)
    (doxymin--user-mail-address)
    > n
    "/// " (doxymin-doxygen-command-char) "date " (current-time-string) > n
    "///" > n
    "/// " (doxymin-doxygen-command-char) "brief " (p "Brief description of this file: ") > n
    "///" > n
    "/// " p > n
    "///" > n)
  "Default C++-style template for file documentation.")

(defconst doxymin-C++!-file-comment-template
  '("//!" > n
    "//! " (doxymin-doxygen-command-char) "file "
    (if (buffer-file-name)
        (file-name-nondirectory (buffer-file-name))
      "") > n
    "//! " (doxymin-doxygen-command-char) "author " (doxymin--spaced-user-name)
    (doxymin--user-mail-address)
    > n
    "//! " (doxymin-doxygen-command-char) "date " (current-time-string) > n
    "//!" > n
    "//! " (doxymin-doxygen-command-char) "brief " (p "Brief description of this file: ") > n
    "//!" > n
    "//! " p > n
    "//!" > n)
  "Default C++!-style template for file documentation.")


(defun doxymin-parm-tempo-element (parms)
  "Insert tempo elements for the given PARMS in the given style."
  (if parms
      (let ((prompt (concat "Parameter " (car parms) ": ")))
        (cond
         ((string= doxymin-doxygen-style "JavaDoc")
          (list 'l " * " (doxymin-doxygen-command-char)
                "param " (car parms) " " (list 'p prompt) '> 'n
                (doxymin-parm-tempo-element (cdr parms))))
         ((string= doxymin-doxygen-style "Qt")
          (list 'l " " (doxymin-doxygen-command-char)
                "param " (car parms) " " (list 'p prompt) '> 'n
                (doxymin-parm-tempo-element (cdr parms))))
         ((string= doxymin-doxygen-style "C++")
          (list 'l "/// " (doxymin-doxygen-command-char)
                "param " (car parms) " " (list 'p prompt) '> 'n
                (doxymin-parm-tempo-element (cdr parms))))
         ((string= doxymin-doxygen-style "C++!")
          (list 'l "//! " (doxymin-doxygen-command-char)
                "param " (car parms) " " (list 'p prompt) '> 'n
                (doxymin-parm-tempo-element (cdr parms))))
         ((string= doxymin-doxygen-style "Fortran")
          (list 'l "!! " (doxymin-doxygen-command-char)
                "param " (car parms) " " (list 'p prompt) '> 'n
                (doxymin-parm-tempo-element (cdr parms))))
         (t
          (doxymin-invalid-style))))
    nil))

(defun doxymin-throws-tempo-element (throws)
  "Insert tempo elements for the THROWS declarations in the given style."
  (if throws
      (let ((prompt (concat "Throws " (car throws) ": ")))
        (cond
         ((string= doxymin-doxygen-style "JavaDoc")
          (list 'l " * " (doxymin-doxygen-command-char)
                "throws " (car throws) " " (list 'p prompt) '> 'n
                (doxymin-throws-tempo-element (cdr throws))))
         ((string= doxymin-doxygen-style "Qt")
          (list 'l " " (doxymin-doxygen-command-char)
                "throws " (car throws) " " (list 'p prompt) '> 'n
                (doxymin-throws-tempo-element (cdr throws))))
         ((string= doxymin-doxygen-style "C++")
          (list 'l "/// " (doxymin-doxygen-command-char)
                "throws " (car throws) " " (list 'p prompt) '> 'n
                (doxymin-throws-tempo-element (cdr throws))))
         ((string= doxymin-doxygen-style "C++!")
          (list 'l "//! " (doxymin-doxygen-command-char)
                "throws " (car throws) " " (list 'p prompt) '> 'n
                (doxymin-throws-tempo-element (cdr throws))))
         ((string= doxymin-doxygen-style "Fortran")
          (list 'l "!! " (doxymin-doxygen-command-char)
                "throws " (car throws) " " (list 'p prompt) '> 'n
                (doxymin-throws-tempo-element (cdr throws))))
         (t
          (doxymin-invalid-style))))
    nil))

(defconst doxymin-Fortran-function-comment-template
  '((let ((next-func (doxymin-find-next-func)))
      (if next-func
          (list
           'l
           "!> " 'p '> 'n
           (doxymin-parm-tempo-element (cdr (assoc 'args next-func)))
           (unless (string-match
                    (regexp-quote (cdr (assoc 'return next-func)))
                    doxymin-void-types)
             '(l "!!" > n "!! " (doxymin-doxygen-command-char)
                 "return " (p "Returns: ") > n)))
        ;; "!<" '>)
        (progn
          (error "Can't find next function declaration")
          nil))))
  "Default Fortran-style template for function documentation.")


(defconst doxymin-JavaDoc-function-comment-template
  '((let ((next-func (doxymin-find-next-func)))
      (if next-func
          (list
           'l
           "/**" '> 'n
           " * " 'p '> 'n
           " *" '> 'n
           (doxymin-parm-tempo-element (cdr (assoc 'args next-func)))
           (doxymin-throws-tempo-element (cdr (assoc 'throws next-func)))
           (unless (string-match
                    (regexp-quote (cdr (assoc 'return next-func)))
                    doxymin-void-types)
             '(l " *" > n " * " (doxymin-doxygen-command-char)
                 "return " (p "Returns: ") > n))
           " */" '>)
        (progn
          (error "Can't find next function declaration")
          nil))))
  "Default JavaDoc-style template for function documentation.")

(defconst doxymin-Qt-function-comment-template
  '((let ((next-func (doxymin-find-next-func)))
      (if next-func
          (list
           'l
           "//! " 'p '> 'n
           "/*! " '> 'n
           " " '> 'n
           (doxymin-parm-tempo-element (cdr (assoc 'args next-func)))
           (unless (string-match
                    (regexp-quote (cdr (assoc 'return next-func)))
                    doxymin-void-types)
             '(l "" > n "  " (doxymin-doxygen-command-char)
                 "return " (p "Returns: ") > n))
           " */" '>)
        (progn
          (error "Can't find next function declaraton")
          nil))))
  "Default Qt-style template for function documentation.")

(defconst doxymin-C++-function-comment-template
  '((let ((next-func (doxymin-find-next-func)))
      (if next-func
          (list
           'l
           "/// " 'p '> 'n
           "///" '> 'n
           (doxymin-parm-tempo-element (cdr (assoc 'args next-func)))
           (unless (string-match
                    (regexp-quote (cdr (assoc 'return next-func)))
                    doxymin-void-types)
             '(l "///" > n "/// " (doxymin-doxygen-command-char)
                 "return " (p "Returns: ") > n))
           "///" '>)
        (progn
          (error "Can't find next function declaraton")
          nil))))
  "Default C++-style template for function documentation.")

(defconst doxymin-C++!-function-comment-template
  '((let ((next-func (doxymin-find-next-func)))
      (if next-func
          (list
           'l
           "//! " 'p '> 'n
           "//!" '> 'n
           (doxymin-parm-tempo-element (cdr (assoc 'args next-func)))
           (unless (string-match
                    (regexp-quote (cdr (assoc 'return next-func)))
                    doxymin-void-types)
             '(l "//!" > n "//! " (doxymin-doxygen-command-char)
                 "return " (p "Returns: ") > n))
           "//!" '>)
        (progn
          (error "Can't find next function declaraton")
          nil))))
  "Default C++!-style template for function documentation.")

(defun doxymin-invalid-style ()
  "Warn the user that he has set `doxymin-doxygen-style' to an invalid style."
  (error (concat
          "Invalid `doxymin-doxygen-style': "
          doxymin-doxygen-style
          ": must be one of \"JavaDoc\", \"Qt\", \"C++\", \"C++!\", or \"Fortran\".")))

;; This should make it easier to add new templates and cut down
;; on copy-and-paste programming.
(defun doxymin-call-template (template-name)
  "Insert the template denoted by TEMPLATE-NAME."
  (let* ((user-template-name (concat "doxymin-" template-name "-template"))
         (user-template (car (read-from-string user-template-name)))
         (default-template-name (concat "doxymin-"
                                        doxymin-doxygen-style "-"
                                        template-name "-template"))
         (default-template (car (read-from-string default-template-name))))
    (cond
     ((and (boundp user-template) ; Make sure it is a non-nil list
           (listp (eval user-template))
           (eval user-template))
      ;; Use the user's template
      (tempo-insert-template user-template tempo-insert-region))
     ((and (boundp default-template)
           (listp (eval default-template))
           (eval default-template))
      ;; Use the default template, based on the current style
      (tempo-insert-template default-template tempo-insert-region))
     (t
      ;; Most likely, `doxymin-doxygen-style' has been set wrong.
      (doxymin-invalid-style)))))

(defun doxymin-insert-file-comment ()
  "Insert Doxygen documentation for the current file at current point."
  (interactive "*")
  (doxymin-call-template "file-comment"))

(defun doxymin-insert-function-comment ()
  "Insert Doxygen documentation for function declaration / definition at point."
  (interactive "*")
  (doxymin-call-template "function-comment"))

;; FIXME
;; The following was borrowed from "simple.el".
;; If anyone knows of a better/simpler way of doing this, please let me know.
(defconst doxymin-comment-indent-function
  (lambda (skip)
    (save-excursion
      (beginning-of-line)
      (let ((eol (save-excursion (end-of-line) (point))))
        (and skip
             (re-search-forward skip eol t)
             (setq eol (match-beginning 0)))
        (goto-char eol)
        (skip-chars-backward " \t")
        (max comment-column (1+ (current-column))))))
  "Function to compute desired indentation for a comment.
This function is called with skip and with point at the beginning of
the comment's starting delimiter.")


;; These are helper functions that search for the next function
;; declarations/definition and extract its name, return type and
;; argument list.  Used for documenting functions.

(defun doxymin-extract-args-list (args-string)
  "Extracts the arguments from ARGS-STRING."
  (cond
   ;; arg list is empty
   ((string-match "\\`[ \t\n]*\\'" args-string)
    nil)
   ;; argument list consists of one word
   ((string-match "\\`[ \t\n]*\\([a-zA-Z0-9_]+\\)[ \t\n]*\\'" args-string)
    ;; ... extract this word
    (let ((arg (substring args-string (match-beginning 1) (match-end 1))))
      ;; if this arg is a void type return nil
      (if (string-match (regexp-quote arg) doxymin-void-types)
          nil
        ;; else return arg
        (list arg))))
   ;; else split the string and extact var names from args
   (t
    (doxymin-extract-args-list-helper
     (doxymin-save-split args-string)))))


(defun doxymin-save-split (args-string)
  "Split the declaration list ARGS-STRING and return list of single declarations."
  (let ((comma-pos (string-match "," args-string))
        (paren-pos (string-match "(" args-string)))
    (cond
     ;; no comma in string found
     ((null comma-pos)     (list args-string))
     ;; comma but no parenthethes: split-string is save
     ((null paren-pos)     (split-string args-string ","))
     ;; comma first then parenthesis
     ((< comma-pos paren-pos)
      (cons (substring args-string 0 comma-pos)
            (doxymin-save-split (substring args-string (1+ comma-pos)))))
     ;; parenthesis first then comma. there must exist a closing parenthesis
     (t
      ;; cut off the (...) part
      ;; create temporary buffer
      (with-current-buffer "*doxymin-scratch*"
        (erase-buffer)
        (insert args-string)
        (goto-char (point-min))
        (search-forward "(")
        (prog1
            (let ((depth 1)
                  (exit)
                  (comma-found))
              (while (not exit)
                ;; step through buffer
                (forward-char 1)
                (cond
                 ;; end of buffer: exit
                 ((eobp) (setq exit t))
                 ;; decrease depth counter
                 ((looking-at ")")        (setq depth (1- depth)))
                 ;; increase depth counter
                 ((looking-at "(")        (setq depth (1+ depth)))
                 ;; comma at depth 0, thats it!
                 ((and (looking-at ",") (= 0 depth))
                  (setq exit t)
                  (setq comma-found t))))
              (if (not comma-found)
                  ;; whole string is one arg
                  (list (buffer-substring 1 (point)))
                ;; else split at comma ...
                (cons (buffer-substring 1 (point))
                      ;; and split rest of declaration list
                      (doxymin-save-split
                       (buffer-substring (1+ (point)) (point-max))))))
          (kill-buffer (current-buffer))))))))


;; This regexp fails if the opt. parentheses
;; contain another level of parentheses.  E.g. for:
;; int f(int (*g)(int (*h)()))
(defun doxymin-extract-args-list-helper (args-list)
  "Recursively get names of arguments in ARGS-LIST."
  (if args-list
      (if (string-match
           (concat
            "\\("
            "([ \t\n]*\\*[ \t\n]*\\([a-zA-Z0-9_]+\\)[ \t\n]*)"; (*varname)
            "\\|"                                        ; or
            "\\*?[ \t\n]*\\([a-zA-Z0-9_]+\\)"            ; opt. *, varname
            "\\)"
            "[ \t\n]*"                                   ; opt. spaces
            "\\(\\[[ \t\n]*[a-zA-Z0-9_]*[ \t\n]*\\]\\|"  ; opt. array bounds
            "([^()]*)\\)?"                               ; or opt. func args
            "[ \t\n]*"                                   ; opt. spaces
            "\\(=[ \t\n]*[^ \t\n]+[ \t\n]*\\)?"          ; optional assignment
            "[ \t\n]*\\'"                                ; end
            ) (car args-list))
          (cons
           (cond
            ;; var name in: (*name)
            ((match-beginning 2)
             (substring (car args-list) (match-beginning 2) (match-end 2)))
            ;; var name in: *name
            ((match-beginning 3)
             (substring (car args-list) (match-beginning 3) (match-end 3)))
            ;; no match: return complete declaration
            (t
             (car args-list)))
           (doxymin-extract-args-list-helper (cdr args-list)))
        ;; else there is no match
        nil)))

(defun doxymin-core-string (s)
  "Trim superfluous whitespace from S."
  (string-match "\\`[ \t\n]*\\(.*?\\)[ \t\n]*\\'" s)
  (if (match-beginning 1)
      (substring s (match-beginning 1) (match-end 1))
    s))

(defun doxymin--eol-is-semicolon-p ()
  "Check if last character in line is a semicolon."
  (save-excursion
    (let ((eol (line-end-position)))
      (string= (buffer-substring-no-properties (- eol 1) eol) ";"))))

(defun doxymin--get-throws-implementation (func-end)
  "Extract all thrown exceptions inside function body until FUNC-END."
  (let ((throws (cl-loop while (re-search-forward "throw " func-end t)
                         collect
                         (buffer-substring-no-properties
                          (point) (progn
                                    (re-search-forward "[({=;]")
                                    (- (point) 1))))))
    (mapcar (lambda (str)
              (let ((s (if (string-search "new " str)
                           (substring str 4)
                         str)))
                (string-trim s " +" " +")))
            throws)))

(defun doxymin--get-throws ()
  "Return the occurences of thrown exceptions."
  (unless (doxymin--eol-is-semicolon-p)
    (save-excursion
      (let ((func-end (progn (re-search-forward "{" nil t)
                              (backward-char 1)
                              (forward-list)
                              (point))))
        (backward-list)
        (doxymin--get-throws-implementation func-end)))))


(defun doxymin-find-next-func ()
  "Return a list describing next function declaration, or nil if not found.

\(cdr (assoc (quote func) (doxymin-find-next-func))) -> function name (string).
\(cdr (assoc (quote args) (doxymin-find-next-func))) -> list of arguments.
\(cdr (assoc (quote return) (doxymin-find-next-func))) -> return type (string).

The argument list is a list of strings."
  (interactive)
  (save-excursion
    (if (re-search-forward
         (concat
          ;; return type
          "\\(\\(const[ \t\n]+\\)?[a-zA-Z0-9_]+[ \t\n*&]+\\)?"

          ;; name
          "\\(\\([a-zA-Z0-9_~:<,>*&]\\|\\([ \t\n]+::[ \t\n]+\\)\\)+"
          "\\(o?perator[ \t\n]*.[^(]*\\)?\\)[ \t\n]*(")
         nil t)

        (let* ((func (buffer-substring (match-beginning 3) (match-end 3)))
               (args (buffer-substring (point) (progn
                                                 (backward-char 1)
                                                 (forward-list)
                                                 (backward-char 1)
                                                 (point))))
               (ret (cond
                     ;; Return type specified
                     ((match-beginning 1)
                      (buffer-substring (match-beginning 1) (match-end 1)))
                     ;;Constructor/destructor
                     ((string-match
                       "^\\([a-zA-Z0-9_<,>:*&]+\\)[ \t\n]*::[ \t\n]*~?\\1$"
                       func) "void")
                     ;;Constructor in class decl.
                     ((save-match-data
                        (re-search-backward
                         (concat
                          "class[ \t\n]+" (regexp-quote func) "[ \t\n]*{")
                         nil t))
                      "void")
                     ;;Destructor in class decl.
                     ((save-match-data
                        (and (string-match "^~\\([a-zA-Z0-9_]+\\)$" func)
                             (save-match-data
                               (re-search-backward
                                (concat
                                 "class[ \t\n]+" (regexp-quote
                                                  (match-string 1 func))
                                 "[ \t\n]*{") nil t))))
                      "void")
                     ;;Default
                     (t "int"))))
          (list (cons 'func func)
                (cons 'args (doxymin-extract-args-list args))
                (cons 'throws (doxymin--get-throws))
                (cons 'return (doxymin-core-string ret))))
      nil)))

;;; doxymin.el ends here
