(require 'git)
; This file adds the following to standard git features:
; - Status buffer:
;   - commit buffer header includes list of files
;   - git commit buffer is cleared each time it is called
;   - ediff takes only the file on the current line, it ignores 
;     marked lines
;   - added commands for unmark-all and delete-marked
; - ediff:
;   - added git-ediff to ediff a buffer with the current git head
;   - ediff cleanup deletes temporary buffers
; - Log buffer: creates a buffer giving the log of buffer file
;   - fetches a buffer copy of any previous version
;   - allows ediff between current file and any previous version,
;     or between two previous versions
; - history buffer: creates a buffer giving git history
;   - initially shows only header information
;   - hitting RETURN shows list of files in the commit
;   - allows fetching a file version from the buffer

; It also adds serial port customization for the mimosa board:
; - hotkeys to setup and jump to two serial buffers
; - custom abbreviation expansion in serial buffers
; - implementation of comint-like features:
;   - M-p: previous-matching-input
;   - M-n: next-matching-input
;   - beginning of line (toggles between beginning of input)

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; git setup
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(setq git-committer-name "russell"
      git-committer-email "russell.young@symbio.com")

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; Serial terminal stuff
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; These are shortcuts for expansion in the serial buffer
;; first: abbrev to expand
;; second: replacement string, or function to eval to get string
;; third: (optional) offset in string to jump to
(setq mimosa-expand-list '(
						   ("mi" "/mnt/jffs2/mimosa.conf")
						   ("er" "/mnt/jffs2/errors.log")
						   ("mn" "/mnt/jffs2/")
						   ("tf" mimosa-tftp 11 )
						   ("ca" "/var/www/core/api/calls/")
						   ("ap" "/var/www/core/api/")
						   ("ph" "/usr/lib/cgi-bin/php-cgi ")
						   ("lo" "grep PHP_ERR /tmp/everything")
						   ("ev" "/tmp/everything")
						   ("ne" "/tmp/neighbors.db")
						   ("se" "setenv serverip 192.168.1.")
						   ))

(defvar *mimosa-term-frame* nil)
(setq term-default-bg-color nil)

(defun term-raw-char (char)
  (interactive "cChar: ") 
  (term-send-raw-string (format "%c" char)))

(defvar my-term-string nil)

(defun mimosa-serial (&optional which)
  "Opens up the WHICH USB serial connection in a buffer.
WHICH is the sequential number of the buffer, not its device ID"
  (interactive)
  (setq which (or current-prefix-arg which 0))
  (let* ((dev (or (nth which (directory-files "/dev" t "^ttyUSB"))
				  (error "There are not %s ttyUSB devices" (1+ which))))
		 (prot (file-modes dev))
		 (frame (cdr (assoc "mimosa" (make-frame-names-alist))))
		 (buffer (get-buffer-create dev))
		 (window (get-buffer-window buffer)))
	(if window (select-window window) 
	  (switch-to-buffer buffer))
	(end-of-buffer)
;	(unless (= (file-modes dev) 438)
;	  (sudo-shell-command (concat "chmod 666 " dev) sudo-password))
	(when *mimosa-term-frame*
	  (select-frame (or frame (make-frame)))
	  (set-frame-name "mimosa")
	  (set-frame-parameter (selected-frame) 'background-mode 'dark)
	  (set-frame-parameter (selected-frame) 'background-color "black")
	  (set-frame-parameter (selected-frame) 'foreground-color "white")
	  (raise-frame))
	(or (get-process dev) (serial-term dev 115200))
	(term-line-mode)
	(local-set-key '[tab] 'term-dynamic-complete)
	(local-set-key "\C-A" 'mimosa-line-start)
	(local-set-key "\C-C\C-C" (lambda () (interactive) (term-raw-char 3)))
	(local-set-key "\C-C\C-D" (lambda () (interactive) (term-raw-char 4)))
	(local-set-key "\M-p" 'my-term-previous-matching-input)
	(local-set-key "\M-n" 'my-term-next-matching-input)
	(local-set-key '[M-up] 'my-term-previous-prompt)
	(local-set-key '[M-down] 'my-term-next-prompt)
	(setq term-dynamic-complete-functions '(mimosa-expand term-replace-by-expanded-history)
		  term-prompt-regexp "^[^$#]*[$#] "
		  term-buffer-maximum-size 0)
	(make-local-variable 'my-term-string)
	))

(global-set-key "\C-z\C-a" 'mimosa-serial)
(global-set-key "\C-z\C-q" (lambda () (interactive) (mimosa-serial 1)))
(global-set-key "\C-z\C-s" 'my-git-status)

;;
;; Recreates the comint buffer commands for serial buffers
;;
(defun my-term-previous-prompt ()
  "Goes up to the previous command prompt"
  (interactive)
  (beginning-of-line)
  (re-search-backward term-prompt-regexp)
  (goto-char (match-end 0)))

(defun my-term-next-prompt ()
  "Goes down to the next command prompt"
  (interactive)
  (re-search-forward term-prompt-regexp))

(defun my-term-previous-matching-input () 
  "Does the same thing as comint-previous-matching-input-from-input"
  (interactive)
  (or (eq last-command 'my-term-previous-matching-input)
	  (setq my-term-string (funcall term-get-old-input)))
  (term-previous-matching-input (concat "^" my-term-string) 1))

(defun my-term-next-matching-input () 
  "Does the same thing as comint-next-matching-input-from-input"
  (interactive)
  (or (eq last-command 'my-term-next-matching-input)
	  (setq my-term-string (funcall term-get-old-input)))
  (term-next-matching-input (concat "^" my-term-string) 1))


(defun mimosa-line-start ()
  "goes to the beginning of a line or the start of the command on the line"
  (interactive)
  (let ((point (point)))
	(end-of-line)
	(if (re-search-backward "^[a-z0-9]*:[a-z0-9/]*[$#] +" (line-beginning-position) t)
		(goto-char (match-end 0))
	  (forward-line 0))
	(if (eq point (point))
		(forward-line 0))))

(defun mimosa-expand () 
  "Expand abbreviations in the serial buffers. See mimosa-expand-list above"
  (interactive)
  (let* ((end (point))
		 (start (if (re-search-backward "[ \t\n]" nil t) (match-end 0) 1))
		 (matched (buffer-substring-no-properties start end))
		 (found (some (lambda (x) (and (equal (car x) matched) x)) mimosa-expand-list)))
	(if (not found) (goto-char end)
	  (if (symbolp (second found))
		  (setq found (funcall (second found) found)))
	  (goto-char start)
	  (delete-char (length (car found)))
	  (insert (second found))
	  (if (third found)
		  (goto-char (+ start (third found)))))))

(defvar *eth1-ip* nil)

(defun mimosa-tftp (list)
  (or *eth1-ip*
	  (setq *eth1-ip* (shell-command-to-string "ifconfig eth1 | awk '/inet addr/ {split($2, a, /:/); printf a[2];}'")))
  (list "tf" (concat "tftp -g -r  " *eth1-ip*) 11))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; Git status buffer
;;
;; Customizations of the git-status command
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;;
;;; Redefine this to give a list of files being committed
;;; (copied from the distributed file, with the change as marked)
(defun git-setup-log-buffer (buffer &optional merge-heads author-name author-email subject date msg)
  "Setup the log buffer for a commit."
  (unless git-status (error "Not in git-status buffer."))
  (let ((dir default-directory)
	(marked-files (git-marked-files))
        (committer-name (git-get-committer-name))
        (committer-email (git-get-committer-email))
        (sign-off git-append-signed-off-by))
    (with-current-buffer buffer
      (cd dir)
      (erase-buffer)
      (insert
       (propertize
        (format "Author: %s <%s>\n%s%s"
                (or author-name committer-name)
                (or author-email committer-email)
                (if date (format "Date: %s\n" date) "")
                (if merge-heads
                    (format "Merge: %s\n"
                            (mapconcat 'identity merge-heads " "))
                  ""))
        'face 'git-header-face)
;;; added V
       "Base: " default-directory 
       "\nFiles:\n   "
       (mapconcat 'git-fileinfo->name marked-files "\n   ")
       "\n"
;;; added ^
       (propertize git-log-msg-separator 'face 'git-separator-face)
       "\n")
      (when subject (insert subject "\n\n"))
      (cond (msg (insert msg "\n"))
            ((file-readable-p ".git/rebase-apply/msg")
             (insert-file-contents ".git/rebase-apply/msg"))
            ((file-readable-p ".git/MERGE_MSG")
             (insert-file-contents ".git/MERGE_MSG")))
      ; delete empty lines at end
      (goto-char (point-min))
      (when (re-search-forward "\n+\\'" nil t)
        (replace-match "\n" t t))
      (when sign-off (git-append-sign-off committer-name committer-email)))
    buffer))

;;; Make sure the git commit buffer is remade each time, otherwise
;;; the right files will not be shown
(defadvice  git-commit-file (before git-clear-commit-buffer activate)
  (when (or (not (string-match "/build/" default-directory))
			(y-or-n-p "Really commit from build tree? "))
    (if (get-buffer "*git-commit*")
		(with-current-buffer "*git-commit*"
		  (erase-buffer))))
  )

;;; Redefine this to work on the file on the current line, not all
;;; selected ones (where it didn't work if more than 1 was selected)
(defun git-diff-file-idiff ()
  "Perform an interactive diff on the current file.
Redefined to use the current line, not the selected files"
  (interactive)
  (flet ((get-filename () 
		       (save-excursion
			 (beginning-of-line)
			 (if (and (re-search-forward "[^ \t]*$" (line-end-position) t)
				  (file-exists-p (match-string 0)))
			     (match-string 0)
			   (error "Cannot find filename on current line")))))
    (let* ((filename (get-filename))
	   (buff1 (find-file-noselect filename))
	   (buff2 (git-run-command-buffer (concat filename ".~HEAD~") "cat-file" "blob" (concat "HEAD:" filename))))
      (ediff-buffers buff1 buff2))))

;;
;; Adjusts the behavior of the git status buffer
;;
(defun my-git-status () 
  (interactive)
  (if (get-buffer "*git-status*")
	  (switch-to-buffer (get-buffer "*git-status*"))
	(git-status "/var/www/v3xsdk/quantenna-sdk-v3x"))
  (local-set-key "D" 'git-delete-marked)
  (local-set-key "C" 'git-unmark-all))

(defun git-delete-marked ()
  (interactive)
  (let ((files (mapcar 'git-fileinfo->name (git-marked-files))))
	(when (y-or-n-p (format "Really delete %s? " files))
		(mapcar 'delete-file files)
		(git-refresh-status)
		(message "Files deleted"))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;
;;; ediff functions
;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defun git-ediff ()
  "Runs ediff on the current buffer and the head"
  (interactive)
  (let* ((git-root (git-root))
	 (git-file (substring buffer-file-name (length git-root)))
	 (buff2 (git-run-command-buffer (concat (file-name-nondirectory git-file) ".~HEAD~") "cat-file" "blob" (concat  "HEAD:" git-file))))
    (ediff-buffers (current-buffer) buff2)))

(defun git-ediff-cleanup ()
  "delete temporary git buffers
If invoked by q only, quit with Q to leave buffers"
  (if (= last-command-char ?q)
      (mapcar (lambda (x) (and x (buffer-name x) (string-match "~$" (buffer-name x)) (kill-buffer x)))
	      (list ediff-buffer-A ediff-buffer-B ediff-buffer-C))))

(add-hook 'ediff-cleanup-hook 'git-ediff-cleanup)
(add-hook 'ediff-keymap-setup-hook (lambda () (define-key ediff-mode-map "Q" 'ediff-quit)))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; git-log 
;;
;; opens up a git log buffer for the current file. Hit RETURN 
;; to compare versions
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defun git-log ()
  "Gets log of file in current buffer"
  (interactive)
  (let ((filename buffer-file-name))
    (or filename (error "Buffer has no file"))
    (switch-to-buffer-other-window 
     (git-run-command-buffer "*git-log*" "rev-list" "--pretty" "HEAD" "--" filename))
    (kill-all-local-variables)
    (make-variable-buffer-local 'git-file)
    (setq default-directory (git-root filename)
	  git-file (substring filename (length default-directory))
	  mode-name "GL"
	  major-mode 'git-log)
    (use-local-map (git-log-keymap))
    (setq buffer-read-only t)
    (goto-char 0)))

(setq *git-selected-face* 'yellow)
(defun git-log-unmark ()
  (interactive)
  (let ((buffer-read-only nil))
	(put-text-property 1 (point-max) 'face nil)))

(defun git-log-mark ()
  (interactive)
  (let ((on (eq (get-text-property (point) 'face) *git-selected-face*)))
	(git-log-unmark)
	(or on
		(save-excursion
		  (end-of-line)
		  (let* ((buffer-read-only nil)
				 (start (re-search-backward "^commit"))
				 (end (progn (end-of-line) (if (re-search-forward "^commit" nil t)
											   (match-beginning 0) (point-max)))))
			(put-text-property 1 (point-max) 'face nil)
			(put-text-property start end 'face *git-selected-face*))))))

(defun git-log-marked ()
  (text-property-any 1 (point-max) 'face *git-selected-face*))

;;; support functions
(defun git-log-keymap ()
  "makes the keymap for git-log-mode"
  (let ((map (make-sparse-keymap)))
    (define-key map '[return] 'git-get-version)
    (define-key map "e" 'ediff-git-version)
    (define-key map "v" 'git-get-version)
    (define-key map "q" 'bury-buffer)
    (define-key map "m" 'git-log-mark)
    (define-key map " " 'git-log-mark)
    (define-key map "u" 'git-log-unmark)
    (define-key map "?" 'git-log-help)
    (define-key map "h" 'git-log-help)
	(define-key map '[M-down] 'git-log-down)
	(define-key map '[M-up] 'git-log-up)
    map))

(defun git-log-down ()
  (interactive)
  (or (re-search-forward "^commit " nil t) (error "End of record"))
  (forward-line 4))

(defun git-log-up ()
  (interactive)
  (or (and (re-search-backward "^commit " nil t) (re-search-backward "^commit " nil t))
	  (error "Beginning of record"))
  (forward-line 4))

(defun git-log-help ()
  (interactive)
  (message "v: Get version on line\ne: ediff version on line with marked version or head
m: mark version\nu: unmark version\nq: quit"))

(defun git-log-version-on-line ()
  "finds the git version information for the current cursor location"
  (save-excursion
    (or (re-search-backward "^commit \\([a-f0-9]+\\)$" nil t)
	(error "Cannot find version for current line"))
    (let ((version (match-string 1)))
      (re-search-forward "date: +[a-z]+ \\([a-z]+ [0-9]+ [0-9:]*\\)" nil t)
      (cons version (match-string 1)))))

(defun git-version-on-line (&optional point)
  (save-excursion
	(when point (goto-char point) (forward-line 1))
	(let* ((v (git-log-version-on-line))
		   (version (car v))
		   (date (cdr v))
		   (name (file-name-nondirectory git-file)))
	  (git-run-command-buffer (concat name ".~" date "~") "cat-file" "blob" (concat version ":" git-file))))
)

(defun ediff-git-version () 
  "Runs ediff on the current file and a particular version
Called from git-log-mode"
  (interactive)
  (let* ((marked (git-log-marked))
		 (buff1 (if marked (git-version-on-line marked) (find-file-noselect git-file)))
		 (buff2 (git-version-on-line)))
	(with-current-buffer buff2 (setq buffer-read-only t))
	(if marked (with-current-buffer buff1 (setq buffer-read-only t)))
    (ediff-buffers buff1 buff2)))

(defun git-get-version ()
  "Gets a version of a file into a buffer"
  (interactive)
  (switch-to-buffer (git-version-on-line))
  (setq buffer-read-only t)
  (goto-char 1))

(defun git-root (&optional dir)
  "Finds the root of the git project"
  (or dir (setq dir default-directory))
  (or (file-directory-p dir) (setq dir (file-name-directory dir)))
  (while (and dir (not (file-exists-p (concat dir ".git"))))
    (setq dir (and (string-match "[^/]+/$" dir) (substring dir 0 (match-beginning 0)))))
  dir)

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; git history
;;
;; makes a history buffer that shows commits and what files they
;; included
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defvar git-root "/var/www/v3xsdk/")

(defun git-history-insert (buffer version author date comment)
  "Insert a formatted commit into the history buffer"
  (with-current-buffer buffer
	(let ((start (point)))
	  (insert date "  " author "\n" comment "======\n")
	  (put-text-property start (point) 'version version)
	  (put-text-property start (point) 'date date))))

(defun git-history ()
  "Lists the history of the project"
  (interactive)
  (let* ((default-directory (if current-prefix-arg 
								(get-dir-from-minibuffer "git directory: ") git-root))
		 (buffer-read-only nil)
		 (tmp-buffer (git-run-command-buffer " *git-history*" "log"))
		 (buffer (get-buffer-create "*git-history*"))
		 version author date comment-start)
	(set-buffer buffer)
	(setq buffer-read-only nil)
	(erase-buffer)
	(set-buffer tmp-buffer)
	(goto-char 0)
	(while (re-search-forward "^commit \\([a-z0-9]+\\)\\|^Author: +\\([^ ]+\\)\\|^Date: +\\([^\n]+\\)[ \n]+" nil t)
	  (if (match-string 1) 
		  (progn (when date
				   (setq comment (buffer-substring comment-start (match-beginning 0)))
				   (git-history-insert buffer version author date comment))
				 (setq version (match-string 1)))
		(if (match-string 2) (setq author (match-string 2))
		  (setq date (match-string 3)
				comment-start (point)))))
	(setq comment (buffer-substring comment-start (point-max)))
	(git-history-insert buffer version author date (concat comment "\n"))
	
	(switch-to-buffer buffer)
	(kill-buffer tmp-buffer)
	(local-set-key '[return] 'git-history-return)
	(local-set-key '[tab] 'git-history-next)
	(local-set-key "q" 'bury-buffer)
	(local-set-key "e" 'git-history-file-version)
	(local-set-key "s" 'git-history-files)
	(local-set-key "h" 'git-history-no-files)
	(local-set-key "x" 'git-history-no-files)
	(local-set-key "?" 'git-history-help)
	(setq buffer-read-only t)
	(goto-char 0)))

(defun git-history-return ()
  (interactive)
  (let ((add-files 
		 (save-excursion
		   (or (re-search-backward "^===" nil t) (beginning-of-buffer))
		   (and (re-search-forward "\\([^=]==\\)\\|^  - " nil t) (match-string 1)))))
	(if add-files (git-history-files)
	  (git-history-file-version))))

(defun git-history-next ()
  "jumps to the next commit record"
  (interactive)
  (re-search-forward "^=+\n" nil t))

(defun git-history-files-string (version)
  "gts and formats the files from a given commit record"
  (with-current-buffer (git-run-command-buffer " *git-tmp*" "show" "--pretty=format:"
											   "--name-only" version)
	(goto-char 0)
	(while (re-search-forward "^." nil t)
	  (forward-char -1)
	  (insert "  - "))
	(buffer-string)))
  
(defun git-history-files ()
  "inserts the files from a commit into the buffer"
  (interactive)
  (let* ((buffer-read-only nil)
		 (version (or (get-text-property (point) 'version) (error "Cannot get version")))
		 (date (get-text-property (point) 'date))
		 (files (git-history-files-string version)))
	(or (re-search-backward "^===" nil t) (beginning-of-buffer))
	(re-search-forward "\\([^=]===\\)\\|^  - " nil t)
	(forward-line -1)
	(and (match-string 1) (< 1 (length files))
		(let ((point (point)))
		  (insert files)
		  (put-text-property point (point) 'version version)
		  (put-text-property point (point) 'date date)
		  (goto-char (1+ point))))))

(defun git-history-file-version ()
  "gets a buffer with the version of a file from a particular commit"
  (interactive)
  (let* ((version (or (get-text-property (line-end-position) 'version) 
					  (error "Could not get version")))
		 (date (get-text-property (line-end-position) 'date))
		 (filename (save-excursion
					 (end-of-line)
					 (or (thing-at-point 'filename) (error "No filename on line"))))
		 (stub (file-name-nondirectory filename))
		 (buffer (git-run-command-buffer (concat stub "(" date ")")
										   "cat-file" "blob" (concat version ":" filename))))
	(with-current-buffer buffer
	  (goto-char 0)
	  (when (re-search-forward ":: bad file\\|^fatal: " nil t)
		(kill-buffer buffer)
		(error "Could not get file")))
	(switch-to-buffer-other-window buffer)
	(setq buffer-read-only t)
	(goto-char 0)))


(defun git-history-help ()
  (interactive)
  (message "[return]: listfiles associated with commit
'e': edit version on line
'q': quit
'?': help"))

(defun git-history-no-files ()
  "remove the files from a git commit record display"
  (interactive)
  (or (re-search-forward "^===" nil t) (end-of-buffer))
  (beginning-of-line)
  (let ((end (point))
		(buffer-read-only nil))
	(or (re-search-backward "^====" nil t) (beginning-of-buffer))
	(when (re-search-forward "^  - " nil t)
		(delete-region (match-beginning 0) end)
		(forward-line -2)
		)))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; Miscellaneous functions
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(require 'shell-screen)

(shell-screen-input-add-descriptor "^ *make " 'check-unsaved)
(defun check-unsaved (text)
  "Checks for unsaved buffers when performing a make command at the shell"
  (let ((buffers (buffer-list))
		(unsaved ()))
	(while buffers
	  (with-current-buffer (car buffers)
		(if (and (buffer-modified-p)
				 buffer-file-name
				 (string-match "^/var/www/v3xsdk/quantenna-sdk-v3x" buffer-file-name))
			(setq unsaved (cons (buffer-file-name) unsaved))))
	  (setq buffers (cdr buffers)))
	(if (and unsaved (not (y-or-n-p "Unsaved files, really make? ")))
		(setq text "")))
  text)

(defun php-debug ()
  "inserts a debugging line into a file"
  (interactive)
  (beginning-of-line)
  (insert "file_put_contents('" (if current-prefix-arg "/mnt/jffs2" "/tmp") "/debug', var_export($, true));  // DEBUG\n");
  (re-search-backward "\\$")
  (forward-char 1)
  (c-indent-line-or-region)
  )

;;;
;;; Make some trees read-only
;;;
(defun mimosa-ro-p (file) 
  "tell if file should be edited ro or not"
  (string-match "/qtv1/\\|/build/" (expand-file-name file)))

;;; Make reference versions read-only
(add-hook 'find-file-hook (lambda () (if (mimosa-ro-p buffer-file-name) (setq buffer-read-only t))) t)

(defun mimosa.conf ()
  (interactive)
  (sudo-shell-command (concat "chmod 666 " "/mnt/jffs2/mimosa.conf") sudo-password))

(provide 'mimosa)

