;;;; SPDX-FileCopyrightText: Atlas Engineer LLC
;;;; SPDX-License-Identifier: BSD-3-Clause

(nyxt:define-package :nyxt/mode/repeat
  (:documentation "Package for `repeat-mode', a mode to repeat actions.

Parts of the API are:
- `repeat-mode' slots:
  - `repeat-action': the action to repeat.
  - `repeat-count': the number of times to repeat the `repeat-action'.
  - `repeat-interval': the interval to repeat `repeat-action' at.
- Internal keyboard dispatchers (can be a good example of Nyxt input handling):
  - `make-repeat-command-dispatcher'.
  - `skip-repeat-dispatch'.

`repeat-mode' is based on `nyxt/mode/process:process-mode' and customizes the
`nyxt/mode/process:firing-condition', `nyxt/mode/process:action', and
`nyxt/mode/process:cleanup' to repeat the action on the dedicated thread.

See the `repeat-mode' for the external user-facing APIs."))
(in-package :nyxt/mode/repeat)

(define-mode repeat-mode (nyxt/mode/process:process-mode)
  "Repeat the execution of a command while enabled.

The commands that `repeat-mode' exposes are:
- `repeat-every' to repeat an action every X seconds.
- `repeat-times' to repeat an action only X times.
- `repeat-key' to repeat the command bound to the keybinding.

See `nyxt/mode/repeat' package documentation for implementation details and
internal programming APIs."
  ((visible-in-status-p nil)
   (repeat-count
    nil
    :type (or integer null)
    :documentation "The number of times to repeat the commands for.")
   (repeat-interval
    1.0
    :type alex:non-negative-real
    :documentation "Time in seconds after which `repeat-action' is repeated.")
   (repeat-action
    nil
    :type (or null (function (repeat-mode)))
    :documentation "The action to repeat.
It takes a `repeat-mode' instance as argument.")
   (nyxt/mode/process:firing-condition
    (lambda (path-url mode)
      (declare (ignore path-url))
      (when (repeat-interval mode)
        (sleep (repeat-interval mode)))
      (cond ((repeat-count mode)
             (if (zerop (repeat-count mode))
                 :return
                 (decf (repeat-count mode))))
            (t t)))
    :documentation "See `nyxt/mode/process:firing-condition'.")
   (nyxt/mode/process:action
    (lambda (path-url mode)
      (declare (ignore path-url))
      (funcall* (repeat-action mode) mode))
    :documentation "See `nyxt/mode/process:action'.")
   (nyxt/mode/process:cleanup
    (lambda (path-url mode)
      (declare (ignore path-url))
      ;; Needed since the mode object might not have been garbage collected.
      (setf (repeat-action mode) nil
            (repeat-count mode) nil
            (repeat-interval mode) 1.0))
    :documentation "See `nyxt/mode/process:cleanup'.")))

(defmethod enable ((mode repeat-mode) &key)
  ;; TODO: Remember prompt input now that we have prompt-buffer hooks.
  (unless (repeat-action mode)
    (let ((prompted-action (prompt1 :prompt "Command to repeat"
                                    :sources 'nyxt:command-source)))
      (setf (repeat-action mode)
            (lambda (mode)
              (declare (ignore mode))
              (funcall prompted-action))))))

(define-command-global repeat-every (&key (seconds (sera:parse-float
                                                    (prompt1 :prompt "Repeat every X seconds"
                                                             :input "5"
                                                             :hide-suggestion-count-p t
                                                             :sources 'prompter:raw-source)
                                                    :type 'single-float))
                                     function)
  "Prompt for FUNCTION to be run every SECONDS."
  (enable-modes* 'repeat-mode
                 (current-buffer)
                 :repeat-interval seconds
                 :repeat-action function))

(define-command-global repeat-times (&key (times
                                           (parse-integer
                                            (prompt1 :prompt "Repeat for X times"
                                                     :input "4"
                                                     :hide-suggestion-count-p t
                                                     :sources 'prompter:raw-source)))
                                     function)
  "Prompt for FUNCTION to be run a number of TIMES."
  (enable-modes* 'repeat-mode
                 (current-buffer)
                 :repeat-count times
                 :repeat-action function))

(defvar *repeat-times-stack* 0
  "The current number of repetitions.")

(defun make-repeat-command-dispatcher (times)
  "Create a command dispatcher that counts the M-digit keys and adds them together.
Once a non-number key is pressed, it dispatches this key to a command and starts
repeating it like a regular `repeat-mode' does."
  (lambda (command)
    (if (eq 'repeat-key command)
        (dispatch-command command)
        (unwind-protect
             (repeat-times :times times
                           :function (lambda (mode)
                                       (declare (ignore mode))
                                       (nyxt::run command)))
          (let ((command-name (name command))
                (current-buffer (current-buffer)))
            (echo "Press a key sequence for command to repeat ~R times: ~a (~a)"
                  *repeat-times-stack*
                  (first (keymaps:pretty-binding-keys
                          command-name
                          (current-keymaps current-buffer)
                          :print-style (keymaps:name (keyscheme current-buffer))))
                  (string-downcase command-name)))
          (setf (command-dispatcher *browser*) #'dispatch-command
                *repeat-times-stack* 0)
          ;; Benign since each command runs in its own thread (see `nyxt::run').
          (sleep 2)
          (echo-dismiss)))))

(define-command-global repeat-key
    (&key (times (or
                  (ignore-errors
                   (parse-integer
                    (keymaps:key-value (nyxt::last-key (current-buffer)))))
                  (ignore-errors
                   (parse-integer
                    (prompt1 :prompt "Repeat for X times"
                             :input "4"
                             :hide-suggestion-count-p t
                             :sources 'prompter:raw-source))))))
  "Repeat the command bound to the user-pressed keybinding TIMES times."
  (setf *repeat-times-stack* (+ times (* 10 *repeat-times-stack*))
        (command-dispatcher *browser*) (make-repeat-command-dispatcher *repeat-times-stack*))
  (echo "Press a key sequence for command to repeat ~R times:" *repeat-times-stack*))
