;;; -*- mode: emacs-lisp -*-

;;; Emacs Flashdeck
;;; Peter Salvi, 2009

;;; (a whole deck of flashcards)

(defvar flashdeck-buffer-size 20
  "*The number of new words the user can handle at once.")
(defvar flashdeck-successive-correct-answers 2
  "*The number of times the user needs to answer correctly in succession
for the word to be removed.")

(defvar flashdeck-words)
(defvar flashdeck-next)
(defvar flashdeck-buffer)
(defvar flashdeck-index)
(defvar flashdeck-state)

(defun flashdeck-shuffle (array &optional start end)
  (let* ((start (or start 0))
         (end (or end (length array)))
         (n (- end start)))
    (dotimes (i (1- n))
      (let* ((real-i (+ start i))
             (index (+ real-i 1 (random (- n i 1))))
             (other (aref array index)))
        (aset array index (aref array real-i))
        (aset array real-i other))))
  array)

(defun flashdeck-init ()
  (random t)
  (let ((count (count-matches ".* = .*" (point-min) (point-max))))
    (when (= count 0)
      (error "There are no usable word pairs in this buffer"))
    (setq flashdeck-words (make-vector count nil))
    (save-excursion
      (goto-char (point-min))
      (dotimes (i count)
        (search-forward-regexp "\\(.*\\) = \\(.*\\)")
        (aset flashdeck-words i (list (match-string 1) (match-string 2)))))
    (flashdeck-shuffle flashdeck-words)
    (let ((n (min count flashdeck-buffer-size)))
      (setq flashdeck-buffer (make-vector n nil))
      (dotimes (i n)
        (aset flashdeck-buffer i (cons 0 (aref flashdeck-words i))))
      (setq flashdeck-next n)
      (setq flashdeck-index 0))))

(defun flashdeck-next ()
  (when (>= flashdeck-index (length flashdeck-buffer))
    (let ((k (floor (length flashdeck-buffer) 2)))
      (flashdeck-shuffle flashdeck-buffer 0 k)
      (flashdeck-shuffle flashdeck-buffer k))
    (setq flashdeck-index 0))
  (setq flashdeck-index (1+ flashdeck-index)))

(defun flashdeck-remaining ()
  (- (+ (length flashdeck-buffer) (length flashdeck-words))
     flashdeck-next))

(defun flashdeck-help ()
  (erase-buffer)
  (insert (format "
Welcome to Emacs Flashdeck!

The definitions appear in the buffer one by one.
Pressing space will show the correct answer.
After the answer is displayed you should press
 - `y' if you remembered it correctly
 - `n' if you didn't

Press q any time to quit (the list of remaining words will be displayed).

There are %d words yet to be learned.

=> " (flashdeck-remaining))))

(defun flashdeck-message (str)
  (let ((buffer-read-only nil))
    (goto-char (point-max))
    (search-backward-regexp "^=> .*$")
    (replace-match "=> ")
    (insert str)))

(defun flashdeck-current ()
  (aref flashdeck-buffer (1- flashdeck-index)))

(defun flashdeck-show-answer ()
  (interactive)
  (when (eq flashdeck-state 'question)
    (let ((word (cdr (flashdeck-current))))
      (flashdeck-message (concat (cadr word) " = " (car word))))
    (setq flashdeck-state 'answer)))

(defun flashdeck-show-next ()
  (interactive)
  (when (eq flashdeck-state 'answer)
    (flashdeck-next)
    (flashdeck-message (caddr (flashdeck-current)))
    (setq flashdeck-state 'question)))

(defun flashdeck-reject-and-show-next ()
  (interactive)
  (setcar (flashdeck-current) 0)
  (flashdeck-show-next))

(defun flashdeck-show-removal-check-end ()
  (let ((buffer-read-only nil))
    (if (= (length flashdeck-buffer) 0)
        (progn
          (erase-buffer)
          (insert "\nNo more words left!\n\nPress any key to quit.")
          (read-char-exclusive)
          (kill-buffer nil)
          (message "Bye!")
          t)
      (goto-char (point-min))
      (search-forward-regexp "There are [0-9]+ words yet to be learned.")
      (replace-match (format "There are %d words yet to be learned."
                             (flashdeck-remaining)))
      nil)))

(defun flashdeck-accept-and-show-next ()
  (interactive)
  (when (eq flashdeck-state 'answer)
    (setcar (flashdeck-current) (1+ (car (flashdeck-current))))
    (when (= (car (flashdeck-current))
             flashdeck-successive-correct-answers)
      (if (= flashdeck-next (length flashdeck-words))
          (progn
            (setq flashdeck-buffer (remove (flashdeck-current) flashdeck-buffer))
            (setq flashdeck-index (1- flashdeck-index)))
        (setcar (flashdeck-current) 0)
        (setcdr (flashdeck-current) (aref flashdeck-words flashdeck-next))
        (setq flashdeck-next (1+ flashdeck-next))))
    (unless (flashdeck-show-removal-check-end)
      (flashdeck-show-next))))

(defun flashdeck-quit ()
  (interactive)
  (let ((buffer-read-only nil))
    (erase-buffer)
    (insert "\nThese words remained:\n\n")
    (dotimes (i (length flashdeck-buffer))
      (let ((word (cdr (aref flashdeck-buffer i))))
        (insert (format "%s = %s\n" (car word) (cadr word)))))
    (dotimes (i (- (length flashdeck-words) flashdeck-next))
      (let ((word (aref flashdeck-words (+ flashdeck-next i))))
        (insert (format "%s = %s\n" (car word) (cadr word)))))
    (setq cursor-type t)
    (message "Bye!")))

(defun flashdeck-impl ()
  (dolist (i '(flashdeck-words flashdeck-next flashdeck-buffer flashdeck-index flashdeck-state))
    (kill-local-variable i))
  (flashdeck-init)
  (switch-to-buffer (generate-new-buffer-name "*flashdeck*"))
  (dolist (i '(flashdeck-words flashdeck-next flashdeck-buffer flashdeck-index flashdeck-state))
    (make-local-variable i))
  (flashdeck-help)
  (setq cursor-type nil)
  (make-local-variable 'show-paren-mode)
  (show-paren-mode 0)
  (setq buffer-read-only t)
  (local-set-key (kbd "SPC") 'flashdeck-show-answer)
  (local-set-key (kbd "n") 'flashdeck-reject-and-show-next)
  (local-set-key (kbd "y") 'flashdeck-accept-and-show-next)
  (local-set-key (kbd "q") 'flashdeck-quit)
  (setq flashdeck-state 'answer)
  (flashdeck-show-next))

(defun flashdeck ()
  "Display flashcards in a new buffer.

Emacs Flashdeck searches the current buffer for lines of the format
`solution = definition' and tests the user if he remembers them.

With a universal argument, it only looks for words in the current region."
  (interactive)
  (if (not current-prefix-arg)
      (flashdeck-impl)
    (let ((buffer (current-buffer)))
      (narrow-to-region (mark) (point))
      (flashdeck-impl)
      (save-excursion
        (set-buffer buffer)
        (widen)))))