;;; drepl-usql.el --- SQL shell based on the usql program  -*- lexical-binding: t; -*-

;; Copyright (C) 2024  Free Software Foundation, Inc.

;; Author: Augusto Stoffel <arstoffel@gmail.com>
;; Keywords: languages, processes
;; URL: https://github.com/astoff/drepl

;; 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:

;; This file defines a shell for various SQL and NoSQL databases.  It
;; is based on the usql program and requires a Go compiler.

;;; Code:

;;; Customization options
(require 'drepl)
(require 'comint-mime)

(defgroup drepl-usql nil
  "SQL shell implemented via dREPL."
  :group 'drepl
  :group 'SQL
  :link '(url-link "https://github.com/astoff/drepl"))

(defvar drepl-usql--directory
  (expand-file-name "drepl-usql/" (file-name-directory
                                   (or load-file-name default-directory)))
  "Directory containing the `drepl-usql' source code.")

(defvar drepl-usql--connection-history nil
  "History list of database connections.")

(defcustom drepl-usql-program
  (expand-file-name "drepl-usql" drepl-usql--directory)
  "Name of the drepl-usql executable."
  :type 'string)

(defcustom drepl-usql-drivers
  '(clickhouse csvq mysql oracle postgres sqlite3 sqlserver)
  "List of drivers to include when building the `drepl-usql' executable.
See https://github.com/xo/usql#database-support for the available
options (second column of the table).

Make sure to run \\[drepl-usql-build] after setting this option."
  :type '(repeat symbol))

(defun drepl-usql-build ()
  "Build the `drepl-usql' executable."
  (interactive)
  (let ((default-directory drepl-usql--directory)
        (gocmd (or (bound-and-true-p go-command) "go")))
    (with-temp-file "drivers.go"
      (insert "package main\n\nimport (\n")
      (dolist (s drepl-usql-drivers)
        (insert (format "\t_ \"github.com/xo/usql/drivers/%s\"\n" s)))
      (insert ")\n"))
    (compile (format "%s build -v" (shell-quote-argument gocmd)))))

;;;###autoload (autoload 'drepl-usql "drepl-usql" nil t)
(drepl--define drepl-usql :display-name "usql")

(cl-defmethod drepl--command ((_ drepl-usql))
  (if-let* ((prog (executable-find drepl-usql-program)))
      (list prog (read-from-minibuffer "Connect to database: "
                                       nil nil nil
                                       'drepl-usql--connection-history))
    (lwarn 'drepl-usql :error "\
`%s' not found.
Use %s to build it.
Note that this requires a Go compiler."
           drepl-usql-program
           (propertize "M-x drepl-usql-build" 'face 'help-key-binding))
    (user-error "%s not found" drepl-usql-program)))

(defun drepl-usql--comint-indirect-setup ()
  "Function to set up indentation in the comint indirect buffer."
  ;; This at least ensures TAB completion works.
  (setq-local indent-line-function #'ignore)
  (when (fboundp 'sql-indent-enable) (sql-indent-enable)))

(cl-defmethod drepl--eval ((repl drepl-usql) code)
  "Send an eval request to REPL with CODE as argument."
  (let ((cb (lambda (data)
              (when (string= (alist-get 'format data) 'html)
                (save-excursion
                  (let* ((start (previous-single-char-property-change (point) 'field))
                         (end (point))
                         (html (buffer-substring-no-properties start end)))
                    (when (eq (char-after start) ?<)
                      (comint-mime-render-html '((type . "text/html")) html)
                      (delete-region start end))))))))
    (drepl--communicate repl cb 'eval :code code)))

(cl-defmethod drepl--init ((repl drepl-usql))
  (cl-call-next-method repl)
  (push '("5151" . comint-mime-osc-handler) ansi-osc-handlers)
  (add-hook 'comint-indirect-setup-hook #'drepl-usql--comint-indirect-setup nil t)
  (drepl--adapt-comint-to-mode ".sql"))

(provide 'drepl-usql)

;;; drepl-usql.el ends here
