(require 'magit)

(defvar magit--bisect-last-pos)
(defvar magit--bisect-tmp-file)
(defvar magit--bisect-info nil)
(make-variable-buffer-local 'magit--bisect-info)
(put 'magit--bisect-info 'permanent-local t)

(defun magit--bisecting-p (&optional required-status)
  "Return t if a bisect session is running.
If REQUIRED-STATUS is not nil then the current status must also
match REQUIRED-STATUS."
  (and (file-exists-p (concat (magit-git-dir) "BISECT_LOG"))
       (or (not required-status)
           (eq (plist-get (magit--bisect-info) :status)
               required-status))))

(defun magit--bisect-info ()
  (with-current-buffer (magit-find-status-buffer)
    (or (if (local-variable-p 'magit--bisect-info) magit--bisect-info)
        (list :status (if (magit--bisecting-p) 'running 'not-running)))))

(defun magit--bisect-cmd (&rest args)
  "Run `git bisect ...' and update the status buffer"
  (with-current-buffer (magit-find-status-buffer)
    (let* ((output (apply 'magit-git-lines (append '("bisect") args)))
           (cmd (car args))
           (first-line (car output)))
      (save-match-data
        (setq magit--bisect-info
              (cond ((string= cmd "reset")
                     (list :status 'not-running))
                    ;; Bisecting: 78 revisions left to test after this (roughly 6 steps)
                    ((string-match "^Bisecting:\\s-+\\([0-9]+\\).+roughly\\s-+\\([0-9]+\\)" first-line)
                     (list :status 'running
                           :revs (match-string 1 first-line)
                           :steps (match-string 2 first-line)))
                    ;; e2596955d9253a80aec9071c18079705597fa102 is the first bad commit
                    ((string-match "^\\([a-f0-9]+\\)\\s-.*first bad commit" first-line)
                     (list :status 'finished
                           :bad (match-string 1 first-line)))
                    (t
                     (list :status 'error)))))))
  (magit-refresh))

(defun magit--bisect-info-for-status (branch)
  "Return bisect info suitable for display in the status buffer"
  (let* ((info (magit--bisect-info))
         (status (plist-get info :status)))
    (cond ((eq status 'not-running)
           (or branch "(detached)"))
          ((eq status 'running)
           (format "(bisecting; %s revisions & %s steps left)"
                   (or (plist-get info :revs) "unknown number of")
                   (or (plist-get info :steps) "unknown number of")))
          ((eq status 'finished)
           (format "(bisected: first bad revision is %s)" (plist-get info :bad)))
          (t
           "(bisecting; unknown error occured)"))))

(defun magit-bisect-start ()
  "Start a bisect session"
  (interactive)
  (if (magit--bisecting-p)
      (error "Already bisecting"))
  (let ((bad (magit-read-rev "Start bisect with known bad revision" "HEAD"))
        (good (magit-read-rev "Good revision" (magit-default-rev))))
    (magit--bisect-cmd "start" bad good)))

(defun magit-bisect-reset ()
  "Quit a bisect session"
  (interactive)
  (unless (magit--bisecting-p)
    (error "Not bisecting"))
  (magit--bisect-cmd "reset"))

(defun magit-bisect-good ()
  "Tell git that the current revision is good during a bisect session"
  (interactive)
  (unless (magit--bisecting-p 'running)
    (error "Not bisecting"))
  (magit--bisect-cmd "good"))

(defun magit-bisect-bad ()
  "Tell git that the current revision is bad during a bisect session"
  (interactive)
  (unless (magit--bisecting-p 'running)
    (error "Not bisecting"))
  (magit--bisect-cmd "bad"))

(defun magit-bisect-skip ()
  "Tell git to skip the current revision during a bisect session."
  (interactive)
  (unless (magit--bisecting-p 'running)
    (error "Not bisecting"))
  (magit--bisect-cmd "skip"))

(defun magit-bisect-log ()
  "Show the bisect log"
  (interactive)
  (unless (magit--bisecting-p)
    (error "Not bisecting"))
  (magit-run-git "bisect" "log")
  (magit-display-process))

(defun magit-bisect-visualize ()
  "Show the remaining suspects with gitk"
  (interactive)
  (unless (magit--bisecting-p)
    (error "Not bisecting"))
  (magit-run-git "bisect" "visualize")
  (unless (getenv "DISPLAY")
    (magit-display-process)))

(easy-mmode-defmap magit-bisect-minibuffer-local-map
  '(("\C-i" . comint-dynamic-complete-filename))
  "Keymap for minibuffer prompting of rebase command."
  :inherit minibuffer-local-map)

(defvar magit-bisect-mode-history nil
  "Previously run bisect commands.")

(defun magit-bisect-run (command)
  "Bisect automatically by running commands after each step"
  (interactive
   (list
    (read-from-minibuffer "Run command (like this): "
                          ""
                          magit-bisect-minibuffer-local-map
                          nil
                          'magit-bisect-mode-history)))
  (unless (magit--bisecting-p)
    (error "Not bisecting"))
  (let ((file (make-temp-file "magit-bisect-run"))
        buffer)
    (with-temp-buffer
      (insert "#!/bin/sh\n" command "\n")
      (write-region (point-min) (point-max) file))
    (set-file-modes file #o755)
    (magit-run-git-async "bisect" "run" file)
    (magit-display-process)
    (setq buffer (get-buffer magit-process-buffer-name))
    (with-current-buffer buffer
      (set (make-local-variable 'magit--bisect-last-pos) 0)
      (set (make-local-variable 'magit--bisect-tmp-file) file))
    (set-process-filter (get-buffer-process buffer) 'magit--bisect-run-filter)
    (set-process-sentinel (get-buffer-process buffer) 'magit--bisect-run-sentinel)))

(defun magit--bisect-run-filter (process output)
  (with-current-buffer (process-buffer process)
    (save-match-data
      (let ((inhibit-read-only t)
            line new-info)
        (insert output)
        (goto-char magit--bisect-last-pos)
        (beginning-of-line)
        (while (< (point) (point-max))
          (cond ( ;; Bisecting: 78 revisions left to test after this (roughly 6 steps)
                 (looking-at "^Bisecting:\\s-+\\([0-9]+\\).+roughly\\s-+\\([0-9]+\\)")
                 (setq new-info (list :status 'running
                                      :revs (match-string 1)
                                      :steps (match-string 2))))
                ( ;; e2596955d9253a80aec9071c18079705597fa102 is the first bad commit
                 (looking-at "^\\([a-f0-9]+\\)\\s-.*first bad commit")
                 (setq new-info (list :status 'finished
                                      :bad (match-string 1)))))
          (forward-line 1))
        (goto-char (point-max))
        (setq magit--bisect-last-pos (point))
        (if new-info
            (with-current-buffer (magit-find-status-buffer)
              (setq magit--bisect-info new-info)
              (magit--bisect-update-status-buffer)))))))

(defun magit--bisect-run-sentinel (process event)
  (if (string-match-p "^finish" event)
      (with-current-buffer (process-buffer process)
        (delete-file magit--bisect-tmp-file)))
  (magit-process-sentinel process event))

(defun magit--bisect-update-status-buffer ()
  (with-current-buffer (magit-find-status-buffer)
    (save-excursion
      (save-match-data
        (let ((inhibit-read-only t))
          (goto-char (point-min))
          (when (search-forward-regexp "Local:" nil t)
            (beginning-of-line)
            (kill-line)
            (insert (format "Local:    %s %s"
                            (propertize (magit--bisect-info-for-status (magit-get-current-branch))
                                        'face 'magit-branch)
                            (abbreviate-file-name default-directory)))))))))

(provide 'magit-bisect)
