;;; github-notifier.el --- Displays your GitHub notifications unread count in mode-line  -*- lexical-binding: t; -*-

;; Copyright (C) 2015, 2016  Chunyang Xu

;; Author: Chunyang Xu <xuchunyang56@gmail.com>
;; URL: https://github.com/xuchunyang/github-notifier.el
;; Package-Version: 20160702.2112
;; Package-Requires: ((emacs "24"))
;; Keywords: github, mode-line
;; Version: 0.1

;; 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:

;; This is a global minor-mode. Turn it on everywhere with:
;;
;;   M-x github-notifier-mode

;;; Code:

(require 'url)
(require 'json)

(defgroup github-notifier nil
  "Github Notifier"
  :group 'emacs)

;;; Custom
(defcustom github-notifier-token nil
  "Access token to get Github Notifications.

To generate an access token, visit
URL `https://github.com/settings/tokens/new?scopes=notifications&description=github-notifier.el'

This is similar to how erc or jabber handle authentication in
emacs, but the following disclaimer always worth reminding.

DISCLAIMER
When you save this variable, DON'T WRITE IT ANYWHERE PUBLIC. This
token grants (very) limited access to your account.
END DISCLAIMER

If nil, Github-Notifier will ask you and remember your token via
`customize-save-variable'."
  :type '(choice (string :tag "Token")
                 (const :tag "Ask me" nil))
  :group 'github-notifier)

(defcustom github-notifier-mode-line
  '(:eval
    (let (unread-text help-text)
      (cond ((null github-notifier-unread-count)
             (setq unread-text "-?"
                   help-text "The Github notifications number is unknown."))
            ((zerop github-notifier-unread-count)
             (setq unread-text ""
                   help-text "Good job, you don't have unread notification."))
            (t
             (setq unread-text (format "-%d%s" github-notifier-unread-count
				       (if (github-notifier-notifications-checked) "*" ""))
                   help-text (if (= github-notifier-unread-count 1)
                                 "You have 1 unread notification.\nmouse-1 Read it on Github."
                               (format "You have %d unread notifications.\nmouse-1 Read them on Github."
                                       github-notifier-unread-count)))))
      (propertize (concat " GH" unread-text)
                  'help-echo help-text
                  'local-map github-notifier-mode-line-map
                  'mouse-face 'mode-line-highlight)))
  "Mode line lighter for Github Notifier."
  :type 'sexp
  :risky t
  :group 'github-notifier)

(defcustom github-notifier-update-interval 60
  "Seconds after which the github notifications count will be updated."
  :type 'integer
  :group 'github-notifier)

(defcustom github-notifier-only-participating nil
  "If non-nil, only counts notifications in which the user is directly participating or mentioned."
  :type 'boolean
  :group 'github-notifier)

(defcustom github-notifier-enterprise-domain nil
  "Domain to Github installation.
Can be overriden to support Enterprise installations"
  :type 'string
  :group 'github-notifier)

;;; Variables
(defvar github-notifier-unread-count nil
  "Github notifications unread count.
Normally, this is a number, however, nil means unknown by Emacs.")

(defvar github-notifier-unread-json nil
  "JSON object contains latest (to github-notifier) unread notifications.")

(defvar github-notifier-update-hook nil
  "Run by `github-notifier-update-cb'.
Functions added to this hook takes one argument, the unread
notification json object BEFORE updating.  Accordingly,
`github-notifier-unread-json' stores the unread notification json
AFTER updating.")

(defvar github-notifier-mode-line-map
  (let ((map (make-sparse-keymap)))
    (define-key map [mode-line mouse-1] 'github-notifier-visit-github)
    map))

(defvar github-notifier-last-notification nil)
(defvar github-notifier-last-notification-checked nil)
(defvar github-notifier-update-timer nil)

;;; Function
(defun github-notifier-get-url (path &optional api-request)
  "Get URL to Github endpoint.
Get a url to PATH on Github or Github enterprise if
`github-enterprise-domain' is set.  If API-REQUEST is true it
will return an API."
  (let ((url
        (if github-notifier-enterprise-domain
            (concat github-notifier-enterprise-domain (when api-request "/api/v3"))
          (concat (when api-request "api.") "github.com"))))
    (concat "https://" url path)))

;; FIXME: Even we use `url-retrieve' to retrieve network asynchronously, Emacs
;; still gets blocked frequently (?), especially when the network situation is
;; bad, once it blocks Emacs, you have to wait to it gets finised or interrupt
;; it by hitting C-g many times. This is very annoying.
;;
;; Maybe we can try to invoke curl(1) as asynchronous process.
(defun github-notifier-update-cb (_status)
  (set-buffer-multibyte t)
  (goto-char (point-min))
  (if (not (string-match "200 OK" (buffer-string)))
      (progn (message "[github-notifier] Problem connecting to the server")
             (setq github-notifier-unread-count nil))
    (re-search-forward "^$" nil 'move)
    (let (json-str
          (old-count github-notifier-unread-count)
          (old-json github-notifier-unread-json))
      (setq json-str (buffer-substring-no-properties (point) (point-max))
            github-notifier-unread-json (json-read-from-string json-str))
      (setq github-notifier-unread-count (length github-notifier-unread-json))
      (when (> github-notifier-unread-count 0)
	(setq github-notifier-last-notification (cdr (assoc 'updated_at (elt github-notifier-unread-json 0)))))
      (unless (and (equal old-count github-notifier-unread-count)
		   (github-notifier-notifications-checked))
        (force-mode-line-update t))
      (run-hook-with-args 'github-notifier-update-hook old-json)
      ;; Debug
      ;; (setq a-json-string json-str)
      ;; (message "Github notification %d unread, updated at %s"
      ;;          github-notifier-unread-count (current-time-string))
      ))
  ;; Debug
  ;; (display-buffer (current-buffer))
  (kill-buffer)
  (when github-notifier-mode
    (setq github-notifier-update-timer
          (run-at-time github-notifier-update-interval nil #'github-notifier-update))))

(defun github-notifier-update (&optional force)
  "Update `github-notifier-unread-count'."
  (when (or force github-notifier-mode)
    (let ((url-request-extra-headers `(("Authorization" .
                                        ,(format "token %s" github-notifier-token))))
          (url (github-notifier-get-url (concat "/notifications"
                                                (when github-notifier-only-participating
                                                  "?participating=true")) t)))
      (condition-case error-data
          (url-retrieve url #'github-notifier-update-cb nil t t)
        (error
         (message "Error retrieving github notification from %s: %s" url error-data)
         (when github-notifier-mode
           (setq github-notifier-update-timer
                 (run-at-time github-notifier-update-interval nil #'github-notifier-update))))))))

(defun github-notifier-visit-github ()
  (interactive)
  (browse-url (github-notifier-get-url "/notifications"))
  (setq github-notifier-last-notification-checked (format-time-string "%FT%TZ" (current-time) t))
  (force-mode-line-update t))

(defun github-notifier-notifications-checked ()
  (and github-notifier-unread-count (> github-notifier-unread-count 0)
       github-notifier-last-notification github-notifier-last-notification-checked
       (string< github-notifier-last-notification github-notifier-last-notification-checked)))

;;; Glboal Minor-mode

;;;###autoload
(defalias 'github-notifier 'github-notifier-mode)

;;;###autoload
(define-minor-mode github-notifier-mode
  "Toggle github notifications count display in mode line (Github Notifier mode).
With a prefix argument ARG, enable Github Notifier mode if ARG is
positive, and disable it otherwise.  If called from Lisp, enable
the mode if ARG is omitted or nil."
  :global t :group 'github-notifier
  (unless github-notifier-token
    (setq github-notifier-token
          (with-temp-buffer
            (when (or
                   (= 0 (call-process "git" nil t nil "config" "github-notifier.oauth-token"))
                   (= 0 (call-process "git" nil t nil "config" "github.oauth-token")))
              (buffer-substring 1 (progn (goto-char 1) (line-end-position)))))))
  (unless (stringp github-notifier-token)
    (browse-url (github-notifier-get-url "/settings/tokens/new?scopes=notifications&description=github-notifier.el"))
    (let (token)
      (unwind-protect
          (setq token (read-string "Paste Your Access Token: "))
        (if (stringp token)
            (customize-save-variable 'github-notifier-token token)
          (message "No Access Token")
          (setq github-notifier-mode nil)))))
  (unless global-mode-string
    (setq global-mode-string '("")))
  (if (not github-notifier-mode)
      (progn
        (setq global-mode-string
              (delq 'github-notifier-mode-line global-mode-string))
        (when github-notifier-update-timer
          (cancel-timer github-notifier-update-timer)
          (setq github-notifier-update-timer nil)))
    (add-to-list 'global-mode-string 'github-notifier-mode-line t)
    (github-notifier-update)))

(provide 'github-notifier)
;;; github-notifier.el ends here
