;;; rudel-debug.el --- Debugging functions for Rudel  -*- lexical-binding:t -*-
;;
;; Copyright (C) 2009-2021  Free Software Foundation, Inc.
;;
;; Author: Jan Moringen <scymtym@users.sourceforge.net>
;; Keywords: rudel, debugging
;; X-RCS: $Id:$
;;
;; This file is part of Rudel.
;;
;; Rudel 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.
;;
;; Rudel 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 rudel. If not, see <http://www.gnu.org/licenses>.


;;; Commentary:
;;
;; This file contains debugging functions for Rudel. The most
;; important aspects are functions to data-debug central Rudel objects
;; and tracing support for basic Rudel objects.


;;; History:
;;
;; 0.2 - New tracing framework
;;
;; 0.1 - Initial version


;;; Code:
;;

(require 'cl-generic)
(require 'eieio)
(require 'data-debug)
(require 'eieio-datadebug)
(require 'rudel)
(require 'rudel-util)
(require 'rudel-state-machine)
(require 'rudel-transport-util)
(require 'rudel-socket)


;;; Customization
;;

(defgroup rudel-debug nil
  "Customization options related to Rudel's debugging functions."
  :group 'rudel)

(defface rudel-debug-sent-data-face
  '((default (:background "orange")))
  "Face used for sent data.")

(defface rudel-debug-received-data-face
  '((default (:background "light sky blue")))
  "Face used for received (but not yet processed) data.")

(defface rudel-debug-state-face
  '((default (:background "light gray")))
  "Face used when indicating state changes.")

(defface rudel-debug-special-face
  '((default (:background "light sea green")))
  "Face used for additional information.")

(defvar rudel-debug-tag-faces
  '((:sent     . (rudel-debug-sent-data-face     "<  "))
    (:received . (rudel-debug-received-data-face ">  "))
    (:state    . (rudel-debug-state-face         "|  "))
    (:special  . (rudel-debug-special-face       ";  ")))
  "Associate tag to faces and prefixes.")


;;; Data debug functions
;;
(defvar rudel-current-session)
(defvar rudel-buffer-document)

(defun rudel-adebug-discover ()
  "Analyze list of discoverable sessions in data debug buffer."
  (interactive)

  (with-current-buffer (data-debug-new-buffer "RUDEL-DISCOVERED-SESSIONS")
    (data-debug-insert-stuff-list (rudel-session-initiation-discover) "# ")))

(defun rudel-adebug-session ()
  "Analyze current session in data debug buffer."
  (interactive)

  ;; Make sure we have a session.
  (unless rudel-current-session
    (error "No active Rudel session"))

  (with-current-buffer (data-debug-new-buffer "RUDEL-SESSION")
    (data-debug-insert-thing rudel-current-session "# " "")))

(defun rudel-adebug-server (server)
  "Analyze server in data debug buffer."
  (interactive)

  (with-current-buffer (data-debug-new-buffer "RUDEL-SERVER")
    (data-debug-insert-thing server "# " "")))


;;; Advice stuff
;;

(advice-add 'rudel-join-session :after #'rudel--debug-join-session)
(defun rudel--debug-join-session (&rest _)
  "Run data-debug inspection on newly created session objects."
  (rudel-adebug-session))

(advice-add 'rudel-host-session :around #'rudel--debug-host-session)
(defun rudel--debug-host-session (orig-fun &rest args)
  "Run data-debug inspection on newly created server objects."
  (rudel-adebug-server (apply orig-fun args)))


;;; Network functions
;;

(defun rudel-suspend-session-socket ()
  "Suspend the socket associated to the current session."
  (interactive)

  ;; Make sure we have a session.
  (unless rudel-current-session
    (error "No active Rudel session"))

  (with-slots (connection) rudel-current-session
    (with-slots (socket) connection
      (stop-process socket))))

(defun rudel-resume-session-socket ()
  "Resume the socket associated to the current session."
  (interactive)

  ;; Make sure we have a session.
  (unless rudel-current-session
    (error "No active Rudel session"))

  (with-slots (connection) rudel-current-session
    (with-slots (socket) connection
      (continue-process socket))))


;;; Reset functions
;;

(defun rudel-kill-processes ()
  "TODO"
  (interactive)
  (mapc #'delete-process (process-list)))

(defun rudel-reset ()
  "TODO"
  (interactive)
  (dolist (buffer (buffer-list))
    (with-current-buffer buffer
      (when rudel-buffer-document
	(setq rudel-buffer-document nil))))
  (rudel-kill-processes)
  (setq rudel-current-session nil))


;;; Utility functions
;;

(cl-defgeneric rudel-debug-target (object)
  "Return debug stream name for OBJECT.")

(cl-defmethod rudel-debug-target ((this eieio-default-superclass))
  "Default implementation simply uses the object name of THIS."
  (object-name-string this))

(defun rudel-debug-write-to-stream (stream tag label data
				    &optional object)
  "Insert DATA and maybe OBJECT into stream using TAG, LABEL as style."
  (let* ((buffer-name (format "*%s stream*" stream))
	 (buffer      (or (get-buffer buffer-name)
			  (data-debug-new-buffer buffer-name)))
	 (appearance  (cdr (assoc tag rudel-debug-tag-faces)))
	 (face        (when appearance
			(or (nth 0 appearance)
			    'default)))
	 (prefix      (or (nth 1 appearance)
			  ""))
	 (string      (cond
		       ((stringp data)
			data)
		       (t
			(cl-prin1-to-string data)))))
    (with-current-buffer buffer
      (goto-char 0)
      (insert prefix
	      (if label
		  (format
		   "%-8s"
		   (propertize label 'face 'font-lock-type-face))
		"        ")
	      " "
	      (propertize string 'face face)
	      (if (and (>= (length string) 1)
		       (string= (substring string -1) "\n"))
		  "" "\n"))
      (when object
	(data-debug-insert-thing
	 object
	 (concat prefix
		 (propertize "OBJECT   " 'face 'font-lock-type-face))
	 ""))))
    )

(defun rudel-debug-write (source tag label data &optional object)
  "Write DATA and OBJECT to debug stream associated to SOURCE.
TAG and LABEL determine the logging style."
  (rudel-debug-write-to-stream
   (rudel-debug-target source) tag label data object))


;;; State machine debugging
;;

(defvar rudel-debug-old-state nil
  "Saves state of state machines across one function call.")

(cl-defmethod rudel-switch :before
  ((this rudel-state-machine) _state &rest _)
  "Store name of STATE for later printing."
  (with-slots (state) this
    (setq rudel-debug-old-state
	  (if state (object-name-string state) "#start")))
  )

(cl-defmethod rudel-switch :after
  ((this rudel-state-machine) _state &rest arguments)
  "Log STATE and ARGUMENTS to debug stream."
  (with-slots (state) this
    (let ((old-state rudel-debug-old-state)
	  (new-state (object-name-string state)))
      (unless (string= old-state new-state)
	(rudel-debug-write
	 this
	 :special
	 "FSM"
	 (if arguments
	     (format "%s -> %s %s" old-state new-state arguments)
	   (format "%s -> %s" old-state new-state))))))
  )


;;; Debugging functions for `rudel-transport-filter'
;;

(cl-defmethod rudel-debug-target ((this rudel-transport-filter))
  "Find target of filter THIS by looking at underlying transport."
  (with-slots (transport) this
    (rudel-debug-target transport)))


;;; Debugging functions for `rudel-assembling-transport-filter'
;;

(cl-defmethod rudel-set-assembly-function :before
  ((this rudel-assembling-transport-filter) function)
  "Log change of assembly function to FUNCTION."
  (with-slots (socket assembly-function) this
    (rudel-debug-write
     this
     :special
     "ASSEMBLE"
     (format "%s -> %s"
	     (symbol-name assembly-function)
	     (symbol-name function))))
  )

(cl-defmethod rudel-set-filter ((this rudel-assembling-transport-filter)
			     filter1)
  "Log DATA as it goes through THIS."
  (with-slots (filter) this
    (setq filter (lambda (data)
                   (rudel-debug-write
                    this
                    :received
                    "ASSEMBLE"
                    data)
                   (funcall filter1 data)))))

(cl-defmethod rudel-send :before
  ((this rudel-assembling-transport-filter) data)
  "Log DATA as it goes through THIS."
  (rudel-debug-write this :sent "RAW" data nil))


;;; Debugging function `rudel-parsing-transport-filter'
;;

(cl-defmethod rudel-set-parse-function :before
  ((this rudel-parsing-transport-filter) function)
  "Log parse function change to FUNCTION."
  (with-slots (parse-function) this
    (rudel-debug-write
     this
     :special
     "PARSE"
     (format "%s -> %s"
	     (symbol-name parse-function)
	     (symbol-name function))))
  )

(cl-defmethod rudel-set-generate-function :before
  ((this rudel-parsing-transport-filter) function)
  "Log generate function change to FUNCTION."
  (with-slots (generate-function) this
    (rudel-debug-write
     this
     :special
     "GENERATE"
     (format "%s -> %s"
	     (symbol-name generate-function)
	     (symbol-name function))))
  )

(cl-defmethod rudel-set-filter ((this rudel-parsing-transport-filter)
			     filter1)
  "Log DATA as it goes through THIS."
  (with-slots (filter) this
    (setq filter (lambda (data)
                   (rudel-debug-write
                    this
                    :received
                    "PARSE"
                    (format "%s" data) data)
                   (funcall filter1 data)))))

(cl-defmethod rudel-send :before
  ((this rudel-parsing-transport-filter) string-or-data)
  "Log STRING-OR-DATA as it goes through THIS."
  (let ((formatted (cond
		    ((stringp string-or-data)
		     string-or-data)
		    (t
		     (cl-prin1-to-string string-or-data)))))
    (rudel-debug-write
     this
     :sent
     "GENERATE"
     formatted (unless (stringp string-or-data)
		 string-or-data)))
  )


;;; Socket transport debugging
;;

(cl-defmethod rudel-set-filter ((this rudel-socket-transport)
			     filter)
  "Log DATA as it goes through THIS."
  (setf (slot-value this :filter)
   (lambda (data)
     (rudel-debug-write this :received "SOCKET" data)
     (funcall filter data))))

(cl-defmethod rudel-send :before ((this rudel-socket-transport)
			       data)
  "Log DATA verbatim as it is sent through the socket of THIS."
  (rudel-debug-write this :sent "SOCKET" data nil))

(provide 'rudel-debug)
;; Local Variables:
;; no-byte-compile: nil
;; End:
;;; rudel-debug.el ends here
