;;; gdscript-utils.el --- Utility functions for gdscript-mode  -*- lexical-binding: t; -*-

;; Copyright (C) 2020-2026 GDQuest and contributors

;; Author: Nathan Lovato <nathan@gdquest.com>
;;         Fabián E. Gallina <fgallina@gnu.org>
;; Maintainer: Jen-Chieh Shen <jcs090218@gmail.com>

;; 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, see <https://www.gnu.org/licenses/>.

;;; Commentary:
;;
;; Misc utility functions for GDScript mode.
;;

;;; Code:

(require 'gdscript-syntax)
(require 'gdscript-customization)

(declare-function projectile-completing-read (prompt collections) "ext:projectile.el")

(defun gdscript--util-goto-line (line-number)
  "Move point to LINE-NUMBER."
  (goto-char (point-min))
  (forward-line (1- line-number)))

(defun gdscript--util-forward-comment (&optional direction)
  "Gdscript mode specific version of `forward-comment'.
Optional argument DIRECTION defines the direction to move to."
  (let ((comment-start (gdscript-syntax-context 'comment))
        (factor (if (< (or direction 0) 0)
                    -99999
                  99999)))
    (when comment-start
      (goto-char comment-start))
    (forward-comment factor)))

(defun gdscript--util-list-directories (directory &optional predicate max-depth)
  "List DIRECTORY subdirs, filtered by PREDICATE and limited by MAX-DEPTH.
Argument PREDICATE defaults to `identity' and must be a function
that takes one argument (a full path) and returns non-nil for
allowed files.  When optional argument MAX-DEPTH is non-nil, stop
searching when depth is reached, else don't limit."
  (let* ((dir (expand-file-name directory))
         (dir-length (length dir))
         (predicate (or predicate #'identity))
         (to-scan (list dir))
         (tally nil))
    (while to-scan
      (let ((current-dir (car to-scan)))
        (when (funcall predicate current-dir)
          (setq tally (cons current-dir tally)))
        (setq to-scan (append (cdr to-scan)
                              (gdscript--util-list-files
                               current-dir #'file-directory-p)
                              nil))
        (when (and max-depth
                   (<= max-depth
                       (length (split-string
                                (substring current-dir dir-length)
                                "/\\|\\\\" t))))
          (setq to-scan nil))))
    (nreverse tally)))

(defun gdscript--util-list-files (dir &optional predicate)
  "List files in DIR, filtering with PREDICATE.
Argument PREDICATE defaults to `identity' and must be a function
that takes one argument (a full path) and returns non-nil for
allowed files."
  (let ((dir-name (file-name-as-directory dir)))
    (apply #'nconc
           (mapcar (lambda (file-name)
                     (let ((full-file-name (expand-file-name file-name dir-name)))
                       (when (and
                              (not (member file-name '("." "..")))
                              (funcall (or predicate #'identity) full-file-name))
                         (list full-file-name))))
                   (directory-files dir-name)))))


(defun gdscript-util--find-project-configuration-file (&optional start-path)
  "Return the path to the file \"project.godot\".

Start the search from START-PATH if provided. Otherwise, the search
starts from the current buffer path.

WARNING: the Godot project must exist for this function to work."
  (let* ((base-path (or start-path default-directory))
         (dominating-file
          (locate-dominating-file base-path
                                  (lambda (parent)
                                    (directory-files parent t "project.godot")))))
    (when dominating-file (expand-file-name dominating-file))))

(defun gdscript-util--get-godot-project-name ()
  "Retrieve the project name from Godot's configuration file."
  (with-temp-buffer
    (insert-file-contents (concat (gdscript-util--find-project-configuration-file) "project.godot"))
    (goto-char (point-min))
    (if (re-search-forward "config/name=\"\\([^\"]*\\)\"" nil t)
        (match-string 1)
      (error "Could not find the name of the project"))))

(defun gdscript-util--get-godot-project-version ()
  "Retrieve the project name from Godot's configuration file."
  (with-temp-buffer
    (insert-file-contents (concat (gdscript-util--find-project-configuration-file) "project.godot"))
    (goto-char (point-min))
    (if (re-search-forward "config_version=\\(3\\|4\\)" nil t) ; Godot 3.0 uses 3, Godot 3.6.2 uses 4
        "3"
      (if (re-search-forward "config/features=PackedStringArray\(\"\\([^,\)]*\\)\"" nil t)
          (let ((version (match-string 1)))
            (if (string-prefix-p "4.0" version) "4" version))
        (error "Could not find the project version")))))

(defun gdscript-util--get-godot-buffer-name (&optional editor)
  "Return buffer name for godot's stdout/stderr output."
  (format (if editor "*godot - %s - Editor*" "*godot - %s*") (gdscript-util--get-godot-project-name)))

(defun gdscript-util--get-gdformat-buffer-name ()
  "Return buffer name for godot's stdout/stderr output."
  (format "*gdformat - %s*" (gdscript-util--get-godot-project-name)))

(defun gdscript-util--get-godot-project-file-path-relative (file-path)
  "Return the relative path of `FILE-PATH' to Godot's configuration file."
  (let ((project-configuration-file (gdscript-util--find-project-configuration-file)))
    (when project-configuration-file
      (concat (file-name-sans-extension
               (file-relative-name file-path project-configuration-file))))))

(defun gdscript-util--flatten (xs)
  "Flatten deeply nested list.

For example:
> (gdscript-util--flatten (list 1 2 (list 3 (list 4 5)) nil))
> (1 2 3 4 5)
"
  (cond
   ((null xs) nil)
   ((listp xs) (append (gdscript-util--flatten (car xs)) (gdscript-util--flatten (cdr xs))))
   (t (list xs))))

(defun gdscript-util--read (items &optional prompt)
  "Let's choose single item from ITEMS from mini-buffer.
PROMPT is prompt for read command. Return `nil' if user aborts."
  (let* ((p (if prompt prompt "Options"))
         (result (cond ((featurep 'projectile)
                        (projectile-completing-read (format "%s: " p) items))
                       (ido-mode
                        (ido-completing-read (format "%s: " p) items))
                       (t
                        (completing-read (format "%s: " p) items nil t)))))
    (if quit-flag nil result)))

(defmacro gdscript-util--with-available-hydra (&rest body)
  ""
  `(progn
     (when (not (featurep 'hydra))
       (error "No `hydra.el' available.  To execute `gdscript-hydra-show' command you need to install hydra.el"))
     ,@body))

(provide 'gdscript-utils)
;;; gdscript-utils.el ends here
