;;; dvc-status.el --- A generic status mode for DVC

;; Copyright (C) 2007 - 2009 by all contributors

;; Author: Stephen Leake, <stephen_leake@stephe-leake.org>

;; This file 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 3, or (at your option)
;; any later version.

;; This file 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 GNU Emacs; see the file COPYING.  If not, write to
;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
;; Boston, MA 02110-1301, USA.

;;; Commentary:

;;

;;; Code:

(require 'dvc-ui)
(require 'dvc-defs)
(require 'dvc-core)
(require 'dvc-fileinfo)
(require 'uniquify)

(defcustom dvc-status-display-known nil
  "If non-nil, display files with 'known' status in xmtn-status buffer."
  :type 'boolean
  :group 'dvc)

(defcustom dvc-status-display-ignored nil
  "If non-nil, display files with 'ignored' status in xmtn-status buffer."
  :type 'boolean
  :group 'dvc)

(defvar dvc-status-mode-map
  (let ((map (make-sparse-keymap)))
    ;; grouped by major function, then alphabetical by dvc-keyvec name
    ;; workspace operations
    (define-key map dvc-keyvec-add                  'dvc-fileinfo-add-files)
    (define-key map dvc-keyvec-commit               'dvc-log-edit)
    (define-key map [?=]                            'dvc-diff-diff)
    (define-key map "E"                             'dvc-fileinfo-toggle-exclude)
    (define-key map "\M-e"                          'dvc-edit-exclude)
    (define-key map dvc-keyvec-ediff                'dvc-status-ediff)
    (define-key map dvc-keyvec-help                 'describe-mode)
    (define-key map dvc-keyvec-logs                 'dvc-log)
    (define-key map "l"                             'dvc-diff-log-single)
    (define-key map "R"                             'dvc-fileinfo-rename)
    (define-key map "t"                             'dvc-fileinfo-add-log-entry)
    (define-key map dvc-keyvec-mark                 'dvc-fileinfo-mark-file)
    (define-key map dvc-keyvec-mark-all             'dvc-fileinfo-mark-all)
    (define-key map dvc-keyvec-next                 'dvc-fileinfo-next)
    (define-key map dvc-keyvec-previous             'dvc-fileinfo-prev)
    (define-key map dvc-keyvec-quit                 'dvc-buffer-quit)
    (define-key map dvc-keyvec-refresh              'dvc-generic-refresh)
    (define-key map dvc-keyvec-revert               'dvc-fileinfo-revert-files)
    (define-key map dvc-keyvec-unmark               'dvc-fileinfo-unmark-file)
    (define-key map dvc-keyvec-unmark-all           'dvc-fileinfo-unmark-all)
    (define-key map [?i]                            'dvc-fileinfo-ignore-files)
    (define-key map [?I]                            'dvc-ignore-file-extensions-in-dir)
    (define-key map "\M-I"                          'dvc-ignore-file-extensions)
    (define-key map (dvc-prefix-tagging-method ?e)  'dvc-edit-ignore-files)
    (define-key map [?k]                            'dvc-fileinfo-kill)
    (define-key map dvc-keyvec-remove               'dvc-fileinfo-remove-files)
    (define-key map "\r"                            'dvc-find-file-other-window)
    (define-key map "\M-d"                          'dvc-status-dtrt)

    ;; database operations
    (define-key map (dvc-prefix-merge ?u)           'dvc-update)
    (define-key map (dvc-prefix-merge ?m)           'dvc-missing)
    (define-key map (dvc-prefix-merge ?M)           'dvc-merge)

    map)
  "Keymap used in `dvc-status-mode'.")

(easy-menu-define dvc-status-mode-menu dvc-status-mode-map
  "`dvc-status' menu"
  `("DVC"
    ["Refresh Buffer"              dvc-generic-refresh               t]
    ["Edit log before commit"      dvc-log-edit                      t]
    ["Quit"                        dvc-buffer-quit                   t]
    ("Merge/Update"
     ["Update"                     dvc-update                        t]
     ["Show missing"               dvc-missing                       t]
     ["Merge"                      dvc-merge                         t]
     )
    ("Mark"
     ["Mark File"                  dvc-fileinfo-mark-file            t]
     ["Mark all"                   dvc-fileinfo-mark-all             t]
     ["Unmark File"                dvc-fileinfo-unmark-file          t]
     ["Unmark all"                 dvc-fileinfo-unmark-all           t]
     )
    ("Ignore"
     ["Ignore Files"               dvc-fileinfo-ignore-files         t]
     ["Ignore Extensions in dir"   dvc-ignore-file-extensions-in-dir t]
     ["Ignore Extensions globally" dvc-ignore-file-extensions        t]
     ["Edit Ignore File"           dvc-edit-ignore-files             t]
     )
    ("Exclude"
     ["Exclude File"               dvc-fileinfo-toggle-exclude t]
     ["Edit Exclude File"          dvc-edit-exclude t]
     )
    ["Do the Right Thing"          dvc-status-dtrt                   t]
    ["Add File"                    dvc-fileinfo-add-files            t]
    ["Ediff File"                  dvc-status-ediff                  t]
    ["diff File"                   dvc-diff-diff                     t]
    ["Delete File"                 dvc-fileinfo-remove-files         t]
    ["Kill File"                   dvc-fileinfo-kill                 t]
    ["Rename File"                 dvc-fileinfo-rename               t]
    ["Revert File"                 dvc-fileinfo-revert-files         t]
    ["Edit File"                   dvc-find-file-other-window        t]
    ["Add log entry"               dvc-fileinfo-add-log-entry        t]
    ["Log (single file)"           dvc-diff-log-single               t]
    ["Log (full tree)"             dvc-log                           t]
    ))

;; "<back-end>-status-mode", if defined, will be used instead of this
;; one. If so, it should be derived from dvc-status-mode (via
;; `define-derived-mode'), and rely on it for as many features as
;; possible (one can, for example, extend the menu and keymap). See
;; `xmtn-status-mode' in xmtn-dvc.el for a good example.
;; Remember to add the new mode to uniquify-list-buffers-directory-modes
(define-derived-mode dvc-status-mode fundamental-mode "dvc-status"
  "Major mode to display workspace status."
  (setq dvc-buffer-current-active-dvc (dvc-current-active-dvc))
  (setq dvc-fileinfo-ewoc (ewoc-create 'dvc-fileinfo-printer))
  (set (make-local-variable 'dvc-get-file-info-at-point-function) 'dvc-fileinfo-current-file)
  (use-local-map dvc-status-mode-map)
  (easy-menu-add dvc-status-mode-menu)
  (dvc-install-buffer-menu)
  (setq buffer-read-only t)
  (buffer-disable-undo)
  (set-buffer-modified-p nil))

(add-to-list 'uniquify-list-buffers-directory-modes 'dvc-status-mode)

(defun dvc-status-prepare-buffer (dvc root base-revision branch header-more refresh)
  "Prepare and return a status buffer. Should be called by <back-end>-dvc-status.
Calls <back-end>-status-mode.
DVC is back-end.
ROOT is absolute path to workspace.
BASE-REVISION is a string identifying the workspace's base revision.
BRANCH is a string identifying the workspace's branch.
HEADER-MORE is a function called to add other text to the ewoc header;
it should return a string.
REFRESH is a function that refreshes the status; see `dvc-buffer-refresh-function'."

  (let ((status-buffer (dvc-get-buffer-create dvc 'status root)))
    (dvc-kill-process-maybe status-buffer)
    (with-current-buffer status-buffer
      (let ((inhibit-read-only t)) (erase-buffer))
      (let ((dvc-temp-current-active-dvc dvc))
        (funcall (dvc-function dvc "status-mode")))
      (let ((header (concat
                      (format "Status for %s:\n" root)
                      (format "  base revision : %s\n" base-revision)
                      (format "  branch        : %s\n" branch)
                      (if (functionp header-more) (funcall header-more))))
            (footer ""))
        (set (make-local-variable 'dvc-buffer-refresh-function) refresh)
        (ewoc-filter dvc-fileinfo-ewoc (lambda (elem) nil))
        (ewoc-set-hf dvc-fileinfo-ewoc header footer)
        (ewoc-enter-last dvc-fileinfo-ewoc (make-dvc-fileinfo-message :text (format "Running %s..." dvc)))
        (ewoc-refresh dvc-fileinfo-ewoc)))
    (dvc-switch-to-buffer-maybe status-buffer)))

(defun dvc-status-dtrt (prefix)
  "Do The Right Thing in a status buffer; update, commit, resolve
conflicts, and/or ediff current files."
  (interactive "P")

  (let (status)
    ;; Note that message elements cannot be marked. Make sure all
    ;; selected files need the same action.
    (if (< 1 (length (dvc-fileinfo-marked-files)))
        (ewoc-map (lambda (fileinfo)
                    (etypecase fileinfo
                      (dvc-fileinfo-message
                       nil)

                      (dvc-fileinfo-file ; also matches dvc-fileinfo-dir
                       (if (dvc-fileinfo-file-mark fileinfo)
                           (if status
                               (if (not (equal status (dvc-fileinfo-file-status fileinfo)))
                                   (error "cannot Do The Right Thing on files with different status"))
                             (setq status (dvc-fileinfo-file-status fileinfo))))
                       ;; don't redisplay the element
                       nil)))
                  dvc-fileinfo-ewoc)
      (setq status (dvc-fileinfo-file-status (dvc-fileinfo-current-fileinfo))))

    (ecase status
      (added
       (dvc-fileinfo-add-log-entry prefix))

      ((deleted rename-source rename-target)
       (dvc-status-ediff))

      (missing
       ;; File is in database, but not in workspace
       (ding)
       (dvc-offer-choices (concat (dvc-fileinfo-current-file) " does not exist in working directory")
                          '((dvc-fileinfo-revert-files "revert")
                            (dvc-fileinfo-remove-files "remove")
                            (dvc-fileinfo-rename "rename"))))

      (modified
       ;; Don't offer undo here; not a common action
       ;; Assume user has started the commit log frame
       (if (< 1 (length (dvc-fileinfo-marked-files)))
           (error "cannot diff more than one file"))
       (dvc-status-ediff))

      (unknown
       (dvc-offer-choices nil
                          '((dvc-fileinfo-add-files "add")
                            (dvc-fileinfo-ignore-files "ignore")
                            (dvc-fileinfo-remove-files "remove")
                            (dvc-fileinfo-rename "rename"))))
      )))

(defun dvc-status-inventory-done (status-buffer)
  (with-current-buffer status-buffer
    (ewoc-enter-last dvc-fileinfo-ewoc (make-dvc-fileinfo-message :text "Parsing inventory..."))
    (ewoc-refresh dvc-fileinfo-ewoc)
    (dvc-redisplay)
    ;; delete "running", "parsing" from the ewoc now, but don't
    ;; refresh until the status is displayed
    (dvc-fileinfo-delete-messages)))

(defun dvc-status-ediff ()
  "Run ediff on the current workspace file, against the database version."
  (interactive)
  ;; FIXME: need user interface to specify other revision to diff
  ;; against. At least BASE and HEAD.
  (let ((dvc-temp-current-active-dvc dvc-buffer-current-active-dvc))
    (dvc-file-ediff (dvc-fileinfo-current-file))))

(provide 'dvc-status)
;;; end of file
