;;; phps-mode-serial.el --- Functions for serial commands with varied synchronicity -*- lexical-binding: t -*-

;; Copyright (C) 2018-2020  Free Software Foundation, Inc.

;;; Commentary:


;;; Code:

;; VARIABLES


(defvar phps-mode-serial--async-processes (make-hash-table :test 'equal)
  "Table of active asynchronous processes.")

(defvar phps-mode-serial--async-threads (make-hash-table :test 'equal)
  "Table of active asynchronous threads.")

(defvar phps-mode-serial--profiling nil
  "Boolean flag whether to profile or not.")

(defface phps-mode-serial--mode-line-face-running
  '((t :inherit warning))
  "Face for PHPs mode \"running\" asynchronous process mode line indicator."
  :group 'phps-mode)

(defface phps-mode-serial--mode-line-face-success
  '((t :inherit success))
  "Face for PHPs mode \"success\" asynchronous process mode line indicator."
  :group 'phps-mode)

(defface phps-mode-serial--mode-line-face-error
  '((t :inherit error))
  "Face for PHPs mode \"error\" asynchronous process mode line indicator."
  :group 'phps-mode)

(defvar-local phps-mode-serial--status nil
  "Current status of serial-commands.")

(defconst phps-mode-serial--mode-line-status-run
  '(":"
    (:propertize (:eval (if (equal phps-mode-serial--status 'running) "Running.." ""))
                 face phps-mode-serial--mode-line-face-running)
    (:propertize (:eval (if (equal phps-mode-serial--status 'error) "Error" ""))
                 face phps-mode-serial--mode-line-face-error)
    (:propertize (:eval (if (equal phps-mode-serial--status 'success) "OK" ""))
                 face phps-mode-serial--mode-line-face-success)))


;; FUNCTIONS


(defun phps-mode-serial--kill-active (key)
  "Kill active command KEY."
  (when (and
         (gethash key phps-mode-serial--async-processes)
         (process-live-p (gethash key phps-mode-serial--async-processes)))
    (let ((process-buffer (process-buffer (gethash key phps-mode-serial--async-processes))))
      (delete-process (gethash key phps-mode-serial--async-processes))
      (kill-buffer process-buffer)))
  (when (and
         (gethash key phps-mode-serial--async-threads)
         (thread-live-p (gethash key phps-mode-serial--async-threads)))
    (thread-signal (gethash key phps-mode-serial--async-threads) 'quit nil)))

(defun phps-mode-serial-commands (key start end &optional start-error end-error async async-by-process)
  "Run command with KEY, first START and if successfully then END with the result of START as argument.  Optional arguments START-ERROR, END-ERROR that are called on errors. ASYNC ASYNC-BY-PROCESS specifies additional options for synchronicity."
  (let ((start-time (current-time)))
    (when phps-mode-serial--profiling
      (message "PHPs - Starting serial commands for buffer '%s'.." key))
    (with-current-buffer key
      (setq mode-line-process phps-mode-serial--mode-line-status-run)
      (setq phps-mode-serial--status 'running))
    (phps-mode-serial--kill-active key)
    (if async
        (if async-by-process
            (if (fboundp 'async-start)
                ;; Run command(s) asynchronously
                (let ((script-filename
                       (file-name-directory
                        (symbol-file 'phps-mode))))
                  (puthash
                   key
                   (async-start
                    (lambda()
                      (add-to-list 'load-path script-filename)
                      (require 'phps-mode)

                      ;; Execute start lambda
                      (condition-case conditions
                          (progn
                            (let ((start-return (funcall start)))
                              (list 'success start-return start-time)))
                        (error (list 'error conditions start-time))))

                    (lambda (start-return)
                      (let ((status (car start-return))
                            (value (car (cdr start-return)))
                            (start-time (car (cdr (cdr start-return))))
                            (end-return nil))

                        ;; Profile execution in debug mode
                        (when phps-mode-serial--profiling
                          (let* ((end-time (current-time))
                                 (end-time-float
                                  (+ (car end-time) (car (cdr end-time)) (* (car (cdr (cdr end-time))) 0.000001)))
                                 (start-time-float
                                  (+ (car start-time) (car (cdr start-time)) (* (car (cdr (cdr start-time))) 0.000001)))
                                 (elapsed (- end-time-float start-time-float)))
                            (message "Serial asynchronous process start finished, elapsed: %fs" elapsed)))

                        (if (string= status "success")
                            (progn

                              ;; Execute end lambda
                              (condition-case conditions
                                  (progn
                                    (let ((return (funcall end value)))
                                      (setq end-return (list 'success return start-time))))
                                (error (setq end-return (list 'error conditions start-time))))

                              ;; Profile execution in debug mode
                              (when phps-mode-serial--profiling
                                (let* ((end-time (current-time))
                                       (end-time-float
                                        (+ (car end-time) (car (cdr end-time)) (* (car (cdr (cdr end-time))) 0.000001)))
                                       (start-time-float
                                        (+ (car start-time) (car (cdr start-time)) (* (car (cdr (cdr start-time))) 0.000001)))
                                       (elapsed (- end-time-float start-time-float)))
                                  (message "Serial synchronous thread finished, elapsed: %fs" elapsed)))

                              (let ((status (car end-return))
                                    (value (cdr end-return)))

                                (when (string= status "success")
                                  (with-current-buffer key
                                    (setq phps-mode-serial--status 'success)))

                                (when (string= status "error")
                                  (with-current-buffer key
                                    (setq phps-mode-serial--status 'error))
                                  (when end-error
                                    (funcall end-error value)))))
                          (when (string= status "error")
                            (with-current-buffer key
                              (setq phps-mode-serial--status 'error))
                            (when start-error
                              (funcall start-error value)))))))
                   phps-mode-serial--async-processes))
              (signal 'error (list "Async-start function is missing")))

          ;; Run command(s) asynchronously
          (puthash
           key
           (make-thread
            (lambda()
              (let ((start-return)
                    (end-return))

                ;; First execute start lambda
                (condition-case conditions
                    (let ((return (funcall start)))
                      (setq start-return (list 'success return start-time)))
                  (error (setq start-return (list 'error conditions start-time))))

                ;; Profile execution in debug mode
                (when phps-mode-serial--profiling
                  (let* ((end-time (current-time))
                         (end-time-float
                          (+ (car end-time) (car (cdr end-time)) (* (car (cdr (cdr end-time))) 0.000001)))
                         (start-time-float
                          (+ (car start-time) (car (cdr start-time)) (* (car (cdr (cdr start-time))) 0.000001)))
                         (elapsed (- end-time-float start-time-float)))
                    (message "Serial asynchronous thread start finished, elapsed: %fs" elapsed)))

                (let ((status (car start-return))
                      (value (car (cdr start-return)))
                      (start-time (car (cdr (cdr start-return)))))

                  (if (string= status "success")
                      (progn

                        ;; Then execute end lambda
                        (condition-case conditions
                            (let ((return (funcall end value)))
                              (setq end-return (list 'success return start-time)))
                          (error (setq end-return (list 'error conditions start-time))))

                        ;; Profile execution
                        (when phps-mode-serial--profiling
                          (let* ((end-time (current-time))
                                 (end-time-float
                                  (+ (car end-time) (car (cdr end-time)) (* (car (cdr (cdr end-time))) 0.000001)))
                                 (start-time-float
                                  (+ (car start-time) (car (cdr start-time)) (* (car (cdr (cdr start-time))) 0.000001)))
                                 (elapsed (- end-time-float start-time-float)))
                            (message "Serial asynchronous thread end finished, elapsed: %fs" elapsed)))

                        (let ((status (car end-return))
                              (value (car (cdr end-return))))

                          (when (string= status "success")
                            (with-current-buffer key
                              (setq phps-mode-serial--status 'success)))

                          (when (string= status "error")
                            (with-current-buffer key
                              (setq phps-mode-serial--status 'error))
                            (when end-error
                              (funcall end-error value)))))

                    (when (string= status "error")
                      (with-current-buffer key
                        (setq phps-mode-serial--status 'error))
                      (when start-error
                        (funcall start-error value)))))))
            key)
           phps-mode-serial--async-threads))

      (let ((start-return)
            (end-return))

        ;; Run start and catch potential errors
        (condition-case conditions
            (progn
              (let ((return (funcall start)))
                (setq start-return (list 'success return start-time))))
          (error (setq start-return (list 'error conditions start-time))))

        ;; Profile execution in debug mode
        (when phps-mode-serial--profiling
          (let* ((end-time (current-time))
                 (end-time-float
                  (+ (car end-time) (car (cdr end-time)) (* (car (cdr (cdr end-time))) 0.000001)))
                 (start-time-float
                  (+ (car start-time) (car (cdr start-time)) (* (car (cdr (cdr start-time))) 0.000001)))
                 (elapsed (- end-time-float start-time-float)))
            (message "Serial synchronous thread start finished, elapsed: %fs" elapsed)))

        (let ((status (car start-return))
              (value (car (cdr start-return)))
              (start-time (car (cdr (cdr start-return)))))

          (if (string= status "success")
              (progn

                ;; Then execute end lambda
                (condition-case conditions
                    (let ((return (funcall end value)))
                      (setq end-return (list 'success return start-time)))
                  (error (setq end-return (list 'error conditions start-time))))

                ;; Profile execution in debug mode
                (when phps-mode-serial--profiling
                  (let* ((end-time (current-time))
                         (end-time-float
                          (+ (car end-time) (car (cdr end-time)) (* (car (cdr (cdr end-time))) 0.000001)))
                         (start-time-float
                          (+ (car start-time) (car (cdr start-time)) (* (car (cdr (cdr start-time))) 0.000001)))
                         (elapsed (- end-time-float start-time-float)))
                    (message "Serial synchronous thread end finished, elapsed: %fs" elapsed)))

                (let ((status (car end-return))
                      (value (car (cdr end-return))))

                  (when (string= status "success")
                    (with-current-buffer key
                      (setq phps-mode-serial--status 'success)))

                  (when (string= status "error")
                    (with-current-buffer key
                      (setq phps-mode-serial--status 'error))
                    (when end-error
                      (funcall end-error value)))))

            (when (string= status "error")
              (with-current-buffer key
                (setq phps-mode-serial--status 'error))
              (when start-error
                (funcall start-error value)))))))))

(provide 'phps-mode-serial)
;;; phps-mode-serial.el ends here
