;;; flashcard.el --- An extensible fact-learning program

;; Copyright (C) 2004, 2005, 2006  Jorgen Schaefer

;; Author: Jorgen Schaefer
;; Maintainer: Damien Elmes <emacs AT repose.cx>
;; Keywords: applications, flashcard
;; URL: http://www.emacswiki.org/cgi-bin/wiki.pl/FlashCard

;; This program 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 2
;; of the License, or (at your option) any later version.

;; This program 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 this program; if not, write to the Free Software
;; Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
;; 02110-1301 USA

;;; Commentary:

;; This mode helps you to learn facts. To use it, just put the
;; following in your .emacs file:

;; (add-to-list 'auto-mode-alist '("\\.deck\\'" . flashcard-mode))

;; Now open any file with a .deck extension (a new file will do). You
;; will now be put into flashcard. The first thing you need to do is
;; to add cards from any file, with either M-x flashcard-add-card, or
;; M-x flashcard-import-from-colon-file.

;; Now you should get questions asked from flashcard. Just save and
;; kill the buffer as usual. If you want to continue where you left
;; off, just open the buffer again like any other file.

;; CAVEAT: Your dialog with the program is NOT saved when you save the
;; buffer!

;; A full bells-and-whistles setup would include the following:

;; (add-hook 'flashcard-mode-hook
;;           'flashcard-add-scroll-to-bottom)
;; (add-hook 'flashcard-positive-feedback-functions
;;           'flashcard-feedback-highlight-answer)
;; (add-hook 'flashcard-positive-feedback-functions
;;           'flashcard-feedback-congratulate)
;; (add-hook 'flashcard-positive-feedback-functions
;;           'flashcard-method-leitner-positive-feedback)

;; For more documentation, see the website:
;; http://www.emacswiki.org/cgi-bin/wiki.pl/FlashCard

;;; Code:

(defvar flashcard-version "2.3.3"
  "The version string for flashcard.el")

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; User customization
(defgroup flashcard nil
  "A mode to learn facts."
  :prefix "flashcard-"
  :group 'applications)

(defcustom flashcard-coding-system (if (featurep 'xemacs)
                                       'binary
                                     'emacs-mule)
  "*The coding system flashcard should use for reading and writing
files.

This should ideally, be a \"catch-all\" coding system, like
`emacs-mule', or `iso-2022-7bit'."
  :type 'symbol
  :group 'flashcard)

(defcustom flashcard-mode-hook nil
  "This hook is run whenever `flashcard-mode' is turned on."
  :type 'hook
  :options '(flashcard-add-scroll-to-bottom)
  :group 'flashcard)

(defcustom flashcard-insert-hook nil
  "This hook is run whenever `flashcard-insert' inserts data into the
flashcard buffer."
  :type 'hook
  :group 'flashcard)

(defcustom flashcard-pre-question-hook nil
  "This hook is run before a question is inserted into the flashcard
buffer."
  :type 'hook
  :group 'flashcard)

(defcustom flashcard-wash-question-hook nil
  "This hook is run after a question is inserted into the flashcard
buffer, with the buffer narrowed to this question. Here is the right
place to add any functions that change the look of questions, e.g.
adding images or similar."
  :type 'hook
  :group 'flashcard)

(defcustom flashcard-post-question-hook nil
  "This hook is run after a question is insert into the flashcard
buffer, and after all functions in `flashcard-wash-question-hook' have
been run. No narrowing is in effect here."
  :type 'hook
  :group 'flashcard)

(defcustom flashcard-positive-feedback-functions nil
  "This hook is run whenever the user answered a card, and should be
used to give positive feedback. The functions in this list will be
passed a single argument, which is the result of
`flashcard-method-check-answer-function'."
  :type 'hook
  :options '(flashcard-feedback-highlight-answer
             flashcard-feedback-congratulate)
  :group 'flashcard)

(defcustom flashcard-deck-finished-hook nil
  "This hook is run when the deck has finished.
It can be used to automatically save progress, etc."
  :type 'hook
  :group 'flashcard)

(defgroup flashcard-faces nil
  "Faces for flashcard."
  :prefix "flashcard-"
  :group 'flashcard)

(defface flashcard-input-face '((t (:foreground "cyan")))
  "The face to use for your input to flashcard."
  :group 'flashcard-faces)

(defface flashcard-question-face '((t (:foreground "cyan")))
  "The face to use for questions."
  :group 'flashcard-faces)

(defface flashcard-answer-face '((t (:foreground "green")))
  "The face to use for answers."
  :group 'flashcard-faces)

(defface flashcard-input-correct-face '((t (:foreground "green" :bold t)))
  "The face to use for correct answers."
  :group 'flashcard-faces)

(defface flashcard-input-wrong-face '((t (:foreground "red")))
  "The face to use for incorrect answers."
  :group 'flashcard-faces)

(defgroup flashcard-methods nil
  "Different methods for flashcard."
  :group 'flashcard)

(defgroup flashcard-method-interface nil
  "Method interface for flashcard. It's usually not very wise to set
these by hand. Instead, try M-x flashcard-method-*."
  :prefix "flashcard-method-"
  :group 'flashcard)

(defcustom flashcard-method 'leitner
  "Which method is currently in effect."
  :type 'symbol
  :options '(leitner)
  :group 'flashcard-method-interface)

(defcustom flashcard-method-get-card-function 'flashcard-method-leitner-get-card
  "The function called to retrieve a new card from a deck. It is
passed a single argument, the DECK where the card should come from.
You can expect this to stay the same in the current buffer."
  :type 'function
  :options '(flashcard-method-leitner-get-card)
  :group 'flashcard-method-interface)

(defcustom flashcard-method-check-answer-function 'flashcard-method-leitner-check-answer
  "The function called to check for the correctness of an answer. It
is passed two arguments, the CARD to be answered and the ANSWER of the
user. The return value is passed to
`flashcard-method-check-answer-function' and ignored otherwise."
  :type 'function
  :options '(flashcard-method-leitner-check-answer)
  :group 'flashcard-method-interface)

(defcustom flashcard-method-answered-function 'flashcard-method-leitner-answered
  "The function called to tell the method how well the user answered a
question. It is passed two arguments, the CARD and the GRADE, which is
the result of `flashcard-method-check-answer-function'. This should
update the card accordingly."
  :type 'function
  :options '(flashcard-method-leitner-answered)
  :group 'flashcard-method-interface)

(defcustom flashcard-method-correct-p-function 'identity
  "This function is used when one needs to know whether the return
value of `flashcard-method-check-answer-function' says that the answer
was correct or not, and is passed exactly that value as the single
argument. When the backend uses grades instead of boolean values, it
should set this to a useful function."
  :type 'function
  :options '(identity)
  :group 'flashcard-method-interface)

(defcustom flashcard-method-initialize-card-function 'flashcard-method-leitner-initialize-card
  "This function is used to initialize a card. There's no provision
taken to avoid initializing a card that's already initialized, so
check for that as well. Initializing means adding all the notes you
need in `flashcard-method-get-card' to retrieve cards."
  :type 'function
  :options '(flashcard-method-leitner-initialize-card)
  :group 'flashcard-method-interface)

(defvar flashcard-message-empty
"The current deck is empty! Either add or import new cards using
`flashcard-add-card' or `flashcard-import-from-*'\n"
"A string to display when a flashcard deck is empty.")

(defvar flashcard-message-finished
"Congratulations! You have finished this deck!
Please either add or import new cards using
`flashcard-add-card' or `flashcard-import-from-*',
or start this deck over using `flashcard-deck-clean'.\n\n"
"A string to display when a flashcard deck is finished.")


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Internal variables
(defvar flashcard-deck []
  "The current flashcard deck.")
(make-variable-buffer-local 'flashcard-deck)

(defvar flashcard-card nil
  "The current card being asked.")
(make-variable-buffer-local 'flashcard-card)

(defvar flashcard-marker nil
  "The input/output marker used by flashcard.")
(make-variable-buffer-local 'flashcard-marker)

(defvar flashcard-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "RET") 'flashcard-input)
    map)
  "Keymap for flashcard mode.")


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Frontend (the major mode itself)
(defun flashcard-mode ()
  "Major mode to learn facts. It will ask you questions. Just write
the answer and submit it using \\[flashcard-input]

Turning on `flashcard-mode' runs the hook `flashcard-mode-hook'."
  (interactive)
  (let ((deck (flashcard-parse-buffer)))
    (unless (flashcard-deck-p deck)
      (error "This buffer is not a flashcard deck!"))
    (kill-all-local-variables)
    (use-local-map flashcard-mode-map)
    (setq mode-name "Flashcard"
          major-mode 'flashcard-mode)
    ;; Local variables
    (setq flashcard-deck deck))
  ;; Saving this file works differently
  (set-buffer-file-coding-system flashcard-coding-system)
  (if (featurep 'xemacs)
      (add-hook 'write-file-data-hooks  'flashcard-save-buffer)
    (add-hook 'write-contents-hooks 'flashcard-save-buffer))
  (add-hook 'change-major-mode-hook 'flashcard-unparse-buffer nil t)
  ;; Initialize the cards
  (flashcard-deck-initialize flashcard-deck)
  ;; Now set up the buffer
  (widen)
  (delete-region (point-min) (point-max))
  (set-buffer-modified-p nil)
  ;; Auto-save directly writes the buffer without giving us a chance
  ;; to convert it back into lisp. For now, just disable auto-save.
  (auto-save-mode -1)
  ;; Input/output marker
  (setq flashcard-marker (make-marker))
  (set-marker flashcard-marker (point-max))
  ;; And run the mode hook
  (run-hooks 'flashcard-mode-hook)
  ;; Now ask the user
  (flashcard-ask))

;; New buffers shouldn't inherit this mode
(put 'flashcard-mode 'mode-class 'special)

(defun flashcard-parse-buffer ()
  "Parse the current buffer and return the new value."
  (if (= (point-min) (point-max))
      (flashcard-make-deck)
    (condition-case nil
        (save-excursion
          (goto-char (point-min))
          (read (current-buffer)))
      (end-of-file
       (error "I could not parse the current buffer as a flashcard file.")))))

(defun flashcard-unparse-buffer ()
  "Delete the contents of the current buffer and insert the value of
`flashcard-deck' together with some annotations."
  (widen)
  (delete-region (point-min) (point-max))
  (insert ";;; This file is a flashcard.el deck file, and is not meant to be\n"
          ";;; edited by hand. Please open it in Emacs, and do M-x flashcard-mode\n"
          ";;; to use it.\n")
  (print flashcard-deck (current-buffer))
  (insert "\n"
          ";;; Local Variables: ***\n"
          ";;; coding: " (symbol-name flashcard-coding-system) " ***\n"
          ;; We can't add "mode: flashcard" here since emacs will call
          ;; the mode function as soon as it finds it, and that will
          ;; delete the other stuff here.
          ";;; End: ***\n")
  (goto-char (point-min)))

(defun flashcard-save-buffer ()
  "Save a representation of the current buffer to `buffer-file-name'."
  (let ((deck flashcard-deck))
    (with-temp-file buffer-file-name
      (let ((flashcard-deck deck))
        (set-buffer-file-coding-system flashcard-coding-system)
        (flashcard-unparse-buffer)))
    (set-buffer-modified-p nil)
    (set-visited-file-modtime)
    t))

(defun flashcard-insert (&rest args)
  "Insert ARGS into the flashcard buffer after the last output."
  ;; If point is on the marker, it won't move when inserting stuff, so
  ;; emulate that by hand.
  (save-restriction
    (widen)
    (let ((movep (= (point) flashcard-marker)))
      (save-excursion
        (goto-char flashcard-marker)
        (apply #'insert args)
        (set-marker flashcard-marker (point)))
      (when movep
        (goto-char flashcard-marker)))
    (run-hooks 'flashcard-insert-hook)))

(defun flashcard-input (&optional n)
  "When in the input area, send the current input to flashcard. Else,
run `newline'."
  (interactive "*P")
  (cond
   ((< (point) flashcard-marker)
    (newline n))
   ((not flashcard-card)
    (error "There's currently no question asked!"))
   (t
    (let ((from (marker-position flashcard-marker))
          (to (point-max))
          (data (buffer-substring flashcard-marker (point-max))))
      (goto-char (point-max))
      (newline)
      (newline)
      (set-marker flashcard-marker (point))
      (add-text-properties from to '(face flashcard-input-face
                                     rear-nonsticky t))
      (flashcard-handle-user-input data)))))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Backend interface
(defun flashcard-ask ()
  "Ask the next question. This is the main driver method."
  (let ((card (or (flashcard-deck-empty-p flashcard-deck)
                  (flashcard-method-get-card flashcard-deck))))
    (cond
     ;; Empty deck
     ((eq card t)
      (setq flashcard-card nil)
      (flashcard-insert flashcard-message-empty))
     ;; No card from backend
     ((not card)
      (setq flashcard-card nil)
      (flashcard-insert flashcard-message-finished)
      (run-hooks 'flashcard-deck-finished-hook))
     ;; No card, but backend supplied reason
     ((stringp card)
      (setq flashcard-card nil)
      (flashcard-insert card
                        "\n"))
     ;; We got a card!
     (t
      (setq flashcard-card card)
      (flashcard-insert-question card)))))

(defun flashcard-insert-question (card)
  "Insert the question from CARD into the flashcard buffer."
  (run-hooks 'flashcard-pre-question-hook)
  (let ((old-marker (marker-position flashcard-marker)))
    (let ((question (flashcard-card-question card)))
      (flashcard-insert question)
      ;; If the question didn't end with a newline, insert one.
      (unless (char-equal ?\n (aref question (1- (length question))))
        (flashcard-insert "\n")))
    (narrow-to-region old-marker flashcard-marker)
    (add-text-properties (point-min) (point-max)
                         '(face flashcard-question-face
                           rear-nonsticky t))
    (run-hooks 'flashcard-wash-question-hook)
    (widen)
    (flashcard-insert "\n")
    (run-hooks 'flashcard-post-question-hook)
    (goto-char flashcard-marker)))

(defun flashcard-handle-user-input (data)
  "Handle DATA as input submitted by the user."
  (let ((grade (flashcard-method-check-answer flashcard-card data)))
    (flashcard-method-answered flashcard-card grade)
    (run-hook-with-args 'flashcard-positive-feedback-functions grade)
    (flashcard-ask)))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Method interface functions
(defun flashcard-method-get-card (deck)
  "Ask the current method to get a card from the deck DECK.

This uses `flashcard-method-get-card-function'."
  (funcall flashcard-method-get-card-function deck))

(defun flashcard-method-check-answer (card answer)
  "Ask the current method whether ANSWER is a correct answer to CARD.
Return a boolean, a grade, or whatever the current method seems fit.
The method is not expected to update the card now, use
`flashcard-method-answered' for this!

This uses `flashcard-method-check-answer-function'."
  (funcall flashcard-method-check-answer-function card answer))

(defun flashcard-method-answered (card grade)
  "Tell the current method that the user has answered CARD with GRADE
- which is the value returned by `flashcard-method-check-answer'. The
method should now update the card accordingly.

This uses `flashcard-method-answered-function'."
  (funcall flashcard-method-answered-function card grade))

(defun flashcard-method-correct-p (grade)
  "Ask the backend whether the return value of
`flashcard-method-check-answer' means that the answer was correct or
not.

This uses `flashcard-method-correct-p-function'."
  (funcall flashcard-method-correct-p-function grade))

(defun flashcard-method-initialize-card (card)
  "Initialize CARD. This shouldn't do anything when CARD is already
initialized.
This uses `flashcard-method-initialize-card-function'."
  (funcall flashcard-method-initialize-card-function card))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Some nice functions
(defun flashcard-feedback-highlight-answer (grade)
  "Highlight the answer the user has given with
`flashcard-input-correct-face' or `flashcard-input-wrong-face'.
This should be added to `flashcard-positive-feedback-functions'."
  (save-excursion
    (let ((start nil)
          (end (point-max)))
      (while (and (> end (point-min))
                  (not (eq (get-text-property end 'face)
                           'flashcard-input-face)))
        (setq end (1- end)))
      (setq start end)
      (while (and (> start (point-min))
                  (eq (get-text-property start 'face)
                      'flashcard-input-face))
        (setq start (1- start)))
      (when (not (= start end))
        (add-text-properties start (1+ end)
                             (list 'face
                                   (if (flashcard-method-correct-p grade)
                                       'flashcard-input-correct-face
                                     'flashcard-input-wrong-face)))))))

(defun flashcard-feedback-congratulate (grade)
  "Congratulate the user when the answer was correct."
  (when (flashcard-method-correct-p grade)
    (let ((congrats '("Congratulations!"
                      "Good work!"
                      "Very good!"
                      "You're doing great!")))
      (flashcard-insert (nth (random (length congrats))
                             congrats)
                        "\n"
                        "\n"))))

(defun flashcard-add-scroll-to-bottom ()
  "Add `flashcard-scroll-to-bottom' to `window-scroll-functions', so
that the input line is always the last line of the window in which the
flashcard buffer is displayed."
  (add-hook 'window-scroll-functions 'flashcard-scroll-to-bottom nil t))

(defun flashcard-scroll-to-bottom (window display-start)
  "Recenter WINDOW so that point is on the last line.

This is added to `window-scroll-functions' by
`flashcard-add-scroll-to-bottom'.

The code is shamelessly taken (but adapted) from ERC."
  (when (and window (window-live-p window))
    (let ((resize-mini-windows nil))
      (save-selected-window
        (select-window window)
        (save-restriction
          (widen)
          (when (>= (point) flashcard-marker)
            (save-excursion
              (recenter -1)
              (sit-for 0))))))))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Deck interface
(defun flashcard-make-deck ()
  "Return a new and empty deck."
  (vector '*flashcard-deck* ; Marker
          '()   ; No cards
          '())) ; No notes

(defun flashcard-deck-p (deck)
  "Returns non-nil if DECK is a flashcard deck."
  (and (vectorp deck)
       (= 3 (length deck))
       (eq '*flashcard-deck*
           (aref deck 0))))

(defun flashcard-deck-cards (deck)
  "Return a list of cards in DECK."
  (aref deck 1))

(defun flashcard-deck-set-cards (deck cards)
  "Set the cards in DECK to CARDS."
  (aset deck 1 cards))

(defun flashcard-deck-get-note (deck name)
  "Return the value of the note named NAME in DECK."
  (cdr (assq name (aref deck 2))))

(defun flashcard-deck-set-note (deck name value)
  "Set the note named NAME to VALUE."
  (aset deck
        2
        (flashcard-assq-set (aref deck 2)
                            name
                            value)))

(defun flashcard-deck-clean-notes (deck)
  "Clean all notes on DECK."
  (aset deck 2 '()))

(defun flashcard-deck-empty-p (deck)
  "Return non-nil if DECK has no cards in it."
  (null (flashcard-deck-cards deck)))

(defun flashcard-deck-initialize (deck)
  "Initialize all cards in the deck using `flashcard-method-initialize-card'."
  (mapc #'flashcard-method-initialize-card
        (flashcard-deck-cards deck)))

(defun flashcard-deck-clean (deck)
  "Clean DECK. That is, remove all notes on all cards, and all notes
on the deck."
  (interactive (list flashcard-deck))
  (mapc #'flashcard-card-clean-notes
        (flashcard-deck-cards deck))
  (flashcard-deck-clean-notes deck)
  (flashcard-deck-initialize deck)
  (set-buffer-modified-p t)
  (when (and (interactive-p)
             (not flashcard-card))
    (flashcard-ask)))

(defun flashcard-add-card (deck card)
  "Add CARD to DECK."
  (interactive (list flashcard-deck
                     (flashcard-make-card
                      (read-from-minibuffer "Question: ")
                      (read-from-minibuffer "Answer: "))))
  (flashcard-method-initialize-card card)
  (flashcard-deck-set-cards deck
                            (cons card
                                  (flashcard-deck-cards deck)))
  (set-buffer-modified-p t)
  (when (and (interactive-p)
             (not flashcard-card))
    (flashcard-ask)))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; card interface
(defun flashcard-make-card (question answer)
  "Return a new card with QUESTION and ANSWER."
  (vector '*flashcard-card*
          question
          answer
          '())) ; No notes

(defun flashcard-card-p (card)
  "Return non-nil if CARD is a flashcard-card."
  (and (vectorp card)
       (= 4 (length card))
       (eq '*flashcard-card* (aref card 0))))

(defun flashcard-card-question (card)
  "Return the question of CARD."
  (aref card 1))

(defun flashcard-card-answer (card)
  "Return the answer of CARD."
  (aref card 2))

(defun flashcard-card-get-note (card name &optional default)
  "Return the note named NAME of CARD. If the note doesn't exist,
return DEFAULT."
  (let ((note (assq name (aref card 3))))
    (if note
        (cdr note)
      default)))

(defun flashcard-card-set-note (card name value)
  "Set the note named NAME of CARD to VALUE."
  (aset card 3
        (flashcard-assq-set (aref card 3)
                            name
                            value))
  (set-buffer-modified-p t))

(defun flashcard-card-clean-notes (card)
  "Clean all notes in CARD."
  (aset card 3 '())
  (set-buffer-modified-p t))

(defun flashcard-assq-set (list name value)
  "Set the value of NAME to VALUE in the association list LIST."
  (let ((elt (assq name list)))
    (if (not elt)
        (cons (cons name value)
              list)
      (setcdr elt value)
      list)))



;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Import methods

(defun flashcard-import-from-colon-file (deck file)
  "Import cards for DECK from FILE, which should be a colon-separated
file. I.e.:

Question : Answer"
  (interactive (list flashcard-deck
                     (read-file-name "File: " nil nil t)))
  (unless (eq major-mode 'flashcard-mode)
    (error "You're not in a deckfile."))
  (with-temp-buffer
    (insert-file-contents file)
    (while (re-search-forward "^ *\\(.*\\) +: +\\(.*\\)$" nil t)
      (flashcard-add-card deck
                          (flashcard-make-card (match-string 1)
                                               (match-string 2)))))
  (when (and (interactive-p)
             (not flashcard-card))
    (flashcard-ask)))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; The Leitner method

;; User customization
(defgroup flashcard-method-leitner nil
  "The original Leitner method."
  :prefix "flashcard-method-leitner-"
  :group 'flashcard-methods)

(defcustom flashcard-method-leitner-compartment-sizes [50 100 250 400 700]
  "The sizes of the compartments of your cardbox, in cards. Leitner
originally suggested 5 compartments of 1cm, 2cm, 5cm, 8cm and 14cm
size each. You can squeeze up to 50 cards in a 1cm slot, but that's a
real tight fit.

You can set this to a vector of a different size, and flashcard will
do The Right Thing."
  :type '(vector integer integer integer integer integer)
  :group 'flashcard-method-leitner)

(defcustom flashcard-method-leitner-minimum-size 3
  "How many cards should usually stay in a compartment. Leitner
originally suggested 3."
  :type 'integer
  :group 'flashcard-method-leitner)

(defcustom flashcard-method-leitner-bundle-size 100
  "Up to how many cards to take from a compartment to make room for
new cards. Leitner originally suggested the width of a finger as a
size, which for me is roughly 100 cards."
  :type 'integer
  :group 'flashcard-method-leitner)

(defcustom flashcard-method-leitner-prepare-functions '()
  "These functions will be called before a bundle of cards is taken
from the deck. You can add functions that shuffle the cards or
something here. The functions in this list will be passed two
arguments: The deck and the compartment number that we're interested
in."
  :type 'hook
  :group 'flashcard-method-leitner)

;; Local variables
(defvar flashcard-method-leitner-bundle nil
  "The current bundle of cards being asked.")
(make-variable-buffer-local 'flashcard-method-leitner-bundle)

;; Implementation
(defun flashcard-method-leitner ()
  "Use the Leitner method in flashcard.
This implements the algorithm of the Lernmaschine in Sebastian
Leitner's book 'So lernt man lernen'. The original algorithm was
tailored for a non-computer approach, specifically a box for
flashcards (that is, cards with a question on one side, and the answer
on the other. The box itself was meant to have 5 compartments of 1cm, 2cm,
5cm, 8cm and 14cm size each (`flashcard-method-leitner-compartment-sizes').

All your cards go into the first compartment. You start by asking
yourself questions from the first compartment. When you get the answer
right, the card goes right into compartment two. If not, it stays in
compartment one. When the first compartment has three or less cards
left (`flashcard-method-leitner-minimum-size'), you add new cards to
the first compartment. When another compartment gets full, you empty
it by asking yourself a number of cards from that
compartment (`flashcard-method-leitner-bundle-size').

Whenever you get a card wrong, it goes right back into compartment
one.

For more information about this method, see my summary of the
book at http://www.forcix.cx/books/leitner72.html - the article
is in German, though."
  (interactive)
  (message "Setting up the Leitner method...")
  (setq flashcard-method 'leitner
        flashcard-method-get-card-function 'flashcard-method-leitner-get-card
        flashcard-method-check-answer-function 'flashcard-method-leitner-check-answer
        flashcard-method-answered-function 'flashcard-method-leitner-answered
        flashcard-method-correct-p-function 'identity
        flashcard-method-initialize-card-function 'flashcard-method-leitner-initialize-card)
  ;; Add positive feedback function
  (add-hook 'flashcard-positive-feedback-functions
            'flashcard-method-leitner-positive-feedback)
  ;; Initialize the cards
  (flashcard-deck-initialize flashcard-deck)
  (message "Setting up the Leitner method...ok"))

(defun flashcard-method-leitner-initialize-card (card)
  "Initialize CARD to be used with the Leitner method. For this, we
only have to remember the compartment the card is in."
  (unless (flashcard-card-get-note card 'leitner-compartment)
    (flashcard-card-set-note card 'leitner-compartment 0)))

(defun flashcard-method-leitner-get-card (deck)
  "Return a new card from DECK according to the algorithm developed by
Sebastian Leitner. We depend on this being the same deck between
invocations in the same buffer."
  ;; If there are cards still in the bundle, take those
  (if flashcard-method-leitner-bundle
      (let ((card (car flashcard-method-leitner-bundle)))
        (setq flashcard-method-leitner-bundle
              (cdr flashcard-method-leitner-bundle))
        card)
    ;; Else, find a suitable new bundle
    (let ((counts (flashcard-method-leitner-compartment-counts deck)))
      (or (flashcard-method-leitner-get-card-from-full-compartment counts deck)
          (flashcard-method-leitner-get-card-from-first-compartment counts deck)
          (flashcard-method-leitner-get-card-from-fullest-compartment counts deck)))))

(defun flashcard-method-leitner-get-card-from-full-compartment (counts deck)
  "If any compartment in COUNTS is full, return a bundle of that
compartment from DECK. Else, return nil."
  (let ((i (1- (length counts))))
    ;; Find a full compartment, start at the end
    (while (and (>= i 0)
                (< (aref counts i)
                   (aref flashcard-method-leitner-compartment-sizes i)))
      (setq i (1- i)))
    (when (>= i 0)
      (flashcard-method-leitner-get-bundle deck i))))

(defun flashcard-method-leitner-get-card-from-first-compartment (counts deck)
  "If there are more then `flashcard-method-leitner-minimum-size'
cards in the first compartment, return a bundle from that compartment
from DECK. Else, return nil."
  (when (> (aref counts 0) flashcard-method-leitner-minimum-size)
    (flashcard-method-leitner-get-bundle deck 0)))

(defun flashcard-method-leitner-get-card-from-fullest-compartment (counts deck)
  "Return a bundle from the compartment from DECK that has according
to COUNTS the most cards in it."
  (let ((max 0)
        (max-i 0)
        (i 0))
    (while (< i (length counts))
      (when (> (aref counts i) max)
        (setq max (aref counts i)
              max-i i))
      (setq i (1+ i)))
    (flashcard-method-leitner-get-bundle deck max-i)))

(defun flashcard-method-leitner-get-bundle (deck compartment)
  "Set up a bundle from DECK's compartment COMPARTMENT to be asked
now, and return the first question of that."
  (run-hook-with-args 'flashcard-method-leitner-prepare-functions deck compartment)
  ;; Premature Optimization. We walk through the cards, keeping a
  ;; pointer to the last element of our list so we can append in O(1).
  ;; We also keep a sentinel so we don't have to special-case for
  ;; anything.
  (let* ((result (list 'sentinel))
         (end result)
         (num 0)
         (cards (flashcard-deck-cards deck)))
    (while (and cards
                (< num flashcard-method-leitner-bundle-size))
      (when (= (flashcard-card-get-note (car cards) 'leitner-compartment)
               compartment)
        (setcdr end (list (car cards)))
        (setq end (cdr end)
              num (1+ num)))
      (setq cards (cdr cards)))
    ;; (cdr result) now is the list of cards
    (setq flashcard-method-leitner-bundle (cddr result))
    (cadr result)))

(defun flashcard-method-leitner-check-answer (card answer)
  "Insert the answer, ask the user whether he was correct, and return
the reply."
  (if (string= (flashcard-card-answer card)
               answer)
      t
    (flashcard-insert "The correct answer is:\n"
                      (propertize (flashcard-card-answer card)
                                  'face 'flashcard-answer-face
                                  'rear-nonsticky t)
                      "\n"
                      "\n")
    (y-or-n-p "Was your answer correct? ")))

(defun flashcard-method-leitner-answered (card answer-correct-p)
  "Move CARD according to whether the user answered correctly. This is,
if ANSWER-CORRECT-P, advance it by one compartment, else move it to
the first compartment."
  (flashcard-card-set-note
   card
   'leitner-compartment
   (if answer-correct-p
       (1+ (flashcard-card-get-note card 'leitner-compartment))
     0)))

(defun flashcard-method-leitner-positive-feedback (correctp)
  "Give the user some feedback on his progress. Tell him how many
cards are left in each compartment."
  (when (eq flashcard-method 'leitner)
    (let ((counts (flashcard-method-leitner-compartment-counts flashcard-deck))
          (i 0))
      (flashcard-insert "Deck compartments: [")
      (while (< i (length counts))
        (flashcard-insert (format "%3i" (aref counts i)))
        (when (< i (1- (length counts)))
          (flashcard-insert " |"))
        (setq i (1+ i))))
      (flashcard-insert " ]\n")
    (flashcard-insert "\n")))

(defun flashcard-method-leitner-compartment-counts (deck)
  "Return a vector the same size as
`flashcard-method-leitner-compartment-sizes', with each element being
the number of cards in that slot."
  (let* ((size (length flashcard-method-leitner-compartment-sizes))
         (vec (make-vector size 0)))
    (mapc (lambda (card)
            (let ((compartment (flashcard-card-get-note card
                                                        'leitner-compartment)))
              (when (and (<= 0 compartment)
                         (< compartment size))
                (aset vec
                      compartment
                      (1+ (aref vec compartment))))))
          (flashcard-deck-cards deck))
    vec))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Support for editing the deck

(defvar flashcard-edit-buffer nil
  "The buffer used for editing.")

(defvar flashcard-edit-buffer-name "*flashcard edit*"
  "The name of the buffer used for editing.")

(defvar flashcard-edit-deck nil
  "Stores the current deck.")

(defvar flashcard-edit-flashcard-buffer nil
  "The flashcard buffer we were called from.")

(defvar flashcard-edit-render-hook nil
  "*Hooks to display extra information, like sm5 stats.
Called with point after the newline, with one argument, CARD.")

(defvar flashcard-edit-save-hook nil
  "*Called after shuffling, but before saving.
You can use this hook to sort the deck in a particular way.")

(defvar flashcard-edit-sort-function 'flashcard-edit-question<
  "*Predicate for sorting the list of cards.")

(defface flashcard-edit-number-face '((t (:foreground "#999999")))
  "A face for the card number.")

(defface flashcard-edit-answer-face '((t (:foreground "#ff9999")))
  "A face for the card answer.")

(defface flashcard-edit-question-face '((t (:foreground "#ffffaa")))
  "A face for the card answer.")
(set-face-attribute 'flashcard-edit-question-face nil :foreground "#aaaaff")

(defvar flashcard-edit-header-line-format
  "[C-c C-k] Delete current line [F9] Save deck")

(defconst flashcard-edit-mode-map
  (let ((map (make-sparse-keymap)))
    (set-keymap-parent map text-mode-map)
    (define-key map (kbd ":") 'ignore)
    (define-key map (kbd "<f9>") 'flashcard-edit-save-and-return)
    (define-key map (kbd "C-c C-k") 'flashcard-edit-delete-line)
    map))

(defun flashcard-edit-init ()
  "Init flashcard-edit-mode, and switch to the buffer."
  (unless (eq major-mode 'flashcard-mode)
    (error "Current buffer is not a flashcard deck!"))
  (when (and flashcard-edit-buffer
             (buffer-live-p flashcard-edit-buffer))
    (when (with-current-buffer flashcard-edit-buffer
            (buffer-modified-p))
      (switch-to-buffer flashcard-edit-buffer)
      (error "An edit buffer already exists! Please save first.")))
  (setq flashcard-edit-buffer
        (get-buffer-create flashcard-edit-buffer-name))
  (setq flashcard-edit-flashcard-buffer (current-buffer))
  (switch-to-buffer flashcard-edit-buffer)
  (let ((inhibit-read-only t))
    (delete-region (point-min) (point-max)))
  (setq major-mode 'flashcard-edit-mode
        mode-name "Flashcard-Edit")
  ;; vc hooks are stupid - we need to have a valid file name
  (setq buffer-file-name "/flashcard")
  (add-hook 'write-contents-hooks 'flashcard-edit-save)
  (setq header-line-format flashcard-edit-header-line-format)
  (use-local-map flashcard-edit-mode-map)
  (run-hooks 'flashcard-edit-mode-hook))

(defun flashcard-edit-ensure-edit-buffer ()
  (unless (eq major-mode 'flashcard-edit-mode)
    (error "Not in a flashcard-edit buffer!")))

(defun flashcard-edit-deinit ()
  "Remove the buffer and return to flashcard."
  (setq flashcard-edit-buffer nil)
  (setq flashcard-edit-flashcard-buffer nil)
  (kill-buffer (current-buffer)))

(defun flashcard-edit-current-deck ()
  "Use the current deck to begin an editing session."
  (interactive)
  (let ((deck flashcard-deck)
        (card flashcard-card))
    (flashcard-edit-init)
    (setq flashcard-edit-deck deck)
    (flashcard-edit-render-lines deck)
    (when card
      (flashcard-edit-center-card card))))

(defun flashcard-edit-center-card (card)
  "Center display on CARD."
  (let ((question (flashcard-card-question card)))
    ;; this shouldn't fail
    (when (search-forward (concat " " question " ") nil t)
      (recenter '(1))
      (goto-char (point-at-bol))
      (forward-word 1)
      (forward-char 1))))

(defun flashcard-edit-render-lines (deck)
  "Draw a line for each entry in the deck."
  (let ((num 0))
    (let ((inhibit-read-only t))
      (dolist (card (flashcard-edit-get-sorted-cards deck))
        (insert (format "%05d " num))
        (setq num (1+ num))
        ;; we'll store all the card properties on this
        (set-text-properties
         (point-at-bol) (point)
         (list 'front-sticky t
               'rear-nonsticky t
               'face 'flashcard-edit-number-face
               'read-only t
               'card-data (flashcard-edit-get-card-data card)))
        (insert (propertize (flashcard-card-question card)
                            'face 'flashcard-edit-question-face
                            'front-sticky t))
        (insert (propertize " :" 'read-only t
                            'face 'flashcard-edit-number-face
                            'rear-nonsticky nil))
        ;; be careful to set the text properties so that the user
        ;; can't delete areas they shouldn't
        (insert (propertize " " 'read-only t
                            'face 'flashcard-edit-number-face
                            'rear-nonsticky t))
        (insert (propertize (flashcard-card-answer card)
                            'face 'flashcard-edit-answer-face
                            'front-sticky t))
        (insert (propertize "\n" 'read-only t))
        ;; do any extra rendering
        (run-hook-with-args 'flashcard-edit-render-hook card))))
  ;; when finished, mark the buffer as unmodified
  (set-buffer-modified-p nil)
  ;; and don't let them undelete the initial insertion
  (setq buffer-undo-list nil)
  (goto-char (point-min)))

(defun flashcard-edit-get-card-data (card)
  "Return any data (notes) attached to CARD."
  (aref card 3))

(defun flashcard-edit-question< (x y)
  (string< (flashcard-card-question x) (flashcard-card-question y)))

(defun flashcard-edit-ease< (x y)
  (<
   (flashcard-card-get-note x 'sm-ease)
   (flashcard-card-get-note y 'sm-ease)))

(defun flashcard-edit-get-sorted-cards (deck)
  "Return a sorted list of cards."
  ;; sort is destructive, so we need to make a copy
  (let* ((cards (copy-sequence (flashcard-deck-cards deck))))
    (sort cards flashcard-edit-sort-function)))

;; currently this depends on de-flashcard to work properly
(defun flashcard-edit-save-and-return ()
  "Save modifications, and return.
Note that this shuffles the deck on return, so order-dependent
backends will need to sort the deck themselves using
`flashcard-edit-save-hook'."
  (interactive)
  (flashcard-edit-ensure-edit-buffer)
  (flashcard-edit-save)
  (let ((old-buffer flashcard-edit-flashcard-buffer))
    (flashcard-edit-deinit)
    (set-buffer old-buffer)
    ;; save and revert the flashcard deck, now that we've made changes.
    (save-buffer)
    ;; FIXME: why does the leitner method require this?
    (delete-region (point-min) (point-max))
    (revert-buffer nil t)))

(defvar flashcard-edit-match-string
  "^\\([[:digit:]]+\\) \\(.*\\) : \\(.*\\)$"
  "A regexp denoting the format of a card.")

(defun flashcard-edit-save ()
  "Save modifications."
  (interactive)
  (flashcard-edit-ensure-edit-buffer)
  (save-excursion
    (goto-char (point-min))
    (let ((cards nil))
      (while (re-search-forward flashcard-edit-match-string nil t)
        (setq cards (cons (flashcard-edit-matched-card) cards)))
      ;; put the cards back in the deck
      (aset flashcard-edit-deck 1 cards)
      (set-buffer-modified-p nil)
      ;; switch to the flashcard buffer
      (set-buffer flashcard-edit-flashcard-buffer)
      ;; since we sorted the deck, we'll want to shuffle it again now.
      (flashcard-shuffle-deck)
      ;; let the backend reorder the deck
      (run-hooks 'flashcard-edit-save-hook))
      t))

(defun flashcard-edit-matched-card ()
  "Return the next card found by flashcard-edit-match-string."
  (let ((question (match-string 2))
        (answer (match-string 3))
        (number (match-string 1))
        notes)
    (setq notes (get-text-property 0 'card-data number))
    ;; probably don't have to do this
    (set-text-properties 0 (length question) nil question)
    (set-text-properties 0 (length answer) nil answer)
    (vector '*flashcard-card*
            question
            answer
            notes)))

(defun flashcard-edit-delete-line ()
  "Delete the current line."
  (interactive)
  (flashcard-edit-ensure-edit-buffer)
  (let ((inhibit-read-only t))
    (goto-char (point-at-bol))
    (kill-line 1)))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Shuffling the deck

;; taken from emms
(defun flashcard-shuffle-vector (vector)
  "Shuffle VECTOR."
  (let ((i (- (length vector) 1)))
    (while (>= i 0)
      (let* ((r (random (1+ i)))
             (old (aref vector r)))
        (aset vector r (aref vector i))
        (aset vector i old))
      (setq i (- i 1))))
  vector)

(defun flashcard-shuffle-deck ()
  "Shuffle FLASHCARD-DECK."
  (interactive)
  ;; ensure we're in a flashcard buffer
  (unless (eq major-mode 'flashcard-mode)
    (error "Not in a flashcard buffer!"))
  (let* ((cards (vconcat (aref flashcard-deck 1)))
         (len (length cards))
         (i 0)
         new-list)
    (flashcard-shuffle-vector cards)
    (while (< i len)
      (push (aref cards i) new-list)
      (setq i (1+ i)))
    (aset flashcard-deck 1 new-list))
  (set-buffer-modified-p t))

(provide 'flashcard)
;;; flashcard.el ends here
