;;; matlab-netshell.el --- Control MATLAB from a network port. -*- lexical-binding: t -*-

;; Author: Eric Ludlam <zappo@gnu.org>

;; Copyright (C) 2024 Free Software Foundation, Inc.
;;
;; 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:
;;
;; Form a back-channel for Emacs to chat with a running MATLAB.
;; Allows you to edit for and with a MATLAB sessions, even if it is not in a
;; matlab-shell buffer.

(require 'matlab)

;;; Code:
(declare-function matlab-shell-active-p "matlab-shell" ())
(declare-function matlab-shell--get-emacsclient-command "matlab-shell" ())
(declare-function matlab-shell-mode-gud-enable-bindings "matlab-shell-gud" ())

(defvar matlab-netshell-listen-port 32475
  "Port used for the Emacs server listening for MATLAB connections.")

(defvar matlab-netshell-server-name "*MATLAB netshell*"
  "Name used for the Netshell server.")

(defvar matlab-netshell-clients nil
  "List of clients created from the MATLAB netshell server.")

;;;###autoload
(defun matlab-netshell-server-active-p ()
  "Return non-nil if there is an active MATLAB netshell server."
  (let ((buff (get-buffer matlab-netshell-server-name)))
    (when (and buff (get-buffer-process buff))
      t)))

;;;###autoload
(defun matlab-netshell-server-start ()
    "Start the MATLAB netshell server."
    (interactive)
    (make-network-process :name matlab-netshell-server-name
			  :buffer matlab-netshell-server-name :family 'ipv4 :host 'local
			  :service matlab-netshell-listen-port
			  :filter #'matlab-netshell-filter
			  :sentinel #'matlab-netshell-sentinel
			  :server t)
    (setq matlab-netshell-clients nil)
    

    )

(defun matlab-netshell-client ()
  "Return a netshell client."
  (car matlab-netshell-clients))

(defun matlab-netshell-server-stop nil
  "Stop the MATLAB Netshell server."
  (interactive)
  (dolist (C matlab-netshell-clients)
    (delete-process C))
  (setq matlab-netshell-clients nil)
  (delete-process matlab-netshell-server-name)
  )

(defvar matlab-netshell-acc ""
  "Text Accumulator for MATLAB's netshell.")
(make-variable-buffer-local 'matlab-netshell-acc)

(defun matlab-netshell-filter (proc string)
  "Filter used for MATLAB Netshell processes.
PROC is the TCP connection that produced STRING."
  ;; Accumulate from the process
  (setq matlab-netshell-acc (concat matlab-netshell-acc string))
  ;; Wait for a NULL command terminator.
  (while (string-match "\0" matlab-netshell-acc)
    (let* ((cmdstr (substring matlab-netshell-acc 0 (match-beginning 0))))
      ;; Trim off our new command from accumulator.
      (setq matlab-netshell-acc (substring matlab-netshell-acc (match-end 0)))

      ;; Is this a test packet?  If so, we're done.
      (when (not (string= cmdstr ""))
      
	;; Find the newline.
	(unless (string-match "\n" cmdstr)
	  (message "Unable to find command in MATLAB command.  Ignoring."))
	
	(let ((cmd (substring cmdstr 0 (match-beginning 0)))
	      (data
	       (let ((me (match-end 0)))
		 (if (> (length cmdstr) me)
		     (substring cmdstr me)
		   ""))))
	  (matlab-netshell-execute-command proc cmd data)
	  )))))
  
(defun matlab-netshell-execute-command (proc cmd data)
  "For MATLAB associated with PROC, execute CMD with DATA.
The CMD is a request from MATLAB to do something in Emacs.
A common command might be to display data to the user as a
response from some Emacs based request."
  ;; Log the command
  (with-current-buffer (process-buffer proc)
    (goto-char (point-max))
    (when (not (= (current-column) 0))
      (insert "\n"))
    (insert "Command: [" cmd "]\n")
    (when (not (string= data ""))
      (insert "Data: :" data "\n"))
    )
  ;; Interpret the command.
  (cond ((string= "init" cmd)
	 ;; Make sure GUD bindings are available, but do so in
	 ;; the netshell buffer so when mlgud bindings run, they
	 ;; don't stomp on C-c matlab-mode bindings.
	 (with-current-buffer (process-buffer proc)
	   (matlab-shell-mode-gud-enable-bindings))
	 ;; Send info about emacs client
	 (when (not (matlab-shell-active-p))
	   (let* ((ecc (matlab-shell--get-emacsclient-command))
	 	  (ecca (if ecc (format "emacs.set('clientcmd', '%s');" ecc))))
	     (when ecc
	       (matlab-netshell-eval ecca))))
	 (message "MATLAB connection initialized.")
	 )
	((string= "ack" cmd)
	 (message "Ack received.  Send ACK back.")
	 (matlab-netshell-send "nowledge" ""))
	((string= "nowledge" cmd)
	 (message "Acknowledgement received."))
	((string= "output" cmd)
	 (message "Output: %S" data))
	((string= "error" cmd)
	 (message "MATLAB Error: %s" data))
	((string= "eval" cmd)
	 ;; (message "MATLAB Evaluating remote request")
	 (let ((forms (read data)))
	   (eval forms t)))
	(t
	 (message "Unknown command from matlab: %S" cmd)
	 )))

(defun matlab-netshell-sentinel (proc msg)
  "Sentinel used for MATLAB Netshell processes.
Identify when a connection is lost, and close down services.
PROC is the TCP stream which generated MSG."
  (cond ((string-match "^open from " msg)
	 ;; New connection - set it up.
	 (setq matlab-netshell-clients (cons proc matlab-netshell-clients))
	 (let ((newbuff (get-buffer-create (process-name proc))))
	   (set-process-buffer proc newbuff))
	 (message "MATLAB Has connected!"))
  
	((string= msg "connection broken by remote peer\n")
	 (setq matlab-netshell-clients (delq proc matlab-netshell-clients))
	 (message (format "MATLAB has dropped its connection")))

	(t
	 (message "Unhandled event."))))

(defun matlab-netshell-send(cmd data)
  "Send a command CMD to MATLAB shell connection with DATA."
  (let ((C (car matlab-netshell-clients)))
    (if C
	(process-send-string C (concat cmd "\n" data "\0"))
      (error "No MATLAB network connection to send to"))))

(defun matlab-netshell-eval (mcode)
  "Send MCODE to the active MATLAB shell connection to eval."
  (interactive "sMCode: ")
  (let ((C (car matlab-netshell-clients)))
    (if C
	(process-send-string C (concat "eval\n" mcode "\0"))
      (error "No MATLAB network connection to send to"))))

(defun matlab-netshell-evalc (mcode)
  "Send MCODE to the active MATLAB shell connection to eval."
  (interactive "sMCode: ")
  (let ((C (car matlab-netshell-clients)))
    (if C
	(process-send-string C (concat "evalc\n" mcode "\0"))
      (error "No MATLAB network connection to send to"))))

(defun matlab-netshell-ack ()
  "Send an ACK to MATLAB to see if it can respond."
  (interactive)
  (matlab-netshell-send "ack" ""))
  

;;(matlab-netshell-server-start)
;;(sleep-for 300)
;;(matlab-netshell-server-stop)


(provide 'matlab-netshell)

;;; matlab-netshell.el ends here

;; LocalWords:  Ludlam zappo emacsclient gud defun ipv setq dolist cmdstr mlgud ecc ecca clientcmd
;; LocalWords:  nowledge newbuff delq mcode MCode
