;;; workout-tracker.el -- Workout Tracker
;;
;;; Copyright (C) 2008 Kyle W T Sherman
;;
;; Author:   Kyle W T Sherman <kylewsherman at gmail dot com>
;; Created:  2008-06-19
;; Version:  0.1
;; Keywords: workout track gym
;;
;;; Commentary:
;;
;; Provides `workout-tracker' function that allows for the entry of workout
;; data (routine, weight, repetitions, etc.), with tracking over time.
;;
;;; Installation:
;;
;; Put `workout-tracker.el' where you keep your elisp files and add something like
;; the following to your .emacs file:
;;
;;   (require 'workout-tracker)
;;
;;; Usage:
;;
;;
;; Example:
;;
;;   (workout-tracker ... ???

;;; Code:

;; cl
(require 'cl)

;; widget
(require 'widget)

;; database
(defvar workout-tracker-data
  nil
  "Buffer local database of workout tracking data.")

;; data file
(defvar workout-tracker-data-file
  nil
  "Buffer local data file (to store workout data).")

;; data file extension
(defvar workout-tracker-data-file-extension
  ".wtd"
  "Data file extension, including period.")

;; data file extension match
(defvar workout-tracker-data-file-extension-match
  (concat
   (replace-regexp-in-string "\\." "\\\\\." workout-tracker-data-file-extension)
   "$")
  "Data file extension match.")

;; data dir
(defcustom workout-tracker-data-dir
  (expand-file-name "~/.workout-tracker")
  "*Directory to store workout data files."
  :type 'directory
  :group 'workout-traker)

;; workout tracker buffer name prefix
(defvar workout-tracker-buffer-name-prefix
  "workout-tracker"
  "Buffer name (prefix) to use for workout tracker.")

;; select server buffer name
(defvar workout-tracker-select-server-buffer-name
  "*Workout-Tracker-Select-Server*"
  "Buffer name to use for select server menu.")

;; workout-tracker-buffers
(defvar workout-tracker-buffers
  nil
  "Association list of active workout tracker buffers and their files.
\nList is in the following format:
  ((FILE . BUFFER) ...)")

;; set data macro
(defmacro workout-tracker-set-data (data)
  "Set local `workout-tracker-data' to DATA."
  `(progn
     (make-variable-buffer-local 'workout-tracker-data)
     (setq workout-tracker-data ,data)))

;; file name macro
(defmacro workout-tracker-file-name (file)
  "Returns FILE sans directory and extension parts."
  `(file-name-sans-extension (file-name-nondirectory ,file)))

;; set data file macro
(defmacro workout-tracker-set-data-file (file)
  "Set local `workout-tracker-data-file' to FILE."
  `(progn
     (make-variable-buffer-local 'workout-tracker-data-file)
     (setq workout-tracker-data-file ,file)))

;; tracker buffer name
(defun workout-tracker-buffer-name (file)
  "Generate buffer name from `workout-tracker-buffer-name-prefix' and FILE."
  (concat workout-tracker-buffer-name-prefix ":" (workout-tracker-file-name file)))

;; set data file extension
(defun workout-tracker-set-data-file-extension (extension)
  "Set `workout-tracker-data-file-extension' and
`workout-tracker-data-file-extension-match'."
  (setq workout-tracker-data-file-extension extension)
  (setq workout-tracker-data-file-extension-match
        (concat
         (replace-regexp-in-string "\\." "\\\\\." workout-tracker-data-file-extension)
         "$")))

;; switch to data file
(defun workout-tracker-switch-to-data-file (file)
  "Return buffer of and switches to an existing data FILE.
\nReturn nil if not found."
  (interactive "F")
  (let ((buffer (assoc file workout-tracker-buffers)))
    (when buffer
      (swtich-to-buffer (cdr buffer))
      (cdr buffer))))

;; load data file
(defun workout-tracker-load-data-file (file)
  "Load data FILE."
  (interactive "F")
  (let ((file (expand-file-name file))
        data
        buffer)
    ;; attempt to switch to an existing data file buffer
    (unless (workout-tracker-switch-to-data-file file)
      (if (file-exists-p file)
          (progn
            (with-temp-buffer
              ;; load file into temp buffer
              (insert-file-contents file)
              ;; persist contents
              (setq data (buffer-substring-no-properties (point-min) (point-max))))
            ;; create workout tracker buffer
            (setq buffer (generate-new-buffer (workout-tracker-buffer-name file)))
            (switch-to-buffer buffer)
            ;; set data file
            (workout-tracker-set-file file)
            ;; copy file contents into local database
            (workout-tracker-set-data (eval data)))
        (message "Data file not found: %s" file)))))

;; save data file
(defun workout-tracker-save-data-file (file)
  "Save data FILE."
  (interactive "F")
  ;; create directory if needed
  (let ((dir (file-name-directory file)))
    (unless (file-directory-p dir)
      (make-directory dir t)))
  (let ((file (expand-file-name file)))
    (with-temp-buffer
      ;; set data file
      (workout-tracker-set-file file)
      ;; write database into temp buffer
      (when workout-tracker-database
        (insert workout-tracker-database))
      ;; save workout-tracker-database contents to file
      (write-file file))))

;; create data file
(defun workout-tracker-create-data-file (file)
  "Create empty data FILE.
\nIf FILE already exists, user is prompted to overwrite."
  (interactive "F")
  ;; TODO: check for unsaved changes
  ;; attempt to switch to an existing data file buffer
  (unless (workout-tracker-switch-to-data-file file)
    (with-temp-buffer
      ;; set data file
      (workout-tracker-set-file file)
      ;; clear database
      (workout-tracker-set-data nil)
      ;; save empty data file
      (workout-tracker-save-data-file file))))

;; open data file
(defun workout-tracker-open-data-file (file)
  "Open data FILE."
  (interactive "F")
  ;; attempt to switch to an existing data file buffer
  (unless (workout-tracker-switch-to-data-file file)
    ;; load data file from disk
    (workout-tracker-load-data-file file)
    ;; TODO: open file in buffer
    )

;; list dir files
(defun workout-tracker-dir-files (dir &optional match)
  "List all files in directory DIR that match pattern MATCH."
  ;; expand dir to full path
  (setq dir (expand-file-name dir))
  ;; make sure dir is a directory
  ;; (unless (file-directory-p dir)
  ;;   (error (format "%s is not a directory" dir)))
  (when (file-directory-p dir)
    ;; set default match if none given
    (unless match
      (setq match ".*"))
    (let (items                                       ; items list to populate
          (files (nreverse (directory-files dir t)))) ; files in dir
      ;; loop through files
      (dolist (file files)
        ;; is item accessable?
        (when (file-readable-p file)
          ;; branch on type of item
          (cond
           ;; `.'
           ((string-match "^\\." (file-name-nondirectory file))
            t)
           ;; matching file or directory
           ((string-match match file)
            ;; only add files
            (unless (file-directory-p file)
              ;; file
              (let ((file-name (file-name-sans-extension (file-name-nondirectory file))))
                ;; add file
                (push (list
                       file-name
                       file)
                      items)))
            t)
           )))
      items
      )))

;; workout tracker (main method)
(defun workout-tracker (&optional file)
  "Start workout tracking.
\nIf FILE is given, use that workout data file.
Otherwise, call `workout-tracker-select-data-file'."
  (interactive)
  (if file
      (workout-tracker-open-data-file file)
    (workout-tracker-select-data-file)))

;; select data file
(defun workout-tracker-select-data-file ()
  "Present a list of data files the user may select from."
  (interactive)
  ;; setup buffer
  (setq buffer (generate-new-buffer workout-tracker-select-server-buffer-name))
  (set-buffer buffer)
  (kill-all-local-variables)
  ;; add header
  (widget-insert "Workout Tracker\n\n")
  (widget-insert "Select an existing data file or create a new one:\n\n")
  ;; add existing data file list
  (let ((files (workout-tracker-dir-files workout-tracker-data-dir workout-tracker-data-file-extension-match)))
    (dolist (file files)
      (widget-create 'push-button
                     :value (car file)
                     :notify (lambda (widget &rest ignore)
                               (let* ((value (widget-value widget))
                                      ;; set file to dir + value + extension
                                      (file (expand-file-name (concat workout-tracker-data-dir
                                                                      "/" value
                                                                      workout-tracker-data-file-extension))))
                                 (kill-buffer nil)
                                 ;; open data file
                                 (workout-tracker-open-data-file file))))
      (widget-insert "\n"))
    (widget-insert "\n"))
  ;; use local variable to store data file
  (make-local-variable 'widget-data-file)
  ;; add create option
  (widget-insert "Data File: ")
  (widget-create 'editable-field
                 :size 40
                 :notify (lambda (widget &rest ignore)
                           (let* ((value (widget-value widget))
                                  (len-value (length value))
                                  (len-ext (length workout-tracker-data-file-extension)))
                             ;; set file to dir + value + extension
                             (setq widget-data-file
                                   (expand-file-name (concat workout-tracker-data-dir
                                                             "/" value
                                                             workout-tracker-data-file-extension)))
                             ;; remove extension if given
                             (when (and (> len-value len-ext)
                                        (string= (substring value (- 0 len-ext))
                                                 workout-tracker-data-file-extension))
                               (setq widget-data-file (substring file 0 (- 0 len-ext)))))))
  (widget-insert "  ")
  (widget-create 'push-button
                 :value "Create"
                 :notify (lambda (widget &rest ignore)
                           (let ((file widget-data-file))
                             (if (file-exists-p file)
                                 (message "File already exists: %s" file)
                               (progn
                                 (kill-buffer nil)
                                 ;; create new data file
                                 (workout-tracker-create-data-file file))))))
  (widget-insert "\n")
  ;; final setup
  (use-local-map widget-keymap)
  (widget-setup)
  (switch-to-buffer buffer)
  (goto-char (point-min))
  (widget-forward 1)
  ))

(provide 'workout-tracker)

;;; workout-tracker.el ends here