;;; circe.el --- Client for IRC in Emacs -*- lexical-binding: t -*-

;; Copyright (C) 2005 - 2015  Jorgen Schaefer

;; Package-Version: 20260525.1114
;; Package-Revision: 4bd1e964066f
;; Package-Requires: ((emacs "25.1") (cl-lib "0.5"))
;; Keywords: IRC, chat, comm
;; Author: Jorgen Schaefer <forcer@forcix.cx>
;; URL: https://github.com/emacs-circe/circe

;; This file is part of Circe.

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

;; Circe is a Client for IRC in Emacs. It integrates well with the rest
;; of the editor, using standard Emacs key bindings and indicating
;; activity in channels in the status bar so it stays out of your way
;; unless you want to use it.

;;; Code:

(defvar circe-version "2.14"
  "Circe version string.")

(require 'circe-compat)

(require 'ring)
(require 'timer)
(require 'lui)
(require 'lui-format)
(require 'lui-logging)
(require 'lcs)
(require 'irc)

;; Used to be optional. But sorry, we're in the 21st century already.
(require 'lui-irc-colors)

;; necessary for inheriting from diff-added and diff-removed faces
(require 'diff-mode)

(defgroup circe nil
  "Yet Another Emacs IRC Client."
  :prefix "circe-"
  :group 'applications)

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Customization Options ;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;;;;;;;;;;;;;;
;;;; Faces ;;;;
;;;;;;;;;;;;;;;

(defface circe-prompt-face
  '((t :weight bold :foreground "Black" :background "LightSeaGreen"))
  "The face for the Circe prompt."
  :group 'circe)

(defface circe-server-face
  '((((type tty)) :foreground "blue" :weight bold)
    (((background dark)) :foreground "#5095cf")
    (((background light)) :foreground "#3840b0")
    (t :foreground "SteelBlue"))
  "The face used to highlight server messages."
  :group 'circe)

(defface circe-highlight-nick-face
  '((default :weight bold)
    (((type tty)) :foreground "cyan")
    (((background dark)) :foreground "#82e2ed")
    (((background light)) :foreground "#0445b7")
    (t :foreground "CadetBlue3"))
  "The face used to highlight messages directed to us."
  :group 'circe)

(defface circe-my-message-face '((t))
  "The face used to highlight our own messages."
  :group 'circe)

(defface circe-originator-face '((t))
  "The face used to highlight the originator of a message."
  :group 'circe)

(defface circe-topic-diff-new-face '((t :inherit diff-added))
  "The face used for text added to a topic.
See the {topic-diff} parameter to `circe-format-server-topic'."
  :group 'circe)

(defface circe-topic-diff-removed-face '((t :inherit diff-removed))
  "The face used for text removed from a topic.
See the {topic-diff} parameter to `circe-format-server-topic'."
  :group 'circe)

(defface circe-fool-face
  '((((type tty)) :foreground "grey40" :bold t)
    (t :foreground "grey40"))
  "The face used for fools.
See `circe-fool-list'."
  :group 'circe)

;;;;;;;;;;;;;;;;;;;
;;;; Variables ;;;;
;;;;;;;;;;;;;;;;;;;

(defcustom circe-default-nick (user-login-name)
  "The default nick for circe."
  :type 'string
  :group 'circe)

(defcustom circe-default-user circe-default-nick
  "The default user name for circe."
  :type 'string
  :group 'circe)

(defcustom circe-default-realname (if (string= (user-full-name) "")
                                      circe-default-nick
                                    (user-full-name))
  "The default real name for circe."
  :type 'string
  :group 'circe)

(defcustom circe-default-ip-family nil
  "Default IP family to use.

  nil  - Use either IPv4 or IPv6.

  \\='ipv4 - Use IPv4

  \\='ipv6 - Use IPv6"
  :type '(choice (const :tag "Both" nil)
                 (const :tag "IPv4" ipv4)
                 (const :tag "IPv6" ipv6))
  :group 'circe)

(defcustom circe-default-directory "~/"
  "The value of `default-directory' for Circe buffers."
  :type 'string
  :group 'circe)

(defcustom circe-network-options nil
  "Network options.

This alist maps network names to respective options.

Common options:

  :pass - The IRC server password to use for this network, or a
          function to fetch it.
  :nick - The nick name to use (defaults to `circe-default-nick')
  :user - The user name to use (defaults to `circe-default-user')
  :realname - The real name to use (defaults to `circe-default-realname')

  :channels - A plist of channels to join (see `circe-channels').
  :server-buffer-name - Format to be used for the server buffer name
                        (see `circe-server-buffer-name')

  :host - The host name of the server to connect to.
  :port - The port or service name for the server.
  :use-tls - A boolean indicating as to whether to use TLS or
             not (defaults to nil). If you set this, you'll likely
             have to set :port as well.
  :tls-keylist - An alist of (client key file, client cert file) pairs
                 as defined in `gnutls-boot-parameters'. This defines
                 the private key and certificate used to connect to the
                 server.
  :ip-family - Option to enforce a specific IP version
               (defaults to `circe-default-ip-family')

  :nickserv-nick - The nick to authenticate with to nickserv, if configured.
                   (defaults to the value of :nick)
  :nickserv-password - The password to use for nickserv
                       authentication or a function to fetch it.

  :sasl-username - The username for SASL authentication.
  :sasl-password - The password for SASL authentication.
  :sasl-external - Option to specify if SASL EXTERNAL authentication should
                   be used.
  :sasl-strict - If SASL authentication is requested and it fails leave the
                 server."
  :type '(alist :key-type string :value-type plist)
  :group 'circe)

(defvar circe-network-defaults
  '(("2600net" :host "irc.2600.net" :port (6667 . 6697) :tls t
     :nickserv-mask "^NickServ!services@services\\.2600\\.net$"
     :nickserv-identify-challenge "This nickname is registered and protected\\."
     :nickserv-identify-command "PRIVMSG NickServ :IDENTIFY {nick} {password}"
     :nickserv-identify-confirmation "Password accepted - you are now recognized\\."
     :nickserv-ghost-command "PRIVMSG NickServ :GHOST {nick} {password}"
     :nickserv-ghost-confirmation "Ghost with your nick has been killed\\."
     )
    ("Bitlbee" :host "localhost" :port 6667
     :nickserv-mask "\\(bitlbee\\|root\\)!\\(bitlbee\\|root\\)@"
     :nickserv-identify-challenge "use the \x02identify\x02 command to identify yourself"
     :nickserv-identify-command "PRIVMSG &bitlbee :identify {password}"
     :nickserv-identify-confirmation "Password accepted, settings and accounts loaded"
     :lagmon-disabled t
     )
    ("DALnet" :host "irc.dal.net" :port (6667 . 6697) :tls t
     :nickserv-mask "^NickServ!service@dal\\.net$"
     :nickserv-identify-challenge "This nick is owned by someone else\\. Please choose another\\."
     :nickserv-identify-command "PRIVMSG NickServ@services.dal.net :IDENTIFY {nick} {password}"
     :nickserv-identify-confirmation "Password accepted for \x02.*\x02\\.$"
     :nickserv-ghost-command "PRIVMSG NickServ@services.dal.net :GHOST {nick} {password}"
     :nickserv-ghost-confirmation "^Your ghost has been successfully killed\\."
     )
    ("EFnet" :host "irc.efnet.org" :port (6667 . 6697) :tls t
     ;; no services, chanfix aside
     )
    ("EsperNet" :host "irc.esper.net" :port (6667 . 6697) :tls t
     ;; services are documented, but gone?
     )
    ("Freenode" :host "chat.freenode.net" :port (6667 . 6697) :tls t
     :nickserv-mask "^NickServ!NickServ@services\\.$"
     :nickserv-identify-challenge "\C-b/msg\\s-NickServ\\s-identify\\s-<password>\C-b"
     :nickserv-identify-command "PRIVMSG NickServ :IDENTIFY {nick} {password}"
     :nickserv-identify-confirmation "^You are now identified for \x02.*\x02\\.$"
     :nickserv-ghost-command "PRIVMSG NickServ :GHOST {nick} {password}"
     :nickserv-ghost-confirmation "has been ghosted\\.$\\|is not online\\.$"
     )
    ("GameSurge" :host "irc.gamesurge.net" :port 6667 :tls nil
     :nickserv-mask "^AuthServ!AuthServ@Services\\.GameSurge\\.net$"
     ;; due to lack of challenge string, none of the following will be used :(
     :nickserv-identify-command "PRIVMSG AuthServ@Services.GameSurge.net :AUTH {nick} {password}"
     :nickserv-identify-confirmation "^I recognize you\\.$"
     :nickserv-ghost-command "PRIVMSG AuthServ@Services.GameSurge.net :GHOST {nick}"
     :nickserv-ghost-confirmation "has been killed as a ghost\\.$"
     )
    ("GIMPNet" :host "irc.gimp.org" :port (6667 . 6697) :tls t
     :nickserv-mask "^NickServ!services@gimpnet-services\\.gimp\\.org$"
     :nickserv-identify-challenge "This nickname is registered and protected\\."
     :nickserv-identify-command "PRIVMSG NickServ :IDENTIFY {nick} {password}"
     :nickserv-identify-confirmation "^You are now logged in as .*\\.$"
     )
    ("Hackint" :host "irc.hackint.org" :port (nil . 6697) :tls t
     :nickserv-mask "^NickServ!NickServ@services\\.hackint\\.org$"
     :nickserv-identify-challenge "\C-b/msg\\s-NickServ\\s-identify\\s-<password>\C-b"
     :nickserv-identify-command "PRIVMSG NickServ :IDENTIFY {nick} {password}"
     :nickserv-identify-confirmation "^You are now identified for \x02.*\x02\\.$"
     :nickserv-ghost-command "PRIVMSG NickServ :GHOST {nick} {password}"
     :nickserv-ghost-confirmation "has been ghosted\\.$\\|is not online\\.$"
     )
    ("HybridIRC" :host "irc.hybridirc.com" :port (6667 . 6697) :tls t
     :nickserv-mask "^NickServ!services@services\\.irchighway\\.net$"
     :nickserv-identify-challenge "This nickname is registered\\."
     :nickserv-identify-command "PRIVMSG NickServ :IDENTIFY {nick} {password}"
     :nickserv-identify-confirmation "^You are now identified for .*\\.$"
     :nickserv-ghost-command "PRIVMSG NickServ :GHOST {nick} {password}"
     :nickserv-ghost-command "has been ghosted\\.$"
     )
    ("IRCnet" :host "irc.ircnet.com" :port (6667 . 6697) :tls t
     ;; no services, chanfix aside
     )
    ("IRCHighWay" :host "irc.irchighway.net" :port (6667 . 6697) :tls t
     :nickserv-mask "^NickServ!services@services\\.irchighway\\.net$"
     :nickserv-identify-challenge "This nickname is registered and protected\\."
     :nickserv-identify-command "PRIVMSG NickServ :IDENTIFY {nick} {password}"
     :nickserv-identify-confirmation "Password accepted - you are now recognized\\."
     :nickserv-ghost-command "PRIVMSG NicKServ :RECOVER {nick} {password}"
     :nickserv-ghost-confirmation "Ghost with your nick has been killed\\."
     )
    ("Libera Chat" :host "irc.libera.chat" :port (6667 . 6697) :tls t
     :nickserv-mask "^NickServ!NickServ@services\\.libera\\.chat$"
     :nickserv-identify-challenge "This nickname is registered\\."
     :nickserv-identify-command "PRIVMSG NickServ :IDENTIFY {nick} {password}"
     :nickserv-identify-confirmation "^You are now identified for \x02.*\x02\\.$"
     :nickserv-ghost-command "PRIVMSG NickServ :GHOST {nick} {password}"
     :nickserv-ghost-confirmation "has been ghosted\\.$\\|is not online\\.$"
     )
    ("OFTC" :host "irc.oftc.net" :port (6667 . 6697) :tls t
     :nickserv-mask "^NickServ!services@services\\.oftc\\.net$"
     :nickserv-identify-challenge "This nickname is registered and protected\\."
     :nickserv-identify-command "PRIVMSG NickServ :IDENTIFY {password} {nick}"
     :nickserv-identify-confirmation "^You are successfully identified as \x02.*\x02\\.$"
     )
    ("QuakeNet" :host "irc.quakenet.org" :port 6667 :tls nil
     ;; due to lack of challenge string, none of the following will be used :(
     :nickserv-mask "^Q!TheQBot@CServe\\.quakenet\\.org$"
     :nickserv-identify-command "PRIVMSG Q@CServe.quakenet.org AUTH {nick} {password}"
     :nickserv-identify-confirmation "^You are now logged in as .*$"
     )
    ("RektNetwork" :host "irc.rekt.network" :port (nil . 6697) :tls t
     :nickserv-mask "^NickServ!NickServ@services\\.int$"
     :nickserv-identify-challenge "This nickname is registered\\. Please choose a different nickname"
     :nickserv-identify-command "PRIVMSG NickServ :IDENTIFY {nick} {password}"
     :nickserv-identify-confirmation "^You are now identified for \x02.*\x02\\.$"
     :nickserv-ghost-command "PRIVMSG NickServ :GHOST {nick} {password}"
     :nickserv-ghost-confirmation "has been ghosted\\.$\\|is not online\\.$"
     )
    ("Rizon" :host "irc.rizon.net" :port (6667 . 6697) :tls t
     :nickserv-mask "^NickServ!service@rizon\\.net$"
     :nickserv-identify-challenge "This nickname is registered and protected\\."
     :nickserv-identify-command "PRIVMSG NickServ :IDENTIFY {password}"
     :nickserv-identify-confirmation "Password accepted - you are now recognized\\."
     :nickserv-ghost-command "PRIVMSG NickServ :GHOST {nick} {password}"
     :nickserv-ghost-confirmation "Ghost with your nick has been killed\\."
     )
    ("Snoonet" :host "irc.snoonet.org" :port (6667 . 6697) :tls t
     :nickserv-mask "^NickServ!NickServ@services$"
     :nickserv-identify-challenge "This nickname is registered and protected\\."
     :nickserv-identify-command "PRIVMSG NickServ :IDENTIFY {nick} {password}"
     :nickserv-identify-confirmation "Password accepted - you are now recognized\\."
     :nickserv-ghost-command "PRIVMSG NickServ :GHOST {nick} {password}"
     :nickserv-ghost-confirmation "Ghost with your nick has been killed\\."
     )
    ("TheAirlock" :host "irc.theairlock.net" :port (6667 . 7778) :tls t
     :nickserv-mask "^NickServ!services@services\\.theairlock\\.net$"
     :nickserv-identify-challenge "type \x02/msg\\s-NickServ\\s-IDENTIFY\\s-\C-_password\C-_\x02"
     :nickserv-identify-command "PRIVMSG NickServ :IDENTIFY {password}"
     :nickserv-identify-confirmation "Password accepted - you are now recognized\\."
     :nickserv-ghost-command "PRIVMSG NickServ :GHOST {nick} {password}"
     :nickserv-ghost-confirmation "Ghost with your nick has been killed\\."
     )
    ("tilde.chat" :host "irc.tilde.chat" :port (6667 . 6697) :tls t
     :nickserv-mask "^NickServ!services@services\\.tilde\\.chat$"
     :nickserv-identify-challenge "This nickname is registered and protected\\."
     :nickserv-identify-command "PRIVMSG NickServ :IDENTIFY {nick} {password}"
     :nickserv-identify-confirmation "Password accepted - you are now recognized\\."
     :nickserv-ghost-command "PRIVMSG NickServ :RECOVER {nick} {password}"
     :nickserv-ghost-confirmation "Ghost with your nick has been killed\\."
     )
    ("Undernet" :host "irc.undernet.org" :port 6667 :tls nil
     ;; due to lack of challenge string, none of the following will be used :(
     :nickserv-mask "^X!cservice@undernet\\.org$"
     :nickserv-identify-command "PRIVMSG X@channels.undernet.org LOGIN {nick} {password}"
     :nickserv-identify-confirmation "^AUTHENTICATION SUCCESSFUL as .*"
     )
    )
  "Alist of networks and connection settings.

See the `circe' command for details of this variable.")

(defcustom circe-default-quit-message "Using Circe, the loveliest of all IRC clients"
  "The default quit message when no other is given.

This is sent when the server buffer is killed or when /QUIT is
given with no argument."
  :type 'string
  :group 'circe)

(defcustom circe-default-part-message "Using Circe, the loveliest of all IRC clients"
  "How to part when a channel buffer is killed, or when no
argument is given to /PART."
  :type 'string
  :group 'circe)

(defcustom circe-auto-query-max 23
  "The maximum number of queries which are opened automatically.
If more messages arrive - typically in a flood situation - they
are displayed in the server buffer."
  :type 'integer
  :group 'circe)

(defcustom circe-use-cycle-completion nil
  "Whether Circe should use cycle completion.

If this is not nil, Circe will set `completion-cycle-threshold'
to t locally in Circe buffers, enabling cycle completion for
nicks no matter what completion style you use in the rest of
Emacs. If you set this to nil, Circe will not touch your default
completion style."
  :type 'boolean
  :group 'circe)

(defcustom circe-reduce-lurker-spam nil
  "If enabled, Circe will stop showing some messages.

This means that JOIN, PART, QUIT and NICK messages are not shown
for users on channels that have not spoken yet (\"lurker\"), or
haven't spoken in `circe-active-users-timeout' seconds. When they
speak for the first time, Circe displays their join time."
  :type 'boolean
  :group 'circe)

(defcustom circe-active-users-timeout nil
  "When non-nil, should be the number of seconds after which
active users are regarded as inactive again after speaking."
  :type 'integer
  :group 'circe)

(defcustom circe-prompt-string (concat (propertize ">"
                                                   'face 'circe-prompt-face)
                                       " ")
  "The string to initialize the prompt with.
To change the prompt dynamically or just in specific buffers, use
`lui-set-prompt' in the appropriate hooks."
  :type 'string
  :group 'circe)

(defcustom circe-extra-nicks nil
  "List of other nicks than your current one to highlight."
  :type '(repeat string)
  :group 'circe)

(defcustom circe-highlight-nick-type 'sender
  "How to highlight occurrences of our own nick.

  \\='sender     - Highlight the nick of the sender
                (messages without a sender and your
                own are highlighted with the occurrence
                type instead)
  \\='occurrence - Highlight the occurrences of the nick
  \\='message    - Highlight the message without the sender
  \\='all        - Highlight the whole line"
  :type '(choice (const :tag "Sender" sender)
                 (const :tag "Occurrences" occurrence)
                 (const :tag "Message" message)
                 (const :tag "Whole line" all))
  :group 'circe)

(defcustom circe-inhibit-nick-highlight-function nil
  "Function for inhibiting nick highlighting.
If non-nil, its value is called with the respective buffer
selected and point in the line that's about to get highlighted.
A non-nil return value inhibits any highlighting."
  :type '(choice (const :tag "None" nil)
                 function)
  :group 'circe)

(defcustom circe-completion-suffix ": "
  "A suffix for completed nicks at the beginning of a line."
  :type '(choice (const :tag "The standard suffix" ": "))
  :group 'circe)

(defcustom circe-ignore-list nil
  "List of regular expressions to ignore.

Each regular expression is matched against nick!user@host."
  :type '(repeat regexp)
  :group 'circe)

(defcustom circe-fool-list nil
  "List of regular expressions for fools.

Each regular expression is matched against nick!user@host.

Messages from such people are still inserted, but not shown. They
can be displayed using \\[lui-fool-toggle-display]."
  :type '(repeat regexp)
  :group 'circe)

(defcustom circe-ignore-functions nil
  "A list of functions to check whether we should ignore a message.

These functions get three arguments: NICK, USERHOST, and BODY. If
one of them returns a non-nil value, the message is ignored."
  :type 'hook
  :group 'circe)

(defcustom circe-split-line-length 440
  "The maximum length of a single message.
If a message exceeds this size, it is broken into multiple ones.

IRC allows for lines up to 512 bytes. Two of them are CR LF.
And a typical message looks like this:

  :nicky!uhuser@host212223.dialin.fnordisp.net PRIVMSG #lazybastards :Hello!

You can limit here the maximum length of the \"Hello!\" part.
Good luck."
  :type 'integer
  :group 'circe)

(defcustom circe-server-max-reconnect-attempts 5
  "How often Circe should attempt to reconnect to the server.
If this is 0, Circe will not reconnect at all. If this is nil,
it will try to reconnect forever (not recommended)."
  :type '(choice integer
                 (const :tag "Forever" nil))
  :group 'circe)

(defcustom circe-netsplit-delay 60
  "The number of seconds a netsplit may be dormant.
If anything happens with a netsplit after this amount of time,
the user is re-notified."
  :type 'number
  :group 'circe)

(defcustom circe-server-killed-confirmation 'ask-and-kill-all
  "How to ask for confirmation when a server buffer is killed.
This can be one of the following values:
  ask - Ask the user for confirmation
  ask-and-kill-all - Ask the user, and kill all associated buffers
  kill-all - Don't ask the user, and kill all associated buffers
  nil - Kill first, ask never"
  :type '(choice (const :tag "Ask before killing" ask)
                 (const :tag "Ask, then kill all associated buffers"
                        ask-and-kill-all)
                 (const :tag "Don't ask, then kill all associated buffers"
                        kill-all)
                 (const :tag "Don't ask" nil))
  :group 'circe)

(defcustom circe-channel-killed-confirmation 'ask
  "How to ask for confirmation when a channel buffer is killed.
This can be one of the following values:
  ask - Ask the user for confirmation
  nil - Don't ask, just kill"
  :type '(choice (const :tag "Ask before killing" ask)
                 (const :tag "Don't ask" nil))
  :group 'circe)

(defcustom circe-track-faces-priorities '(circe-highlight-nick-face
                                          lui-highlight-face
                                          circe-my-message-face
                                          circe-server-face)
  "A list of faces which should show up in the tracking.
The first face is kept if the new message has only lower faces,
or faces that don't show up at all."
  :type '(repeat face)
  :group 'circe)

(defcustom circe-server-send-unknown-command-p nil
  "Non-nil when Circe should just pass on commands it doesn't know.
E.g. /fnord foo bar would then just send \"fnord foo bar\" to the
server."
  :type 'boolean
  :group 'circe)

(defcustom circe-server-connected-hook nil
  "Hook run when we successfully connected to a server.
This is run from a 001 (RPL_WELCOME) message handler."
  :type 'hook
  :group 'circe)

(defcustom circe-server-auto-join-default-type :immediate
  "The default auto-join type to use.

Possible options:

:immediate - Immediately after registering on the server
:after-auth - After nickserv authentication succeeded
:after-cloak - After we have acquired a cloaked host name
:after-nick - After we regained our preferred nick, or after
              nickserv authentication if we don't need to regain
              it. See `circe-nickserv-ghost-style'.

See `circe-channels' for more details."
  :type '(choice (const :tag "Immediately" :immediate)
                 (const :tag "After Authentication" :after-auth)
                 (const :tag "After Cloaking" :after-cloak)
                 (const :tag "After Nick Regain" :after-nick))
  :group 'circe)

;;;;;;;;;;;;;;;;;
;;;; Formats ;;;;
;;;;;;;;;;;;;;;;;

(defgroup circe-format nil
  "Format strings for Circe.
All these formats always allow the {mynick} and {chattarget} format
strings."
  :prefix "circe-format-"
  :group 'circe)

(defcustom circe-format-not-tracked
  '(circe-format-server-message
    circe-format-server-notice
    circe--irc-format-server-numeric
    circe-format-server-topic
    circe-format-server-rejoin
    circe-format-server-lurker-activity
    circe-format-server-topic-time
    circe-format-server-topic-time-for-channel
    circe-format-server-netmerge
    circe-format-server-join
    circe-format-server-join-in-channel
    circe-format-server-host-change
    circe-format-server-mode-change
    circe-format-server-nick-change-self
    circe-format-server-nick-change
    circe-format-server-nick-regain
    circe-format-server-setname
    circe-format-server-part
    circe-format-server-netsplit
    circe-format-server-quit-channel
    circe-format-server-quit)
  "A list of formats that should not trigger tracking."
  :type '(repeat symbol)
  :group 'circe-format)

(defcustom circe-format-server-message "*** {body}"
  "The format for generic server messages.
{body} - The body of the message."
  :type 'string
  :group 'circe-format)

(defcustom circe-format-self-say "> {body}"
  "The format for messages to queries or channels.
{nick} - Your nick.
{body} - The body of the message."
  :type 'string
  :group 'circe-format)

(defcustom circe-format-self-action "* {nick} {body}"
  "The format for actions to queries or channels.
{nick} - Your nick.
{body} - The body of the action."
  :type 'string
  :group 'circe-format)

(defcustom circe-format-self-message "-> *{chattarget}* {body}"
  "The format for messages sent to other people outside of queries.
{chattarget} - The target nick.
{body} - The body of the message."
  :type 'string
  :group 'circe-format)

(defcustom circe-format-action "* {nick} {body}"
  "The format for actions in queries or channels.
{nick} - The nick doing the action.
{body} - The body of the action."
  :type 'string
  :group 'circe-format)

(defcustom circe-format-message-action "* *{nick}* {body}"
  "The format for actions in messages outside of queries.
{nick} - The nick doing the action.
{body} - The body of the action."
  :type 'string
  :group 'circe-format)

(defcustom circe-chat-buffer-name "{target}@{network}"
  "The format for buffer names.
{target} - The target of the buffer.
{network} - The name of the network."
  :type 'string
  :group 'circe-format)

(defcustom circe-format-say "<{nick}> {body}"
  "The format for normal channel or query talk.
{nick} - The nick talking.
{body} - The message."
  :type 'string
  :group 'circe-format)

(defcustom circe-format-message "*{nick}* {body}"
  "The format for a message outside of a query.
{nick} - The originator.
{body} - The message."
  :type 'string
  :group 'circe-format)

(defcustom circe-format-notice "-{nick}- {body}"
  "The format for a notice.
{nick} - The originator.
{body} - The notice."
  :type 'string
  :group 'circe-format)

(defcustom circe-format-server-notice "-Server Notice- {body}"
  "The format for a server notice.
{body} - The notice."
  :type 'string
  :group 'circe-format)

(defcustom circe-format-server-topic "*** Topic change by {nick} ({userhost}): {new-topic}"
  "The format for topic changes.

The following format arguments are available:

  nick       - The nick of the user who changed the topic
  userhost   - The user@host string of that user
  channel    - Where the topic change happened
  new-topic  - The new topic
  old-topic  - The previous topic
  topic-diff - A colorized diff of the topics"
  :type 'string
  :group 'circe-format)

(defcustom circe-format-server-lurker-activity
  "*** First activity: {nick} joined {joindelta} ago."
  "The format for the first-activity notice of a user.
{nick} - The originator.
{jointime} - The join time of the user (in seconds).
{joindelta} - The duration from joining until now."
  :type 'string
  :group 'circe-format)

(defcustom circe-format-server-rejoin
  "*** Re-join: {nick} ({userinfo}), left {departuredelta} ago"
  "The format for the re-join notice of a user.

The following format arguments are available:

  nick           - The nick of the user who joined
  userhost       - The user@host string of the user who joined
  accountname    - The account name, if the server supports this
  realname       - The real name, if the server supports this
  userinfo       - A combination of userhost, accountname, and realname
  channel        - A date string describing this time
  departuretime  - Time in seconds when the originator had left.
  departuredelta - Description of the time delta since the originator left."
  :type 'string
  :group 'circe-format)

(defcustom circe-server-buffer-name "{host}:{port}"
  "The format for the server buffer name.

The following format arguments are available:

  network  - The name of the network
  host     - The host name of the server
  port     - The port number or service name
  service  - Alias for port"
  :type 'string
  :group 'circe-format)

(defcustom circe-format-server-whois-idle-with-signon "*** {whois-nick} is {idle-duration} idle (signon on {signon-date}, {signon-ago} ago)"
  "Format for RPL_WHOISIDLE messages.

The following format arguments are available:

  whois-nick      - The nick this is about
  idle-seconds    - The number of seconds this nick has been idle
  idle-duration   - A textual description of the duration of the idle time
  signon-time     - The time (in seconds since the epoch) when this user
                    signed on
  signon-date     - A date string describing this time
  signon-ago      - A textual description of the duraction since signon"
  :type 'string
  :group 'circe-format)

(defcustom circe-format-server-whois-idle "*** {whois-nick} is {idle-duration} idle"
  "Format for RPL_WHOISIDLE messages.

The following format arguments are available:

  whois-nick      - The nick this is about
  idle-seconds    - The number of seconds this nick has been idle
  idle-duration   - A textual description of the duration of the idle time"
  :type 'string
  :group 'circe-format)

(defcustom circe-format-server-topic-time "*** Topic set by {setter} on {topic-date}, {topic-ago} ago"
  "Format for RPL_TOPICWHOTIME messages for the current channel.

The following format arguments are available:

  channel         - The channel the topic is for
  setter          - The nick of the person who set the topic
  setter-userhost - The user@host string of the person who set the topic
  topic-time      - The time the topic was set, in seconds since the epoch
  topic-date      - A date string describing this time
  topic-ago       - A textual description of the duration since the topic
                    was set"
  :type 'string
  :group 'circe-format)

(defcustom circe-format-server-topic-time-for-channel "*** Topic for {channel} set by {setter} on {topic-date}, {topic-ago} ago"
  "Format for RPL_TOPICWHOTIME messages for a channel we are not on.

The following format arguments are available:

  channel         - The channel the topic is for
  setter          - The nick of the person who set the topic
  setter-userhost - The user@host string of the person who set the topic
  topic-time      - The time the topic was set, in seconds since the epoch
  topic-date      - A date string describing this time
  topic-ago       - A textual description of the duration since the topic
                    was set"
  :type 'string
  :group 'circe-format)

(defcustom circe-format-server-channel-creation-time "*** Channel {channel} created on {date}, {ago} ago"
  "Format for RPL_CREATIONTIME messages for the current channel.

The following format arguments are available:

  channel  - The channel the topic is for
  date     - A date string describing this time
  ago      - A textual description of the duration since the channel
             was created"
  :type 'string
  :group 'circe-format)

(defcustom circe-format-server-ctcp-ping "*** CTCP PING request from {nick} ({userhost}) to {target}: {body} ({ago} ago)"
  "Format for CTCP PING requests.

The following format arguments are available:

  nick      - The nick of the user who sent this PING request
  userhost  - The user@host string of the user who sent this request
  target    - The target of the message, usually us, but can be a channel
  body      - The argument of the PING request, usually a number
  ago       - A textual description of the duration since the request
              was sent, if parseable"
  :type 'string
  :group 'circe-format)

(defcustom circe-format-server-ctcp-ping-reply "*** CTCP PING reply from {nick} ({userhost}) to {target}: {ago} ago ({body})"
  "Format for CTCP PING replies.

The following format arguments are available:

  nick      - The nick of the user who sent this PING request
  userhost  - The user@host string of the user who sent this request
  target    - The target of the message, usually us, but can be a channel
  body      - The argument of the PING request, usually a number
  ago       - A textual description of the duration since the request
              was sent, if parseable"
  :type 'string
  :group 'circe-format)

(defcustom circe-format-server-ctcp "*** CTCP {command} request from {nick} ({userhost}) to {target}: {body}"
  "Format for CTCP requests.

The following format arguments are available:

  nick      - The nick of the user who sent this PING request
  userhost  - The user@host string of the user who sent this request
  target    - The target of the message, usually us, but can be a channel
  command   - The CTCP command used
  body      - The argument of the PING request, usually a number"
  :type 'string
  :group 'circe-format)

(defcustom circe-format-server-netsplit "*** Netsplit: {split} (Use /WL to see who left)"
  "Format for netsplit notifications.

The following format arguments are available:

  split   - The name of the split, usually describing the servers involved"
  :type 'string
  :group 'circe-format)

(defcustom circe-format-server-netmerge "*** Netmerge: {split}, split {ago} ago (Use /WL to see who's still missing)"
  "Format for netmerge notifications.

The following format arguments are available:

  split   - The name of the split, usually describing the servers involved
  time    - The time when this split happened, in seconds
  date    - A date string describing this time
  ago     - A textual description of the duration since the split happened"
  :type 'string
  :group 'circe-format)

(defcustom circe-format-server-join "*** Join: {nick} ({userinfo})"
  "Format for join messages in a channel buffer.

The following format arguments are available:

  nick         - The nick of the user joining
  userhost     - The user@host string for the user
  accountname  - The account name, if the server supports this
  realname     - The real name, if the server supports this
  userinfo     - A combination of userhost, accountname, and realname
  channel      - The channel this user is joining"
  :type 'string
  :group 'circe-format)

(defcustom circe-format-server-join-in-channel "*** Join: {nick} ({userinfo}) joined {channel}"
  "Format for join messages in query buffers of the joining user.

The following format arguments are available:

  nick         - The nick of the user joining
  userhost     - The user@host string for the user
  accountname  - The account name, if the server supports this
  realname     - The real name, if the server supports this
  userinfo     - A combination of userhost, accountname, and realname
  channel      - The channel this user is joining"
  :type 'string
  :group 'circe-format)

(defcustom circe-format-server-mode-change "*** Mode change: {change} on {target} by {setter} ({userhost})"
  "Format for mode changes.

The following format arguments are available:

  setter       - The name of the split, usually describing the servers involved
  userhost     - The user@host string for the user
  target       - The target of this mode change
  change       - The actual changed modes"
  :type 'string
  :group 'circe-format)

(defcustom circe-format-server-nick-change-self "*** Nick change: You are now known as {new-nick}"
  "Format for nick changes of the current user.

The following format arguments are available:

  old-nick - The old nick this change was from
  new-nick - The new nick this change was to
  userhost - The user@host string for the user"
  :type 'string
  :group 'circe-format)

(defcustom circe-format-server-nick-change "*** Nick change: {old-nick} ({userhost}) is now known as {new-nick}"
  "Format for nick changes of other users.

The following format arguments are available:

  old-nick - The old nick this change was from
  new-nick - The new nick this change was to
  userhost - The user@host string for the user"
  :type 'string
  :group 'circe-format)

(defcustom circe-format-server-nick-regain "*** Nick regain: {old-nick} ({userhost}) is now known as {new-nick}"
  "Format for nick regains of the current user.

The following format arguments are available:

  old-nick - The old nick this change was from
  new-nick - The new nick this change was to
  userhost - The user@host string for the user"
  :type 'string
  :group 'circe-format)

(defcustom circe-format-server-setname "*** Realname changed from {oldname} to {newname}"
  "Format for realname change of the current user.

The following format arguments are available:

  oldname - The old realname this change was from
  newname - The new realname this change was to"
  :type 'string
  :group 'circe-format)

(defcustom circe-format-server-host-change "*** Nick changed host: {nick} is now known as {userhost}"
  "Format for username/host change of the current user.

The following format arguments are available:

  nick     - The nickname of the affected user
  old-user - The old username this change was from
  old-host - The old host this change was from
  new-user - The new username this change was to
  new-host - The new host this change was to
  userhost - The new user@host string"
  :type 'string
  :group 'circe-format)

(defcustom circe-format-server-part "*** Part: {nick} ({userhost}) left {channel}: {reason}"
  "Format for users parting a channel.

The following format arguments are available:

  nick     - The nick of the user who left
  userhost - The user@host string for this user
  channel  - The channel they left
  reason   - The reason they gave for leaving"
  :type 'string
  :group 'circe-format)

(defcustom circe-format-server-quit-channel "*** Quit: {nick} ({userhost}) left {channel}: {reason}"
  "Format for users quitting from a channel.

The following format arguments are available:

  nick     - The nick of the user who left
  userhost - The user@host string for this user
  channel  - The channel they left
  reason   - The reason they gave for leaving"
  :type 'string
  :group 'circe-format)

(defcustom circe-format-server-quit "*** Quit: {nick} ({userhost}) left IRC: {reason}"
  "Format for users quitting.

The following format arguments are available:

  nick     - The nick of the user who left
  userhost - The user@host string for this user
  reason   - The reason they gave for leaving"
  :type 'string
  :group 'circe-format)

;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Private variables ;;;
;;;;;;;;;;;;;;;;;;;;;;;;;

(defvar circe-source-url "https://github.com/emacs-circe/circe"
  "URL to Circe's source repository")

(defvar circe-host nil
  "The name of the server we're currently connected to.")
(make-variable-buffer-local 'circe-host)

(defvar circe-port nil
  "The port number or service name of the server.")
(make-variable-buffer-local 'circe-host)

(defvar circe-network nil
  "The network name of the server we're currently connected to.")
(make-variable-buffer-local 'circe-network)

(defvar circe-ip-family nil
  "The IP family in use.
See `make-network-process' and :family for valid values.")
(make-variable-buffer-local 'circe-ip-family)

(defvar circe-nick nil
  "Our current nick.")
(make-variable-buffer-local 'circe-nick)

(defvar circe-user nil
  "The current user name.")
(make-variable-buffer-local 'circe-user)

(defvar circe-realname nil
  "The current real name.")
(make-variable-buffer-local 'circe-realname)

(defvar circe-pass nil
  "The password for the current server or a function to recall it.

If a function is set it will be called with the value of `circe-host'.")
(make-variable-buffer-local 'circe-pass)

(defvar circe-sasl-external nil
  "Use SASL external authentication.")
(make-variable-buffer-local 'circe-sasl-external)

(defvar circe-sasl-strict nil
  "Use SASL authentication in strict mode.")
(make-variable-buffer-local 'circe-sasl-strict)

(defvar circe-tls-keylist nil
  "Client certificates to use when connecting via TLS.")
(make-variable-buffer-local 'circe-tls-keylist)

(defvar circe-sasl-username nil
  "The username for SASL authentication.")
(make-variable-buffer-local 'circe-sasl-username)

(defvar circe-sasl-password nil
  "The password for SASL authentication.

If a function is set it will be called with the value of
`circe-host'.")
(make-variable-buffer-local 'circe-sasl-password)

(defvar circe-use-tls nil
  "If non-nil, connect to the server using TLS.")
(make-variable-buffer-local 'circe-use-tls)

(defvar circe-server-process nil
  "The process of the server connection.")
(make-variable-buffer-local 'circe-server-process)

(defvar circe-server-last-active-buffer nil
  "The last active circe buffer.")
(make-variable-buffer-local 'circe-server-last-active-buffer)

(defvar circe-display-table nil
  "A hash table mapping commands to their display functions.")

(defvar circe-server-inhibit-auto-reconnect-p nil
  "Non-nil when Circe should not reconnect.

This can be set from commands to avoid reconnecting when the
server disconnects.")
(make-variable-buffer-local 'circe-server-inhibit-auto-reconnect-p)

(defvar circe-chat-calling-server-buffer-and-target nil
  "Internal variable to pass the server buffer and target to chat modes.")

(defvar circe-chat-target nil
  "The current target for the buffer.
This is either a channel or a nick name.")
(make-variable-buffer-local 'circe-chat-target)

(defvar circe-nick-syntax-table
  (let ((table (make-syntax-table text-mode-syntax-table))
        (special (string-to-list "[]\\`_^{}|-")))
    (dolist (char special)
      (modify-syntax-entry char "w" table))
    table)
  "Syntax table to treat nicks as words.
This is not entirely accurate, as exact chars constituting a nick
can vary between networks.")

(defvar circe-nickserv-mask nil
  "The regular expression to identify the nickserv on this network.

Matched against nick!user@host.")
(make-variable-buffer-local 'circe-nickserv-mask)

(defvar circe-nickserv-identify-challenge nil
  "A regular expression matching the nickserv challenge to identify.")
(make-variable-buffer-local 'circe-nickserv-identify-challenge)

(defvar circe-nickserv-identify-command nil
  "The IRC command to send to identify with nickserv.

This must be a full IRC command. It accepts the following
formatting options:

 {nick} - The nick to identify as
 {password} - The configured nickserv password")
(make-variable-buffer-local 'circe-nickserv-identify-command)

(defvar circe-nickserv-identify-confirmation nil
  "A regular expression matching a confirmation of authentication.")
(make-variable-buffer-local 'circe-nickserv-identify-confirmation)

(defvar circe-nickserv-ghost-command nil
  "The IRC command to send to regain/ghost your nick.

This must be a full IRC command. It accepts the following
formatting options:

  {nick} - The nick to ghost
  {password} - The configured nickserv password")
(make-variable-buffer-local 'circe-nickserv-ghost-command)

(defvar circe-nickserv-ghost-confirmation nil
  "A regular expression matching a confirmation for the GHOST command.

This is used to know when we can set our nick to the regained one
Leave nil if regaining automatically sets your nick")
(make-variable-buffer-local 'circe-nickserv-ghost-confirmation)

(defvar circe-nickserv-nick nil
  "The nick we are registered with for nickserv.

Do not set this variable directly. Use `circe-network-options' or
pass an argument to the `circe' function for this.")
(make-variable-buffer-local 'circe-nickserv-nick)

(defvar circe-nickserv-password nil
  "The password we use for nickserv on this network.

Can be either a string or a unary function of the nick returning
a string.

Do not set this variable directly. Use `circe-network-options' or
pass an argument to the `circe' function for this.")
(make-variable-buffer-local 'circe-nickserv-password)

(defvar circe-channels nil
  "The default channels to join on this server.

Don't set this variable by hand, use `circe-network-options'.

The value should be a list of channels to join, with optional
keywords to configure the behavior of the following channels.

Best explained in an example:

\(\"#emacs\" :after-auth \"#channel\" \"#channel2\")

Possible keyword options are:

:immediate - Immediately after registering on the server
:after-auth - After nickserv authentication succeeded
:after-cloak - After we have acquired a cloaked host name
:after-nick - After we regained our preferred nick, or after
              nickserv authentication if we don't need to regain
              it. See `circe-nickserv-ghost-style'.

The default is set in `circe-server-auto-join-default-type'.

A keyword in the first position of the channels list overrides
`circe-server-auto-join-default-type' for re-joining manually
joined channels.")
(make-variable-buffer-local 'circe-channels)

(defvar circe-logging nil
  "Whether logging should be enabled for this network.")
(make-variable-buffer-local 'circe-logging)

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; Server Buffer Management ;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; Every Circe buffer has an associated server buffer (which might be
;; the buffer itself). Circe buffers should set the
;; `circe-server-buffer' variable to the associated server buffer.

(defun circe-server-buffer ()
  "Return the server buffer for the current buffer."
  (let ((buf (if (eq major-mode 'circe-server-mode)
                 (current-buffer)
               circe-server-buffer)))
    (cond
     ((not buf)
      (error "Not in a Circe buffer"))
     ((not (buffer-live-p buf))
      (error "The server buffer died, functionality is limited"))
     (t
      buf))))

(defmacro with-circe-server-buffer (&rest body)
  "Run BODY with the current buffer being the current server buffer."
  (declare (indent 0))
  `(with-current-buffer (circe-server-buffer)
     ,@body))

;;;;;;;;;;;;;;;;;;;;;;;
;;; Editor Commands ;;;
;;;;;;;;;;;;;;;;;;;;;;;

;;;###autoload
(defun circe-version ()
  "Display Circe's version."
  (interactive)
  (message "Circe %s" (circe--version)))

(defun circe--version ()
  "Return Circe's version"
  (let ((circe-git-version (circe--git-version)))
    (if circe-git-version
        (format "%s-%s" circe-version circe-git-version)
      (format "%s" circe-version))))

(defun circe--git-version ()
  (let ((current-file-path (or load-file-name buffer-file-name)))
    (when (or (not current-file-path)
              (not (equal (file-name-nondirectory current-file-path)
                          "circe.el")))
      (setq current-file-path (locate-library "circe.el")))
    (let ((vcs-path (locate-dominating-file current-file-path ".git")))
      (when vcs-path
        (let ((default-directory vcs-path))
          ;; chop off the trailing newline
          (substring (shell-command-to-string "git rev-parse --short HEAD")
                     0 -1))))))

;;;###autoload
(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))
    (pop-to-buffer-same-window buffer)))

(defun circe--read-network-and-options ()
  "Read a host or network name with completion.

If it's not a network, also read some extra options.

This uses `circe-network-defaults' and `circe-network-options' for
network names."
  (let ((default-network (if (null circe-network-options)
                             (caar circe-network-defaults)
                           (caar circe-network-options)))
        (networks nil)
        (completion-ignore-case t)
        network-or-host)
    (dolist (network-spec (append circe-network-options
                                  circe-network-defaults))
      (when (not (member (car network-spec) networks))
        (push (car network-spec) networks)))
    (setq networks (sort networks 'string-lessp))
    (setq network-or-host (completing-read "Network or host: "
                                           networks
                                           nil nil nil nil
                                           default-network))
    (dolist (network-name networks)
      (when (equal (downcase network-or-host)
                   (downcase network-name))
        (setq network-or-host network-name)))
    (if (member network-or-host networks)
        (list network-or-host)
      (list network-or-host
            :host network-or-host
            :port (read-number "Port: " 6667)))))

(defun circe--server-get-network-options (network server-options)
  "Combine server and network options with network defaults.

See `circe-network-options' and `circe-network-defaults'."
  (let ((options (mapcar 'circe--translate-option-names
                         (append server-options
                                 (cdr (assoc network circe-network-options))
                                 (cdr (assoc network circe-network-defaults))
                                 (list :network network)))))
    (when (not (plist-get options :host))
      (plist-put options :host network))
    (let ((port (plist-get options :port))
          (use-tls (plist-get options :use-tls)))
      (when (consp port)
        (if use-tls
            (plist-put options :port (cdr port))
          (plist-put options :port (car port)))))
    (dolist (required-option '(:host :port))
      (when (not (plist-get options required-option))
        (error (format "Network option %s not specified" required-option))))
    options))

(defun circe--translate-option-names (option)
  "Translate option names to make them unique.

Some options have multiple names, mainly for historical reasons.
Unify them here."
  (cond
   ((eq option :service) :port)
   ((eq option :tls) :use-tls)
   ((eq option :family) :ip-family)
   (t option)))

(defun circe--server-generate-buffer (options)
  "Return the server buffer for the connection described in OPTIONS."
  (let* ((network (plist-get options :network))
         (host (plist-get options :host))
         (port (plist-get options :port))
         (buffer-name (lui-format (or (plist-get options :server-buffer-name)
                                      circe-server-buffer-name)
                                  :network network
                                  :host host
                                  :port port
                                  :service port)))
    (generate-new-buffer buffer-name)))

(defun circe--server-set-variables (options)
  "Set buffer-local variables described in OPTIONS.

OPTIONS is a plist as passed to `circe'. All options therein are
set as buffer-local variables. Only the first occurrence of each
variable is set."
  (setq circe-nick circe-default-nick
        circe-user circe-default-user
        circe-realname circe-default-realname
        circe-ip-family circe-default-ip-family)
  (let ((done nil)
        (todo options))
    (while todo
      (when (not (memq (car todo) done))
        (push (car todo) done)
        (let ((var (intern (format "circe-%s"
                                   (substring (symbol-name (car todo)) 1))))
              (val (cadr todo)))
          (cond
           ((boundp var)
            (set (make-local-variable var) val))
           ((memq var '(circe-lagmon-disabled circe-pingmon-disabled))
            ;; the intent is to disable the module when it's not
            ;; loaded up, so we can just safely ignore it
            )
           (t
            (warn "Unknown option %s, ignored" (car todo))))))
      (setq todo (cddr todo)))))

(defvar circe-server-reconnect-attempts 0
  "The number of reconnect attempts that Circe has done so far.
See `circe-server-max-reconnect-attempts'.")
(make-variable-buffer-local 'circe-server-reconnect-attempts)

(defun circe-reconnect ()
  "Reconnect the current server."
  (interactive)
  (with-circe-server-buffer
    (when (or (called-interactively-p 'any)
              (circe--reconnect-p))
      (setq circe-server-inhibit-auto-reconnect-p t
            circe-server-reconnect-attempts (+ circe-server-reconnect-attempts
                                               1))
      (unwind-protect
          (circe-reconnect--internal)
        (setq circe-server-inhibit-auto-reconnect-p nil)))))

(defun circe--reconnect-p ()
  (cond
   (circe-server-inhibit-auto-reconnect-p
    nil)
   ((not circe-server-max-reconnect-attempts)
    t)
   ((<= circe-server-reconnect-attempts
        circe-server-max-reconnect-attempts)
    t)
   (t
    nil)))

(defun circe--validate-password (key value)
  "Validate provided password and fail loudly for invalid type.
KEY and VALUE correspond to the respective keyword and password
value."
  (when value
    (cond
     ((functionp value)
      (let ((pass (funcall value circe-host)))
        (when (and pass (not (stringp pass)))
          (user-error "Password function for %s (%s) did not \
evaluate to a string: %S" key circe-host pass))
        pass))
     ((stringp value)
      value)
     (t
      (user-error "Invalid password type for %s (%s): %S"
                  key circe-host value)))))

(defun circe-reconnect--internal ()
  "The internal function called for reconnecting unconditionally.

Do not use this directly, use `circe-reconnect'"
  (when (and circe-server-process
             (process-live-p circe-server-process))
    (delete-process circe-server-process))
  (circe-display-server-message "Connecting...")
  (dolist (buf (circe-server-chat-buffers))
    (with-current-buffer buf
      (circe-display-server-message "Connecting...")))
  (setq circe-server-process
        (irc-connect
         :host circe-host
         :service circe-port
         :tls circe-use-tls
         :tls-keylist circe-tls-keylist
         :sasl-external circe-sasl-external
         :sasl-strict circe-sasl-strict
         :ip-family circe-ip-family
         :handler-table (circe-irc-handler-table)
         :server-buffer (current-buffer)
         :nick circe-nick
         :nick-alternatives (list (circe--nick-next circe-nick)
                                  (circe--nick-next
                                   (circe--nick-next circe-nick)))
         :user circe-user
         ;; as per RFC 2812 section 3.1.3, request +i
         :mode 8
         :realname circe-realname
         :pass (circe--validate-password :pass circe-pass)
         :cap-req (append (when (or (and circe-sasl-username
                                         circe-sasl-password)
                                    circe-sasl-external)
                            '("sasl"))
                          '("chghost"
                            "extended-join"
                            "message-tags"
                            "multi-prefix"
                            "setname"
                            "server-time"
                            "standard-replies"))
         :nickserv-nick (or circe-nickserv-nick
                            circe-nick)
         :nickserv-password (circe--validate-password :nickserv-password
                                                      circe-nickserv-password)
         :nickserv-mask circe-nickserv-mask
         :nickserv-identify-challenge circe-nickserv-identify-challenge
         :nickserv-identify-command circe-nickserv-identify-command
         :nickserv-identify-confirmation
         circe-nickserv-identify-confirmation
         :nickserv-ghost-command circe-nickserv-ghost-command
         :nickserv-ghost-confirmation circe-nickserv-ghost-confirmation
         :sasl-username circe-sasl-username
         :sasl-password (circe--validate-password :sasl-password
                                                  circe-sasl-password)
         :ctcp-version (format "Circe: Client for IRC in Emacs, version %s"
                               circe-version)
         :ctcp-source circe-source-url
         :ctcp-clientinfo "CLIENTINFO PING SOURCE TIME VERSION"
         :auto-join-after-registration
         (append (circe--auto-join-channel-buffers)
                 (circe--auto-join-list :immediate))
         :auto-join-after-host-hiding
         (circe--auto-join-list :after-cloak)
         :auto-join-after-nick-acquisition
         (circe--auto-join-list :after-nick)
         :auto-join-after-nickserv-identification
         (circe--auto-join-list :after-auth)
         :auto-join-after-sasl-login
         (circe--auto-join-list :after-auth))))

(defun circe-reconnect-all ()
  "Reconnect all Circe connections."
  (interactive)
  (dolist (buf (circe-server-buffers))
    (with-current-buffer buf
      (if (called-interactively-p 'any)
          (call-interactively 'circe-reconnect)
        (circe-reconnect)))))

(defun circe--auto-join-list (type)
  "Return the list of channels to join for type TYPE."
  (let ((result nil)
        (current-type circe-server-auto-join-default-type))
    (dolist (channel circe-channels)
      (cond
       ((keywordp channel)
        (setq current-type channel))
       ((eq current-type type)
        (push channel result))))
    (nreverse result)))

(defun circe--auto-join-channel-buffers ()
  "Return a list of channels to join based on channel buffers.

This includes all channel buffers of the current server, but
excludes and channel that is already listed in
`circe-channels'."
  (let ((channels nil))
    (dolist (buf (circe-server-chat-buffers))
      (let ((name (with-current-buffer buf
                    (when (derived-mode-p 'circe-channel-mode)
                      circe-chat-target))))
        (when (and name
                   (not (member name circe-channels)))
          (push name channels))))
    channels))

;;;;;;;;;;;;;;;;;
;;; Base Mode ;;;
;;;;;;;;;;;;;;;;;

(defvar circe-mode-hook nil
  "Hook run for any Circe mode.")

(defvar circe-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "C-c C-j") 'circe-command-JOIN)
    (define-key map (kbd "C-c C-r") 'circe-reconnect)
    map)
  "The base keymap for all Circe modes (server, channel, query)")

(defvar circe-server-buffer nil
  "The buffer of the server associated with the current chat buffer.")
(make-variable-buffer-local 'circe-server-buffer)

(define-derived-mode circe-mode lui-mode "Circe"
  "Base mode for all Circe buffers.

A buffer should never be in this mode directly, but rather in
modes that derive from this.

The mode inheritance hierarchy looks like this:

lui-mode
`-circe-mode
  `-circe-server-mode
  `-circe-chat-mode
    `-circe-channel-mode
    `-circe-query-mode"
  (add-hook 'lui-pre-output-hook 'lui-irc-colors
            t t)
  (add-hook 'lui-pre-output-hook 'circe--output-highlight-nick
            t t)
  (add-hook 'completion-at-point-functions 'circe--completion-at-point
            nil t)
  (lui-set-prompt circe-prompt-string)
  (goto-char (point-max))
  (setq lui-input-function 'circe--input
        default-directory (expand-file-name circe-default-directory)
        circe-server-last-active-buffer (current-buffer)
        flyspell-generic-check-word-predicate 'circe--flyspell-check-word-predicate)
  (when circe-use-cycle-completion
    (set (make-local-variable 'completion-cycle-threshold)
         t))
  ;; Tab completion should be case-insensitive
  (set (make-local-variable 'completion-ignore-case)
       t)
  (set (make-local-variable 'tracking-faces-priorities)
       circe-track-faces-priorities))

;;;;;;;;;;;;;;;;;;;;
;;;; Displaying ;;;;
;;;;;;;;;;;;;;;;;;;;

(defun circe-display (format &rest keywords)
  "Display FORMAT formatted with KEYWORDS in the current Circe buffer.
See `lui-format' for a description of the format.

If FORMAT contains the word server, the resulting string receives
a `circe-server-face'. If FORMAT contains the word self, the
whole string receives a `circe-my-message-face'. If FORMAT is in
`circe-format-not-tracked', a message of this type is never
tracked by Lui.

Keywords with the name :nick receive a `circe-originator-face'.

It is always possible to use the mynick or target formats."
  (when (not (circe--display-ignored-p format keywords))
    (let* ((name (symbol-name format))
           (face (cond
                  ((string-match "\\<server\\>" name)
                   'circe-server-face)
                  ((string-match "\\<self\\>" name)
                   'circe-my-message-face)))
           (keywords (append `(:mynick ,(circe-nick)
                                       :chattarget ,circe-chat-target)
                             (circe--display-add-nick-property
                              (if (and (not (null keywords))
                                       (null (cdr keywords)))
                                  (car keywords)
                                keywords))))
           (text (lui-format format keywords))
           (time (and irc-message-tags (cdr (assoc 'time irc-message-tags)))))
      (when (circe--display-fool-p format keywords)
        (add-face-text-property 0 (length text)
                                'circe-fool-face t text)
        (put-text-property 0 (length text)
                           'lui-fool t
                           text))
      (when face
        (add-face-text-property 0 (length text)
                                face t text))
      (if time
          (let ((lui-time-stamp-time (encode-time (parse-time-string time))))
            (lui-insert text (memq format circe-format-not-tracked)))
        (lui-insert text (memq format circe-format-not-tracked))))))

(defun circe-display-server-message (message)
  "Display MESSAGE as a server message."
  (circe-display 'circe-format-server-message
                 :body message))

(defun circe--display-add-nick-property (keywords)
  "Add a face to the value of the :nick property in KEYWORDS."
  (let ((keyword nil))
    (mapcar (lambda (entry)
              (cond
               ((or (eq keyword :nick)
                    (eq keyword 'nick))
                (setq keyword nil)
                (propertize entry 'face 'circe-originator-face))
               (t
                (setq keyword entry)
                entry)))
            keywords)))

(defun circe--display-ignored-p (_format keywords)
  (let ((nick (plist-get keywords :nick))
        (userhost (plist-get keywords :userhost))
        (body (plist-get keywords :body)))
    (circe--ignored-p nick userhost body)))

(defun circe--display-fool-p (_format keywords)
  (let ((nick (plist-get keywords :nick))
        (userhost (plist-get keywords :userhost))
        (body (plist-get keywords :body)))
    (circe--fool-p nick userhost body)))

(defun circe--ignored-p (nick userhost body)
  "True if this user or message is being ignored.

See `circe-ignore-functions' and `circe-ignore-list'.

NICK, USER and HOST should be the sender of a the command
COMMAND, which had the arguments ARGS."
  (or (run-hook-with-args-until-success 'circe-ignore-functions
                                        nick userhost body)
      (circe--ignore-matches-p nick userhost body circe-ignore-list)))

(defun circe--fool-p (nick userhost body)
  "True if this user or message is a fool.

See `circe-fool-list'.

NICK, USER and HOST should be the sender of a the command
COMMAND, which had the arguments ARGS."
  (circe--ignore-matches-p nick userhost body circe-fool-list))

(defun circe--ignore-matches-p (nick userhost body patterns)
  "Check if a given command does match an ignore pattern.

A pattern matches if it either matches the user NICK!USER@HOST,
or if it matches the first word in BODY.

PATTERNS should be the list of regular expressions."
  (let ((string (format "%s!%s" nick userhost))
        (target (when (and body
                           (string-match "^\\([^ ]*\\)[:,]" body))
                  (match-string 1 body))))
    (catch 'return
      (dolist (regex patterns)
        (when (string-match regex string)
          (throw 'return t))
        (when (and (stringp target)
                   (string-match regex target))
          (throw 'return t)))
      nil)))

;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; Nick Highlighting ;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defun circe--output-highlight-nick ()
  "Highlight the nick of the user in the buffer.

This is used in `lui-pre-output-hook'."
  (goto-char (or (text-property-any (point-min) (point-max)
                                    'lui-format-argument 'body)
                 (point-min)))
  (when (or (not circe-inhibit-nick-highlight-function)
            (not (funcall circe-inhibit-nick-highlight-function)))
    (let* ((nick (circe-nick))
           (nicks (append (and nick (list nick))
                          circe-extra-nicks)))
      (when nicks
        ;; Can't use \<...\> because that won't match for \<forcer-\> We
        ;; might eventually use \_< ... \_> if we define symbols to be
        ;; nicks \\= is necessary, because it might be found right where we
        ;; are, and that might not be the beginning of a line... (We start
        ;; searching from the beginning of the body)
        (let ((nick-regex (concat "\\(?:^\\|\\W\\|\\=\\)"
                                  "\\(" (regexp-opt nicks) "\\)"
                                  "\\(?:$\\|\\W\\)")))
          (cond
           ((eq circe-highlight-nick-type 'sender)
            (if (text-property-any (point-min)
                                   (point-max)
                                   'face 'circe-originator-face)
                (when (re-search-forward nick-regex nil t)
                  (circe--extend-text-having-face
                   (point-min) (point-max)
                   'circe-originator-face
                   'circe-highlight-nick-face))
              (let ((circe-highlight-nick-type 'occurrence))
                (circe--output-highlight-nick))))
           ((eq circe-highlight-nick-type 'occurrence)
            (while (re-search-forward nick-regex nil t)
              (add-face-text-property (match-beginning 1)
                                      (match-end 1)
                                      'circe-highlight-nick-face)))
           ((eq circe-highlight-nick-type 'message)
            (when (re-search-forward nick-regex nil t)
              (let* ((start (text-property-any (point-min)
                                               (point-max)
                                               'lui-format-argument 'body))
                     (end (when start
                            (next-single-property-change start
                                                         'lui-format-argument))))
                (when (and start end)
                  (add-face-text-property start end
                                          'circe-highlight-nick-face)))))
           ((eq circe-highlight-nick-type 'all)
            (when (re-search-forward nick-regex nil t)
              (add-face-text-property (point-min) (point-max)
                                      'circe-highlight-nick-face)))))))))

(defun circe--extend-text-having-face (from to existing new)
  "Extend property values.

In the text between FROM and TO, find any text that has its face
property set to EXISTING, and prepend NEW to the value of its
face property, when necessary by turning it into a list."
  (let ((beg (text-property-any from to 'face existing)))
    (while beg
      (let ((end (next-single-property-change beg 'face)))
        (add-face-text-property beg end new)
        (setq beg (text-property-any end to 'face existing))))))

;;;;;;;;;;;;;;;
;;;; Input ;;;;
;;;;;;;;;;;;;;;

(defun circe--input (str)
  "Process STR as input.

This detects commands and interprets them, or sends the input
using the /SAY command."
  (set-text-properties 0 (length str) nil str)
  (cond
   ((string= str "")
    nil)
   ;; Ignore commands in multiline input
   ((and (not (string-match "\n" str))
         (string-match "\\`/\\([^/ ][^ ]*\\|[^/ ]*\\) ?\\([^\n]*\\)\\'" str))
    (let* ((command (match-string 1 str))
           (args (match-string 2 str))
           (handler (intern-soft (format "circe-command-%s"
                                         (upcase command)))))
      (cond
       ((string= command "")
        (circe-command-SAY args))
       (handler
        (funcall handler args))
       (circe-server-send-unknown-command-p
        (irc-send-raw (circe-server-process)
                      (format "%s %s"
                              (upcase command)
                              args)))
       (t
        (circe-display-server-message (format "Unknown command: %s"
                                      command))))))
   (t
    (mapc #'circe-command-SAY
          (circe--list-drop-right (split-string str "\n")
                                  "^ *$")))))

;;;;;;;;;;;;;;;;;;
;;;; Flyspell ;;;;
;;;;;;;;;;;;;;;;;;

(defun circe--flyspell-check-word-predicate ()
  "Return a true value if flyspell check the word before point.

This is a suitable value for `flyspell-generic-check-word-predicate'. It
will also call `flyspell-generic-check-word-predicate'."
  (cond
   ((not (lui-flyspell-check-word-p))
    nil)
   ((circe-channel-user-p (circe--flyspell-nick-before-point))
    nil)
   (t
    t)))

(defun circe--flyspell-nick-before-point ()
  "Return the IRC nick before point"
  (with-syntax-table circe-nick-syntax-table
    (let (beg end)
      (save-excursion
        (forward-word -1)
        (setq beg (point))
        (forward-word 1)
        (setq end (point)))
      (buffer-substring beg end))))

;;;;;;;;;;;;;;;;;;;;
;;;; Completion ;;;;
;;;;;;;;;;;;;;;;;;;;

(defun circe--completion-at-point ()
  "Return a list of possible completions for the current buffer.

This is used in `completion-at-point-functions'."
  ;; Use markers so they move when input happens
  (let ((start (make-marker))
        (end (make-marker)))
    (set-marker end (point))
    (set-marker start
                (save-excursion
                  (when (or (looking-back (regexp-quote
                                           circe-completion-suffix)
                                          (length circe-completion-suffix))
                            (looking-back " " 1))
                    (goto-char (match-beginning 0)))
                  (cond
                   ((<= (point) lui-input-marker)
                    lui-input-marker)
                   ((re-search-backward "\\s-" lui-input-marker t)
                    (1+ (point)))
                   (t
                    lui-input-marker))))
    (list start end 'circe--completion-table)))

(defun circe--completion-table (string pred action)
  "Completion table to use for Circe buffers.

See `minibuffer-completion-table' for details."
  (cond
   ;; Best completion of STRING
   ((eq action nil)
    (try-completion string
                    (circe--completion-candidates
                     (if (= (- (point) (length string))
                            lui-input-marker)
                         circe-completion-suffix
                       " "))
                    pred))
   ;; A list of possible completions of STRING
   ((eq action t)
    (all-completions string
                     (circe--completion-candidates
                      (if (= (- (point) (length string))
                             lui-input-marker)
                          circe-completion-suffix
                        " "))
                     pred))
   ;; t iff STRING is a valid completion as it stands
   ((eq action 'lambda)
    (test-completion string
                     (circe--completion-candidates
                      (if (= (- (point) (length string))
                             lui-input-marker)
                          circe-completion-suffix
                        " "))
                     pred))
   ;; Boundaries
   ((eq (car-safe action) 'boundaries)
    `(boundaries 0 . ,(length (cdr action))))
   ;; Metadata
   ((eq action 'metadata)
    '(metadata (cycle-sort-function . circe--completion-sort)))))

(defun circe--completion-clean-nick (string)
  (with-temp-buffer
    (insert string)
    (goto-char (point-max))
    (when (or (looking-back circe-completion-suffix nil)
              (looking-back " " nil))
      (replace-match ""))
    (buffer-string)))

(defun circe--completion-sort (collection)
  "Sort the COLLECTION by channel activity for nicks."
  (let* ((proc (circe-server-process))
         (channel (when (and circe-chat-target proc)
                    (irc-connection-channel proc circe-chat-target)))
         (decorated (mapcar (lambda (entry)
                              (let* ((nick (circe--completion-clean-nick
                                            entry))
                                     (user (when channel
                                             (irc-channel-user channel nick))))
                                (list (when user
                                        (irc-user-last-activity-time user))
                                      (length entry)
                                      entry)))
                            collection))
         (sorted (sort decorated
                       (lambda (a b)
                         (cond
                          ((and (car a)
                                (car b))
                           (> (car a)
                              (car b)))
                          ((and (not (car a))
                                (not (car b)))
                           (< (cadr a)
                              (cadr b)))
                          ((car a)
                           t)
                          (t
                           nil))))))
    (mapcar (lambda (entry)
              (nth 2 entry))
            sorted)))

;; FIXME: I do not know why this is here.
(defvar circe--completion-old-completion-all-sorted-completions nil
  "Variable to know if we can return a cached result.")
(make-variable-buffer-local
 'circe--completion-old-completion-all-sorted-completions)
(defvar circe--completion-cache nil
  "The results we can cache.")
(make-variable-buffer-local 'circe--completion-cache)

(defun circe--completion-candidates (nick-suffix)
  (if (and circe--completion-old-completion-all-sorted-completions
           (eq completion-all-sorted-completions
               circe--completion-old-completion-all-sorted-completions))
      circe--completion-cache
    (let ((completions (append (circe--commands-list)
                               (mapcar (lambda (buf)
                                         (with-current-buffer buf
                                           circe-chat-target))
                                       (circe-server-channel-buffers)))))
      (cond
       ;; In a server buffer, complete all nicks in all channels
       ((eq major-mode 'circe-server-mode)
        (dolist (buf (circe-server-channel-buffers))
          (with-current-buffer buf
            (dolist (nick (circe-channel-nicks))
              (setq completions (cons (concat nick nick-suffix)
                                      completions))))))
       ;; In a channel buffer, only complete nicks in this channel
       ((eq major-mode 'circe-channel-mode)
        (setq completions (append (delete (concat (circe-nick)
                                                  nick-suffix)
                                          (mapcar (lambda (nick)
                                                    (concat nick nick-suffix))
                                                  (circe-channel-nicks)))
                                  completions)))
       ;; In a query buffer, only complete this query partner
       ((eq major-mode 'circe-query-mode)
        (setq completions (cons (concat circe-chat-target nick-suffix)
                                completions)))
       ;; Else, we're doing something wrong
       (t
        (error "`circe-possible-completions' called outside of Circe")))
      (setq circe--completion-old-completion-all-sorted-completions
            completion-all-sorted-completions
            circe--completion-cache completions)
      completions)))

(defun circe--commands-list ()
  "Return a list of possible Circe commands."
  (mapcar (lambda (symbol)
            (let ((str (symbol-name symbol)))
              (if (string-match "^circe-command-\\(.*\\)" str)
                  (concat "/" (match-string 1 str) " ")
                str)))
          (apropos-internal "^circe-command-")))

;;;;;;;;;;;;;;;;;;;
;;; Server Mode ;;;
;;;;;;;;;;;;;;;;;;;

(defvar circe-server-mode-hook nil
  "Hook run when a new Circe server buffer is created.")

(defvar circe-server-mode-map (make-sparse-keymap)
  "The key map for server mode buffers.")

(define-derived-mode circe-server-mode circe-mode "Circe Server"
  "The mode for circe server buffers.

This buffer represents a server connection. When you kill it, the
server connection is closed. This will make all associated
buffers unusable. Be sure to use \\[circe-reconnect] if you want
to reconnect to the server.

\\{circe-server-mode-map}"
  (add-hook 'kill-buffer-hook 'circe-server-killed nil t))

(defun circe-server-killed ()
  "Run when the server buffer got killed.

This will quit IRC, and ask the user whether to kill all of the
server's chat buffers."
  (when circe-server-killed-confirmation
    (let ((network-name (with-circe-server-buffer circe-network)))
      (when (not (y-or-n-p
                  (if (eq circe-server-killed-confirmation 'ask-and-kill-all)
                      (format "Really kill all buffers of network %s? (if not, try `circe-reconnect') "
                              network-name)
                    (format "Really kill the IRC connection to network %s? (if not, try `circe-reconnect') "
                            network-name))))
        (error "Buffer of network %s not killed as per user request"
               network-name))))
  (setq circe-server-inhibit-auto-reconnect-p t)
  (ignore-errors
    (irc-send-QUIT circe-server-process circe-default-quit-message))
  (ignore-errors
    (delete-process circe-server-process))
  (when (or (eq circe-server-killed-confirmation 'ask-and-kill-all)
            (eq circe-server-killed-confirmation 'kill-all))
    (dolist (buf (circe-server-chat-buffers))
      (let ((circe-channel-killed-confirmation nil))
        (kill-buffer buf)))))

(defun circe-server-buffers ()
  "Return a list of all server buffers in this Emacs instance."
  (let ((result nil))
    (dolist (buf (buffer-list))
      (with-current-buffer buf
        (when (eq major-mode 'circe-server-mode)
          (setq result (cons buf result)))))
    (nreverse result)))

(defun circe-server-process ()
  "Return the current server process."
  (with-circe-server-buffer
    circe-server-process))

(defun circe-server-my-nick-p (nick)
  "Return non-nil when NICK is our current nick."
  (let ((proc (circe-server-process)))
    (when proc
      (irc-current-nick-p proc nick))))

(defun circe-nick ()
  "Return our current nick."
  (let ((proc (circe-server-process)))
    (when proc
      (irc-current-nick proc))))

(defun circe-server-last-active-buffer ()
  "Return the last active buffer of this server."
  (with-circe-server-buffer
    (if (and circe-server-last-active-buffer
             (bufferp circe-server-last-active-buffer)
             (buffer-live-p circe-server-last-active-buffer))
        circe-server-last-active-buffer
      (current-buffer))))

;; There really ought to be a hook for this
(define-advice select-window (:after (window &optional _norecord)
                                     circe-server-track-select-window)
  "Remember the current buffer as the last active buffer.
This is used by Circe to know where to put spurious messages."
  (with-current-buffer (window-buffer window)
    (when (derived-mode-p 'circe-mode)
      (let ((buf (current-buffer)))
        (ignore-errors
          (with-circe-server-buffer
            (setq circe-server-last-active-buffer buf)))))))

(defun circe-reduce-lurker-spam ()
  "Return the value of `circe-reduce-lurker-spam'.

This uses a buffer-local value first, then the one in the server
buffer.

Use this instead of accessing the variable directly to enable
setting the variable through network options."
  (if (local-variable-p 'circe-reduce-lurker-spam)
      circe-reduce-lurker-spam
    (with-circe-server-buffer
      circe-reduce-lurker-spam)))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; Chat Buffer Management ;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; Server buffers keep track of associated chat buffers. This enables
;; us to not rely on buffer names staying the same, as well as keeping
;; buffers from different servers and even server connections apart
;; cleanly.

(defvar circe-server-chat-buffer-table nil
  "A hash table of chat buffers associated with this server.")
(make-variable-buffer-local 'circe-server-chat-buffer-table)

(defun circe-server-get-chat-buffer (target)
  "Return the chat buffer addressing TARGET, or nil if none."
  (with-circe-server-buffer
    (when circe-server-chat-buffer-table
      (let* ((target-name (irc-isupport--case-fold (circe-server-process)
                                                   target))
             (buf (gethash target-name circe-server-chat-buffer-table)))
        (if (buffer-live-p buf)
            buf
          (remhash target-name circe-server-chat-buffer-table)
          nil)))))

(defun circe-server-create-chat-buffer (target chat-mode)
  "Return a new buffer addressing TARGET in CHAT-MODE."
  (with-circe-server-buffer
    (let* ((target-name (irc-isupport--case-fold (circe-server-process)
                                                 target))
           (buffer-name (lui-format 'circe-chat-buffer-name
                                    :target target
                                    :network circe-network))
           (chat-buffer (generate-new-buffer buffer-name))
           (server-buffer (current-buffer))
           (circe-chat-calling-server-buffer-and-target (cons server-buffer
                                                              target-name)))
      (when (not circe-server-chat-buffer-table)
        (setq circe-server-chat-buffer-table (make-hash-table :test 'equal)))
      (puthash target-name chat-buffer circe-server-chat-buffer-table)
      (with-current-buffer chat-buffer
        (funcall chat-mode))
      chat-buffer)))

(defun circe-server-get-or-create-chat-buffer (target chat-mode)
  "Return a buffer addressing TARGET; create one in CHAT-MODE if none exists."
  (let ((buf (circe-server-get-chat-buffer target)))
    (if buf
        buf
      (circe-server-create-chat-buffer target chat-mode))))

(defun circe-server-remove-chat-buffer (target-or-buffer)
  "Remove the buffer addressing TARGET-OR-BUFFER."
  (with-circe-server-buffer
    (let* ((target (if (bufferp target-or-buffer)
                       (circe-server-chat-buffer-target target-or-buffer)
                     target-or-buffer))
           (target-name  (irc-isupport--case-fold (circe-server-process)
                                                  target)))
      (remhash target-name circe-server-chat-buffer-table))))

(defun circe-server-rename-chat-buffer (old-name new-name)
  "Note that the chat buffer addressing OLD-NAME now addresses NEW-NAME."
  (with-circe-server-buffer
    (let* ((old-target-name (irc-isupport--case-fold (circe-server-process)
                                                     old-name))
           (new-target-name (irc-isupport--case-fold (circe-server-process)
                                                     new-name))
           (buf (gethash old-target-name circe-server-chat-buffer-table)))
      (when buf
        (remhash old-target-name circe-server-chat-buffer-table)
        (puthash new-target-name buf circe-server-chat-buffer-table)
        (with-current-buffer buf
          (setq circe-chat-target new-name)
          (rename-buffer new-name t))))))

(defun circe-server-chat-buffer-target (&optional buffer)
  "Return the chat target of BUFFER, or the current buffer if none."
  (if buffer
      (with-current-buffer buffer
        circe-chat-target)
    circe-chat-target))

(defun circe-server-chat-buffers ()
  "Return the list of chat buffers on this server."
  (with-circe-server-buffer
    (when circe-server-chat-buffer-table
      (let ((buffer-list nil))
        (maphash (lambda (target-name buffer)
                   (if (buffer-live-p buffer)
                       (push buffer buffer-list)
                     (remhash target-name circe-server-chat-buffer-table)))
                 circe-server-chat-buffer-table)
        buffer-list))))

(defun circe-server-channel-buffers ()
  "Return a list of all channel buffers of this server."
  (let ((result nil))
    (dolist (buf (circe-server-chat-buffers))
      (with-current-buffer buf
        (when (eq major-mode 'circe-channel-mode)
          (setq result (cons buf result)))))
    (nreverse result)))

;;;;;;;;;;;;;;;;;
;;; Chat Mode ;;;
;;;;;;;;;;;;;;;;;

(defvar circe-chat-mode-hook nil
  "Hook run when a new chat buffer (channel or query) is created.")

(defvar circe-chat-mode-map (make-sparse-keymap)
  "Base key map for all Circe chat buffers (channel, query).")

(define-derived-mode circe-chat-mode circe-mode "Circe Chat"
  "The circe chat major mode.

This is the common mode used for both queries and channels.
It should not be used directly.
TARGET is the default target to send data to.
SERVER-BUFFER is the server buffer of this chat buffer."
  (setq circe-server-buffer (car circe-chat-calling-server-buffer-and-target)
        circe-chat-target (cdr circe-chat-calling-server-buffer-and-target))
  (let ((network (with-circe-server-buffer circe-network))
        (logging (with-circe-server-buffer circe-logging)))
    (setq lui-logging-format-arguments
          `(:target ,circe-chat-target :network ,network))
    (when logging
      (enable-lui-logging)))
  (when (equal circe-chat-target "#emacs-circe")
    (set (make-local-variable 'lui-button-issue-tracker)
         "https://github.com/emacs-circe/circe/issues/%s")))

(defun circe-chat-disconnected ()
  "The current buffer got disconnected."
  (circe-display-server-message "Disconnected"))

;;;;;;;;;;;;;;;;;;;;
;;; Channel Mode ;;;
;;;;;;;;;;;;;;;;;;;;

(defvar circe-channel-mode-hook nil
  "Hook run in a new channel buffer.")

(defvar circe-channel-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "C-c C-n") 'circe-command-NAMES)
    (define-key map (kbd "C-c C-t") 'circe-command-CHTOPIC)
    map)
  "The key map for channel mode buffers.")

(define-derived-mode circe-channel-mode circe-chat-mode "Circe Channel"
  "The circe channel chat major mode.
This mode represents a channel you are talking in.

TARGET is the default target to send data to.
SERVER-BUFFER is the server buffer of this chat buffer.

\\{circe-channel-mode-map}"
  (add-hook 'kill-buffer-hook 'circe-channel-killed nil t))

(defun circe-channel-killed ()
  "Called when the channel buffer got killed.

If we are not on the channel being killed, do nothing. Otherwise,
if the server is live, and the user wants to kill the buffer,
send PART to the server and clean up the channel's remaining
state."
  (when (buffer-live-p circe-server-buffer)
    (let ((network-name (with-circe-server-buffer circe-network)))
      (when (and circe-channel-killed-confirmation
                 (not (y-or-n-p (format "Really leave channel %s on network %s? "
                                        circe-chat-target
                                        network-name))))
        (error (format "Channel %s on network %s not left."
                       circe-chat-target
                       network-name))))
    (ignore-errors
      (irc-send-PART (circe-server-process)
                     circe-chat-target
                     circe-default-part-message))
    (ignore-errors
      (circe-server-remove-chat-buffer circe-chat-target))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; Channel User Tracking ;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; Channel mode buffers provide some utility functions to check if a
;; given user is idle or not.

(defun circe-channel-user-nick-regain-p (_old new)
  "Return true if a nick change from OLD to NEW constitutes a nick regain.

A nick was regained if the NEW nick was also a recent user."
  (let ((channel (irc-connection-channel (circe-server-process)
                                         circe-chat-target)))
    (when channel
      (irc-channel-recent-user channel new))))

(defun circe-channel-user-p (nick)
  "Return non-nil when NICK belongs to a channel user."
  (cond
   ((eq major-mode 'circe-query-mode)
    (irc-string-equal-p (circe-server-process)
                        nick
                        circe-chat-target))
   ((eq major-mode 'circe-channel-mode)
    (let ((channel (irc-connection-channel (circe-server-process)
                                           circe-chat-target)))
      (when channel
        (if (irc-channel-user channel nick)
            t
          nil))))))

(defun circe-channel-nicks ()
  "Return a list of nicks in the current channel."
  (let* ((channel (irc-connection-channel (circe-server-process)
                                          circe-chat-target))
         (nicks nil))
    (when channel
      (maphash (lambda (_folded-nick user)
                 (push (irc-user-nick user) nicks))
               (irc-channel-users channel)))
    nicks))

(defun circe-user-channels (nick)
  "Return a list of channel buffers for the user named NICK."
  (let* ((result nil))
    (dolist (channel (irc-connection-channel-list (circe-server-process)))
      (when (irc-channel-user channel nick)
        (let* ((name (irc-channel-name channel))
               (buf (circe-server-get-chat-buffer name)))
          (when buf
            (push buf result)))))
    result))

(defun circe-lurker-p (nick)
  "Return a true value if this nick is regarded inactive."
  (let* ((channel (irc-connection-channel (circe-server-process)
                                          circe-chat-target))
         (user (when channel
                 (irc-channel-user channel nick)))
         (recent-user (when channel
                        (irc-channel-recent-user channel nick)))
         (last-active (cond
                       (user
                        (irc-user-last-activity-time user))
                       (recent-user
                        (irc-user-last-activity-time recent-user)))))
    (cond
     ;; If we do not track lurkers, no one is ever a lurker.
     ((not (circe-reduce-lurker-spam))
      nil)
     ;; We ourselves are never lurkers (in this sense).
     ((circe-server-my-nick-p nick)
      nil)
     ;; Someone who isn't even on the channel (e.g. NickServ) isn't a
     ;; lurker, either.
     ((and (not user)
           (not recent-user))
      nil)
     ;; If someone has never been active, they most definitely *are* a
     ;; lurker.
     ((not last-active)
      t)
     ;; But if someone has been active, and we mark active users
     ;; inactive again after a timeout ...
     (circe-active-users-timeout
      ;; They are still lurkers if their activity has been too long
      ;; ago.
      (> (- (float-time)
            last-active)
         circe-active-users-timeout))
     ;; Otherwise, they have been active and we don't mark active
     ;; users inactive again, so nope, not a lurker.
     (t
      nil))))

(defun circe-lurker-rejoin-p (nick channel)
  "Return true if NICK is rejoining CHANNEL.

A user is considered to be rejoining if they were on the channel
shortly before, and were active then."
  (let* ((channel (irc-connection-channel (circe-server-process)
                                          channel))
         (user (when channel
                 (irc-channel-recent-user channel nick))))
    (when user
      (irc-user-last-activity-time user))))

(defun circe-lurker-display-active (nick userhost)
  "Show that this user is active if they are a lurker."
  (let* ((channel (irc-connection-channel (circe-server-process)
                                          circe-chat-target))
         (user (when channel
                 (irc-channel-user channel nick)))
         (join-time (when user
                      (irc-user-join-time user))))
    (when (and (circe-lurker-p nick)
               ;; If we saw them when we joined the channel, no need to
               ;; say "they're suddenly active!!111one".
               join-time)
      (circe-display 'circe-format-server-lurker-activity
                     :nick nick
                     :userhost (or userhost "server")
                     :jointime join-time
                     :joindelta (circe-duration-string
                                 (- (float-time)
                                    join-time))))))

;;;;;;;;;;;;;;;;;;
;;; Query Mode ;;;
;;;;;;;;;;;;;;;;;;

(defvar circe-query-mode-hook nil
  "Hook run when query mode is activated.")

(defvar circe-query-mode-map
  (let ((map (make-sparse-keymap)))
    (set-keymap-parent map circe-chat-mode-map)
    map)
  "The key map for query mode buffers.")

(define-derived-mode circe-query-mode circe-chat-mode "Circe Query"
  "The circe query chat major mode.
This mode represents a query you are talking in.

TARGET is the default target to send data to.
SERVER-BUFFER is the server buffer of this chat buffer.

\\{circe-query-mode-map}"
  (add-hook 'kill-buffer-hook 'circe-query-killed nil t))

(defun circe-query-killed ()
  "Called when the query buffer got killed."
  (ignore-errors
    (circe-server-remove-chat-buffer circe-chat-target)))

(defun circe-query-auto-query-buffer (who)
  "Return a buffer for a query with `WHO'.

This adheres to `circe-auto-query-max'."
  (or (circe-server-get-chat-buffer who)
      (when (< (circe--query-count)
               circe-auto-query-max)
        (circe-server-get-or-create-chat-buffer who 'circe-query-mode))))

(defun circe--query-count ()
  "Return the number of queries on the current server."
  (let ((num 0))
    (dolist (buf (circe-server-chat-buffers))
      (with-current-buffer buf
        (when (eq major-mode 'circe-query-mode)
          (setq num (+ num 1)))))
    num))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; IRC Protocol Handling ;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defvar circe--irc-handler-table nil
  "The handler table for Circe's IRC connections.

Do not use this directly. Instead, call `circe-irc-handler-table'.")

(defun circe-irc-handler-table ()
  (when (not circe--irc-handler-table)
    (let ((table (irc-handler-table)))
      (irc-handler-add table "irc.registered" #'circe--irc-conn-registered)
      (irc-handler-add table "conn.disconnected" #'circe--irc-conn-disconnected)
      (irc-handler-add table nil #'circe--irc-display-event)
      (irc-handle-registration table)
      (irc-handle-ping-pong table)
      (irc-handle-isupport table)
      (irc-handle-initial-nick-acquisition table)
      (irc-handle-ctcp table)
      (irc-handle-state-tracking table)
      (irc-handle-nickserv table)
      (irc-handle-auto-join table)
      (setq circe--irc-handler-table table)))
  circe--irc-handler-table)

(defun circe--irc-conn-registered (conn _event _nick)
  (with-current-buffer (irc-connection-get conn :server-buffer)
    (setq circe-server-reconnect-attempts 0)
    (run-hooks 'circe-server-connected-hook)))

(defun circe--irc-conn-disconnected (conn _event)
  (with-current-buffer (irc-connection-get conn :server-buffer)
    (dolist (buf (circe-server-chat-buffers))
      (with-current-buffer buf
        (circe-chat-disconnected)))

    (circe-reconnect)))

(defun circe--irc-display-event (conn event &optional sender &rest args)
  "Display an IRC message.

NICK, USER and HOST specify the originator of COMMAND with ARGS
as arguments."
  (with-current-buffer (irc-connection-get conn :server-buffer)
    (let* ((display (circe-get-display-handler event))
           (nick (when sender
                   (irc-userstring-nick sender)))
           (userhost (when sender
                       (irc-userstring-userhost sender))))
      (cond
       ;; Functions get called
       ((functionp display)
        (apply display nick userhost event args))
       ;; Lists describe patterns
       ((consp display)
        (circe--irc-display-format (elt display 1)
                                     (elt display 0)
                                     nick userhost event args))
       ;; No configured display handler, show a default
       (t
        (circe--irc-display-default nick userhost event args))))))

(defvar circe--irc-format-server-numeric "*** %s"
  "The format to use for server messages. Do not set this.")

(defun circe--irc-display-format (format target nick userhost event args)
  (let* ((target+name (circe--irc-display-target target nick args))
         (target (car target+name))
         (name (cdr target+name))
         (origin (if userhost
                     (format "%s (%s)" nick userhost)
                   (format "%s" nick))))
    (with-current-buffer (or target
                             (circe-server-last-active-buffer))
      (let ((circe--irc-format-server-numeric
             (if target
                 (format "*** %s" format)
               (format "*** [%s] %s" name format))))
        (circe-display 'circe--irc-format-server-numeric
                       :nick (or nick "(unknown)")
                       :userhost (or userhost "server")
                       :origin origin
                       :event event
                       :command event
                       :target name
                       :indexed-args args)))))

(defun circe--irc-display-target (target nick args)
  "Return the target buffer and name.
The buffer might be nil if it is not alive.

See `circe-set-display-handler' for a description of target.

NICK and USERHOST are the originator of COMMAND which had ARGS as
arguments."
  (cond
   ((eq target 'nick)
    (cons (circe-server-get-chat-buffer nick)
          nick))
   ((numberp target)
    (let ((name (nth target
                     args)))
      (cons (circe-server-get-chat-buffer name)
            name)))
   ((eq target 'active)
    (let ((buf (circe-server-last-active-buffer)))
      (cons buf
            (buffer-name buf))))
   ((eq target 'server)
    (cons (current-buffer) (buffer-name)))
   (t
    (error "Bad target in format string: %s" target))))

(defun circe--irc-display-default (nick userhost event args)
  (with-current-buffer (circe-server-last-active-buffer)
    (let ((target (if (circe-server-my-nick-p (car args))
                      ""
                    (format " to %s" (car args)))))
      (cond
       ((string-match "\\`irc.ctcpreply.\\(.*\\)\\'" event)
        (circe-display-server-message
         (format "CTCP %s reply from %s (%s)%s: %s"
                 (match-string 1 event) nick userhost target (cadr args))))
       ((string-match "\\`irc.ctcp.\\(.*\\)\\'" event)
        (circe-display-server-message
         (format "Unknown CTCP request %s from %s (%s)%s: %s"
                 (match-string 1 event) nick userhost target (cadr args))))
       (t
        (circe-display-server-message
         (format "[%s from %s%s] %s"
                 event
                 nick
                 (if userhost
                     (format " (%s)" userhost)
                   "")
                 (mapconcat #'identity args " "))))))))

(defun circe-set-display-handler (command handler)
  "Set the display handler for COMMAND to HANDLER.

A handler is either a function or a list.

A function gets called in the server buffer with at least three
arguments, but possibly more. There's at least NICK and USERHOST
of the sender, which can be nil, and COMMAND, which is the event
which triggered this. Further arguments are arguments to the
event.

Alternatively, the handler can be a list of two elements:

  target   - The target of this message
  format   - The format for this string

The target can be any of:

  \\='active  - The last active buffer of this server
  \\='nick    - The nick who sent this message
  \\='server  - The server buffer for this server
  number   - The index of the argument of the target

The format is passed to `lui-format'. Possible format string
substitutions are {mynick}, {target}, {nick}, {userhost},
{origin}, {command}, {target}, and indexed arguments for the
arguments to the IRC message."
  (when (not circe-display-table)
    (setq circe-display-table (make-hash-table :test 'equal)))
  (puthash command handler circe-display-table))

(defun circe-get-display-handler (command)
  "Return the display handler for COMMAND.

See `circe-set-display-handler' for more information."
  (when circe-display-table
    (gethash command circe-display-table)))

;;;;;;;;;;;;;;;;
;;; Commands ;;;
;;;;;;;;;;;;;;;;

(defun circe-command-AWAY (reason)
  "Set yourself away with REASON."
  (interactive "sReason: ")
  (irc-send-AWAY (circe-server-process) reason))

(defun circe-command-BACK (&optional _ignored)
  "Mark yourself not away anymore.

Arguments are IGNORED."
  (interactive)
  (irc-send-AWAY (circe-server-process)))

(defun circe-command-CHTOPIC (&optional _ignored)
  "Insert the topic of the current channel.

Arguments are IGNORED."
  (interactive)
  (if (not circe-chat-target)
      (circe-display-server-message "No target for current buffer")
    (let* ((channel (irc-connection-channel (circe-server-process)
                                            circe-chat-target))
           (topic (when channel
                    (irc-channel-topic channel))))
      (lui-replace-input (format "/TOPIC %s %s"
                                 circe-chat-target (or topic ""))))
    (goto-char (point-max))))

(defun circe-command-CLEAR (&optional _ignored)
  "Delete all buffer contents before the lui prompt."
  (let ((inhibit-read-only t))
    (delete-region (point-min) lui-output-marker)))

(defun circe-command-CTCP (who &optional command argument)
  "Send a CTCP message to WHO containing COMMAND with ARGUMENT.
If COMMAND is not given, WHO is parsed to contain all of who,
command and argument.
If ARGUMENT is nil, it is interpreted as no argument."
  (when (not command)
    (if (string-match "^\\([^ ]*\\) *\\([^ ]*\\) *\\(.*\\)" who)
        (setq command (upcase (match-string 2 who))
              argument (match-string 3 who)
              who (match-string 1 who))
      (circe-display-server-message "Usage: /CTCP <who> <what>")))
  (when (not (string= "" command))
    (irc-send-ctcp (circe-server-process)
                   who
                   command
                   (if (and argument (not (equal argument "")))
                       argument
                     nil))))

(defun circe-command-FOOL (line)
  "Add the regex on LINE to the `circe-fool-list'."
  (with-current-buffer (circe-server-last-active-buffer)
    (cond
     ((string-match "\\S-+" line)
      (let ((regex (match-string 0 line)))
        (add-to-list 'circe-fool-list regex)
        (circe-display-server-message (format "Recognizing %s as a fool"
                                      regex))))
     ((not circe-fool-list)
      (circe-display-server-message "Your do not know any fools"))
     (t
      (circe-display-server-message "Your list of fools:")
      (dolist (regex circe-fool-list)
        (circe-display-server-message (format "- %s" regex)))))))

(defun circe-command-GAWAY (reason)
  "Set yourself away on all servers with reason REASON."
  (interactive "sReason: ")
  (dolist (buf (circe-server-buffers))
    (with-current-buffer buf
      (irc-send-AWAY circe-server-process reason))))

(defun circe-command-GQUIT (reason)
  "Quit all servers with reason REASON."
  (interactive "sReason: ")
  (dolist (buf (circe-server-buffers))
    (with-current-buffer buf
      (setq circe-server-inhibit-auto-reconnect-p t)
      (irc-send-QUIT circe-server-process reason))))

(defun circe-command-HELP (&optional _ignored)
  "Display a list of recognized commands, nicely formatted."
  (circe-display-server-message
   (concat "Recognized commands are: "
           (mapconcat (lambda (s) s) (circe--commands-list) ""))))

(defun circe-command-IGNORE (line)
  "Add the regex on LINE to the `circe-ignore-list'."
  (with-current-buffer (circe-server-last-active-buffer)
    (cond
     ((string-match "\\S-+" line)
      (let ((regex (match-string 0 line)))
        (add-to-list 'circe-ignore-list regex)
        (circe-display-server-message (format "Ignore list, meet %s"
                                      regex))))
     ((not circe-ignore-list)
      (circe-display-server-message "Your ignore list is empty"))
     (t
      (circe-display-server-message "Your ignore list:")
      (dolist (regex circe-ignore-list)
        (circe-display-server-message (format "- %s" regex)))))))

(defun circe-command-INVITE (nick &optional channel)
  "Invite NICK to CHANNEL.
When CHANNEL is not given, NICK is assumed to be a string
consisting of two words, the nick and the channel."
  (interactive "sInvite who: \nsWhere: ")
  (when (not channel)
    (if (string-match "^\\([^ ]+\\) +\\([^ ]+\\)" nick)
        (setq channel (match-string 2 nick)
              nick (match-string 1 nick))
      (when (or (string= "" nick) (null nick))
        (circe-display-server-message "Usage: /INVITE <nick> <channel>"))))
  (irc-send-INVITE (circe-server-process)
                   nick
                   (if (and (null channel)
                            (not (null nick)))
                       circe-chat-target
                     channel)))

(defun circe-command-JOIN (channel)
  "Join CHANNEL. This can also contain a key."
  (interactive "sChannel: ")
  (let (channels keys)
    (when (string-match "^\\s-*\\([^ ]+\\)\\(:? \\([^ ]+\\)\\)?$" channel)
      (setq channels (match-string 1 channel)
            keys (match-string 3 channel))
      (dolist (channel (split-string channels ","))
        (pop-to-buffer
         (circe-server-get-or-create-chat-buffer channel
                                                 'circe-channel-mode)))
      (irc-send-JOIN (circe-server-process) channels keys))))

(defun circe-command-ME (line)
  "Send LINE to IRC as an action."
  (interactive "sAction: ")
  (if (not circe-chat-target)
      (circe-display-server-message "No target for current buffer")
    (circe-display 'circe-format-self-action
                   :body line
                   :nick (circe-nick))
    (irc-send-ctcp (circe-server-process)
                   circe-chat-target
                   "ACTION" line)))

(defun circe-command-MSG (who &optional what)
  "Send a message.

Send WHO a message containing WHAT.

If WHAT is not given, WHO should contain both the nick and the
message separated by a space."
  (when (not what)
    (if (string-match "^\\([^ ]*\\) \\(.*\\)" who)
        (setq what (match-string 2 who)
              who (match-string 1 who))
      (circe-display-server-message "Usage: /MSG <who> <what>")))
  (when what
    (let ((buf (circe-query-auto-query-buffer who)))
      (if buf
          (with-current-buffer buf
            (circe-command-SAY what)
            (lui-add-input what))
        (with-current-buffer (circe-server-last-active-buffer)
          (irc-send-PRIVMSG (circe-server-process)
                            who what)
          (circe-display 'circe-format-self-message
                         :target who
                         :body what))))))

(defun circe-command-NAMES (&optional channel)
  "List the names of the current channel or CHANNEL."
  (interactive)
  (let ((target (when channel
                  (string-trim channel))))
    (when (or (not target)
              (equal target ""))
      (setq target circe-chat-target))
    (if (not target)
        (circe-display-server-message "No target for current buffer")
      (irc-send-NAMES (circe-server-process)
                      target))))

(defun circe-command-NICK (newnick)
  "Change nick to NEWNICK."
  (interactive "sNew nick: ")
  (let ((newnick (string-trim newnick)))
    (irc-send-NICK (circe-server-process) newnick)))

(defun circe-command-SETNAME (newname)
  ;; NOTE: the spec claims this is fine to use unconditionally, even
  ;; if not negotiated
  (irc-send-SETNAME (circe-server-process) newname))

(defun circe-command-PART (reason)
  "Part the current channel because of REASON."
  (interactive "sReason: ")
  (if (not circe-chat-target)
      (circe-display-server-message "No target for current buffer")
    (irc-send-PART (circe-server-process)
                   circe-chat-target
                   (if (equal "" reason)
                       circe-default-part-message
                     reason))))

(defun circe-command-PING (target)
  "Send a CTCP PING request to TARGET."
  (interactive "sWho: ")
  (let ((target (string-trim target)))
    (irc-send-ctcp (circe-server-process)
                   target
                   "PING" (format "%s" (float-time)))))

(defun circe-command-QUERY (arg)
  "Open a query with WHO."
  ;; Eventually, this should probably be just the same as
  ;; circe-command-MSG
  (interactive "sQuery with: ")
  (let* (who what)
    (if (string-match "\\`\\s-*\\(\\S-+\\)\\s-\\(\\s-*\\S-.*\\)\\'" arg)
        (setq who (match-string 1 arg)
              what (match-string 2 arg))
      (setq who (string-trim arg)))
    (when (string= who "")
      (circe-display-server-message "Usage: /query <nick> [something to say]"))
    (pop-to-buffer
     (circe-server-get-or-create-chat-buffer who 'circe-query-mode))
    (when what
      (circe-command-SAY what)
      (lui-add-input what))))

(defun circe-command-QUIT (reason)
  "Quit the current server giving REASON."
  (interactive "sReason: ")
  (with-circe-server-buffer
    (setq circe-server-inhibit-auto-reconnect-p t)
    (irc-send-QUIT (circe-server-process)
                   (if (equal "" reason)
                       circe-default-quit-message
                     reason))))

(defun circe-command-QUOTE (line)
  "Send LINE verbatim to the server."
  (interactive "Line: ")
  (irc-send-raw (circe-server-process) line)
  (with-current-buffer (circe-server-last-active-buffer)
    (circe-display-server-message (format "Sent to server: %s"
                                  line))))

(defun circe-command-SAY (line)
  "Say LINE to the current target."
  (interactive "sSay: ")
  (if (not circe-chat-target)
      (circe-display-server-message "No target for current buffer")
    (dolist (line (circe--split-line line))
      (circe-display 'circe-format-self-say
                     :body line
                     :nick (circe-nick))
      (irc-send-PRIVMSG (circe-server-process)
                        circe-chat-target
                        ;; Some IRC servers give an error if there is
                        ;; no text at all.
                        (if (string= line "")
                            " "
                          line)))))

(defun circe--split-line (longline)
  "Splits LONGLINE into smaller components.

IRC silently truncates long lines. This splits a long line into
parts that each are not longer than `circe-split-line-length'."
  (if (< (length longline)
         circe-split-line-length)
      (list longline)
    (with-temp-buffer
      (insert longline)
      (let ((fill-column circe-split-line-length))
        (fill-region (point-min) (point-max)
                     nil t))
      (split-string (buffer-string) "\n"))))

(defun circe-command-SV (&optional _ignored)
  "Tell the current channel about your client and Emacs version.

Arguments are IGNORED."
  (interactive)
  (circe-command-SAY (format (concat "I'm using Circe version %s "
                                     "with %s %s (of %s)")
                             (circe--version)
                             "GNU Emacs"
                             emacs-version
                             (format-time-string "%Y-%m-%d"
                                                 emacs-build-time))))

(defun circe-command-TOPIC (channel &optional newtopic)
  "Change the topic of CHANNEL to NEWTOPIC."
  (interactive "sChannel: \nsNew topic: ")
  (when (string-match "^\\s-*$" channel)
    (setq channel nil))
  (when (and channel
             (not newtopic)
             (string-match "^\\s-*\\(\\S-+\\)\\( \\(.*\\)\\)?" channel))
    (setq newtopic (match-string 3 channel)
          channel (match-string 1 channel)))
  (cond
   ((and channel newtopic)
    (irc-send-TOPIC (circe-server-process) channel newtopic))
   (channel
    (irc-send-TOPIC (circe-server-process) channel))
   (circe-chat-target
    (irc-send-TOPIC (circe-server-process) circe-chat-target))
   (t
    (circe-display-server-message "No channel given, and no default target."))))

(defun circe-command-UNFOOL (line)
  "Remove the entry LINE from `circe-fool-list'."
  (with-current-buffer (circe-server-last-active-buffer)
    (cond
     ((string-match "\\S-+" line)
      (let ((regex (match-string 0 line)))
        (setq circe-fool-list (delete regex circe-fool-list))
        (circe-display-server-message (format "Assuming %s is not a fool anymore"
                                      regex))))
     (t
      (circe-display-server-message
       "No one is not a fool anymore? UNFOOL requires one argument")))))

(defun circe-command-UNIGNORE (line)
  "Remove the entry LINE from `circe-ignore-list'."
  (with-current-buffer (circe-server-last-active-buffer)
    (cond
     ((string-match "\\S-+" line)
      (let ((regex (match-string 0 line)))
        (setq circe-ignore-list (delete regex circe-ignore-list))
        (circe-display-server-message (format "Ignore list forgot about %s"
                                      regex))))
     (t
      (circe-display-server-message
       "Who do you want to unignore? UNIGNORE requires one argument")))))

(defun circe-command-WHOAMI (&optional _ignored)
  "Request WHOIS information about yourself.

Arguments are IGNORED."
  (interactive)
  (irc-send-WHOIS (circe-server-process)
                  (circe-nick)))

(defun circe-command-WHOIS (whom)
  "Request WHOIS information about WHOM."
  (interactive "sWhois: ")
  (let* ((whom-server-name (split-string whom))
         (whom (car whom-server-name))
         (server-or-name (cadr whom-server-name)))
    (irc-send-WHOIS (circe-server-process) whom server-or-name)))

(defun circe-command-WHOWAS (whom)
  "Request WHOWAS information about WHOM."
  (interactive "sWhois: ")
  (let ((whom (string-trim whom)))
    (irc-send-WHOWAS (circe-server-process) whom)))

(defun circe-command-STATS (query)
  "Request statistics from a server."
  (interactive)
  ;; Split string into query and server if we can
  (let ((query (split-string query)))
    (irc-send-STATS (circe-server-process) (car query) (cadr query))))

(defun circe-command-WL (&optional split)
  "Show the people who left in a netsplit.
Without any arguments, shows shows the current netsplits and how
many people are missing. With an argument SPLIT, which must be a
number, it shows the missing people due to that split."
  (let ((circe-netsplit-list (with-circe-server-buffer
                               circe-netsplit-list)))
    (if (or (not split)
            (and (stringp split)
                 (string= split "")))
        (if (null circe-netsplit-list)
            (circe-display-server-message "No net split at the moment")
          (let ((n 0))
            (dolist (entry circe-netsplit-list)
              (circe-display-server-message (format "(%d) Missing %d people due to %s"
                                            n
                                            (hash-table-count (nth 3 entry))
                                            (car entry)))
              (setq n (+ n 1)))))
      (let* ((index (if (numberp split)
                        split
                      (string-to-number split)))
             (entry (nth index circe-netsplit-list)))
        (if (not entry)
            (circe-display-server-message (format "No split number %s - use /WL to see a list"
                                          split))
          (let ((missing nil))
            (maphash (lambda (_key value)
                       (setq missing (cons value missing)))
                     (nth 3 entry))
            (circe-display-server-message
             (format "Missing people due to %s: %s"
                     (car entry)
                     (mapconcat 'identity
                                (sort missing
                                      (lambda (a b)
                                        (string< (downcase a)
                                                 (downcase b))))
                                ", ")))))))))

;;;;;;;;;;;;;;;;;;;;;;;;
;;; Display Handlers ;;;
;;;;;;;;;;;;;;;;;;;;;;;;

(defun circe-display-ignore (_nick _userhost _command &rest _args)
  "Don't show a this message.

NICK and USERHOST are the originator of COMMAND which had ARGS as
arguments."
  'noop)

(circe-set-display-handler "317" 'circe-display-317)
(defun circe-display-317 (_sender _ignored _numeric _target nick
                                  idletime &optional signon-time body)
  "Show a 317 numeric (RPL_WHOISIDLE).

Arguments are either of the two:

:<server> 317 <ournick> <nick> <idle> :seconds idle
:<server> 317 <ournick> <nick> <idle> <signon> :seconds idle, signon time"
  (with-current-buffer (circe-server-last-active-buffer)
    (let ((seconds-idle (string-to-number idletime))
          (signon-time (when body
                         (string-to-number signon-time))))
      (if signon-time
          (circe-display 'circe-format-server-whois-idle-with-signon
                         :whois-nick nick
                         :idle-seconds seconds-idle
                         :idle-duration (circe-duration-string seconds-idle)
                         :signon-time signon-time
                         :signon-date (current-time-string
                                       (seconds-to-time signon-time))
                         :signon-ago (circe-duration-string (- (float-time)
                                                               signon-time)))
        (circe-display 'circe-format-server-whois-idle
                       :whois-nick nick
                       :idle-seconds seconds-idle
                       :idle-duration (circe-duration-string seconds-idle))))))

(circe-set-display-handler "329" 'circe-display-329)
(defun circe-display-329 (_server _ignored _numeric _target channel timestamp)
  "Show a 329 numeric (RPL_CREATIONTIME)."
  (with-current-buffer (or (circe-server-get-chat-buffer channel)
                           (circe-server-last-active-buffer))
    (let ((creation-time (string-to-number timestamp)))
      (circe-display 'circe-format-server-channel-creation-time
                     :channel channel
                     :date (current-time-string
                            (seconds-to-time creation-time))
                     :ago (circe-duration-string (- (float-time)
                                                    creation-time))))))

(circe-set-display-handler "333" 'circe-display-333)
(defun circe-display-333 (_server _ignored _numeric target
                                  channel setter topic-time)
  "Show a 333 numeric (RPL_TOPICWHOTIME).

Arguments are either of the two:

:<server> 333 <target> <channel> <nick> 1434996762
:<server> 333 <target> <channel> <nick>!<user>@<host> 1434996803"
  (let ((channel-buffer (circe-server-get-chat-buffer channel))
        (topic-time (string-to-number topic-time)))
    (with-current-buffer (or channel-buffer
                             (circe-server-last-active-buffer))
      (circe-display (if channel-buffer
                         'circe-format-server-topic-time
                       'circe-format-server-topic-time-for-channel)
                     :nick target
                     :channel channel
                     :setter (irc-userstring-nick setter)
                     :setter-userhost (or (irc-userstring-userhost setter)
                                          "(unknown)")
                     :topic-time topic-time
                     :topic-date (current-time-string
                                  (seconds-to-time topic-time))
                     :topic-ago (circe-duration-string (- (float-time)
                                                          topic-time))))))

(circe-set-display-handler "AUTHENTICATE" 'circe-display-ignore)
(circe-set-display-handler "CAP" 'circe-display-ignore)

(circe-set-display-handler "channel.chghost" 'circe-display-channel-chghost)
(circe-set-display-handler "CHGHOST" 'circe-display-chghost)

(defun circe-display-channel-chghost (nick old-userhost _command channel
                                           new-user new-host)
  (with-current-buffer (circe-server-get-or-create-chat-buffer
                        channel 'circe-channel-mode)
    (let ((old-user (irc-userstring-user old-userhost))
          (old-host (irc-userstring-host old-userhost))
          (new-userhost (irc-make-userstring nick new-user new-host)))
      (circe-display 'circe-format-server-host-change
                      :nick nick
                      :old-user old-user
                      :old-host old-host
                      :new-user new-user
                      :new-host new-host
                      :userhost new-userhost))))

(defun circe-display-chghost (nick old-userhost _command new-user new-host)
  (let ((old-user (irc-userstring-user old-userhost))
        (old-host (irc-userstring-host old-userhost))
        (new-userhost (irc-make-userstring nick new-user new-host)))
    (with-current-buffer (circe-server-last-active-buffer)
      (circe-display 'circe-format-server-host-change
                  :nick nick
                  :old-user old-user
                  :old-host old-host
                  :new-user new-user
                  :new-host new-host
                  :userhost new-userhost))))

(circe-set-display-handler "conn.connected" 'circe-display-ignore)
(circe-set-display-handler "conn.disconnected" 'circe-display-ignore)

(circe-set-display-handler "irc.ctcp" 'circe-display-ignore)
(circe-set-display-handler "irc.ctcpreply" 'circe-display-ignore)

(circe-set-display-handler "irc.ctcp.ACTION" 'circe-display-ctcp-action)
(defun circe-display-ctcp-action (nick userhost _command target text)
  "Show an ACTION."
  (cond
   ;; Query
   ((circe-server-my-nick-p target)
    (let ((query-buffer (circe-query-auto-query-buffer nick)))
      (with-current-buffer (or query-buffer
                               (circe-server-last-active-buffer))
        (circe-display (if query-buffer
                           'circe-format-action
                         'circe-format-message-action)
                       :nick nick
                       :userhost (or userhost "server")
                       :body text))))
   ;; Channel
   (t
    (with-current-buffer (circe-server-get-or-create-chat-buffer
                          target 'circe-channel-mode)
      (circe-lurker-display-active nick userhost)
      (circe-display 'circe-format-action
                     :nick nick
                     :userhost (or userhost "server")
                     :body text)))))

(circe-set-display-handler "irc.ctcp.CLIENTINFO" 'circe-display-ctcp)

(circe-set-display-handler "irc.ctcp.PING" 'circe-display-ctcp-ping)
(defun circe-display-ctcp-ping (nick userhost _command target text)
  "Show a CTCP PING request."
  (with-current-buffer (circe-server-last-active-buffer)
    (circe-display 'circe-format-server-ctcp-ping
                   :nick nick
                   :userhost (or userhost "server")
                   :target target
                   :body (or text "")
                   :ago (let ((time (when text
                                      (string-to-number text))))
                          (if time
                              (format "%.2f seconds" (- (float-time) time))
                            "unknown seconds")))))

(circe-set-display-handler "irc.ctcpreply.PING" 'circe-display-ctcp-ping-reply)
(defun circe-display-ctcp-ping-reply (nick userhost _command target text)
  "Show a CTCP PING reply."
  (with-current-buffer (circe-server-last-active-buffer)
    (circe-display 'circe-format-server-ctcp-ping-reply
                   :nick nick
                   :userhost (or userhost "server")
                   :target target
                   :body text
                   :ago (let ((time (string-to-number text)))
                          (if time
                              (format "%.2f seconds" (- (float-time) time))
                            "unknown seconds")))))

(circe-set-display-handler "irc.ctcp.SOURCE" 'circe-display-ctcp)
(circe-set-display-handler "irc.ctcp.TIME" 'circe-display-ctcp)
(circe-set-display-handler "irc.ctcp.VERSION" 'circe-display-ctcp)
(defun circe-display-ctcp (nick userhost command target text)
  "Show a CTCP request that does not require special handling."
  (with-current-buffer (circe-server-last-active-buffer)
    (circe-display 'circe-format-server-ctcp
                   :nick nick
                   :userhost (or userhost "server")
                   :target target
                   :command (substring command 9)
                   :body (or text ""))))

(circe-set-display-handler "irc.registered" 'circe-display-ignore)

(circe-set-display-handler "JOIN" 'circe-display-JOIN)
(defun circe-display-JOIN (nick userhost _command channel
                                ;; cap: extended-join
                                &optional accountname realname)
  "Show a JOIN message.

The command receives an extra argument, the account name, on some
IRC servers."
  (let* ((accountname (if (equal accountname "*")
                          "(unauthenticated)"
                        accountname))
         (userinfo (if accountname
                       (format "%s, %s: %s" userhost accountname realname)
                     userhost))
         (split (circe--netsplit-join nick)))
    ;; First, update the channel
    (with-current-buffer (circe-server-get-or-create-chat-buffer
                          channel 'circe-channel-mode)
      (cond
       (split
        (let ((split-time (cadr split)))
          (when (< (+ split-time circe-netsplit-delay)
                   (float-time))
            (if (zerop split-time) ; we don't know when they joined
                (circe-display 'circe-format-server-netmerge
                               :split (car split)
                               :time -1
                               :date "?"
                               :ago "?")
                (circe-display 'circe-format-server-netmerge
                               :split (car split)
                               :time split-time
                               :date (current-time-string
                                      (seconds-to-time split-time))
                               :ago (circe-duration-string
                                     (- (float-time) split-time)))))))
       ((and (circe-reduce-lurker-spam)
             (circe-lurker-rejoin-p nick circe-chat-target))
        (let* ((channel (irc-connection-channel (circe-server-process)
                                                circe-chat-target))
               (user (when channel
                       (irc-channel-recent-user channel nick)))
               (departed (when user
                           (irc-user-part-time user))))
          (circe-display 'circe-format-server-rejoin
                         :nick nick
                         :userhost (or userhost "server")
                         :accountname accountname
                         :realname realname
                         :userinfo userinfo
                         :departuretime departed
                         :departuredelta (circe-duration-string
                                          (- (float-time)
                                             departed)))))
       ((not (circe-reduce-lurker-spam))
        (circe-display 'circe-format-server-join
                       :nick nick
                       :userhost (or userhost "server")
                       :accountname accountname
                       :realname realname
                       :userinfo userinfo
                       :channel circe-chat-target))))
    ;; Next, a possible query buffer. We do this even when the message
    ;; should be ignored by a netsplit, since this can't flood.
    (let ((buf (circe-server-get-chat-buffer nick)))
      (when buf
        (with-current-buffer buf
          (circe-display 'circe-format-server-join-in-channel
                         :nick nick
                         :userhost (or userhost "server")
                         :accountname accountname
                         :realname realname
                         :userinfo userinfo
                         :channel circe-chat-target))))))

(circe-set-display-handler "MODE" 'circe-display-MODE)
(defun circe-display-MODE (setter userhost _command target &rest modes)
  "Show a MODE message."
  (with-current-buffer (or (circe-server-get-chat-buffer target)
                           (circe-server-last-active-buffer))
    (circe-display 'circe-format-server-mode-change
                   :setter setter
                   :userhost (or userhost "server")
                   :target target
                   :change (mapconcat #'identity modes " "))))

(circe-set-display-handler "NICK" 'circe-display-NICK)
(defun circe-display-NICK (old-nick userhost _command new-nick)
  "Show a nick change."
  (if (circe-server-my-nick-p new-nick)
      (dolist (buf (cons (or circe-server-buffer
                             (current-buffer))
                         (circe-server-chat-buffers)))
        (with-current-buffer buf
          (circe-display 'circe-format-server-nick-change-self
                         :old-nick old-nick
                         :userhost (or userhost "server")
                         :new-nick new-nick)))
    (let ((query-buffer (circe-server-get-chat-buffer old-nick)))
      (when query-buffer
        (with-current-buffer query-buffer
          (circe-server-rename-chat-buffer old-nick new-nick)
          (circe-display 'circe-format-server-nick-change
                         :old-nick old-nick
                         :new-nick new-nick
                         :userhost (or userhost "server")))))
    (dolist (buf (circe-user-channels new-nick))
      (with-current-buffer buf
        (cond
         ((and (circe-reduce-lurker-spam)
               (circe-lurker-p new-nick))
          nil)
         ((circe-channel-user-nick-regain-p old-nick new-nick)
          (circe-display 'circe-format-server-nick-regain
                         :old-nick old-nick
                         :new-nick new-nick
                         :userhost (or userhost "server")))
         (t
          (circe-display 'circe-format-server-nick-change
                         :old-nick old-nick
                         :new-nick new-nick
                         :userhost (or userhost "server"))))))))

(circe-set-display-handler "nickserv.identified" 'circe-display-ignore)

;; NOTICE is also used to encode CTCP replies. irc.el will send
;; irc.notice events for NOTICEs without CTCP replies, so we show
;; that, not the raw notice.
(circe-set-display-handler "NOTICE" 'circe-display-ignore)
(circe-set-display-handler "irc.notice" 'circe-display-NOTICE)
(defun circe-display-NOTICE (nick userhost _command target text)
  "Show a NOTICE message."
  (cond
   ((not userhost)
    (with-current-buffer (circe-server-last-active-buffer)
      (circe-display 'circe-format-server-notice
                     :server nick
                     :body text)))
   ((circe-server-my-nick-p target)
    (with-current-buffer (or (circe-server-get-chat-buffer nick)
                             (circe-server-last-active-buffer))
      (circe-display 'circe-format-notice
                     :nick nick
                     :userhost (or userhost "server")
                     :body text)))
   (t
    (with-current-buffer (or (circe-server-get-chat-buffer target)
                             (circe-server-last-active-buffer))
      (circe-display 'circe-format-notice
                     :nick nick
                     :userhost (or userhost "server")
                     :body text)))))

(circe-set-display-handler "PART" 'circe-display-PART)
(defun circe-display-PART (nick userhost _command channel &optional reason)
  "Show a PART message."
  (with-current-buffer (or (circe-server-get-chat-buffer channel)
                           (circe-server-last-active-buffer))
    (when (or (not circe-chat-target)
              (not (circe-lurker-p nick)))
      (circe-display 'circe-format-server-part
                     :nick nick
                     :userhost (or userhost "server")
                     :channel channel
                     :reason (or reason "[No reason given]")))))

(circe-set-display-handler "PING" 'circe-display-ignore)
(circe-set-display-handler "PONG" 'circe-display-ignore)

;; PRIVMSG is also used to encode CTCP requests. irc.el will send
;; irc.message events for PRIVMSGs without CTCP messages, so we show
;; that, not the raw message.
(circe-set-display-handler "PRIVMSG" 'circe-display-ignore)
(circe-set-display-handler "irc.message" 'circe-display-PRIVMSG)
(defun circe-display-PRIVMSG (nick userhost _command target text)
  "Show a PRIVMSG message."
  (cond
   ((circe-server-my-nick-p target)
    (let ((buf (circe-query-auto-query-buffer nick)))
      (if buf
          (with-current-buffer buf
            (circe-display 'circe-format-say
                           :nick nick
                           :userhost (or userhost "server")
                           :body text))
        (with-current-buffer (circe-server-last-active-buffer)
          (circe-display 'circe-format-message
                         :nick nick
                         :userhost (or userhost "server")
                         :body text)))))
   (t
    (with-current-buffer (circe-server-get-or-create-chat-buffer
                          target 'circe-channel-mode)
      (circe-lurker-display-active nick userhost)
      (circe-display 'circe-format-say
                     :nick nick
                     :userhost (or userhost "server")
                     :body text)))))

(circe-set-display-handler "SETNAME" 'circe-display-SETNAME)
(defun circe-display-SETNAME (_nick _userhost _command newname)
  (let ((oldname (irc-connection-get circe-server-process :realname)))
    (irc-connection-put circe-server-process :realname newname)
    (with-current-buffer (circe-server-last-active-buffer)
      (circe-display 'circe-format-server-setname
                     :oldname oldname
                     :newname newname))))

(circe-set-display-handler "TAGMSG" 'circe-display-ignore)

(circe-set-display-handler "TOPIC" 'circe-display-topic)
(defun circe-display-topic (nick userhost _command channel new-topic)
  "Show a TOPIC change."
  (with-current-buffer (circe-server-get-or-create-chat-buffer
                        channel 'circe-channel-mode)
    (let* ((channel-obj (irc-connection-channel (circe-server-process)
                                                channel))
           (old-topic (or (when channel
                            (irc-channel-last-topic channel-obj))
                          "")))
      (circe-display 'circe-format-server-topic
                     :nick nick
                     :userhost (or userhost "server")
                     :channel channel
                     :new-topic new-topic
                     :old-topic old-topic
                     :topic-diff (circe--topic-diff old-topic new-topic)))))

(defun circe--topic-diff (old new)
  "Return a colored topic diff between OLD and NEW."
  (mapconcat (lambda (elt)
               (cond
                ((eq '+ (car elt))
                 (let ((s (cadr elt)))
                   (add-face-text-property 0 (length s)
                                           'circe-topic-diff-new-face nil s)
                   s))
                ((eq '- (car elt))
                 (let ((s (cadr elt)))
                   (add-face-text-property 0 (length s)
                                           'circe-topic-diff-removed-face nil s)
                   s))
                (t
                 (cadr elt))))
             (lcs-unified-diff (circe--topic-diff-split old)
                               (circe--topic-diff-split new)
                               'string=)
             ""))

(defun circe--topic-diff-split (str)
  "Split STR into a list of components.
The list consists of words and spaces."
  (let ((lis nil))
    (with-temp-buffer
      (insert str)
      (goto-char (point-min))
      (while (< (point)
                (point-max))
        (if (or (looking-at "\\w+\\W*")
                (looking-at ".\\s-*"))
            (progn
              (setq lis (cons (match-string 0)
                              lis))
              (replace-match ""))
          (error "Can't happen"))))
    (nreverse lis)))

(circe-set-display-handler "channel.quit" 'circe-display-channel-quit)
(defun circe-display-channel-quit (nick userhost _command channel
                                        &optional reason)
  "Show a QUIT message."
  (let ((split (circe--netsplit-quit reason nick)))
    (with-current-buffer (circe-server-get-or-create-chat-buffer
                          channel 'circe-channel-mode)
      (cond
       (split
        (when (< (+ split circe-netsplit-delay)
                 (float-time))
          (circe-display 'circe-format-server-netsplit
                         :split reason)))
       ((not (circe-lurker-p nick))
        (circe-display 'circe-format-server-quit-channel
                       :nick nick
                       :userhost (or userhost "server")
                       :channel channel
                       :reason (or reason "[no reason given]")))))))

(circe-set-display-handler "QUIT" 'circe-display-QUIT)
(defun circe-display-QUIT (nick userhost _command &optional reason)
  "Show a QUIT message.

Channel quits are shown already, so just show quits in queries."
  (let ((buf (circe-server-get-chat-buffer nick)))
    (when buf
      (with-current-buffer buf
        (circe-display 'circe-format-server-quit
                       :nick nick
                       :userhost (or userhost "server")
                       :reason (or reason "[no reason given]"))))))

(defvar circe-netsplit-list nil
  "A list of recorded netsplits.
Every item is a list with four elements:
- The quit message for this split.
- The time when last we heard about a join in this split
- The time when last we heard about a quit in this split
- A hash table noting which nicks did leave")
(make-variable-buffer-local 'circe-netsplit-list)

(defun circe--netsplit-join (nick)
  "Search for NICK in the netsplit lists.
This either returns a pair whose car is the quit message of this
split, and the cadr the time we last heard anything of the split
of that user. If the NICK isn't split, this returns nil."
  (with-circe-server-buffer
    (catch 'return
      (dolist (entry circe-netsplit-list)
        (let ((table (nth 3 entry)))
          (when (gethash nick table)
            (let ((name (nth 0 entry))
                  (time (nth 1 entry)))
              (remhash nick table)
              (when (= 0 (hash-table-count table))
                (setq circe-netsplit-list
                      (delq entry circe-netsplit-list)))
              (setcar (cdr entry)
                      (float-time))
              (throw 'return (list name time))))))
      nil)))

(defun circe--netsplit-quit (reason nick)
  "If REASON indicates a netsplit, mark NICK as split.
This either returns the time when last we heard about this split,
or nil when this isn't a split."
  (when (circe--netsplit-reason-p reason)
    (with-circe-server-buffer
      (let ((entry (assoc reason circe-netsplit-list)))
        (if entry
            (let ((time (nth 2 entry))
                  (table (nth 3 entry)))
              (setcar (cddr entry)
                      (float-time))
              (puthash nick nick table)
              time)
          ;; New split!
          (let ((table (make-hash-table :test 'equal)))
            (puthash nick nick table)
            (setq circe-netsplit-list
                  (cons (list reason 0 (float-time) table)
                        circe-netsplit-list))
            0))))))

(defun circe--netsplit-reason-p (reason)
  "Return non-nil if REASON is the quit message of a netsplit.
This is true when it contains exactly two hosts, with a single
space in between them. The hosts must include at least one dot,
and must not include colons or slashes (else they might be
URLs). (Thanks to irssi for this criteria list)"
  (if (string-match "^[^ :/]+\\.[^ :/]* [^ :/]+\\.[^ :/]*$"
                    reason)
      t
    nil))

(let ((simple-format-specifiers
       '(("INVITE" active "Invite: {origin} invites you to {1}")
         ("KICK" 0 "Kick: {1} kicked by {origin}: {2}")
         ("ERROR" active "Error: {0-}")
         ;; https://ircv3.net/specs/extensions/standard-replies
         ("FAIL" server "FAIL: {0-}")
         ("WARN" server "WARN: {0-}")
         ("NOTE" server "NOTE: {0-}")
         ;; numerics
         ("001" server "{1}")
         ("002" server "{1}")
         ("003" server "{1}")
         ("004" server "{1-}")
         ("005" server "{1-}")
         ;; IRCnet: * Please wait while we process your connection.
         ("020" server "{0-}")
         ;; IRCnet
         ("042" server "Your unique ID is {1}")
         ("200" active "{1-}")
         ("201" active "{1-}")
         ("203" active "{1-}")
         ("204" active "{1-}")
         ("205" active "{1-}")
         ("206" active "{1-}")
         ("207" active "{1-}")
         ("208" active "{1-}")
         ("209" active "{1-}")
         ("211" active "{1-}")
         ("212" active "{1-}")
         ("219" active "{1-}")
         ("221" active "User mode: {1-}")
         ("234" active "Service: {1-}")
         ("235" active "{1-}")
         ("242" active "{1}")
         ("243" active "{1-}")
         ("250" server "{1}")
         ("251" server "{1}")
         ("252" server "{1-}")
         ("253" server "{1-}")
         ("254" server "{1-}")
         ("255" server "{1}")
         ("256" active "{1-}")
         ("257" active "{1}")
         ("258" active "{1}")
         ("259" active "{1}")
         ("261" active "{1-}")
         ("262" active "{1-}")
         ("263" active "{1-}")
         ("265" server "{1-}")
         ("266" server "{1-}")
         ;; This is returned on both WHOIS and PRIVMSG. It
         ;; should go to the active window for the former, and
         ;; the query window for the latter. Oh well.
         ("301" active "User away: {1}")
         ("302" active "User hosts: {1}")
         ("303" active "Users online: {1}")
         ("305" active "{1}")
         ("306" active "{1}")
         ("307" active "{1-}")
         ;; Coldfront: 310 <nick> is available for help.
         ("310" active "{1-}")
         ("311" active "{1} is {2}@{3} ({5})")
         ("312" active "{1} is on {2} ({3})")
         ("313" active "{1} {2}")
         ("314" active "{1} was {2}@{3} ({5})")
         ("315" active "{2}")
         ("318" active "{2}")
         ("319" active "{1} is on {2}")
         ("320" active "{1-}")
         ("322" active "{1-}")
         ("323" active "{1-}")
         ("324" 1 "Channel mode for {1}: {2-}")
         ("325" 1 "Unique operator on {1} is {2}")
         ("328" 1 "Channel homepage for {1}: {2-}")
         ("330" active "{1} is logged in as {2}")
         ("331" 1 "No topic for {1} set")
         ("332" 1 "Topic for {1}: {2}")
         ("341" active "Inviting {1} to {2}")
         ("346" 1 "Invite mask: {2}")
         ("347" 1 "{2}")
         ("348" 1 "Except mask: {2}")
         ("349" 1 "{2}")
         ("351" active "{1-}")
         ("352" active "{5} ({2}@{3}) in {1} on {4}: {6-}")
         ("353" 2 "Names: {3}")
         ("364" active "{1-}")
         ("365" active "{1-}")
         ("366" 1 "{2}")
         ("367" 1 "Ban mask: {2}")
         ("368" 1 "{2}")
         ("369" active "{1} {2}")
         ("371" active "{1}")
         ("372" server "{1}")
         ("374" active "{1}")
         ("375" server "{1}")
         ("376" server "{1}")
         ("378" active "{1-}")
         ("381" active "{1}")
         ("382" active "{1-}")
         ("391" active "Time on {1}: {2}")
         ("401" active "No such nick: {1}")
         ("402" active "No such server: {1}")
         ("403" active "No such channel: {1}")
         ("404" 1 "Can not send to channel {1}")
         ("405" active "Can not join {1}: {2}")
         ("406" active "{1-}")
         ("407" active "{1-}")
         ("408" active "No such service: {1}")
         ("422" active "{1}")
         ("432" active "Erroneous nick name: {1}")
         ("433" active "Nick name in use: {1}")
         ("437" active "Nick/channel is temporarily unavailable: {1}")
         ("441" 2 "User not on channel: {1}")
         ("442" active "You are not on {1}")
         ("443" 2 "User {1} is already on channel {2}")
         ;; Coldfront: 451 * :You have not registered
         ("451" active "{1-}")
         ("461" active "{1-}")
         ("464" active "{1}")
         ("467" 1 "{2}")
         ("470" 1 "{1} made you join {2}: {3-}")
         ("471" 1 "{2}")
         ("472" active "{1-}")
         ("473" active "{1-}")
         ("474" active "{1-}")
         ("475" active "{1-}")
         ("476" active "{1-}")
         ("477" active "{1-}")
         ("481" 1 "{2-}")
         ("484" active "{1-}")
         ("491" active "{1}")
         ;; Coldfront: 671 <nick> is using a Secure Connection
         ("671" active "{1-}")
         ("728" 1 "Quiet mask: {3}")
         ("729" 1 "{3-}")
         ;; Freenode SASL auth
         ("900" active "SASL: {3-}")
         ("903" active "{1-}"))))
  (dolist (fmt simple-format-specifiers)
    (circe-set-display-handler (car fmt) (cdr fmt))))

(defun circe-set-message-target (command target)
  "Set the target of COMMAND to TARGET.

This can be used to change format-based display handlers more
easily."
  (let ((handler (circe-get-display-handler command)))
    (when (not (consp handler))
      (error "Handler of command %s is not a list" command))
    (setcar handler target)))

;;;;;;;;;;;;;;;;;;;;;;;;
;;; Helper Functions ;;;
;;;;;;;;;;;;;;;;;;;;;;;;

(defun circe--list-drop-right (list pattern)
  "Drop elements from the right of LIST that match PATTERN.

LIST should be a list of strings, and PATTERN is used as a
regular expression."
  (let ((list (reverse list)))
    (while (and list
                (string-match pattern (car list)))
      (setq list (cdr list)))
    (nreverse list)))

(defun circe--nick-next (oldnick)
  "Return a new nick to try for OLDNICK."
  (cond
   ;; If the nick ends with -+, replace those with _
   ((string-match "^\\(.*[^-]\\)\\(-+\\)$" oldnick)
    (concat (match-string 1 oldnick)
            (make-string (- (match-end 2)
                            (match-beginning 2))
                         ?_)))
   ;; If the nick is 9 chars long, take prefix and rotate.
   ((>= (length oldnick)
        9)
    (when (string-match "^\\(.*[^-_]\\)[-_]*$" oldnick)
      (let ((nick (match-string 1 oldnick)))
        (concat (substring nick 1)
                (string (aref nick 0))))))
   ;; If the nick ends with _+ replace those with - and add one
   ((string-match "^\\(.*[^_]\\)\\(_+\\)$" oldnick)
    (concat (match-string 1 oldnick)
            (make-string (- (match-end 2)
                            (match-beginning 2))
                         ?-)
            "-"))
   ;; Else, just append -
   (t
    (concat oldnick "-"))))

(defun circe-duration-string (duration)
  "Return a description of a DURATION in seconds."
  (let ((parts `((,(* 12 30 24 60 60) "year")
                 (,(* 30 24 60 60) "month")
                 (,(* 24 60 60) "day")
                 (,(* 60 60) "hour")
                 (60 "minute")
                 (1 "second")))
        (duration (round duration))
        (result nil))
    (dolist (part parts)
      (let* ((seconds-per-part (car part))
             (description (cadr part))
             (count (/ duration seconds-per-part)))
        (when (not (zerop count))
          (setq result (cons (format "%d %s%s"
                                     count description
                                     (if (= count 1) "" "s"))
                             result)))
        (setq duration (- duration (* count seconds-per-part)))))
    (if result
        (mapconcat #'identity
                   (nreverse result)
                   " ")
      "a moment")))

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