;;; elcord.el --- Allows you to integrate Rich Presence from Discord

;; Copyright (C) 2017 heatingdevice

;; Author: heatingdevice
;;      Wilfredo Velázquez-Rodríguez <zulu.inuoe@gmail.com>
;; Created: 21 Nov 2017
;; Package-Version: 20260902.44
;; Package-Revision: e15175367488
;; Keywords: games
;; Homepage: https://github.com/Mstrodl/elcord
;; Package-Requires: ((emacs "25.1"))
;; License: MIT

;;; Commentary:
;; elcord allows you to show off your buffer with all your Discord friends via the new rich presence feature
;; To use, enable the global minor mode `elcord-mode'
;; When enabled, elcord will communicate with the local Discord client in order to display information under
;; the 'Playing a Game' status, updating this information at a regular interval.
;;
;; elcord will display an Emacs title, icon, as well as information about your current buffer:
;; 1) The name and an icon (if available) for the current major mode
;; 2) The name of the current buffer
;; 3) The line number of the cursor, as well as total line count for the buffer
;; `elcord-display-buffer-details' can be customized so that buffer name and line number are omitted.

;;; Code:

(require 'bindat)
(require 'cl-lib)
(require 'json)
(require 'subr-x)

(defgroup elcord nil
  "Options for elcord."
  :prefix "elcord-"
  :group 'external)

(defcustom elcord-client-id '"388338871475240965"
  "ID of elcord client (Application ID).
See <https://discordapp.com/developers/applications/me>."
  :type '(choice (const :tag "'Native' Application ID" "388338871475240965")
                 (string :tag "Use the specified ID")
                 (function :tag "Call the function with no args to get the ID."))
  :group 'elcord)

(defcustom elcord-icon-base
  '"https://raw.githubusercontent.com/Mstrodl/elcord/master/icons/"
  "Base URL for icon images. Mode icons will be loaded from this URL + the icon name + '.png'"
  :type '(choice (const :tag "Elcord GitHub Repository"
                        "https://raw.githubusercontent.com/Mstrodl/elcord/master/icons/")
                 (string :tag "Use the specified URL base")
                 (function :tag "Call the function with icon name as an arg to get the URL base."))
  :group 'elcord)

(defcustom elcord-refresh-rate 15
  "How often to send updates to Discord, in seconds."
  :type 'integer
  :group 'elcord)

(defcustom elcord-idle-timer 300
  "How long to wait before setting the status to idle."
  :type 'integer
  :group 'elcord)

(defcustom elcord-idle-message "Getting something to drink..."
  "Message to show when elcord status is idle."
  :type 'string)

(defcustom elcord-quiet 'nil
  "Whether or not to supress elcord messages (connecting, disconnecting, etc.)"
  :type 'boolean
  :group 'elcord)

(defcustom elcord-mode-icon-alist '((agda-mode . "agda-mode_icon")
                                    (assembly-mode . "assembly-mode_icon")
                                    (bqn-mode . "bqn-mode_icon")
                                    (c-mode . "c-mode_icon")
                                    (c++-mode . "cpp-mode_icon")
                                    (clojure-mode . "clojure-mode_icon")
                                    (csharp-mode . "csharp-mode_icon")
                                    (comint-mode . "comint-mode_icon")
                                    (cperl-mode . "cperl-mode_icon")
                                    (dart-mode . "dart-mode_icon")
                                    (dockerfile-mode . "dockerfile-mode_icon")
                                    (elixir-mode . "elixir-mode_icon")
                                    (emacs-lisp-mode . (elcord--editor-icon))
                                    (enh-ruby-mode . "ruby-mode_icon")
                                    (erc-mode . "irc-mode_icon")
                                    (erlang-mode . "erlang-mode_icon")
                                    (forth-mode . "forth-mode_icon")
                                    (fortran-mode . "fortran-mode_icon")
                                    (fsharp-mode . "fsharp-mode_icon")
                                    (gdscript-mode . "gdscript-mode_icon")
                                    (haskell-mode . "haskell-mode_icon")
                                    (haskell-interactive-mode . "haskell-mode_icon")
                                    (hy-mode . "hy-mode_icon")
                                    (java-mode . "java-mode_icon")
                                    (julia-mode . "julia-mode_icon")
                                    (js-mode . "javascript-mode_icon")
                                    (kotlin-mode . "kotlin-mode_icon")
                                    (go-mode . "go-mode_icon")
                                    (latex-mode . "latex-mode_icon")
                                    (lisp-mode . "lisp-mode_icon")
                                    (lua-mode . "lua-mode_icon")
                                    (magit-mode . "magit-mode_icon")
                                    (markdown-mode . "markdown-mode_icon")
                                    (meson-mode . "meson-mode_icon")
                                    (nasm-mode . "nasm-mode_icon")
                                    (nim-mode . "nim-mode_icon")
                                    (nix-mode . "nix-mode_icon")
                                    (ocaml-mode . "ocaml-mode_icon")
                                    (octave-mode . "octave-mode_icon")
                                    (org-mode . "org-mode_icon")
                                    (pascal-mode . "pascal-mode_icon")
                                    (php-mode . "php-mode_icon")
                                    (prolog-mode . "prolog-mode_icon")
                                    (puml-mode . "puml-mode_icon")
                                    (puppet-mode . "puppet-mode_icon")
                                    (python-mode . "python-mode_icon")
                                    (racket-mode . "racket-mode_icon")
                                    (ruby-mode . "ruby-mode_icon")
                                    (rust-mode . "rust-mode_icon")
                                    (rustic-mode . "rust-mode_icon")
                                    (scala-mode . "scala-mode_icon")
                                    (solidity-mode . "solidity-mode_icon")
                                    (sh-mode . "comint-mode_icon")
                                    (terraform-mode . "terraform-mode_icon")
                                    (typescript-mode . "typescript-mode_icon")
                                    (zig-mode . "zig-mode_icon")
                                    (janet-mode . "janet-mode_icon")
                                    ("^slime-.*" . "lisp-mode_icon")
                                    ("^sly-.*$" . "lisp-mode_icon"))
  "Mapping alist of major modes to icon names to have elcord use.
Note, these icon names must be available as 'small_image' in Discord."
  :type '(alist :key-type (choice (symbol :tag "Mode name")
                                  (regexp :tag "Regex"))
                :value-type (choice (string :tag "Icon name")
                                    (function :tag "Mapping function")))
  :group 'elcord)

(defcustom elcord-mode-text-alist '((agda-mode . "Agda")
                                    (assembly-mode . "Assembly")
                                    (bqn-mode . "BQN")
                                    (c-mode . "C  ")
                                    (c++-mode . "C++")
                                    (csharp-mode . "C#")
                                    (cperl-mode . "Perl")
                                    (dart-mode . "Dart")
                                    (elixir-mode . "Elixir")
                                    (enh-ruby-mode . "Ruby")
                                    (erlang-mode . "Erlang")
                                    (fsharp-mode . "F#")
                                    (gdscript-mode . "GDScript")
                                    (hy-mode . "Hy")
                                    (java-mode . "Java")
                                    (julia-mode . "Julia")
                                    (lisp-mode . "Common Lisp")
                                    (markdown-mode . "Markdown")
                                    (magit-mode . "It's Magit!")
                                    ("mhtml-mode" . "HTML")
                                    (nasm-mode . "NASM")
                                    (nim-mode . "Nim")
                                    (ocaml-mode . "OCaml")
                                    (pascal-mode . "Pascal")
                                    (prolog-mode . "Prolog")
                                    (puml-mode . "UML")
                                    (scala-mode . "Scala")
                                    (sh-mode . "Shell")
                                    (slime-repl-mode . "SLIME-REPL")
                                    (sly-mrepl-mode . "Sly-REPL")
                                    (solidity-mode . "Solidity")
                                    (terraform-mode . "Terraform")
                                    (typescript-mode . "Typescript")
                                    (php-mode "PHP"))
  "Mapping alist of major modes to text labels to have elcord use."
  :type '(alist :key-type (choice (symbol :tag "Mode name")
                                  (regexp :tag "Regex"))
                :value-type (choice (string :tag "Text label")
                                    (function :tag "Mapping function")))
  :group 'elcord)

(defcustom elcord-display-elapsed 't
  "When enabled, Discord status will display the elapsed time since Emacs \
has been started."
  :type 'boolean
  :group 'elcord)

(defvar elcord--startup-time (string-to-number (format-time-string "%3N" (current-time))))

(defcustom elcord-display-buffer-details 't
  "When enabled, Discord status will display buffer name and line numbers:
\"Editing <buffer-name>\"
  \"Line <line-number> (<line-number> of <line-count>)\"

Otherwise, it will display:
  \"Editing\"
  \"<elcord-mode-text>\"

The mode text is the same found by `elcord-mode-text-alist'"
  :type 'boolean
  :group 'elcord)

(defcustom elcord-display-line-numbers 't
  "When enabled, shows the total line numbers of current buffer.
Including the position of the cursor in the buffer."
  :type 'boolean
  :group 'elcord)

(defcustom elcord-buffer-details-format-function 'elcord-buffer-details-format
  "Function to return the buffer details string shown on discord.
Swap this with your own function if you want a custom buffer-details message."
  :type 'function
  :group 'elcord)

(defcustom elcord-use-major-mode-as-main-icon 'nil
  "When enabled, the major mode determines the main icon, rather than it being the editor."
  :type 'boolean
  :group 'elcord)

(defcustom elcord-show-small-icon 't
  "When enabled, show the small icon as well as the main icon."
  :type 'boolean
  :group 'elcord)

(defcustom elcord-editor-icon 'nil
  "Icon to use for the text editor. When nil, use the editor's native icon."
  :type '(choice (const :tag "Editor Default" nil)
                 (const :tag "Emacs" "emacs_icon")
                 (const :tag "Emacs (Pen)" "emacs_pen_icon")
                 (const :tag "Emacs (Material)" "emacs_material_icon")
                 (const :tag "Emacs (Legacy)" "emacs_legacy_icon")
                 (const :tag "Emacs (Dragon)" "emacs_dragon_icon")
                 (const :tag "Spacemacs" "spacemacs_icon")
                 (const :tag "Doom" "doom_icon")
                 (const :tag "Doom Cute" "doom_cute_icon"))
  :group 'elcord)

(defcustom elcord-boring-buffers-regexp-list '("^ "
                                               "\\\\*Messages\\\\*")
  "A list of regexp's to match boring buffers.
When visiting a boring buffer, it will not show in the elcord presence."
  :type '(repeat regexp)
  :group 'elcord)

(defcustom elcord-discord-ipc-path 'nil
  "Path to the Discord IPC pipe. When nil, the default path is used."
  :type 'string
  :group 'elcord)

;;;###autoload
(define-minor-mode elcord-mode
  "Global minor mode for displaying Rich Presence in Discord."
  nil nil nil
  :require 'elcord
  :global t
  :group 'elcord
  :after-hook
  (progn
    (cond
     (elcord-mode
      (elcord--enable))
     (t
      (elcord--disable)))))

(defvar elcord--editor-name
  (cond
   ((boundp 'spacemacs-version) "Spacemacs")
   ((boundp 'doom-version) "DOOM Emacs")
   (t "Emacs"))
  "The name to use to represent the current editor.")

(defvar elcord--discord-ipc-pipe-format "discord-ipc-%d"
  "The name of the discord IPC pipe.")

(defvar elcord--update-presence-timer nil
  "Timer which periodically updates Discord Rich Presence.
nil when elcord is not active.")

(defvar elcord--reconnect-timer nil
  "Timer used by elcord to attempt connection periodically, when active but disconnected.")

(defvar elcord--sock nil
  "The process used to communicate with Discord IPC.")

(defvar elcord--last-known-position (count-lines (point-min) (point))
  "Last known position (line number) recorded by elcord.")

(defvar elcord--last-known-buffer-name (buffer-name)
  "Last known buffer recorded by elcord.")

(defvar elcord--stdpipe-path (expand-file-name
                              "stdpipe.ps1"
                              (file-name-directory (file-truename load-file-name)))
  "Path to the 'stdpipe' script.
On Windows, this script is used as a proxy for the Discord named pipe.
Unused on other platforms.")

(defvar elcord--idle-status nil
  "Current idle status.")

(defun elcord--find-discord-ipc-pipe ()
  "Find the path to the Discord IPC pipe."
  (if elcord-discord-ipc-path
      ;; If the user has specified the pipe location, trust them
      elcord-discord-ipc-path
    ;; Otherwise, try to find the pipe in the usual places
    (let ((candidates
           (mapcan
            (lambda (dir)
              (mapcar
               (lambda (num)
                 (expand-file-name (format elcord--discord-ipc-pipe-format num) dir))
               (number-sequence 0 9)))
            (list (expand-file-name "app/com.discordapp.Discord"
                                    (getenv "XDG_RUNTIME_DIR"))
                  (getenv "XDG_RUNTIME_DIR")
                  (getenv "TMPDIR")
                  (getenv "TMP")
                  (getenv "TEMP")
                  "/tmp"))))
      (cl-loop for candidate in candidates
               until (file-exists-p candidate)
               finally return candidate))))

(defun elcord--make-process ()
  "Make the asynchronous process that communicates with Discord IPC."
  (let ((default-directory "~/"))
    (cl-case system-type
      (windows-nt
       (make-process
        :name "*elcord-sock*"
        :command (list
                  "PowerShell"
                  "-NoProfile"
                  "-ExecutionPolicy" "Bypass"
                  "-Command" elcord--stdpipe-path "." (format elcord--discord-ipc-pipe-format 0))
        :connection-type 'pipe
        :sentinel 'elcord--connection-sentinel
        :filter 'elcord--connection-filter
        :noquery t))
      (t
       (make-network-process
        :name "*elcord-sock*"
        :remote (elcord--find-discord-ipc-pipe)
        :service nil
        :sentinel 'elcord--connection-sentinel
        :filter 'elcord--connection-filter
        :noquery t)))))

(defun elcord--enable ()
  "Called when variable ‘elcord-mode’ is enabled."
  (setq elcord--startup-time (string-to-number (format-time-string "%s" (current-time))))
  (unless (elcord--resolve-client-id)
    (warn "elcord: no elcord-client-id available"))
  (when (eq system-type 'windows-nt)
    (unless (executable-find "powershell")
      (warn "elcord: powershell not available"))
    (unless (file-exists-p elcord--stdpipe-path)
      (warn "elcord: 'stdpipe' script does not exist (%s)" elcord--stdpipe-path)))
  (when elcord-idle-timer
    (run-with-idle-timer
     elcord-idle-timer t 'elcord--start-idle))

  ;;Start trying to connect
  (elcord--start-reconnect))

(defun elcord--disable ()
  "Called when variable ‘elcord-mode’ is disabled."
  ;;Cancel updates
  (elcord--cancel-updates)
  ;;Cancel any reconnect attempt
  (elcord--cancel-reconnect)

  ;;If we're currently connected
  (when elcord--sock
    ;;Empty our presence
    (elcord--empty-presence))

  ;;Stop running idle hook
  (cancel-function-timers 'elcord--start-idle)

  (elcord--disconnect))

(defun elcord--empty-presence ()
  "Sends an empty presence for when elcord is disabled."
  (let* ((nonce (format-time-string "%s%N"))
         (presence
          `(("cmd" . "SET_ACTIVITY")
            ("args" . (("activity" . nil)
                       ("pid" . ,(emacs-pid))))
            ("nonce" . ,nonce))))
    (elcord--send-packet 1 presence)))

(defun elcord--resolve-client-id ()
  "Evaluate `elcord-client-id' and return the client ID to use."
  (cl-typecase elcord-client-id
    (null
     nil)
    (string
     elcord-client-id)
    (function
     (funcall elcord-client-id))))

(defun elcord--resolve-icon-base (icon)
  "Evaluate `elcord-icon-base' and return the URL to use.
Argument ICON the name of the icon we're resolving."
  (cl-typecase elcord-icon-base
    (null
     nil)
    (string
     (concat elcord-icon-base icon ".png"))
    (function
     (funcall elcord-icon-base icon))))

(defun elcord--connection-sentinel (process evnt)
  "Track connection state change on Discord connection.
Argument PROCESS The process this sentinel is attached to.
Argument EVNT The event which triggered the sentinel to run."
  (cl-case (process-status process)
    ((closed exit)
     (elcord--handle-disconnect))
    (t)))

(defun elcord--connection-filter (process evnt)
  "Track incoming data from Discord connection.
Argument PROCESS The process this filter is attached to.
Argument EVNT The available output from the process."
  (elcord--start-updates))

(defun elcord--connect ()
  "Connects to the Discord socket."
  (or elcord--sock
      (ignore-errors
        (unless elcord-quiet
          (message "elcord: attempting reconnect.."))
        (setq elcord--sock (elcord--make-process))
        (condition-case nil
            (elcord--send-packet 0 `(("v" . 1) ("client_id" . ,(elcord--resolve-client-id))))
          (error
           (delete-process elcord--sock)
           (setq elcord--sock nil)))
        elcord--sock)))

(defun elcord--disconnect ()
  "Disconnect elcord."
  (when elcord--sock
    (delete-process elcord--sock)
    (setq elcord--sock nil)))

(defun elcord--reconnect ()
  "Attempt to reconnect elcord."
  (when (elcord--connect)
    ;;Reconnected.
    ;; Put a pending message unless we already got first handshake
    (unless (or elcord--update-presence-timer elcord-quiet)
      (message "elcord: connecting..."))
    (elcord--cancel-reconnect)))

(defun elcord--start-reconnect ()
  "Start attempting to reconnect."
  (unless (or elcord--sock elcord--reconnect-timer)
    (setq elcord--reconnect-timer (run-at-time 0 15 'elcord--reconnect))))

(defun elcord--cancel-reconnect ()
  "Cancels any ongoing reconnection attempt."
  (when elcord--reconnect-timer
    (cancel-timer elcord--reconnect-timer)
    (setq elcord--reconnect-timer nil)))

(defun elcord--handle-disconnect ()
  "Handles reconnecting when socket disconnects."
  (unless elcord-quiet
    (message "elcord: disconnected by remote host"))
  ;;Stop updating presence for now
  (elcord--cancel-updates)
  (setq elcord--sock nil)
  ;;Start trying to reconnect
  (when elcord-mode
    (elcord--start-reconnect)))

(defun elcord--send-packet (opcode obj)
  "Packs and sends a packet to the IPC server.
Argument OPCODE OP code to send.
Argument OBJ The data to send to the IPC server."
  (let* ((jsonstr
          (encode-coding-string
           (json-encode obj)
           'utf-8))
         (datalen (length jsonstr))
         (message-spec
          `((:op u32r)
            (:len u32r)
            (:data str ,datalen)))
         (packet
          (bindat-pack
           message-spec
           `((:op . ,opcode)
             (:len . ,datalen)
             (:data . ,jsonstr)))))
    (when elcord--sock
      (process-send-string elcord--sock packet))))

(defun elcord--test-match-p (test mode)
  "Test `MODE' against `TEST'.
if `test' is a symbol, it is compared directly to `mode'.
if `test' is a string, it is a regex to compare against the name of `mode'."
  (cl-typecase test
    (symbol (eq test mode))
    (string (string-match-p test (symbol-name mode)))))

(defun elcord--entry-value (entry mode)
  "Test `ENTRY' against `MODE'.  Return the value of `ENTRY'.
`entry' is a cons who's `car' is `elcord--test-match-p' with `mode''
When `mode' matches, if the `cdr' of `entry' is a string, return that,
otherwise if it is a function, call it with `mode' and return that value."
  (when (elcord--test-match-p (car entry) mode)
    (let ((mapping (cdr entry)))
      (cl-typecase mapping
        (string mapping)
        (function (funcall mapping mode))))))

(defun elcord--find-mode-entry (alist mode)
  "Get the first entry in `ALIST' matching `MODE'.
`alist' Should be an alist like `elcord-mode-icon-alist' where each value is
 either a string,or a function of one argument `mode'.
 If it is a function, it should return a string, or nil if no match."
  (let ((cell alist)
        (result nil))
    (while cell
      (setq result (elcord--entry-value (car cell) mode)
            cell (if result nil (cdr cell))))
    result))

(defun elcord--editor-icon ()
  "The icon to use to represent the current editor."
  (elcord--resolve-icon-base
   (cond
    ((progn elcord-editor-icon) elcord-editor-icon)
    ((boundp 'spacemacs-version) "spacemacs_icon")
    ((boundp 'doom-version) "doom_icon")
    (t "emacs_icon"))))

(defun elcord--mode-icon ()
  "Figure out what icon to use for the current major mode.
If an icon is mapped by `elcord-mode-icon-alist', then that is used.
Otherwise, if the mode is a derived mode, try to find an icon for it.
If no icon is available, use the default icon."
  (let* ((modes (derived-mode-all-parents major-mode))
         (mode (pop modes))
         (ret (elcord--editor-icon)))
    (while mode
      (if-let ((icon (elcord--find-mode-entry elcord-mode-icon-alist mode)))
          (setq ret (elcord--resolve-icon-base icon)
                mode nil)
        (setq mode (pop modes))))
    ret))

(defun elcord--mode-text ()
  "Figure out what text to use for the current major mode.
If an icon is mapped by `elcord-mode-text-alist', then that is used.
Otherwise, if the mode is a derived mode, try to find text for its parent,
If no text is available, use the value of `mode-name'."
  (let ((mode major-mode)
        (ret mode-name))
    (while mode
      (if-let ((text (elcord--find-mode-entry elcord-mode-text-alist mode)))
          (setq ret text
                mode nil)
        (setq mode (get mode 'derived-mode-parent))))
    (unless (stringp ret)
      (setq ret (format-mode-line ret)))
    ret))

(defun elcord--mode-icon-and-text ()
  "Obtain the icon & text to use for the current major mode.
\((\"large_text\" . <text>)
  (\"large_image\" . <icon-name>)
  (\"small_text\" . <text>)
  (\"small_image\" . <icon-name>))"
  (let ((text (elcord--mode-text))
        (icon (elcord--mode-icon))
        large-text large-image
        small-text small-image)
    (cond
     (elcord-use-major-mode-as-main-icon
      (setq large-text text
            large-image icon
            small-text elcord--editor-name
            small-image (elcord--editor-icon)))
     (t
      (setq large-text elcord--editor-name
            large-image (elcord--editor-icon)
            small-text text
            small-image icon)))
    (cond
     (elcord-show-small-icon
      (list
       (cons "large_text" large-text)
       (cons "large_image" large-image)
       (cons "small_text" small-text)
       (cons "small_image" small-image)))
     (t
      (list
       (cons "large_text" large-text)
       (cons "large_image" large-image)
       (cons "small_text" small-text))))))

(defun elcord-buffer-details-format ()
  "Return the buffer details string shown on discord."
  (format "Editing %s" (buffer-name)))

(defun elcord--details-and-state ()
  "Obtain the details and state to use for Discord's Rich Presence."
  (let ((activity (if elcord-display-buffer-details
                      (if elcord-display-line-numbers
                          (list
                           (cons "details" (funcall elcord-buffer-details-format-function))
                           (cons "state" (format "Line %s of %S"
                                                 (format-mode-line "%l")
                                                 (+ 1 (count-lines (point-min) (point-max))))))
                        (list
                         (cons "details" (funcall elcord-buffer-details-format-function))))
                    (list
                     (cons "details" "Editing")
                     (cons "state" (elcord--mode-text))))))
    (when elcord-display-elapsed
      (push (list "timestamps" (cons "start" elcord--startup-time)) activity))
    activity))

(defun elcord--set-presence ()
  "Set presence."
  (let* ((activity
          `(("assets" . (,@(elcord--mode-icon-and-text)))
            ,@(elcord--details-and-state)))
         (nonce (format-time-string "%s%N"))
         (presence
          `(("cmd" . "SET_ACTIVITY")
            ("args" . (("activity" . ,activity)
                       ("pid" . ,(emacs-pid))))
            ("nonce" . ,nonce))))
    (elcord--send-packet 1 presence)))

(defun elcord--buffer-boring-p (buffer-name)
  "Return non-nil if `BUFFER-NAME' is non-boring per `ELCORD-BORING-BUFFERS-REGEXP-LIST'."
  (let ((cell elcord-boring-buffers-regexp-list)
        (result nil))
    (while cell
      (if (string-match-p (car cell) buffer-name)
          (setq result t
                cell nil)
        (setq cell (cdr cell))))
    result))

(defun elcord--find-non-boring-window ()
  "Try to find a live window displaying a non-boring buffer."
  (let ((cell (window-list))
        (result nil))
    (while cell
      (let ((window (car cell)))
        (if (not (elcord--buffer-boring-p (buffer-name (window-buffer window))))
            (setq result window
                  cell nil)
          (setq cell (cdr cell)))))
    result))

(defun elcord--try-update-presence (new-buffer-name new-buffer-position)
  "Try updating presence with `NEW-BUFFER-NAME' and `NEW-BUFFER-POSITION' while handling errors and disconnections."
  (setq elcord--last-known-buffer-name new-buffer-name
        elcord--last-known-position new-buffer-position)
  (condition-case err
      ;;Try and set the presence
      (elcord--set-presence)
    (error
     (message "elcord: error setting presence: %s" (error-message-string err))
     ;;If we hit an error, cancel updates
     (elcord--cancel-updates)
     ;; Disconnect
     (elcord--disconnect)
     ;; and try reconnecting
     (elcord--start-reconnect))))

(defun elcord--update-presence ()
  "Conditionally update presence by testing the current buffer/line.
If there is no 'previous' buffer attempt to find a non-boring buffer to initialize to."
  (if (= elcord--last-known-position -1)
      (when-let ((window (elcord--find-non-boring-window)))
        (with-current-buffer (window-buffer window)
          (elcord--try-update-presence (buffer-name) (count-lines (point-min) (point)))))
    (let ((new-buffer-name (buffer-name (current-buffer))))
      (unless (elcord--buffer-boring-p new-buffer-name)
        (let ((new-buffer-position (count-lines (point-min) (point))))
          (unless (and (string= new-buffer-name elcord--last-known-buffer-name)
                       (= new-buffer-position elcord--last-known-position))
            (elcord--try-update-presence new-buffer-name new-buffer-position)))))))

(defun elcord--start-updates ()
  "Start sending periodic update to Discord Rich Presence."
  (unless elcord--update-presence-timer
    (unless elcord-quiet
      (message "elcord: connected. starting updates"))
    ;;Start sending updates now that we've heard from discord
    (setq elcord--last-known-position -1
          elcord--last-known-buffer-name ""
          elcord--update-presence-timer (run-at-time 0 elcord-refresh-rate 'elcord--update-presence))))

(defun elcord--cancel-updates ()
  "Stop sending periodic update to Discord Rich Presence."
  (when elcord--update-presence-timer
    (cancel-timer elcord--update-presence-timer)
    (setq elcord--update-presence-timer nil)))

(defun elcord--start-idle ()
  "Set presence to idle, pause update and timer."
  (unless elcord--idle-status
    (unless elcord-quiet
      (message (format "elcord: %s" elcord-idle-message)))

    ;;hacky way to stop updates and store elapsed time
    (cancel-timer elcord--update-presence-timer)
    (setq elcord--startup-time (string-to-number (format-time-string "%s" (time-subtract nil elcord--startup-time)))

          elcord--idle-status t)

    (let* ((activity
            `(("assets" . (,@(elcord--mode-icon-and-text)))
              ("timestamps" ("start" ,@(string-to-number (format-time-string "%s" (current-time)))))
              ("details" . "Idle") ("state" .  ,elcord-idle-message)))
           (nonce (format-time-string "%s%N"))
           (presence
            `(("cmd" . "SET_ACTIVITY")
              ("args" . (("activity" . ,activity)
                         ("pid" . ,(emacs-pid))))
              ("nonce" . ,nonce))))
      (elcord--send-packet 1 presence))
    (add-hook 'pre-command-hook 'elcord--cancel-idle)))

(defun elcord--cancel-idle ()
  "Resume presence update and timer."
  (when elcord--idle-status
    (remove-hook 'pre-command-hook 'elcord--cancel-idle)

    ;;resume timer with elapsed time
    (setq elcord--startup-time (string-to-number (format-time-string "%s" (time-subtract nil elcord--startup-time)))
          elcord--idle-status nil
          ;;hacky way to resume updates
          elcord--update-presence-timer nil)
    (elcord--start-updates)

    (unless elcord-quiet
      (message "elcord: welcome back"))))


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