;;; semanticdb-matlab.el --- Semantic database extensions for MATLAB -*- lexical-binding: t -*-

;; Author: David Engster <dengste@eml.cc>

;; Copyright (C) 2008-2024 Free Software Foundation, Inc.
;;
;; This file 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 file.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:
;;
;; Support for Semantic Databases for MATLAB buffers.
;; Based heavily on semanticdb-skel.el (C) Eric Ludlam


;; For generic function searching.
(require 'eieio)
(require 'eieio-opt)

;; eval-and-compile needed because of the condition-case.
;; Normally a require is eval'd during compile, so this is the same.
(eval-and-compile
  (condition-case nil
      (require 'semanticdb)
    (error (require 'semantic/db))))

(eval-and-compile
  (require 'matlab)
  (require 'matlab-shell))

;;; Code:

;; Put all directories which should be recursively scanned for your
;; personal MATLAB files here.

(defvar semanticdb-matlab-include-paths
  (if (file-exists-p (expand-file-name "~/matlab"))
      (list (expand-file-name "~/matlab") ;; Default location for extra code.
	    )
    ;; Else, no default path.
    nil)
  "Directories which should be scanned for m-files.")

;;; Classes:
(defclass semanticdb-table-matlab (semanticdb-search-results-table)
  ((major-mode :initform 'matlab-mode)
   )
  "A table for returning search results from MATLAB path.")

(defclass semanticdb-project-database-matlab
  (semanticdb-project-database
   ;; Use SINGLETON if there should be only one copy of this database.
   ;; Do not use this if you need a different copy for different projects.
   ;; eieio-singleton
   )
  ((new-table-class :initform 'semanticdb-table-matlab
		    :type class
		    :documentation
		    "New tables created for this database are of this class.")
   )
  "Database representing MATLAB path.")

;; Create the database, and add it to searchable databases for matlab mode.
(defvar-mode-local matlab-mode semanticdb-project-system-databases
  (list
   (make-instance 'semanticdb-project-database-matlab))
  "Search MATLAB path for symbols.")

;; NOTE: Be sure to modify this to the best advantage of your
;;       language.
(defvar-mode-local matlab-mode semanticdb-find-default-throttle
  '(project omniscience)
  "Search project files, then search this omniscience database.
It is not necessary to system or recursive searching because of
the omniscience database.")

;;; Filename based methods
;;
(cl-defmethod semanticdb-get-database-tables ((obj semanticdb-project-database-matlab))
  "For a MATLAB database OBJ, there are no explicit tables.
Create one of our special tables that can act as an intermediary."
  ;; NOTE: This method overrides an accessor for the `tables' slot in
  ;;       a database.  You can either construct your own (like tmp here
  ;;       or you can manage any number of tables.

  ;; We need to return something since there is always the "master table"
  ;; The table can then answer file name type questions.
  (when (not (slot-boundp obj 'tables))
    (let ((newtable (make-instance 'semanticdb-table-matlab)))
      (oset obj tables (list newtable))
      (oset newtable parent-db obj)
      (oset newtable tags nil)
      ))
  (cl-call-next-method))

(cl-defmethod semanticdb-file-table ((obj semanticdb-project-database-matlab) filename)
  "From OBJ, return FILENAME's associated table object."
  (ignore filename)
  ;; NOTE: See not for `semanticdb-get-database-tables'.
  (car (semanticdb-get-database-tables obj))
  )

(cl-defmethod semanticdb-get-tags ((table semanticdb-table-matlab ))
  "Return the list of tags belonging to TABLE."
  ;; NOTE: Omniscient databases probably don't want to keep large tabes
  ;;       lolly-gagging about.  Keep internal Emacs tables empty and
  ;;       refer to alternate databases when you need something.
  (ignore table)
  nil)

(cl-defmethod semanticdb-equivalent-mode ((table semanticdb-table-matlab) &optional buffer)
  "Return non-nil if TABLE's mode is equivalent to BUFFER.
Equivalent modes are specified by `semantic-equivalent-major-modes'
local variable."
  (ignore table)
  (with-current-buffer buffer
    (eq (or mode-local-active-mode major-mode) 'matlab-mode)))

(cl-defmethod semanticdb-full-filename ((obj semanticdb-table-matlab))
  "Fetch the full filename that OBJ refers to.
This function is currently a stub."
  (ignore obj)
  ;; FIXME
  ;; return filename for object - what should we do with builtin functions?
  nil)

;;; Usage
;;
;; Unlike other tables, an omniscient database does not need to
;; be associated with a path.  Use this routine to always add ourselves
;; to a search list.
(define-mode-local-override semanticdb-find-translate-path matlab-mode
  (path brutish)
  "Return a list of semanticdb tables associated with PATH.
If brutish, do the default action.
If not brutish, do the default action, and append the system
database (if available.)"
  (let ((default
	  ;; When we recurse, disable searching of system databases
	  ;; so that our MATLAB database only shows up once when
	  ;; we append it in this iteration.
	  (let ((semanticdb-search-system-databases nil)
		)
	    (if (fboundp 'semanticdb-find-translate-path-default)
		(semanticdb-find-translate-path-default path brutish)
	      (error "Variable semanticdb-find-translate-path-default doesn't exist")
	      ))))
    ;; Don't add anything if BRUTISH is on (it will be added in that fcn)
    ;; or if we aren't supposed to search the system.
    (if (or brutish (not semanticdb-search-system-databases))
	default
      (let ((tables (apply #'append
			   (mapcar
			    (lambda (db) (semanticdb-get-database-tables db))
			    semanticdb-project-system-databases))))
	(append default tables)))))

;;; Search Overrides
;;
;; NOTE WHEN IMPLEMENTING: Be sure to add doc-string updates explaining
;; how your new search routines are implemented.
;;


(defvar semanticdb-matlab-system-files-cache '(nil)
  "Internal cache for system M files.
This variable caches all M files in the directories listed in
`semantic-matlab-system-paths-include' under MATLAB root
directory.  Users can reset this cache using
`semanticdb-matlab-reset-files-cache'")

(defvar semanticdb-matlab-user-files-cache '(nil)
  "Internal cache for user M files.
This variable caches all M files in the directories listed in
`semanticdb-matlab-include-paths'.  Users can reset this cache
using `semanticdb-matlab-reset-files-cache'.")

(defvar semanticdb-matlab-user-class-cache nil
  "Internal cache for user classes.")

(defun semanticdb-matlab-reset-files-cache ()
  "Reset semanticdb-matlab file cache."
  (interactive)
  (setq semanticdb-matlab-user-files-cache '(nil))
  (setq semanticdb-matlab-system-files-cache '(nil)))

(defun semanticdb-matlab-possibly-add-buffer-to-cache ()
  "Add current buffer file name to cache.
This function will add the current buffer file name to
`semanticdb-matlab-user-files-cache' if not already there.  Meant
to be called in local `after-save-hook'."
  (unless (and semanticdb-matlab-user-files-cache
	       (member (buffer-file-name)
		       (cdr semanticdb-matlab-user-files-cache)))
    (setcdr semanticdb-matlab-user-files-cache
	  (append (cdr semanticdb-matlab-user-files-cache)
		  (list (buffer-file-name))))))

;; Make sure newly created MATLAB files get in the user-files-cache
(add-hook 'matlab-mode-hook
	  (lambda ()
	    ;; add buffer-local after-save-hook
	    (add-hook
	     'after-save-hook
	     #'semanticdb-matlab-possibly-add-buffer-to-cache t t)))

;; Helper functions

(defun semanticdb-matlab-scan-directories
  (dirs &optional recursive exclude-classes exclude-private)
  "Get list of all m-files in DIRS.
DIRS is a list of directories.  If RECURSIVE, every subdirectory
will be included in the search.  If EXCLUDE-CLASSES, class
directories (beginning with \\='@\\=') will be skipped.  If
EXCLUDE-PRIVATE, \\='private\\=' directories will be skipped."
  (if dirs
      (let (files)
	(dolist (dir dirs)
	  (let (subdirs)
	    (dolist (cur (directory-files dir t "[^.]" t))
	      (if (file-directory-p cur)
		  (when (and recursive
			     (not (and exclude-classes
				       (string-match ".*/@" cur)))
			     (not (and exclude-private
				       (string-match ".*/private$" cur))))
		    (push cur subdirs))
		(when (string-match "\\.m$" cur)
		  (push cur files))))
	    (when subdirs
	      (setq files
		    (append files
			    (semanticdb-matlab-scan-directories
			     subdirs recursive exclude-classes exclude-private))))))
	files)
    nil))

(defvar semantic-matlab-dependency-system-include-path) ;; quiet compiler warning

(defun semanticdb-matlab-cache-files ()
  "Cache user and system MATLAB files if necessary."
  ;; car of *-file-cache variables is used as flag
  (unless (car semanticdb-matlab-system-files-cache)
    (setq semanticdb-matlab-system-files-cache
	  (cons t
		(semanticdb-matlab-scan-directories
		 semantic-matlab-dependency-system-include-path t t t))))
  (unless (car semanticdb-matlab-user-files-cache)
    (setq semanticdb-matlab-user-files-cache
	  (cons t
		(semanticdb-matlab-scan-directories
		 semanticdb-matlab-include-paths t nil nil)))
    ))

(defun semanticdb-matlab-find-name (name &optional type)
  "Find NAME in matlab file names.
If TYPE is \\='regex, NAME is a regular expression.
If TYPE is \\='prefix, NAME is a prefix."
  (semanticdb-matlab-cache-files)
  (let ((files (append (cdr semanticdb-matlab-system-files-cache)
		       (cdr semanticdb-matlab-user-files-cache)))
	regexp results)
    (cond
     ((eq type 'prefix)
      (setq regexp (format "^%s.*\\.m$" name)))
     ((eq type 'regex)
      (setq regexp (format "%s\\.m$" name)))
     (t
      (setq regexp (format "^%s\\.m" name))))
    (dolist (cur files)
      (when (string-match regexp (file-name-nondirectory cur))
	(push cur results)))
    results))

(define-mode-local-override semantic-ctxt-current-class-list
  matlab-mode (point)
  "Return a list of tag classes that are allowed at point.
If point is nil, the current buffer location is used."
  (ignore point)
  (cond
   ((looking-at ".+=")
    '(variable type))
   ((looking-back "\\(get\\|set\\)([a-zA-Z_0-9]*" nil)
    '(variable type))
   ((looking-back "\\(get\\|set\\)([a-zA-Z_0-9]+,'[a-zA-Z_0-9]*" nil)
    '(variable))
   ((looking-back "\\.[a-zA-Z_0-9]*" nil)
    '(variable))
   ((looking-at "\\s-*([a-zA-Z_0-9]+,")
    '(function))
   (t
    '(function variable type))))

;; Search functions

(cl-defmethod semanticdb-find-tags-by-name-method
  ((table semanticdb-table-matlab) name &optional tags)
  "Find all tags named NAME in TABLE.
Return a list of tags."
  (ignore table)
  ;; If we have tags, go up.
  (if tags (cl-call-next-method)
    (let (where)
      ;; If MATLAB shell is active, use it.
      (when (and (matlab-shell-active-p)
		 (setq where (matlab-shell-which-fcn name)))
	(when (and (not (file-exists-p (car where)))
		   ;; Sometimes MATLAB builtin functions lie.
		   (string-match "@" (car where)))
	  (setq where
		(list
		 (concat
		  (substring (car where) 0 (match-beginning 0))
		  name ".m")))))
      (unless (car where)
	;; Fall back to home-made database.
	(setq where
	      (list (car (semanticdb-matlab-find-name name)))))
      (if (car where)
	  (list (car (semanticdb-file-stream (car where))))
	nil))))

(cl-defmethod semanticdb-find-tags-by-name-regexp-method
  ((table semanticdb-table-matlab) regex &optional tags)
  "Find all tags with name matching REGEX in TABLE.
Optional argument TAGS is a list of tags to search.
Return a list of tags."
  (ignore table)
  (if tags (cl-call-next-method)
    (let ((files (semanticdb-matlab-find-name regex 'regex)))
      (delq nil
	    (mapcar #'(lambda (x)
			(car (semanticdb-file-stream x)))
		    files)))))

(cl-defmethod semanticdb-find-tags-for-completion-method
  ((table semanticdb-table-matlab) prefix &optional tags)
  "In TABLE, find all occurrences of tags matching PREFIX.
Optional argument TAGS is a list of tags to search.
Returns a table of all matching tags."
  (ignore table)
  ;; If we have tags, go up.
  (if tags (cl-call-next-method)
    ;; first, get completions from home-made database...
    (let ((compdb (semanticdb-matlab-find-name prefix 'prefix))
	  compshell)
      ;; ...and from MATLAB shell, if available
      (when (matlab-shell-active-p)
	(setq compshell
	      (mapcar
	       (lambda (x)
		 (when (stringp x)
		   (let ((where (matlab-shell-which-fcn (car x))))
		     ;; correct name for builtin functions
		     (when (and (cdr where)
				(string-match
				 "\\(.*\\)/@.*\\(/[A-Za-z_0-9]+\\.m\\)"
				 (car where)))
		       (setq where
			     (list
			      (concat (match-string 1 (car where))
				      (match-string 2 (car where))))))
		     (list (car where)))))
	       (matlab-shell-completion-list prefix)))
	;; combine results
	(mapc
	 (lambda (x)
	   (unless (member x compdb)
	     (setq compdb (append compdb x))))
	 compshell))
      ;; generate tags
      (delq nil
	    (mapcar #'(lambda (x)
			(car (semanticdb-file-stream x)))
		    compdb)))))

(provide 'semanticdb-matlab)

;;; semanticdb-matlab.el ends here

;; LocalWords:  Engster dengste skel Ludlam eieio eval'd defclass initform defmethod tmp boundp
;; LocalWords:  newtable oset FILENAME's TABLE's fboundp mapcar defun setq cdr setcdr dirs dolist
;; LocalWords:  subdirs nondirectory ctxt delq compdb compshell stringp mapc
