//
// Copyright (c) 2014 - 2022, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//

// xS is a straight-forward port of xD IRCd from C.
package main

import (
	"bufio"
	"bytes"
	"crypto/sha256"
	"crypto/tls"
	"encoding/hex"
	"errors"
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"log/syslog"
	"net"
	"os"
	"os/signal"
	"os/user"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"syscall"
	"time"
)

const projectName = "xS"

var projectVersion = "?"

var debugMode = false

// --- Logging -----------------------------------------------------------------

type logPrio int

const (
	prioFatal logPrio = iota
	prioError
	prioWarning
	prioStatus
	prioDebug
)

func (lp logPrio) prefix() string {
	switch lp {
	case prioFatal:
		return "fatal: "
	case prioError:
		return "error: "
	case prioWarning:
		return "warning: "
	case prioStatus:
		return ""
	case prioDebug:
		return "debug: "
	default:
		panic("unhandled log priority")
	}
}

func (lp logPrio) syslogPrio() syslog.Priority {
	switch lp {
	case prioFatal:
		return syslog.LOG_ERR
	case prioError:
		return syslog.LOG_ERR
	case prioWarning:
		return syslog.LOG_WARNING
	case prioStatus:
		return syslog.LOG_INFO
	case prioDebug:
		return syslog.LOG_DEBUG
	default:
		panic("unhandled log priority")
	}
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

func logMessageStdio(prio logPrio, format string, args ...interface{}) {
	// TODO: isatty-enabled colors based on prio.
	os.Stderr.WriteString(time.Now().Format("2006-01-02 15:04:05 ") +
		prio.prefix() + fmt.Sprintf(format, args...) + "\n")
}

func logMessageSystemd(prio logPrio, format string, args ...interface{}) {
	if prio == prioFatal {
		// There is no corresponding syslog severity.
		format = "fatal: " + format
	}
	fmt.Fprintf(os.Stderr, "<%d>%s\n",
		prio.syslogPrio(), fmt.Sprintf(format, args...))
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

var logMessage = logMessageStdio

func printDebug(format string, args ...interface{}) {
	if debugMode {
		logMessage(prioDebug, format, args...)
	}
}

func printStatus(format string, args ...interface{}) {
	logMessage(prioStatus, format, args...)
}
func printWarning(format string, args ...interface{}) {
	logMessage(prioWarning, format, args...)
}
func printError(format string, args ...interface{}) {
	logMessage(prioError, format, args...)
}

// "fatal" is reserved for failures that would harm further operation.

func printFatal(format string, args ...interface{}) {
	logMessage(prioFatal, format, args...)
}

func exitFatal(format string, args ...interface{}) {
	printFatal(format, args...)
	os.Exit(1)
}

// --- Utilities ---------------------------------------------------------------

// Split a string by a set of UTF-8 delimiters, optionally ignoring empty items.
func splitString(s, delims string, ignoreEmpty bool) (result []string) {
	for {
		end := strings.IndexAny(s, delims)
		if end < 0 {
			break
		}
		if !ignoreEmpty || end != 0 {
			result = append(result, s[:end])
		}
		s = s[end+1:]
	}
	if !ignoreEmpty || s != "" {
		result = append(result, s)
	}
	return
}

// Trivial SSL/TLS autodetection. The first block of data returned by Recvfrom
// must be at least three octets long for this to work reliably, but that should
// not pose a problem in practice. We might try waiting for them.
//
//	SSL2:      1xxx xxxx | xxxx xxxx |    <1>
//	              (message length)  (client hello)
//	SSL3/TLS:    <22>    |    <3>    | xxxx xxxx
//	          (handshake)|  (protocol version)
//
// Note that Go 1.12's crypto/tls offers a slightly more straight-forward
// solution: "If a client sends an initial message that does not look like TLS,
// the server will no longer reply with an alert, and it will expose the
// underlying net.Conn in the new field Conn of RecordHeaderError."
func detectTLS(sysconn syscall.RawConn) (isTLS bool) {
	sysconn.Read(func(fd uintptr) (done bool) {
		var buf [3]byte
		n, _, err := syscall.Recvfrom(int(fd), buf[:], syscall.MSG_PEEK)
		switch {
		case n == 3:
			isTLS = buf[0]&0x80 != 0 && buf[2] == 1
			fallthrough
		case n == 2:
			isTLS = isTLS || buf[0] == 22 && buf[1] == 3
		case n == 1:
			isTLS = buf[0] == 22
		case err == syscall.EAGAIN:
			return false
		}
		return true
	})
	return isTLS
}

// --- File system -------------------------------------------------------------

// Look up the value of an XDG path from environment, or fall back to a default.
func getXDGHomeDir(name, def string) string {
	env := os.Getenv(name)
	if env != "" && env[0] == filepath.Separator {
		return env
	}

	home := ""
	if v, ok := os.LookupEnv("HOME"); ok {
		home = v
	} else if u, _ := user.Current(); u != nil {
		home = u.HomeDir
	}
	return filepath.Join(home, def)
}

func resolveRelativeFilenameGeneric(paths []string, filename string) string {
	for _, path := range paths {
		// As per XDG spec, relative paths are ignored.
		if path == "" || path[0] != filepath.Separator {
			continue
		}

		file := filepath.Join(path, filename)
		if _, err := os.Stat(file); err == nil {
			return file
		}
	}
	return ""
}

// Retrieve all XDG base directories for configuration files.
func getXDGConfigDirs() (result []string) {
	home := getXDGHomeDir("XDG_CONFIG_HOME", ".config")
	if home != "" {
		result = append(result, home)
	}
	dirs := os.Getenv("XDG_CONFIG_DIRS")
	if dirs == "" {
		dirs = "/etc/xdg"
	}
	for _, path := range strings.Split(dirs, ":") {
		if path != "" {
			result = append(result, path)
		}
	}
	return
}

func resolveRelativeConfigFilename(filename string) string {
	return resolveRelativeFilenameGeneric(getXDGConfigDirs(),
		filepath.Join(projectName, filename))
}

func findTildeHome(username string) string {
	if username != "" {
		if u, _ := user.Lookup(username); u != nil {
			return u.HomeDir
		}
	} else if u, _ := user.Current(); u != nil {
		return u.HomeDir
	} else if v, ok := os.LookupEnv("HOME"); ok {
		return v
	}
	printDebug("failed to expand the home directory for %s", username)
	return "~" + username
}

func resolveFilename(filename string, relativeCB func(string) string) string {
	// Absolute path is absolute.
	if filename == "" || filename[0] == filepath.Separator {
		return filename
	}
	if filename[0] != '~' {
		return relativeCB(filename)
	}

	// Paths to home directories ought to be absolute.
	var n int
	for n = 0; n < len(filename); n++ {
		if filename[n] == filepath.Separator {
			break
		}
	}
	return findTildeHome(filename[1:n]) + filename[n:]
}

// --- Simple file I/O ---------------------------------------------------------

// Overwrites filename contents with data; creates directories as needed.
func writeFile(path string, data []byte) error {
	if dir := filepath.Dir(path); dir != "." {
		if err := os.MkdirAll(dir, 0755); err != nil {
			return err
		}
	}
	return ioutil.WriteFile(path, data, 0644)
}

// Wrapper for writeFile that makes sure that the new data has been written
// to disk in its entirety before overriding the old file.
func writeFileSafe(path string, data []byte) error {
	temp := path + ".new"
	if err := writeFile(temp, data); err != nil {
		return err
	}
	return os.Rename(temp, path)
}

// --- Simple configuration ----------------------------------------------------

// This is the bare minimum to make an application configurable.
// Keys are stripped of surrounding whitespace, values are not.

type simpleConfigItem struct {
	key         string // INI key
	def         string // default value
	description string // documentation
}

type simpleConfig map[string]string

func (sc simpleConfig) loadDefaults(table []simpleConfigItem) {
	for _, item := range table {
		sc[item.key] = item.def
	}
}

func (sc simpleConfig) updateFromFile() error {
	basename := projectName + ".conf"
	path := resolveFilename(basename, resolveRelativeConfigFilename)
	if path == "" {
		return &os.PathError{
			Op:   "cannot find",
			Path: basename,
			Err:  os.ErrNotExist,
		}
	}

	f, err := os.Open(path)
	if err != nil {
		return err
	}
	defer f.Close()

	scanner := bufio.NewScanner(f)
	for lineNo := 1; scanner.Scan(); lineNo++ {
		line := strings.TrimLeft(scanner.Text(), " \t")
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}

		equals := strings.IndexByte(line, '=')
		if equals <= 0 {
			return fmt.Errorf("%s:%d: malformed line", path, lineNo)
		}

		sc[strings.TrimRight(line[:equals], " \t")] = line[equals+1:]
	}
	return scanner.Err()
}

func writeConfigurationFile(pathHint string, data []byte) (string, error) {
	path := pathHint
	if path == "" {
		path = filepath.Join(getXDGHomeDir("XDG_CONFIG_HOME", ".config"),
			projectName, projectName+".conf")
	}

	if err := writeFileSafe(path, data); err != nil {
		return "", err
	}
	return path, nil
}

func simpleConfigWriteDefault(pathHint string, prolog string,
	table []simpleConfigItem) (string, error) {
	data := []byte(prolog)
	for _, item := range table {
		data = append(data, fmt.Sprintf("# %s\n%s=%s\n",
			item.description, item.key, item.def)...)
	}
	return writeConfigurationFile(pathHint, data)
}

// Convenience wrapper suitable for most simple applications.
func callSimpleConfigWriteDefault(pathHint string, table []simpleConfigItem) {
	prologLines := []string{
		`# ` + projectName + ` ` + projectVersion + ` configuration file`,
		"#",
		`# Relative paths are searched for in ${XDG_CONFIG_HOME:-~/.config}`,
		`# /` + projectName + ` as well as in $XDG_CONFIG_DIRS/` + projectName,
		``,
		``,
	}

	path, err := simpleConfigWriteDefault(
		pathHint, strings.Join(prologLines, "\n"), table)
	if err != nil {
		exitFatal("%s", err)
	}

	printStatus("configuration written to `%s'", path)
}

// --- Configuration -----------------------------------------------------------

var configTable = []simpleConfigItem{
	{"server_name", "", "Server name"},
	{"server_info", "My server", "Brief server description"},
	{"motd", "", "MOTD filename"},
	{"catalog", "", "Localisation catalog"},

	{"bind", ":6667", "Bind addresses of the IRC server"},
	{"tls_cert", "", "Server TLS certificate (PEM)"},
	{"tls_key", "", "Server TLS private key (PEM)"},
	{"webirc_password", "", "Password for WebIRC"},

	{"operators", "", "IRCop TLS certificate SHA-256 fingerprints"},

	{"max_connections", "0", "Global connection limit"},
	{"ping_interval", "180", "Interval between PINGs (sec)"},
}

// --- Rate limiter ------------------------------------------------------------

type floodDetector struct {
	interval   time.Duration // interval for the limit in seconds
	limit      uint          // maximum number of events allowed
	timestamps []time.Time   // timestamps of last events
	pos        uint          // index of the oldest event
}

func newFloodDetector(interval time.Duration, limit uint) *floodDetector {
	return &floodDetector{
		interval:   interval,
		limit:      limit,
		timestamps: make([]time.Time, limit+1),
		pos:        0,
	}
}

func (fd *floodDetector) check() bool {
	now := time.Now()
	fd.timestamps[fd.pos] = now

	fd.pos++
	if fd.pos > fd.limit {
		fd.pos = 0
	}

	var count uint
	begin := now.Add(-fd.interval)
	for _, ts := range fd.timestamps {
		if ts.After(begin) {
			count++
		}
	}
	return count <= fd.limit
}

// --- IRC token validation ----------------------------------------------------

// Everything as per RFC 2812
const (
	ircMaxNickname      = 9
	ircMaxHostname      = 63
	ircMaxChannelName   = 50
	ircMaxMessageLength = 510
)

const (
	reClassSpecial = "\\[\\]\\\\`_^{|}"
	// "shortname" from RFC 2812 doesn't work how its author thought it would.
	reShortname = "[0-9A-Za-z](-*[0-9A-Za-z])*"
)

var (
	reHostname = regexp.MustCompile(
		`^` + reShortname + `(\.` + reShortname + `)*$`)

	// Extending ASCII to the whole range of Unicode letters.
	reNickname = regexp.MustCompile(
		`^[\pL` + reClassSpecial + `][\pL` + reClassSpecial + `0-9-]*$`)

	// Notably, this won't match invalid UTF-8 characters, although the
	// behaviour seems to be unstated in the documentation.
	reUsername = regexp.MustCompile(`^[^\0\r\n @]+$`)

	reChannelName = regexp.MustCompile(`^[^\0\007\r\n ,:]+$`)
	reKey         = regexp.MustCompile(`^[^\r\n\f\t\v ]{1,23}$`)
	reUserMask    = regexp.MustCompile(`^[^!@]+![^!@]+@[^@!]+$`)
	reFingerprint = regexp.MustCompile(`^[a-fA-F0-9]{64}$`)
)

func ircValidateHostname(hostname string) error {
	if hostname == "" {
		return errors.New("the value is empty")
	}
	if !reHostname.MatchString(hostname) {
		return errors.New("invalid format")
	}
	if len(hostname) > ircMaxHostname {
		return errors.New("the value is too long")
	}
	return nil
}

func ircIsValidNickname(nickname string) bool {
	return len(nickname) <= ircMaxNickname && reNickname.MatchString(nickname)
}

func ircIsValidUsername(username string) bool {
	// XXX: We really should put a limit on this
	// despite the RFC not mentioning one.
	return reUsername.MatchString(username)
}

func ircIsValidChannelName(name string) bool {
	return len(name) <= ircMaxChannelName && reChannelName.MatchString(name)
}

func ircIsValidKey(key string) bool {
	// XXX: Should be 7-bit as well but whatever.
	return reKey.MatchString(key)
}

func ircIsValidUserMask(mask string) bool {
	return reUserMask.MatchString(mask)
}

func ircIsValidFingerprint(fp string) bool {
	return reFingerprint.MatchString(fp)
}

// --- Clients (equals users) --------------------------------------------------

type connCloseWriter interface {
	net.Conn
	CloseWrite() error
}

const ircSupportedUserModes = "aiwros"

const (
	ircUserModeInvisible uint = 1 << iota
	ircUserModeRxWallops
	ircUserModeRestricted
	ircUserModeOperator
	ircUserModeRxServerNotices
)

const (
	ircCapMultiPrefix uint = 1 << iota
	ircCapInviteNotify
	ircCapEchoMessage
	ircCapUserhostInNames
	ircCapServerTime
)

type client struct {
	transport net.Conn        // underlying connection
	tls       *tls.Conn       // TLS, if detected
	conn      connCloseWriter // high-level connection
	recvQ     []byte          // unprocessed input
	sendQ     []byte          // unprocessed output
	reading   bool            // whether a reading goroutine is running
	writing   bool            // whether a writing goroutine is running
	closing   bool            // whether we're closing the connection
	killTimer *time.Timer     // hard kill timeout

	opened            time.Time // when the connection was opened
	nSentMessages     uint      // number of sent messages total
	sentBytes         int       // number of sent bytes total
	nReceivedMessages uint      // number of received messages total
	receivedBytes     int       // number of received bytes total

	hostname string // hostname or IP shown to the network
	port     string // port of the peer as a string
	address  string // full network address

	pingTimer      *time.Timer // we should send a PING
	timeoutTimer   *time.Timer // connection seems to be dead
	registered     bool        // the user has registered
	capNegotiating bool        // negotiating capabilities
	capsEnabled    uint        // enabled capabilities
	capVersion     uint        // CAP protocol version

	tlsCertFingerprint string // client certificate fingerprint

	nickname string // IRC nickname (main identifier)
	username string // IRC username
	realname string // IRC realname (or e-mail)

	mode        uint            // user's mode
	awayMessage string          // away message
	lastActive  time.Time       // last PRIVMSG, to get idle time
	invites     map[string]bool // channel invitations by operators
	antiflood   *floodDetector  // flood detector
}

// --- Channels ----------------------------------------------------------------

const ircSupportedChanModes = "ov" + "beI" + "imnqpst" + "kl"

const (
	ircChanModeInviteOnly uint = 1 << iota
	ircChanModeModerated
	ircChanModeNoOutsideMsgs
	ircChanModeQuiet
	ircChanModePrivate
	ircChanModeSecret
	ircChanModeProtectedTopic

	ircChanModeOperator
	ircChanModeVoice
)

type channel struct {
	name      string    // channel name
	modes     uint      // channel modes
	key       string    // channel key
	userLimit int       // user limit or -1
	created   time.Time // creation time

	topic     string    // channel topic
	topicWho  string    // who set the topic
	topicTime time.Time // when the topic was set

	userModes map[*client]uint // modes for all channel users

	banList       []string // ban list
	exceptionList []string // exceptions from bans
	inviteList    []string // exceptions from +I
}

func (ch *channel) getMode(discloseSecrets bool) string {
	var buf []byte
	if 0 != ch.modes&ircChanModeInviteOnly {
		buf = append(buf, 'i')
	}
	if 0 != ch.modes&ircChanModeModerated {
		buf = append(buf, 'm')
	}
	if 0 != ch.modes&ircChanModeNoOutsideMsgs {
		buf = append(buf, 'n')
	}
	if 0 != ch.modes&ircChanModeQuiet {
		buf = append(buf, 'q')
	}
	if 0 != ch.modes&ircChanModePrivate {
		buf = append(buf, 'p')
	}
	if 0 != ch.modes&ircChanModeSecret {
		buf = append(buf, 's')
	}
	if 0 != ch.modes&ircChanModeProtectedTopic {
		buf = append(buf, 'r')
	}

	if ch.userLimit != -1 {
		buf = append(buf, 'l')
	}
	if ch.key != "" {
		buf = append(buf, 'k')
	}

	// XXX: Is it correct to split it? Try it on an existing implementation.
	if discloseSecrets {
		if ch.userLimit != -1 {
			buf = append(buf, fmt.Sprintf(" %d", ch.userLimit)...)
		}
		if ch.key != "" {
			buf = append(buf, fmt.Sprintf(" %s", ch.key)...)
		}
	}
	return string(buf)
}

// --- IRC server context ------------------------------------------------------

type whowasInfo struct {
	nickname, username, realname, hostname string
}

func newWhowasInfo(c *client) *whowasInfo {
	return &whowasInfo{
		nickname: c.nickname,
		username: c.username,
		realname: c.realname,
		hostname: c.hostname,
	}
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

type ircCommand struct {
	requiresRegistration bool
	handler              func(*message, *client)

	nReceived     uint // number of commands received
	bytesReceived int  // number of bytes received total
}

type preparedEvent struct {
	client *client
	host   string // client's hostname or literal IP address
	isTLS  bool   // the client seems to use TLS
}

type readEvent struct {
	client *client
	data   []byte // new data from the client
	err    error  // read error
}

type writeEvent struct {
	client  *client
	written int   // amount of bytes written
	err     error // write error
}

// TODO: Maybe we want to keep it in a struct?
// A better question might be: can we run multiple instances of it?
var (
	// network

	listeners []net.Listener
	clients   = make(map[*client]bool)

	// IRC state

	// XXX: Beware that maps with identifier keys need to be indexed correctly.
	// We might want to enforce accessor functions for users and channels.

	started  time.Time                      // when the server has been started
	users    = make(map[string]*client)     // maps nicknames to clients
	channels = make(map[string]*channel)    // maps channel names to data
	whowas   = make(map[string]*whowasInfo) // WHOWAS registry

	// event loop

	quitting  bool
	quitTimer <-chan time.Time

	sigs     = make(chan os.Signal, 1)
	conns    = make(chan net.Conn)
	prepared = make(chan preparedEvent)
	reads    = make(chan readEvent)
	writes   = make(chan writeEvent)
	timers   = make(chan func())

	// configuration

	config         simpleConfig    // server configuration
	tlsConf        *tls.Config     // TLS connection configuration
	serverName     string          // our server name
	pingInterval   time.Duration   // ping interval
	maxConnections int             // max connections allowed or 0
	motd           []string        // MOTD (none if empty)
	catalog        map[int]string  // message catalog for server msgs
	operators      map[string]bool // TLS certificate fingerprints for IRCops
)

// Forcefully tear down all connections.
func forceQuit(reason string) {
	if !quitting {
		exitFatal("forceQuit called without initiateQuit")
	}

	printStatus("forced shutdown (%s)", reason)
	for c := range clients {
		// initiateQuit has already unregistered the client.
		c.kill("Shutting down")
	}
}

// Initiate a clean shutdown of the whole daemon.
func initiateQuit() {
	printStatus("shutting down")
	for _, ln := range listeners {
		if err := ln.Close(); err != nil {
			printError("%s", err)
		}
	}
	for c := range clients {
		c.closeLink("Shutting down")
	}

	quitTimer = time.After(5 * time.Second)
	quitting = true
}

func ircChannelCreate(name string) *channel {
	ch := &channel{
		name:      name,
		userLimit: -1,
		created:   time.Now(),
		userModes: make(map[*client]uint),
	}
	channels[ircToCanon(name)] = ch
	return ch
}

func ircChannelDestroyIfEmpty(ch *channel) {
	if len(ch.userModes) == 0 {
		delete(channels, ircToCanon(ch.name))
	}
}

func ircNotifyRoommates(c *client, message string) {
	targets := make(map[*client]bool)
	for _, ch := range channels {
		_, present := ch.userModes[c]
		if !present || 0 != ch.modes&ircChanModeQuiet {
			continue
		}
		for client := range ch.userModes {
			targets[client] = true
		}
	}

	for roommate := range targets {
		if roommate != c {
			roommate.send(message)
		}
	}
}

// --- Clients (continued) -----------------------------------------------------

func (c *client) printDebug(format string, args ...interface{}) {
	if debugMode {
		printDebug("(%s) %s", c.address, fmt.Sprintf(format, args...))
	}
}

func ircAppendClientModes(m uint, mode []byte) []byte {
	if 0 != m&ircUserModeInvisible {
		mode = append(mode, 'i')
	}
	if 0 != m&ircUserModeRxWallops {
		mode = append(mode, 'w')
	}
	if 0 != m&ircUserModeRestricted {
		mode = append(mode, 'r')
	}
	if 0 != m&ircUserModeOperator {
		mode = append(mode, 'o')
	}
	if 0 != m&ircUserModeRxServerNotices {
		mode = append(mode, 's')
	}
	return mode
}

func (c *client) getMode() string {
	var mode []byte
	if c.awayMessage != "" {
		mode = append(mode, 'a')
	}
	return string(ircAppendClientModes(c.mode, mode))
}

func (c *client) send(line string) {
	if c.conn == nil || c.closing {
		return
	}

	oldSendQLen := len(c.sendQ)

	// So far there's only one message tag we use, so we can do it simple;
	// note that a 1024-character limit applies to messages with tags on.
	if 0 != c.capsEnabled&ircCapServerTime {
		c.sendQ = time.Now().UTC().
			AppendFormat(c.sendQ, "@time=2006-01-02T15:04:05.000Z ")
	}

	bytes := []byte(line)
	if len(bytes) > ircMaxMessageLength {
		bytes = bytes[:ircMaxMessageLength]
	}

	c.printDebug("<- %s", bytes)

	// TODO: Kill the connection above some "SendQ" threshold (careful!)
	c.sendQ = append(c.sendQ, bytes...)
	c.sendQ = append(c.sendQ, "\r\n"...)
	c.flushSendQ()

	// Technically we haven't sent it yet but that's a minor detail
	c.nSentMessages++
	c.sentBytes += len(c.sendQ) - oldSendQLen
}

func (c *client) sendf(format string, a ...interface{}) {
	c.send(fmt.Sprintf(format, a...))
}

func (c *client) addToWhowas() {
	// Only keeping one entry for each nickname.
	// TODO: Make sure this list doesn't get too long, for example by
	// putting them in a linked list ordered by time.
	whowas[ircToCanon(c.nickname)] = newWhowasInfo(c)
}

func (c *client) nicknameOrStar() string {
	if c.nickname == "" {
		return "*"
	}
	return c.nickname
}

func (c *client) unregister(reason string) {
	if !c.registered {
		return
	}

	ircNotifyRoommates(c, fmt.Sprintf(":%s!%s@%s QUIT :%s",
		c.nickname, c.username, c.hostname, reason))

	// The QUIT message will take care of state on clients.
	for _, ch := range channels {
		delete(ch.userModes, c)
		ircChannelDestroyIfEmpty(ch)
	}

	c.addToWhowas()
	delete(users, ircToCanon(c.nickname))
	c.nickname = ""
	c.registered = false
}

// Close the connection and forget about the client.
func (c *client) kill(reason string) {
	if reason == "" {
		c.unregister("Client exited")
	} else {
		c.unregister(reason)
	}

	c.printDebug("client destroyed (%s)", reason)

	// Try to send a "close notify" alert if the TLS object is ready,
	// otherwise just tear down the transport.
	if c.conn != nil {
		_ = c.conn.Close()
	} else {
		_ = c.transport.Close()
	}

	c.cancelTimers()
	delete(clients, c)
}

// Tear down the client connection, trying to do so in a graceful manner.
func (c *client) closeLink(reason string) {
	// Let's just cut the connection, the client can try again later.
	// We also want to avoid accidentally writing to the socket before
	// address resolution has finished.
	if c.conn == nil {
		c.kill(reason)
		return
	}
	if c.closing {
		return
	}

	// We push an "ERROR" message to the write buffer and let the writer send
	// it, with some arbitrary timeout. The "closing" state makes sure
	// that a/ we ignore any successive messages, and b/ that the connection
	// is killed after the write buffer is transferred and emptied.
	// (Since we send this message, we don't need to call CloseWrite here.)
	c.sendf("ERROR :Closing link: %s[%s] (%s)",
		c.nicknameOrStar(), c.hostname /* TODO host IP? */, reason)
	c.closing = true

	c.unregister(reason)
	c.setKillTimer()
}

func (c *client) inMaskList(masks []string) bool {
	client := fmt.Sprintf("%s!%s@%s", c.nickname, c.username, c.hostname)
	for _, mask := range masks {
		if ircFnmatch(mask, client) {
			return true
		}
	}
	return false
}

func (c *client) getTLSCertFingerprint() string {
	if c.tls == nil {
		return ""
	}

	peerCerts := c.tls.ConnectionState().PeerCertificates
	if len(peerCerts) == 0 {
		return ""
	}

	hash := sha256.Sum256(peerCerts[0].Raw)
	return hex.EncodeToString(hash[:])
}

// --- Timers ------------------------------------------------------------------

// Free the resources of timers that haven't fired yet and for timers that are
// in between firing and being collected by the event loop, mark that the event
// should not be acted upon.
func (c *client) cancelTimers() {
	for _, timer := range []**time.Timer{
		&c.killTimer, &c.timeoutTimer, &c.pingTimer,
	} {
		if *timer != nil {
			(*timer).Stop()
			*timer = nil
		}
	}
}

// Arrange for a function to be called later from the main goroutine.
func (c *client) setTimer(timer **time.Timer, delay time.Duration, cb func()) {
	c.cancelTimers()

	var identityCapture *time.Timer
	identityCapture = time.AfterFunc(delay, func() {
		timers <- func() {
			// The timer might have been cancelled or even replaced.
			// When the client is killed, this will be nil.
			if *timer == identityCapture {
				cb()
			}
		}
	})

	*timer = identityCapture
}

func (c *client) setKillTimer() {
	c.setTimer(&c.killTimer, pingInterval, func() {
		c.kill("Timeout")
	})
}

func (c *client) setTimeoutTimer() {
	c.setTimer(&c.timeoutTimer, pingInterval, func() {
		c.closeLink(fmt.Sprintf("Ping timeout: >%d seconds",
			pingInterval/time.Second))
	})
}

func (c *client) setPingTimer() {
	c.setTimer(&c.pingTimer, pingInterval, func() {
		c.sendf("PING :%s", serverName)
		c.setTimeoutTimer()
	})
}

// --- IRC command handling ----------------------------------------------------

func (c *client) makeReply(id int, ap ...interface{}) string {
	s := fmt.Sprintf(":%s %03d %s ", serverName, id, c.nicknameOrStar())
	if reply, ok := catalog[id]; ok {
		return s + fmt.Sprintf(reply, ap...)
	}
	return s + fmt.Sprintf(defaultReplies[id], ap...)
}

// XXX: This way simple static analysis cannot typecheck the arguments, so we
// need to be careful.
func (c *client) sendReply(id int, args ...interface{}) {
	c.send(c.makeReply(id, args...))
}

// Send a space-separated list of words across as many replies as needed.
func (c *client) sendReplyVector(id int, items []string, args ...interface{}) {
	common := c.makeReply(id, args...)

	// We always send at least one message (there might be a client that
	// expects us to send this message at least once).
	if len(items) == 0 {
		items = append(items, "")
	}

	for len(items) > 0 {
		// If not even a single item fits in the limit (which may happen,
		// in theory) it just gets cropped. We could also skip it.
		reply := append([]byte(common), items[0]...)
		items = items[1:]

		// Append as many items as fits in a single message.
		for len(items) > 0 &&
			len(reply)+1+len(items[0]) <= ircMaxMessageLength {
			reply = append(reply, ' ')
			reply = append(reply, items[0]...)
			items = items[1:]
		}

		c.send(string(reply))
	}
}

func (c *client) sendMOTD() {
	if len(motd) == 0 {
		c.sendReply(ERR_NOMOTD)
		return
	}

	c.sendReply(RPL_MOTDSTART, serverName)
	for _, line := range motd {
		c.sendReply(RPL_MOTD, line)
	}
	c.sendReply(RPL_ENDOFMOTD)
}

func (c *client) sendLUSERS() {
	nUsers, nServices, nOpers, nUnknown := 0, 0, 0, 0
	for c := range clients {
		if c.registered {
			nUsers++
		} else {
			nUnknown++
		}
		if 0 != c.mode&ircUserModeOperator {
			nOpers++
		}
	}

	nChannels := 0
	for _, ch := range channels {
		if 0 != ch.modes&ircChanModeSecret {
			nChannels++
		}
	}

	c.sendReply(RPL_LUSERCLIENT, nUsers, nServices, 1 /* servers total */)
	if nOpers != 0 {
		c.sendReply(RPL_LUSEROP, nOpers)
	}
	if nUnknown != 0 {
		c.sendReply(RPL_LUSERUNKNOWN, nUnknown)
	}
	if nChannels != 0 {
		c.sendReply(RPL_LUSERCHANNELS, nChannels)
	}
	c.sendReply(RPL_LUSERME, nUsers+nServices+nUnknown, 0 /* peer servers */)
}

func ircIsThisMe(target string) bool {
	// Target servers can also be matched by their users
	if ircFnmatch(target, serverName) {
		return true
	}
	_, ok := users[ircToCanon(target)]
	return ok
}

func (c *client) sendISUPPORT() {
	// Only # channels, +e supported, +I supported, unlimited arguments to MODE.
	c.sendReply(RPL_ISUPPORT, fmt.Sprintf("CHANTYPES=# EXCEPTS INVEX MODES"+
		" TARGMAX=WHOIS:,LIST:,NAMES:,PRIVMSG:1,NOTICE:1,KICK:"+
		" NICKLEN=%d CHANNELLEN=%d", ircMaxNickname, ircMaxChannelName))
}

func (c *client) tryFinishRegistration() {
	if c.registered || c.capNegotiating {
		return
	}
	if c.nickname == "" || c.username == "" {
		return
	}

	c.registered = true
	c.sendReply(RPL_WELCOME, c.nickname, c.username, c.hostname)

	c.sendReply(RPL_YOURHOST, serverName, projectVersion)
	// The purpose of this message eludes me.
	c.sendReply(RPL_CREATED, started.Format("Mon, 02 Jan 2006"))
	c.sendReply(RPL_MYINFO, serverName, projectVersion,
		ircSupportedUserModes, ircSupportedChanModes)

	c.sendISUPPORT()
	c.sendLUSERS()
	c.sendMOTD()

	if mode := c.getMode(); mode != "" {
		c.sendf(":%s MODE %s :+%s", c.nickname, c.nickname, mode)
	}

	c.tlsCertFingerprint = c.getTLSCertFingerprint()
	if c.tlsCertFingerprint != "" {
		c.sendf(":%s NOTICE %s :Your TLS client certificate fingerprint is %s",
			serverName, c.nickname, c.tlsCertFingerprint)
	}

	delete(whowas, ircToCanon(c.nickname))
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

// IRCv3 capability negotiation. See http://ircv3.org for details.

type ircCapArgs struct {
	subcommand string   // the subcommand being processed
	fullParams string   // whole parameter string
	params     []string // split parameters
	target     string   // target parameter for replies
}

var ircCapTable = []struct {
	flag uint   // flag
	name string // name of the capability
}{
	{ircCapMultiPrefix, "multi-prefix"},
	{ircCapInviteNotify, "invite-notify"},
	{ircCapEchoMessage, "echo-message"},
	{ircCapUserhostInNames, "userhost-in-names"},
	{ircCapServerTime, "server-time"},
}

func (c *client) handleCAPLS(a *ircCapArgs) {
	if len(a.params) == 1 {
		if ver, err := strconv.ParseUint(a.params[0], 10, 32); err != nil {
			c.sendReply(ERR_INVALIDCAPCMD, a.subcommand,
				"Ignoring invalid protocol version number")
		} else {
			c.capVersion = uint(ver)
		}
	}

	c.capNegotiating = true
	c.sendf(":%s CAP %s LS :multi-prefix invite-notify echo-message"+
		" userhost-in-names server-time", serverName, a.target)
}

func (c *client) handleCAPLIST(a *ircCapArgs) {
	caps := []string{}
	for _, cap := range ircCapTable {
		if 0 != c.capsEnabled&cap.flag {
			caps = append(caps, cap.name)
		}
	}

	c.sendf(":%s CAP %s LIST :%s", serverName, a.target,
		strings.Join(caps, " "))
}

func ircDecodeCapability(name string) uint {
	for _, cap := range ircCapTable {
		if cap.name == name {
			return cap.flag
		}
	}
	return 0
}

func (c *client) handleCAPREQ(a *ircCapArgs) {
	c.capNegotiating = true

	newCaps := c.capsEnabled
	ok := true
	for _, param := range a.params {
		removing := false
		name := param
		if name[:1] == "-" {
			removing = true
			name = name[1:]
		}

		if cap := ircDecodeCapability(name); cap == 0 {
			ok = false
		} else if removing {
			newCaps &= ^cap
		} else {
			newCaps |= cap
		}
	}

	if ok {
		c.capsEnabled = newCaps
		c.sendf(":%s CAP %s ACK :%s", serverName, a.target, a.fullParams)
	} else {
		c.sendf(":%s CAP %s NAK :%s", serverName, a.target, a.fullParams)
	}
}

func (c *client) handleCAPACK(a *ircCapArgs) {
	if len(a.params) > 0 {
		c.sendReply(ERR_INVALIDCAPCMD, a.subcommand,
			"No acknowledgable capabilities supported")
	}
}

func (c *client) handleCAPEND(a *ircCapArgs) {
	c.capNegotiating = false
	c.tryFinishRegistration()
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

var ircCapHandlers = map[string]func(*client, *ircCapArgs){
	"LS":   (*client).handleCAPLS,
	"LIST": (*client).handleCAPLIST,
	"REQ":  (*client).handleCAPREQ,
	"ACK":  (*client).handleCAPACK,
	"END":  (*client).handleCAPEND,
}

// XXX: Maybe these also deserve to be methods for client?  They operate on
// global state, though.

func ircParseWEBIRCOptions(options string, out map[string]string) {
	for _, option := range strings.Split(options, " ") {
		if equal := strings.IndexByte(option, '='); equal < 0 {
			out[option] = ""
		} else {
			out[option[:equal]] = ircUnescapeMessageTag(option[equal+1:])
		}
	}
}

func ircHandleWEBIRC(msg *message, c *client) {
	if len(msg.params) < 4 {
		c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
		return
	}

	password, gateway, hostname := msg.params[0], msg.params[1], msg.params[2]
	if config["webirc_password"] != password {
		c.closeLink("Invalid WebIRC password")
		return
	}

	options := make(map[string]string)
	if len(msg.params) >= 5 {
		ircParseWEBIRCOptions(msg.params[4], options)
	}

	c.hostname = hostname
	c.port = "WebIRC-" + gateway
	c.address = net.JoinHostPort(hostname, c.port)

	// Note that this overrides the gateway's certificate, conditionally.
	fp, _ := options["certfp-sha-256"]
	if _, secure := options["secure"]; secure && ircIsValidFingerprint(fp) {
		c.tlsCertFingerprint = strings.ToLower(fp)
	}
}

func ircHandleCAP(msg *message, c *client) {
	if len(msg.params) < 1 {
		c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
		return
	}

	args := &ircCapArgs{
		target:     c.nicknameOrStar(),
		subcommand: msg.params[0],
		fullParams: "",
		params:     []string{},
	}

	if len(msg.params) > 1 {
		args.fullParams = msg.params[1]
		args.params = splitString(args.fullParams, " ", true)
	}

	if fn, ok := ircCapHandlers[ircToCanon(args.subcommand)]; !ok {
		c.sendReply(ERR_INVALIDCAPCMD, args.subcommand,
			"Invalid CAP subcommand")
	} else {
		fn(c, args)
	}
}

func ircHandlePASS(msg *message, c *client) {
	if c.registered {
		c.sendReply(ERR_ALREADYREGISTERED)
	} else if len(msg.params) < 1 {
		c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
	}

	// We have TLS client certificates for this purpose; ignoring.
}

func ircHandleNICK(msg *message, c *client) {
	if len(msg.params) < 1 {
		c.sendReply(ERR_NONICKNAMEGIVEN)
		return
	}

	nickname := msg.params[0]
	if !ircIsValidNickname(nickname) {
		c.sendReply(ERR_ERRONEOUSNICKNAME, nickname)
		return
	}

	nicknameCanon := ircToCanon(nickname)
	if client, ok := users[nicknameCanon]; ok && client != c {
		c.sendReply(ERR_NICKNAMEINUSE, nickname)
		return
	}

	if c.registered {
		c.addToWhowas()

		message := fmt.Sprintf(":%s!%s@%s NICK :%s",
			c.nickname, c.username, c.hostname, nickname)
		ircNotifyRoommates(c, message)
		c.send(message)
	}

	// Release the old nickname and allocate a new one.
	if c.nickname != "" {
		delete(users, ircToCanon(c.nickname))
	}

	c.nickname = nickname
	users[nicknameCanon] = c

	c.tryFinishRegistration()
}

func ircHandleUSER(msg *message, c *client) {
	if c.registered {
		c.sendReply(ERR_ALREADYREGISTERED)
		return
	}
	if len(msg.params) < 4 {
		c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
		return
	}

	username, mode, realname := msg.params[0], msg.params[1], msg.params[3]

	// Unfortunately, the protocol doesn't give us any means of rejecting it.
	if !ircIsValidUsername(username) {
		username = "*"
	}

	c.username = username
	c.realname = realname

	c.mode = 0
	if m, err := strconv.ParseUint(mode, 10, 32); err != nil {
		if 0 != m&4 {
			c.mode |= ircUserModeRxWallops
		}
		if 0 != m&8 {
			c.mode |= ircUserModeInvisible
		}
	}

	c.tryFinishRegistration()
}

func ircHandleUSERHOST(msg *message, c *client) {
	if len(msg.params) < 1 {
		c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
		return
	}

	var reply []byte
	for i := 0; i < 5 && i < len(msg.params); i++ {
		nick := msg.params[i]
		target := users[ircToCanon(nick)]
		if target == nil {
			continue
		}
		if i != 0 {
			reply = append(reply, ' ')
		}

		reply = append(reply, nick...)
		if 0 != target.mode&ircUserModeOperator {
			reply = append(reply, '*')
		}

		if target.awayMessage != "" {
			reply = append(reply, "=-"...)
		} else {
			reply = append(reply, "=+"...)
		}
		reply = append(reply, (target.username + "@" + target.hostname)...)
	}
	c.sendReply(RPL_USERHOST, string(reply))
}

func ircHandleLUSERS(msg *message, c *client) {
	if len(msg.params) > 1 && !ircIsThisMe(msg.params[1]) {
		c.sendReply(ERR_NOSUCHSERVER, msg.params[1])
		return
	}
	c.sendLUSERS()
}

func ircHandleMOTD(msg *message, c *client) {
	if len(msg.params) > 0 && !ircIsThisMe(msg.params[0]) {
		c.sendReply(ERR_NOSUCHSERVER, msg.params[1])
		return
	}
	c.sendMOTD()
}

func ircHandlePING(msg *message, c *client) {
	// XXX: The RFC is pretty incomprehensible about the exact usage.
	if len(msg.params) > 1 && !ircIsThisMe(msg.params[1]) {
		c.sendReply(ERR_NOSUCHSERVER, msg.params[1])
	} else if len(msg.params) < 1 {
		c.sendReply(ERR_NOORIGIN)
	} else {
		c.sendf(":%s PONG :%s", serverName, msg.params[0])
	}
}

func ircHandlePONG(msg *message, c *client) {
	// We are the only server, so we don't have to care too much.
	if len(msg.params) < 1 {
		c.sendReply(ERR_NOORIGIN)
		return
	}

	// Set a new timer to send another PING
	c.setPingTimer()
}

func ircHandleQUIT(msg *message, c *client) {
	reason := c.nickname
	if len(msg.params) > 0 {
		reason = msg.params[0]
	}

	c.closeLink("Quit: " + reason)
}

func ircHandleTIME(msg *message, c *client) {
	if len(msg.params) > 0 && !ircIsThisMe(msg.params[0]) {
		c.sendReply(ERR_NOSUCHSERVER, msg.params[0])
		return
	}

	c.sendReply(RPL_TIME, serverName,
		time.Now().Format("Mon Jan _2 2006 15:04:05"))
}

func ircHandleVERSION(msg *message, c *client) {
	if len(msg.params) > 0 && !ircIsThisMe(msg.params[0]) {
		c.sendReply(ERR_NOSUCHSERVER, msg.params[0])
		return
	}

	postVersion := 0
	if debugMode {
		postVersion = 1
	}

	c.sendReply(RPL_VERSION, projectVersion, postVersion, serverName,
		projectName+" "+projectVersion)
	c.sendISUPPORT()
}

func ircChannelMulticast(ch *channel, msg string, except *client) {
	for c := range ch.userModes {
		if c != except {
			c.send(msg)
		}
	}
}

func ircModifyMode(mask *uint, mode uint, add bool) bool {
	orig := *mask
	if add {
		*mask |= mode
	} else {
		*mask &= ^mode
	}
	return *mask != orig
}

func ircUpdateUserMode(c *client, newMode uint) {
	oldMode := c.mode
	c.mode = newMode

	added, removed := newMode & ^oldMode, oldMode & ^newMode

	var diff []byte
	if added != 0 {
		diff = append(diff, '+')
		diff = ircAppendClientModes(added, diff)
	}
	if removed != 0 {
		diff = append(diff, '-')
		diff = ircAppendClientModes(removed, diff)
	}

	if len(diff) > 0 {
		c.sendf(":%s MODE %s :%s", c.nickname, c.nickname, string(diff))
	}
}

func ircHandleUserModeChange(c *client, modeString string) {
	newMode := c.mode
	adding := true

	for _, flag := range modeString {
		switch flag {
		case '+':
			adding = true
		case '-':
			adding = false

		case 'a':
			// Ignore, the client should use AWAY.
		case 'i':
			ircModifyMode(&newMode, ircUserModeInvisible, adding)
		case 'w':
			ircModifyMode(&newMode, ircUserModeRxWallops, adding)
		case 'r':
			// It's not possible ot un-restrict yourself.
			if adding {
				newMode |= ircUserModeRestricted
			}
		case 'o':
			if !adding {
				newMode &= ^ircUserModeOperator
			} else if operators[c.tlsCertFingerprint] {
				newMode |= ircUserModeOperator
			} else {
				c.sendf(":%s NOTICE %s :Either you're not using an TLS"+
					" client certificate, or the fingerprint doesn't match",
					serverName, c.nickname)
			}
		case 's':
			ircModifyMode(&newMode, ircUserModeRxServerNotices, adding)
		default:
			c.sendReply(ERR_UMODEUNKNOWNFLAG)
			return
		}
	}
	ircUpdateUserMode(c, newMode)
}

func ircSendChannelList(c *client, channelName string, list []string,
	reply, endReply int) {
	for _, line := range list {
		c.sendReply(reply, channelName, line)
	}
	c.sendReply(endReply, channelName)
}

func ircCheckExpandUserMask(mask string) string {
	var result []byte
	result = append(result, mask...)

	// Make sure it is a complete mask.
	if bytes.IndexByte(result, '!') < 0 {
		result = append(result, "!*"...)
	}
	if bytes.IndexByte(result, '@') < 0 {
		result = append(result, "@*"...)
	}

	// And validate whatever the result is.
	s := string(result)
	if !ircIsValidUserMask(s) {
		return ""
	}

	return s
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

// Channel MODE command handling. This is by far the worst command to implement
// from the whole RFC; don't blame me if it doesn't work exactly as expected.

type modeProcessor struct {
	params []string // mode string parameters

	c       *client  // who does the changes
	ch      *channel // the channel we're modifying
	present bool     // c present on ch
	modes   uint     // channel user modes

	adding   bool // currently adding modes
	modeChar byte // currently processed mode char

	added   []byte  // added modes
	removed []byte  // removed modes
	output  *[]byte // "added" or "removed"

	addedParams   []string  // params for added modes
	removedParams []string  // params for removed modes
	outputParams  *[]string // "addedParams" or "removedParams"
}

func (mp *modeProcessor) nextParam() string {
	if len(mp.params) == 0 {
		return ""
	}

	param := mp.params[0]
	mp.params = mp.params[1:]
	return param
}

func (mp *modeProcessor) checkOperator() bool {
	if (mp.present && 0 != mp.modes&ircChanModeOperator) ||
		0 != mp.c.mode&ircUserModeOperator {
		return true
	}

	mp.c.sendReply(ERR_CHANOPRIVSNEEDED, mp.ch.name)
	return false
}

func (mp *modeProcessor) doUser(mode uint) {
	target := mp.nextParam()
	if !mp.checkOperator() || target == "" {
		return
	}

	if client := users[ircToCanon(target)]; client == nil {
		mp.c.sendReply(ERR_NOSUCHNICK, target)
	} else if modes, present := mp.ch.userModes[client]; !present {
		mp.c.sendReply(ERR_USERNOTINCHANNEL, target, mp.ch.name)
	} else if ircModifyMode(&modes, mode, mp.adding) {
		mp.ch.userModes[client] = modes
		*mp.output = append(*mp.output, mp.modeChar)
		*mp.outputParams = append(*mp.outputParams, client.nickname)
	}
}

func (mp *modeProcessor) doChan(mode uint) bool {
	if !mp.checkOperator() || !ircModifyMode(&mp.ch.modes, mode, mp.adding) {
		return false
	}
	*mp.output = append(*mp.output, mp.modeChar)
	return true
}

func (mp *modeProcessor) doChanRemove(modeChar byte, mode uint) {
	if mp.adding && ircModifyMode(&mp.ch.modes, mode, false) {
		mp.removed = append(mp.removed, modeChar)
	}
}

func (mp *modeProcessor) doList(list *[]string, listMsg, endMsg int) {
	target := mp.nextParam()
	if target == "" {
		if mp.adding {
			ircSendChannelList(mp.c, mp.ch.name, *list, listMsg, endMsg)
		}
		return
	}

	if !mp.checkOperator() {
		return
	}

	mask := ircCheckExpandUserMask(target)
	if mask == "" {
		return
	}

	var i int
	for i = 0; i < len(*list); i++ {
		if ircEqual((*list)[i], mask) {
			break
		}
	}

	found := i < len(*list)
	if found != mp.adding {
		if mp.adding {
			*list = append(*list, mask)
		} else {
			*list = append((*list)[:i], (*list)[i+1:]...)
		}

		*mp.output = append(*mp.output, mp.modeChar)
		*mp.outputParams = append(*mp.outputParams, mask)
	}
}

func (mp *modeProcessor) doKey() {
	target := mp.nextParam()
	if !mp.checkOperator() || target == "" {
		return
	}

	if !mp.adding {
		if mp.ch.key == "" || !ircEqual(target, mp.ch.key) {
			return
		}

		mp.removed = append(mp.removed, mp.modeChar)
		mp.removedParams = append(mp.removedParams, mp.ch.key)
		mp.ch.key = ""
	} else if !ircIsValidKey(target) {
		// TODO: We should notify the user somehow.
		return
	} else if mp.ch.key != "" {
		mp.c.sendReply(ERR_KEYSET, mp.ch.name)
	} else {
		mp.ch.key = target
		mp.added = append(mp.added, mp.modeChar)
		mp.addedParams = append(mp.addedParams, mp.ch.key)
	}
}

func (mp *modeProcessor) doLimit() {
	if !mp.checkOperator() {
		return
	}

	if !mp.adding {
		if mp.ch.userLimit == -1 {
			return
		}

		mp.ch.userLimit = -1
		mp.removed = append(mp.removed, mp.modeChar)
	} else if target := mp.nextParam(); target != "" {
		if x, err := strconv.ParseInt(target, 10, 32); err == nil && x > 0 {
			mp.ch.userLimit = int(x)
			mp.added = append(mp.added, mp.modeChar)
			mp.addedParams = append(mp.addedParams, target)
		}
	}
}

func (mp *modeProcessor) step(modeChar byte) bool {
	mp.modeChar = modeChar
	switch mp.modeChar {
	case '+':
		mp.adding = true
		mp.output = &mp.added
		mp.outputParams = &mp.addedParams
	case '-':
		mp.adding = false
		mp.output = &mp.removed
		mp.outputParams = &mp.removedParams

	case 'o':
		mp.doUser(ircChanModeOperator)
	case 'v':
		mp.doUser(ircChanModeVoice)

	case 'i':
		mp.doChan(ircChanModeInviteOnly)
	case 'm':
		mp.doChan(ircChanModeModerated)
	case 'n':
		mp.doChan(ircChanModeNoOutsideMsgs)
	case 'q':
		mp.doChan(ircChanModeQuiet)
	case 't':
		mp.doChan(ircChanModeProtectedTopic)

	case 'p':
		if mp.doChan(ircChanModePrivate) {
			mp.doChanRemove('s', ircChanModeSecret)
		}
	case 's':
		if mp.doChan(ircChanModeSecret) {
			mp.doChanRemove('p', ircChanModePrivate)
		}

	case 'b':
		mp.doList(&mp.ch.banList, RPL_BANLIST, RPL_ENDOFBANLIST)
	case 'e':
		mp.doList(&mp.ch.banList, RPL_EXCEPTLIST, RPL_ENDOFEXCEPTLIST)
	case 'I':
		mp.doList(&mp.ch.banList, RPL_INVITELIST, RPL_ENDOFINVITELIST)

	case 'k':
		mp.doKey()
	case 'l':
		mp.doLimit()

	default:
		// It's not safe to continue, results could be undesired.
		mp.c.sendReply(ERR_UNKNOWNMODE, modeChar, mp.ch.name)
		return false
	}
	return true
}

func ircHandleChanModeChange(c *client, ch *channel, params []string) {
	modes, present := ch.userModes[c]
	mp := &modeProcessor{
		c:       c,
		ch:      ch,
		present: present,
		modes:   modes,
		params:  params,
	}

Outer:
	for {
		modeString := mp.nextParam()
		if modeString == "" {
			break
		}

		mp.step('+')
		for _, modeChar := range []byte(modeString) {
			if !mp.step(modeChar) {
				break Outer
			}
		}
	}

	// TODO: Limit to three changes with parameter per command.
	if len(mp.added) > 0 || len(mp.removed) > 0 {
		buf := []byte(fmt.Sprintf(":%s!%s@%s MODE %s ",
			mp.c.nickname, mp.c.username, mp.c.hostname, mp.ch.name))
		if len(mp.added) > 0 {
			buf = append(buf, '+')
			buf = append(buf, mp.added...)
		}
		if len(mp.removed) > 0 {
			buf = append(buf, '-')
			buf = append(buf, mp.removed...)
		}
		for _, param := range mp.addedParams {
			buf = append(buf, ' ')
			buf = append(buf, param...)
		}
		for _, param := range mp.removedParams {
			buf = append(buf, ' ')
			buf = append(buf, param...)
		}
		ircChannelMulticast(mp.ch, string(buf), nil)
	}
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

func ircHandleMODE(msg *message, c *client) {
	if len(msg.params) < 1 {
		c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
		return
	}

	target := msg.params[0]
	client := users[ircToCanon(target)]
	ch := channels[ircToCanon(target)]

	if client != nil {
		if !ircEqual(target, c.nickname) {
			c.sendReply(ERR_USERSDONTMATCH)
			return
		}

		if len(msg.params) < 2 {
			c.sendReply(RPL_UMODEIS, c.getMode())
		} else {
			ircHandleUserModeChange(c, msg.params[1])
		}
	} else if ch != nil {
		if len(msg.params) < 2 {
			_, present := ch.userModes[c]
			c.sendReply(RPL_CHANNELMODEIS, target, ch.getMode(present))
			c.sendReply(RPL_CREATIONTIME, target, ch.created.Unix())
		} else {
			ircHandleChanModeChange(c, ch, msg.params[1:])
		}
	} else {
		c.sendReply(ERR_NOSUCHNICK, target)
	}
}

func ircHandleUserMessage(msg *message, c *client,
	command string, allowAwayReply bool) {
	if len(msg.params) < 1 {
		c.sendReply(ERR_NORECIPIENT, msg.command)
		return
	}
	if len(msg.params) < 2 || msg.params[1] == "" {
		c.sendReply(ERR_NOTEXTTOSEND)
		return
	}

	target, text := msg.params[0], msg.params[1]
	message := fmt.Sprintf(":%s!%s@%s %s %s :%s",
		c.nickname, c.username, c.hostname, command, target, text)

	if client := users[ircToCanon(target)]; client != nil {
		client.send(message)
		if allowAwayReply && client.awayMessage != "" {
			c.sendReply(RPL_AWAY, target, client.awayMessage)
		}

		// Acknowledging a message from the client to itself would be silly.
		if client != c && (0 != c.capsEnabled&ircCapEchoMessage) {
			c.send(message)
		}
	} else if ch := channels[ircToCanon(target)]; ch != nil {
		modes, present := ch.userModes[c]

		outsider := !present && 0 != ch.modes&ircChanModeNoOutsideMsgs
		moderated := 0 != ch.modes&ircChanModeModerated &&
			0 == modes&(ircChanModeVoice|ircChanModeOperator)
		banned := c.inMaskList(ch.banList) && !c.inMaskList(ch.exceptionList)

		if outsider || moderated || banned {
			c.sendReply(ERR_CANNOTSENDTOCHAN, target)
			return
		}

		except := c
		if 0 != c.capsEnabled&ircCapEchoMessage {
			except = nil
		}

		ircChannelMulticast(ch, message, except)
	} else {
		c.sendReply(ERR_NOSUCHNICK, target)
	}
}

func ircHandlePRIVMSG(msg *message, c *client) {
	ircHandleUserMessage(msg, c, "PRIVMSG", true /* allowAwayReply */)
	c.lastActive = time.Now()
}

func ircHandleNOTICE(msg *message, c *client) {
	ircHandleUserMessage(msg, c, "NOTICE", false /* allowAwayReply */)
}

func ircHandleLIST(msg *message, c *client) {
	if len(msg.params) > 1 && !ircIsThisMe(msg.params[1]) {
		c.sendReply(ERR_NOSUCHSERVER, msg.params[1])
		return
	}

	// XXX: Maybe we should skip ircUserModeInvisible from user counts.
	if len(msg.params) == 0 {
		for _, ch := range channels {
			if _, present := ch.userModes[c]; present ||
				0 == ch.modes&(ircChanModePrivate|ircChanModeSecret) {
				c.sendReply(RPL_LIST, ch.name, len(ch.userModes), ch.topic)
			}
		}
	} else {
		for _, target := range splitString(msg.params[0], ",", true) {
			if ch := channels[ircToCanon(target)]; ch != nil &&
				0 == ch.modes&ircChanModeSecret {
				c.sendReply(RPL_LIST, ch.name, len(ch.userModes), ch.topic)
			}
		}
	}
	c.sendReply(RPL_LISTEND)
}

func ircAppendPrefixes(c *client, modes uint, buf []byte) []byte {
	var all []byte
	if 0 != modes&ircChanModeOperator {
		all = append(all, '@')
	}
	if 0 != modes&ircChanModeVoice {
		all = append(all, '+')
	}

	if len(all) > 0 {
		if 0 != c.capsEnabled&ircCapMultiPrefix {
			buf = append(buf, all...)
		} else {
			buf = append(buf, all[0])
		}
	}
	return buf
}

func ircMakeRPLNAMREPLYItem(c, target *client, modes uint) string {
	result := string(ircAppendPrefixes(c, modes, nil)) + target.nickname
	if 0 != c.capsEnabled&ircCapUserhostInNames {
		result += fmt.Sprintf("!%s@%s", target.username, target.hostname)
	}
	return result
}

func ircSendRPLNAMREPLY(c *client, ch *channel, usedNicks map[*client]bool) {
	kind := '='
	if 0 != ch.modes&ircChanModeSecret {
		kind = '@'
	} else if 0 != ch.modes&ircChanModePrivate {
		kind = '*'
	}

	_, present := ch.userModes[c]

	var nicks []string
	for client, modes := range ch.userModes {
		if !present && 0 != client.mode&ircUserModeInvisible {
			continue
		}
		if usedNicks != nil {
			usedNicks[client] = true
		}
		nicks = append(nicks, ircMakeRPLNAMREPLYItem(c, client, modes))
	}
	c.sendReplyVector(RPL_NAMREPLY, nicks, kind, ch.name, "")
}

func ircSendDisassociatedNames(c *client, usedNicks map[*client]bool) {
	var nicks []string
	for _, client := range users {
		if 0 == client.mode&ircUserModeInvisible && !usedNicks[client] {
			nicks = append(nicks, ircMakeRPLNAMREPLYItem(c, client, 0))
		}
	}
	if len(nicks) > 0 {
		c.sendReplyVector(RPL_NAMREPLY, nicks, '*', "*", "")
	}
}

func ircHandleNAMES(msg *message, c *client) {
	if len(msg.params) > 1 && !ircIsThisMe(msg.params[1]) {
		c.sendReply(ERR_NOSUCHSERVER, msg.params[1])
		return
	}

	if len(msg.params) == 0 {
		usedNicks := make(map[*client]bool)
		for _, ch := range channels {
			if _, present := ch.userModes[c]; present ||
				0 == ch.modes&(ircChanModePrivate|ircChanModeSecret) {
				ircSendRPLNAMREPLY(c, ch, usedNicks)
			}
		}

		// Also send all visible users we haven't listed yet.
		ircSendDisassociatedNames(c, usedNicks)
		c.sendReply(RPL_ENDOFNAMES, "*")
	} else {
		for _, target := range splitString(msg.params[0], ",", true) {
			if ch := channels[ircToCanon(target)]; ch == nil {
			} else if _, present := ch.userModes[c]; present ||
				0 == ch.modes&ircChanModeSecret {
				ircSendRPLNAMREPLY(c, ch, nil)
				c.sendReply(RPL_ENDOFNAMES, target)
			}
		}
	}
}

func ircSendRPLWHOREPLY(c *client, ch *channel, target *client) {
	var chars []byte
	if target.awayMessage != "" {
		chars = append(chars, 'G')
	} else {
		chars = append(chars, 'H')
	}

	if 0 != target.mode&ircUserModeOperator {
		chars = append(chars, '*')
	}

	channelName := "*"
	if ch != nil {
		channelName = ch.name
		if modes, present := ch.userModes[target]; present {
			chars = ircAppendPrefixes(c, modes, chars)
		}
	}

	c.sendReply(RPL_WHOREPLY, channelName,
		target.username, target.hostname, serverName,
		target.nickname, string(chars), 0 /* hop count */, target.realname)
}

func ircMatchSendRPLWHOREPLY(c, target *client, mask string) {
	isRoommate := false
	for _, ch := range channels {
		_, presentClient := ch.userModes[c]
		_, presentTarget := ch.userModes[target]
		if presentClient && presentTarget {
			isRoommate = true
			break
		}
	}
	if !isRoommate && 0 != target.mode&ircUserModeInvisible {
		return
	}

	if !ircFnmatch(mask, target.hostname) &&
		!ircFnmatch(mask, target.nickname) &&
		!ircFnmatch(mask, target.realname) &&
		!ircFnmatch(mask, serverName) {
		return
	}

	// Try to find a channel they're on that's visible to us.
	var userCh *channel
	for _, ch := range channels {
		_, presentClient := ch.userModes[c]
		_, presentTarget := ch.userModes[target]
		if presentTarget && (presentClient ||
			0 == ch.modes&(ircChanModePrivate|ircChanModeSecret)) {
			userCh = ch
			break
		}
	}
	ircSendRPLWHOREPLY(c, userCh, target)
}

func ircHandleWHO(msg *message, c *client) {
	onlyOps := len(msg.params) > 1 && msg.params[1] == "o"

	shownMask, usedMask := "*", "*"
	if len(msg.params) > 0 {
		shownMask = msg.params[0]
		if shownMask != "0" {
			usedMask = shownMask
		}
	}

	if ch := channels[ircToCanon(usedMask)]; ch != nil {
		_, present := ch.userModes[c]
		if present || 0 == ch.modes&ircChanModeSecret {
			for client := range ch.userModes {
				if (present || 0 == client.mode&ircUserModeInvisible) &&
					(!onlyOps || 0 != client.mode&ircUserModeOperator) {
					ircSendRPLWHOREPLY(c, ch, client)
				}
			}
		}
	} else {
		for _, client := range users {
			if !onlyOps || 0 != client.mode&ircUserModeOperator {
				ircMatchSendRPLWHOREPLY(c, client, usedMask)
			}
		}
	}
	c.sendReply(RPL_ENDOFWHO, shownMask)
}

func ircSendWHOISReply(c, target *client) {
	nick := target.nickname
	c.sendReply(RPL_WHOISUSER, nick,
		target.username, target.hostname, target.realname)
	c.sendReply(RPL_WHOISSERVER, nick, serverName, config["server_info"])
	if 0 != target.mode&ircUserModeOperator {
		c.sendReply(RPL_WHOISOPERATOR, nick)
	}
	c.sendReply(RPL_WHOISIDLE, nick,
		time.Now().Sub(target.lastActive)/time.Second)
	if target.awayMessage != "" {
		c.sendReply(RPL_AWAY, nick, target.awayMessage)
	}

	var chans []string
	for _, ch := range channels {
		_, presentClient := ch.userModes[c]
		modes, presentTarget := ch.userModes[target]
		if presentTarget && (presentClient ||
			0 == ch.modes&(ircChanModePrivate|ircChanModeSecret)) {
			// TODO: Deduplicate, ircAppendPrefixes just also cuts prefixes.
			var all []byte
			if 0 != modes&ircChanModeOperator {
				all = append(all, '@')
			}
			if 0 != modes&ircChanModeVoice {
				all = append(all, '+')
			}
			chans = append(chans, string(all)+ch.name)
		}
	}
	c.sendReplyVector(RPL_WHOISCHANNELS, chans, nick, "")
	c.sendReply(RPL_ENDOFWHOIS, nick)
}

func ircHandleWHOIS(msg *message, c *client) {
	if len(msg.params) < 1 {
		c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
		return
	}
	if len(msg.params) > 1 && !ircIsThisMe(msg.params[0]) {
		c.sendReply(ERR_NOSUCHSERVER, msg.params[0])
		return
	}

	masksStr := msg.params[0]
	if len(msg.params) > 1 {
		masksStr = msg.params[1]
	}

	for _, mask := range splitString(masksStr, ",", true /* ignoreEmpty */) {
		if strings.IndexAny(mask, "*?") < 0 {
			if target := users[ircToCanon(mask)]; target == nil {
				c.sendReply(ERR_NOSUCHNICK, mask)
			} else {
				ircSendWHOISReply(c, target)
			}
		} else {
			found := false
			for _, target := range users {
				if ircFnmatch(mask, target.nickname) {
					ircSendWHOISReply(c, target)
					found = true
				}
			}
			if !found {
				c.sendReply(ERR_NOSUCHNICK, mask)
			}
		}
	}
}

func ircHandleWHOWAS(msg *message, c *client) {
	if len(msg.params) < 1 {
		c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
		return
	}
	if len(msg.params) > 2 && !ircIsThisMe(msg.params[2]) {
		c.sendReply(ERR_NOSUCHSERVER, msg.params[2])
		return
	}
	// The "count" parameter is ignored, we only store one entry for a nick.

	for _, nick := range splitString(msg.params[0], ",", true) {
		if info := whowas[ircToCanon(nick)]; info == nil {
			c.sendReply(ERR_WASNOSUCHNICK, nick)
		} else {
			c.sendReply(RPL_WHOWASUSER, nick,
				info.username, info.hostname, info.realname)
			c.sendReply(RPL_WHOISSERVER, nick,
				serverName, config["server_info"])
		}
		c.sendReply(RPL_ENDOFWHOWAS, nick)
	}
}

func ircSendRPLTOPIC(c *client, ch *channel) {
	if ch.topic == "" {
		c.sendReply(RPL_NOTOPIC, ch.name)
	} else {
		c.sendReply(RPL_TOPIC, ch.name, ch.topic)
		c.sendReply(RPL_TOPICWHOTIME,
			ch.name, ch.topicWho, ch.topicTime.Unix())
	}
}

func ircHandleTOPIC(msg *message, c *client) {
	if len(msg.params) < 1 {
		c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
		return
	}

	channelName := msg.params[0]
	ch := channels[ircToCanon(channelName)]
	if ch == nil {
		c.sendReply(ERR_NOSUCHCHANNEL, channelName)
		return
	}

	if len(msg.params) < 2 {
		ircSendRPLTOPIC(c, ch)
		return
	}

	modes, present := ch.userModes[c]
	if !present {
		c.sendReply(ERR_NOTONCHANNEL, channelName)
		return
	}

	if 0 != ch.modes&ircChanModeProtectedTopic &&
		0 == modes&ircChanModeOperator {
		c.sendReply(ERR_CHANOPRIVSNEEDED, channelName)
		return
	}

	ch.topic = msg.params[1]
	ch.topicWho = fmt.Sprintf("%s@%s@%s", c.nickname, c.username, c.hostname)
	ch.topicTime = time.Now()

	message := fmt.Sprintf(":%s!%s@%s TOPIC %s :%s",
		c.nickname, c.username, c.hostname, channelName, ch.topic)
	ircChannelMulticast(ch, message, nil)
}

func ircTryPart(c *client, channelName string, reason string) {
	if reason == "" {
		reason = c.nickname
	}

	ch := channels[ircToCanon(channelName)]
	if ch == nil {
		c.sendReply(ERR_NOSUCHCHANNEL, channelName)
		return
	}

	if _, present := ch.userModes[c]; !present {
		c.sendReply(ERR_NOTONCHANNEL, channelName)
		return
	}

	message := fmt.Sprintf(":%s@%s@%s PART %s :%s",
		c.nickname, c.username, c.hostname, channelName, reason)
	if 0 == ch.modes&ircChanModeQuiet {
		ircChannelMulticast(ch, message, nil)
	} else {
		c.send(message)
	}

	delete(ch.userModes, c)
	ircChannelDestroyIfEmpty(ch)
}

func ircPartAllChannels(c *client) {
	for _, ch := range channels {
		if _, present := ch.userModes[c]; present {
			ircTryPart(c, ch.name, "")
		}
	}
}

func ircHandlePART(msg *message, c *client) {
	if len(msg.params) < 1 {
		c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
		return
	}

	reason := ""
	if len(msg.params) > 1 {
		reason = msg.params[1]
	}

	for _, channelName := range splitString(msg.params[0], ",", true) {
		ircTryPart(c, channelName, reason)
	}
}

func ircTryKick(c *client, channelName, nick, reason string) {
	ch := channels[ircToCanon(channelName)]
	if ch == nil {
		c.sendReply(ERR_NOSUCHCHANNEL, channelName)
		return
	}

	if modes, present := ch.userModes[c]; !present {
		c.sendReply(ERR_NOTONCHANNEL, channelName)
		return
	} else if 0 == modes&ircChanModeOperator {
		c.sendReply(ERR_CHANOPRIVSNEEDED, channelName)
		return
	}

	client := users[ircToCanon(nick)]
	if _, present := ch.userModes[client]; client == nil || !present {
		c.sendReply(ERR_USERNOTINCHANNEL, nick, channelName)
		return
	}

	message := fmt.Sprintf(":%s@%s@%s KICK %s %s :%s",
		c.nickname, c.username, c.hostname, channelName, nick, reason)
	if 0 == ch.modes&ircChanModeQuiet {
		ircChannelMulticast(ch, message, nil)
	} else {
		c.send(message)
	}

	delete(ch.userModes, client)
	ircChannelDestroyIfEmpty(ch)
}

func ircHandleKICK(msg *message, c *client) {
	if len(msg.params) < 2 {
		c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
		return
	}

	reason := c.nickname
	if len(msg.params) > 2 {
		reason = msg.params[2]
	}

	targetChannels := splitString(msg.params[0], ",", true)
	targetUsers := splitString(msg.params[1], ",", true)

	if len(channels) == 1 {
		for i := 0; i < len(targetUsers); i++ {
			ircTryKick(c, targetChannels[0], targetUsers[i], reason)
		}
	} else {
		for i := 0; i < len(channels) && i < len(targetUsers); i++ {
			ircTryKick(c, targetChannels[i], targetUsers[i], reason)
		}
	}
}

func ircSendInviteNotifications(ch *channel, c, target *client) {
	for client := range ch.userModes {
		if client != target && 0 != client.capsEnabled&ircCapInviteNotify {
			client.sendf(":%s!%s@%s INVITE %s %s",
				c.nickname, c.username, c.hostname, target.nickname, ch.name)
		}
	}
}

func ircHandleINVITE(msg *message, c *client) {
	if len(msg.params) < 2 {
		c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
		return
	}

	target, channelName := msg.params[0], msg.params[1]
	client := users[ircToCanon(target)]
	if client == nil {
		c.sendReply(ERR_NOSUCHNICK, target)
		return
	}

	if ch := channels[ircToCanon(channelName)]; ch != nil {
		invitingModes, invitingPresent := ch.userModes[c]
		if !invitingPresent {
			c.sendReply(ERR_NOTONCHANNEL, channelName)
			return
		}
		if _, present := ch.userModes[client]; present {
			c.sendReply(ERR_USERONCHANNEL, target, channelName)
			return
		}

		if 0 != invitingModes&ircChanModeOperator {
			client.invites[ircToCanon(channelName)] = true
		} else if 0 != ch.modes&ircChanModeInviteOnly {
			c.sendReply(ERR_CHANOPRIVSNEEDED, channelName)
			return
		}

		// It's not specified when and how we should send out invite-notify.
		if 0 != ch.modes&ircChanModeInviteOnly {
			ircSendInviteNotifications(ch, c, client)
		}
	}

	client.sendf(":%s!%s@%s INVITE %s %s",
		c.nickname, c.username, c.hostname, client.nickname, channelName)
	if client.awayMessage != "" {
		c.sendReply(RPL_AWAY, client.nickname, client.awayMessage)
	}
	c.sendReply(RPL_INVITING, client.nickname, channelName)
}

func ircTryJoin(c *client, channelName, key string) {
	ch := channels[ircToCanon(channelName)]
	var userMode uint
	if ch == nil {
		if !ircIsValidChannelName(channelName) {
			c.sendReply(ERR_BADCHANMASK, channelName)
			return
		}
		ch = ircChannelCreate(channelName)
		userMode = ircChanModeOperator
	} else if _, present := ch.userModes[c]; present {
		return
	}

	_, invitedByChanop := c.invites[ircToCanon(channelName)]
	if 0 != ch.modes&ircChanModeInviteOnly && c.inMaskList(ch.inviteList) &&
		!invitedByChanop {
		c.sendReply(ERR_INVITEONLYCHAN, channelName)
		return
	}
	if ch.key != "" && (key == "" || key != ch.key) {
		c.sendReply(ERR_BADCHANNELKEY, channelName)
		return
	}
	if ch.userLimit != -1 && len(ch.userModes) >= ch.userLimit {
		c.sendReply(ERR_CHANNELISFULL, channelName)
		return
	}
	if c.inMaskList(ch.banList) && !c.inMaskList(ch.exceptionList) &&
		!invitedByChanop {
		c.sendReply(ERR_BANNEDFROMCHAN, channelName)
		return
	}

	// Destroy any invitation as there's no other way to get rid of it.
	delete(c.invites, ircToCanon(channelName))

	ch.userModes[c] = userMode

	message := fmt.Sprintf(":%s!%s@%s JOIN %s",
		c.nickname, c.username, c.hostname, channelName)
	if 0 == ch.modes&ircChanModeQuiet {
		ircChannelMulticast(ch, message, nil)
	} else {
		c.send(message)
	}

	ircSendRPLTOPIC(c, ch)
	ircSendRPLNAMREPLY(c, ch, nil)
	c.sendReply(RPL_ENDOFNAMES, ch.name)
}

func ircHandleJOIN(msg *message, c *client) {
	if len(msg.params) < 1 {
		c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
		return
	}

	if msg.params[0] == "0" {
		ircPartAllChannels(c)
		return
	}

	targetChannels := splitString(msg.params[0], ",", true)

	var keys []string
	if len(msg.params) > 1 {
		keys = splitString(msg.params[1], ",", true)
	}

	for i, name := range targetChannels {
		key := ""
		if i < len(keys) {
			key = keys[i]
		}
		ircTryJoin(c, name, key)
	}
}

func ircHandleSUMMON(msg *message, c *client) {
	c.sendReply(ERR_SUMMONDISABLED)
}

func ircHandleUSERS(msg *message, c *client) {
	c.sendReply(ERR_USERSDISABLED)
}

func ircHandleAWAY(msg *message, c *client) {
	if len(msg.params) < 1 {
		c.awayMessage = ""
		c.sendReply(RPL_UNAWAY)
	} else {
		c.awayMessage = msg.params[0]
		c.sendReply(RPL_NOWAWAY)
	}
}

func ircHandleISON(msg *message, c *client) {
	if len(msg.params) < 1 {
		c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
		return
	}

	var on []string
	for _, nick := range msg.params {
		if client := users[ircToCanon(nick)]; client != nil {
			on = append(on, nick)
		}
	}
	c.sendReply(RPL_ISON, strings.Join(on, " "))
}

func ircHandleADMIN(msg *message, c *client) {
	if len(msg.params) > 0 && !ircIsThisMe(msg.params[0]) {
		c.sendReply(ERR_NOSUCHSERVER, msg.params[0])
		return
	}
	c.sendReply(ERR_NOADMININFO, serverName)
}

func ircHandleStatsLinks(c *client, msg *message) {
	// There is only an "l" query in RFC 2812 but we cannot link,
	// so instead we provide the "L" query giving information for all users.
	filter := ""
	if len(msg.params) > 1 {
		filter = msg.params[1]
	}

	for _, client := range users {
		if filter != "" && !ircEqual(client.nickname, filter) {
			continue
		}
		c.sendReply(RPL_STATSLINKINFO,
			client.address,    // linkname
			len(client.sendQ), // sendq
			client.nSentMessages, client.sentBytes/1024,
			client.nReceivedMessages, client.receivedBytes/1024,
			time.Now().Sub(client.opened)/time.Second)
	}
}

func ircHandleStatsCommands(c *client) {
	for name, handler := range ircHandlers {
		if handler.nReceived > 0 {
			c.sendReply(RPL_STATSCOMMANDS, name,
				handler.nReceived, handler.bytesReceived, 0)
		}
	}
}

// We need to do it this way because of an initialization loop concerning
// ircHandlers. Workaround proposed by rsc in go #1817.
var ircHandleStatsCommandsIndirect func(c *client)

func init() {
	ircHandleStatsCommandsIndirect = ircHandleStatsCommands
}

func ircHandleStatsUptime(c *client) {
	uptime := time.Now().Sub(started) / time.Second

	days := uptime / 60 / 60 / 24
	hours := (uptime % (60 * 60 * 24)) / 60 / 60
	mins := (uptime % (60 * 60)) / 60
	secs := uptime % 60

	c.sendReply(RPL_STATSUPTIME, days, hours, mins, secs)
}

func ircHandleSTATS(msg *message, c *client) {
	var query byte
	if len(msg.params) > 0 && len(msg.params[0]) > 0 {
		query = msg.params[0][0]
	}

	if len(msg.params) > 1 && !ircIsThisMe(msg.params[1]) {
		c.sendReply(ERR_NOSUCHSERVER, msg.params[0])
		return
	}
	if 0 == c.mode&ircUserModeOperator {
		c.sendReply(ERR_NOPRIVILEGES)
		return
	}

	switch query {
	case 'L':
		ircHandleStatsLinks(c, msg)
	case 'm':
		ircHandleStatsCommandsIndirect(c)
	case 'u':
		ircHandleStatsUptime(c)
	}
	c.sendReply(RPL_ENDOFSTATS, query)
}

func ircHandleLINKS(msg *message, c *client) {
	if len(msg.params) > 1 && !ircIsThisMe(msg.params[0]) {
		c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
		return
	}

	mask := "*"
	if len(msg.params) > 0 {
		if len(msg.params) > 1 {
			mask = msg.params[1]
		} else {
			mask = msg.params[0]
		}
	}

	if ircFnmatch(mask, serverName) {
		c.sendReply(RPL_LINKS, mask, serverName,
			0 /* hop count */, config["server_info"])
	}
	c.sendReply(RPL_ENDOFLINKS, mask)
}

func ircHandleWALLOPS(msg *message, c *client) {
	if len(msg.params) < 1 {
		c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
		return
	}
	if 0 == c.mode&ircUserModeOperator {
		c.sendReply(ERR_NOPRIVILEGES)
		return
	}

	// Our interpretation: anonymize the sender,
	// and target all users who want to receive these messages.
	for _, client := range users {
		if client == c || 0 != client.mode&ircUserModeRxWallops {
			client.sendf(":%s WALLOPS :%s", serverName, msg.params[0])
		}
	}
}

func ircHandleKILL(msg *message, c *client) {
	if len(msg.params) < 2 {
		c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
		return
	}
	if 0 == c.mode&ircUserModeOperator {
		c.sendReply(ERR_NOPRIVILEGES)
		return
	}

	target := users[ircToCanon(msg.params[0])]
	if target == nil {
		c.sendReply(ERR_NOSUCHNICK, msg.params[0])
		return
	}

	c.sendf(":%s!%s@%s KILL %s :%s",
		c.nickname, c.username, c.hostname, target.nickname, msg.params[1])
	target.closeLink(fmt.Sprintf("Killed by %s: %s", c.nickname, msg.params[1]))
}

func ircHandleDIE(msg *message, c *client) {
	if 0 == c.mode&ircUserModeOperator {
		c.sendReply(ERR_NOPRIVILEGES)
	} else if !quitting {
		initiateQuit()
	}
}

// -----------------------------------------------------------------------------

// TODO: Add an index for ERR_NOSUCHSERVER validation?
// TODO: Add a minimal parameter count?
// TODO: Add a field for oper-only commands? Use flags?
var ircHandlers = map[string]*ircCommand{
	"WEBIRC": {false, ircHandleWEBIRC, 0, 0},
	"CAP":    {false, ircHandleCAP, 0, 0},
	"PASS":   {false, ircHandlePASS, 0, 0},
	"NICK":   {false, ircHandleNICK, 0, 0},
	"USER":   {false, ircHandleUSER, 0, 0},

	"USERHOST": {true, ircHandleUSERHOST, 0, 0},
	"LUSERS":   {true, ircHandleLUSERS, 0, 0},
	"MOTD":     {true, ircHandleMOTD, 0, 0},
	"PING":     {true, ircHandlePING, 0, 0},
	"PONG":     {false, ircHandlePONG, 0, 0},
	"QUIT":     {false, ircHandleQUIT, 0, 0},
	"TIME":     {true, ircHandleTIME, 0, 0},
	"VERSION":  {true, ircHandleVERSION, 0, 0},
	"USERS":    {true, ircHandleUSERS, 0, 0},
	"SUMMON":   {true, ircHandleSUMMON, 0, 0},
	"AWAY":     {true, ircHandleAWAY, 0, 0},
	"ADMIN":    {true, ircHandleADMIN, 0, 0},
	"STATS":    {true, ircHandleSTATS, 0, 0},
	"LINKS":    {true, ircHandleLINKS, 0, 0},
	"WALLOPS":  {true, ircHandleWALLOPS, 0, 0},

	"MODE":    {true, ircHandleMODE, 0, 0},
	"PRIVMSG": {true, ircHandlePRIVMSG, 0, 0},
	"NOTICE":  {true, ircHandleNOTICE, 0, 0},
	"JOIN":    {true, ircHandleJOIN, 0, 0},
	"PART":    {true, ircHandlePART, 0, 0},
	"KICK":    {true, ircHandleKICK, 0, 0},
	"INVITE":  {true, ircHandleINVITE, 0, 0},
	"TOPIC":   {true, ircHandleTOPIC, 0, 0},
	"LIST":    {true, ircHandleLIST, 0, 0},
	"NAMES":   {true, ircHandleNAMES, 0, 0},
	"WHO":     {true, ircHandleWHO, 0, 0},
	"WHOIS":   {true, ircHandleWHOIS, 0, 0},
	"WHOWAS":  {true, ircHandleWHOWAS, 0, 0},
	"ISON":    {true, ircHandleISON, 0, 0},

	"KILL": {true, ircHandleKILL, 0, 0},
	"DIE":  {true, ircHandleDIE, 0, 0},
}

func ircProcessMessage(c *client, msg *message, raw string) {
	if c.closing {
		return
	}

	c.nReceivedMessages++
	c.receivedBytes += len(raw) + 2

	if !c.antiflood.check() {
		c.closeLink("Excess flood")
		return
	}

	if cmd, ok := ircHandlers[ircToCanon(msg.command)]; !ok {
		c.sendReply(ERR_UNKNOWNCOMMAND, msg.command)
	} else {
		cmd.nReceived++
		cmd.bytesReceived += len(raw) + 2

		if cmd.requiresRegistration && !c.registered {
			c.sendReply(ERR_NOTREGISTERED)
		} else {
			cmd.handler(msg, c)
		}
	}
}

// --- Network I/O -------------------------------------------------------------

// Handle the results from initializing the client's connection.
func (c *client) onPrepared(host string, isTLS bool) {
	c.printDebug("client resolved to %s, TLS %t", host, isTLS)
	if !isTLS {
		c.conn = c.transport.(connCloseWriter)
	} else if tlsConf != nil {
		c.tls = tls.Server(c.transport, tlsConf)
		c.conn = c.tls
	} else {
		c.printDebug("could not initialize TLS: disabled")
		c.kill("TLS support disabled")
		return
	}

	c.hostname = host
	c.address = net.JoinHostPort(host, c.port)

	// If we tried to send any data before now, we would need to flushSendQ.
	go read(c)
	c.reading = true
	c.setPingTimer()
}

// Handle the results from trying to read from the client connection.
func (c *client) onRead(data []byte, readErr error) {
	if !c.reading {
		// Abusing the flag to emulate CloseRead and skip over data, see below.
		return
	}

	c.recvQ = append(c.recvQ, data...)
	for {
		// XXX: This accepts even simple LF newlines, even though they're not
		// really allowed by the protocol.
		advance, token, _ := bufio.ScanLines(c.recvQ, false /* atEOF */)
		if advance == 0 {
			break
		}

		// XXX: And since it accepts LF, we miscalculate receivedBytes within.
		c.recvQ = c.recvQ[advance:]
		line := string(token)
		c.printDebug("-> %s", line)

		if msg := ircParseMessage(line); msg == nil {
			c.printDebug("error: invalid line")
		} else {
			ircProcessMessage(c, msg, line)
		}
	}

	if readErr != nil {
		c.reading = false

		if readErr != io.EOF {
			c.printDebug("%s", readErr)
			c.kill(readErr.Error())
		} else if c.closing {
			// Disregarding whether a clean shutdown has happened or not.
			c.printDebug("client finished shutdown")
			c.kill("")
		} else {
			c.printDebug("client EOF")
			c.closeLink("")
		}
	} else if len(c.recvQ) > 8192 {
		c.closeLink("recvQ overrun")

		// tls.Conn doesn't have the CloseRead method (and it needs to be able
		// to read from the TCP connection even for writes, so there isn't much
		// sense in expecting the implementation to do anything useful),
		// otherwise we'd use it to block incoming packet data.
		c.reading = false
	}
}

// Spawn a goroutine to flush the sendQ if possible and necessary.
func (c *client) flushSendQ() {
	if !c.writing && c.conn != nil {
		go write(c, c.sendQ)
		c.writing = true
	}
}

// Handle the results from trying to write to the client connection.
func (c *client) onWrite(written int, writeErr error) {
	c.sendQ = c.sendQ[written:]
	c.writing = false

	if writeErr != nil {
		c.printDebug("%s", writeErr)
		c.kill(writeErr.Error())
	} else if len(c.sendQ) > 0 {
		c.flushSendQ()
	} else if c.closing {
		if c.reading {
			c.conn.CloseWrite()
		} else {
			c.kill("")
		}
	}
}

// --- Worker goroutines -------------------------------------------------------

func accept(ln net.Listener) {
	for {
		// Error handling here may be tricky, see go #6163, #24808.
		if conn, err := ln.Accept(); err != nil {
			// See go #4373, they're being dicks. Another solution would be to
			// pass a done channel to this function and close it before closing
			// all the listeners, returning from here if it's readable.
			if strings.Contains(err.Error(),
				"use of closed network connection") {
				return
			}
			// XXX: net.Error.Temporary() has been deprecated in 1.18.
			if op, ok := err.(net.Error); !ok || !op.Temporary() {
				exitFatal("%s", err)
			} else {
				printError("%s", err)
			}
		} else {
			// TCP_NODELAY is set by default on TCPConns.
			conns <- conn
		}
	}
}

func prepare(client *client) {
	conn, host := client.transport, client.hostname

	// The Cgo resolver doesn't pthread_cancel getnameinfo threads, so not
	// bothering with pointless contexts.
	ch := make(chan string, 1)
	go func() {
		defer close(ch)
		if names, err := net.LookupAddr(host); err != nil {
			printError("%s", err)
		} else {
			ch <- names[0]
		}
	}()

	// While we can't cancel it, we still want to set a timeout on it.
	select {
	case <-time.After(5 * time.Second):
	case resolved, ok := <-ch:
		if ok {
			host = resolved
		}
	}

	// Note that in this demo application the autodetection prevents non-TLS
	// clients from receiving any messages until they send something.
	isTLS := false
	if sysconn, err := conn.(syscall.Conn).SyscallConn(); err != nil {
		// This is just for the TLS detection and doesn't need to be fatal.
		printError("%s", err)
	} else {
		isTLS = detectTLS(sysconn)
	}

	prepared <- preparedEvent{client, host, isTLS}
}

func read(client *client) {
	// A new buffer is allocated each time we receive some bytes, because of
	// thread-safety. Therefore the buffer shouldn't be too large, or we'd
	// need to copy it each time into a precisely sized new buffer.
	var err error
	for err == nil {
		var (
			buf [512]byte
			n   int
		)
		n, err = client.conn.Read(buf[:])
		reads <- readEvent{client, buf[:n], err}
	}
}

// Flush sendQ, which is passed by parameter so that there are no data races.
func write(client *client, data []byte) {
	// We just write as much as we can, the main goroutine does the looping.
	n, err := client.conn.Write(data)
	writes <- writeEvent{client, n, err}
}

// --- Event loop --------------------------------------------------------------

func processOneEvent() {
	select {
	case <-sigs:
		if quitting {
			forceQuit("requested by user")
		} else {
			initiateQuit()
		}

	case <-quitTimer:
		forceQuit("timeout")

	case callback := <-timers:
		callback()

	case conn := <-conns:
		if maxConnections > 0 && len(clients) >= maxConnections {
			printDebug("connection limit reached, refusing connection")
			conn.Close()
			break
		}

		address := conn.RemoteAddr().String()
		host, port, err := net.SplitHostPort(address)
		if err != nil {
			// In effect, we require TCP/UDP, as they have port numbers.
			exitFatal("%s", err)
		}

		c := &client{
			transport:  conn,
			address:    address,
			hostname:   host,
			port:       port,
			capVersion: 301,
			opened:     time.Now(),
			lastActive: time.Now(),
			// TODO: Make this configurable and more fine-grained.
			antiflood: newFloodDetector(10*time.Second, 20),
		}

		clients[c] = true
		c.printDebug("new client")
		go prepare(c)

		// The TLS autodetection in prepare needs to have a timeout.
		c.setKillTimer()

	case ev := <-prepared:
		if _, ok := clients[ev.client]; ok {
			ev.client.onPrepared(ev.host, ev.isTLS)
		}

	case ev := <-reads:
		if _, ok := clients[ev.client]; ok {
			ev.client.onRead(ev.data, ev.err)
		}

	case ev := <-writes:
		if _, ok := clients[ev.client]; ok {
			ev.client.onWrite(ev.written, ev.err)
		}
	}
}

// --- Application setup -------------------------------------------------------

func ircInitializeTLS() error {
	configCert, configKey := config["tls_cert"], config["tls_key"]

	// Only try to enable SSL support if the user configures it; it is not
	// a failure if no one has requested it.
	if configCert == "" && configKey == "" {
		return nil
	} else if configCert == "" {
		return errors.New("no TLS certificate set")
	} else if configKey == "" {
		return errors.New("no TLS private key set")
	}

	pathCert := resolveFilename(configCert, resolveRelativeConfigFilename)
	if pathCert == "" {
		return fmt.Errorf("cannot find file: %s", configCert)
	}

	pathKey := resolveFilename(configKey, resolveRelativeConfigFilename)
	if pathKey == "" {
		return fmt.Errorf("cannot find file: %s", configKey)
	}

	cert, err := tls.LoadX509KeyPair(pathCert, pathKey)
	if err != nil {
		return err
	}

	tlsConf = &tls.Config{
		Certificates:           []tls.Certificate{cert},
		ClientAuth:             tls.RequestClientCert,
		SessionTicketsDisabled: true,
	}
	return nil
}

func ircInitializeCatalog() error {
	configCatalog := config["catalog"]
	if configCatalog == "" {
		return nil
	}

	path := resolveFilename(configCatalog, resolveRelativeConfigFilename)
	if path == "" {
		return fmt.Errorf("cannot find file: %s", configCatalog)
	}

	f, err := os.Open(path)
	if err != nil {
		return fmt.Errorf("failed reading the MOTD file: %s", err)
	}
	defer f.Close()

	scanner := bufio.NewScanner(f)
	catalog = make(map[int]string)
	for lineNo := 1; scanner.Scan(); lineNo++ {
		line := strings.TrimLeft(scanner.Text(), " \t")
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}

		delim := strings.IndexAny(line, " \t")
		if delim < 0 {
			return fmt.Errorf("%s:%d: malformed line", path, lineNo)
		}

		id, err := strconv.ParseUint(line[:delim], 10, 16)
		if err != nil {
			return fmt.Errorf("%s:%d: %s", path, lineNo, err)
		}

		catalog[int(id)] = line[delim+1:]
	}
	return scanner.Err()
}

func ircInitializeMOTD() error {
	configMOTD := config["motd"]
	if configMOTD == "" {
		return nil
	}

	path := resolveFilename(configMOTD, resolveRelativeConfigFilename)
	if path == "" {
		return fmt.Errorf("cannot find file: %s", configMOTD)
	}

	f, err := os.Open(path)
	if err != nil {
		return fmt.Errorf("failed reading the MOTD file: %s", err)
	}
	defer f.Close()

	scanner := bufio.NewScanner(f)
	motd = nil
	for scanner.Scan() {
		motd = append(motd, scanner.Text())
	}
	return scanner.Err()
}

type configProcessor struct {
	err error // any error that has occurred so far
}

func (cp *configProcessor) read(name string, process func(string) string) {
	if cp.err != nil {
		return
	}
	if err := process(config[name]); err != "" {
		cp.err = fmt.Errorf("invalid configuration value for `%s': %s",
			name, err)
	}
}

// This function handles values that require validation before their first use,
// or some kind of a transformation (such as conversion to an integer) needs
// to be done before they can be used directly.
func ircParseConfig() error {
	cp := &configProcessor{}
	cp.read("ping_interval", func(value string) string {
		if u, err := strconv.ParseUint(
			config["ping_interval"], 10, 32); err != nil {
			return err.Error()
		} else if u < 1 {
			return "the value is out of range"
		} else {
			pingInterval = time.Second * time.Duration(u)
		}
		return ""
	})
	cp.read("max_connections", func(value string) string {
		if i, err := strconv.ParseInt(
			value, 10, 32); err != nil {
			return err.Error()
		} else if i < 0 {
			return "the value is out of range"
		} else {
			maxConnections = int(i)
		}
		return ""
	})
	cp.read("operators", func(value string) string {
		operators = make(map[string]bool)
		for _, fp := range splitString(value, ",", true) {
			if !ircIsValidFingerprint(fp) {
				return "invalid fingerprint value"
			}
			operators[strings.ToLower(fp)] = true
		}
		return ""
	})
	return cp.err
}

func ircInitializeServerName() error {
	if value := config["server_name"]; value != "" {
		if err := ircValidateHostname(value); err != nil {
			return err
		}
		serverName = value
		return nil
	}

	if hostname, err := os.Hostname(); err != nil {
		return err
	} else if err := ircValidateHostname(hostname); err != nil {
		return err
	} else {
		serverName = hostname
	}
	return nil
}

func ircSetupListenFDs() error {
	for _, address := range splitString(config["bind"], ",", true) {
		ln, err := net.Listen("tcp", address)
		if err != nil {
			return err
		}
		listeners = append(listeners, ln)
		printStatus("listening on %s", address)
	}
	if len(listeners) == 0 {
		return errors.New("network setup failed: no ports to listen on")
	}
	for _, ln := range listeners {
		go accept(ln)
	}
	return nil
}

// --- Main --------------------------------------------------------------------

func main() {
	flag.BoolVar(&debugMode, "debug", false, "run in verbose debug mode")
	version := flag.Bool("version", false, "show version and exit")
	writeDefaultCfg := flag.Bool("writedefaultcfg", false,
		"write a default configuration file and exit")
	systemd := flag.Bool("systemd", false, "log in systemd format")

	flag.Parse()

	if *version {
		fmt.Printf("%s %s\n", projectName, projectVersion)
		return
	}
	if *writeDefaultCfg {
		callSimpleConfigWriteDefault("", configTable)
		return
	}
	if *systemd {
		logMessage = logMessageSystemd
	}
	if flag.NArg() > 0 {
		flag.Usage()
		os.Exit(2)
	}

	// Note that this has become unnecessary since Go 1.19.
	var limit syscall.Rlimit
	if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &limit); err == nil &&
		limit.Cur != limit.Max {
		limit.Cur = limit.Max
		syscall.Setrlimit(syscall.RLIMIT_NOFILE, &limit)
	}

	config = make(simpleConfig)
	config.loadDefaults(configTable)
	if err := config.updateFromFile(); err != nil && !os.IsNotExist(err) {
		printError("error loading configuration: %s", err)
		os.Exit(1)
	}

	started = time.Now()
	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)

	for _, fn := range []func() error{
		ircInitializeTLS,
		ircInitializeServerName,
		ircInitializeMOTD,
		ircInitializeCatalog,
		ircParseConfig,
		ircSetupListenFDs,
	} {
		if err := fn(); err != nil {
			exitFatal("%s", err)
		}
	}

	for !quitting || len(clients) > 0 {
		processOneEvent()
	}
}