;;; +circe.el -*- lexical-binding: t; -*-

;;; Code:

(require '+util)
(require 'circe)

(defgroup +circe nil
  "Extra customizations for Circe."
  :group 'circe)

(defcustom +circe-left-margin 16
  "The size of the margin on the left."
  :type 'integer)

(defcustom +circe-network-inhibit-autoconnect nil
  "Servers to inhibit autoconnecting from `circe-network-options'."
  :type '(repeat string))

;;; Connecting to IRC

;;;###autoload
(defun +irc ()
  "Connect to all IRC networks in `circe-network-options'."
  (interactive)
  (dolist (network (mapcar 'car circe-network-options))
    (unless (member network +circe-network-inhibit-autoconnect)
      (+circe-maybe-connect network))))

(defun +circe-network-connected-p (network)
  "Return t if connected to NETWORK, nil otherwise."
  (catch 'return
    (dolist (buffer (circe-server-buffers))
      (with-current-buffer buffer
        (when (string= network circe-server-network)
          (throw 'return t))))))

(defun +circe-maybe-connect (network)
  "Connect to NETWORK, asking for confirmation to reconnect."
  (interactive ("sNetwork: "))
  (when (or (not (+circe-network-connected-p network))
            (yes-or-no-p (format "Already connected to %s, reconnect? "
                                 network)))
    (circe network)))

;;; Channel information

(defun +circe-current-topic (&optional message)
  "Return the topic of the current channel.
When called with optional MESSAGE non-nil, or interactively, also
message the current topic."
  (interactive "p")
  (let ((topic
         (save-excursion
           (goto-char (point-max))
           (or (re-search-backward
                (rx (group "*** "
                           (or "Topic" "topic" "TOPIC")
                           (* (not ":")) ": ")
                    (group (+ nonl)))))
           (buffer-substring-no-properties
            (match-beginning 2) (match-end 2)))))
    (when message
      (message "%s" topic))
    topic))

;;; Formatting messages

(defun +circe-format-meta (string &optional no-nick)
  "Return a format string for `lui-format' for metadata messages.
Include nick unless NO-NICK is non-nil.  If NO-NICK is a string,
replace {nick} in the string with {NO-NICK}."
  (cond
   ((stringp no-nick)
    (format "{%1$s:%2$d.%2$ds} *** %3$s"
            no-nick (- +circe-left-margin 3) string))
   (no-nick
    (format (format "%%%ds *** %s" (- +circe-left-margin 3) string) " "))
   (t
    (format "{nick:%1$d.%1$ds} *** %s" (- +circe-left-margin 3) string))))

(defun +circe-format-meta* (string)
  "Return a format string for `lui-format' for metadata messages, /without/ ")

(defmacro +lui-make-formatting-list-rx (char)
  "Make a formatting regex for CHAR delimiters.
For entry into `lui-formatting-list'."
  `(rx (or bol whitespace)
         (group ,char (+? (not (any whitespace ,char))) ,char)
         (or eol whitespace)))

;;; Hooks & Advice

(defun +circe-chat@set-prompt ()
  "Set the prompt to the (shortened) buffer name."
  (interactive)
  (lui-set-prompt (propertize (+string-align (buffer-name) +circe-left-margin
                                             :after " > "
                                             :ellipsis "~"
                                             :alignment 'right))))

(defun +circe-kill-buffer (&rest _)
  "Kill a circe buffer without confirmation, and after a delay."
  (let ((circe-channel-killed-confirmation)
        (circe-server-killed-confirmation))
    (when (derived-mode-p 'lui-mode)    ; don't spuriously kill
      (ignore-errors
        (kill-buffer)))))

(defun +circe-quit@kill-buffer (&rest _)
  "ADVICE: kill all buffers of a server after `circe-command-QUIT'."
  (with-circe-server-buffer
    (dolist (buf (circe-server-buffers))
      (with-current-buffer buf
        (+circe-kill-buffer)))
    (+circe-kill-buffer)))

(defun +circe-gquit@kill-buffer (&rest _)
  "ADVICE: kill all Circe buffers after `circe-command-GQUIT'."
  (let ((circe-channel-killed-confirmation)
        (circe-server-killed-confirmation))
    (dolist (buf (circe-server-buffers))
      (with-current-buffer buf
        (+circe-quit@kill-buffer)))))

(defun +circe-quit-all@kill-emacs ()
  "Quit all circe buffers when killing Emacs."
  (ignore-errors
    (advice-remove 'circe-command-GQUIT
                   'circe-gquit@kill-buffer)
    (circe-command-GQUIT "Quitting Emacs, bye!")))

;;; Patches

(require 'el-patch)

(el-patch-feature circe)
(defvar +circe-server-buffer-action 'pop-to-buffer-same-window
  "What to do with `circe-server' buffers when created.")

(el-patch-defun circe (network-or-server &rest server-options)
    "Connect to IRC.

Connect to the given network specified by NETWORK-OR-SERVER.

When this function is called, it collects options from the
SERVER-OPTIONS argument, the user variable
`circe-network-options', and the defaults found in
`circe-network-defaults', in this order.

If NETWORK-OR-SERVER is not found in any of these variables, the
argument is assumed to be the host name for the server, and all
relevant settings must be passed via SERVER-OPTIONS.

All SERVER-OPTIONS are treated as variables by getting the string
\"circe-\" prepended to their name. This variable is then set
locally in the server buffer.

See `circe-network-options' for a list of common options."
    (interactive (circe--read-network-and-options))
    (let* ((options (circe--server-get-network-options network-or-server
                                                       server-options))
           (buffer (circe--server-generate-buffer options)))
      (with-current-buffer buffer
        (circe-server-mode)
        (circe--server-set-variables options)
        (circe-reconnect))
      (el-patch-swap (pop-to-buffer-same-window buffer)
                     (funcall +circe-server-buffer-action buffer))))

;;; Chat commands

(defun circe-command-SLAP (nick)
  "Slap NICK around a bit with a large trout."
  (interactive (list (completing-read "Nick to slap: "
                                      (circe-channel-nicks)
                                      nil t nil)))
  (circe-command-ME (format "slaps %s about a bit with a large trout" nick)))

;;; Filtering functions
;; Set `lui-input-function' to `+lui-filter', then add the filters you want to
;; `circe-channel-mode-hook'.

(defvar +lui-filters nil
  "Stack of input functions to apply.
This is an alist with cells of the structure (TAG . FN), so we
can easily remove elements.")
(make-variable-buffer-local '+lui-filters)

(defun +lui-filter (text &optional fn-alist)
  (let ((fs (nreverse (purecopy (or fn-alist +lui-filters)))))
    (while fs
      (setq text (funcall (cdr (pop fs)) text)))
    (circe--input text)))

(defmacro +circe-define-filter (name docstring &rest body)
  "Define a filter for circe-inputted text."
  (declare (doc-string 2)
           (indent 1))
  (let (plist)
    (while (keywordp (car-safe body))
      (push (pop body) plist)
      (push (pop body) plist))
    ;; Return value
    `(define-minor-mode ,name
       ,docstring
       ,@(nreverse plist)
       (when (derived-mode-p 'circe-chat-mode)
         (if ,name
             (push '(,name . (lambda (it) ,@body)) +lui-filters)
           (setq +lui-filters
                 (assoc-delete-all ',name +lui-filters)))))))

;; CAPPY HOUR! (Pure idiocy)

(+circe-define-filter +circe-cappy-hour-mode
  "ENABLE CAPPY HOUR IN CIRCE!"
  :lighter " CAPPY HOUR"
  (upcase it))

;; URL Shortener

(+circe-define-filter +circe-shorten-url-mode
  "Shorten long urls when chatting."
  :lighter " c0x0"
  (+circe-0x0-shorten-urls it))

(defvar +circe-0x0-max-length 20
  "Maximum length of URLs before using a shortener.")

(defun +circe-0x0-shorten-urls (text)
  "Find urls in TEXT and shorten them using `0x0'."
  (require '0x0)
  (require 'browse-url)
  (let ((case-fold-search t))
    (replace-regexp-in-string
     browse-url-button-regexp
     (lambda (match)
       (if (> (length match) +circe-0x0-max-length)
           (+with-message (format "Shortening URL: %s" match)
             (0x0-shorten-uri (0x0--choose-server)
                              (substring-no-properties match)))
         match))
     text)))

(defun +circe-shorten-urls-all ()
  "Turn on `+circe-shorten-url-mode' in all chat buffers."
  (interactive)
  (+mapc-some-buffers
   (lambda () (+circe-shorten-url-mode +1))
   (lambda (buf)
     (derived-mode-p 'circe-chat-mode))))

;; Temperature conversion

(+circe-define-filter +circe-F/C-mode
  "Convert degF to degF/degC for international chats."
  :lighter " F/C"
  (str-F/C it))

(defun fahrenheit-to-celsius (degf)
  "Convert DEGF to Celsius."
  (round (* (/ 5.0 9.0) (- degf 32))))

(defun celsius-to-fahrenheit (degc)
  "Convert DEGC to Fahrenheit."
  (round (+ 32 (* (/ 9.0 5.0) degc))))

(defun str-F/C (text)
  (replace-regexp-in-string "[^.]\\([[:digit:]]+\\(?:\\.[[:digit:]]+\\)?[fF]\\)"
                            (lambda (match)
                              (format "%s/%dC" match
                                      (fahrenheit-to-celsius
                                       (string-to-number match))))
                            text
                            nil 1))

(provide '+circe)
;;; +circe.el ends here
