;;; timu-symbol-extract.el --- Extract symbols to org table -*- lexical-binding: t; -*-

;; Author: Aimé Bertrand <aime.bertrand@macowners.club>
;; Package-Version: 1.0
;; Package-Revision: 1198f4b97ee8
;; Package-Requires: ((emacs "28.1"))
;; Created: 2025-11-25
;; Keywords: defaults tools helper
;; Homepage: https://gitlab.com/aimebertrand/dotemacs
;; This file is NOT part of GNU Emacs.

;; The MIT License (MIT)
;;
;; Copyright (C) 2023-2025 Aimé Bertrand
;;
;; Permission is hereby granted, free of charge, to any person obtaining
;; a copy of this software and associated documentation files (the
;; "Software"), to deal in the Software without restriction, including
;; without limitation the rights to use, copy, modify, merge, publish,
;; distribute, sublicense, and/or sell copies of the Software, and to
;; permit persons to whom the Software is furnished to do so, subject to
;; the following conditions:
;;
;; The above copyright notice and this permission notice shall be
;; included in all copies or substantial portions of the Software.
;;
;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
;; EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
;; MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
;; IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
;; CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
;; TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
;; SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

;;; Commentary:
;; Functions to scan directory for function and variable definitions
;; and output to org-mode table

;;; Code:

(require 'lisp-mnt)
(require 'org)

(defun timu-symbol-extract-functions-from-buffer ()
  "Extract all function definitions from current buffer.
Returns a list of plists with :name, :kind, and :docstring-first-line."
  (save-excursion
    (goto-char (point-min))
    (let ((functions '()))
      (while (re-search-forward "^(defun\\s-+\\(\\(?:\\sw\\|\\s_\\)+\\)" nil t)
        (let* ((func-name (match-string 1))
               (func-symbol (intern func-name))
               (func-start (match-beginning 0))
               (func-form nil)
               (kind nil)
               (docstring nil)
               (docstring-first-line ""))
          ;; Load the function definition
          (save-excursion
            (goto-char func-start)
            (condition-case nil
                (progn
                  (setq func-form (read (current-buffer)))
                  (eval func-form))
              (error nil)))
          (when func-form
            ;; Use commandp to check if it's a command
            (setq kind (if (commandp func-symbol) "command" "function"))
            ;; Extract docstring
            (when (and (listp func-form)
                       (>= (length func-form) 4)
                       (stringp (nth 3 func-form)))
              (setq docstring (nth 3 func-form)))
            ;; If no docstring at position 3, check position 4 (after arg list)
            (when (and (not docstring)
                       (listp func-form)
                       (>= (length func-form) 5)
                       (stringp (nth 4 func-form)))
              (setq docstring (nth 4 func-form)))
            ;; Get first line of docstring
            (when docstring
              (setq docstring-first-line
                    (car (split-string docstring "\n" t "[ \t]+"))))

            (push (list :name func-name
                        :kind kind
                        :docstring-first-line docstring-first-line)
                  functions))))
      (nreverse functions))))

(defun timu-symbol-extract-variables-from-buffer ()
  "Extract all variable definitions (defvar and defcustom) from current buffer.
Returns a list of plists with :name, :kind, and :docstring-first-line."
  (save-excursion
    (goto-char (point-min))
    (let ((variables '()))
      (while (re-search-forward
              "^(\\(defvar\\|defcustom\\)\\s-+\\(\\(?:\\sw\\|\\s_\\)+\\)" nil t)
        (let* ((kind (if (equal (match-string 1) "defvar")
                         "variable"
                       "custom variable"))
               (var-name (match-string 2))
               (var-start (match-beginning 0))
               (var-form nil)
               (docstring nil)
               (docstring-first-line ""))
          ;; Read the form
          (save-excursion
            (goto-char var-start)
            (condition-case nil
                (setq var-form (read (current-buffer)))
              (error nil)))
          (when var-form
            ;; Extract docstring - it's the 4th element (index 3) for both
            ;; defvar: (defvar name value "docstring")
            ;; defcustom: (defcustom name value "docstring" :type ...)
            (when (and (listp var-form)
                       (>= (length var-form) 4)
                       (stringp (nth 3 var-form)))
              (setq docstring (nth 3 var-form)))
            ;; Get first line of docstring
            (when docstring
              (setq docstring-first-line
                    (car (split-string docstring "\n" t "[ \t]+"))))
            (push (list :name var-name
                        :kind kind
                        :docstring-first-line docstring-first-line)
                  variables))))
      (nreverse variables))))

(defun timu-symbol-extract-definitions-from-buffer ()
  "Extract all definitions (functions and variables) from current buffer.
Returns a list of plists with :name, :kind, and :docstring-first-line."
  (append (timu-symbol-extract-functions-from-buffer)
          (timu-symbol-extract-variables-from-buffer)))

(defun timu-symbol-extract-definitions-from-file (file)
  "Extract function and variable definitions from FILE.
Returns a list of plists with definition information."
  (with-temp-buffer
    (insert-file-contents file)
    (emacs-lisp-mode)
    (timu-symbol-extract-definitions-from-buffer)))

(defun timu-symbol-extract-functions-from-file (file)
  "Extract function definitions from FILE.
Returns a list of plists with function information."
  (with-temp-buffer
    (insert-file-contents file)
    (emacs-lisp-mode)
    (timu-symbol-extract-functions-from-buffer)))

(defun timu-symbol-extract-variables-from-file (file)
  "Extract variable definitions from FILE.
Returns a list of plists with variable information."
  (with-temp-buffer
    (insert-file-contents file)
    (emacs-lisp-mode)
    (timu-symbol-extract-variables-from-buffer)))

(defun timu-symbol-extract-definitions-from-directory (dir &optional pattern)
  "Extract all definitions (functions and variables) from elisp files in DIR.
Optional PATTERN specifies file pattern (default: \"\\\\.el$\").
Returns a list of plists with :file, :name, :kind, :docstring-first-line."
  (interactive "DDirectory: ")
  (let* ((pattern (or pattern "\\.el$"))
         (files (directory-files-recursively dir pattern))
         (all-definitions '()))
    (dolist (file files)
      (message "Processing %s..." file)
      (let ((definitions (timu-symbol-extract-definitions-from-file file)))
        (dolist (def definitions)
          (push (plist-put def :file (file-relative-name file dir))
                all-definitions))))
    (nreverse all-definitions)))

(defun timu-symbol-extract-functions-from-directory (dir &optional pattern)
  "Extract all function definitions from elisp files in DIR.
Optional PATTERN specifies file pattern (default: \"\\\\.el$\").
Returns a list of plists with :file, :name, :kind, :docstring-first-line."
  (interactive "DDirectory: ")
  (let* ((pattern (or pattern "\\.el$"))
         (files (directory-files-recursively dir pattern))
         (all-functions '()))
    (dolist (file files)
      (message "Processing %s..." file)
      (let ((functions (timu-symbol-extract-functions-from-file file)))
        (dolist (func functions)
          (push (plist-put func :file (file-relative-name file dir))
                all-functions))))
    (nreverse all-functions)))

(defun timu-symbol-extract-variables-from-directory (dir &optional pattern)
  "Extract all variable definitions from elisp files in DIR.
Optional PATTERN specifies file pattern (default: \"\\\\.el$\").
Returns a list of plists with :file, :name, :kind, :docstring-first-line."
  (interactive "DDirectory: ")
  (let* ((pattern (or pattern "\\.el$"))
         (files (directory-files-recursively dir pattern))
         (all-variables '()))
    (dolist (file files)
      (message "Processing %s..." file)
      (let ((variables (timu-symbol-extract-variables-from-file file)))
        (dolist (var variables)
          (push (plist-put var :file (file-relative-name file dir))
                all-variables))))
    (nreverse all-variables)))

;;;###autoload
(defun timu-symbol-extract-definitions-to-org-table (dir &optional pattern)
  "Create an `org-mode' table of all definitions from DIR.
Optional PATTERN specifies file pattern (default: \"\\\\.el$\")."
  (interactive "DDirectory: ")
  (let* ((definitions
          (timu-symbol-extract-definitions-from-directory dir pattern))
         (buffer (get-buffer-create "*Definition Extraction*")))
    (with-current-buffer buffer
      (erase-buffer)
      (org-mode)
      ;; Insert header with file column
      (insert "| Kind | File | Name | Docstring |\n")
      (insert "|-\n")
      ;; Insert data rows
      (dolist (def definitions)
        (let ((name (plist-get def :name))
              (kind (plist-get def :kind))
              (docstring (or (plist-get def :docstring-first-line) ""))
              (file (plist-get def :file)))
          ;; Escape pipe characters in docstring
          (setq docstring (replace-regexp-in-string "|" "\\\\vert{}" docstring))
          (insert (format "| %s | %s | %s | %s |\n"
                          kind file name docstring))))
      ;; Align the table
      (goto-char (point-min))
      (org-table-align))
    (switch-to-buffer buffer)
    (message "Extracted %d definitions" (length definitions))))

;;;###autoload
(defun timu-symbol-extract-functions-to-org-table (dir &optional pattern)
  "Create an `org-mode' table of functions from DIR.
Optional PATTERN specifies file pattern (default: \"\\\\.el$\")."
  (interactive "DDirectory: ")
  (let* ((functions (timu-symbol-extract-functions-from-directory dir pattern))
         (buffer (get-buffer-create "*Function Extraction*")))
    (with-current-buffer buffer
      (erase-buffer)
      (org-mode)
      ;; Insert header with file column
      (insert "| Kind | File | Name | Docstring |\n")
      (insert "|-\n")
      ;; Insert data rows
      (dolist (func functions)
        (let ((name (plist-get func :name))
              (kind (plist-get func :kind))
              (docstring (or (plist-get func :docstring-first-line) ""))
              (file (plist-get func :file)))
          ;; Escape pipe characters in docstring
          (setq docstring (replace-regexp-in-string "|" "\\\\vert{}" docstring))
          (insert (format "| %s | %s | %s | %s |\n"
                          kind file name docstring))))
      ;; Align the table
      (goto-char (point-min))
      (org-table-align))
    (switch-to-buffer buffer)
    (message "Extracted %d functions" (length functions))))

;;;###autoload
(defun timu-symbol-extract-variables-to-org-table (dir &optional pattern)
  "Create an `org-mode' table of variables from DIR.
Optional PATTERN specifies file pattern (default: \"\\\\.el$\")."
  (interactive "DDirectory: ")
  (let* ((variables (timu-symbol-extract-variables-from-directory dir pattern))
         (buffer (get-buffer-create "*Variable Extraction*")))
    (with-current-buffer buffer
      (erase-buffer)
      (org-mode)
      ;; Insert header with file column
      (insert "| Kind | File | Name | Docstring |\n")
      (insert "|-\n")
      ;; Insert data rows
      (dolist (var variables)
        (let ((name (plist-get var :name))
              (kind (plist-get var :kind))
              (docstring (or (plist-get var :docstring-first-line) ""))
              (file (plist-get var :file)))
          ;; Escape pipe characters in docstring
          (setq docstring (replace-regexp-in-string "|" "\\\\vert{}" docstring))
          (insert (format "| %s | %s | %s | %s |\n"
                          kind file name docstring))))
      ;; Align the table
      (goto-char (point-min))
      (org-table-align))
    (switch-to-buffer buffer)
    (message "Extracted %d variables" (length variables))))

(defun timu-symbol-extract-definitions-to-org-table-current-file (&optional buf)
  "Create an `org-mode' table of all definitions from current buffer.
Optional BUF specifies where to put the table."
  (interactive)
  (let* ((definitions (timu-symbol-extract-definitions-from-buffer))
         (buffer (or buf (get-buffer-create "*Definition Extraction*")))
         (current-file (or (buffer-file-name) (buffer-name))))
    (with-current-buffer buffer
      (erase-buffer)
      (org-mode)
      ;; Insert header with file column
      (insert "| Kind | File | Name | Docstring |\n")
      (insert "|-\n")
      ;; Insert data rows
      (dolist (def definitions)
        (let ((name (plist-get def :name))
              (kind (plist-get def :kind))
              (docstring (or (plist-get def :docstring-first-line) "")))
          ;; Escape pipe characters in docstring
          (setq docstring (replace-regexp-in-string "|" "\\\\vert{}" docstring))
          (insert (format "| %s | %s | %s | %s |\n"
                          kind current-file name docstring))))
      ;; Align the table
      (goto-char (point-min))
      (org-table-align))
    (switch-to-buffer buffer)
    (message "Extracted %d definitions" (length definitions))))

(defun timu-symbol-extract-functions-to-org-table-current-file (&optional buf)
  "Create an `org-mode' table of functions from current buffer.
Optional BUF specifies where to put the table."
  (interactive)
  (let* ((functions (timu-symbol-extract-functions-from-buffer))
         (buffer (or buf (get-buffer-create "*Function Extraction*")))
         (current-file (or (buffer-file-name) (buffer-name))))
    (with-current-buffer buffer
      (erase-buffer)
      (org-mode)
      ;; Insert header with file column
      (insert "| Kind | File | Name | Docstring |\n")
      (insert "|-\n")
      ;; Insert data rows
      (dolist (func functions)
        (let ((name (plist-get func :name))
              (kind (plist-get func :kind))
              (docstring (or (plist-get func :docstring-first-line) "")))
          ;; Escape pipe characters in docstring
          (setq docstring (replace-regexp-in-string "|" "\\\\vert{}" docstring))
          (insert (format "| %s | %s | %s | %s |\n"
                          kind current-file name docstring))))
      ;; Align the table
      (goto-char (point-min))
      (org-table-align))
    (switch-to-buffer buffer)
    (message "Extracted %d functions" (length functions))))

(defun timu-symbol-extract-variables-to-org-table-current-file (&optional buf)
  "Create an `org-mode' table of variables from current buffer.
Optional BUF specifies where to put the table."
  (interactive)
  (let* ((variables (timu-symbol-extract-variables-from-buffer))
         (buffer (or buf (get-buffer-create "*Variable Extraction*")))
         (current-file (or (buffer-file-name) (buffer-name))))
    (with-current-buffer buffer
      (erase-buffer)
      (org-mode)
      ;; Insert header with file column
      (insert "| Kind | File | Name | Docstring |\n")
      (insert "|-\n")
      ;; Insert data rows
      (dolist (var variables)
        (let ((name (plist-get var :name))
              (kind (plist-get var :kind))
              (docstring (or (plist-get var :docstring-first-line) "")))
          ;; Escape pipe characters in docstring
          (setq docstring (replace-regexp-in-string "|" "\\\\vert{}" docstring))
          (insert (format "| %s | %s | %s | %s |\n"
                          kind current-file name docstring))))
      ;; Align the table
      (goto-char (point-min))
      (org-table-align))
    (switch-to-buffer buffer)
    (message "Extracted %d variables" (length variables))))


(provide 'timu-symbol-extract)

;;; timu-symbol-extract.el ends here
