;;; oni.el --- Functions for emacs

;; Copyright (C) 2012  Tom Willemse

;; Author: Tom Willemse <tom@ryuslash.org>
;; Keywords: local

;; 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 3 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, see <http://www.gnu.org/licenses/>.

;;; Commentary:

;;

;;; Code:

(autoload 'notifications-notify "notifications")
(autoload 'jabber-send-message "jabber-chat")

(defmacro oni:email (user at host dot com)
  "Turn arguments into an email address.
The resulting email address will look like: USER@HOST.COM, AT and
DOT are intentionally being skipped."
  (concat (symbol-name user) "@" (symbol-name host) "."
          (symbol-name com)))

(defmacro oni:eval-after-init (&rest body)
  "Defer execution of BODY until after Emacs init.

    Some functionality is dependent on code loaded by package.el.
    Instead of requiring package.el to load very early on, have some
    functionality deferred to a point after Emacs has initialized and
    package.el is loaded anyway."
  `(add-hook 'emacs-startup-hook #'(lambda () ,@body)))

(defun oni:after-save-func ()
  "Function for `after-save-hook'."
  (oni:compile-el)
  (executable-make-buffer-file-executable-if-script-p)
  (let* ((dom-dir (locate-dominating-file (buffer-file-name) "Makefile"))
         (TAGSp (not (string= "" (shell-command-to-string
                                  (concat "grep \"^TAGS:\" " dom-dir "Makefile"))))))
    (when (and dom-dir TAGSp)
      (shell-command
       (concat "make -C " dom-dir " TAGS >/dev/null 2>&1")))))

(defun oni:appt-display-window-and-jabber (min-to-app new-time appt-msg)
  "Send a message to my phone jabber account."
  (jabber-send-message (car jabber-connections) "phone@ryuslash.org"
                       nil (format "%s%s (in %s minutes)"
                                   new-time appt-msg min-to-app) nil)
  (appt-disp-window min-to-app new-time appt-msg))
;; (jabber-send-message (car jabber-connections)
;; "aethon@muc.ryuslash.org" nil "Hi, I'm a programmatic message; this
;; upens up possibilities :)" "groupchat")
(defun oni:before-save-func ()
  "Function for `before-save-hook'."
  (if (eq major-mode 'html-mode)
      (oni:replace-html-special-chars))
  (if (not (eq major-mode 'markdown-mode))
      (delete-trailing-whitespace)))

(defun oni:c-mode-func ()
  "Function for `c-mode-hook'."
  (local-set-key [f9] 'compile)
  (local-set-key "\C-j" 'oni:newline-and-indent))

(defun oni:change-prev-case (num dir)
  (let ((regfunc (if (eq dir 'up) 'upcase-region 'downcase-region))
        (wordfunc (if (eq dir 'up) 'upcase-word 'downcase-word)))
    (if (> num 1)
        (funcall regfunc (point) (- (point) num))
      (funcall wordfunc -1))))

(defun oni:close-client-window ()
  "Close a client's frames."
  (interactive)
  (server-save-buffers-kill-terminal nil))

(defun oni:color-for (object)
  "Generate a hex color by taking the first 6 characters of OBJECT's MD5 sum."
  (format "#%s" (substring (md5 object) 0 6)))

(defun oni:compile-el ()
  "Compile the current buffer file if it is an .el file."
  (let* ((full-file-name (buffer-file-name))
         (file-name (file-name-nondirectory full-file-name))
         (suffix (file-name-extension file-name)))
      (if (and (not (string-equal file-name ".dir-locals.el"))
               (string-equal suffix "el"))
          (byte-compile-file full-file-name))))

(defun oni:css-mode-func ()
  "Function for `css-mode-hook'."
  (local-set-key "\C-j" 'oni:newline-and-indent))

(defun oni:current-jabber-status ()
  "Return a string representing the current jabber status."
  (or (and (not *jabber-connected*) "Offline")
      (and (not (string= *jabber-current-status* ""))
           *jabber-current-status*)
      "Online"))

(defun oni:diary-display-func ()
  "Function for `diary-display-hook'."
  (diary-fancy-display))

(defun oni:downcase-prev (num)
  (interactive "p")
  (oni:change-prev-case num 'down))

(defun oni:emacs-startup-func ()
  "Function for `emacs-init-hook'."
  (require 'auto-complete-config)
  (ac-config-default))

(defun oni:go-mode-func ()
  "Function for `go-mode-hook'."
  (setq indent-tabs-mode nil)
  (local-set-key "\C-j" 'oni:newline-and-indent))

(defun oni:gtags-mode-func ()
  "Function for `gtags-mode-hook'."
  (local-set-key "\M-," 'gtags-find-tag)
  (local-set-key "\M-." 'gtags-find-rtag))

(defun oni:haskell-mode-func ()
  "Function for `haskell-mode-hook'."
  (turn-on-haskell-indentation))

(defun oni:hostname ()
  "Get the current machine's hostname."
  (substring (shell-command-to-string "hostname") 0 -1))

(defun oni:indent-shift-left (start end &optional count)
  "Rigidly indent region.
Region is from START to END.  Move
COUNT number of spaces if it is non-nil otherwise use
`tab-width'."
  (interactive
   (if mark-active
       (list (region-beginning) (region-end) current-prefix-arg)
     (list (line-beginning-position)
           (line-end-position)
           current-prefix-arg)))
  (if count
      (setq count (prefix-numeric-value count))
    (setq count tab-width))
  (when (> count 0)
    (let ((deactivate-mark nil))
      (save-excursion
        (goto-char start)
        (while (< (point) end)
          (if (and (< (current-indentation) count)
                   (not (looking-at "[ \t]*$")))
              (error "Can't shift all lines enough"))
          (forward-line))
        (indent-rigidly start end (- count))))))

(defun oni:indent-shift-right (start end &optional count)
  "Indent region between START and END rigidly to the right.
If COUNT has been specified indent by that much, otherwise look at
`tab-width'."
  (interactive
   (if mark-active
       (list (region-beginning) (region-end) current-prefix-arg)
     (list (line-beginning-position)
           (line-end-position)
           current-prefix-arg)))
  (let ((deactivate-mark nil))
    (if count
        (setq count (prefix-numeric-value count))
      (setq count tab-width))
    (indent-rigidly start end count)))

(defun oni:jabber-alert-message-func (from buffer text title)
  (notifications-notify :title title
                        :body text))

(defun oni:jabber-roster-mode-func ()
  "Function for `jabber-roster-mode-hook'."
  (setq mode-line-format
        (list (propertize " %m" 'face 'mode-line-buffer-id))))

(defun oni:java-mode-func ()
  "Function for `java-mode-hook'."
  (local-set-key "\C-j" 'oni:newline-and-indent))

(defun oni:js-mode-func ()
  "Function for `js-mode-hook'."
  (local-set-key "\C-j" 'oni:newline-and-indent))

(defun oni:js2-mode-func ()
  "Function for `js2-mode-hook'."
  (oni:js-mode-func)
  (local-set-key (kbd "<f5>") #'slime-js-reload))

(defun oni:kill-region-or-backward-char ()
  "Either `kill-region' or `backward-delete-char-untabify'."
  (interactive)
  (if (region-active-p)
      (kill-region (region-beginning) (region-end))
    (backward-delete-char-untabify 1)))

(defun oni:kill-region-or-forward-char ()
  "Either `kill-region' or `delete-forward-char'."
  (interactive)
  (if (region-active-p)
      (kill-region (region-beginning) (region-end))
    (delete-forward-char 1)))

(defun oni:kill-region-or-line ()
  "Either `kill-region' or `kill-line'."
  (interactive)
  (if (region-active-p)
      (kill-region (region-beginning) (region-end))
    (kill-line)))

(defun oni:lua-mode-func()
  "Function for `lua-mode-hook'."
  (local-unset-key (kbd ")"))
  (local-unset-key (kbd "]"))
  (local-unset-key (kbd "}")))

(defun oni:markdown-mode-func ()
  "Function for `markdown-mode-hook'."
  (setq-local whitespace-style '(face trailing)))

(defun oni:mini-fix-timestamp-string (date-string)
  "A minimal version of Xah Lee's `fix-timestamp-string'.
Turn DATE-STRING into something else that can be worked with in
code.  Found at http://xahlee.org/emacs/elisp_parse_time.html"
  (setq date-string (replace-regexp-in-string "Jan" "01" date-string)
        date-string (replace-regexp-in-string "Feb" "02" date-string)
        date-string (replace-regexp-in-string "Mar" "03" date-string)
        date-string (replace-regexp-in-string "Apr" "04" date-string)
        date-string (replace-regexp-in-string "May" "05" date-string)
        date-string (replace-regexp-in-string "Jun" "06" date-string)
        date-string (replace-regexp-in-string "Jul" "07" date-string)
        date-string (replace-regexp-in-string "Aug" "08" date-string)
        date-string (replace-regexp-in-string "Sep" "09" date-string)
        date-string (replace-regexp-in-string "Oct" "10" date-string)
        date-string (replace-regexp-in-string "Nov" "11" date-string)
        date-string (replace-regexp-in-string "Dec" "12" date-string))
  (string-match
   "^\\([0-9]\\{2\\}\\)-\\([0-9]\\{2\\}\\)-\\([0-9]\\{4\\}\\)$"
   date-string)
  (format "%s-%s-%s"
          (match-string 3 date-string)
          (match-string 2 date-string)
          (match-string 1 date-string)))

(defun oni:mode-line-current-song ()
  "Extract current song information from a path.

EMMS only shows me the absolute path of a song, this function
extracts the parts I want to know about."
  (let* ((song (emms-track-name (emms-playlist-current-selected-track)))
         (matchp (string-match "\\([^/]+\\)/\\([0-9]\\{4\\}\\) - \\(.+\\)/\\([0-9]\\{2,3\\}\\) - \\(.+\\)\\..\\{3,4\\}$" song))
         (band (substring song (match-beginning 1) (match-end 1)))
         (year (substring song (match-beginning 2) (match-end 2)))
         (album (substring song (match-beginning 3) (match-end 3)))
         (track (substring song (match-beginning 4) (match-end 4)))
         (title (substring song (match-beginning 5) (match-end 5))))
    (format "[%s - %s]" band title)))

(defun oni:move-beginning-of-dwim ()
  "Move to beginning of line either after indentation or before."
  (interactive)
  (let ((start (point)))
    (back-to-indentation)
    (if (= start (point))
        (beginning-of-line))))

(defun oni:move-end-of-dwim ()
  "Move to end of line, either before any comments or after."
  (interactive)
  (let ((start (point))
        (eolpos (line-end-position)))
    (beginning-of-line)
    (if (and comment-start
             (comment-search-forward eolpos t))
        (progn
          (search-backward-regexp (concat "[^ \t" comment-start "]"))
          (forward-char)

          (when (or (bolp)
                    (= start (point)))
            (end-of-line)))
      (end-of-line))))

(defun oni:myepisodes-formatter (plist)
  "Format RSS items from MyEpisodes as org tasks.
PLIST contains all the pertinent information."
  (let ((str (plist-get plist :title)))
    (string-match
     "^\\[ \\([^\]]+\\) \\]\\[ \\([^\]]+\\) \\]\\[ \\([^\]]+\\) \\]\\[ \\([^\]]+\\) \\]$"
     str)
    (let* ((title (match-string 1 str))
           (episode (match-string 2 str))
           (name (match-string 3 str))
           (date (oni:mini-fix-timestamp-string (match-string 4 str))))
      (format "* ACQUIRE %s %s - %s \n  SCHEDULED: <%s>"
              title episode name date))))

(defun oni:newline-and-indent ()
  "`newline-and-indent', but with a twist.
When dealing with braces, add another line and indent that too."
  (interactive)
  (if (and (not (or (= (point) (point-max))
                    (= (point) (point-min))))
           (or (and (char-equal (char-before) ?{)
                    (char-equal (char-after) ?}))
               (and (char-equal (char-before) ?\()
                    (char-equal (char-after) ?\)))))
      (save-excursion (newline-and-indent)))
  (newline-and-indent))

(defun oni:php-mode-func ()
  "Function for `php-mode-hook'."
  (local-set-key "\C-j" 'oni:newline-and-indent)
  (c-set-offset 'arglist-intro '+)
  (c-set-offset 'arglist-close '0)
  (setq-local fci-rule-column 80))

(defun oni:prog-mode-func ()
  "Function for `prog-mode-hook'."
  (setq-local comment-auto-fill-only-comments t))

(defun oni:python-mode-func ()
  "Function for `python-mode-hook'."
  (local-set-key (kbd "C->") 'python-indent-shift-right)
  (local-set-key (kbd "C-<") 'python-indent-shift-left)
  (set (make-local-variable 'electric-indent-chars) nil)
  (setq fci-rule-column 79
        fill-column 72)
  (setq-local whitespace-style '(tab-mark)))

(defun oni:raise-ansi-term (arg)
  "Create or show an `ansi-term' buffer."
  (interactive "P")
  (let ((buffer (get-buffer "*ansi-term*")))
    (if (and buffer (not arg))
        (switch-to-buffer buffer)
      (ansi-term (getenv "SHELL")))))

(defun oni:raise-scratch (&optional mode)
  "Show the *scratch* buffer.
If called with a universal argument, ask the user which mode to
use.  If MODE is not nil, open a new buffer with the name
*MODE-scratch* and load MODE as its major mode."
  (interactive (list (if current-prefix-arg
                         (read-string "Mode: ")
                       nil)))
  (let* ((bname (if mode
                    (concat "*" mode "-scratch*")
                  "*scratch*"))
         (buffer (get-buffer bname))
         (mode-sym (intern (concat mode "-mode"))))

    (unless buffer
      (setq buffer (generate-new-buffer bname))
      (with-current-buffer buffer
        (when (fboundp mode-sym)
          (funcall mode-sym))))

    (select-window (display-buffer buffer))))

(defun oni:replace-html-special-chars ()
  "Replace special characters with HTML escaped entities."
  (oni:replace-occurrences "é" "&eacute;"))

(defun oni:replace-occurrences (from to)
  "Replace all occurrences of FROM with TO in the current buffer."
  (save-excursion
    (goto-char (point-min))
    (while (search-forward from nil t)
      (replace-match to))))

(defun oni:request-pull ()
  "Start a mail to request pulling from a git repository."
  (interactive)
  (let* ((default-directory
           (expand-file-name
            (or (locate-dominating-file default-directory ".git")
                (magit-read-top-dir nil))))
         (refs (magit-list-interesting-refs magit-uninteresting-refs))
         (from (cdr (assoc (completing-read "From: " refs) refs)))
         (url (read-from-minibuffer "Pull URL: "))
         (to (symbol-name (read-from-minibuffer "Up to (HEAD): "
                                                nil nil t nil "HEAD")))
         (patchp (and current-prefix-arg (listp current-prefix-arg))))
    (message "Requesting pull for %s from %s to %s at %s with%s patch"
             default-directory from to url (if patchp "" "out"))

    (compose-mail
     nil (concat
          "Requesting pull for "
          (file-name-base (directory-file-name default-directory))))

    (save-excursion
      (goto-char (point-max))
      (insert
       (shell-command-to-string
        (concat "git --git-dir='" default-directory ".git' --work-tree='"
                default-directory "' request-pull " (when patchp "-p ")
                from " " url " " to))))))

(defun oni:self-insert-dwim ()
  "Execute self insert, but when the region is active call self
insert at the end of the region and at the beginning."
  (interactive)
  (if (region-active-p)
      (let ((electric-pair-mode nil)
            (beginning (region-beginning))
            (end (region-end)))
        (goto-char end)
        (self-insert-command 1)
        (save-excursion
          (goto-char beginning)
          (self-insert-command 1)))
    (self-insert-command 1)))

(defun oni:shorten-dir (dir)
  "Shorten a directory, (almost) like fish does it."
  (while (string-match "\\(/\\.?[^./]\\)[^/]+/" dir)
    (setq dir (replace-match "\\1/" nil nil dir)))
  dir)

(defun oni:show-buffer-position ()
  "Show the position in the current buffer."
  (interactive)
  (message (format "%d:%d" (line-number-at-pos) (current-column))))

(defun oni:show-org-index ()
  "Show the index of my org files."
  (interactive)
  (find-file "~/documents/org/index.org"))

(defun oni:skip-ex-tag ()
  (let ((tags (org-entry-get (point) "TAGS")))
    (when (and tags (string-match-p ":ex:" tags))
      (save-excursion
        (org-forward-element)
        (point)))))

(defun oni:split-window-interactive (dir)
  "Split windows in direction DIR.

Can also delete or switch to another window."
  (interactive
   (list (read-char "Direction (h,v,q,d,o): ")))
  (case dir
    ((?v) (split-window-vertically))
    ((?h) (split-window-horizontally))
    ((?q) (delete-other-windows))
    ((?d) (delete-window))
    ((?o) (other-window 1))))

(defun oni:split-window-interactively (window)
  "Ask for a direction and split WINDOW that way.

If no direction is given, don't split."
  (let ((dir (read-char "Direction (h,v): ")))
    (case dir
      ((?v) (split-window-vertically))
      ((?h) (split-window-horizontally))
      (t window))))

(defun oni:start-python-test-mail-server ()
  "Run the python test mailserver."
  (interactive)
  (start-process "python-test-mail-server" "*py-mail-server*" "python" "-m"
                 "smtpd" "-n" "-c" "DebuggingServer" "localhost:1025"))

(defun oni:stop-python-test-mail-server ()
  "Stop the python test mailserver."
  (interactive)
  (kill-process "python-test-mail-server"))

(defun oni:term-mode-func ()
  "Function for `term-mode-hook'."
  (setq truncate-lines nil))

(defun oni:upcase-prev (num)
  (interactive "p")
  (oni:change-prev-case num 'up))

(defun oni:vala-mode-func ()
  "Function for `vala-mode-hook'."
  (setq indent-tabs-mode nil))

(defun oni:write-file-func ()
  "Function for `write-file-hooks'."
  (time-stamp))

(defun oni:yas-minor-mode-func ()
  "Function for `yas-minor-mode-hook'."
  (define-key yas-minor-mode-map (kbd "TAB") nil)
  (define-key yas-minor-mode-map [(tab)] nil)
  (define-key yas-minor-mode-map (kbd "C-\\") 'yas-expand))

(defvar oni:mailbox-map
  '("top" ("menu"
           ("ryulash.org" . "ryuslash")
           ("ninthfloor"  . "ninthfloor")
           ("gmail"       . "gmail")
           ("aethon"      . "aethon")))
  "A mailbox map for use with `tmm-prompt'.")

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