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

;;; Emacs Flashcards
;;; Peter Salvi, 2008

(defvar *flashcard-words*)
(defvar *flashcard-next*)
(defvar *flashcard-state*)

(defun flashcard-shuffle ()
  (let ((n (length *flashcard-words*)))
    (dotimes (i (1- n))
      (let* ((index (+ i 1 (random (- n i 1))))
             (other (aref *flashcard-words* index)))
        (aset *flashcard-words* index (aref *flashcard-words* i))
        (aset *flashcard-words* i other))))
  (setq *flashcard-next* 0))

(defun flashcard-count-matches (regexp rstart rend)
  "For compliance with version < 22 emacsen."
  (let ((count 0))
    (save-excursion
      (goto-char rstart)
      (while (search-forward-regexp regexp rend t)
        (setq count (1+ count))))
    count))

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

(defun flashcard-next ()
  (when (>= *flashcard-next* (length *flashcard-words*))
    (flashcard-shuffle))
  (setq *flashcard-next* (1+ *flashcard-next*)))

(defun flashcard-help ()
  (erase-buffer)
  (insert (format "
Welcome to Emacs Flashcards!

The definitions appear in the buffer one by one.
Pressing space will show the correct answer.
After the answer is displayed you can press
 - `r' to remove this word (i.e. it won't be asked again)
 - `n' to show the next definition

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

There are %d words in the database.

Last removed word: N/A

=> " (length *flashcard-words*))))

(defun flashcard-show-removal (str)
  "Returns T if there are still words in the database."
  (let ((buffer-read-only nil)
        (n (length *flashcard-words*)))
    (if (= n 0)
        (progn
          (erase-buffer)
          (insert "\nNo more words left!\n\nPress any key to quit.")
          (read-char-exclusive)
          (kill-buffer nil)
          (message "Bye!")
          nil)
      (goto-char (point-min))
      (search-forward-regexp "There are [0-9]+ words in the database.")
      (replace-match (format "There are %d words in the database." n))
      (goto-char (point-min))
      (search-forward-regexp "Last removed word: .*$")
      (replace-match (concat "Last removed word: "))
      (insert str)
      t)))

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

(defun flashcard-show-answer ()
  (interactive)
  (when (eq *flashcard-state* 'question)
    (let ((word (aref *flashcard-words* (1- *flashcard-next*))))
      (flashcard-message (concat (cadr word) " = " (car word))))
    (setq *flashcard-state* 'answer)))

(defun flashcard-show-next ()
  (interactive)
  (when (eq *flashcard-state* 'answer)
    (flashcard-next)
    (flashcard-message (cadr (aref *flashcard-words* (1- *flashcard-next*))))
    (setq *flashcard-state* 'question)))

(defun flashcard-remove-and-show-next ()
  (interactive)
  (when (eq *flashcard-state* 'answer)
    (let ((word (aref *flashcard-words* (1- *flashcard-next*))))
      (setq *flashcard-words* (remove word *flashcard-words*)
            *flashcard-next* (1- *flashcard-next*))
      (when (flashcard-show-removal (format "%s (%s)" (cadr word) (car word)))
        (flashcard-show-next)))))

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

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

Emacs Flashcards searches the current buffer for lines of the format
`solution = definition' and tests the user if he remembers them."
  (interactive)
  (dolist (i '(*flashcard-words* *flashcard-next* *flashcard-state*))
    (kill-local-variable i))
  (flashcard-init)
  (switch-to-buffer (generate-new-buffer-name "*flashcard*"))
  (dolist (i '(*flashcard-words* *flashcard-next* *flashcard-state*))
    (make-local-variable i))
  (flashcard-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") 'flashcard-show-answer)
  (local-set-key (kbd "n") 'flashcard-show-next)
  (local-set-key (kbd "r") 'flashcard-remove-and-show-next)
  (local-set-key (kbd "q") 'flashcard-quit)
  (setq *flashcard-state* 'answer)
  (flashcard-show-next))