/* *********************************************************************
 *                  _____         _               _
 *                 |_   _|____  _| |_ _   _  __ _| |
 *                   | |/ _ \ \/ / __| | | |/ _` | |
 *                   | |  __/>  <| |_| |_| | (_| | |
 *                   |_|\___/_/\_\\__|\__,_|\__,_|_|
 *
 * * Copyright (c) 2008 - 2010 Satoshi Nakagawa <psychs AT limechat DOT net>
 * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors.
 *      Please see Acknowledgements.pdf for additional information.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *  * Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *  * Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *  * Neither the name of Textual, "Codeux Software, LLC", nor the
 *    names of its contributors may be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 *********************************************************************** */

/* A portion of this source file contains copyrighted work derived from one or more
 3rd-party, open source projects. The use of this work is hereby acknowledged. */

/* This source file contains work that originated from the Chat Core 
 framework of the Colloquy project. The source in question is in relation
 to the handling of SASL authentication requests. The license of the 
 Chat Core project is as follows: 

 This document can be found mirrored at the author's website:
 <http://colloquy.info/project/browser/trunk/Resources/BSD%20License.txt>

 No actual copyright is presented in the license file or the actual 
 source file in which this work was obtained so the work is assumed to
 be Copyright © 2000 - 2012 the Colloquy IRC Client

 ------- License -------

 Redistribution and use in source and binary forms, with or without
 modification, are permitted provided that the following conditions are
 met:

 1. Redistributions of source code must retain the above copyright
 notice, this list of conditions and the following disclaimer.
 2. Redistributions in binary form must reproduce the above copyright
 notice, this list of conditions and the following disclaimer in the
 documentation and/or other materials provided with the distribution.
 3. The name of the author may not be used to endorse or promote
 products derived from this software without specific prior written
 permission.

 THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
 NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
 TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

#import <objc/message.h>

#import "NSObjectHelperPrivate.h"
#import "NSStringHelper.h"
#import "GCDAsyncSocketExtensions.h"
#import "TPCApplicationInfo.h"
#import "TPCPathInfo.h"
#import "TPCPreferencesLocalPrivate.h"
#import "TPCPreferencesUserDefaults.h"
#import "TPCResourceManager.h"
#import "TPCThemeController.h"
#import "TPCTheme.h"
#import "THOPluginDispatcherPrivate.h"
#import "THOPluginManagerPrivate.h"
#import "THOPluginProtocol.h"
#import "TLOEncryptionManagerPrivate.h"
#import "TLOFileLoggerPrivate.h"
#import "TLOInputHistoryPrivate.h"
#import "TLOLocalization.h"
#import "TLONotificationControllerPrivate.h"
#import "TLOpenLink.h"
#import "TLOSoundPlayer.h"
#import "TLOSpeechSynthesizerPrivate.h"
#import "TLOSpokenNotificationPrivate.h"
#import "TLOTimer.h"
#import "TXGlobalModelsPrivate.h"
#import "TXMasterControllerPrivate.h"
#import "TXMenuControllerPrivate.h"
#import "TXWindowControllerPrivate.h"
#import "TVCDockIconPrivate.h"
#import "TVCLogControllerPrivate.h"
#import "TVCLogControllerInlineMediaServicePrivate.h"
#import "TVCLogControllerOperationQueuePrivate.h"
#import "TVCLogRenderer.h"
#import "TVCLogViewPrivate.h"
#import "TVCMainWindowPrivate.h"
#import "TVCMainWindowTextViewPrivate.h"
#import "TVCServerListPrivate.h"
#import "TDCAlert.h"
#import "TDCChannelBanListSheetPrivate.h"
#import "TDCFileTransferDialogPrivate.h"
#import "TDCFileTransferDialogTransferControllerPrivate.h"
#import "TDCServerChannelListDialogPrivate.h"
#import "TDCServerHighlightListSheetPrivate.h"
#import "IRC.h"
#import "IRCAddressBook.h"
#import "IRCAddressBookMatchCachePrivate.h"
#import "IRCAddressBookUserTrackingPrivate.h"
#import "IRCChannelConfig.h"
#import "IRCChannelModePrivate.h"
#import "IRCChannelUserPrivate.h"
#import "IRCChannelPrivate.h"
#import "IRCClientConfigPrivate.h"
#import "IRCClientRequestedCommandsPrivate.h"
#import "IRCColorFormatPrivate.h"
#import "IRCConnectionPrivate.h"
#import "IRCConnectionConfig.h"
#import "IRCConnectionErrors.h"
#import "IRCExtrasPrivate.h"
#import "IRCHighlightLogEntryPrivate.h"
#import "IRCHighlightMatchCondition.h"
#import "IRCISupportInfoPrivate.h"
#import "IRCMessagePrivate.h"
#import "IRCMessageBatchPrivate.h"
#import "IRCModeInfo.h"
#import "IRCNumerics.h"
#import "IRCSendingMessage.h"
#import "IRCServerPrivate.h"
#import "IRCTimerCommandPrivate.h"
#import "IRCTreeItemPrivate.h"
#import "IRCUserPrivate.h"
#import "IRCUserRelationsPrivate.h"
#import "IRCWorldPrivate.h"
#import "IRCClientPrivate.h"

NS_ASSUME_NONNULL_BEGIN

#define _autojoinDelayedWarningInterval		90 // max delay after identification is 10 so keep this above that
#define _autojoinDelayedWarningMaxCount		3

#define _isonCheckInterval			30
#define _pingInterval				270
#define _pongCheckInterval			30
#define _reconnectInterval			20
#define _retryInterval				240
#define _timeoutInterval			360
#define _whoCheckInterval			120

NSString * const IRCClientConfigurationWasUpdatedNotification = @"IRCClientConfigurationWasUpdatedNotification";

NSString * const IRCClientChannelListWasModifiedNotification = @"IRCClientChannelListWasModifiedNotification";

NSString * const IRCClientWillConnectNotification = @"IRCClientWillConnectNotification";
NSString * const IRCClientDidConnectNotification = @"IRCClientDidConnectNotification";

NSString * const IRCClientWillSendQuitNotification = @"IRCClientWillSendQuitNotification";
NSString * const IRCClientWillDisconnectNotification = @"IRCClientWillDisconnectNotification";
NSString * const IRCClientDidDisconnectNotification = @"IRCClientDidDisconnectNotification";

NSString * const IRCClientUserNicknameChangedNotification = @"IRCClientUserNicknameChangedNotification";

@interface IRCClient ()
// Properties that are public in IRCClient.h
@property (nonatomic, copy, readwrite) IRCClientConfig *config;
@property (nonatomic, copy, readwrite, nullable) IRCServer *server;
@property (nonatomic, strong, readwrite) IRCISupportInfo *supportInfo;
@property (nonatomic, assign, readwrite) BOOL isAutojoined;
@property (nonatomic, assign, readwrite) BOOL isAutojoining;
@property (nonatomic, assign, readwrite) BOOL isConnecting;
@property (nonatomic, assign, readwrite) BOOL isConnected;
@property (nonatomic, assign, readwrite) BOOL isConnectedToZNC;
@property (nonatomic, assign, readwrite) BOOL isLoggedIn;
@property (nonatomic, assign, readwrite) BOOL isQuitting;
@property (nonatomic, assign, readwrite) BOOL isDisconnecting;
@property (nonatomic, assign, readwrite) BOOL isReconnecting;
@property (nonatomic, assign, readwrite) BOOL isSecured;
@property (nonatomic, assign, readwrite) BOOL userIsAway;
@property (nonatomic, assign, readwrite) BOOL userIsIRCop;
@property (nonatomic, assign, readwrite) BOOL userIsIdentifiedWithNickServ;
@property (nonatomic, assign, readwrite) BOOL isWaitingForNickServ;
@property (nonatomic, assign, readwrite) BOOL serverHasNickServ;
@property (nonatomic, assign, readwrite) NSTimeInterval lastMessageReceived;
@property (nonatomic, assign, readwrite) NSTimeInterval lastMessageServerTime;
@property (nonatomic, assign, readwrite) ClientIRCv3SupportedCapability capabilities;
@property (nonatomic, copy, readwrite) NSArray<IRCHighlightLogEntry *> *cachedHighlights;
@property (nonatomic, copy, readwrite, nullable) NSString *userHostmask;
@property (nonatomic, copy, readwrite) NSString *userNickname;
@property (nonatomic, copy, readwrite) NSString *serverAddress;
@property (nonatomic, copy, readwrite, nullable) NSString *preAwayUserNickname;
@property (nonatomic, assign, readwrite) NSUInteger logFileSessionCount;

// Properties private
@property (nonatomic, assign) BOOL configurationIsStale;
@property (nonatomic, strong, nullable) IRCConnection *socket;
@property (nonatomic, strong) IRCMessageBatchMessageContainer *batchMessages;
@property (nonatomic, strong, nullable) TLOFileLogger *logFile;
@property (nonatomic, strong) TLOTimer *autojoinTimer;
@property (nonatomic, strong) TLOTimer *autojoinNextJoinTimer;
@property (nonatomic, strong) TLOTimer *autojoinDelayedWarningTimer;
@property (nonatomic, strong) TLOTimer *isonTimer;
@property (nonatomic, strong) TLOTimer *pongTimer;
@property (nonatomic, strong) TLOTimer *reconnectTimer;
@property (nonatomic, strong) TLOTimer *retryTimer;
@property (nonatomic, strong) TLOTimer *whoTimer;
@property (nonatomic, assign) BOOL capabilityNegotiationIsPaused;
@property (nonatomic, assign) BOOL invokingISONCommandForFirstTime;
@property (nonatomic, assign) BOOL isTerminating; // Is being destroyed
@property (nonatomic, assign) BOOL inWhoisResponse;
@property (nonatomic, assign) BOOL inWhowasResponse;
@property (nonatomic, assign) BOOL reconnectEnabled;
@property (nonatomic, assign) BOOL reconnectEnabledBecauseOfSleepMode;
@property (nonatomic, assign) BOOL timeoutWarningShownToUser;
@property (nonatomic, assign) BOOL zncBouncerIsSendingCertificateInfo;
@property (nonatomic, assign) BOOL zncBouncerIsPlayingBackHistory;
@property (nonatomic, strong) NSMutableArray<NSNumber *> *capabilitiesPending;
@property (nonatomic, assign) NSUInteger connectDelay;
@property (nonatomic, assign) NSUInteger lastServerSelected;
@property (nonatomic, assign) NSUInteger lastWhoRequestChannelListIndex;
@property (nonatomic, assign) NSUInteger successfulConnects;
@property (nonatomic, assign) NSUInteger tryingNicknameNumber;
@property (nonatomic, assign) NSUInteger autojoinDelayedWarningCount;
@property (nonatomic, copy, nullable) NSString *tryingNicknameSentNickname;
@property (nonatomic, strong) NSMutableArray<IRCChannel *> *channelListPrivate;
@property (nonatomic, strong, nullable) NSMutableArray<IRCChannel *> *channelsToAutojoin;
@property (nonatomic, strong) IRCAddressBookMatchCache *addressBookMatchCache;
@property (nonatomic, strong) IRCAddressBookUserTrackingContainer *trackedUsers;
@property (nonatomic, strong) IRCClientRequestedCommands *requestedCommands;
@property (nonatomic, strong) NSMutableDictionary<NSString *, IRCTimedCommand *> *timedCommands;
@property (nonatomic, strong) NSMutableDictionary<NSString *, IRCUser *> *userListPrivate;
@property (nonatomic, strong, nullable) NSMutableString *zncBouncerCertificateChainDataMutable;
@property (nonatomic, copy, nullable) NSString *temporaryServerAddressOverride;
@property (nonatomic, assign) uint16_t temporaryServerPortOverride;
@property (readonly) BOOL isBrokenIRCd_aka_Twitch;
@property (readonly) BOOL monitorAwayStatus;
@property (readonly) BOOL supportsAdvancedTracking;
@property (readonly, copy) NSArray<NSString *> *nickServSupportedNeedIdentificationTokens;
@property (readonly, copy) NSArray<NSString *> *nickServSupportedSuccessfulIdentificationTokens;
@property (nonatomic, strong, nullable) IRCChannel *rawDataLogQuery;
@property (nonatomic, strong, nullable) IRCChannel *hiddenCommandResponsesQuery;
@end

@implementation IRCClient

#pragma mark -
#pragma mark Initialization

- (instancetype)init
{
	[self doesNotRecognizeSelector:_cmd];

	return nil;
}

- (instancetype)initWithConfigDictionary:(NSDictionary<NSString *, id> *)dic
{
	NSParameterAssert(dic != nil);

	IRCClientConfig *config = [[IRCClientConfig alloc] initWithDictionary:dic];

	return [self initWithConfig:config];
}

- (instancetype)initWithConfig:(IRCClientConfig *)config
{
	NSParameterAssert(config != nil);

	if ((self = [super init])) {
		self.config = config;

		[self writePasswordsToKeychain];

		[self prepareInitialState];

		return self;
	}

	return nil;
}

- (void)prepareInitialState
{
	self.batchMessages = [IRCMessageBatchMessageContainer new];

	self.supportInfo = [[IRCISupportInfo alloc] initWithClient:self];

	self.connectType = IRCClientConnectModeNormal;
	self.disconnectType = IRCClientDisconnectModeNormal;

	self.cachedHighlights = @[];

	self.capabilitiesPending = [NSMutableArray array];

	self.channelListPrivate = [NSMutableArray array];

	self.timedCommands = [NSMutableDictionary dictionary];

	self.userListPrivate = [NSMutableDictionary dictionary];

	self.addressBookMatchCache = [[IRCAddressBookMatchCache alloc] initWithClient:self];
	
	self.trackedUsers = [[IRCAddressBookUserTrackingContainer alloc] initWithClient:self];

	self.requestedCommands = [IRCClientRequestedCommands new];

	self.lastMessageServerTime = self.config.lastMessageServerTime;

	self.lastServerSelected = NSNotFound;

	self.autojoinTimer =
	[TLOTimer timerWithActionBlock:^(TLOTimer *sender) {
		[self onAutojoinTimer];
	}];

	self.autojoinNextJoinTimer =
	[TLOTimer timerWithActionBlock:^(TLOTimer *sender) {
		[self onAutojoinNextJoinTimer];
	}];

	self.autojoinDelayedWarningTimer =
	[TLOTimer timerWithActionBlock:^(TLOTimer *sender) {
		[self onAutojoinDelayedWarningTimer];
	}];

	self.isonTimer =
	[TLOTimer timerWithActionBlock:^(TLOTimer *sender) {
		[self onISONTimer];
	}];

	self.reconnectTimer =
	[TLOTimer timerWithActionBlock:^(TLOTimer *sender) {
		[self onReconnectTimer];
	}];

	self.retryTimer =
	[TLOTimer timerWithActionBlock:^(TLOTimer *sender) {
		[self onRetryTimer];
	}];

	self.pongTimer =
	[TLOTimer timerWithActionBlock:^(TLOTimer *sender) {
		[self onPongTimer];
	}];

	self.whoTimer =
	[TLOTimer timerWithActionBlock:^(TLOTimer *sender) {
		[self onWhoTimer];
	}];

	[RZNotificationCenter() addObserver:self selector:@selector(willDestroyChannel:) name:IRCWorldWillDestroyChannelNotification object:nil];
}

- (void)dealloc
{
	[RZNotificationCenter() removeObserver:self];

	[self.autojoinTimer stop];
	[self.autojoinNextJoinTimer stop];
	[self.autojoinDelayedWarningTimer stop];
	[self.isonTimer	stop];
	[self.pongTimer	stop];
	[self.reconnectTimer stop];
	[self.retryTimer stop];
	[self.whoTimer stop];

	self.autojoinTimer = nil;
	self.autojoinNextJoinTimer = nil;
	self.autojoinDelayedWarningTimer = nil;
	self.isonTimer = nil;
	self.pongTimer = nil;
	self.reconnectTimer = nil;
	self.retryTimer = nil;
	self.whoTimer = nil;

	self.addressBookMatchCache = nil;
	self.batchMessages = nil;
	self.cachedHighlights = nil;
	self.channelListPrivate = nil;
	self.channelsToAutojoin = nil;
	self.logFile = nil;
	self.socket = nil;
	self.supportInfo = nil;
	self.timedCommands = nil;
	self.trackedUsers = nil;
	self.requestedCommands = nil;
	self.userListPrivate = nil;

	[self cancelPerformRequests];
}

- (void)updateConfig:(IRCClientConfig *)config
{
	[self updateConfig:config updateSelection:YES];
}

- (void)updateConfig:(IRCClientConfig *)config updateSelection:(BOOL)updateSelection
{
	NSParameterAssert(config != nil);

	if (self.isTerminating) {
		return;
	}

	IRCClientConfig *currentConfig = self.config;

	if ([currentConfig isEqual:config]) {
		return;
	}

	if ([currentConfig.uniqueIdentifier isEqualToString:config.uniqueIdentifier] == NO) {
		LogToConsoleError("Tried to load configuration for incorrect client");

		return;
	}

	self.config = config;

	/* Update channel list */
	{
		NSMutableArray<IRCChannel *> *channelListOld = [self.channelList mutableCopy];

		NSMutableArray<IRCChannel *> *channelListNew = [NSMutableArray array];

		NSMutableArray<NSString *> *channelListNewNames = [NSMutableArray array];

		NSArray *channelConfigurations = self.config.channelList;

		for (IRCChannelConfig *channelConfig in channelConfigurations) {
			/* Block duplicate channel names by maintaining array of names */
			NSString *channelName = channelConfig.channelName;

			if ([channelListNewNames containsObject:channelName] == NO) {
				[channelListNewNames addObject:channelName];
			} else {
				continue;
			}

			/* Check whether the channel exists in the current list of channels */
			/* If it does not exist, then create it. Otherwise, update it. */
			IRCChannel *channel = [self findChannel:channelConfig.channelName inList:channelListOld];

			if (channel == nil) {
				channel = [worldController() createChannelWithConfig:channelConfig onClient:self add:NO adjust:NO reload:NO];
			} else {
				[channel updateConfig:channelConfig fireChangedNotification:NO updateStoredChannelList:NO];

				[channelListOld removeObjectIdenticalTo:channel];
			}

			[channelListNew addObject:channel];
		}

		/* Any channels left in the old array can be destroyed 
		 or if they are not a channel, then they can be reinserted
		 because we do not care about private messages being updated
		 above so they must be reinserted here. */
		for (IRCChannel *channel in channelListOld) {
			if (channel.isChannel == NO) {
				[channelListNew addObject:channel];
			} else {
				[worldController() destroyChannel:channel reload:NO];
			}
		}

		/* Save updated channel list then safe its contents */
		self.channelList = channelListNew;
	}

	/* Update server list */
	{
		/* To update the server list, we first make a map of all existing
		 servers in a dictionary with the key as the identifier and the
		 object is the server itself. */
		NSArray *serverListOld = currentConfig.serverList;

		NSMutableDictionary<NSString *, IRCServer *> *serverListOldMap =
		[[NSMutableDictionary alloc] initWithCapacity:serverListOld.count];

		for (IRCServer *server in serverListOld) {
			serverListOldMap[server.uniqueIdentifier] = server;
		}

		/* We then make a map of the new server list */
		NSArray *serverListNew = self.config.serverList;

		NSMutableDictionary<NSString *, IRCServer *> *serverListNewMap =
		[[NSMutableDictionary alloc] initWithCapacity:serverListNew.count];

		for (IRCServer *server in serverListNew) {
			serverListNewMap[server.uniqueIdentifier] = server;
		}

		/* Record information about the current server (if any). */
		IRCServer *serverInUse = self.server;

		NSString *uniqueIdentifierInUse = serverInUse.uniqueIdentifier;

		/* Enumerate old server list */
		/* If an old server no longer appears in the new list of identifiers,
		 then we destroy its keychain items. If the server is the active server,
		 then we mark the keychain items to be destroyed later, incase they
		 need to be reused by IRCClient. */
		[serverListOldMap enumerateKeysAndObjectsUsingBlock:^(NSString *uniqueIdentifier, IRCServer *server, BOOL *stop) {
			if ([serverListNewMap containsKey:uniqueIdentifier]) {
				return;
			}

			if ([uniqueIdentifier isEqualToString:uniqueIdentifierInUse]) {
				serverInUse.destroyKeychainItemsDuringDealloc = YES;
			} else {
				[server destroyServerPasswordKeychainItem];
			}
		}];

		/* Enumerate new server list */
		/* All servers in the new server list have their keychain item written. */
		if (serverListNew.count == 0) {
			self.lastServerSelected = NSNotFound;
		} else {
			[serverListNewMap enumerateKeysAndObjectsUsingBlock:^(NSString *uniqueIdentifier, IRCServer *server, BOOL *stop) {
				[server writeServerPasswordToKeychain];
			}];
		}
	}

	/* -reloadItem will drop the views and reload them. */
	/* We need to remember the selection because of this. */
	if (updateSelection) {
		[self reloadServerListItems];
	}

	/* Update navigation list */
	[menuController() populateNavigationChannelList];

	/* Write passwords to keychain */
	[self writePasswordsToKeychain];

	[self destroyServerPasswordKeychainItemAfterMigration];

	/* Update main window title */
	[mainWindow() updateTitleFor:self];

	/* Rebuild list of users that are ignored and/or tracked */
	[self clearAddressBookCache];

	[self populateISONTrackedUsersList];

	/* Post notification */
	[RZNotificationCenter() postNotificationName:IRCClientConfigurationWasUpdatedNotification object:self];
}

- (void)reloadServerListItems
{
	mainWindow().ignoreOutlineViewSelectionChanges = YES;

	[mainWindowServerList() beginUpdates];

	[mainWindowServerList() reloadItem:self reloadChildren:YES];

	[mainWindowServerList() endUpdates];

	[mainWindow() adjustSelection];

	mainWindow().ignoreOutlineViewSelectionChanges = NO;
}

- (void)writePasswordsToKeychain
{
	[self.config writeNicknamePasswordToKeychain];
	[self.config writeProxyPasswordToKeychain];
}

- (void)destroyServerPasswordKeychainItemAfterMigration
{
	[self.config destroyServerPasswordKeychainItemAfterMigration];
}

- (void)updateStoredConfiguration
{
	if (self.configurationIsStale == NO) {
		return;
	}

	IRCClientConfigMutable *configMutable = [self.config mutableCopy];

	configMutable.lastMessageServerTime = self.lastMessageServerTime;

	configMutable.sidebarItemExpanded = self.sidebarItemIsExpanded;

	self.config = configMutable;
}

- (void)updateStoredChannelList
{
	/* Rebuild list of channel configurations */
	NSMutableArray<IRCChannelConfig *> *channelList = [NSMutableArray array];

	for (IRCChannel *channel in self.channelList) {
		if (channel.isUtility) {
			continue;
		}

		if (channel.isChannel == NO && [TPCPreferences rememberServerListQueryStates] == NO) {
			continue;
		}

		[channelList addObject:channel.config];
	}

	/* Save list */
	IRCClientConfigMutable *mutableConfig = [self.config mutableCopy];

	mutableConfig.channelList = channelList;

	self.config = mutableConfig;

	/* Post notification */
	[RZNotificationCenter() postNotificationName:IRCClientChannelListWasModifiedNotification object:self];
}

- (NSDictionary<NSString *, id> *)configurationDictionary
{
	[self updateStoredConfiguration];

	return [self.config dictionaryValue];
}

- (void)prepareForApplicationTermination
{
	self.isTerminating = YES;

	LogToConsoleTerminationProgress("Preparing client: <%{public}@>", self.uniqueIdentifier);

	LogToConsoleTerminationProgress("[%{public}@] Closing dialogs", self.uniqueIdentifier);

	[self closeDialogs];

	if (self.isConnecting || self.isConnected) {
		LogToConsoleTerminationProgress("[%{public}@] Performing disconnect", self.uniqueIdentifier);

		__weak IRCClient *weakSelf = self;

		self.disconnectCallback = ^{
			[weakSelf prepareForApplicationTerminationPostflight];
		};

		[self quit];

		return;
	}

	[self prepareForApplicationTerminationPostflight];
}

- (void)prepareForApplicationTerminationPostflight
{
	LogToConsoleTerminationProgress("[%{public}@] Closing log file", self.uniqueIdentifier);

	[self closeLogFile];

	LogToConsoleTerminationProgress("[%{public}@] Removing unspoken messages from speech synthesizer", self.uniqueIdentifier);

	[self clearEventsToSpeak];

	LogToConsoleTerminationProgress("[%{public}@] Emptying Address Book cache", self.uniqueIdentifier);

	[self clearAddressBookCache];

	LogToConsoleTerminationProgress("[%{public}@] Removing all tracked users", self.uniqueIdentifier);

	[self clearTrackedUsers];

	LogToConsoleTerminationProgress("[%{public}@] Preparing channels: %{public}ld", self.uniqueIdentifier, self.channelCount);

	for (IRCChannel *c in self.channelList) {
		[c prepareForApplicationTermination];
	}

	LogToConsoleTerminationProgress("[%{public}@] Preparing view controller: <%{public}@>",
					self.uniqueIdentifier, self.viewController.uniqueIdentifier);

	[self.viewController prepareForApplicationTermination];

	LogToConsoleTerminationProgress("[%{public}@] Decrementing client count", self.uniqueIdentifier);

	masterController().terminatingClientCount -= 1;
}

- (void)prepareForPermanentDestruction
{
	self.isTerminating = YES;

//	[self disconnect];	// Disconnect is called by IRCWorld for us

	[self closeDialogs];

	[self closeLogFile];

	[self clearEventsToSpeak];
	
	[self clearAddressBookCache];

	[self clearTrackedUsers];

	[self.config destroyNicknamePasswordKeychainItem];
	[self.config destroyProxyPasswordKeychainItem];

	[self destroyServerPasswordsKeychainItems];

	for (IRCChannel *c in self.channelList) {
		[c prepareForPermanentDestruction];
	}

	[[mainWindow() inputHistoryManager] destroy:self];

	[self.viewController prepareForPermanentDestruction];
}

- (void)closeDialogs
{
	TDCServerChannelListDialog *channelListDialog = [self channelListDialog];

	if (channelListDialog) {
		[channelListDialog close];
	}

	NSArray *openWindows =
	[windowController() windowsFromWindowList:@[@"TDCChannelInviteSheet",
												@"TDCServerChangeNicknameSheet",
												@"TDCServerHighlightListSheet",
												@"TDCServerPropertiesSheet"]];

	for (TDCSheetBase <TDCClientPrototype> *windowObject in openWindows) {
		if ([windowObject.clientId isEqualToString:self.uniqueIdentifier]) {
			[windowObject close];
		}
	}
}

- (void)preferencesChanged
{
	for (IRCChannel *c in self.channelList) {
		[c preferencesChanged];
	}

	if (self.monitorAwayStatus == NO) {
		[self resetAwayStatusForUsers];
	}
}

- (void)willDestroyChannel:(NSNotification *)notification
{
	IRCChannel *channel = notification.object;

	if (channel.associatedClient != self) {
		return;
	}

	[self zncPlaybackClearChannel:channel];

	if (self.hiddenCommandResponsesQuery == channel) {
		self.hiddenCommandResponsesQuery = channel;
	}

	if (self.rawDataLogQuery == channel) {
		self.rawDataLogQuery = nil;
	}
}

- (id)copyWithZone:(nullable NSZone *)zone
{
	/* Implement this method to allow client to be
	 used as a dictionary key. */

	return self;
}

#pragma mark -
#pragma mark Servers

- (void)enumerateServers:(void (NS_NOESCAPE ^)(IRCServer *server, NSUInteger index, BOOL *stop))block
{
	[self.config.serverList enumerateObjectsUsingBlock:block];
}

- (void)writeServerPasswordsToKeychain
{
	[self enumerateServers:^(IRCServer *server, NSUInteger index, BOOL *stop) {
		[server writeServerPasswordToKeychain];
	}];
}

- (void)destroyServerPasswordsKeychainItems
{
	[self enumerateServers:^(IRCServer *server, NSUInteger index, BOOL *stop) {
		[server destroyServerPasswordKeychainItem];
	}];
}

#pragma mark -
#pragma mark Properties

- (NSString *)description
{
	return [NSString stringWithFormat:@"<IRCClient [%@]: %@>", self.networkNameAlt, self.serverAddress];
}

- (NSString *)uniqueIdentifier
{
	return self.config.uniqueIdentifier;
}

- (NSString *)name
{
	return self.config.connectionName;
}

- (nullable NSString *)networkName
{
	return self.supportInfo.networkNameFormatted;
}

- (NSString *)networkNameAlt
{
	NSString *networkName = self.networkName;

	if (networkName) {
		return networkName;
	}

	return self.config.connectionName;
}

- (nullable NSString *)serverAddress
{
	NSString *serverAddress = self.supportInfo.serverAddress;

	if (serverAddress) {
		return serverAddress;
	}

	NSString *serverAddressOnSocket = self.socket.config.serverAddress;

	if (serverAddressOnSocket) {
		return serverAddressOnSocket;
	}

	return self.server.serverAddress;
}

- (NSString *)userNickname
{
	NSString *userNickname = self->_userNickname;

	if (userNickname) {
		return userNickname;
	}

	return self.config.nickname;
}

#if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1
- (NSString *)encryptionAccountNameForLocalUser
{
	return [sharedEncryptionManager() accountNameForUser:self.userNickname onClient:self];
}

- (NSString *)encryptionAccountNameForUser:(NSString *)nickname
{
	NSParameterAssert(nickname != nil);

	return [sharedEncryptionManager() accountNameForUser:nickname onClient:self];
}
#endif

- (TDCFileTransferDialog *)fileTransferController
{
	return [TXSharedApplication sharedFileTransferDialog];
}

- (BOOL)isReconnecting
{
	return self.reconnectTimer.timerIsActive;
}

- (void)setSidebarItemIsExpanded:(BOOL)sidebarItemIsExpanded
{
	/* This is a non-critical property that can be saved periodically */
	if (self->_sidebarItemIsExpanded != sidebarItemIsExpanded) {
		self->_sidebarItemIsExpanded = sidebarItemIsExpanded;

		self.configurationIsStale = YES;

		[worldController() savePeriodically];
	}
}

- (void)setLastMessageServerTime:(NSTimeInterval)lastMessageServerTime
{
	/* This is a non-critical property that can be saved periodically */
	if (self->_lastMessageServerTime != lastMessageServerTime) {
		self->_lastMessageServerTime = lastMessageServerTime;

		self.configurationIsStale = YES;

		[worldController() savePeriodically];
	}
}

- (BOOL)isSecured
{
	if (self.socket) {
		return self.socket.isSecured;
	}

	return NO;
}

- (nullable NSData *)zncBouncerCertificateChainData
{
	/* If the data is still being processed, then return
	 nil so that partial data is not returned. */
	if (self.isConnectedToZNC == NO ||
		self.zncBouncerIsSendingCertificateInfo ||
		self.zncBouncerCertificateChainDataMutable == nil)
	{
		return nil;
	}

	return [self.zncBouncerCertificateChainDataMutable dataUsingEncoding:NSASCIIStringEncoding];
}

- (BOOL)isBrokenIRCd_aka_Twitch
{
	return [self.serverAddress hasSuffix:@".twitch.tv"];
}

- (BOOL)supportsAdvancedTracking
{
	return ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityMonitorCommand] ||
			[self isCapabilityEnabled:ClientIRCv3SupportedCapabilityWatchCommand]);
}

- (BOOL)monitorAwayStatus
{
	return ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityAwayNotify] ||
			[TPCPreferences trackUserAwayStatusMaximumChannelSize] > 0);
}

- (nullable TVCLogLine *)lastLine
{
	return self.viewController.lastLine;
}

#pragma mark -
#pragma mark Standalone Utilities

- (BOOL)messageIsFromMyself:(IRCMessage *)message
{
	NSParameterAssert(message != nil);

	return [self nicknameIsMyself:message.senderNickname];
}

- (BOOL)nicknameIsMyself:(NSString *)nickname
{
	NSParameterAssert(nickname != nil);

	return [self.userNickname isEqualToStringIgnoringCase:nickname];
}

- (BOOL)stringIsNickname:(NSString *)string
{
	NSParameterAssert(string != nil);

	return ([string isHostmaskNicknameOn:self] && [string isChannelNameOn:self] == NO);
}

- (BOOL)stringIsChannelName:(NSString *)string
{
	NSParameterAssert(string != nil);

	return [string isChannelNameOn:self];
}

- (BOOL)stringIsChannelNameOrZero:(NSString *)string
{
	NSParameterAssert(string != nil);

	return ([self stringIsChannelName:string] || [string isEqualToString:@"0"]);
}

- (NSArray<NSString *> *)compileListOfModeChangesForModeSymbol:(NSString *)modeSymbol modeIsSet:(BOOL)modeIsSet parameterString:(NSString *)parameterString
{
	return [self compileListOfModeChangesForModeSymbol:modeSymbol modeIsSet:modeIsSet parameterString:parameterString characterSet:[NSCharacterSet whitespaceCharacterSet]];
}

- (NSArray<NSString *> *)compileListOfModeChangesForModeSymbol:(NSString *)modeSymbol modeIsSet:(BOOL)modeIsSet parameterString:(NSString *)parameterString characterSet:(NSCharacterSet *)characterList
{
	NSParameterAssert(parameterString != nil);
	NSParameterAssert(characterList != nil);

	NSArray *modeParameters = [parameterString componentsSeparatedByCharactersInSet:characterList];

	return [self compileListOfModeChangesForModeSymbol:modeSymbol modeIsSet:modeIsSet modeParameters:modeParameters];
}

- (NSArray<NSString *> *)compileListOfModeChangesForModeSymbol:(NSString *)modeSymbol modeIsSet:(BOOL)modeIsSet modeParameters:(NSArray<NSString *> *)modeParameters
{
	NSParameterAssert(modeSymbol.length == 1);
	NSParameterAssert(modeParameters != nil);

	if (modeParameters.count == 0) {
		return @[];
	}

	NSMutableArray<NSString *> *listOfChanges = [NSMutableArray array];

	NSMutableString *modeSetString = [NSMutableString string];
	NSMutableString *modeParamString = [NSMutableString string];

	NSUInteger numberOfEntries = 0;

	for (NSString *modeParameter in modeParameters) {
		if (modeParameter.length == 0) {
			continue;
		}

		if (modeSetString.length == 0) {
			if (modeIsSet) {
				[modeSetString appendFormat:@"+%@", modeSymbol];
			} else {
				[modeSetString appendFormat:@"-%@", modeSymbol];
			}
		} else {
			[modeSetString appendString:modeSymbol];
		}

		[modeParamString appendFormat:@" %@", modeParameter];

		numberOfEntries += 1;

		if (numberOfEntries == self.supportInfo.maximumModeCount) {
			numberOfEntries = 0;

			NSString *modeSetCombined = [modeSetString stringByAppendingString:modeParamString];

			[listOfChanges addObject:modeSetCombined];

			[modeSetString setString:@""];
			[modeParamString setString:@""];
		}
	}

	if (modeSetString.length > 0 && modeParamString.length > 0) {
		NSString *modeSetCombined = [modeSetString stringByAppendingString:modeParamString];

		[listOfChanges addObject:modeSetCombined];
	}

	return [listOfChanges copy];
}

- (void)separateTargetsInString:(NSString *)targetString withCompletionBlock:(void (NS_NOESCAPE ^)(NSArray<NSString *> *targets))completionBlock
{
	NSParameterAssert(targetString != nil);
	NSParameterAssert(completionBlock != nil);

	NSArray *targets = [targetString componentsSeparatedByString:@","];

	completionBlock(targets);
}

- (void)enumerateTargetsInString:(NSString *)targetString withBlock:(void (^)(NSString *target, NSUInteger atIndex, NSUInteger ofTotal, BOOL *stop))block
{
	NSParameterAssert(targetString != nil);
	NSParameterAssert(block != nil);

	NSArray *targets = [targetString componentsSeparatedByString:@","];

	NSUInteger targetsCount = targets.count;

	[targets enumerateObjectsUsingBlock:^(id object, NSUInteger index, BOOL *stop) {
		block(object, index, targetsCount, stop);
	}];
}

#pragma mark -
#pragma mark Highlights

- (void)clearCachedHighlights
{
	self.cachedHighlights = @[];
}

- (void)cacheHighlightInChannel:(IRCChannel *)channel withLogLine:(TVCLogLine *)logLine
{
	NSParameterAssert(channel != nil);
	NSParameterAssert(logLine != nil);

	if ([TPCPreferences logHighlights] == NO) {
		return;
	}

	/* Create entry */
	IRCHighlightLogEntryMutable *newEntry = [IRCHighlightLogEntryMutable new];

	newEntry.clientId = self.uniqueIdentifier;
	newEntry.channelId = channel.uniqueIdentifier;

	newEntry.lineLogged = logLine;

	/* We insert at head so that latest is always on top. */
	NSMutableArray *cachedHighlights = [self.cachedHighlights mutableCopy];

	[cachedHighlights insertObject:[newEntry copy] atIndex:0];

	self.cachedHighlights = cachedHighlights;

	/* Reload table if the window is open. */
	TDCServerHighlightListSheet *highlightListSheet = [windowController() windowFromWindowList:@"TDCServerHighlightListSheet"];

	if ([highlightListSheet.clientId isEqualToString:self.uniqueIdentifier] == NO) {
		return;
	}

	[highlightListSheet addEntry:self.cachedHighlights.firstObject];
}

#pragma mark -
#pragma mark Reachability

- (void)noteReachabilityChanged:(BOOL)reachable
{
	if (reachable) {
		return;
	}

	[self disconnectOnReachabilityChange];
}

- (void)disconnectOnReachabilityChange
{
	if (self.isLoggedIn == NO) {
		return;
	}

	if (self.config.performDisconnectOnReachabilityChange == NO) {
		return;
	}

	self.disconnectType = IRCClientDisconnectModeReachabilityChange;

	self.reconnectEnabled = YES;

	XRPerformBlockSynchronouslyOnMainQueue(^{
		[self disconnect];
	});
}

#pragma mark -
#pragma mark Channel Storage

- (void)selectFirstChannelInChannelList
{
	NSArray *channelList = self.channelList;

	if (channelList.count == 0) {
		return;
	}

	[mainWindow() select:channelList[0]];
}

- (void)addChannel:(IRCChannel *)channel
{
	NSParameterAssert(channel != nil);

	@synchronized(self.channelListPrivate) {
		if ([self.channelListPrivate containsObject:channel]) {
			return;
		}

		/* Add channels atop of the first non-channel (private message).
		 Private messages can be add to the bottom of the array. */
		if (channel.isChannel == NO)
		{
			[self.channelListPrivate addObject:channel];
		}
		else
		{
			NSUInteger privateMessageIndex =
			[self.channelListPrivate indexOfObjectPassingTest:^BOOL(IRCChannel *object, NSUInteger index, BOOL *stop) {
				return (object.isChannel == NO);
			}];

			if (privateMessageIndex == NSNotFound) {
				[self.channelListPrivate addObject:channel];
			} else {
				[self.channelListPrivate insertObject:channel atIndex:privateMessageIndex];
			}
		}

		[self updateStoredChannelList];
	}
}

- (void)addChannel:(IRCChannel *)channel atPosition:(NSUInteger)position
{
	NSParameterAssert(channel != nil);

	@synchronized(self.channelListPrivate) {
		if ([self.channelListPrivate containsObject:channel]) {
			return;
		}

		[self.channelListPrivate insertObject:channel atIndex:position];

		[self updateStoredChannelList];
	}
}

- (void)removeChannel:(IRCChannel *)channel
{
	NSParameterAssert(channel != nil);

	@synchronized(self.channelListPrivate) {
		[self.channelListPrivate removeObjectIdenticalTo:channel];

		[self updateStoredChannelList];
	}
}

- (NSUInteger)indexOfChannel:(IRCChannel *)channel
{
	NSParameterAssert(channel != nil);

	@synchronized (self.channelListPrivate) {
		return [self.channelListPrivate indexOfObject:channel];
	}
}

- (NSUInteger)channelCount
{
	@synchronized (self.channelListPrivate) {
		return self.channelListPrivate.count;
	}
}

- (NSArray<IRCChannel *> *)channelList
{
	@synchronized (self.channelListPrivate) {
		return [self.channelListPrivate copy];
	}
}

- (void)setChannelList:(NSArray<IRCChannel *> *)channelList
{
	NSParameterAssert(channelList != nil);

	@synchronized (self.channelListPrivate) {
		[self.channelListPrivate removeAllObjects];

		[self.channelListPrivate addObjectsFromArray:channelList];

		[self updateStoredChannelList];
	}
}

#pragma mark -
#pragma mark IRCTreeItem

- (BOOL)isClient
{
	return YES;
}

- (BOOL)isActive
{
	return self.isLoggedIn;
}

- (nullable IRCClient *)associatedClient
{
	return self;
}

- (nullable IRCChannel *)associatedChannel
{
	return nil;
}

- (NSUInteger)numberOfChildren
{
	return self.channelCount;
}

- (nullable id)childAtIndex:(NSUInteger)index
{
	return self.channelList[index];
}

- (NSString *)label
{
	return self.config.connectionName;
}

#pragma mark -
#pragma mark Encoding

- (nullable NSData *)convertToCommonEncoding:(NSString *)string
{
	NSParameterAssert(string != nil);

	NSData *data = [string dataUsingEncoding:self.config.primaryEncoding allowLossyConversion:NO];

	if (data == nil) {
		data = [string dataUsingEncoding:self.config.fallbackEncoding allowLossyConversion:NO];

		if (data == nil) {
			data = [string dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES];
		}
	}

	if (data == nil) {
		LogToConsoleError("NSData encode failure");
		LogStackTrace();
	}

	return data;
}

- (nullable NSString *)convertFromCommonEncoding:(NSData *)data
{
	NSParameterAssert(data != nil);

	NSString *string = [NSString stringWithBytes:data.bytes length:data.length encoding:self.config.primaryEncoding];

	if (string == nil) {
		string = [NSString stringWithBytes:data.bytes length:data.length encoding:self.config.fallbackEncoding];

		if (string == nil) {
			string = [NSString stringWithBytes:data.bytes length:data.length encoding:NSASCIIStringEncoding];
		}
	}

	if (string == nil) {
		LogToConsoleError("NSData decode failure");
		LogStackTrace();
	}

	return string;
}

#pragma mark -
#pragma mark Address Book

- (NSArray<IRCAddressBookEntry *> *)findIgnoresForHostmask:(NSString *)hostmask
{
	return [self.addressBookMatchCache findIgnoresForHostmask:hostmask];
}

- (nullable IRCAddressBookEntry *)findUserTrackingAddressBookEntryForHostmask:(NSString *)hostmask
{
	/* We chop off the nickname from the host and only use that in the
	 matching to keep everything a little more consistent internally. */
	NSString *nickname = hostmask.nicknameFromHostmask;
	
	if (nickname == nil) {
		return nil;
	}
	
	return [self findUserTrackingAddressBookEntryForNickname:nickname];
}

- (nullable IRCAddressBookEntry *)findUserTrackingAddressBookEntryForNickname:(NSString *)nickname
{
	NSParameterAssert(nickname != nil);

	NSString *hostmask = [NSString stringWithFormat:@"%@!*@*", nickname];
	
	return [self findAddressBookEntryForHostmask:hostmask];
}

- (nullable IRCAddressBookEntry *)findAddressBookEntryForHostmask:(NSString *)hostmask
{
	return [self.addressBookMatchCache findAddressBookEntryForHostmask:hostmask];
}

- (void)clearAddressBookCache
{
	[self.addressBookMatchCache clearCachedMatches];
}

- (void)clearAddressBookCacheForHostmask:(NSString *)hostmask
{
	/* Clear host */
	[self.addressBookMatchCache clearCachedMatchesForHostmask:hostmask];

	/* Clear nickname */
	NSString *nickname = hostmask.nicknameFromHostmask;

	NSString *nicknameHostmask = [NSString stringWithFormat:@"%@!*@*", nickname];
	
	[self.addressBookMatchCache clearCachedMatchesForHostmask:nicknameHostmask];
}

#pragma mark -
#pragma mark Output Rules

- (BOOL)outputRuleMatchedInMessage:(NSString *)message inChannel:(nullable IRCChannel *)channel
{
	NSParameterAssert(message != nil);

	if ([TPCPreferences removeAllFormatting] == NO) {
		message = message.stripIRCEffects;
	}

	NSArray *rules = sharedPluginManager().pluginOutputSuppressionRules;

	for (THOPluginOutputSuppressionRule *rule in rules) {
		if ([XRRegularExpression string:message isMatchedByRegex:rule.match] == NO) {
			continue;
		}

		if (channel) {
			if ((channel.isChannel && rule.restrictChannel) ||
				(channel.isPrivateMessage && rule.restrictPrivateMessage))
			{
				return YES;
			}
		} else {
			if (rule.restrictConsole) {
				return YES;
			}
		}
	}

	return NO;
}

#pragma mark -
#pragma mark Encryption and Decryption

- (NSDictionary<NSString *, NSString *> *)listOfNicknamesToDisallowEncryption
{
	return [TPCResourceManager dictionaryFromResources:@"StaticStore" key:@"IRCClient List of Nicknames that Encryption Forbids"];
}

#if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1
- (BOOL)encryptionAllowedForTarget:(NSString *)target
{
	return [self encryptionAllowedForTarget:target lenient:NO];
}

- (BOOL)encryptionAllowedForTarget:(NSString *)target lenient:(BOOL)lenient
{
	NSParameterAssert(target != nil);

	/* Encryption is disabled */
	if ([TPCPreferences textEncryptionIsEnabled] == NO) {
		return NO;
	}

	/* General rules */
	if ([self stringIsNickname:target] == NO) { // Do not allow channel names
		return NO;
	} else if ([self nicknameIsMyself:target] && lenient == NO) { // Do not allow the local user
		return NO;
	} else if ([self nicknameIsZNCUser:target] && lenient == NO) { // Do not allow a ZNC private user
		return NO;
	}

	/* Build context information for lookup */
	NSDictionary *exceptionRules = [self listOfNicknamesToDisallowEncryption];

	NSString *lowercaseNickname = target.lowercaseString;

	/* Check network specific rules (such as "X" on UnderNet) */
	NSString *networkName = self.supportInfo.networkName;

	if (networkName) {
		NSArray *networkSpecificData = [exceptionRules arrayForKey:networkName];

		if ([networkSpecificData containsObject:lowercaseNickname]) {
			return NO;
		}
	}

	/* Look up rules for all networks */
	NSArray *defaultsData = exceptionRules[@"-default-"];

	if ([defaultsData containsObject:lowercaseNickname]) {
		return NO;
	}

	/* Allow the nickname through when there are no rules */
	return YES;
}
#endif

- (NSUInteger)lengthOfEncryptedMessageDirectedAt:(NSString *)messageTo thatFitsWithinBounds:(NSUInteger)maximumLength
{
	return 0;
}

- (void)encryptMessage:(NSString *)messageBody directedAt:(NSString *)messageTo encodingCallback:(TLOEncryptionManagerEncodingDecodingCallbackBlock)encodingCallback injectionCallback:(TLOEncryptionManagerInjectCallbackBlock)injectionCallback
{
	NSParameterAssert(messageBody != nil);
	NSParameterAssert(messageTo != nil);
	NSParameterAssert(encodingCallback != nil);
	NSParameterAssert(injectionCallback != nil);

#if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1
	/* Check if we are accepting encryption from this user */
	if (messageBody.length == 0 || [self encryptionAllowedForTarget:messageTo] == NO) {
#endif
		if (encodingCallback) {
			encodingCallback(messageBody, NO);
		}

		if (injectionCallback) {
			injectionCallback(messageBody);
		}

#if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1
		return;
	}

	/* Continue with normal encryption operations */
	[sharedEncryptionManager() encryptMessage:messageBody
										 from:[self encryptionAccountNameForLocalUser]
										   to:[self encryptionAccountNameForUser:messageTo]
							 encodingCallback:encodingCallback
							injectionCallback:injectionCallback];
#endif
}

- (void)decryptMessage:(NSString *)messageBody from:(NSString *)messageFrom target:(NSString *)target decodingCallback:(TLOEncryptionManagerEncodingDecodingCallbackBlock)decodingCallback
{
	NSParameterAssert(messageBody != nil);
	NSParameterAssert(messageFrom != nil);
	NSParameterAssert(target != nil);
	NSParameterAssert(decodingCallback != nil);

#if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1
	/* Check if we are accepting encryption from this user */
	if (messageBody.length == 0 || [self encryptionAllowedForTarget:target lenient:YES] == NO) {
#endif
		if (decodingCallback) {
			decodingCallback(messageBody, NO);
		}

#if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1
		return;
	}

	/* Continue with normal encryption operations */
	[sharedEncryptionManager() decryptMessage:messageBody
										 from:[self encryptionAccountNameForUser:messageFrom]
										   to:[self encryptionAccountNameForLocalUser]
							 decodingCallback:decodingCallback];
#endif
}

- (void)encryptionAuthenticateUser:(NSString *)nickname
{
	NSParameterAssert(nickname != nil);

#if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1
	/* Encryption is disabled */
	if ([TPCPreferences textEncryptionIsEnabled] == NO) {
		return;
	}

	/* General rules */
	if ([self stringIsNickname:nickname] == NO) {
		return;
	}

	if ([self nicknameIsMyself:nickname]) {
		return;
	}

	/* Authenticate user */
	[sharedEncryptionManager() authenticateUser:[self encryptionAccountNameForUser:nickname]
										   from:[self encryptionAccountNameForLocalUser]];
#endif

}

#pragma mark -
#pragma mark Notifications

- (nullable NSString *)formatNotificationToSpeak:(TLOSpokenNotification *)notification
{
	NSParameterAssert(notification != nil);

	if (self.isTerminating) {
		return nil;
	}

	NSString *formattedMessage = nil;

	TXNotificationType eventType = notification.notificationType;

	IRCChannel *channel = notification.channel;

	NSString *nickname = notification.nickname;

	NSString *text = notification.text;

	if (text) {
		text = text.trim;

		if ([TPCPreferences removeAllFormatting] == NO) {
			text = text.stripIRCEffects;
		}
	}

	switch (eventType) {
		case TXNotificationTypeHighlight:
		{
			NSParameterAssert(channel != nil);
			NSParameterAssert(nickname != nil);
			NSParameterAssert(text != nil);

			if (text.length == 0) {
				break;
			}

			/* Highlights are spoken regardless of whether the user has configured
			 Channel Messages to be only spoken for selection. When the user has 
			 configured that preference, then we exclude the channel name at least
			 because that information is uninteresting. */
			/* For private messages, we speak everything, regardless of any preference. */
			BOOL isChannel = channel.isChannel;

			BOOL onlySpeakEventsForSelection = [TPCPreferences onlySpeakEventsForSelection];

			BOOL speakChannelName =
			/* 1 */	(isChannel == NO ||
			/* 2 */ (onlySpeakEventsForSelection == NO &&
					 [TPCPreferences channelMessageSpeakChannelName]) ||
			/* 2 */	(onlySpeakEventsForSelection &&
					 [mainWindow() isItemSelected:channel] == NO));

			BOOL speakNickname = (isChannel == NO ||
					[TPCPreferences channelMessageSpeakNickname]);

			NSMutableString *mutableMessage = [NSMutableString string];

			[mutableMessage appendString:TXTLS(@"Notifications[qld-sa]")];

			if (speakChannelName || speakNickname) {
				if (speakChannelName) {
					if (isChannel) {
						[mutableMessage appendString:TXTLS(@"Notifications[ke8-17]", channel.name.channelNameWithoutBang)]; // Channel
					} else {
						[mutableMessage appendString:TXTLS(@"Notifications[6nw-ec]")]; // Private Message
					}
				}

				if (speakNickname) {
					if (isChannel) {
						[mutableMessage appendString:TXTLS(@"Notifications[qy9-86]", nickname)]; // by <nickname>
					} else {
						[mutableMessage appendString:TXTLS(@"Notifications[a7i-pu]", nickname)]; // from <nickname>
					}
				}

				[mutableMessage appendString:TXTLS(@"Notifications[g9t-bt]")];
			}

			[mutableMessage appendString:text];

			formattedMessage = [mutableMessage copy];

			break;
		}
		case TXNotificationTypeChannelMessage:
		case TXNotificationTypeChannelNotice:
		{
			NSParameterAssert(channel != nil);
			NSParameterAssert(nickname != nil);
			NSParameterAssert(text != nil);

			if (text.length == 0) {
				break;
			}

			BOOL onlySpeakEventsForSelection = [TPCPreferences onlySpeakEventsForSelection];

			BOOL channelIsSelected = [mainWindow() isItemSelected:channel];

			if (onlySpeakEventsForSelection && channelIsSelected == NO) {
				break;
			}

			BOOL speakChannelName = (onlySpeakEventsForSelection == NO &&
									 [TPCPreferences channelMessageSpeakChannelName]);

			BOOL speakNickname = [TPCPreferences channelMessageSpeakNickname];

			NSMutableString *mutableMessage = [NSMutableString string];

			if (speakChannelName || speakNickname) {
				if (eventType == TXNotificationTypeChannelMessage) {
					[mutableMessage appendString:TXTLS(@"Notifications[tao-i2]")];
				} else if (eventType == TXNotificationTypeChannelNotice) {
					[mutableMessage appendString:TXTLS(@"Notifications[kk4-68]")];
				}

				if (speakChannelName) {
					[mutableMessage appendString:TXTLS(@"Notifications[ke8-17]", channel.name.channelNameWithoutBang)];
				}

				if (speakNickname) {
					[mutableMessage appendString:TXTLS(@"Notifications[qy9-86]", nickname)];
				}

				[mutableMessage appendString:TXTLS(@"Notifications[g9t-bt]")];
			}

			[mutableMessage appendString:text];

			formattedMessage = [mutableMessage copy];

			break;
		}
		case TXNotificationTypeNewPrivateMessage:
		case TXNotificationTypePrivateMessage:
		case TXNotificationTypePrivateNotice:
		{
			NSParameterAssert(nickname != nil);
			NSParameterAssert(text != nil);

			if (text.length == 0) {
				break;
			}

			NSString *formatter = nil;

			if (eventType == TXNotificationTypeNewPrivateMessage) {
				formatter = @"Notifications[rvb-9l]";
			} else if (eventType == TXNotificationTypePrivateMessage) {
				formatter = @"Notifications[2bu-ep]";
			} else if (eventType == TXNotificationTypePrivateNotice) {
				formatter = @"Notifications[6jl-vh]";
			}

			formattedMessage = TXTLS(formatter, nickname, text);

			break;
		}
		case TXNotificationTypeKick:
		{
			NSParameterAssert(channel != nil);
			NSParameterAssert(nickname != nil);

			NSString *formatter = @"Notifications[5yu-bf]";

			formattedMessage = TXTLS(formatter, channel.name.channelNameWithoutBang, nickname);

			break;
		}
		case TXNotificationTypeInvite:
		{
			NSParameterAssert(nickname != nil);
			NSParameterAssert(text != nil);

			NSString *formatter = @"Notifications[l5i-at]";

			formattedMessage = TXTLS(formatter, text.channelNameWithoutBang, nickname);

			break;
		}
		case TXNotificationTypeConnect:
		case TXNotificationTypeDisconnect:
		{
			NSString *formatter = nil;

			if (eventType == TXNotificationTypeConnect) {
				formatter = @"Notifications[z4p-yr]";
			} else if (eventType == TXNotificationTypeDisconnect) {
				formatter = @"Notifications[fd0-f8]";
			}

			formattedMessage = TXTLS(formatter, self.networkNameAlt);

			break;
		}
		case TXNotificationTypeAddressBookMatch:
		{
			NSParameterAssert(text != nil);

			formattedMessage = text;

			break;
		}
		case TXNotificationTypeFileTransferSendSuccessful:
		case TXNotificationTypeFileTransferReceiveSuccessful:
		case TXNotificationTypeFileTransferSendFailed:
		case TXNotificationTypeFileTransferReceiveFailed:
		case TXNotificationTypeFileTransferReceiveRequested:
		{
			NSParameterAssert(nickname != nil);

			NSString *formatter = nil;

			if (eventType == TXNotificationTypeFileTransferSendSuccessful) {
				formatter = @"Notifications[5e4-vg]";
			} else if (eventType == TXNotificationTypeFileTransferReceiveSuccessful) {
				formatter = @"Notifications[4cd-p3]";
			} else if (eventType == TXNotificationTypeFileTransferSendFailed) {
				formatter = @"Notifications[f0u-32]";
			} else if (eventType == TXNotificationTypeFileTransferReceiveFailed) {
				formatter = @"Notifications[mak-bj]";
			} else if (eventType == TXNotificationTypeFileTransferReceiveRequested) {
				formatter = @"Notifications[at0-vi]";
			}

			formattedMessage = TXTLS(formatter, nickname);

			break;
		}
		case TXNotificationTypeUserJoined:
		case TXNotificationTypeUserParted:
		{
			NSParameterAssert(channel != nil);
			NSParameterAssert(nickname != nil);

			NSString *formatter = nil;

			if (eventType == TXNotificationTypeUserJoined) {
				formatter = @"Notifications[bwu-ps]";
			} else if (eventType == TXNotificationTypeUserParted) {
				formatter = @"Notifications[4aq-hz]";
			}

			formattedMessage = TXTLS(formatter, nickname, channel.name.channelNameWithoutBang);

			break;
		}
		case TXNotificationTypeUserDisconnected:
		{
			NSParameterAssert(nickname != nil);

			NSString *formatter = @"Notifications[sqf-4y]";

			formattedMessage = TXTLS(formatter, nickname);

			break;
		}
	}

	return formattedMessage;
}

- (void)clearEventsToSpeak
{
	[[TXSharedApplication sharedSpeechSynthesizer] clearQueueForClient:self];
}

- (void)speakEvent:(TXNotificationType)eventType lineType:(TVCLogLineType)lineType target:(null_unspecified IRCTreeItem *)target nickname:(null_unspecified NSString *)nickname text:(null_unspecified NSString *)text
{
	if ([sharedNotificationController() speakEvent:eventType inChannel:(IRCChannel *)target] == NO) {
		return;
	}

	if (target == nil) {
		target = self;
	}

	TLOSpokenNotification *notification =
	[[TLOSpokenNotification alloc] initWithNotification:eventType
											   lineType:lineType
												 target:target
											   nickname:nickname
												   text:text];

	[[TXSharedApplication sharedSpeechSynthesizer] speak:notification];
}

- (BOOL)notifyText:(TXNotificationType)eventType lineType:(TVCLogLineType)lineType target:(IRCChannel *)target nickname:(NSString *)nickname text:(NSString *)text
{
	return [self notifyEvent:eventType lineType:lineType target:target nickname:nickname text:text userInfo:nil];
}

- (BOOL)notifyEvent:(TXNotificationType)eventType lineType:(TVCLogLineType)lineType
{
	return [self notifyEvent:eventType lineType:lineType target:nil nickname:nil text:nil userInfo:nil];
}

- (BOOL)notifyEvent:(TXNotificationType)eventType lineType:(TVCLogLineType)lineType target:(null_unspecified IRCChannel *)target nickname:(null_unspecified NSString *)nickname text:(null_unspecified NSString *)text
{
	return [self notifyEvent:eventType lineType:lineType target:target nickname:nickname text:text userInfo:nil];
}

- (BOOL)notifyEvent:(TXNotificationType)eventType lineType:(TVCLogLineType)lineType target:(null_unspecified IRCChannel *)target nickname:(null_unspecified NSString *)nickname text:(null_unspecified NSString *)text userInfo:(nullable NSDictionary<NSString *, id> *)userInfo
{
	if (self.isTerminating) {
		return NO;
	}

	BOOL isTextEvent =
	(eventType == TXNotificationTypeHighlight			||
	 eventType == TXNotificationTypeNewPrivateMessage	||
	 eventType == TXNotificationTypeChannelMessage		||
	 eventType == TXNotificationTypeChannelNotice		||
	 eventType == TXNotificationTypePrivateMessage		||
	 eventType == TXNotificationTypePrivateNotice);

	if (isTextEvent) {
		if ([self nicknameIsMyself:nickname]) {
			return NO;
		}
	}

	if (target && text != nil) {
		if ([self outputRuleMatchedInMessage:text inChannel:target]) {
			return NO;
		}
	}

	IRCChannelConfig *targetConfig = nil;

	if (target) {
		targetConfig = target.config;

		if (eventType == TXNotificationTypeHighlight) {
			if (targetConfig.ignoreHighlights) {
				return YES;
			}
		} else {
			if (targetConfig.pushNotifications == NO) {
				return YES;
			}
		}
	}

	if ([sharedNotificationController() bounceDockIconForEvent:eventType inChannel:target]) {
		if ([sharedNotificationController() bounceDockIconRepeatedlyForEvent:eventType inChannel:target]) {
			[NSApp requestUserAttention:NSCriticalRequest];
		} else {
			[NSApp requestUserAttention:NSInformationalRequest];
		}
	}

	if (sharedNotificationController().areNotificationsDisabled) {
		return YES;
	}

	BOOL mainWindowIsFocused = (mainWindow().inactive == NO);

	BOOL postNotificationsWhileFocused = [TPCPreferences postNotificationsWhileInFocus];

	BOOL targetIsSelected = [mainWindow() isItemSelected:target];

	BOOL onlySpeakEvent = (postNotificationsWhileFocused && mainWindowIsFocused && targetIsSelected);

	if ([TPCPreferences soundIsMuted] == NO) {
		if (onlySpeakEvent == NO) {
			NSString *soundName = [sharedNotificationController() soundForEvent:eventType inChannel:target];

			if (soundName) {
				[TLOSoundPlayer playAlertSound:soundName];
			}
		}

		[self speakEvent:eventType lineType:lineType target:target nickname:nickname text:text];
	}

	if (onlySpeakEvent) {
		return YES;
	}

	if ([sharedNotificationController() notificationEnabledForEvent:eventType inChannel:target] == NO) {
		return YES;
	}

	if (postNotificationsWhileFocused == NO && mainWindowIsFocused) {
		if (eventType != TXNotificationTypeAddressBookMatch) {
			return YES;
		}
	}

	if ([sharedNotificationController() disabledWhileAwayForEvent:eventType inChannel:target]) {
		if (self.userIsAway) {
			return YES;
		}
	}

	NSString *eventTitle = nil;

	NSString *eventDescription = nil;

	if (userInfo == nil) {
		if (target) {
			userInfo = @{TXNotificationUserInfoClientIdentifierKey : self.uniqueIdentifier,
						 TXNotificationUserInfoChannelIdentifierKey: target.uniqueIdentifier};
		} else {
			userInfo = @{TXNotificationUserInfoClientIdentifierKey : self.uniqueIdentifier};
		}
	}

	switch (eventType) {
		case TXNotificationTypeHighlight:
		case TXNotificationTypeNewPrivateMessage:
		case TXNotificationTypeChannelMessage:
		case TXNotificationTypeChannelNotice:
		case TXNotificationTypePrivateMessage:
		case TXNotificationTypePrivateNotice:
		{
			NSParameterAssert(nickname != nil);
			NSParameterAssert(text != nil);

			if (eventType == TXNotificationTypeHighlight ||
				eventType == TXNotificationTypeChannelMessage ||
				eventType == TXNotificationTypeChannelNotice)
			{
				NSParameterAssert(target != nil);

				eventTitle = target.name;
			}

			if (lineType == TVCLogLineTypeAction || lineType == TVCLogLineTypeActionNoHighlight) {
				eventDescription = [NSString stringWithFormat:TXNotificationDialogActionNicknameFormat, nickname, text];
			} else {
				nickname = [self formatNickname:nickname inChannel:target];

				eventDescription = [NSString stringWithFormat:TXNotificationDialogStandardNicknameFormat, nickname, text];
			}

			break;
		}
		case TXNotificationTypeFileTransferSendSuccessful:
		case TXNotificationTypeFileTransferReceiveSuccessful:
		case TXNotificationTypeFileTransferSendFailed:
		case TXNotificationTypeFileTransferReceiveFailed:
		case TXNotificationTypeFileTransferReceiveRequested:
		{
			NSParameterAssert(nickname != nil);
			NSParameterAssert(text != nil);

			eventTitle = nickname;

			eventDescription = text;

			break;
		}
		case TXNotificationTypeConnect:
		{
			eventTitle = self.networkNameAlt;

			break;
		}
		case TXNotificationTypeDisconnect:
		{
			eventTitle = self.networkNameAlt;

			break;
		}
		case TXNotificationTypeAddressBookMatch:
		{
			NSParameterAssert(text != nil);

			eventDescription = text;

			break;
		}
		case TXNotificationTypeKick:
		{
			NSParameterAssert(target != nil);
			NSParameterAssert(nickname != nil);
			NSParameterAssert(text != nil);

			eventTitle = self.networkNameAlt;

			eventDescription = TXTLS(@"Notifications[fkt-p3]", nickname, target.name, text);

			break;
		}
		case TXNotificationTypeInvite:
		{
			NSParameterAssert(nickname != nil);
			NSParameterAssert(text != nil);

			eventTitle = self.networkNameAlt;

			eventDescription = TXTLS(@"Notifications[xl5-dn]", nickname, text);

			break;
		}
		case TXNotificationTypeUserJoined:
		{
			NSParameterAssert(target != nil);
			NSParameterAssert(nickname != nil);

			eventTitle = self.networkNameAlt;

			eventDescription = TXTLS(@"Notifications[yas-us]", nickname, target.name);

			break;
		}
		case TXNotificationTypeUserParted:
		{
			NSParameterAssert(target != nil);
			NSParameterAssert(nickname != nil);
			NSParameterAssert(text != nil);

			eventTitle = self.networkNameAlt;

			if (text == nil || text.length == 0) {
				eventDescription = TXTLS(@"Notifications[bu2-9m]", nickname, target.name);
			} else {
				eventDescription = TXTLS(@"Notifications[3ur-i8]", nickname, target.name, text);
			}

			break;
		}
		case TXNotificationTypeUserDisconnected:
		{
			NSParameterAssert(nickname != nil);
			NSParameterAssert(text != nil);

			eventTitle = self.networkNameAlt;

			if (text == nil || text.length == 0) {
				eventDescription = TXTLS(@"Notifications[7ao-n8]", nickname);
			} else {
				eventDescription = TXTLS(@"Notifications[ssw-m6]", nickname, text);
			}

			break;
		}
		default:
		{
			return YES;
		}
	}

	[sharedNotificationController() notify:eventType title:eventTitle description:eventDescription userInfo:userInfo];

	return YES;
}

#pragma mark -
#pragma mark Playback

- (void)playbackClearChannel:(IRCChannel *)channel
{
	NSParameterAssert(channel != nil);

	if ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityPlayback] == NO) {
		return;
	}

	if (channel.isPrivateMessage == NO || channel.isPrivateMessageForZNCUser) {
		return;
	}

	NSString *command = [NSString stringWithFormat:@"clear %@", channel.name];

	if (self.isConnectedToZNC) {
		[self sendCommand:command toZNCModuleNamed:@"playback"];

		return;
	}

	[self send:@"PRIVMSG", @"*playback", command, nil];
}

- (void)requestPlayback
{
	if ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityPlayback] == NO) {
		return;
	}

	/* For our first connect, only playback using timestamp if logging was enabled. */
	/* For all other connects, then playback timestamp regardless of logging. */
	NSString *command = nil;

	if ((self.successfulConnects > 1 || (self.successfulConnects == 1 && self.config.zncOnlyPlaybackLatest)) && self.lastMessageServerTime > 0) {
		command = [NSString stringWithFormat:@"play * %.0f", self.lastMessageServerTime];
	} else {
		command = @"play * 0";
	}

	if (self.isConnectedToZNC) {
		[self sendCommand:command toZNCModuleNamed:@"playback"];

		return;
	}

	[self send:@"PRIVMSG", @"*playback", command, nil];
}

#pragma mark -
#pragma mark ZNC Bouncer Accessories

- (void)zncPlaybackClearChannel:(IRCChannel *)channel
{
	NSParameterAssert(channel != nil);

	if (self.isConnectedToZNC == NO) {
		return;
	}

	[self playbackClearChannel:channel];
}

- (BOOL)nicknameIsZNCUser:(NSString *)nickname
{
	NSParameterAssert(nickname != nil);

	if (self.isConnectedToZNC == NO) {
		return NO;
	}

	return [nickname hasPrefix:@"*"];
}

- (BOOL)nickname:(NSString *)nickname isZNCUser:(NSString *)zncNickname
{
	NSParameterAssert(nickname != nil);
	NSParameterAssert(zncNickname != nil);
	
	return [nickname isEqualToString:[self nicknameAsZNCUser:zncNickname]];
}

- (nullable NSString *)nicknameAsZNCUser:(NSString *)nickname
{
	NSParameterAssert(nickname != nil);

	if (self.isConnectedToZNC == NO) {
		return nil;
	}

	return [@"*" stringByAppendingString:nickname];
}

- (BOOL)isSafeToPostNotificationForMessage:(IRCMessage *)message inChannel:(nullable IRCChannel *)channel
{
	NSParameterAssert(message != nil);

	if (self.isConnectedToZNC == NO) {
		return YES;
	}

	if (self.config.zncIgnoreUserNotifications) {
		if (channel && [self nicknameIsZNCUser:channel.name]) {
			return NO;
		}
	}

	if (self.config.zncIgnorePlaybackNotifications == NO) {
		return YES;
	}

	if ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityBatch]) {
		NSString *batchType = message.parentBatchMessage.batchType;

		return ([batchType isEqualToString:@"znc.in/playback"] == NO);
	}

	return (message.isHistoric == NO);
}

- (void)updateConnectedToZNCPropertyWithMessage:(IRCMessage *)message
{
	NSParameterAssert(message != nil);

	if (self.isConnectedToZNC) {
		return;
	}

	if (message.senderIsServer == NO) {
		return;
	}

	if ([message.senderNickname isEqualToString:@"irc.znc.in"]) {
		self.isConnectedToZNC = YES;

		LogToConsole("ZNC detected...");
	}
}

- (void)sendCommand:(NSString *)command toZNCModuleNamed:(NSString *)module
{
	NSParameterAssert(command != nil);
	NSParameterAssert(module != nil);

	NSString *destination = [self nicknameAsZNCUser:module];

	if (destination == nil) {
		return;
	}

	NSString *stringToSend = [NSString stringWithFormat:@"ZNC %@ %@", destination, command];

	[self sendLine:stringToSend];
}

#pragma mark -
#pragma mark Channel States

- (void)setHighlightStateForChannel:(IRCChannel *)channel
{
	NSParameterAssert(channel != nil);

	if (mainWindow().keyWindow && [mainWindow() isItemSelected:channel]) {
		return;
	}

	channel.nicknameHighlightCount += 1;

	[TVCDockIcon updateDockIcon];

	[mainWindow() reloadTreeItem:channel];
}

- (void)setUnreadStateForChannel:(IRCChannel *)channel
{
	[self setUnreadStateForChannel:channel isHighlight:NO];
}

- (void)setUnreadStateForChannel:(IRCChannel *)channel isHighlight:(BOOL)isHighlight
{
	NSParameterAssert(channel != nil);

	if (mainWindow().keyWindow && [mainWindow() isItemSelected:channel]) {
		return;
	}

	if (channel.isChannel == NO || [TPCPreferences displayPublicMessageCountOnDockBadge]) {
		channel.dockUnreadCount += 1;

		[TVCDockIcon updateDockIcon];
	}

	channel.treeUnreadCount += 1;

	// The isHighlight flag is not sent for the purpose of incrementing
	// a count. It's passed so that we can know whether the option to
	// show badge count should be ignored when performing update.
	if (isHighlight || channel.config.showTreeBadgeCount) {
		[mainWindowServerList() refreshMessageCountForItem:channel];
	}
}

#pragma mark -
#pragma mark Find Channel

- (nullable IRCChannel *)findChannel:(NSString *)withName inList:(NSArray<IRCChannel *> *)channelList
{
	NSParameterAssert(withName != nil);
	NSParameterAssert(channelList != nil);

	NSUInteger channelIndex =
	[channelList indexOfObjectWithOptions:NSEnumerationConcurrent
							  passingTest:^BOOL(IRCChannel *channel, NSUInteger index, BOOL *stop) {
								  NSString *channelName = channel.name;

								  return [withName isEqualToStringIgnoringCase:channelName];
							  }];

	if (channelIndex != NSNotFound) {
		return channelList[channelIndex];
	}

	return nil;
}

- (nullable IRCChannel *)findChannel:(NSString *)name
{
	return [self findChannel:name inList:self.channelList];
}

- (nullable IRCChannel *)findChannelOrCreate:(NSString *)name
{
	return [self findChannelOrCreate:name isPrivateMessage:NO];
}

- (nullable IRCChannel *)findChannelOrCreate:(NSString *)withName isPrivateMessage:(BOOL)isPrivateMessage
{
	NSParameterAssert(withName != nil);

	if (isPrivateMessage == NO) {
		return [self findChannelOrCreate:withName asType:IRCChannelTypeChannel];
	} else {
		return [self findChannelOrCreate:withName asType:IRCChannelTypePrivateMessage];
	}
}

- (nullable IRCChannel *)findChannelOrCreate:(NSString *)withName isUtility:(BOOL)isUtility
{
	NSParameterAssert(withName != nil);

	if (isUtility == NO) {
		return [self findChannelOrCreate:withName asType:IRCChannelTypeChannel];
	} else {
		return [self findChannelOrCreate:withName asType:IRCChannelTypeUtility];
	}
}

- (nullable IRCChannel *)findChannelOrCreate:(NSString *)withName asType:(IRCChannelType)type
{
	NSParameterAssert(withName != nil);

	IRCChannel *channel = [self findChannel:withName];

	if (channel) {
		return channel;
	}

	if (type == IRCChannelTypeChannel) {
		IRCChannelConfig *config = [IRCChannelConfig seedWithName:withName];

		channel = [worldController() createChannelWithConfig:config onClient:self add:YES adjust:YES reload:YES];

		[worldController() savePeriodically];
	} else {
		channel = [worldController() createPrivateMessage:withName onClient:self asType:type];
	}

	return channel;
}

#pragma mark -
#pragma mark User List 

- (nullable IRCUser *)myself
{
	return [self findUser:self.userNickname];
}

- (BOOL)userExists:(NSString *)nickname
{
	NSParameterAssert(nickname != nil);

	return ([self findUser:nickname] != nil);
}

- (nullable IRCUser *)findUser:(NSString *)nickname
{
	NSParameterAssert(nickname != nil);

	nickname = nickname.lowercaseString;

	@synchronized (self.userListPrivate) {
		return self.userListPrivate[nickname];
	}
}

- (IRCUserMutable *)mutableCopyOfUserWithNickname:(NSString *)nickname
{
	NSParameterAssert(nickname != nil);

	IRCUser *user = [self findUser:nickname];

	if (user == nil) {
		return [[IRCUserMutable alloc] initWithNickname:nickname onClient:self];
	} else {
		return [user mutableCopy];
	}
}

- (NSUInteger)numberOfUsers
{
	@synchronized (self.userListPrivate) {
		return self.userListPrivate.count;
	}
}

- (NSArray<IRCUser *> *)userList
{
	@synchronized (self.userListPrivate) {
		return self.userListPrivate.allValues;
	}
}

- (void)addUser:(IRCUser *)user
{
	[self addUserAndReturn:user];
}

- (IRCUser *)addUserAndReturn:(IRCUser *)user
{
	NSParameterAssert(user != nil);

	if ([user isKindOfClass:[IRCUserMutable class]]) {
		user = [user copy];
	}

	NSString *nickname = user.lowercaseNickname;

	@synchronized (self.userListPrivate) {
		self.userListPrivate[nickname] = user;
	}

	[user becamePrimaryUser];

	return user;
}

- (IRCUser *)findUserOrCreate:(NSString *)nickname
{
	NSParameterAssert(nickname != nil);

	IRCUser *user = [self findUser:nickname];

	if (user == nil) {
		user = [[IRCUser alloc] initWithNickname:nickname onClient:self];

		[self addUser:user];
	}

	return user;
}

- (void)removeUser:(IRCUser *)user
{
	NSParameterAssert(user != nil);

	[user cancelRemoveUserTimer];
	
	NSString *hostmask = user.hostmask;

	if (hostmask) {
		[self clearAddressBookCacheForHostmask:hostmask];
	}

	[self removeUserWithNickname:user.nickname];
}

- (void)removeUserWithNickname:(NSString *)nickname
{
	NSParameterAssert(nickname != nil);

	nickname = nickname.lowercaseString;

	@synchronized (self.userListPrivate) {
		[self.userListPrivate removeObjectForKey:nickname];
	}
}

- (void)renameUser:(IRCUser *)user to:(NSString *)toNickname
{
	NSParameterAssert(user != nil);
	NSParameterAssert(toNickname != nil);

	[self modifyUser:user withBlock:^(IRCUserMutable *userMutable) {
		userMutable.nickname = toNickname;
	}];
}

- (void)renameUserWithNickname:(NSString *)fromNickname to:(NSString *)toNickname
{
	NSParameterAssert(fromNickname != nil);
	NSParameterAssert(toNickname != nil);

	IRCUser *user = [self findUser:fromNickname];

	if (user == nil) {
		return;
	}

	[self renameUser:user to:toNickname];
}

- (void)modifyUser:(IRCUser *)user withBlock:(void(NS_NOESCAPE ^)(IRCUserMutable *userMutable))block
{
	NSParameterAssert(user != nil);
	NSParameterAssert(block != nil);

	IRCUserMutable *userMutable = [user mutableCopy];

	block(userMutable);

	if ([user.nickname isEqualToString:userMutable.nickname] == NO) {
		[self removeUser:user];
	}

	[self addUser:userMutable];
}

- (void)modifyUserUserWithNickname:(NSString *)nickname withBlock:(void(NS_NOESCAPE ^)(IRCUserMutable *userMutable))block
{
	NSParameterAssert(nickname != nil);
	NSParameterAssert(block != nil);

	IRCUser *user = [self findUser:nickname];

	if (user == nil) {
		return;
	}

	[self modifyUser:user withBlock:block];
}

- (void)modifyUserWithNickname:(NSString *)nickname asAway:(BOOL)away
{
	NSParameterAssert(nickname != nil);
	
	IRCUser *user = [self findUser:nickname];
	
	if (user == nil) {
		return;
	}

	[self modifyUser:user asAway:away];
}

- (void)modifyUser:(IRCUser *)user asAway:(BOOL)away
{
	NSParameterAssert(user != nil);
	
	if (self.monitorAwayStatus == NO) {
		return;
	}

	if (away) {
		[user markAsAway];
	} else {
		[user markAsReturned];
	}
	
	[mainWindow() updateDrawingForUserInUserList:user];
}

- (void)resetAwayStatusForUsers
{
	[self.userList makeObjectsPerformSelector:@selector(markAsReturned)];
}

#pragma mark -
#pragma mark Send Raw Data

- (void)sendLine:(NSString *)string
{
	NSParameterAssert(string != nil);

	if (self.isConnected == NO) {
		[self printDebugInformationToConsole:TXTLS(@"IRC[6rj-2r]")];

		return;
	}

	[self.socket sendLine:string];

	worldController().bandwidthOut += string.length;

	worldController().messagesSent += 1;
}

- (void)send:(NSString *)string arguments:(NSArray<NSString *> *)arguments
{
	NSParameterAssert(string != nil);
	NSParameterAssert(arguments != nil);

	NSString *stringToSend = [IRCSendingMessage stringWithCommand:string arguments:arguments];

	[self sendLine:stringToSend];
}

- (void)send:(NSString *)string, ...
{
	NSParameterAssert(string != nil);

	NSMutableArray<NSString *> *argumentsOut = [NSMutableArray array];

	va_list argumentsIn;
	va_start(argumentsIn, string);

	NSString *argumentInString = nil;

	while ((argumentInString = va_arg(argumentsIn, NSString *))) {
		[argumentsOut addObject:argumentInString];
	}

	va_end(argumentsIn);

	[self send:string arguments:argumentsOut];
}

#pragma mark -
#pragma mark Sending Text

- (void)inputText:(id)string asCommand:(IRCRemoteCommand)command
{
	IRCTreeItem *destination = mainWindow().selectedItem;

	[self inputText:string asCommand:command destination:destination];
}

- (void)inputText:(id)string destination:(IRCTreeItem *)destination
{
	[self inputText:string asCommand:IRCRemoteCommandPrivmsg destination:destination];
}

- (void)inputText:(id)string asCommand:(IRCRemoteCommand)command destination:(IRCTreeItem *)destination
{
	NSParameterAssert(string != nil);
	NSParameterAssert(destination != nil);

	if (self.isTerminating) {
		return;
	}

	BOOL inputIsNSString = [string isKindOfClass:[NSString class]];

	if (inputIsNSString == NO && [string isKindOfClass:[NSAttributedString class]] == NO) {
		NSAssert(NO, @"'string' must be NSString or NSAttributedString");
	}

	if (command != IRCRemoteCommandPrivmsg &&
		command != IRCRemoteCommandPrivmsgAction &&
		command != IRCRemoteCommandNotice)
	{
		NSAssert(NO, @"Bad 'command' value");
	}

	if ([string length] == 0) {
		return;
	}

	NSAttributedString *stringIn = nil;

	if (inputIsNSString) {
		stringIn = [NSAttributedString attributedStringWithString:string];
	} else {
		stringIn = string;
	}

	NSArray *lines = ((NSAttributedString *)stringIn).splitIntoLines;

	/* Warn if the split value is above 4 lines or if the total string 
	 length exceeds TXMaximumIRCBodyLength times 4. */
	if (lines.count > 4 || (stringIn.length > (TXMaximumIRCBodyLength * 4))) {
		BOOL continueInput = [TDCAlert modalAlertWithMessage:TXTLS(@"IRC[lql-8i]")
													   title:TXTLS(@"IRC[u4c-7i]")
											   defaultButton:TXTLS(@"Prompts[mvh-ms]")
											 alternateButton:TXTLS(@"Prompts[99q-gg]")
											  suppressionKey:@"input_text_possible_flood_warning"
											 suppressionText:nil];

		if (continueInput == NO) {
			return;
		}
	}

	for (__strong NSAttributedString *line in lines) {
		NSString *lineString = line.string;

		BOOL isPrefixed = [lineString hasPrefix:@"/"];

		if (destination.isClient) {
			if (isPrefixed) {
				line = [line attributedSubstringFromIndex:1];
			}

			[self sendCommand:line];

			continue;
		}

		NSUInteger lineLength = line.length;

		IRCChannel *channel = (IRCChannel *)destination;

		if (isPrefixed && [lineString hasPrefix:@"//"] == NO && lineLength > 1) {
			line = [line attributedSubstringFromIndex:1];

			[self sendCommand:line];
		} else {
			if (isPrefixed && lineLength > 1) {
				line = [line attributedSubstringFromIndex:1];
			}

			[self sendText:line asCommand:command toChannel:channel];
		}
	}
}

- (void)sendText:(NSAttributedString *)string asCommand:(IRCRemoteCommand)command toChannel:(IRCChannel *)channel
{
	[self sendText:string asCommand:command toChannel:channel withEncryption:YES];
}

- (void)sendText:(NSAttributedString *)string asCommand:(IRCRemoteCommand)command toChannel:(IRCChannel *)channel withEncryption:(BOOL)encryptText
{
	NSParameterAssert(string != nil);
	NSParameterAssert(channel != nil);

	if (string.length == 0) {
		return;
	}

	if (channel.isUtility) {
		[self printDebugInformation:TXTLS(@"IRC[z2r-sd]") inChannel:channel];

		return;
	}

	NSString *commandToSend = nil;

	TVCLogLineType lineType = TVCLogLineTypeUndefined;

	if (command == IRCRemoteCommandPrivmsg) {
		commandToSend = @"PRIVMSG";

		lineType = TVCLogLineTypePrivateMessage;
	} else if (command == IRCRemoteCommandPrivmsgAction) {
		commandToSend = @"PRIVMSG";

		lineType = TVCLogLineTypeAction;
	} else if (command == IRCRemoteCommandNotice) {
		commandToSend = @"NOTICE";

		lineType = TVCLogLineTypeNotice;
	}

	NSParameterAssert(lineType != TVCLogLineTypeUndefined);

	NSArray *lines = string.splitIntoLines;

	for (NSAttributedString *line in lines) {
		NSMutableAttributedString *lineMutable = [line mutableCopy];

		while (lineMutable.length > 0)
		{
			NSString *unencryptedMessage = [lineMutable stringFormattedForChannel:channel.name onClient:self withLineType:lineType];

			TLOEncryptionManagerEncodingDecodingCallbackBlock encryptionBlock = ^(NSString *originalString, BOOL wasEncrypted) {
				if ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityEchoMessage] && wasEncrypted == NO) {
					return;
				}

				[self print:originalString
						 by:self.userNickname
				  inChannel:channel
					 asType:lineType
					command:commandToSend
				 receivedAt:[NSDate date]
				isEncrypted:wasEncrypted];
			};

			TLOEncryptionManagerInjectCallbackBlock injectionBlock = ^(NSString *encodedString) {
				NSString *sendMessage = encodedString;

				if (lineType == TVCLogLineTypeAction) {
					sendMessage = [NSString stringWithFormat:@"%cACTION %@%c", 0x01, sendMessage, 0x01];
				}

				[self send:commandToSend, channel.name, sendMessage, nil];
			};

			if (encryptText == NO) {
				encryptionBlock(unencryptedMessage, NO);

				injectionBlock(unencryptedMessage);

				continue;
			}

			[self encryptMessage:unencryptedMessage
					  directedAt:channel.name
				encodingCallback:encryptionBlock
			   injectionCallback:injectionBlock];
		}
	}

	[self processBundlesUserMessage:string.string command:commandToSend];
}

- (void)sendPrivmsg:(NSString *)message toChannel:(IRCChannel *)channel
{
	XRPerformBlockSynchronouslyOnMainQueue(^{
		[self sendText:[NSAttributedString attributedStringWithString:message]
			 asCommand:IRCRemoteCommandPrivmsg
			 toChannel:channel];
	});
}

- (void)sendAction:(NSString *)message toChannel:(IRCChannel *)channel
{
	XRPerformBlockSynchronouslyOnMainQueue(^{
		[self sendText:[NSAttributedString attributedStringWithString:message]
			 asCommand:IRCRemoteCommandPrivmsgAction
			 toChannel:channel];
	});
}

- (void)sendNotice:(NSString *)message toChannel:(IRCChannel *)channel
{
	XRPerformBlockSynchronouslyOnMainQueue(^{
		[self sendText:[NSAttributedString attributedStringWithString:message]
			 asCommand:IRCRemoteCommandNotice
			 toChannel:channel];
	});
}

- (void)sendPrivmsgToSelectedChannel:(NSString *)message
{
	IRCChannel *channel = [mainWindow() selectedChannelOn:self];

	if (channel == nil) {
		return;
	}

	[self sendPrivmsg:message toChannel:channel];
}

- (void)sendCTCPQuery:(NSString *)nickname command:(NSString *)command text:(nullable NSString *)text
{
	NSParameterAssert(nickname != nil);
	NSParameterAssert(command != nil);

	NSString *stringToSend = nil;

	if (text == nil) {
		stringToSend = command;
	} else {
		stringToSend = [NSString stringWithFormat:@"%@ %@", command, text];
	}

	NSString *message = [NSString stringWithFormat:@"%c%@%c", 0x01, stringToSend, 0x01];

	[self send:@"PRIVMSG", nickname, message, nil];
}

- (void)sendCTCPReply:(NSString *)nickname command:(NSString *)command text:(nullable NSString *)text
{
	NSParameterAssert(nickname != nil);
	NSParameterAssert(command != nil);

	NSString *stringToSend = nil;

	if (text == nil) {
		stringToSend = command;
	} else {
		stringToSend = [NSString stringWithFormat:@"%@ %@", command, text];
	}

	NSString *message = [NSString stringWithFormat:@"%c%@%c", 0x01, stringToSend, 0x01];

	[self send:@"NOTICE", nickname, message, nil];
}

- (void)sendCTCPPing:(NSString *)nickname
{
	NSParameterAssert(nickname != nil);
	
	NSString *text = [NSString stringWithFormat:@"%f", [NSDate timeIntervalSince1970]];

	[self sendCTCPQuery:nickname
				command:@"PING"
				   text:text];
}

#pragma mark -
#pragma mark Send Command

- (void)sendCommand:(id)string
{
	[self sendCommand:string completeTarget:YES target:nil];
}

- (void)sendCommand:(id)string completeTarget:(BOOL)completeTarget target:(nullable NSString *)targetChannelName
{
	NSParameterAssert(string != nil);

	BOOL inputIsNSString = [string isKindOfClass:[NSString class]];

	if (inputIsNSString == NO && [string isKindOfClass:[NSAttributedString class]] == NO) {
		NSAssert(NO, @"'string' must be NSString or NSAttributedString");
	}

	if ([string length] == 0) {
		return;
	}

	NSMutableAttributedString *stringIn = nil;

	if (inputIsNSString) {
		stringIn = [[NSMutableAttributedString alloc] initWithString:string];
	} else {
		stringIn = [string mutableCopy];
	}

	if ([stringIn.string hasPrefix:@"/"]) {
		[stringIn deleteCharactersInRange:NSMakeRange(0, 1)];
	}

	NSString *command = stringIn.tokenAsString;

	NSString *lowercaseCommand = command.lowercaseString;
	NSString *uppercaseCommand = command.uppercaseString;

	IRCClient *selectedClient = mainWindow().selectedClient;
	IRCChannel *selectedChannel = mainWindow().selectedChannel;

	IRCChannel *targetChannel = nil;

	NSInteger commandNumeric = [IRCCommandIndex indexOfLocalCommand:command];

	if (completeTarget && targetChannelName != nil) {
		targetChannel = [self findChannel:targetChannelName];
	} else if (completeTarget && selectedClient == self && selectedChannel) {
		targetChannel = selectedChannel;
	}

	switch (commandNumeric) {
		case IRCLocalCommandAme: // Command: AME
		case IRCLocalCommandAmsg: // Command: AMSG
		{
			NSAssertReturnLoopBreak(self.isLoggedIn);

			if (stringIn.length == 0) {
				[self printInvalidSyntaxMessageForCommand:command];
				
				break;
			}

			IRCRemoteCommand sendAsCommand = 0;

			if (commandNumeric == IRCLocalCommandAmsg) {
				sendAsCommand = IRCRemoteCommandPrivmsg;
			} else {
				sendAsCommand = IRCRemoteCommandPrivmsgAction;
			}

			for (IRCClient *client in worldController().clientList) {
				if (client != self && [TPCPreferences amsgAllConnections] == NO) {
					continue;
				}

				for (IRCChannel *channel in client.channelList) {
					if (channel.isActive == NO || channel.isChannel == NO) {
						continue;
					}

					[client sendText:stringIn asCommand:sendAsCommand toChannel:channel];
				}
			}

			break;
		}
		case IRCLocalCommandAquote: // Command: AQUOTE
		case IRCLocalCommandAraw: // Command: ARAW
		{
			NSAssertReturnLoopBreak(self.isConnected);

			if (stringIn.length == 0) {
				[self printInvalidSyntaxMessageForCommand:command];
				
				break;
			}

			for (IRCClient *client in worldController().clientList) {
				[client sendLine:stringIn.string];
			}

			break;
		}
		case IRCLocalCommandAutojoin: // Command: AUTOJOIN
		{
			NSAssertReturnLoopBreak(self.isLoggedIn);

			[self performAutoJoinInitiatedByUser:YES];

			break;
		}
		case IRCLocalCommandAway: // Command: AWAY
		{
			NSAssertReturnLoopBreak(self.isLoggedIn);

			for (IRCClient *client in worldController().clientList) {
				if (client != self && [TPCPreferences awayAllConnections] == NO) {
					continue;
				}

				NSString *comment = stringIn.string;

				/* We enforce the maximum away length here instead of
				 -toggleAwayStatusWithComment: so that we have an easier
				 way to know when it is truncated so the user can be informed. */
				NSUInteger commentMaximumLength = client.supportInfo.maximumAwayLength;

				if (commentMaximumLength > 0 && comment.length > commentMaximumLength) {
					[client printDebugInformation:TXTLS(@"IRC[41y-p2]", self.networkNameAlt, commentMaximumLength)];
				}

				[client toggleAwayStatusWithComment:comment];
			}

			break;
		}
		case IRCLocalCommandBack: // Command: BACK
		{
			NSAssertReturnLoopBreak(self.isLoggedIn);

			for (IRCClient *client in worldController().clientList) {
				if (client != self && [TPCPreferences awayAllConnections] == NO) {
					continue;
				}

				[client toggleAwayStatus:NO withComment:nil];
			}

			break;
		}
		case IRCLocalCommandCap: // Command: CAP
		case IRCLocalCommandCaps: // Command: CAPS
		{
			NSString *capabilities = self.enabledCapabilitiesStringValue;

			if (capabilities.length == 0) {
				[self printDebugInformation:TXTLS(@"IRC[5wa-lb]")];
			} else {
				[self printDebugInformation:TXTLS(@"IRC[7p9-rs]", capabilities)];
			}

			break;
		}
		case IRCLocalCommandClear: // Command: CLEAR
		{
			if (targetChannel) {
				[mainWindow() clearContentsOfChannel:targetChannel];
			} else {
				[mainWindow() clearContentsOfClient:self];
			}

			break;
		}
		case IRCLocalCommandClearall: // Command: CLEARALL
		{
			for (IRCClient *client in worldController().clientList) {
				if (client != self && [TPCPreferences clearAllConnections] == NO) {
					continue;
				}

				[mainWindow() clearContentsOfClient:client];

				for (IRCChannel *channel in client.channelList) {
					[mainWindow() clearContentsOfChannel:channel];
				}
			}

			break;
		}
		case IRCLocalCommandClose: // Command: CLOSE
		case IRCLocalCommandRemove: // Command: REMOVE
		{
			NSString *channelName = stringIn.tokenAsString;
			
			if (channelName.length == 0) {
				if (targetChannel) {
					[worldController() destroyChannel:targetChannel];
				}

				break;
			}

			IRCChannel *channel = [self findChannel:channelName];

			if (channel == nil) {
				[self printDebugInformation:TXTLS(@"IRC[pxa-ox]", channelName)];
				
				break;
			}
			
			[worldController() destroyChannel:channel];

			break;
		}
		case IRCLocalCommandConn: // Command: CONN
		{
			NSString *serverAddress = stringIn.lowercaseGetToken;

			if (serverAddress.length > 0) {
				if (serverAddress.isValidInternetAddress == NO) {
					[self printDebugInformation:TXTLS(@"IRC[zef-q9]")];
					
					break;
				}

				self.temporaryServerAddressOverride = serverAddress;
			}

			if (self.isConnecting || self.isConnected) {
				__weak IRCClient *weakSelf = self;

				self.disconnectCallback = ^{
					[weakSelf connect];
				};

				[self quit];

				break;
			}

			[self connect];

			break;
		}
		case IRCLocalCommandCtcp: // Command: CTCP
		case IRCLocalCommandCtcpreply: // Command: CTCPREPLY
		{
			NSAssertReturnLoopBreak(self.isLoggedIn);

			if (targetChannel && targetChannel != selectedChannel) {
				if (targetChannel.isUtility) {
					[self printDebugInformation:TXTLS(@"sxf-qx")];

					break;
				}

				targetChannelName = targetChannel.name;
			} else {
				targetChannelName = stringIn.tokenAsString;
			}

			NSString *subCommand = stringIn.uppercaseGetToken;

			if (subCommand.length == 0) {
				[self printInvalidSyntaxMessageForCommand:command];
				
				break;
			}

			if (commandNumeric == IRCLocalCommandCtcp) {
				if ([subCommand isEqualToString:@"PING"]) {
					[self sendCTCPPing:targetChannelName];
				} else {
					[self sendCTCPQuery:targetChannelName command:subCommand text:stringIn.string];
				}
			} else {
				[self sendCTCPReply:targetChannelName command:subCommand text:stringIn.string];
			}

			break;
		}
		case IRCLocalCommandCycle: // Command: CYCLE
		case IRCLocalCommandHop: // Command: HOP
		case IRCLocalCommandRejoin: // Command: REJOIN
		{
			NSAssertReturnLoopBreak(self.isLoggedIn);

			if (targetChannel == nil || targetChannel.isChannel == NO) {
				[self printDebugInformation:TXTLS(@"IRC[g01-qn]")];
				
				break;
			}

			[self partChannel:targetChannel];

			[self forceJoinChannel:targetChannel.name password:targetChannel.secretKey];

			break;
		}
		case IRCLocalCommandDehalfop: // Command: DEHALFOP
		case IRCLocalCommandDeop: // Command: DEOP
		case IRCLocalCommandDevoice: // Command: DEVOICE
		case IRCLocalCommandHalfop: // Command: HALFOP
		case IRCLocalCommandOp: // Command: OP
		case IRCLocalCommandVoice: // Command: VOICE
		{
			NSAssertReturnLoopBreak(self.isLoggedIn);
			
			BOOL modeIsSet = (commandNumeric == IRCLocalCommandOp ||
							  commandNumeric == IRCLocalCommandHalfop ||
							  commandNumeric == IRCLocalCommandVoice);

			NSString *modeSymbol = nil;

			if (commandNumeric == IRCLocalCommandOp || commandNumeric == IRCLocalCommandDeop) {
				modeSymbol = @"o";
			} else if (commandNumeric == IRCLocalCommandHalfop || commandNumeric == IRCLocalCommandDehalfop) {
				modeSymbol = @"h";
			} else if (commandNumeric == IRCLocalCommandVoice || commandNumeric == IRCLocalCommandDevoice) {
				modeSymbol = @"v";
			}

			if ([self.supportInfo modeSymbolIsUserPrefix:modeSymbol] == NO) {
				[self printDebugInformation:TXTLS(@"IRC[dwi-d1]", modeSymbol)];

				break;
			}

			if ([self stringIsChannelName:stringIn.string] == NO) {
				if (targetChannel && targetChannel.isChannel) {
					targetChannelName = targetChannel.name;
				} else {
					[self printDebugInformation:TXTLS(@"IRC[g01-qn]")];
					
					break;
				}
			} else {
				targetChannelName = stringIn.tokenAsString;
			}
			
			NSString *nicknamesString = stringIn.string;

			if (nicknamesString.length == 0) {
				[self printInvalidSyntaxMessageForCommand:command];
				
				break;
			}

			NSArray *modeChanges =
			[self compileListOfModeChangesForModeSymbol:modeSymbol
											  modeIsSet:modeIsSet
										parameterString:nicknamesString];

			for (NSString *modeChange in modeChanges) {
				[self send:@"MODE", targetChannelName, modeChange, nil];
			}

			break;
		}
		case IRCLocalCommandDebug: // Command: DEBUG
		case IRCLocalCommandEcho: // Command: ECHO
		{
			if (stringIn.length == 0) {
				[self printInvalidSyntaxMessageForCommand:command];
				
				break;
			}
			
			NSString *stringInString = stringIn.string;

			if ([stringInString isEqualToStringIgnoringCase:@"raw on"])
			{
				[self createRawDataLogQuery];
			}
			else if ([stringInString isEqualToStringIgnoringCase:@"raw off"])
			{
				[self destroyRawDataLogQuery];
			}
			else
			{
				[self printDebugInformation:stringInString];
			}

			break;
		}
		case IRCLocalCommandDefaults: // Command: DEFAULTS
		{
			if (stringIn.length == 0) {
				[self printDebugInformation:TXTLS(@"IRC[1dz-jb]")];

				break;
			}

			NSString *action = stringIn.tokenAsString;

			/* Present help */
			if ([action isEqualToString:@"help"])
			{
				NSString *help = TXTLS(@"IRC[bkk-lo]");

				[self printDebugInformationMultiline:help];

				break;
			}

			/* Present list of features */
			else if ([action isEqualToString:@"features"])
			{
				[TLOpenLink openWithString:@"https://help.codeux.com/textual/Command-Reference.kb#cr=defaults" inBackground:NO];

				break;
			}

			/* Prepare to toggle feature */
			NSString *feature = stringIn.tokenInsideQuotes.string;

			BOOL applyToAll = [feature isEqualToString:@"-a"];

			if (applyToAll) {
				feature = stringIn.tokenInsideQuotes.string;
			}

			NSDictionary *features = @{
				@"Ignore Notifications by Private ZNC Users"		: @"setZncIgnoreUserNotifications:",
				@"Send Authentication Requests to UserServ"			: @"setSendAuthenticationRequestsToUserServ:",
				@"Disable Automatic SASL EXTERNAL Response"			: @"setSaslAuthenticationDisableExternalMechanism:",
				@"Send WHO Command Requests to Channels"			: @"setSendWhoCommandRequestsToChannels:",
			};

			BOOL enableFeature = [action isEqualToString:@"enable"];

			/* Cannot toggle feature if the user doesn't tell us which */
			if (feature.length == 0) {
				[self printDebugInformation:TXTLS(@"IRC[1dz-jb]")];

				break;
			}

			/* Make sure the feature exists */
			if ([features containsKey:feature] == NO) {
				if (enableFeature) {
					[self printDebugInformation:TXTLS(@"IRC[pc4-67]", feature)];
				} else {
					[self printDebugInformation:TXTLS(@"IRC[d7y-pv]", feature)];
				}

				break;
			}

			/* Toggle the feature by mutating the client's configuration,
			 invoking the appropriate method, then saving it. */
			void (^toggleFeature)(IRCClient *, NSString *, BOOL) = ^(IRCClient *client, NSString *featureKey, BOOL featureValue) {
				NSString *selectorString = features[featureKey];

				SEL selector = NSSelectorFromString(selectorString);

				IRCClientConfigMutable *mutableClientConfig = [client.config mutableCopy];

				NSMethodSignature *signature = [mutableClientConfig methodSignatureForSelector:selector];

				NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];

				[invocation setTarget:mutableClientConfig];
				[invocation setSelector:selector];

				[invocation setArgument:&featureValue atIndex:2];

				[invocation invoke];

				client.config = mutableClientConfig;
			};

			/* Toggle feature */
			for (IRCClient *client in worldController().clientList) {
				if (client != self && applyToAll == NO) {
					continue;
				}

				toggleFeature(client, feature, enableFeature);

				if (enableFeature) {
					[client printDebugInformation:TXTLS(@"IRC[5ke-18]", feature)];
				} else {
					[client printDebugInformation:TXTLS(@"IRC[0gn-cb]", feature)];
				}
			}

			/* Save modified client */
			[worldController() save];

			break;
		}
		case IRCLocalCommandGetscripts: // Command: GETSCRIPTS
		{
			[sharedPluginManager() extrasInstallerLaunchInstaller];

			break;
		}
		case IRCLocalCommandGline: // Command: GLINE
		case IRCLocalCommandGzline: // Command: GZLINE
		case IRCLocalCommandShun:  // Command: SHUN
		case IRCLocalCommandTempshun: // Command: TEMPSHUN
		case IRCLocalCommandZline: // Command: ZLINE
		{
			NSAssertReturnLoopBreak(self.isLoggedIn);

			NSString *segment1 = stringIn.getTokenAsString;
			NSString *segment2 = stringIn.getTokenAsString;

			[self send:uppercaseCommand, segment1, segment2, stringIn.string, nil];

			break;
		}
		case IRCLocalCommandGoto: // Command: GOTO
		{
			if (stringIn.length == 0) {
				[self printInvalidSyntaxMessageForCommand:command];
				
				break;
			}

			NSString *needle = stringIn.tokenAsString;

			IRCTreeItem *bestMatch = mainWindow().selectedItem;

			CGFloat bestScore = 0.0;

			for (IRCClient *client in worldController().clientList) {
				for (IRCChannel *channel in client.channelList) {
					CGFloat currentScore = [channel.name compareWithWord:needle lengthPenaltyWeight:0.1];

					if (currentScore > bestScore) {
						bestMatch = channel;

						bestScore = currentScore;
					}
				}
			}

			[mainWindow() select:bestMatch];

			break;
		}
		case IRCLocalCommandIgnore: // Command: IGNORE
		case IRCLocalCommandUnignore: // Command: UNIGNORE
		{
			BOOL isIgnoreCommand = (commandNumeric == IRCLocalCommandIgnore);

			if (stringIn.length == 0 || targetChannel == nil) {
				if (isIgnoreCommand) {
					[menuController() showServerPropertiesSheetForClient:self withSelection:TDCServerPropertiesSheetSelectionNewIgnoreEntry context:@""];
				} else {
					[menuController() showServerPropertiesSheetForClient:self withSelection:TDCServerPropertiesSheetSelectionAddressBook context:nil];
				}

				break;
			}

			NSString *nickname = stringIn.tokenAsString;

			IRCUser *member = [self findUser:nickname];

			if (member == nil) {
				if (isIgnoreCommand) {
					[menuController() showServerPropertiesSheetForClient:self withSelection:TDCServerPropertiesSheetSelectionNewIgnoreEntry context:nickname];
				} else {
					[menuController() showServerPropertiesSheetForClient:self withSelection:TDCServerPropertiesSheetSelectionAddressBook context:nil];
				}

				break;
			}

			/* Build list of ignores that already match the user's host */
			NSString *hostmask = member.hostmask;

			if (hostmask == nil) {
				hostmask = [NSString stringWithFormat:@"%@!*@*", nickname];
			}

			NSMutableArray *matchedIgnores = [NSMutableArray array];

			for (IRCAddressBookEntry *ignore in self.config.ignoreList) {
				if (ignore.entryType != IRCAddressBookEntryTypeIgnore) {
					continue;
				}

				if ([ignore checkMatch:hostmask]) {
					[matchedIgnores addObject:ignore];
				}
			}

			/* Cancel if there is nothing to change */
			if (isIgnoreCommand) {
				if (matchedIgnores.count > 0) {
					[self printDebugInformation:TXTLS(@"IRC[5ix-zn]", member.nickname)];

					break;
				}
			} else {
				if (matchedIgnores.count == 0) {
					[self printDebugInformation:TXTLS(@"IRC[wu0-jp]", member.nickname)];

					break;
				} else if (matchedIgnores.count > 1) {
					[self printDebugInformation:TXTLS(@"IRC[vrx-1f]", member.nickname)];
					
					break;
				}
			}

			/* Modify ignore list and inform user of change */
			NSMutableArray *mutableIgnoreList = [self.config.ignoreList mutableCopy];

			if (isIgnoreCommand) {
				IRCAddressBookEntry *ignore =
				[IRCAddressBookEntry newIgnoreEntryForHostmask:member.banMask];

				[self printDebugInformation:TXTLS(@"IRC[ret-20]", member.nickname, ignore.hostmask)];

				[mutableIgnoreList addObject:ignore];
			} else{
				IRCAddressBookEntry *ignore = matchedIgnores[0];
				
				[self printDebugInformation:TXTLS(@"IRC[jzg-g8]", member.nickname, ignore.hostmask)];

				[mutableIgnoreList removeObjectIdenticalTo:ignore];
			}

			/* Save modified ignore list */
			IRCClientConfigMutable *mutableClientConfig = [self.config mutableCopy];

			mutableClientConfig.ignoreList = mutableIgnoreList;

			self.config = mutableClientConfig;
			
			/* Clear cache */
			/* If we have a host, then it's easy to clear only that.
			 If we don't have a host, then we have to clear everything. */
			if (hostmask) {
				[self clearAddressBookCacheForHostmask:hostmask];
			} else {
				[self clearAddressBookCache];
			}

			break;
		}
		case IRCLocalCommandInvite: // Command: INVITE
		{
			NSAssertReturnLoopBreak(self.isLoggedIn);

			if (stringIn.length == 0) {
				[self printInvalidSyntaxMessageForCommand:command];
				
				break;
			}

			NSArray *nicknames = [stringIn.string componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];

			if ([self stringIsChannelName:nicknames.lastObject] == NO) {
				if (targetChannel && targetChannel.isChannel) {
					targetChannelName = targetChannel.name;
				} else {
					[self printDebugInformation:TXTLS(@"IRC[g01-qn]")];
					
					break;
				}
			} else {
				targetChannelName = nicknames.lastObject;
				
				nicknames = [nicknames subarrayWithRange:NSMakeRange(0, (nicknames.count - 1))];
			}

			for (NSString *nickname in nicknames) {
				if ([self stringIsNickname:nickname] == NO) {
					continue;
				}

				[self send:@"INVITE", nickname, targetChannelName, nil];
			}

			break;
		}
		case IRCLocalCommandIson: // Command: ISON
		{
			NSAssertReturnLoopBreak(self.isLoggedIn);

			if (stringIn.length == 0) {
				[self printInvalidSyntaxMessageForCommand:command];
				
				break;
			}

			[self createHiddenCommandResponses];

			[self.requestedCommands recordIsonRequestOpenedAsVisible];

			[self send:@"ISON", stringIn.string, nil];

			break;
		}
		case IRCLocalCommandJ: // Command: J
		case IRCLocalCommandJoin:  // Command: JOIN
		{
			NSAssertReturnLoopBreak(self.isLoggedIn);

			if (stringIn.length == 0) {
				if (targetChannel && targetChannel.isChannel) {
					targetChannelName = targetChannel.name;
				} else {
					[self printDebugInformation:TXTLS(@"IRC[g01-qn]")];
					
					break;
				}
			} else {
				targetChannelName = stringIn.tokenAsString;

				if (targetChannelName.length == 0) {
					[self printInvalidSyntaxMessageForCommand:command];

					break;
				}

				if ([self stringIsChannelNameOrZero:targetChannelName] == NO) {
					targetChannelName = [@"#" stringByAppendingString:targetChannelName];
				}
			}

			[self joinUnlistedChannelsWithStringAndSelectBestMatch:targetChannelName passwords:stringIn.string];

			break;
		}
		case IRCLocalCommandJoinRandom: // Command: JOIN_RANDOM
		{
			NSAssertReturnLoopBreak(self.isLoggedIn);

			NSInteger numberOfChannelsToJoin = 0;

			NSString *numberOfChannelsToken = stringIn.tokenAsString;

			if (numberOfChannelsToken.isNumericOnly) {
				numberOfChannelsToJoin = numberOfChannelsToken.integerValue;
			}

			if (numberOfChannelsToJoin <= 0) {
				numberOfChannelsToJoin = 1;
			}

			for (NSUInteger i = 0; i < numberOfChannelsToJoin; i++) {
				NSString *channelName = [NSString stringWithFormat:@"#debug-channel-%lu", TXRandomNumber(9999999)];

				[self send:@"JOIN", channelName, nil];
			}

			break;
		}
		case IRCLocalCommandBan: // Command: BAN
		case IRCLocalCommandKb: // Command: KB
		case IRCLocalCommandKick: // Command: KICK
		case IRCLocalCommandKickban: // Command: KICKBAN
		case IRCLocalCommandUnban: // Command: UNBAN
		case IRCLocalCommandUnquiet: // Command: UNQUIET
		case IRCLocalCommandQuiet: // Command: QUIET
		{
			NSAssertReturnLoopBreak(self.isLoggedIn);

			NSString *nickname = stringIn.tokenAsString;

			if ([self stringIsChannelName:nickname] == NO) {
				if (targetChannel && targetChannel.isChannel) {
					targetChannelName = targetChannel.name;
				} else {
					[self printDebugInformation:TXTLS(@"IRC[g01-qn]")];
					
					break;
				}
			} else {
				targetChannelName = nickname;

				targetChannel = [self findChannel:targetChannelName];
				
				nickname = stringIn.tokenAsString;
			}

			if (nickname.length == 0) {
				[self printInvalidSyntaxMessageForCommand:command];
					
				break;
			}

			if (commandNumeric == IRCLocalCommandKickban ||
				commandNumeric == IRCLocalCommandKb ||
				commandNumeric == IRCLocalCommandBan ||
				commandNumeric == IRCLocalCommandUnban ||
				commandNumeric == IRCLocalCommandQuiet ||
				commandNumeric == IRCLocalCommandUnquiet)
			{
				IRCChannelUser *member = [targetChannel findMember:nickname];

				NSString *banMask = member.user.banMask;

				if (banMask == nil) {
					banMask = nickname;
				}

				NSString *modeSymbol = nil;

				if (commandNumeric == IRCLocalCommandQuiet ||
					commandNumeric == IRCLocalCommandUnquiet)
				{
					modeSymbol = @"q";
				} else {
					modeSymbol = @"b";
				}

				if ([self.supportInfo modeSymbolIsUserPrefix:modeSymbol]) {
					[self printDebugInformation:TXTLS(@"IRC[dwi-d1]", modeSymbol)];

					break;
				}

				if (commandNumeric == IRCLocalCommandUnban ||
					commandNumeric == IRCLocalCommandUnquiet)
				{
					[self send:@"MODE", targetChannelName, [@"-" stringByAppendingString:modeSymbol], banMask, nil];
				} else {
					[self send:@"MODE", targetChannelName, [@"+" stringByAppendingString:modeSymbol], banMask, nil];
				}
			}

			if (commandNumeric == IRCLocalCommandKb ||
				commandNumeric == IRCLocalCommandKick ||
				commandNumeric == IRCLocalCommandKickban)
			{
				NSString *reason = stringIn.string;

				if (reason.length == 0) {
					reason = [TPCPreferences defaultKickMessage];
				}

				NSUInteger reasonMaximumLength = self.supportInfo.maximumKickLength;

				if (reasonMaximumLength > 0 && reason.length > reasonMaximumLength) {
					[self printDebugInformation:TXTLS(@"IRC[59a-ir]", self.networkNameAlt, reasonMaximumLength)];
				}

				[self send:@"KICK", targetChannelName, nickname, reason, nil];
			}

			break;
		}
		case IRCLocalCommandKill: // Command: KILL
		{
			NSAssertReturnLoopBreak(self.isLoggedIn);

			NSString *nickname = stringIn.getTokenAsString;

			if (nickname.length == 0) {
				[self printInvalidSyntaxMessageForCommand:command];
				
				break;
			}

			NSString *reason = stringIn.string;

			if (reason.length == 0) {
				reason = [TPCPreferences IRCopDefaultKillMessage];
			}

			[self send:@"KILL", nickname, reason, nil];

			break;
		}
		case IRCLocalCommandLagcheck: // Command: LAGCHECK
		case IRCLocalCommandMylag: // Command: MYLAG
		{
			NSAssertReturnLoopBreak(self.isLoggedIn);

			/* We only accept LAGCHECK CTCP responses from ourselves which
			 means it is relatively safe to pack some data on the end of
			 the CTCP so that we can provide ourselves some context. */
			NSMutableDictionary *lagCheckContext = [NSMutableDictionary dictionaryWithCapacity:3];

			lagCheckContext[@"connection"] = self.socket.uniqueIdentifier;
			
			lagCheckContext[@"time"] = @([NSDate timeIntervalSince1970]);

			if (commandNumeric == IRCLocalCommandMylag) {
				IRCChannel *selectedChannel = [mainWindow() selectedChannelOn:self];
				
				if (selectedChannel) {
					lagCheckContext[@"channel"] = selectedChannel.name;
				}
			}
			
			NSString *ctcpContext = [lagCheckContext formDataUsingSeparator:@"&"];

			[self sendCTCPQuery:self.userNickname
						command:@"LAGCHECK"
						   text:ctcpContext];

			[self printDebugInformation:TXTLS(@"IRC[qoh-kt]")];

			break;
		}
		case IRCLocalCommandLeave: // Command: LEAVE
		case IRCLocalCommandPart: // Command: PART
		{
			NSAssertReturnLoopBreak(self.isLoggedIn);

			if ([self stringIsChannelName:stringIn.string] == NO) {
				if (targetChannel && targetChannel.isChannel) {
					targetChannelName = targetChannel.name;
				} else if (targetChannel) {
					[worldController() destroyChannel:targetChannel];

					break;
				} else {
					break;
				}
			} else {
				targetChannelName = stringIn.tokenAsString;
			}
			
			NSString *reason = stringIn.string;

			if (reason.length == 0) {
				reason = self.config.normalLeavingComment;
			}

			[self send:@"PART", targetChannelName, reason, nil];

			break;
		}
		case IRCLocalCommandList: // Command: LIST
		{
			NSAssertReturnLoopBreak(self.isLoggedIn);

			TDCServerChannelListDialog *channelListDialog = [self channelListDialog];

			if (channelListDialog == nil) {
				[self createChannelListDialog];
			}

			[self requestChannelList];

			break;
		}
		case IRCLocalCommandM: // Command: M
		case IRCLocalCommandMode: // Command: MODE
		{
			NSAssertReturnLoopBreak(self.isLoggedIn);

			NSString *modeString = stringIn.string;

			if (  modeString.length == 0 ||
				([modeString hasPrefix:@"+"] ||
				 [modeString hasPrefix:@"-"]))
			{
				if (targetChannel && targetChannel.isChannel) {
					targetChannelName = targetChannel.name;
				} else {
					[self printInvalidSyntaxMessageForCommand:command];
					
					break;
				}
			} else {
				targetChannelName = stringIn.tokenAsString;
			}

			if (modeString.length == 0) {
				[self send:@"MODE", targetChannelName, nil];
			} else {
				[self send:@"MODE", targetChannelName, modeString, nil];
			}

			break;
		}
		case IRCLocalCommandMute: // Command: MUTE
		{
			if ([TPCPreferences soundIsMuted]) {
				[self printDebugInformation:TXTLS(@"IRC[sdn-yr]")];
			} else {
				[self printDebugInformation:TXTLS(@"IRC[u48-aa]")];

				[menuController() toggleMuteOnNotificationSoundsShortcutOn:YES];
			}

			break;
		}
		case IRCLocalCommandMyversion: // Command: MYVERSION
		{
			NSString *applicationName = [TPCApplicationInfo applicationNameWithoutVersion];
			NSString *versionLong = [TPCApplicationInfo applicationVersion];
			NSString *versionShort = [TPCApplicationInfo applicationVersionShort];
//			NSString *buildScheme = [TPCApplicationInfo applicationBuildScheme];

			NSString *downloadSource = @""; // Assume standalone by default
			NSString *buildType = @""; // Assume universal binary by default

#if TEXTUAL_BUILT_AS_UNIVERSAL_BINARY == 0
			NSString *hostType = nil;

#if TARGET_CPU_ARM64
			hostType = TXTLS(@"IRC[g1u-os]");
#elif TARGET_CPU_X86_64
			hostType = TXTLS(@"IRC[swz-uj]");
#endif

			buildType = TXTLS(@"IRC[b8p-44]", hostType);
#endif // Universal

			NSString *message = TXTLS(@"IRC[ccb-ur]", applicationName, versionShort, versionLong, downloadSource, buildType);

			if (targetChannel) {
				message = TXTLS(@"IRC[pqj-1y]", message);

				[self sendPrivmsg:message toChannel:targetChannel];
			} else {
				[self printDebugInformationToConsole:message];
			}

			break;
		}
		case IRCLocalCommandNick: // Command: NICK
		{
			/* Use -isConnected instead of -isLoggedIn so that
			 user can change their nickname during registration
			 phase before the 001 numeric is received. */
			NSAssertReturnLoopBreak(self.isConnected);
			
			NSString *newNickname = stringIn.tokenAsString;
			
			if (newNickname.length == 0) {
				[self printInvalidSyntaxMessageForCommand:command];
				
				break;
			}

			for (IRCClient *client in worldController().clientList) {
				if (client != self && [TPCPreferences nickAllConnections] == NO) {
					continue;
				}

				[client changeNickname:newNickname];
			}

			break;
		}
		case IRCLocalCommandNotifybubble: // Command: NOTIFYBUBBLE
		{
			if ([self stringIsChannelName:stringIn.string]) {
				targetChannel = [self findChannel:stringIn.tokenAsString];
			} else {
				targetChannel = nil;
			}
			
			if (stringIn.length == 0) {
				[self printInvalidSyntaxMessageForCommand:command];
				
				break;
			}

			NSString *title = [TPCApplicationInfo applicationNameWithoutVersion];

			NSString *message = stringIn.string;

			[sharedNotificationController() scheduleNotificationWithTitle:title
																  message:message
															   forChannel:targetChannel
																 onClient:self];

			break;
		}
		case IRCLocalCommandNotifysound: // Command: NOTIFYSOUND
		{
			NSString *soundName = stringIn.tokenAsString;
			
			if (soundName.length == 0) {
				[self printInvalidSyntaxMessageForCommand:command];
				
				break;
			}

			[TLOSoundPlayer playAlertSound:soundName];

			break;
		}
		case IRCLocalCommandNotifyspeak: // Command: NOTIFYSPEAK
		{
			if (stringIn.length == 0) {
				[self printInvalidSyntaxMessageForCommand:command];
				
				break;
			}
			
			[[TXSharedApplication sharedSpeechSynthesizer] speak:stringIn.string];

			break;
		}
		case IRCLocalCommandQuery: // Command: QUERY
		{
			NSString *nickname = stringIn.tokenAsString;
			
			if (nickname.length == 0) {
				[self printInvalidSyntaxMessageForCommand:command];
				
				break;
			}

			if ([self stringIsNickname:nickname] == NO) {
				[self printDebugInformation:TXTLS(@"IRC[zef-q9]")];
				
				break;
			}

			IRCChannel *query = [self findChannelOrCreate:nickname isPrivateMessage:YES];

			[mainWindow() select:query];

			if (stringIn.length > 0) {
				[self sendText:stringIn asCommand:IRCRemoteCommandPrivmsg toChannel:query];
			}

			break;
		}
		case IRCLocalCommandQuote: // Command: QUOTE
		case IRCLocalCommandRaw: // Command: RAW
		{
			NSAssertReturnLoopBreak(self.isConnected);
			
			if (stringIn.length == 0) {
				[self printInvalidSyntaxMessageForCommand:command];
				
				break;
			}
			
			[self sendLine:stringIn.string];

			break;
		}
		case IRCLocalCommandQuit: // Command: QUIT
		{
			NSAssertReturnLoopBreak(self.isConnected);

			if (stringIn.length == 0) {
				[self quit];
			} else {
				[self quitWithComment:stringIn.string];
			}

			break;
		}
		case IRCLocalCommandNames: // Command: NAMES
		{
			NSAssertReturnLoopBreak(self.isLoggedIn);

			if (stringIn.length == 0) {
				[self printInvalidSyntaxMessageForCommand:command];
				
				break;
			}

			[self createHiddenCommandResponses];

			[self send:@"NAMES", stringIn.string, nil];

			break;
		}
		case IRCLocalCommandRecv: // Command: RECV
		{
			if (stringIn.length == 0) {
				[self printInvalidSyntaxMessageForCommand:command];

				break;
			}

			[self ircConnection:self.socket didReceiveData:stringIn.string];

			break;
		}
		case IRCLocalCommandSetcolor: // Command: SETCOLOR
		{
			if ([TPCPreferences disableNicknameColorHashing]) {
				[self printDebugInformation:TXTLS(@"IRC[026-qv]")];

				break;
			}

			NSString *nickname = stringIn.lowercaseGetToken;

			if (nickname.length == 0) {
				[self printInvalidSyntaxMessageForCommand:command];
				
				break;
			}

			if ([self stringIsNickname:nickname] == NO) {
				[self printDebugInformation:TXTLS(@"IRC[8dy-6f]", nickname)];

				break;
			}

			[menuController() memberChangeColor:nickname];

			break;
		}
		case IRCLocalCommandSetqueryname: // Command: SETQUERYNAME
		{
			if (targetChannel == nil || targetChannel.isPrivateMessage == NO) {
				[self printDebugInformation:TXTLS(@"IRC[m6o-z1]")];

				break;
			}

			NSString *nickname = stringIn.getTokenAsString;

			if (nickname.length == 0) {
				[self printInvalidSyntaxMessageForCommand:command];

				break;
			}

			if ([self stringIsNickname:nickname] == NO) {
				[self printDebugInformation:TXTLS(@"IRC[zef-q9]")];

				break;
			}

			IRCChannel *oldQuery = [self findChannel:nickname];

			if (oldQuery) {
				BOOL deleteOldQuery =
				[TDCAlert modalAlertWithMessage:TXTLS(@"Prompts[61s-jc]")
										  title:TXTLS(@"Prompts[d22-76]", oldQuery.name)
								  defaultButton:TXTLS(@"Prompts[mvh-ms]")
								alternateButton:TXTLS(@"Prompts[99q-gg]")];

				if (deleteOldQuery) {
					[worldController() destroyChannel:oldQuery];
				} else {
					break;
				}
			}

			targetChannel.name = nickname;

			[mainWindow() reloadTreeItem:targetChannel];

			[mainWindow() updateTitleFor:targetChannel];

			break;
		}
		case IRCLocalCommandServer: // Command: SERVER
		{
			if (stringIn.length == 0) {
				[self printInvalidSyntaxMessageForCommand:command];
				
				break;
			}

			[IRCExtras createConnectionToServer:stringIn.string channelList:nil connectWhenCreated:YES];

			break;
		}
		case IRCLocalCommandSslcontext: // Command: SSLCONTEXT
		{
			[self presentCertificateTrustInformation];

			break;
		}
		case IRCLocalCommandTage: // Command: TAGE
		{
			NSTimeInterval timePassed = [NSDate timeIntervalSinceNow:[TPCApplicationInfo applicationBirthday]];

			NSString *message = TXTLS(@"IRC[v9x-18]", TXHumanReadableTimeInterval(timePassed, NO, 0));

			if (targetChannel) {
				[self sendPrivmsg:message toChannel:targetChannel];
			} else {
				[self printDebugInformationToConsole:message];
			}

			break;
		}
		case IRCLocalCommandTimer: // Command: TIMER
		{
			if (stringIn.length == 0) {
				[self printDebugInformation:TXTLS(@"IRC[jj9-94]")];

				break;
			}

			NSString *action = stringIn.tokenAsString;

			/* Present help */
			if ([action isEqualToString:@"help"])
			{
				NSString *topic = stringIn.tokenAsString;

				[self printDebugInformation:TXTLS(@"IRC[aox-zz]")]; // divider

				if ([topic isEqualToStringIgnoringCase:@"add"])
				{
					[self printDebugInformationMultiline:TXTLS(@"IRC[6r0-il]")];
				}
				else if ([topic isEqualToStringIgnoringCase:@"remove"])
				{
					[self printDebugInformationMultiline:TXTLS(@"IRC[i2d-x5]")];
				}
				else if ([topic isEqualToStringIgnoringCase:@"list"])
				{
					[self printDebugInformationMultiline:TXTLS(@"IRC[x1n-ve]")];
				}
				else if ([topic isEqualToStringIgnoringCase:@"stop"])
				{
					[self printDebugInformationMultiline:TXTLS(@"IRC[bx2-n1]")];
				}
				else if ([topic isEqualToStringIgnoringCase:@"restart"])
				{
					[self printDebugInformationMultiline:TXTLS(@"IRC[r27-tv]")];
				}
				else
				{
					[self printDebugInformationMultiline:TXTLS(@"IRC[xkq-rt]")];
				}

				break;
			}

			/* Stop timer */
			if ([action isEqualToString:@"stop"])
			{
				NSString *identifier = stringIn.tokenAsString;

				if (identifier.length == 0) {
					[self printDebugInformation:TXTLS(@"IRC[p6l-o4]")];

					break;
				}

				IRCTimedCommand *timedCommand = [self timedCommandWithIdentifier:identifier];

				if (timedCommand == nil) {
					[self printDebugInformation:TXTLS(@"IRC[vzu-xh]", identifier)];

					break;
				}

				if (timedCommand.timerIsActive == NO) {
					[self printDebugInformation:TXTLS(@"IRC[ax6-n9]", identifier)];

					break;
				}

				[self stopTimedCommand:timedCommand];

				[self printDebugInformation:TXTLS(@"IRC[hs0-up]", identifier)];

				break;
			}

			/* Restart timer */
			if ([action isEqualToString:@"restart"])
			{
				NSString *identifier = stringIn.tokenAsString;

				if (identifier.length == 0) {
					[self printDebugInformation:TXTLS(@"IRC[p6l-o4]")];

					break;
				}

				IRCTimedCommand *timedCommand = [self timedCommandWithIdentifier:identifier];

				if (timedCommand == nil) {
					[self printDebugInformation:TXTLS(@"IRC[vzu-xh]", identifier)];

					break;
				}

				if ([self restartTimedCommand:timedCommand]) {
					[self printDebugInformation:TXTLS(@"IRC[qb7-mi]", identifier)];
				} else {
					[self printDebugInformation:TXTLS(@"IRC[dgp-d4]", identifier)];
				}

				break;
			}

			/* Remove timer */
			if ([action isEqualToString:@"remove"])
			{
				NSString *identifier = stringIn.tokenAsString;

				if (identifier.length == 0) {
					[self printDebugInformation:TXTLS(@"IRC[p6l-o4]")];

					break;
				}

				if ([identifier isEqualToStringIgnoringCase:@"all"]) {
					[self removeTimedCommands];

					[self printDebugInformation:TXTLS(@"IRC[808-bs]")];

					break;
				}

				IRCTimedCommand *timedCommand = [self timedCommandWithIdentifier:identifier];

				if (timedCommand == nil) {
					[self printDebugInformation:TXTLS(@"IRC[vzu-xh]", identifier)];

					break;
				}

				[self removeTimedCommand:timedCommand];

				[self printDebugInformation:TXTLS(@"IRC[p7s-is]", identifier)];

				break;
			}

			/* List timers */
			if ([action isEqualToString:@"list"])
			{
				NSArray *timedCommands = [self listOfTimedCommands];

				NSUInteger numberOfTimers = timedCommands.count;

				if (numberOfTimers == 0) {
					[self printDebugInformation:TXTLS(@"IRC[pqk-5k]")];

					break;
				} else if (numberOfTimers == 1) {
					[self printDebugInformation:TXTLS(@"IRC[6ts-oi]", numberOfTimers)];
				} else {
					[self printDebugInformation:TXTLS(@"IRC[q1m-1e]", numberOfTimers)];
				}

				for (IRCTimedCommand *timedCommand in timedCommands) {
					NSString *description = [self descriptionForTimedCommand:timedCommand];

					[self printDebugInformation:description];
				}

				break;
			}

			/* Add timer */
			/*
			 This command is designed to be backwards compatible.

			 Old syntax:
			 "/timer <seconds> <command>"

			 New syntax:
			 "/timer <seconds> <repeat> <command>"

			 We check the value of the second argument. If it's a number,
			 then we treat it as the new syntax which means that number
			 is the repeat count for the timer.
			 */

			/* Parse interval */
			NSString *intervalString = action;

			if (intervalString.length == 0) {
				[self printDebugInformation:TXTLS(@"IRC[jj9-94]")];

				break;
			}

			NSInteger interval = intervalString.integerValue;

			if (interval <= 0) {
				[self printDebugInformation:TXTLS(@"IRC[327-pv]")];

				break;
			}

			/* Parse repeat count and/or command */
			/*
			 Repeat argument is treated as such:
			   == 0 — repeat and never stop
			   == 1 - perform timer once, do not repeat
			   > 1 — repeat number of times
			 */
			NSString *repeatString = stringIn.tokenAsString;

			NSInteger repeat = NSNotFound;

			NSString *command = stringIn.trimmedString;

			if ([repeatString contentsIsOfType:CSStringTypeAnyNumber])
			{
				/* Contents of second argument is a number,
				 which means we want to treat it as the repeat count. */
				repeat = repeatString.integerValue;
			}
			else
			{
				/* Contents of second argument is NOT a number,
				 which means we need to merge that back with the
				 remainder of the command value. */
				if (command.length == 0) {
					command = repeatString;
				} else {
					command = [repeatString stringByAppendingFormat:@" %@", command];
				}
			}

			/* Perform additional validation */
			if (repeat < 0) {
				[self printDebugInformation:TXTLS(@"IRC[eud-kc]")];

				break;
			}

			if (command.length == 0) {
				[self printDebugInformation:TXTLS(@"IRC[jj9-94]")];

				break;
			}

			/* If we have no repeat value, then treat is one pass. */
			if (repeat == NSNotFound) {
				repeat = 1;
			}

			/* Add timer */
			IRCTimedCommand *timedCommand = nil;

			if (targetChannel == nil) {
				timedCommand = [[IRCTimedCommand alloc] initWithCommand:command onClient:self];
			} else {
				timedCommand = [[IRCTimedCommand alloc] initWithCommand:command onClient:self inChannel:targetChannel];
			}

			[self addTimedCommand:timedCommand];

			[self startTimedCommand:timedCommand interval:interval onRepeat:(repeat != 1) iterations:repeat];

			break;
		}
		case IRCLocalCommandT: // Command: T
		case IRCLocalCommandTopic: // Command: TOPIC
		{
			NSAssertReturnLoopBreak(self.isLoggedIn);

			if ([self stringIsChannelName:stringIn.string] == NO) {
				if (targetChannel && targetChannel.isChannel) {
					targetChannelName = targetChannel.name;
				} else {
					break;
				}
			} else {
				targetChannelName = stringIn.tokenAsString;
			}

			NSString *topic = stringIn.stringFormattedForIRC;

			NSUInteger topicLength = topic.length;

			if (topicLength == 0) {
				[self send:@"TOPIC", targetChannelName, nil];

				return;
			}

			NSUInteger topicLengthMaximum = self.supportInfo.maximumTopicLength;

			if (topicLengthMaximum > 0 && topicLength > topicLengthMaximum) {
				[self printDebugInformation:TXTLS(@"IRC[1oo-3b]", self.networkNameAlt, topicLengthMaximum)];
			}

			[self send:@"TOPIC", targetChannelName, topic, nil];

			break;
		}
		case IRCLocalCommandUmode: // Command: UMODE
		{
			NSAssertReturnLoopBreak(self.isLoggedIn);

			if (stringIn.length == 0) {
				[self send:@"MODE", self.userNickname, nil];
			} else {
				[self send:@"MODE", self.userNickname, stringIn.string, nil];
			}

			break;
		}
		case IRCLocalCommandUnmute: // Command: UNMUTE
		{
			if ([TPCPreferences soundIsMuted] == NO) {
				[self printDebugInformation:TXTLS(@"IRC[5rf-mj]")];
			} else {
				[self printDebugInformation:TXTLS(@"IRC[190-f2]")];

				[menuController() toggleMuteOnNotificationSoundsShortcutOn:NO];
			}

			break;
		}
		case IRCLocalCommandWallops: // Command: WALLOPS
		{
			NSAssertReturnLoopBreak(self.isLoggedIn);

			if (stringIn.length == 0) {
				[self printInvalidSyntaxMessageForCommand:command];
				
				break;
			}

			[self send:@"WALLOPS", stringIn.string, nil];

			break;
		}
		case IRCLocalCommandMonitor: // Command: MONITOR
		case IRCLocalCommandWatch: // Command: WATCH
		{
			NSAssertReturnLoopBreak(self.isLoggedIn);

			if (stringIn.length == 0) {
				[self printInvalidSyntaxMessageForCommand:command];
				
				break;
			}

			BOOL isClearCommand = NO;
			BOOL isModifierCommand = NO;

			/* UnrealIRCd splits arguments using space and loops through all of them
			 which means a modifier or clear command can appear at the same time. */
			NSArray *arguments = [stringIn.string componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];

			for (NSString *argument in arguments) {
				if ([argument hasPrefix:@"-"] || [stringIn.string hasPrefix:@"+"] ) {
					isModifierCommand = YES;

					break;
				} else if ([argument isEqualIgnoringCase:@"c"]) {
					isClearCommand = YES;
				}
			}

			if (isModifierCommand) {
				[self printDebugInformation:TXTLS(@"IRC[khw-4y]")];

				break;
			}

			/* The clear command doesn't produce a result */
			if (isClearCommand == NO) {
				[self createHiddenCommandResponses];
			}

			[self sendCommand:uppercaseCommand withData:stringIn.string];

			break;
		}
		case IRCLocalCommandWho: // Command: WHO
		{
			NSAssertReturnLoopBreak(self.isLoggedIn);

			if (stringIn.length == 0) {
				[self printInvalidSyntaxMessageForCommand:command];
				
				break;
			}

			[self createHiddenCommandResponses];

			[self.requestedCommands recordWhoRequestOpenedAsVisible];

			[self send:@"WHO", stringIn.string, nil];

			break;
		}
		case IRCLocalCommandWeights: // Command: WEIGHTS
		{
			if (targetChannel == nil || targetChannel.isChannel == NO) {
				[self printDebugInformation:TXTLS(@"IRC[g01-qn]")];

				break;
			}

			[self printDebugInformation:TXTLS(@"IRC[zud-u3]", targetChannel.name)];

			BOOL haveWeights = NO;

			for (IRCChannelUser *member in targetChannel.memberList) {
				double incomingWeight = member.incomingWeight;
				double outgoingWeight = member.outgoingWeight;

				CGFloat weight = (incomingWeight + outgoingWeight);

				if (weight > 0) {
					haveWeights = YES;

					[self printDebugInformation:TXTLS(@"IRC[24r-8c]",
							member.user.nickname, outgoingWeight, incomingWeight, weight)];
				}
			}

			if (haveWeights == NO) {
				[self printDebugInformation:TXTLS(@"IRC[dje-41]")];
			}

			break;
		}
		case IRCLocalCommandWhois: // Command: WHOIS
		{
			NSAssertReturnLoopBreak(self.isLoggedIn);

			NSString *nickname1 = stringIn.tokenAsString;

			if (nickname1.length == 0) {
				if (targetChannel && targetChannel.isPrivateMessage) {
					nickname1 = targetChannel.name;
				} else {
					[self printInvalidSyntaxMessageForCommand:command];
					
					break;
				}
			}

			NSString *nickname2 = stringIn.tokenAsString;
	
			if (nickname2.length == 0) {
				[self send:@"WHOIS", nickname1, nickname1, nil];
			} else {
				[self send:@"WHOIS", nickname1, nickname2, nil];
			}

			break;
		}
		case IRCLocalCommandMe: // Command: ME
		case IRCLocalCommandMsg: // Command: MSG
		case IRCLocalCommandNotice: // Command: NOTICE
		case IRCLocalCommandOmsg: // Command: OMSG
		case IRCLocalCommandOnotice: // Command: ONOTICE
		case IRCLocalCommandSme: // Command: SME
		case IRCLocalCommandSmsg: // Command: SMSG
		case IRCLocalCommandUme: // Command: UME
		case IRCLocalCommandUmsg: // Command: UMSG
		case IRCLocalCommandUnotice: // Command: UNOTICE
		{
			/* Where would se send data to? */
			if (self.isLoggedIn == NO) {
				[self printDebugInformationToConsole:TXTLS(@"IRC[6rj-2r]")];

				break;
			}

			/* Establish context for comand */
			NSString *channelNamePrefix = nil;

			BOOL isOperatorMessage = NO;
			BOOL isSecretMessage = NO;
			BOOL isUnencryptedMessage = NO;

			NSString *commandToSend = nil;

			TVCLogLineType lineType = TVCLogLineTypeUndefined;

			if (commandNumeric == IRCLocalCommandMsg ||
				commandNumeric == IRCLocalCommandOmsg ||
				commandNumeric == IRCLocalCommandSmsg ||
				commandNumeric == IRCLocalCommandUmsg)
			{
				commandToSend = @"PRIVMSG";

				lineType = TVCLogLineTypePrivateMessage;

				isOperatorMessage = (commandNumeric == IRCLocalCommandOmsg);
				isSecretMessage = (commandNumeric == IRCLocalCommandSmsg);
				isUnencryptedMessage = (commandNumeric == IRCLocalCommandUmsg);
			}
			else if (commandNumeric == IRCLocalCommandMe ||
					 commandNumeric == IRCLocalCommandSme ||
					 commandNumeric == IRCLocalCommandUme)
			{
				commandToSend = @"PRIVMSG";

				lineType = TVCLogLineTypeAction;

				isSecretMessage = (commandNumeric == IRCLocalCommandSme);
				isUnencryptedMessage = (commandNumeric == IRCLocalCommandUme);
			}
			else if (commandNumeric == IRCLocalCommandNotice || // Command: NOTICE
					 commandNumeric == IRCLocalCommandOnotice || // Command: ONOTICE
					 commandNumeric == IRCLocalCommandUnotice)   // Command: UNOTICE
			{
				commandToSend = @"NOTICE";

				lineType = TVCLogLineTypeNotice;

				isOperatorMessage = (commandNumeric == IRCLocalCommandOnotice);
				isUnencryptedMessage = (commandNumeric == IRCLocalCommandUnotice);
			}

			if (isOperatorMessage) {
				channelNamePrefix = [self.supportInfo statusMessagePrefixForModeSymbol:@"o"];

				/* If the user is sending an operator message and the user mode +o does 
				 not exist, then fail here. The user may be trying to send something 
				 secret with an expectation for privacy and we cannot deliver that. */
				if (channelNamePrefix == nil) {
					[self printDebugInformation:TXTLS(@"IRC[54l-h7]", @"o")];

					break;
				}
			}

			/* Pick the best target */
			/* All actions except (SME) should use the target channel */
			/* Operator messages should use the target channel unless the
			 string in is a channel name */
			/* All other scenarios use the string in (token) */
			if (isSecretMessage == NO && lineType == TVCLogLineTypeAction && targetChannel) {
				if (targetChannel.isUtility) {
					[self printDebugInformation:TXTLS(@"sxf-qx")];

					break;
				}

				targetChannelName = targetChannel.name;
			} else if (isOperatorMessage && [self stringIsChannelName:stringIn.string] == NO && targetChannel.isChannel) {
				targetChannelName = targetChannel.name;
			} else {
				targetChannelName = stringIn.tokenAsString;
			}

			if (targetChannelName.length == 0) {
				[self printInvalidSyntaxMessageForCommand:command];

				break;
			}

			/* Actions are allowed to have an empty message but all other
			 types are not. Empty actions use a whitespace. */
			if (stringIn.length == 0) {
				if (lineType == TVCLogLineTypeAction) {
					[stringIn replaceCharactersInRange:NSMakeRange(0, 0)
											withString:@" "];
				} else {
					break;
				}
			}

			/* At this point, the following will occur:
			 1. Each destination is looped over
				1. Prefix characters are removed from the destination name
				2. Try to find channel that already exists which matches
				   the destination. If a channel does not exist, then we 
				   create one depending on whether this is a secret message.
				3. The message is then encrypted and sent off.
			 */
			NSArray *destinations = [targetChannelName componentsSeparatedByString:@","];

			IRCChannel *destinationToSelect = nil;

			for (__strong NSString *destinationName in destinations) {
				/* If the user prefixed the target with a mode (e.g. +#channel)
				 to indicate that they want the message only seen by that group
				 of users, then we first have to remove that prefix to perform
				 our own processing of the target. When it comes time to send 
				 the message, then the prefix is added back to the target name. */
				NSString *destinationNamePrefix = [self.supportInfo extractStatusMessagePrefixFromChannelNamed:destinationName];

				if (destinationNamePrefix.length == 0) {
					destinationNamePrefix = channelNamePrefix;
				} else {
					destinationName = [destinationName substringFromIndex:1];
				}

				/* Locate object that matches destination */
				IRCChannel *destination = [self findChannel:destinationName];

				if (isSecretMessage == NO) {
					/* If the destination does not exist and this isn't a secret
					 message, then create a private message if the destination 
					 is believed to be a user. */
					if (destination == nil && [self stringIsNickname:destinationName]) {
						destination = [worldController() createPrivateMessage:destinationName onClient:self];
					}

					/* Define the channel that will be selected */
					if ([TPCPreferences giveFocusOnMessageCommand]) {
						if (destinationToSelect == nil) {
							destinationToSelect = destination;
						}
					}
				}

				/* Add prefix back if the destination is a channel */
				BOOL destinationIsChannel =
				(destination.isChannel || (destination == nil && [self stringIsChannelName:destinationName]));

				if (destinationNamePrefix && destinationIsChannel) {
					destinationName = [destinationNamePrefix stringByAppendingString:destinationName];
				}

				/* Break text up into substrings which can then be sent. */
				NSMutableAttributedString *lineMutable = [stringIn mutableCopy];

				while (lineMutable.length > 0)
				{
					NSString *unencryptedMessage = [lineMutable stringFormattedForChannel:destinationName onClient:self withLineType:lineType];

					TLOEncryptionManagerEncodingDecodingCallbackBlock encryptionBlock = ^(NSString *originalString, BOOL wasEncrypted) {
						if (destination == nil) {
							return;
						}

						if ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityEchoMessage] && wasEncrypted == NO) {
							return;
						}

						[self print:originalString
								 by:self.userNickname
						  inChannel:destination
							 asType:lineType
							command:command
						 receivedAt:[NSDate date]
						isEncrypted:wasEncrypted];
					};

					TLOEncryptionManagerInjectCallbackBlock injectionBlock = ^(NSString *encodedString) {
						NSString *sendMessage = encodedString;

						if (lineType == TVCLogLineTypeAction) {
							sendMessage = [NSString stringWithFormat:@"%cACTION %@%c", 0x01, sendMessage, 0x01];
						}

						[self send:commandToSend, destinationName, sendMessage, nil];
					};

					if (destination == nil || isUnencryptedMessage) {
						encryptionBlock(unencryptedMessage, NO);

						injectionBlock(unencryptedMessage);

						continue;
					}

					[self encryptMessage:unencryptedMessage
							  directedAt:destination.name
						encodingCallback:encryptionBlock
					   injectionCallback:injectionBlock];
				}
			} // destination for()

			/* Focus destination */
			if (destinationToSelect) {
				[mainWindow() select:destinationToSelect];
			}

			break;
		}
		default:
		{
			/* Find an addon responsible for this command. */
			NSString *addonPath = nil;

			BOOL pluginFound = NO;
			BOOL scriptFound = NO;

			BOOL commandIsReserved = NO;

			[sharedPluginManager() findHandlerForOutgoingCommand:lowercaseCommand path:&addonPath isReserved:&commandIsReserved isScript:&scriptFound isExtension:&pluginFound];

			if (commandIsReserved) {
				[sharedPluginManager() extrasInstallerAskUserIfTheyWantToInstallCommand:lowercaseCommand];
			}

			/* Perform script or plugin. */
			if (pluginFound && scriptFound)
			{
				[self printDebugInformation:TXTLS(@"IRC[d3c-9b]", uppercaseCommand)];
				
				break;
			}
			else if (pluginFound && scriptFound == NO)
			{
				[self processBundlesUserMessage:stringIn.string command:lowercaseCommand];

				break;
			}
			else if (pluginFound == NO && scriptFound)
			{
				NSMutableDictionary<NSString *, NSString *> *context = [NSMutableDictionary dictionaryWithCapacity:3];

				[context maybeSetObject:stringIn.string forKey:@"inputString"];

				[context maybeSetObject:addonPath forKey:@"path"];

				[context maybeSetObject:targetChannel.name forKey:@"targetChannel"];

				[self executeTextualCmdScriptInContext:context];

				break;
			}

			/* Send input to server */
			[self sendCommand:uppercaseCommand withData:stringIn.string];

			break;
		}
	}
}

- (void)sendCommand:(NSString *)command withData:(NSString *)data
{
	NSParameterAssert(command != nil);
	NSParameterAssert(data != nil);

	NSString *stringToSend = [NSString stringWithFormat:@"%@ %@", command, data];

	[self sendLine:stringToSend];
}

- (void)printInvalidSyntaxMessageForCommand:(NSString *)command
{
	NSParameterAssert(command != nil);
	
	NSString *syntax = [IRCCommandIndex syntaxForLocalCommand:command];
	
	if (syntax == nil) {
		return;
	}
	
	[self printDebugInformation:TXTLS(@"IRC[atq-93]", syntax)];
}

#pragma mark -
#pragma mark Log File

- (void)reopenLogFileIfNeeded
{
	if ([TPCPreferences logToDiskIsEnabled]) {
		if ( self.logFile) {
			[self.logFile reopenIfNeeded];
		}
	} else {
		[self closeLogFile];
	}
}

- (void)closeLogFile
{
	if (self.logFile == nil) {
		return;
	}

	[self.logFile close];
}

- (void)writeToLogLineToLogFile:(TVCLogLine *)logLine
{
	NSParameterAssert(logLine != nil);

	if ([TPCPreferences logToDiskIsEnabled] == NO) {
		return;
	}

	// Perform addition before if statement to avoid infinite loop
	self.logFileSessionCount += 1;

	if (self.logFileSessionCount == 1) {
		[self logFileWriteSessionBegin];
	}

	if (self.logFile == nil) {
		self.logFile = [[TLOFileLogger alloc] initWithClient:self];
	}

	[self.logFile writeLogLine:logLine];
}

- (void)logFileRecordSessionChanged:(BOOL)toNewSession inChannel:(nullable IRCChannel *)channel
{
	NSParameterAssert(channel.isUtility == NO);

	NSString *localization = nil;

	if (toNewSession) {
		localization = @"IRC[qrg-ua]";
	} else {
		localization = @"IRC[d5d-uy]";
	}

	TVCLogLineMutable *logLine = [TVCLogLineMutable new];

	/* ============================ */

	logLine.messageBody = @" ";

	if (channel) {
		[channel writeToLogLineToLogFile:logLine];
	} else {
		[self writeToLogLineToLogFile:logLine];
	}

	/* ============================ */

	logLine.messageBody = TXTLS(localization);

	if (channel) {
		[channel writeToLogLineToLogFile:logLine];
	} else {
		[self writeToLogLineToLogFile:logLine];
	}

	/* ============================ */

	logLine.messageBody = @" ";

	if (channel) {
		[channel writeToLogLineToLogFile:logLine];
	} else {
		[self writeToLogLineToLogFile:logLine];
	}
}

- (void)endLoggingSessions
{
	for (IRCChannel *channel in self.channelList) {
		if (channel.isUtility) {
			continue;
		}

		[channel logFileWriteSessionEnd];
	}

	[self logFileWriteSessionEnd];
}

- (void)logFileWriteSessionBegin
{
	[self logFileRecordSessionChanged:YES inChannel:nil];
}

- (void)logFileWriteSessionEnd
{
	[self logFileRecordSessionChanged:NO inChannel:nil];

	self.logFileSessionCount = 0;
}

#pragma mark -
#pragma mark Print

- (NSString *)formatNickname:(NSString *)nickname inChannel:(nullable IRCChannel *)channel
{
	return [self formatNickname:nickname inChannel:channel withFormat:nil];
}

- (NSString *)formatNickname:(NSString *)nickname inChannel:(nullable IRCChannel *)channel withFormat:(nullable NSString *)format
{
	NSParameterAssert(nickname != nil);

	if (format.length == 0) {
		format = themeSettings().themeNicknameFormat;
	}

	if (format.length == 0) {
		format = [TPCPreferences themeNicknameFormat];
	}

	if (format.length == 0) {
		format = [TPCPreferences themeNicknameFormatDefault];
	}

	NSString *modeSymbol = @"";

	if (channel.isChannel) {
		IRCChannelUser *member = [channel findMember:nickname];

		if (member) {
			modeSymbol = member.mark;
		}
	}

	NSString *formatMarker = @"%";

	NSString *chunk = nil;

	NSScanner *scanner = [NSScanner scannerWithString:format];

	scanner.charactersToBeSkipped = nil;

	NSMutableString *buffer = [NSMutableString new];

	while (scanner.atEnd == NO) {
		if ([scanner scanUpToString:formatMarker intoString:&chunk]) {
			[buffer appendString:chunk];
		}

		if ([scanner scanString:formatMarker intoString:nil] == NO) {
			break;
		}

		NSInteger paddingWidth = 0;

		[scanner scanInteger:&paddingWidth];

		/* Read the output type marker */
		NSString *outputValue = nil;

		if ([scanner scanString:@"@" intoString:nil]) {
			outputValue = modeSymbol;
		} else if ([scanner scanString:@"n" intoString:nil]) {
			outputValue = nickname;
		} else if ([scanner scanString:formatMarker intoString:nil]) {
			outputValue = formatMarker;
		}

		if (outputValue) {
			if (paddingWidth < 0 && ABS(paddingWidth) > outputValue.length) {
				NSString *paddedString = [@"" stringByPaddingToLength:(ABS(paddingWidth) - outputValue.length) withString:@" " startingAtIndex:0];

				[buffer appendString:paddedString];
			}

			[buffer appendString:outputValue];

			if (paddingWidth > 0 && paddingWidth > outputValue.length) {
				NSString *paddedString = [@"" stringByPaddingToLength:(paddingWidth - outputValue.length) withString:@" " startingAtIndex:0];

				[buffer appendString:paddedString];
			}
		}
	}

	return [buffer copy];
}

- (void)printAndLog:(TVCLogLine *)logLine completionBlock:(TVCLogControllerPrintOperationCompletionBlock)completionBlock
{
	NSParameterAssert(logLine != nil);

	[self.viewController print:logLine completionBlock:completionBlock];

	[self writeToLogLineToLogFile:logLine];
}

- (void)print:(NSString *)messageBody by:(nullable NSString *)nickname inChannel:(nullable IRCChannel *)channel asType:(TVCLogLineType)lineType command:(NSString *)command
{
	[self print:messageBody by:nickname inChannel:channel asType:lineType command:command receivedAt:[NSDate date] isEncrypted:NO escapeMessage:YES referenceMessage:nil completionBlock:nil];
}

- (void)print:(NSString *)messageBody by:(nullable NSString *)nickname inChannel:(nullable IRCChannel *)channel asType:(TVCLogLineType)lineType command:(NSString *)command escapeMessage:(BOOL)escapeMessage
{
	[self print:messageBody by:nickname inChannel:channel asType:lineType command:command receivedAt:[NSDate date] isEncrypted:NO escapeMessage:escapeMessage referenceMessage:nil completionBlock:nil];
}

- (void)print:(NSString *)messageBody by:(nullable NSString *)nickname inChannel:(nullable IRCChannel *)channel asType:(TVCLogLineType)lineType command:(NSString *)command receivedAt:(NSDate *)receivedAt
{
	[self print:messageBody by:nickname inChannel:channel asType:lineType command:command receivedAt:receivedAt isEncrypted:NO escapeMessage:YES referenceMessage:nil completionBlock:nil];
}

- (void)print:(NSString *)messageBody by:(nullable NSString *)nickname inChannel:(nullable IRCChannel *)channel asType:(TVCLogLineType)lineType command:(NSString *)command receivedAt:(NSDate *)receivedAt isEncrypted:(BOOL)isEncrypted
{
	[self print:messageBody by:nickname inChannel:channel asType:lineType command:command receivedAt:receivedAt isEncrypted:isEncrypted escapeMessage:YES referenceMessage:nil completionBlock:nil];
}

- (void)print:(NSString *)messageBody by:(nullable NSString *)nickname inChannel:(nullable IRCChannel *)channel asType:(TVCLogLineType)lineType command:(nullable NSString *)command receivedAt:(NSDate *)receivedAt isEncrypted:(BOOL)isEncrypted referenceMessage:(nullable IRCMessage *)referenceMessage
{
	[self print:messageBody by:nickname inChannel:channel asType:lineType command:command receivedAt:receivedAt isEncrypted:isEncrypted escapeMessage:YES referenceMessage:referenceMessage completionBlock:nil];
}

- (void)print:(NSString *)messageBody by:(nullable NSString *)nickname inChannel:(nullable IRCChannel *)channel asType:(TVCLogLineType)lineType command:(nullable NSString *)command receivedAt:(NSDate *)receivedAt isEncrypted:(BOOL)isEncrypted referenceMessage:(nullable IRCMessage *)referenceMessage completionBlock:(nullable TVCLogControllerPrintOperationCompletionBlock)completionBlock
{
	[self print:messageBody by:nickname inChannel:channel asType:lineType command:command receivedAt:receivedAt isEncrypted:isEncrypted escapeMessage:YES referenceMessage:referenceMessage completionBlock:completionBlock];
}

- (void)print:(NSString *)messageBody by:(nullable NSString *)nickname inChannel:(nullable IRCChannel *)channel asType:(TVCLogLineType)lineType command:(nullable NSString *)command receivedAt:(NSDate *)receivedAt isEncrypted:(BOOL)isEncrypted escapeMessage:(BOOL)escapeMessage referenceMessage:(nullable IRCMessage *)referenceMessage completionBlock:(nullable TVCLogControllerPrintOperationCompletionBlock)completionBlock
{
	NSParameterAssert(messageBody != nil);
	NSParameterAssert(command != nil || referenceMessage != nil);

	if (self.isTerminating) {
		return;
	}

	/* If an operation does not specify a command value, 
	 then try to obtain it from the reference message. */
	if (command == nil) {
		command = referenceMessage.command;
	}

	/* Prevent stupid plugin authors */
	if ([channel isKindOfClass:[IRCChannel class]] == NO) {
		channel = nil;
	}

	/* Do not print this message? */
	if (channel) {
		if ([self outputRuleMatchedInMessage:messageBody inChannel:channel]) {
			return;
		}
	}

	/* Define where the message originated */
	NSString *localNickname = self.userNickname;

	TVCLogLineMemberType memberType = TVCLogLineMemberTypeNormal;

	if ([nickname isEqualToString:localNickname]) {
		memberType = TVCLogLineMemberTypeLocalUser;
	}

	/* Define list of highlight keywords */
	BOOL matchHighlights =
		(channel &&
		 channel.config.ignoreHighlights == NO &&
		 (lineType == TVCLogLineTypePrivateMessage || lineType == TVCLogLineTypeAction) &&
		 memberType == TVCLogLineMemberTypeNormal);

	NSMutableArray<NSString *> *excludeKeywords = nil;
	NSMutableArray<NSString *> *matchKeywords = nil;
	
	if (matchHighlights) {
		/* Global highlight keywords */
		excludeKeywords = [[TPCPreferences highlightExcludeKeywords] mutableCopy];
		matchKeywords = [[TPCPreferences highlightMatchKeywords] mutableCopy];

		/* Self nickname keyword */
		if ([TPCPreferences highlightMatchingMethod] != TXNicknameHighlightMatchTypeRegularExpression &&
			[TPCPreferences highlightCurrentNickname])
		{
			[matchKeywords addObjectWithoutDuplication:localNickname];
		}

		/* Client/channel specific keywords */
		NSArray *clientHighlightList = self.config.highlightList;

		NSString *channelId = channel.uniqueIdentifier;

		for (IRCHighlightMatchCondition *e in clientHighlightList) {
			NSString *matchChannelId = e.matchChannelId;
			
			if (matchChannelId.length > 0) {
				if ([matchChannelId isEqualToString:channelId] == NO) {
					continue;
				}
			}
			
			if (e.matchIsExcluded) {
				[excludeKeywords addObjectWithoutDuplication:e.matchKeyword];
			} else {
				[matchKeywords addObjectWithoutDuplication:e.matchKeyword];
			}
		}
	} // matchKeywords

	if (lineType == TVCLogLineTypeActionNoHighlight) {
		lineType = TVCLogLineTypeAction;
	} else if (lineType == TVCLogLineTypePrivateMessageNoHighlight) {
		lineType = TVCLogLineTypePrivateMessage;
	}

	/* Renderer attributes */
	NSDictionary<NSString *, id> *rendererAttributes = nil;

	if (escapeMessage == NO) {
		rendererAttributes = @{
			TVCLogRendererConfigurationDoNotEscapeBodyAttribute : @(YES)
		};
	}

	/* Create new log entry */
	TVCLogLineMutable *logLine = [TVCLogLineMutable new];

	logLine.command	= command.lowercaseString;

	logLine.lineType = lineType;
	logLine.memberType = memberType;

	logLine.isEncrypted = isEncrypted;

	logLine.excludeKeywords = excludeKeywords;
	logLine.highlightKeywords = matchKeywords;

	logLine.rendererAttributes = rendererAttributes;

	logLine.nickname = nickname;

	logLine.messageBody = messageBody;

	/* Has the date changed */
	TVCLogLine *lastLine = ((channel) ? channel.lastLine : self.lastLine);

	if (lastLine == nil) {
		logLine.isFirstForDay = YES;
	} else {
		logLine.isFirstForDay = ([receivedAt isInSameDayAsDate:lastLine.receivedAt] == NO);
	}

	logLine.receivedAt = receivedAt;

	/* Print to server console if there is no channel */
	if (channel == nil) {
		[self printAndLog:logLine completionBlock:completionBlock];

		return;
	}

	/* Add scrollback marker to channel if conditions are met */
	if ([TPCPreferences autoAddScrollbackMark]) {
		if ([mainWindow() isItemVisible:channel] == NO || mainWindow().mainWindow == NO) {
			if (channel.isUnread == NO &&
				(lineType == TVCLogLineTypePrivateMessage ||
				 lineType == TVCLogLineTypeAction ||
				 lineType == TVCLogLineTypeNotice))
			{
				[channel.viewController mark];
			}
		}
	}

	/* Print to channel */
	[channel print:logLine completionBlock:completionBlock];
}

- (void)printReply:(IRCMessage *)message
{
	[self printReply:message inChannel:nil];
}

- (void)printReply:(IRCMessage *)message inChannel:(nullable IRCChannel *)channel
{
	[self printReply:message inChannel:channel withSequence:1];
}

- (void)printReply:(IRCMessage *)message inChannel:(nullable IRCChannel *)channel withSequence:(NSUInteger)sequence
{
	NSParameterAssert(message != nil);

	[self print:[message sequence:sequence] by:nil inChannel:channel asType:TVCLogLineTypeDebug command:message.command receivedAt:message.receivedAt];
}

- (void)printUnknownReply:(IRCMessage *)message
{
	[self printUnknownReply:message inChannel:nil];
}

- (void)printUnknownReply:(IRCMessage *)message inChannel:(nullable IRCChannel *)channel
{
	[self printUnknownReply:message inChannel:channel withSequence:1];
}

- (void)printUnknownReply:(IRCMessage *)message inChannel:(nullable IRCChannel *)channel withSequence:(NSUInteger)sequence
{
	NSParameterAssert(message != nil);

	[self print:[message sequence:sequence] by:nil inChannel:channel asType:TVCLogLineTypeDebug command:message.command receivedAt:message.receivedAt];
}

- (void)printErrorReply:(IRCMessage *)message
{
	[self printErrorReply:message inChannel:nil];
}

- (void)printErrorReply:(IRCMessage *)message inChannel:(nullable IRCChannel *)channel
{
	[self printErrorReply:message inChannel:channel withSequence:NSNotFound];
}

- (void)printErrorReply:(IRCMessage *)message inChannel:(nullable IRCChannel *)channel withSequence:(NSUInteger)sequence
{
	NSParameterAssert(message != nil);

	NSString *sequenceMessage = nil;

	if (sequence == NSNotFound) {
		sequenceMessage = message.sequence;
	} else {
		sequenceMessage = [message sequence:sequence];
	}

	NSString *errorMessage = TXTLS(@"IRC[3yo-gw]", message.commandNumeric, sequenceMessage);

	[self print:errorMessage by:nil inChannel:channel asType:TVCLogLineTypeDebug command:message.command];
}

- (void)printError:(NSString *)errorMessage asCommand:(NSString *)command
{
	[self print:errorMessage by:nil inChannel:nil asType:TVCLogLineTypeDebug command:command];
}

- (void)printDebugInformationToConsole:(NSString *)message
{
	[self printDebugInformationToConsole:message asCommand:TVCLogLineDefaultCommandValue escapeMessage:YES];
}

- (void)printDebugInformationToConsole:(NSString *)message asCommand:(NSString *)command
{
	[self printDebugInformationToConsole:message asCommand:command escapeMessage:YES];
}

- (void)printDebugInformationToConsole:(NSString *)message escapeMessage:(BOOL)escapeMessage
{
	[self printDebugInformationToConsole:message asCommand:TVCLogLineDefaultCommandValue escapeMessage:escapeMessage];
}

- (void)printDebugInformationToConsole:(NSString *)message asCommand:(NSString *)command escapeMessage:(BOOL)escapeMessage
{
	[self print:message by:nil inChannel:nil asType:TVCLogLineTypeDebug command:command escapeMessage:escapeMessage];
}

- (void)printDebugInformation:(NSString *)message
{
	[self printDebugInformation:message asCommand:TVCLogLineDefaultCommandValue escapeMessage:YES];
}

- (void)printDebugInformationMultiline:(NSString *)message
{
	[message enumerateSplitOnNewLinesWithBlock:^(NSString *sequence, BOOL *stop) {
		[self printDebugInformation:sequence];
	}];
}

- (void)printDebugInformation:(NSString *)message asCommand:(NSString *)command
{
	[self printDebugInformation:message asCommand:command escapeMessage:YES];
}

- (void)printDebugInformation:(NSString *)message escapeMessage:(BOOL)escapeMessage
{
	[self printDebugInformation:message asCommand:TVCLogLineDefaultCommandValue escapeMessage:escapeMessage];
}

- (void)printDebugInformation:(NSString *)message asCommand:(NSString *)command escapeMessage:(BOOL)escapeMessage
{
	IRCChannel *channel = [mainWindow() selectedChannelOn:self];

	[self printDebugInformation:message inChannel:channel asCommand:TVCLogLineDefaultCommandValue escapeMessage:YES];
}

- (void)printDebugInformation:(NSString *)message inChannel:(IRCChannel *)channel
{
	[self printDebugInformation:message inChannel:channel asCommand:TVCLogLineDefaultCommandValue escapeMessage:YES];
}

- (void)printDebugInformation:(NSString *)message inChannel:(IRCChannel *)channel asCommand:(NSString *)command
{
	[self print:message by:nil inChannel:channel asType:TVCLogLineTypeDebug command:command escapeMessage:YES];
}

- (void)printDebugInformation:(NSString *)message inChannel:(IRCChannel *)channel escapeMessage:(BOOL)escapeMessage
{
	[self printDebugInformation:message inChannel:channel asCommand:TVCLogLineDefaultCommandValue escapeMessage:escapeMessage];
}

- (void)printDebugInformation:(NSString *)message inChannel:(IRCChannel *)channel asCommand:(NSString *)command escapeMessage:(BOOL)escapeMessage
{
	[self print:message by:nil inChannel:channel asType:TVCLogLineTypeDebug command:command escapeMessage:escapeMessage];
}

- (void)printDebugInformationInAllViews:(NSString *)message
{
	[self printDebugInformationInAllViews:message asCommand:TVCLogLineDefaultCommandValue escapeMessage:YES];
}

- (void)printDebugInformationInAllViews:(NSString *)message asCommand:(NSString *)command
{
	[self printDebugInformationInAllViews:message asCommand:command escapeMessage:YES];
}

- (void)printDebugInformationInAllViews:(NSString *)message escapeMessage:(BOOL)escapeMessage
{
	[self printDebugInformationInAllViews:message asCommand:TVCLogLineDefaultCommandValue escapeMessage:escapeMessage];
}

- (void)printDebugInformationInAllViews:(NSString *)message asCommand:(NSString *)command escapeMessage:(BOOL)escapeMessage
{
	for (IRCChannel *channel in self.channelList) {
		[self printDebugInformation:message inChannel:channel asCommand:command escapeMessage:escapeMessage];
	}

	[self printDebugInformationToConsole:message asCommand:command escapeMessage:escapeMessage];
}

#pragma mark -
#pragma mark IRCConnection Delegate

- (void)resetAllPropertyValues
{
	// Some properties are purposely excluded from this method
	// because their state must be kept or they are reset elsewhere

	[self.batchMessages dequeueEntries];

	self.connectDelay = 0;

	self.invokingISONCommandForFirstTime = NO;

	self.isAutojoining = NO;
	self.isAutojoined = NO;

	self.autojoinDelayedWarningCount = 0;

	self.isConnected = NO;
	self.isConnecting = NO;
	self.isLoggedIn = NO;
	self.isQuitting = NO;
	self.isDisconnecting = NO;

	self.inWhoisResponse = NO;
	self.inWhowasResponse = NO;

	self.isWaitingForNickServ = NO;
	self.serverHasNickServ = NO;
	self.userIsIdentifiedWithNickServ = NO;

	self.userIsAway = NO;
	self.userIsIRCop = NO;

	self.isConnectedToZNC = NO;
	self.zncBouncerIsSendingCertificateInfo = NO;
	self.zncBouncerCertificateChainDataMutable = nil;
	self.zncBouncerIsPlayingBackHistory = NO;

	self.reconnectEnabled = NO;

	self.timeoutWarningShownToUser = NO;

	self.lastWhoRequestChannelListIndex = 0;

	self.server = nil;

	self.userHostmask = nil;
	self.userNickname = nil;

	self.tryingNicknameNumber = 0;
	self.tryingNicknameSentNickname = nil;

	self.preAwayUserNickname = nil;

	self.lastMessageReceived = 0;

	self.capabilities = 0;
	self.capabilityNegotiationIsPaused = NO;

	@synchronized (self.capabilitiesPending) {
		[self.capabilitiesPending removeAllObjects];
	}

	@synchronized (self.userListPrivate) {
		[self.userListPrivate removeAllObjects];
	}
}

- (void)changeStateOff
{
	[self changeStateOffWithError:nil];
}

- (void)changeStateOffWithError:(nullable NSError *)disconnectError
{
	if (self.isConnecting == NO && self.isConnected == NO) {
		return;
	}

	BOOL isTerminating = self.isTerminating;

	self.socket = nil;

	[self removeTimedCommands];

	[self removeRequestedCommands];

	[self stopAutojoinTimer];
	[self stopAutojoinNextJoinTimer];
	[self stopAutojoinDelayedWarningTimer];
	[self stopISONTimer];
	[self stopPongTimer];
	[self stopRetryTimer];

	[self cancelPerformRequests];

	if (isTerminating == NO && self.reconnectEnabled) {
		[self startReconnectTimer];
	}

	[self.supportInfo reset];

	[self clearAddressBookCache];

	[self clearTrackedUsers];

	if (isTerminating == NO) {
		/* -prepareForApplicationTermination in TVCLogController will cancel
		 all operations for this client for us during termination. */
		[[TXSharedApplication sharedPrintingQueue] cancelOperationsForClient:self];

		IRCClientDisconnectMode disconnectType = self.disconnectType;

		if (disconnectError) {
			// TODO: Don't hardcode the error domain and code
			if ( disconnectError.code == IRCConnectionErrorCodeBadCertificate &&
				[disconnectError.domain isEqualToString:IRCConnectionErrorDomain])
			{
				disconnectType = IRCClientDisconnectModeBadCertificate;
			}

			[self printError:disconnectError.localizedDescription asCommand:TVCLogLineDefaultCommandValue];
		}

		NSString *disconnectMessage = nil;

		switch (disconnectType) {
			case IRCClientDisconnectModeNormal:
			{
				disconnectMessage = TXTLS(@"IRC[9b4-10]");

				break;
			}
			case IRCClientDisconnectModeComputerSleep:
			{
				disconnectMessage = TXTLS(@"IRC[drg-b7]");

				break;
			}
			case IRCClientDisconnectModeBadCertificate:
			{
				disconnectMessage = TXTLS(@"IRC[zro-bg]");

				break;
			}
			case IRCClientDisconnectModeServerRedirect:
			{
				disconnectMessage = TXTLS(@"IRC[wcl-po]");

				break;
			}
			case IRCClientDisconnectModeReachabilityChange:
			{
				disconnectMessage = TXTLS(@"IRC[isx-fi]");

				break;
			}
		} // switch()

		for (IRCChannel *channel in self.channelList) {
			if (channel.isActive == NO) {
				channel.errorOnLastJoinAttempt = NO;
			} else {
				[channel deactivate];

				if (channel.isUtility) {
					continue;
				}

				[self printDebugInformation:disconnectMessage inChannel:channel];
			}
		}

		[self printDebugInformationToConsole:disconnectMessage];

		[self.viewController mark];

		if (self.isConnected) {
			[self notifyEvent:TXNotificationTypeDisconnect lineType:TVCLogLineTypeDebug];
		}

		[self postEventToViewController:@"serverDisconnected"];
	}

	[self endLoggingSessions];

	[self resetAllPropertyValues];

	if (isTerminating == NO) {
		[mainWindow() reloadTreeGroup:self];

		[mainWindow() updateTitleFor:self];
	}
}

- (void)ircConnection:(IRCConnection *)sender willConnectToProxy:(NSString *)proxyHost port:(uint16_t)proxyPort
{
	NSParameterAssert(sender == self.socket);

	IRCConnectionProxyType proxyType = self.socket.config.proxyType;

	if (proxyType == IRCConnectionProxyTypeSocks4) {
		[self printDebugInformationToConsole:TXTLS(@"IRC[p7h-un]", proxyHost, proxyPort)];
	} else if (proxyType == IRCConnectionProxyTypeSocks5) {
		[self printDebugInformationToConsole:TXTLS(@"IRC[ni5-cy]", proxyHost, proxyPort)];
	} else if (proxyType == IRCConnectionProxyTypeHTTP) {
		[self printDebugInformationToConsole:TXTLS(@"IRC[oby-av]", proxyHost, proxyPort)];
	}
}

- (void)ircConnectionDidSecureConnection:(IRCConnection *)sender withProtocolType:(tls_protocol_version_t)protocolType cipherSuite:(tls_ciphersuite_t)cipherSuite
{
	NSParameterAssert(sender == self.socket);

	NSString *protocolDescription = [RCMSecureTransport descriptionForProtocolType:protocolType];

	NSString *cipherDescription = [RCMSecureTransport descriptionForCipherSuite:cipherSuite];

	if (protocolDescription == nil || cipherDescription == nil) {
		return;
	}

	NSString *description = nil;

	if ([RCMSecureTransport isCipherSuiteDeprecated:cipherSuite] == NO) {
		description = TXTLS(@"IRC[uyz-4r]", protocolDescription, cipherDescription);
	} else {
		description = TXTLS(@"IRC[xwj-xy]", protocolDescription, cipherDescription);
	}

	[self printDebugInformationToConsole:TXTLS(@"IRC[ex4-f8]", description)];
}

- (void)ircConnectionDidConnect:(IRCConnection *)sender
{
	NSParameterAssert(sender == self.socket);

	if (self.isTerminating) {
		return;
	}

	[self startRetryTimer];

	/* If the address we are connecting to is not an IP address,
	 then we report back the actual IP address it was resolved to. */
	NSString *connectedAddress = self.socket.connectedAddress;

	if (connectedAddress == nil || self.socket.config.serverAddress.IPAddress) {
		[self printDebugInformationToConsole:TXTLS(@"IRC[4vt-ow]")];
	} else {
		[self printDebugInformationToConsole:TXTLS(@"IRC[l21-p7]", connectedAddress)];
	}

	self.isConnecting = NO;
	self.isConnected = YES;

	self.userNickname = self.config.nickname;

	self.tryingNicknameSentNickname = self.config.nickname;

	[mainWindow() updateTitleFor:self];

	[RZNotificationCenter() postNotificationName:IRCClientDidConnectNotification object:self];

	NSString *username = self.config.username;
	NSString *realName = self.config.realName;

	NSString *modeSymbols = @"0";

	NSString *serverPassword = self.server.serverPassword;

	if (self.config.setInvisibleModeOnConnect) {
		modeSymbols = @"8";
	}

	if (username.length == 0) {
		username = self.config.nickname;
	}

	if (realName.length == 0) {
		realName = self.config.nickname;
	}

	[self sendCapability:@"LS" data:@"302"];

	if (serverPassword) {
		[self sendPassword:serverPassword];
	}

	[self changeNickname:self.tryingNicknameSentNickname];

	[self send:@"USER", username, modeSymbols, @"*", realName, nil];
}

- (void)ircConnection:(IRCConnection *)sender didDisconnectWithError:(nullable NSError *)disconnectError
{
	NSParameterAssert(sender == self.socket);

//	if (self.isTerminating) {
//		return;
//	}

	[self changeStateOffWithError:disconnectError];

	if (self.disconnectCallback) {
		self.disconnectCallback();
		self.disconnectCallback = nil;
	}

	[RZNotificationCenter() postNotificationName:IRCClientDidDisconnectNotification object:self];
}

- (void)ircConnectionDidCloseReadStream:(IRCConnection *)sender
{
	NSParameterAssert(sender == self.socket);

	if (self.isTerminating) {
		return;
	}

	if (self.isDisconnecting) {
		return;
	}

	if (self.isQuitting) {
		[self disconnect];

		return;
	}

	[self printDebugInformationToConsole:TXTLS(@"IRC[5h5-sl]")];
}

/* This delegate call is not invoked on the main thread
 which means if it is modified to interact with UI,
 then it must invoke on the main thread eventually. */
- (void)ircConnection:(IRCConnection *)sender didReceiveData:(NSString *)data
{
	NSParameterAssert(sender == self.socket);

	if (self.isConnected == NO || self.isTerminating) {
		return;
	}

	if (data.length == 0) {
		return;
	}

	self.lastMessageReceived = [NSDate timeIntervalSince1970];

	worldController().bandwidthIn += data.length;

	worldController().messagesReceived += 1;

	[self rawDataLogIncomingTraffic:data];

	if ([TPCPreferences removeAllFormatting]) {
		data = data.stripIRCEffects;
	}

	IRCMessage *message = [[IRCMessage alloc] initWithLine:data onClient:self];

	if (message == nil) {
		return;
	}

	message = [THOPluginDispatcher interceptServerInput:message for:self];

	if (message == nil) {
		return;
	}

	if ([self filterBatchCommandIncomingData:message]) {
		return;
	}

	[self processIncomingMessage:message];
}

- (void)processIncomingMessageAttributes:(IRCMessage *)message
{
	NSParameterAssert(message != nil);

	if (self.isLoggedIn == NO) {
		return;
	}

	if (message.isHistoric == NO) {
		return;
	}

	NSTimeInterval receivedTime = message.receivedAt.timeIntervalSince1970;

	if (receivedTime <= self.lastMessageServerTime) {
		return;
	}

	self.lastMessageServerTime = receivedTime;

	/* If the playback module is in use, then all messages are
	 set as historic, so we set any lines above our current 
	 reference date as not historic to avoid collisions. */
	if ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityPlayback]) {
		[message markAsNotHistoric];
	}
}

- (void)processIncomingMessage:(IRCMessage *)message
{
	NSParameterAssert(message != nil);

	XRPerformBlockSynchronouslyOnMainQueue(^{
		[self _processIncomingMessage:message];
	});
}

- (void)_processIncomingMessage:(IRCMessage *)message
{
	NSParameterAssert(message != nil);

	[self processIncomingMessageAttributes:message];

	if (message.commandNumeric > 0) {
		[self receiveNumericReply:message];
	} else {
		NSUInteger commandNumeric = [IRCCommandIndex indexOfRemoteCommand:message.command];

		switch (commandNumeric) {
			case IRCRemoteCommandNotice: // Command: NOTICE
			case IRCRemoteCommandPrivmsg: // Command: PRIVMSG
			{
				[self receivePrivmsgAndNotice:message];

				break;
			}
			case IRCRemoteCommandError: // Command: ERROR
			{
				[self receiveError:message];

				break;
			}
			case IRCRemoteCommandInvite: // Command: INVITE
			{
				[self receiveInvite:message];

				break;
			}
			case IRCRemoteCommandJoin: // Command: JOIN
			{
				[self receiveJoin:message];

				break;
			}
			case IRCRemoteCommandKick: // Command: KICK
			{
				[self receiveKick:message];

				break;
			}
			case IRCRemoteCommandKill: // Command: KILL
			{
				[self receiveKill:message];

				break;
			}
			case IRCRemoteCommandMode: // Command: MODE
			{
				[self receiveMode:message];

				break;
			}
			case IRCRemoteCommandNick: // Command: NICK
			{
				[self receiveNick:message];

				break;
			}
			case IRCRemoteCommandPart: // Command: PART
			{
				[self receivePart:message];

				break;
			}
			case IRCRemoteCommandPing: // Command: PING
			{
				[self receivePing:message];

				break;
			}
			case IRCRemoteCommandQuit: // Command: QUIT
			{
				[self receiveQuit:message];

				break;
			}
			case IRCRemoteCommandTopic: // Command: TOPIC
			{
				[self receiveTopic:message];

				break;
			}
			case IRCRemoteCommandWallops: // Command: WALLOPS
			{
				[self receiveWallops:message];

				break;
			}
			case IRCRemoteCommandAuthenticate: // Command: AUTHENTICATE
			case IRCRemoteCommandCap: // Command: CAP
			{
				[self updateConnectedToZNCPropertyWithMessage:message];

				[self receiveCapabilityOrAuthenticationRequest:message];

				break;
			}
			case IRCRemoteCommandAway: // Command: AWAY (away-notify CAP)
			{
				[self receiveAwayNotifyCapability:message];

				break;
			}
			case IRCRemoteCommandBatch: // BATCH
			{
				[self receiveBatch:message];

				break;
			}
			case IRCRemoteCommandCertinfo: // CERTINFO
			{
				[self receiveCertInfo:message];

				break;
			}
			case IRCRemoteCommandChghost:
			{
				[self receiveChangeHost:message];

				break;
			}
		} // switch
	}

	[self processBundlesServerMessage:message];
}

- (void)ircConnection:(IRCConnection *)sender willSendData:(NSString *)data
{
	NSParameterAssert(sender == self.socket);

	if (self.isTerminating) {
		return;
	}

	[self rawDataLogOutgoingTraffic:data];
}

#pragma mark -
#pragma mark Raw Data Logging

- (void)createRawDataLogQuery
{
	if (self.isTerminating) {
		return;
	}

	if (self.rawDataLogQuery != nil) {
		return;
	}

	IRCChannel *query = [self findChannelOrCreate:@"Server Traffic" isUtility:YES];

	self.rawDataLogQuery = query;

	[mainWindow() select:query];

	[self rawDataLog:TXTLS(@"IRC[ik6-dl]")];
}

- (void)destroyRawDataLogQuery
{
	if (self.isTerminating) {
		return;
	}

	if (self.rawDataLogQuery == nil) {
		return;
	}

	[worldController() destroyChannel:self.rawDataLogQuery];
}

- (void)rawDataLog:(NSString *)data
{
	NSParameterAssert(data != nil);

	if (self.isTerminating) {
		return;
	}

	XRPerformBlockSynchronouslyOnMainQueue(^{
		[self printDebugInformation:data inChannel:self.rawDataLogQuery];
	});
}

- (void)rawDataLogOutgoingTraffic:(NSString *)data
{
	NSParameterAssert(data != nil);

	if (self.rawDataLogQuery == nil) {
		return;
	}

	NSString *dataToLog = [NSString stringWithFormat:@"<< %@", data];

	[self rawDataLog:dataToLog];
}

- (void)rawDataLogIncomingTraffic:(NSString *)data
{
	NSParameterAssert(data != nil);

	if (self.rawDataLogQuery == nil) {
		return;
	}

	NSString *dataToLog = [NSString stringWithFormat:@">> %@", data];

	[self rawDataLog:dataToLog];
}

#pragma mark -
#pragma mark NickServ Information

- (NSArray<NSString *> *)nickServSupportedNeedIdentificationTokens
{
	return [TPCResourceManager arrayFromResources:@"StaticStore" key:@"IRCClient List of NickServ Needs Identification Tokens"];
}

- (NSArray<NSString *> *)nickServSupportedSuccessfulIdentificationTokens
{
	return [TPCResourceManager arrayFromResources:@"StaticStore" key:@"IRCClient List of NickServ Successfully Identified Tokens"];
}

#pragma mark -
#pragma mark Protocol Handlers

- (void)receiveWallops:(IRCMessage *)m
{
	NSParameterAssert(m != nil);

	/* WALLOPS are rewritten so that they can be parsed as regular notices */
	NSMutableArray *paramsMutable = [m.params mutableCopy];

	[paramsMutable insertObject:self.userNickname atIndex:0];

	NSString *text = [NSString stringWithFormat:TVCLogLineSpecialNoticeMessageFormat, m.command, paramsMutable[1]];

	paramsMutable[1] = text;

	/* ======================================== */

	IRCMessageMutable *messageMutable = [m mutableCopy];

	messageMutable.command = @"NOTICE";

	messageMutable.params = paramsMutable;

	/* ======================================== */

	[self receivePrivmsgAndNotice:[messageMutable copy]];
}

- (void)receivePrivmsgAndNotice:(IRCMessage *)m
{
	NSParameterAssert(m != nil);

	NSAssertReturn([m paramsCount] > 1);

	NSString *text = [m paramAt:1];

	if ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityIdentifyCTCP] && ([text hasPrefix:@"+\x01"] || [text hasPrefix:@"-\x01"])) {
		text = [text substringFromIndex:1];
	} else if ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityIdentifyMsg] && ([text hasPrefix:@"+"] || [text hasPrefix:@"-"])) {
		text = [text substringFromIndex:1];
	}

	TVCLogLineType lineType = TVCLogLineTypePrivateMessage;

	BOOL isPlainText = [m.command isEqualToString:@"PRIVMSG"];

	if ([text hasPrefix:@"\x01"]) {
		text = [text substringFromIndex:1];

		NSInteger closingIndex = [text stringPosition:@"\x01"];

		if (closingIndex >= 0) {
			text = [text substringToIndex:closingIndex];
		}

		if (isPlainText) {
			if ([text hasPrefixIgnoringCase:@"ACTION "]) {
				text = [text substringFromIndex:7];

				lineType = TVCLogLineTypeAction;
			} else {
				lineType = TVCLogLineTypeCTCPQuery;
			}
		} else {
			lineType = TVCLogLineTypeCTCPReply; // notice -> query
		}
	} else if (isPlainText == NO) {
		lineType = TVCLogLineTypeNotice;
	}

	if (lineType == TVCLogLineTypeAction ||
		lineType == TVCLogLineTypePrivateMessage ||
		lineType == TVCLogLineTypeNotice)
	{
		[self receiveText:m lineType:lineType text:text];
	} else if (lineType == TVCLogLineTypeCTCPQuery) {
		[self receiveCTCPQuery:m text:text];
	} else if (lineType == TVCLogLineTypeCTCPReply) {
		[self receiveCTCPReply:m text:text];
	}
}

- (void)receiveText:(IRCMessage *)m lineType:(TVCLogLineType)lineType text:(NSString *)text
{
	NSParameterAssert(m != nil);

	NSParameterAssert(lineType == TVCLogLineTypeAction ||
					  lineType == TVCLogLineTypeActionNoHighlight ||
					  lineType == TVCLogLineTypePrivateMessage ||
					  lineType == TVCLogLineTypePrivateMessageNoHighlight ||
					  lineType == TVCLogLineTypeNotice);

	NSParameterAssert(text != nil);

	NSAssertReturn([m paramsCount] > 1);

	/* Allow empty actions but no other type */
	if (text.length == 0) {
		if (lineType == TVCLogLineTypeAction ||
			lineType == TVCLogLineTypeActionNoHighlight)
		{
			text = @" ";
		} else {
			return;
		}
	}

	/* Process target */
	NSString *target = [m paramAt:0];

	if (target.length == 0) {
		return;
	}

	/* It is possible for a channel to have a user mode character in front
	 of it when the message is being addressed to a specific group of users.
	 For example, "+#channel" as the channel name means that the message is
	 addressed to only the voiced users of #channel. We don't care about this
	 mode but we still have to remove it while also taking into account
	 channels who use a character other than the pound symbol as their prefix. */
	NSString *targetPrefix = [self.supportInfo extractStatusMessagePrefixFromChannelNamed:target];

	if (targetPrefix.length == 1) {
		target = [target substringFromIndex:1];
	}

	/* Perform ignore check */
	IRCAddressBookEntry *ignoreInfo = [self findAddressBookEntryForHostmask:m.senderHostmask];

	if (ignoreInfo.ignorePublicMessageHighlights) {
		if (lineType == TVCLogLineTypeAction) {
			lineType = TVCLogLineTypeActionNoHighlight;
		} else if (lineType == TVCLogLineTypePrivateMessage) {
			lineType = TVCLogLineTypePrivateMessageNoHighlight;
		}
	}

	if (lineType == TVCLogLineTypeNotice) {
		if (ignoreInfo.ignoreNoticeMessages) {
			return;
		}
	}

	/* Even though OTR doesn't allow channel decryption, we still wrap everything
	 in a decryption block because Blowfish plugin may swizzle the logic.
	 That plugin does support channel decryption. */
	BOOL performDecryption = YES;

	TLOEncryptionManagerEncodingDecodingCallbackBlock decryptionBlock = nil;

	/* Public message (directed at channel) */
	if ([self stringIsChannelName:target]) {
		if (ignoreInfo.ignorePublicMessages) {
			return;
		}

		decryptionBlock = ^(NSString *originalString, BOOL wasEncrypted) {
			[self _receiveText_Public:m lineType:lineType target:target text:originalString wasEncrypted:wasEncrypted];
		};
	}

	/* Private message (from user) */
	else if (m.senderIsServer == NO) {
		if (ignoreInfo.ignorePrivateMessages) {
			return;
		}

		decryptionBlock = ^(NSString *originalString, BOOL wasEncrypted) {
			[self _receiveText_Private:m lineType:lineType target:target text:originalString wasEncrypted:wasEncrypted];
		};
	}

	/* Private message (from server) */
	else {
		/* It is not possible to hold an OTR conversation with a server. */
		performDecryption = NO;

		decryptionBlock = ^(NSString *originalString, BOOL wasEncrypted) {
			[self _receiveText_PrivateServer:m lineType:lineType target:target text:originalString wasEncrypted:wasEncrypted];
		};
	}

	/* Perform decryption */
	if (performDecryption) {
		NSString *sender = m.senderNickname;

		[self decryptMessage:text from:sender target:target decodingCallback:decryptionBlock];
	} else {
		decryptionBlock(text, NO);
	}
}

- (void)_receiveText_Public:(IRCMessage *)m lineType:(TVCLogLineType)lineType target:(NSString *)target text:(NSString *)text wasEncrypted:(BOOL)wasEncrypted
{
	NSParameterAssert(m != nil);
	NSParameterAssert(target != nil);
	NSParameterAssert(text != nil);

	IRCChannel *channel = [self findChannel:target];

	if (channel == nil) {
		return;
	}

	NSString *sender = m.senderNickname;

	BOOL isSelfMessage = NO;

	if ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityEchoMessage]) {
		isSelfMessage = [self nicknameIsMyself:sender];
	}

	TVCLogControllerPrintOperationCompletionBlock printCompletionBlock = nil;

	BOOL isPlainText = (lineType != TVCLogLineTypeNotice);

	if (isPlainText == NO) {
		/* Completion block for notices */

		printCompletionBlock =
		^(TVCLogControllerPrintOperationContext *context)
		{
			if ([self isSafeToPostNotificationForMessage:m inChannel:channel]) {
				[self notifyText:TXNotificationTypeChannelNotice lineType:lineType target:channel nickname:sender text:text];
			}
		};
	} else {
		/* Completion block for regular messages */

		printCompletionBlock =
		^(TVCLogControllerPrintOperationContext *context)
		{
			if (isSelfMessage) {
				return;
			}

			BOOL isHighlight = context.highlight;

			BOOL postEvent = YES;

			if ([self isSafeToPostNotificationForMessage:m inChannel:channel]) {
				if (isHighlight) {
					postEvent = [self notifyText:TXNotificationTypeHighlight lineType:lineType target:channel nickname:sender text:text];
				} else {
					postEvent = [self notifyText:TXNotificationTypeChannelMessage lineType:lineType target:channel nickname:sender text:text];
				}
			}

			if (postEvent == NO) {
				return;
			}

			if (isHighlight) {
				[self setHighlightStateForChannel:channel];
			}

			[self setUnreadStateForChannel:channel isHighlight:isHighlight];
		};
	}

	/* Ask for permission to print message */
	BOOL printMessage = YES;

	if ([sharedPluginManager() supportsFeature:THOPluginItemSupportedFeatureDidReceivePlainTextMessageEvent]) {
		printMessage = [THOPluginDispatcher receivedText:text
											  authoredBy:m.sender
											 destinedFor:channel
											  asLineType:lineType
												onClient:self
											  receivedAt:m.receivedAt
											wasEncrypted:wasEncrypted];
	}

	/* Print message */
	if (printMessage) {
		[self print:text
				 by:sender
		  inChannel:channel
			 asType:lineType
			command:m.command
		 receivedAt:m.receivedAt
		isEncrypted:wasEncrypted
   referenceMessage:m
	completionBlock:printCompletionBlock];
	}

	/* The remaining logic does not apply to notices */
	if (isPlainText == NO) {
		return;
	}

	/* Update weights of user we're talking with */
	IRCChannelUser *senderMember = [channel findMember:sender];

	if (senderMember == nil) {
		return;
	}

	NSString *localNickname = [self.userNickname trimCharacters:@"_"]; // Remove any underscores from around nickname (Guest___ becomes Guest)

	/* If we are mentioned in this piece of text, then update our weight for the user */
	if ([text contains:localNickname]) {
		[senderMember outgoingConversation];
	} else {
		[senderMember conversation];
	}
}

- (void)_receiveText_Private:(IRCMessage *)m lineType:(TVCLogLineType)lineType target:(NSString *)target text:(NSString *)text wasEncrypted:(BOOL)wasEncrypted
{
	NSParameterAssert(m != nil);
	NSParameterAssert(target != nil);
	NSParameterAssert(text != nil);

	NSString *sender = m.senderNickname;

	TVCLogControllerPrintOperationCompletionBlock printCompletionBlock = nil;

	BOOL isPlainText = (lineType != TVCLogLineTypeNotice);

	/* If the self-message CAP is not enabled, we still check if we are on a ZNC
	 based connections because older versions of ZNC combined with the privmsg
	 module need the correct behavior which the self-message CAP evolved into. */
	BOOL isSelfMessage = NO;

	if ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityEchoMessage] ||
		[self isCapabilityEnabled:ClientIRCv3SupportedCapabilityZNCSelfMessage] ||
		self.isConnectedToZNC)
	{
		isSelfMessage = [self nicknameIsMyself:sender];
	}

	/* Does the query for the sender already exist?... */
	IRCChannel *query = nil;

	if (isSelfMessage) {
		query = [self findChannel:target]; // Look for a query related to target, rather than sender
	} else {
		query = [self findChannel:sender];
	}

	BOOL newPrivateMessage = NO;

	if (isPlainText == NO) {
		/* Logic for notices */

		/* Process services */
		if ([sender isEqualToStringIgnoringCase:@"ChanServ"]) {
			[self _receiveText_PrivateNoticeFromChanServ:&query text:&text];
		} else if ([sender isEqualToStringIgnoringCase:@"NickServ"]) {
			[self _receiveText_PrivateNoticeFromNickServ:&query text:&text];
		}

		/* Determine where to send notice messages */
		if ([TPCPreferences locationToSendNotices] == TXNoticeSendLocationSelectedChannel) {
			query = [mainWindow() selectedChannelOn:self];
		}

		/* Do not create query until after ChanServ notices have been processed
		 so that entry messages do not create a new window. */
		if (query == nil) {
			if ([TPCPreferences locationToSendNotices] == TXNoticeSendLocationQuery) {
//				newPrivateMessage = YES;

				if (isSelfMessage) {
					query = [worldController() createPrivateMessage:target onClient:self];
				} else {
					query = [worldController() createPrivateMessage:sender onClient:self];
				}
			}
		}

		printCompletionBlock =
		^(TVCLogControllerPrintOperationContext *context)
		{
			if (isSelfMessage) {
				return;
			}

			BOOL postEvent = YES;

			if ([self isSafeToPostNotificationForMessage:m inChannel:query]) {
				postEvent = [self notifyText:TXNotificationTypePrivateNotice lineType:lineType target:query nickname:sender text:text];
			}

			if (postEvent && query != nil) {
				[self setUnreadStateForChannel:query];
			}
		};
	}
	else // NOTICE message
	{
		/* Logic for regular messages */

		if (query == nil) {
			newPrivateMessage = YES;

			if (isSelfMessage) {
				query = [worldController() createPrivateMessage:target onClient:self];
			} else {
				query = [worldController() createPrivateMessage:sender onClient:self];
			}
		}

		printCompletionBlock =
		^(TVCLogControllerPrintOperationContext *context)
		{
			if (isSelfMessage) {
				return;
			}

			BOOL isHighlight = context.highlight;

			BOOL postEvent = YES;

			if ([self isSafeToPostNotificationForMessage:m inChannel:query]) {
				if (isHighlight) {
					postEvent = [self notifyText:TXNotificationTypeHighlight lineType:lineType target:query nickname:sender text:text];
				} else {
					if (newPrivateMessage) {
						postEvent = [self notifyText:TXNotificationTypeNewPrivateMessage lineType:lineType target:query nickname:sender text:text];
					} else {
						postEvent = [self notifyText:TXNotificationTypePrivateMessage lineType:lineType target:query nickname:sender text:text];
					}
				}
			}

			if (postEvent == NO) {
				return;
			}

			if (isHighlight) {
				[self setHighlightStateForChannel:query];
			}

			[self setUnreadStateForChannel:query isHighlight:isHighlight];
		};
	}

	/* Ask for permission to print message */
	BOOL printMessage = YES;

	if ([sharedPluginManager() supportsFeature:THOPluginItemSupportedFeatureDidReceivePlainTextMessageEvent]) {
		printMessage = [THOPluginDispatcher receivedText:text
											  authoredBy:m.sender
											 destinedFor:query
											  asLineType:lineType
												onClient:self
											  receivedAt:m.receivedAt
											wasEncrypted:wasEncrypted];
	}

	/* Print message */
	if (printMessage) {
		[self print:text
				 by:sender
		  inChannel:query
			 asType:lineType
			command:m.command
		 receivedAt:m.receivedAt
		isEncrypted:wasEncrypted
   referenceMessage:m
	completionBlock:printCompletionBlock];
	}

	/* The remaining logic does not apply to notices */
	if (isPlainText == NO) {
		return;
	}

	/* Update query status */
	if (query.isActive == NO) {
		[query activate];

		[mainWindow() reloadTreeItem:query];
	}
}

- (void)_receiveText_PrivateNoticeFromChanServ:(IRCChannel **)target text:(NSString **)text
{
	NSParameterAssert(target != NULL);
	NSParameterAssert(text != NULL);

	NSString *textIn = (*text);

	/* Forward entry messages to the channel they are associated with. */
	/* Format we are going for: -ChanServ- [#channelname] blah blah... */
	NSInteger spacePosition = [textIn stringPosition:@" "];

	if ([textIn hasPrefix:@"["] == NO || spacePosition < 4) {
		return;
	}

	NSString *textHead = [textIn substringToIndex:spacePosition];

	if ([textHead hasSuffix:@"]"] == NO) {
		return;
	}

	textHead = [textHead substringToIndex:(textHead.length - 1)]; // Remove the ]
	textHead = [textHead substringFromIndex:1]; // Remove the [

	if ([self stringIsChannelName:textHead] == NO) {
		return;
	}

	IRCChannel *channel = [self findChannel:textHead];

	if (channel == nil) {
		return;
	}

	*text = [textIn substringFromIndex:(textHead.length + 2)]; // Remove the [#channelname] from the text

	*target = channel;
}

- (void)_receiveText_PrivateNoticeFromNickServ:(IRCChannel **)target text:(NSString **)text
{
	NSParameterAssert(target != NULL);
	NSParameterAssert(text != NULL);

	self.serverHasNickServ = YES;

	NSString *textIn = nil;

	if ([TPCPreferences removeAllFormatting] == NO) {
		textIn = (*text).stripIRCEffects;
	} else {
		textIn = (*text);
	}

	/* If we are not waiting for a response from NickServ, 
	 then try sending our password if that's what it requested. */
	if (self.isWaitingForNickServ == NO) {
		NSString *nicknamePassword = self.config.nicknamePassword;

		if (nicknamePassword.length == 0) {
			return;
		}

		for (NSString *token in self.nickServSupportedNeedIdentificationTokens) {
			if ([textIn containsIgnoringCase:token] == NO) {
				continue;
			}

			// Send password
			if ([self.serverAddress hasSuffix:@".dal.net"])
			{
				NSString *message = [NSString stringWithFormat:@"IDENTIFY %@", nicknamePassword];

				[self send:@"PRIVMSG", @"NickServ@services.dal.net", message, nil];
			}
			else if (self.config.sendAuthenticationRequestsToUserServ)
			{
				NSString *message = [NSString stringWithFormat:@"login %@ %@", self.config.nickname, nicknamePassword];

				[self send:@"PRIVMSG", @"userserv", message, nil];
			}
			else
			{
				NSString *message = [NSString stringWithFormat:@"IDENTIFY %@", nicknamePassword];

				[self send:@"PRIVMSG", @"NickServ", message, nil];
			}

			// Reset properties
			self.isWaitingForNickServ = YES;

			self.userIsIdentifiedWithNickServ = NO;

			break;
		}

		return;
	}

	/* Scan for messages telling us that we are now identified */
	for (NSString *token in self.nickServSupportedSuccessfulIdentificationTokens) {
		if ([textIn containsIgnoringCase:token] == NO) {
			continue;
		}

		self.isWaitingForNickServ = NO;

		self.userIsIdentifiedWithNickServ = YES;

		if (self.config.autojoinWaitsForNickServ) {
			[self performAutoJoin];
		}

		break;
	}
}

- (void)_receiveText_PrivateServer:(IRCMessage *)m lineType:(TVCLogLineType)lineType target:(NSString *)target text:(NSString *)text wasEncrypted:(BOOL)wasEncrypted
{
	NSParameterAssert(m != nil);
	NSParameterAssert(target != nil);
	NSParameterAssert(text != nil);

	NSString *sender = m.senderNickname;

	BOOL isPlainText = (lineType != TVCLogLineTypeNotice);

	IRCChannel *query = nil;

	/* For notices, send to a query if a query for the server already exists.
	 Otherwise, it is always sent to the console. Plain text messages always
	 create a new query but does not post a notification. */
	if (isPlainText == NO) {
		query = [self findChannel:sender];
	} else { // NOTICE message
		query = [self findChannelOrCreate:sender isPrivateMessage:YES];
	}

	/* Print message */
	BOOL printMessage = YES;

	if ([sharedPluginManager() supportsFeature:THOPluginItemSupportedFeatureDidReceivePlainTextMessageEvent]) {
		printMessage = [THOPluginDispatcher receivedText:text
											  authoredBy:m.sender
											 destinedFor:query
											  asLineType:lineType
												onClient:self
											  receivedAt:m.receivedAt
											wasEncrypted:wasEncrypted];
	}

	if (printMessage) {
		[self print:text
				 by:sender
		  inChannel:query
			 asType:lineType
			command:m.command
		 receivedAt:m.receivedAt
		isEncrypted:wasEncrypted];
	}

	/* Disconnect and reconnect if message is believed to be from an irssi proxy */
	/* If we do not do this, the internal state of the client becomes fucked all around */
	if ([sender hasSuffix:@".proxy"] && [text isEqualToString:@"Connected to server"]) {
		__weak IRCClient *weakSelf = self;

		self.disconnectCallback = ^{
			[weakSelf printDebugInformationToConsole:TXTLS(@"IRC[5i4-qq]")];

			[weakSelf connect:IRCClientConnectModeReconnect];
		};

		[self disconnect];
	}
}

- (void)receiveCTCPQuery:(IRCMessage *)m text:(NSString *)text
{
	NSParameterAssert(m != nil);
	NSParameterAssert(text != nil);

	NSString *sender = m.senderNickname;

	BOOL myself = [self nicknameIsMyself:sender];

	IRCAddressBookEntry *ignoreInfo = nil;

	if (myself) {
		/* Ignore messages echoed back to ourselves */
		if ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityEchoMessage]) {
			return;
		}
	} else {
		/* Find ignore for sender and possibly exit method */
		ignoreInfo = [self findAddressBookEntryForHostmask:m.senderHostmask];

		if (ignoreInfo.ignoreClientToClientProtocol) {
			return;
		}
	}

	/* Context */
	NSMutableString *textMutable = [text mutableCopy];

	NSString *command = textMutable.uppercaseGetToken;

	if (command.length == 0) {
		return;
	}

	/* Lag check responses should only ever come from ourselves so we
	 let it through. The method we call into already has a built-in
	 check for myself so no need to wrap an if statement here. */
	if ([command isEqualToString:@"LAGCHECK"]) {
		[self receiveCTCPLagCheckQuery:m text:textMutable];

		return;
	}

	/* Ignore query if the user has configured Textual to do so */
	if ([TPCPreferences replyToCTCPRequests] == NO) {
		[self printDebugInformationToConsole:TXTLS(@"IRC[bg3-h2]", command, sender)];

		return;
	}

	/* Process DCC requests elsewhere */
	if ([command isEqualToString:@"DCC"]) {
		[self receivedDCCQuery:m text:textMutable ignoreInfo:ignoreInfo];

		return;
	}

	/* Print message */
	IRCChannel *printTarget = nil;

	if ([TPCPreferences locationToSendNotices] == TXNoticeSendLocationSelectedChannel) {
		printTarget = [mainWindow() selectedChannelOn:self];
	}

	NSString *messageToPrint = TXTLS(@"IRC[6o8-eu]", command, sender);

	[self print:messageToPrint
			 by:nil
	  inChannel:printTarget
		 asType:TVCLogLineTypeCTCPQuery
		command:m.command
	 receivedAt:m.receivedAt];

	/* CLIENTINFO command */
	if ([command isEqualToString:@"CLIENTINFO"])
	{
		[self sendCTCPReply:sender command:command text:TXTLS(@"IRC[jer-ju]")];
	}

	/* FINGER command */
	else if ([command isEqualToString:@"FINGER"])
	{
		[self sendCTCPReply:sender command:command text:TXTLS(@"IRC[en6-mw]")];
	}

	/* PING command */
	else if ([command isEqualToString:@"PING"])
	{
		if (textMutable.length > 50) {
			LogToConsoleFault("Ignoring PING query that exceeds 50 bytes");

			return;
		}

		[self sendCTCPReply:sender command:command text:textMutable];
	}

	/* TIME command */
	else if ([command isEqualToString:@"TIME"])
	{
		NSDateFormatter *dateFormatter = TXSharedISOStandardDateFormatter();

		NSString *text = [dateFormatter stringFromDate:[NSDate date]];

		[self sendCTCPReply:sender command:command text:text];
	}

	/* USERINFO command */
	else if ([command isEqualToString:@"USERINFO"])
	{
		[self sendCTCPReply:sender command:command text:self.config.realName];
	}

	/* VERSION command */
	else if ([command isEqualToString:@"VERSION"])
	{
		NSString *fakeVersion = [TPCPreferences masqueradeCTCPVersion];

		if (fakeVersion.length > 0) {
			[self sendCTCPReply:sender command:command text:fakeVersion];

			return;
		}

		NSString *applicationName = [TPCApplicationInfo applicationNameWithoutVersion];
		NSString *versionShort = [TPCApplicationInfo applicationVersionShort];

		NSString *text = TXTLS(@"IRC[vzu-u7]", applicationName, versionShort);

		[self sendCTCPReply:sender command:command text:text];
	}
}

- (void)receiveCTCPLagCheckQuery:(IRCMessage *)m text:(NSString *)text
{
	NSParameterAssert(m != nil);

	if ([self messageIsFromMyself:m] == NO) {
		return;
	}
	
	NSDictionary *lagCheckContext = [text formDataUsingSeparator:@"&"];
	
	if (lagCheckContext.count == 0) {
		return;
	}

	NSString *connectionIdentifier = lagCheckContext[@"connection"];
	
	if ([connectionIdentifier isEqualToString:self.socket.uniqueIdentifier] == NO) {
		/* We check which connection this event is linked to so that
		 we ignore events after reconnects (such as from a bouncer). */

		return;
	}
	
	NSTimeInterval firstTime = [lagCheckContext doubleForKey:@"time"];

	double delta = (([NSDate timeIntervalSince1970] - firstTime) * 1000);
	
	NSString *ratingString = nil;
	
	if (delta < 10) { // Yeah, okay…
		ratingString = TXTLS(@"IRC[58g-m9]");
	} else if (delta > 10 && delta <= 25) { // Are you plugged into the server?
		ratingString = TXTLS(@"IRC[0jp-93]");
	} else if (delta > 25 && delta <= 100) { // Pretty good
		ratingString = TXTLS(@"IRC[yym-8y]");
	} else if (delta > 100 && delta <= 125) { // Not bad
		ratingString = TXTLS(@"IRC[mic-qe]");
	} else if (delta > 125 && delta <= 200) { // Okay
		ratingString = TXTLS(@"IRC[mqg-wi]");
	} else if (delta > 200 && delta <= 225) { // Needs work
		ratingString = TXTLS(@"IRC[ut8-7s]");
	} else if (delta > 225 && delta <= 300) { // Slow
		ratingString = TXTLS(@"IRC[8fo-ss]");
	} else if (delta > 300) { // Very Slow
		ratingString = TXTLS(@"IRC[4oc-p2]");
	}
	
	NSString *message = TXTLS(@"IRC[5bf-jp]", self.serverAddress, delta, ratingString);
	
	NSString *channelName = lagCheckContext[@"channel"];
	
	IRCChannel *channel = nil;
	
	if (channelName) {
		channel = [self findChannel:channelName];
	}

	if (channel) {
		[self sendPrivmsg:message toChannel:channel];
	} else {
		[self printDebugInformation:message];
	}
}

- (void)receiveCTCPReply:(IRCMessage *)m text:(NSString *)text
{
	NSParameterAssert(m != nil);
	NSParameterAssert(text != nil);

	/* Find ignore for sender and possibly exit method */
	IRCAddressBookEntry *ignoreInfo = [self findAddressBookEntryForHostmask:m.senderHostmask];

	if (ignoreInfo.ignoreClientToClientProtocol) {
		return;
	}

	/* Context */
	NSMutableString *textMutable = [text mutableCopy];

	NSString *sender = m.senderNickname;

	NSString *command = textMutable.uppercaseGetToken;

	if (command.length == 0) {
		return;
	}

	/* Print message */
	IRCChannel *printTarget = nil;

	if ([TPCPreferences locationToSendNotices] == TXNoticeSendLocationSelectedChannel) {
		printTarget = [mainWindow() selectedChannelOn:self];
	}

	NSString *messageToPrint = nil;

	if ([command isEqualToString:@"PING"]) {
		double delta = ([NSDate timeIntervalSince1970] - textMutable.doubleValue);

		messageToPrint = TXTLS(@"IRC[vy7-pk]", sender, command, delta);
	} else {
		messageToPrint = TXTLS(@"IRC[dri-l7]", sender, command, textMutable);
	}

	[self print:messageToPrint
			 by:nil
	  inChannel:printTarget
		 asType:TVCLogLineTypeCTCPReply
		command:m.command
	 receivedAt:m.receivedAt];
}

- (void)receiveJoin:(IRCMessage *)m
{
	NSAssertReturn([m paramsCount] > 0);

	BOOL isPrintOnlyMessage = m.isPrintOnlyMessage;

	NSString *sender = m.senderNickname;

	BOOL myself = [self nicknameIsMyself:sender];

	NSString *channelName = [m paramAt:0];

	IRCChannel *channel = nil;

	if (isPrintOnlyMessage == NO && myself)
	{
		channel = [self findChannelOrCreate:channelName];

		if (channel.isActive == NO && channel.isChannel) {
			[channel activate];
		} else {
			return;
		}

		self.userHostmask = m.senderHostmask;

		[mainWindow() reloadTreeItem:channel];
	}
	else // myself
	{
		channel = [self findChannel:channelName];

		if (channel == nil || channel.isChannel == NO) {
			return;
		}
	}

	if (isPrintOnlyMessage == NO) {
		/* A user might already exist by having a private message open */
		IRCUserMutable *userMutable = [self mutableCopyOfUserWithNickname:sender];

		userMutable.nickname = m.senderNickname;
		userMutable.username = m.senderUsername;
		userMutable.address = m.senderAddress;

		IRCUser *userAdded = [self addUserAndReturn:userMutable];

		IRCChannelUser *member = [[IRCChannelUser alloc] initWithUser:userAdded];

		[channel addMember:member checkForDuplicates:YES];
	}

	if (isPrintOnlyMessage == NO && myself == NO) {
		IRCChannel *senderQuery = [self findChannel:sender];

		if (senderQuery && senderQuery.isActive == NO) {
			[senderQuery activate];

			[self print:TXTLS(@"IRC[q0q-ch]", sender)
					 by:nil
			  inChannel:senderQuery
				 asType:TVCLogLineTypeJoin
				command:m.command
			 receivedAt:m.receivedAt];

			[mainWindow() reloadTreeItem:senderQuery];
		}
	}

	IRCAddressBookEntry *ignoreInfo = nil;

	if (myself == NO) {
		ignoreInfo = [self findAddressBookEntryForHostmask:m.senderHostmask];

		if (ignoreInfo && isPrintOnlyMessage == NO) {
			[self updateUserTrackingStatusForEntry:ignoreInfo withMessage:m];
		}
	}

	BOOL printMessage = [self postReceivedMessage:m withText:nil destinedFor:channel];

	if (printMessage && myself == NO)
	{
		if ([TPCPreferences showJoinLeave] == NO) {
			printMessage = NO;
		} else if (channel.config.ignoreGeneralEventMessages) {
			printMessage = NO;
		} else if (ignoreInfo) {
			printMessage = (ignoreInfo.ignoreGeneralEventMessages == NO);
		}
	}

	if (printMessage) {
		NSString *message = TXTLS(@"IRC[ziu-p9]", sender, m.senderUsername, m.senderAddress.stringByAppendingIRCFormattingStop);

		[self print:message
				 by:nil
		  inChannel:channel
			 asType:TVCLogLineTypeJoin
			command:m.command
		 receivedAt:m.receivedAt];
	}

	if (isPrintOnlyMessage) {
		return;
	}

	[mainWindow() updateTitleFor:channel];

	if (myself) {
		if (self.config.sendWhoCommandRequestsToChannels && self.isBrokenIRCd_aka_Twitch == NO) {
			[self requestModesForChannel:channel];
		}
	} else {
		[self notifyEvent:TXNotificationTypeUserJoined lineType:TVCLogLineTypeJoin target:channel nickname:sender text:nil];
	}
}

- (void)receivePart:(IRCMessage *)m
{
	NSParameterAssert(m != nil);

	NSAssertReturn([m paramsCount] > 0);

	/* ZNC sends PART messages for every channel when the client disconnects to force
	 it to update its local status. This is incredibly misleading to the user, as they
	 see the message that they left the channel and believe they did. This condition
	 filters out these messages. Because Textual is intelligent enough to clear status
	 related to channels when the connection is quit, this is safe. */
	if (self.isQuitting && self.isConnectedToZNC) {
		return;
	}

	BOOL isPrintOnlyMessage = m.isPrintOnlyMessage;

	NSString *channelName = [m paramAt:0];

	IRCChannel *channel = [self findChannel:channelName];

	if (channel == nil || channel.isChannel == NO) {
		return;
	}

	NSString *sender = m.senderNickname;

	NSString *comment = [m paramAt:1];

	BOOL myself = [self nicknameIsMyself:sender];

	if (isPrintOnlyMessage == NO) {
		if (myself) {
			[channel deactivate];

			[mainWindow() reloadTreeItem:channel];
		} else {
			[channel removeMemberWithNickname:sender];

			/* Notify user */
			[self notifyEvent:TXNotificationTypeUserParted lineType:TVCLogLineTypePart target:channel nickname:sender text:comment];
		}
	}

	BOOL printMessage = [self postReceivedMessage:m withText:comment destinedFor:channel];

	if (printMessage && myself == NO)
	{
		if ([TPCPreferences showJoinLeave] == NO) {
			printMessage = NO;
		} else if (channel.config.ignoreGeneralEventMessages) {
			printMessage = NO;
		} else {
			IRCAddressBookEntry *ignoreInfo = [self findAddressBookEntryForHostmask:m.senderHostmask];

			if (ignoreInfo) {
				printMessage = (ignoreInfo.ignoreGeneralEventMessages == NO);
			}
		}
	}

	if (printMessage) {
		NSString *message = TXTLS(@"IRC[nkr-kf]", sender, m.senderUsername, m.senderAddress.stringByAppendingIRCFormattingStop);

		if (comment.length > 0) {
			message = TXTLS(@"IRC[ozy-6i]", message, comment.stringByAppendingIRCFormattingStop);
		}

		[self print:message
				 by:nil
		  inChannel:channel
			 asType:TVCLogLineTypePart
			command:m.command
		 receivedAt:m.receivedAt];
	}

	if (isPrintOnlyMessage == NO) {
		[mainWindow() updateTitleFor:channel];
	}
}

- (void)receiveKick:(IRCMessage *)m
{
	NSParameterAssert(m != nil);

	NSAssertReturn([m paramsCount] > 1);

	BOOL isPrintOnlyMessage = m.isPrintOnlyMessage;

	NSString *channelName = [m paramAt:0];

	IRCChannel *channel = [self findChannel:channelName];

	if (channel == nil || channel.isChannel == NO) {
		return;
	}

	NSString *sender = m.senderNickname;

	NSString *target = [m paramAt:1];
	NSString *comment = [m paramAt:2];

	BOOL myself = [self nicknameIsMyself:target];

	if (isPrintOnlyMessage == NO) {
		if (myself)
		{
			[channel deactivate];

			[mainWindow() reloadTreeItem:channel];

			/* Notify user */
			[self notifyEvent:TXNotificationTypeKick lineType:TVCLogLineTypeKick target:channel nickname:sender text:comment];

			/* Rejoin channel */
			if ([TPCPreferences rejoinOnKick] && channel.errorOnLastJoinAttempt == NO) {
				[self printDebugInformation:TXTLS(@"IRC[zzj-2h]") inChannel:channel];

				[self cancelPerformRequestsWithSelector:@selector(joinKickedChannel:) object:channel];

				[self performSelectorInCommonModes:@selector(joinKickedChannel:) withObject:channel afterDelay:3.0];
			}
		}
		else // myself
		{
			[channel removeMemberWithNickname:target];
		}
	}

	BOOL printMessage = [self postReceivedMessage:m withText:comment destinedFor:channel];

	if (printMessage && myself == NO)
	{
		if ([TPCPreferences showJoinLeave] == NO) {
			printMessage = NO;
		} else if (channel.config.ignoreGeneralEventMessages) {
			printMessage = NO;
		} else {
			IRCAddressBookEntry *ignoreInfo = [self findAddressBookEntryForHostmask:m.senderHostmask];

			if (ignoreInfo) {
				printMessage = (ignoreInfo.ignoreGeneralEventMessages == NO);
			}
		}
	}

	if (printMessage) {
		NSString *message = TXTLS(@"IRC[9aj-bd]", sender, target, comment.stringByAppendingIRCFormattingStop);

		[self print:message
				 by:nil
		  inChannel:channel
			 asType:TVCLogLineTypeKick
			command:m.command
		 receivedAt:m.receivedAt];
	}

	if (isPrintOnlyMessage == NO) {
		[mainWindow() updateTitleFor:channel];
	}
}

- (void)receiveQuit:(IRCMessage *)m
{
	NSParameterAssert(m != nil);

	NSAssertReturn([m paramsCount] > 0);

	BOOL isPrintOnlyMessage = m.isPrintOnlyMessage;

	NSString *channelName = nil;

	NSString *comment = nil;

	if (isPrintOnlyMessage) {
		channelName = [m paramAt:0];

		comment = [m paramAt:1];
	} else {
		comment = [m paramAt:0];
	}

	NSString *sender = m.senderNickname;

	BOOL myself = [self nicknameIsMyself:sender];

	IRCUser *user = nil;

	if (isPrintOnlyMessage == NO) {
		user = [self findUser:sender];

		if (user == nil) {
			return;
		}
	}

	IRCAddressBookEntry *ignoreInfo = nil;

	if (myself == NO) {
		ignoreInfo = [self findAddressBookEntryForHostmask:m.senderHostmask];

		if (ignoreInfo && isPrintOnlyMessage == NO) {
			[self updateUserTrackingStatusForEntry:ignoreInfo withMessage:m];
		}
	}

	NSString *messageToPrint = TXTLS(@"IRC[53b-dm]", sender, m.senderUsername, m.senderAddress.stringByAppendingIRCFormattingStop);

	if (comment.length > 0) {
		messageToPrint = TXTLS(@"IRC[tok-st]", messageToPrint, comment.stringByAppendingIRCFormattingStop);
	}

	void (^printingBlock)(IRCChannel *) = ^(IRCChannel *channel)
	{
		if (myself == NO && isPrintOnlyMessage == NO) {
			switch (channel.type) {
				case IRCChannelTypeChannel:
				{
					IRCChannelUser *member = [user userAssociatedWithChannel:channel];

					if (member == nil) {
						return;
					}

					[channel removeMember:member];

					break;
				}
				case IRCChannelTypePrivateMessage:
				{
					if ([sender isEqualToStringIgnoringCase:channel.name] == NO) {
						return;
					}

					if (channel.isActive) {
						[channel deactivate];

						[mainWindow() reloadTreeItem:channel];
					}

					break;
				}
				default:
				{
					return;
				}
			}
		}

		NSString *message = messageToPrint;

		if (channel.isChannel)
		{
			BOOL printMessage = [self postReceivedMessage:m withText:comment destinedFor:channel];

			if (printMessage && myself == NO)
			{
				if ([TPCPreferences showJoinLeave] == NO) {
					printMessage = NO;
				} else if (channel.config.ignoreGeneralEventMessages) {
					printMessage = NO;
				} else if (ignoreInfo) {
					printMessage = (ignoreInfo.ignoreGeneralEventMessages == NO);
				}
			}

			[mainWindow() updateTitleFor:channel];

			if (printMessage == NO) {
				return;
			}
		}
		else // -isChannel
		{
			message = TXTLS(@"IRC[8bk-mx]", sender);
		}

		[self print:message
				 by:nil
		  inChannel:channel
			 asType:TVCLogLineTypeQuit
			command:m.command
		 receivedAt:m.receivedAt];
	};

	if (isPrintOnlyMessage) {
		IRCChannel *channel = [self findChannel:channelName];

		if (channel == nil) {
			return;
		}

		printingBlock(channel);

		return;
	}

	for (IRCChannel *c in self.channelList) {
		printingBlock(c);
	}

	if (myself == NO) {
		[mainWindow() updateTitleFor:self];

		[self notifyEvent:TXNotificationTypeUserDisconnected lineType:TVCLogLineTypeQuit target:nil nickname:sender text:comment];
	}
}

- (void)receiveKill:(IRCMessage *)m
{
	NSParameterAssert(m != nil);

	NSAssertReturn([m paramsCount] > 0);

	NSString *nickname = [m paramAt:0];

	for (IRCChannel *c in self.channelList) {
		[c removeMemberWithNickname:nickname];
	}
}

- (void)receiveNick:(IRCMessage *)m
{
	NSParameterAssert(m != nil);

	NSAssertReturn([m paramsCount] == 1);

	/* Print only messages target specific channels which means
	 the index of incoming data will be different */
	BOOL isPrintOnlyMessage = m.isPrintOnlyMessage;

	NSString *channelName = nil;

	NSString *newNickname = nil;

	if (isPrintOnlyMessage) {
		channelName = [m paramAt:0];

		newNickname = [m paramAt:1];
	} else {
		newNickname = [m paramAt:0];
	}

	/* There's no reason to perform an update if nothing changed */
	NSString *oldNickname = m.senderNickname;

	if ([oldNickname isEqualToString:newNickname]) {
		return;
	}

	BOOL myself = [self nicknameIsMyself:oldNickname];

	/* Find address book entry for old nickname and update tracking
	 status. This entry will also be used later on,	 when printing, 
	 to decide whether to print the message. */
	IRCAddressBookEntry *oldNicknameIgnoreInfo = nil;

	if (myself == NO) {
		oldNicknameIgnoreInfo = [self findAddressBookEntryForHostmask:m.senderHostmask];
	}

	/* Perform restricted actions */
	if (isPrintOnlyMessage == NO) {
		if (myself)
		{
			self.userNickname = newNickname;

			if (self.tryingNicknameSentNickname != nil) {
				self.tryingNicknameSentNickname = newNickname;
			}

			/* Reload window title (our nickname is shown there) */
			[mainWindow() updateTitleFor:self];
		}
		else
		{
			/* Update user tracking status for old nickname */
			if (oldNicknameIgnoreInfo) {
				[self updateUserTrackingStatusForEntry:oldNicknameIgnoreInfo withMessage:m];
			}

			/* Update user tracking status for new nickname */
			IRCAddressBookEntry *newNicknameIgnoreInfo = [self findUserTrackingAddressBookEntryForNickname:newNickname];

			if (newNicknameIgnoreInfo) {
				[self updateUserTrackingStatusForEntry:newNicknameIgnoreInfo withMessage:m];
			}
		}

		/* Inform style of change */
		[self postEventToViewController:@"nicknameChanged"];
	}

	/* Inform observers */
	[RZNotificationCenter() postNotificationName:IRCClientUserNicknameChangedNotification
										  object:self
										userInfo:@{
											@"oldNickname" : oldNickname,
											@"newNickname" : newNickname
										}];

	/* Look for user */
	IRCUser *user = nil;

	if (isPrintOnlyMessage == NO) {
		user = [self findUser:oldNickname];

		if (user == nil) {
			return;
		}
	}

	/* Setup block that is used by printing operations */
	NSString *messageToPrint = nil;

	if (myself) {
		messageToPrint = TXTLS(@"IRC[rr6-yo]", newNickname);
	} else {
		messageToPrint = TXTLS(@"IRC[fxw-5s]", oldNickname, newNickname);
	}

	void (^printingBlock)(IRCChannel *) = ^(IRCChannel *channel)
	{
		if (isPrintOnlyMessage == NO) {
			switch (channel.type) {
				case IRCChannelTypeChannel:
				{
					/* Rename the user in the channel */
					IRCChannelUser *member = [user userAssociatedWithChannel:channel];

					if (member == nil) {
						return;
					}

					[channel resortMember:member];

					break;
				}
				case IRCChannelTypePrivateMessage:
				{
					/* Rename private message if one with old name is found */
					if ([oldNickname isEqualToStringIgnoringCase:channel.name] == NO) {
						return;
					}

					IRCChannel *newNicknameQuery = [self findChannel:newNickname];

					if (newNicknameQuery) {
						break;
					}

					channel.name = newNickname;

					[mainWindow() reloadTreeItem:channel];

					[mainWindow() updateTitleFor:channel]; // Refresh hostmask

					break;
				}
				default:
				{
					return;
				}
			}
		} // isPrintOnlyMessage == NO

		/* Determine whether the message should be printed */
		if (channel.isChannel) {
			BOOL printMessage = [self postReceivedMessage:m withText:newNickname destinedFor:channel];

			if (printMessage && myself == NO)
			{
				if ([TPCPreferences showJoinLeave] == NO) {
					printMessage = NO;
				} else if (channel.config.ignoreGeneralEventMessages) {
					printMessage = NO;
				} else if (oldNicknameIgnoreInfo) {
					printMessage = (oldNicknameIgnoreInfo.ignoreGeneralEventMessages == NO);
				}
			}

			if (printMessage == NO) {
				return;
			}
		}

		/* Print message */
		[self print:messageToPrint
				 by:nil
		  inChannel:channel
			 asType:TVCLogLineTypeNick
			command:m.command
		 receivedAt:m.receivedAt];
	};

	/* Target print */
	if (isPrintOnlyMessage) {
		IRCChannel *channel = [self findChannel:channelName];

		if (channel == nil) {
			return;
		}

		printingBlock(channel);

		return;
	}

	/* Continue with normal operations */
	[self renameUser:user to:newNickname];

	for (IRCChannel *c in self.channelList) {
		printingBlock(c);
	}
}

- (void)receiveMode:(IRCMessage *)m
{
	NSParameterAssert(m != nil);

	NSAssertReturn([m paramsCount] > 1);

	BOOL isPrintOnlyMessage = m.isPrintOnlyMessage;

	NSString *sender = m.senderNickname;

	NSString *channelName = [m paramAt:0];
	NSString *modeString = [m sequence:1];

	/* Present user modes */
	if ([self stringIsChannelName:channelName] == NO) {
		BOOL printMessage = [self postReceivedCommand:@"UMODE" withText:modeString destinedFor:nil referenceMessage:m];

		if (printMessage) {
			[self print:TXTLS(@"IRC[v5d-ix]", sender, modeString)
					 by:nil
			  inChannel:nil
				 asType:TVCLogLineTypeMode
				command:m.command
			 receivedAt:m.receivedAt];
		}

		return;
	}

	/* Present channel modes */
	IRCChannel *channel = [self findChannel:channelName];

	if (channel == nil || channel.isChannel == NO) {
		return;
	}

	if (isPrintOnlyMessage == NO) {
		NSArray *modes = [channel.modeInfo updateModes:modeString];

		for (IRCModeInfo *mode in modes) {
			if ([mode isModeForChangingMemberModeOn:self] == NO) {
				continue;
			}

			[channel changeMember:mode.modeParameter mode:mode.modeSymbol value:mode.modeIsSet];
		}
	}

	BOOL printMessage = [self postReceivedMessage:m withText:modeString destinedFor:channel];

	if (printMessage) {
		printMessage = ([TPCPreferences showJoinLeave] && channel.config.ignoreGeneralEventMessages == NO);
	}

	if (printMessage) {
		[self print:TXTLS(@"IRC[v5d-ix]", sender, modeString)
				 by:nil
		  inChannel:channel
			 asType:TVCLogLineTypeMode
			command:m.command
		 receivedAt:m.receivedAt];
	}

	if (isPrintOnlyMessage == NO) {
		[mainWindow() updateTitleFor:channel];
	}
}

- (void)receiveTopic:(IRCMessage *)m
{
	NSParameterAssert(m != nil);

	NSAssertReturn([m paramsCount] == 2);

	BOOL isPrintOnlyMessage = m.isPrintOnlyMessage;

	NSString *sender = m.senderNickname;

	NSString *channelName = [m paramAt:0];
	NSString *topic = [m paramAt:1];

	IRCChannel *channel = [self findChannel:channelName];

	if (channel == nil || channel.isChannel == NO) {
		return;
	}

	if (isPrintOnlyMessage == NO) {
		channel.topic = topic;
	}

	BOOL printMessage = [self postReceivedMessage:m withText:topic destinedFor:channel];

	if (printMessage) {
		[self print:TXTLS(@"IRC[qq2-66]", sender, topic)
				 by:nil
		  inChannel:channel
			 asType:TVCLogLineTypeTopic
			command:m.command
		 receivedAt:m.receivedAt];
	}
}

- (void)receiveInvite:(IRCMessage *)m
{
	NSParameterAssert(m != nil);

	NSAssertReturn([m paramsCount] == 2);

	NSString *sender = m.senderNickname;

	NSString *channelName = [m paramAt:1];

	NSString *message = TXTLS(@"IRC[qw4-t3]", sender, m.senderUsername, m.senderAddress, channelName);

	/* Invite notifications are sent to frontmost channel on server of if it is
	 not on server, then it will be redirected to console. */
	BOOL printMessage = [self postReceivedMessage:m withText:channelName destinedFor:nil];

	if (printMessage) {
		[self print:message
				 by:nil
		  inChannel:[mainWindow() selectedChannelOn:self]
			 asType:TVCLogLineTypeInvite
			command:m.command
		 receivedAt:m.receivedAt];
	}

	[self notifyEvent:TXNotificationTypeInvite lineType:TVCLogLineTypeInvite target:nil nickname:sender text:channelName];

	if ([TPCPreferences autoJoinOnInvite]) {
		[self joinUnlistedChannel:channelName];
	}
}

- (void)receiveError:(IRCMessage *)m
{
	NSParameterAssert(m != nil);

	NSString *message = m.sequence;

	if (([message hasPrefix:@"Closing Link:"] && [message hasSuffix:@"(Excess Flood)"]) ||
		([message hasPrefix:@"Closing Link:"] && [message hasSuffix:@"(Max SendQ exceeded)"]))
	{
		__weak IRCClient *weakSelf = self;

		self.disconnectCallback = ^{
			[weakSelf cancelReconnect];
		};
	}

	[self printError:message asCommand:m.command];
}

- (void)receiveCertInfo:(IRCMessage *)m
{
	NSParameterAssert(m != nil);

	NSAssertReturn([m paramsCount] == 2);

	/* CERTINFO is not a standard command for Textual to
	 receive which means we should be strict about what
	 conditions we will accept it under. */
	if (self.zncBouncerIsSendingCertificateInfo == NO ||
		m.senderIsServer == NO ||
		[m.senderNickname isEqualToString:@"znc.in"] == NO)
	{
		return;
	}

	/* The data we expect to receive should be chunk split 
	 which means it is safe to assume a maximum length. */
	NSString *data = m.sequence;

	if (data.length < 2 || data.length > 65) {
		return;
	}

	/* Write line to the mutable buffer */
	if ( self.zncBouncerCertificateChainDataMutable) {
		[self.zncBouncerCertificateChainDataMutable appendFormat:@"%@\n", data];
	}
}

- (void)receiveBatch:(IRCMessage *)m
{
	NSParameterAssert(m != nil);

	NSAssertReturn([m paramsCount] >= 1);

	NSString *batchToken = [m paramAt:0];

	if (batchToken.length <= 1) {
		LogToConsoleError("Cannot process BATCH command because [batchToken length] <= 1");

		return;
	}

	NSString *batchType = [m paramAt:1];

	BOOL isBatchOpening = NO;

	if ([batchToken hasPrefix:@"+"]) {
		 batchToken = [batchToken substringFromIndex:1];

		isBatchOpening = YES;
	} else if ([batchToken hasPrefix:@"-"]) {
		batchToken = [batchToken substringFromIndex:1];

		isBatchOpening = NO;
	} else {
		LogToConsoleError("Cannot process BATCH command because there was no open or close modifier");

		return;
	}

	if ([batchToken onlyContainsCharactersFromCharacterSet:[NSCharacterSet Ato9UnderscoreDash]] == NO) {
		LogToConsoleError("Cannot process BATCH command because the batch token contains illegal characters");

		return;
	}

	if (isBatchOpening == NO)
	{
		/* Find batch message matching known token */
		IRCMessageBatchMessage *thisBatchMessage = [self.batchMessages queuedEntryWithBatchToken:batchToken];

		if (thisBatchMessage == nil) {
			LogToConsoleError("Cannot process BATCH command because -queuedEntryWithBatchToken: returned nil");

			return;
		}

		thisBatchMessage.batchIsOpen = NO;

		/* If this batch message has a parent batch, then we 
		 do not remove this batch or process it until the close
		 statement for the parent is received. */
		if (thisBatchMessage.parentBatchMessage) {
			return; // Nothing left to do...
		}

		batchType = thisBatchMessage.batchType;

		/* Process queued entries for this batch message. */
		/* The method used for processing queued entries will 
		 also remove it from queue once completed. */
		[self recursivelyProcessBatchMessage:thisBatchMessage];

		/* Set vendor specific flags based on BATCH command values */
		if ([batchType isEqualToString:@"znc.in/playback"]) {
			self.zncBouncerIsPlayingBackHistory = NO;
		} else if ([batchType isEqualToString:@"znc.in/tlsinfo"]) {
			self.zncBouncerIsSendingCertificateInfo = NO;
		}
	}
	else // isBatchOpening == NO
	{
		/* Check batch= value to look for possible parent batch.*/
		IRCMessageBatchMessage *parentBatchMessage = nil;

		NSString *parentBatchMessageToken = m.batchToken;

		if (parentBatchMessageToken) {
			parentBatchMessage = [self.batchMessages queuedEntryWithBatchToken:parentBatchMessageToken];
		}

		/* Create new batch message and queue it. */
		IRCMessageBatchMessage *newBatchMessage = [IRCMessageBatchMessage new];

		newBatchMessage.batchIsOpen = YES;

		newBatchMessage.batchToken = batchToken;
		newBatchMessage.batchType = batchType;

		newBatchMessage.parentBatchMessage = parentBatchMessage;

		[self.batchMessages queueEntry:newBatchMessage];

		/* Set vendor specific flags based on BATCH command values */
		if ([batchType isEqualToString:@"znc.in/playback"]) {
			self.zncBouncerIsPlayingBackHistory = self.isConnectedToZNC;
		} else if ([batchType isEqualToString:@"znc.in/tlsinfo"]) {
			self.zncBouncerIsSendingCertificateInfo = self.isConnectedToZNC;

			/* If this is parent batch (there is no @batch=), then we
			 reset the mutable object to read new data. */
			if (parentBatchMessageToken == nil) {
				self.zncBouncerCertificateChainDataMutable = [NSMutableString string];
			}
		}
	}
}

- (void)receiveChangeHost:(IRCMessage *)m
{
	NSParameterAssert(m != nil);

	NSAssertReturn([m paramsCount] == 2);

	NSString *username = [m paramAt:0];

	if ([username isHostmaskUsernameOn:self] == NO) {
		LogToConsoleError("Username ('%{private}@') received from CHGHOST command is improperly formatted", username);

		return;
	}

	NSString *address = [m paramAt:1];

	if ([address isHostmaskAddressOn:self] == NO) {
		LogToConsoleError("Address ('%{private}@') received from CHGHOST command is improperly formatted", address);

		return;
	}

	NSString *nickname = m.senderNickname;

	[self modifyUserUserWithNickname:nickname withBlock:^(IRCUserMutable *userMutable) {
		userMutable.username = username;
		userMutable.address = address;
	}];
}

#pragma mark -
#pragma mark BATCH Command

- (id)queuedBatchMessageWithToken:(NSString *)batchToken
{
	return [self.batchMessages queuedEntryWithBatchToken:batchToken];
}

- (BOOL)filterBatchCommandIncomingData:(IRCMessage *)m
{
	NSParameterAssert(m != nil);

	NSString *batchToken = m.batchToken;

	if (batchToken) {
		IRCMessageBatchMessage *thisBatchMessage = [self.batchMessages queuedEntryWithBatchToken:batchToken];

		if (thisBatchMessage.batchIsOpen) {
			[thisBatchMessage queueEntry:m];

			return YES;
		}
	}

	return NO;
}

- (void)recursivelyProcessBatchMessage:(IRCMessageBatchMessage *)batchMessage
{
	[self recursivelyProcessBatchMessage:batchMessage depth:0];
}

- (void)recursivelyProcessBatchMessage:(IRCMessageBatchMessage *)batchMessage depth:(NSInteger)recursionDepth
{
	NSParameterAssert(batchMessage != nil);

	if (batchMessage.batchIsOpen) {
		return;
	}

	NSArray *queuedEntries = batchMessage.queuedEntries;

	for (id queuedEntry in queuedEntries) {
		if ([queuedEntry isKindOfClass:[IRCMessage class]]) {
			[self processIncomingMessage:queuedEntry];
		} else if ([queuedEntry isKindOfClass:[IRCMessageBatchMessage class]]) {
			[self recursivelyProcessBatchMessage:queuedEntry depth:(recursionDepth + 1)];
		}
	}

	if (recursionDepth == 0) {
		[self.batchMessages dequeueEntry:batchMessage];
	}
}

#pragma mark -
#pragma mark Server Capability

- (void)enableCapability:(ClientIRCv3SupportedCapability)capability
{
	if ([self isCapabilityEnabled:capability] == NO) {
		self->_capabilities |= capability;
	}
}

- (void)disableCapability:(ClientIRCv3SupportedCapability)capability
{
	if ([self isCapabilityEnabled:capability]) {
		self->_capabilities &= ~capability;
	}
}

- (BOOL)isCapabilityEnabled:(ClientIRCv3SupportedCapability)capability
{
	return ((self->_capabilities & capability) == capability);
}

- (void)enablePendingCapability:(ClientIRCv3SupportedCapability)capability
{
	@synchronized (self.capabilitiesPending) {
		[self.capabilitiesPending addObjectWithoutDuplication:@(capability)];
	}
}

- (void)disablePendingCapability:(ClientIRCv3SupportedCapability)capability
{
	@synchronized (self.capabilitiesPending) {
		[self.capabilitiesPending removeObject:@(capability)];
	}
}

- (BOOL)isPendingCapabilityEnabled:(ClientIRCv3SupportedCapability)capability
{
	@synchronized (self.capabilitiesPending) {
		return [self.capabilitiesPending containsObject:@(capability)];
	}
}

- (nullable NSString *)capabilityStringValue:(ClientIRCv3SupportedCapability)capability
{
	NSString *stringValue = nil;

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wswitch"

	switch (capability) {
		case ClientIRCv3SupportedCapabilityAwayNotify:
		{
			stringValue = @"away-notify";

			break;
		}
		case ClientIRCv3SupportedCapabilityBatch:
		{
			stringValue = @"batch";

			break;
		}
		case ClientIRCv3SupportedCapabilityChangeHost:
		{
			stringValue = @"chghost";

			break;
		}
		case ClientIRCv3SupportedCapabilityEchoMessage:
		{
			stringValue = @"echo-message";

			break;
		}
		case ClientIRCv3SupportedCapabilityIdentifyCTCP:
		{
			stringValue = @"identify-ctcp";

			break;
		}
		case ClientIRCv3SupportedCapabilityIdentifyMsg:
		{
			stringValue = @"identify-msg";

			break;
		}
		case ClientIRCv3SupportedCapabilityMultiPrefix:
		{
			stringValue = @"multi-prefix";

			break;
		}
		case ClientIRCv3SupportedCapabilityPlayback:
		{
			stringValue = @"playback";

			break;
		}
		case ClientIRCv3SupportedCapabilitySASLExternal:
		case ClientIRCv3SupportedCapabilitySASLPlainText:
		case ClientIRCv3SupportedCapabilitySASLGeneric:
		case ClientIRCv3SupportedCapabilityIsIdentifiedWithSASL:
		case ClientIRCv3SupportedCapabilityIsInSASLNegotiation:
		{
			stringValue = @"sasl";

			break;
		}
		case ClientIRCv3SupportedCapabilityServerTime:
		{
			stringValue = @"server-time";

			break;
		}
		case ClientIRCv3SupportedCapabilityUserhostInNames:
		{
			stringValue = @"userhost-in-names";

			break;
		}
		case ClientIRCv3SupportedCapabilityMonitorCommand:
		{
			stringValue = @"monitor-command";

			break;
		}
		case ClientIRCv3SupportedCapabilityWatchCommand:
		{
			stringValue = @"watch-command";

			break;
		}
		case ClientIRCv3SupportedCapabilityPlanioPlayback:
		{
			stringValue = @"plan.io/playback";

			break;
		}
		case ClientIRCv3SupportedCapabilityZNCCertInfoModule:
		{
			stringValue = @"znc.in/tlsinfo";

			break;
		}
		case ClientIRCv3SupportedCapabilityZNCPlaybackModule:
		{
			stringValue = @"znc.in/playback";

			break;
		}
		case ClientIRCv3SupportedCapabilityZNCSelfMessage:
		{
			stringValue = @"znc.in/self-message";

			break;
		}
		case ClientIRCv3SupportedCapabilityZNCServerTime:
		{
			stringValue = @"znc.in/server-time";

			break;
		}
		case ClientIRCv3SupportedCapabilityZNCServerTimeISO:
		{
			stringValue = @"znc.in/server-time-iso";

			break;
		}
	}

#pragma clang diagnostic pop

	return stringValue;
}

- (ClientIRCv3SupportedCapability)capabilityFromStringValue:(NSString *)capabilityString
{
	NSParameterAssert(capabilityString != nil);

	if ([capabilityString isEqualToStringIgnoringCase:@"away-notify"]) {
		return ClientIRCv3SupportedCapabilityAwayNotify;
	} else if ([capabilityString isEqualToStringIgnoringCase:@"batch"]) {
		return ClientIRCv3SupportedCapabilityBatch;
	} else if ([capabilityString isEqualToStringIgnoringCase:@"chghost"]) {
		return ClientIRCv3SupportedCapabilityChangeHost;
	} else if ([capabilityString isEqualToStringIgnoringCase:@"echo-message"]) {
		return ClientIRCv3SupportedCapabilityEchoMessage;
	} else if ([capabilityString isEqualToStringIgnoringCase:@"multi-prefix"]) {
		return ClientIRCv3SupportedCapabilityMultiPrefix;
	} else if ([capabilityString isEqualToStringIgnoringCase:@"identify-msg"]) {
		return ClientIRCv3SupportedCapabilityIdentifyMsg;
	} else if ([capabilityString isEqualToStringIgnoringCase:@"identify-ctcp"]) {
		return ClientIRCv3SupportedCapabilityIdentifyCTCP;
	} else if ([capabilityString isEqualToStringIgnoringCase:@"sasl"]) {
		return ClientIRCv3SupportedCapabilitySASLGeneric;
	} else if ([capabilityString isEqualToStringIgnoringCase:@"server-time"]) {
		return ClientIRCv3SupportedCapabilityServerTime;
	} else if ([capabilityString isEqualToStringIgnoringCase:@"userhost-in-names"]) {
		return ClientIRCv3SupportedCapabilityUserhostInNames;
	} else if ([capabilityString isEqualToStringIgnoringCase:@"plan.io/playback"]) {
		return ClientIRCv3SupportedCapabilityPlanioPlayback;
	} else if ([capabilityString isEqualToStringIgnoringCase:@"znc.in/playback"]) {
		return ClientIRCv3SupportedCapabilityZNCPlaybackModule;
	} else if ([capabilityString isEqualToStringIgnoringCase:@"znc.in/self-message"]) {
		return ClientIRCv3SupportedCapabilityZNCSelfMessage;
	} else if ([capabilityString isEqualToStringIgnoringCase:@"znc.in/server-time"]) {
		return ClientIRCv3SupportedCapabilityZNCServerTime;
	} else if ([capabilityString isEqualToStringIgnoringCase:@"znc.in/server-time-iso"]) {
		return ClientIRCv3SupportedCapabilityZNCServerTimeISO;
	} else if ([capabilityString isEqualToStringIgnoringCase:@"znc.in/tlsinfo"]) {
		return ClientIRCv3SupportedCapabilityZNCCertInfoModule;
	}

	return 0;
}

- (NSString *)enabledCapabilitiesStringValue
{
	NSMutableArray *enabledCapabilities = [NSMutableArray array];

	void (^appendValue)(ClientIRCv3SupportedCapability) = ^(ClientIRCv3SupportedCapability capability) {
		if ([self isCapabilityEnabled:capability] == NO) {
			return;
		}

		NSString *stringValue = [self capabilityStringValue:capability];

		if (stringValue) {
			[enabledCapabilities addObject:stringValue];
		}
	};

	appendValue(ClientIRCv3SupportedCapabilityAwayNotify);
	appendValue(ClientIRCv3SupportedCapabilityBatch);
	appendValue(ClientIRCv3SupportedCapabilityChangeHost);
	appendValue(ClientIRCv3SupportedCapabilityEchoMessage);
	appendValue(ClientIRCv3SupportedCapabilityIdentifyCTCP);
	appendValue(ClientIRCv3SupportedCapabilityIdentifyMsg);
	appendValue(ClientIRCv3SupportedCapabilityIsIdentifiedWithSASL);
	appendValue(ClientIRCv3SupportedCapabilityMultiPrefix);
	appendValue(ClientIRCv3SupportedCapabilityPlayback);
	appendValue(ClientIRCv3SupportedCapabilityServerTime);
	appendValue(ClientIRCv3SupportedCapabilityUserhostInNames);
	appendValue(ClientIRCv3SupportedCapabilityZNCCertInfoModule);
	appendValue(ClientIRCv3SupportedCapabilityZNCPlaybackModule);
	appendValue(ClientIRCv3SupportedCapabilityZNCSelfMessage);

	NSString *stringValue = [enabledCapabilities componentsJoinedByString:@", "];

	return stringValue;
}

- (void)sendNextCapability
{
	if (self.capabilityNegotiationIsPaused) {
		return;
	}

	@synchronized (self.capabilitiesPending) {
		/* -CapabilitiesPending can contain values that are used internally for state traking 
		 and should never meet the socket. To workaround this as best we can, we scan the 
		 array for the first capability that is acceptable for negotiation. */
		NSUInteger nextCapabilityIndex =
		[self.capabilitiesPending indexOfObjectPassingTest:^BOOL(NSNumber *capabilityPending, NSUInteger index, BOOL *stop) {
			ClientIRCv3SupportedCapability capability = capabilityPending.unsignedIntegerValue;

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wtautological-compare"

			return
			(capability == ClientIRCv3SupportedCapabilityAwayNotify				||
			 capability == ClientIRCv3SupportedCapabilityBatch					||
			 capability == ClientIRCv3SupportedCapabilityChangeHost				||
			 capability == ClientIRCv3SupportedCapabilityEchoMessage			||
			 capability == ClientIRCv3SupportedCapabilityIdentifyCTCP			||
			 capability == ClientIRCv3SupportedCapabilityIdentifyMsg			||
			 capability == ClientIRCv3SupportedCapabilityMultiPrefix			||
			 capability == ClientIRCv3SupportedCapabilitySASLGeneric			||
			 capability == ClientIRCv3SupportedCapabilityServerTime				||
			 capability == ClientIRCv3SupportedCapabilityUserhostInNames		||
			 capability == ClientIRCv3SupportedCapabilityPlanioPlayback			||
			 capability == ClientIRCv3SupportedCapabilityZNCCertInfoModule		||
			 capability == ClientIRCv3SupportedCapabilityZNCPlaybackModule		||
			 capability == ClientIRCv3SupportedCapabilityZNCSelfMessage			||
			 capability == ClientIRCv3SupportedCapabilityZNCServerTime			||
			 capability == ClientIRCv3SupportedCapabilityZNCServerTimeISO);

#pragma clang diagnostic pop
		}];

		if (nextCapabilityIndex == NSNotFound) {
			[self sendCapability:@"END" data:nil];

			return;
		}

		ClientIRCv3SupportedCapability capability =
		[self.capabilitiesPending unsignedIntegerAtIndex:nextCapabilityIndex];

		[self.capabilitiesPending removeObjectAtIndex:nextCapabilityIndex];

		NSString *stringValue = [self capabilityStringValue:capability];

		[self sendCapability:@"REQ" data:stringValue];
	}
}

- (void)pauseCapabilityNegotiation
{
	self.capabilityNegotiationIsPaused = YES;
}

- (void)resumeCapabilityNegotiation
{
	self.capabilityNegotiationIsPaused = NO;

	[self sendNextCapability];
}

- (BOOL)isCapabilitySupported:(NSString *)capabilityString
{
	NSParameterAssert(capabilityString != nil);

	// Information about several of these supported CAP
	// extensions can be found at: http://ircv3.atheme.org

	if ([capabilityString isEqualToStringIgnoringCase:@"echo-message"]) {
		return [TPCPreferences enableEchoMessageCapability];
	}

	return
	([capabilityString isEqualToStringIgnoringCase:@"away-notify"]				||
	 [capabilityString isEqualToStringIgnoringCase:@"batch"]					||
	 [capabilityString isEqualToStringIgnoringCase:@"chghost"]					||
	 [capabilityString isEqualToStringIgnoringCase:@"identify-ctcp"]			||
	 [capabilityString isEqualToStringIgnoringCase:@"identify-msg"]				||
	 [capabilityString isEqualToStringIgnoringCase:@"multi-prefix"]				||
	 [capabilityString isEqualToStringIgnoringCase:@"sasl"]						||
	 [capabilityString isEqualToStringIgnoringCase:@"server-time"]				||
	 [capabilityString isEqualToStringIgnoringCase:@"userhost-in-names"]		||
	 [capabilityString isEqualToStringIgnoringCase:@"plan.io/playback"]			||
	 [capabilityString isEqualToStringIgnoringCase:@"znc.in/playback"]			||
	 [capabilityString isEqualToStringIgnoringCase:@"znc.in/self-message"]		||
	 [capabilityString isEqualToStringIgnoringCase:@"znc.in/server-time"]		||
	 [capabilityString isEqualToStringIgnoringCase:@"znc.in/server-time-iso"]	||
	 [capabilityString isEqualToStringIgnoringCase:@"znc.in/tlsinfo"]);
}

- (void)toggleCapability:(NSString *)capabilityString enabled:(BOOL)enabled
{
	[self toggleCapability:capabilityString enabled:enabled isUpdateRequest:NO];
}

- (void)toggleCapability:(NSString *)capabilityString enabled:(BOOL)enabled isUpdateRequest:(BOOL)isUpdateRequest
{
	NSParameterAssert(capabilityString != nil);

	if ([capabilityString isEqualToStringIgnoringCase:@"sasl"]) {
		if (enabled) {
			if ([self sendSASLIdentificationRequest]) {
				[self pauseCapabilityNegotiation];
			}
		}

		return;
	}

	ClientIRCv3SupportedCapability capability = [self capabilityFromStringValue:capabilityString];

	if (capability == 0) {
		return;
	}

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wtautological-compare"

	if (capability == ClientIRCv3SupportedCapabilityZNCServerTime ||
		capability == ClientIRCv3SupportedCapabilityZNCServerTimeISO)
	{
		capability = ClientIRCv3SupportedCapabilityServerTime;
	}

	if (capability == ClientIRCv3SupportedCapabilityPlanioPlayback ||
		capability == ClientIRCv3SupportedCapabilityZNCPlaybackModule)
	{
		capability = ClientIRCv3SupportedCapabilityPlayback;
	}

#pragma clang diagnostic pop

	if (enabled) {
		[self enableCapability:capability];
	} else {
		[self disableCapability:capability];
	}
}

- (void)processPendingCapability:(NSString *)capabilityString
{
	NSParameterAssert(capabilityString != nil);

	NSArray *components = [capabilityString componentsSeparatedByString:@"="];

	NSString *capability = capabilityString;

	NSArray<NSString *> *capabilityOptions = nil;

	if (components.count == 2) {
		capability = components[0];

		capabilityOptions = [components[1] componentsSeparatedByString:@","];
	}

	[self processPendingCapability:capability options:capabilityOptions];
}

- (void)processPendingCapability:(NSString *)capabilityString options:(nullable NSArray<NSString *> *)capabilityOptions
{
	NSParameterAssert(capabilityString != nil);

	if ([self isCapabilitySupported:capabilityString] == NO) {
		return;
	}

	if ([capabilityString isEqualToString:@"sasl"]) {
		[self processPendingCapabilityForSASL:capabilityOptions];

		return;
	}

	ClientIRCv3SupportedCapability capability = [self capabilityFromStringValue:capabilityString];

	[self enablePendingCapability:capability];
}

- (void)receiveCapabilityOrAuthenticationRequest:(IRCMessage *)m
{
	/* Implementation based off Colloquy's own. */
	NSParameterAssert(m != nil);

	NSAssertReturn([m paramsCount] > 0);

	NSString *command = m.command;
	NSString *modifier = [m paramAt:0];
	NSString *subcommand = [m paramAt:1];
	NSString *actions = [m sequence:2];

	if ([command isEqualToStringIgnoringCase:@"CAP"])
	{
		if ([subcommand isEqualToStringIgnoringCase:@"LS"]) {
			NSArray *caps = [actions componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];

			for (NSString *cap in caps) {
				[self processPendingCapability:cap];
			}
		} else if ([subcommand isEqualToStringIgnoringCase:@"ACK"]) {
			NSArray *caps = [actions componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];

			for (NSString *cap in caps) {
				[self toggleCapability:cap enabled:YES isUpdateRequest:NO];
			}
		} else if ([subcommand isEqualToStringIgnoringCase:@"NAK"]) {
			NSArray *caps = [actions componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];

			for (NSString *cap in caps) {
				[self toggleCapability:cap enabled:NO isUpdateRequest:NO];
			}
		} else if ([subcommand isEqualToStringIgnoringCase:@"NEW"]) {
			NSArray *caps = [actions componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];

			for (NSString *cap in caps) {
				[self processPendingCapability:cap];
			}
		} else if ([subcommand isEqualToStringIgnoringCase:@"DEL"]) {
			NSArray *caps = [actions componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];

			for (NSString *cap in caps) {
				[self toggleCapability:cap enabled:NO isUpdateRequest:YES];
			}
		}

		[self sendNextCapability];
	}
	else if ([command isEqualToStringIgnoringCase:@"AUTHENTICATE"])
	{
		if ([modifier isEqualToString:@"+"]) {
			[self sendSASLIdentificationInformation];
		}
	}

	[self postReceivedMessage:m];
}

#pragma mark -
#pragma mark SASL Negotiation

- (void)processPendingCapabilityForSASL:(nullable NSArray<NSString *> *)capabilityOptions
{
	ClientIRCv3SupportedCapability identificationMechanism = 0;

	if (self.socket.isConnectedWithClientSideCertificate &&
		self.config.saslAuthenticationDisableExternalMechanism == NO)
	{
		if (capabilityOptions.count == 0 ||
			[capabilityOptions containsObjectIgnoringCase:@"EXTERNAL"])
		{
			identificationMechanism = ClientIRCv3SupportedCapabilitySASLExternal;

			[self enablePendingCapability:ClientIRCv3SupportedCapabilitySASLExternal];
		}
	}

	if (identificationMechanism == 0 &&
		self.config.nicknamePassword.length > 0)
	{
		if (capabilityOptions.count == 0 ||
			[capabilityOptions containsObjectIgnoringCase:@"PLAIN"])
		{
			identificationMechanism = ClientIRCv3SupportedCapabilitySASLPlainText;

			[self enablePendingCapability:ClientIRCv3SupportedCapabilitySASLPlainText];
		}
	}

	if (identificationMechanism != 0) {
		[self enablePendingCapability:ClientIRCv3SupportedCapabilitySASLGeneric];
	}
}

- (void)sendSASLIdentificationInformation
{
	if ([self isPendingCapabilityEnabled:ClientIRCv3SupportedCapabilityIsInSASLNegotiation] == NO) {
		return;
	}

	if ([self isPendingCapabilityEnabled:ClientIRCv3SupportedCapabilitySASLPlainText])
	{
		NSString *authString = [NSString stringWithFormat:@"%@%C%@%C%@",
								 self.config.username, 0x00,
								 self.config.username, 0x00,
								 self.config.nicknamePassword];

		NSArray *authStrings = [authString base64EncodingWithLineLength:400];

		for (NSString *string in authStrings) {
			[self sendCapabilityAuthenticate:string];
		}

		if (authStrings.count == 0 || ((NSString *)authStrings.lastObject).length == 400) {
			[self sendCapabilityAuthenticate:@"+"];
		}
	}
	else if ([self isPendingCapabilityEnabled:ClientIRCv3SupportedCapabilitySASLExternal])
	{
		[self sendCapabilityAuthenticate:@"+"];
	}
}

- (BOOL)sendSASLIdentificationRequest
{
	if ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityIsIdentifiedWithSASL]) {
		return NO;
	}

	if ([self isPendingCapabilityEnabled:ClientIRCv3SupportedCapabilityIsInSASLNegotiation]) {
		return NO;
	}

	[self enablePendingCapability:ClientIRCv3SupportedCapabilityIsInSASLNegotiation];

	if ([self isPendingCapabilityEnabled:ClientIRCv3SupportedCapabilitySASLPlainText]) {
		[self sendCapabilityAuthenticate:@"PLAIN"];

		return YES;
	} else if ([self isPendingCapabilityEnabled:ClientIRCv3SupportedCapabilitySASLExternal]) {
		[self sendCapabilityAuthenticate:@"EXTERNAL"];

		return YES;
	}

	return NO;
}

- (void)resetSASLNegotiation
{
	[self disablePendingCapability:ClientIRCv3SupportedCapabilitySASLGeneric];
	[self disablePendingCapability:ClientIRCv3SupportedCapabilitySASLPlainText];
	[self disablePendingCapability:ClientIRCv3SupportedCapabilitySASLExternal];
	[self disablePendingCapability:ClientIRCv3SupportedCapabilityIsInSASLNegotiation];

	[self disableCapability:ClientIRCv3SupportedCapabilityIsIdentifiedWithSASL];
}

#pragma mark -
#pragma mark Protocol Handlers

- (void)receivePing:(IRCMessage *)m
{
	NSParameterAssert(m != nil);

	NSAssertReturn([m paramsCount] > 0);

	NSString *token = [m sequence:0];

	[self sendPong:token];

	[self postReceivedMessage:m];
}

- (void)receiveAwayNotifyCapability:(IRCMessage *)m
{
	NSParameterAssert(m != nil);

	if ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityAwayNotify] == NO) {
		return;
	}

	BOOL away = (m.sequence.length > 0);

	NSString *nickname = m.senderNickname;

	[self modifyUserWithNickname:nickname asAway:away];
}

- (void)receiveInit:(IRCMessage *)m // Raw numeric = 001
{
	NSParameterAssert(m != nil);

	/* Manage timers */
	[self startPongTimer];

	[self stopRetryTimer];

	/* Manage properties */
	self.isLoggedIn = YES;

	self.supportInfo.serverAddress = m.senderHostmask;

	self.invokingISONCommandForFirstTime = YES;

	self.reconnectEnabledBecauseOfSleepMode = NO;

	self.tryingNicknameSentNickname = nil;

	self.userNickname = [m paramAt:0];

	self.successfulConnects += 1;

	/* Begin enforcing flood control */
	[self.socket enforceFloodControl];

	/* Post event */
	[self postEventToViewController:@"serverConnected"];

	[self notifyEvent:TXNotificationTypeConnect lineType:TVCLogLineTypeDebug];

	/* Perform login commands */
	for (__strong NSString *command in self.config.loginCommands) {
		if ([command hasPrefix:@"/"]) {
			command = [command substringFromIndex:1];
		}

		[self sendCommand:command completeTarget:NO target:nil];
	}

	/* Request certificate information */
	if ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityZNCCertInfoModule]) {
		[self sendCommand:@"send-data" toZNCModuleNamed:@"tlsinfo"];
	}

	/* Request playback since the last seen message when previously connected */
	[self requestPlayback];

	/* Activate existing queries */
	for (IRCChannel *c in self.channelList) {
		if (c.privateMessage) {
			[c activate];

			[mainWindow() reloadTreeItem:c];
		}
	}

	[mainWindow() reloadTreeItem:self];

	[mainWindow() updateTitleFor:self];

	[mainWindowTextField() updateSegmentedController];

	/* Everything else */
	if (self.config.autojoinWaitsForNickServ == NO || [self isCapabilityEnabled:ClientIRCv3SupportedCapabilityIsIdentifiedWithSASL]) {
		[self performAutoJoin];
	} else {
		/* If we wait for NickServ we set a timer of 3.0 seconds before performing auto join.
		 When this timer is executed, if we do not have any knowledge of NickServ existing
		 on the current server, then we perform the autojoin. This is primarily a fix for the
		 ZNC SASL module which will complete identification before connecting and once connected
		 Textual will have no knowledge of whether the local user is identified or not. */
		/* NickServ will send a notice asking for identification as soon as connection occurs so
		 this is the best patch. At least for right now. */

		if (self.isConnectedToZNC) {
			[self performSelectorInCommonModes:@selector(performAutoJoin) withObject:nil afterDelay:3.0];
		} else {
			[self startAutojoinDelayedWarningTimer];
		}
	}

	/* We need time for the server to send its configuration */
	[self performSelectorInCommonModes:@selector(populateISONTrackedUsersList) withObject:nil afterDelay:10.0];
}

- (void)receiveNumericReply:(IRCMessage *)m
{
	NSParameterAssert(m != nil);

	NSInteger numeric = m.commandNumeric;

	if (numeric > 400 && numeric < 597 && numeric != ERR_NOMOTD) {
		[self receiveErrorNumericReply:m];

		return;
	}

	BOOL printMessage = YES;

	/* These numerics are treated differently below which is why the exception exists.
	 For example, for channel topic, only the contents of the topic are sent to the filter. */
	if (numeric != RPL_UMODEIS &&
		numeric != RPL_CHANNELMODEIS &&
		numeric != RPL_TOPIC &&
		numeric != RPL_TOPICWHOTIME)
	{
		printMessage = [self postReceivedMessage:m];
	}

	switch (numeric) {
		case RPL_WELCOME:
		{
			[self receiveInit:m];

			if (printMessage) {
				[self printReply:m];
			}

			break;
		}
		case RPL_YOURHOST:
		case RPL_CREATED:
		case RPL_MYINFO:
		{
			if (printMessage) {
				[self printReply:m];
			}

			break;
		}
		case RPL_ISUPPORT:
		{
			NSAssertReturn([m paramsCount] >= 3);

			NSMutableArray *params = [m.params mutableCopy];

			[params removeObjectAtIndex:0]; // Remove nickname

			NSString *message = params.lastObject;

			[params removeLastObject]; // Remove "are supported by this server"

			NSString *configuration = [params componentsJoinedByString:@" "];

			[self.supportInfo processConfigurationData:configuration];

			if (printMessage) {
				NSString *configurationFormatted = self.supportInfo.stringValueForLastUpdate;

				[self printDebugInformationToConsole:TXTLS(@"IRC[u51-nn]", configurationFormatted, message)
										   asCommand:m.command];
			}

			break;
		}
		case RPL_REDIR:
		{
			NSAssertReturn([m paramsCount] == 4);

			NSString *serverAddress = [m paramAt:1];
			NSString *serverPort = [m paramAt:2];

			self.disconnectType = IRCClientDisconnectModeServerRedirect;

			/* If the address is thought to be invalid, then we still
			 perform the disconnect suggested by the redirect, but
			 we do not go any further than that. */
			if (serverAddress.validInternetAddress == NO ||
				serverPort.validInternetPort == NO)
			{
				[self disconnect];

				return;
			}

			/* Perform reconnect to specified locations */
			__weak IRCClient *weakSelf = self;

			self.disconnectCallback = ^{
				[weakSelf connect];
			};

			[self disconnect];

			/* -disconnect would destroy this so we set them after... */
			self.temporaryServerAddressOverride = serverAddress;
			self.temporaryServerPortOverride = serverPort.integerValue;

			break;
		}
		case RPL_STATSCONN:
		case RPL_LUSERCLIENT:
		case RPL_LUSERHOP:
		case RPL_LUSERUNKNOWN:
		case RPL_LUSERCHANNELS:
		case RPL_LUSERME:
		{
			if (printMessage) {
				[self printReply:m];
			}

			break;
		}
		case RPL_LOCALUSERS:
		case RPL_GLOBALUSERS:
		{
			NSAssertReturn(printMessage);

			NSString *message = nil;

			if (m.paramsCount == 4) {
				/* Removes user count from in front of messages on IRCds that send them.
				 Example: ">> :irc.example.com 265 Guest 2 3 :Current local users 2, max 3" */

				message = [m sequence:3];
			} else {
				message = m.sequence;
			}

			[self print:message
					 by:nil
			  inChannel:nil
				 asType:TVCLogLineTypeDebug
				command:m.command
			 receivedAt:m.receivedAt];

			break;
		}
		case RPL_MOTD:
		case RPL_MOTDSTART:
		case RPL_ENDOFMOTD:
		case ERR_NOMOTD:
		{
			NSAssertReturn(printMessage);

			if ([TPCPreferences displayServerMOTD] == NO) {
				break;
			}

			if (numeric == ERR_NOMOTD) {
				[self printErrorReply:m];
			} else {
				[self printReply:m];
			}

			break;
		}
		case RPL_UMODEIS:
		{
			NSAssertReturn([m paramsCount] > 1);

			NSString *nickname = [m paramAt:0];

			NSString *modeString = [m paramAt:1];

			if ([modeString isEqualToString:@"+"]) {
				break;
			}

			printMessage = [self postReceivedMessage:m withText:modeString destinedFor:nil];

			if (printMessage) {
				[self print:TXTLS(@"IRC[ipj-34]", nickname, modeString)
						 by:nil
				  inChannel:nil
					 asType:TVCLogLineTypeDebug
					command:m.command
				 receivedAt:m.receivedAt];
			}

			break;
		}
		case RPL_AWAY:
		{
			NSAssertReturn([m paramsCount] == 3);

			NSString *awayNickname = [m paramAt:1];
			NSString *awayComment = [m paramAt:2];

			IRCChannel *channel = [self findChannel:awayNickname];

			NSString *message = TXTLS(@"IRC[c1h-fq]", awayNickname, awayComment);

			if (channel == nil) {
				channel = [mainWindow() selectedChannelOn:self];
			}

			IRCUser *user = [self findUser:awayNickname];

			if ( user) {
				if (self.monitorAwayStatus) {
					[user markAsAway];
				}

				if (user.presentAwayMessageFor301 == NO) {
					break;
				}
			}

			if (printMessage) {
				[self print:message
						 by:nil
				  inChannel:channel
					 asType:TVCLogLineTypeDebug
					command:m.command
				 receivedAt:m.receivedAt];
			}

			break;
		}
		case RPL_UNAWAY:
		case RPL_NOWAWAY:
		{
			BOOL away = (numeric == RPL_NOWAWAY);

			self.userIsAway = away;
			
			[mainWindow() updateTitle];

			if (printMessage) {
				[self printReply:m];
			}

			/* Update our own status. This has to only be done with away-notify CAP enabled.
			 Old, WHO based information requests will still show our own status. */
			IRCUser *myself = self.myself;

			if (myself == nil) {
				break;
			}

			[self modifyUser:myself asAway:away];

			break;
		}
		case RPL_CHANNELSMSG:
		case RPL_WHOISBOT:
		case RPL_WHOISHELPOP:
		case RPL_WHOISHOST:
		case RPL_WHOISMODES:
		case RPL_WHOISOPERATOR:
		case RPL_WHOISREALIP:
		case RPL_WHOISREGNICK:
		case RPL_WHOISSECURE:
		case RPL_WHOISSPECIAL:
		{
			NSAssertReturn([m paramsCount] > 2);

			if (printMessage) {
				[self printReply:m inChannel:[mainWindow() selectedChannelOn:self]];
			}

			break;
		}
		case RPL_WHOISACTUALLY:
		{
			NSAssertReturn([m paramsCount] == 5);

			NSAssertReturn(printMessage);

			NSString *nickname = [m paramAt:1];
			NSString *hostmask = [m paramAt:2];
			NSString *ipAddress = [m paramAt:3];

			NSString *message = nil;

			if (self.inWhowasResponse) { // bahamut sends RPL_WHOISACTUALLY in WHOWAS
				message = TXTLS(@"IRC[x69-rz]", nickname, hostmask, ipAddress);
			} else {
				message = TXTLS(@"IRC[3oa-mv]", nickname, hostmask, ipAddress);
			}

			[self print:message
					 by:nil
			  inChannel:[mainWindow() selectedChannelOn:self]
				 asType:TVCLogLineTypeDebug
				command:m.command
			 receivedAt:m.receivedAt];

			break;
		}
		case RPL_WHOISUSER:
		case RPL_WHOWASUSER:
		{
			NSAssertReturn([m paramsCount] >= 6);

			NSString *nickname = [m paramAt:1];
			NSString *username = [m paramAt:2];
			NSString *address = [m paramAt:3];
			NSString *realName = [m paramAt:5];

			if ([realName hasPrefix:@":"]) {
				realName = [realName substringFromIndex:1];
			}

			self.inWhoisResponse = (numeric == RPL_WHOISUSER);
			self.inWhowasResponse = (numeric == RPL_WHOWASUSER);

			NSString *message = nil;

			if (self.inWhowasResponse) {
				if (printMessage) {
					message = TXTLS(@"IRC[32c-87]", nickname, username, address, realName);
				}
			} else {
				if (printMessage) {
					message = TXTLS(@"IRC[plg-lr]", nickname, username, address, realName);
				}

				/* Update local cache of our hostmask */
				if ([self nicknameIsMyself:nickname]) {
					NSString *hostmask = [NSString stringWithFormat:@"%@!%@@%@", nickname, username, address];

					self.userHostmask = hostmask;
				}
			}

			if (message) {
				[self print:message
						 by:nil
				  inChannel:[mainWindow() selectedChannelOn:self]
					 asType:TVCLogLineTypeDebug
					command:m.command
				 receivedAt:m.receivedAt];
			}

			break;
		}
		case RPL_WHOISSERVER:
		{
			NSAssertReturn([m paramsCount] == 4);

			NSAssertReturn(printMessage);

			NSString *nickname = [m paramAt:1];
			NSString *serverAddress = [m paramAt:2];
			NSString *serverInfo = [m paramAt:3];

			NSString *message = nil;

			if (self.inWhowasResponse) { // bahamut sends RPL_WHOISSERVER in WHOWAS
				NSString *timeInfo = TXFormatDateLongStyle(serverInfo, YES);

				if (timeInfo == nil) {
					timeInfo = serverInfo;
				}

				message = TXTLS(@"IRC[cdu-ed]", nickname, serverAddress, timeInfo);
			} else {
				message = TXTLS(@"IRC[h19-n2]", nickname, serverAddress, serverInfo);
			}

			[self print:message
					 by:nil
			  inChannel:[mainWindow() selectedChannelOn:self]
				 asType:TVCLogLineTypeDebug
				command:m.command
			 receivedAt:m.receivedAt];

			break;
		}
		case RPL_WHOISIDLE:
		{
			NSAssertReturn([m paramsCount] == 5);

			NSAssertReturn(printMessage);

			NSString *nickname = [m paramAt:1];
			NSString *idleTime = [m paramAt:2];
			NSString *connectTime = [m paramAt:3];

			idleTime = TXHumanReadableTimeInterval(idleTime.doubleValue, NO, 0);

			NSDate *connTimeDate = [NSDate dateWithTimeIntervalSince1970:connectTime.doubleValue];

			connectTime = TXFormatDateLongStyle(connTimeDate, YES);

			NSString *message = TXTLS(@"IRC[6hn-o6]", nickname, connectTime, idleTime);

			[self print:message
					 by:nil
			  inChannel:[mainWindow() selectedChannelOn:self]
				 asType:TVCLogLineTypeDebug
				command:m.command
			 receivedAt:m.receivedAt];

			break;
		}
		case RPL_WHOISCHANNELS:
		{
			NSAssertReturn([m paramsCount] == 3);

			NSAssertReturn(printMessage);

			NSString *nickname = [m paramAt:1];
			NSString *channels = [m paramAt:2];

			NSString *message = TXTLS(@"IRC[onk-l5]", nickname, channels);

			[self print:message
					 by:nil
			  inChannel:[mainWindow() selectedChannelOn:self]
				 asType:TVCLogLineTypeDebug
				command:m.command
			 receivedAt:m.receivedAt];

			break;
		}
		case RPL_WHOISACCOUNT:
		{
			NSAssertReturn([m paramsCount] == 4);

			NSAssertReturn(printMessage);

			NSString *message = [NSString stringWithFormat:@"%@ %@ %@", [m paramAt:1], [m sequence:3], [m paramAt:2]];

			[self print:message
					 by:nil
			  inChannel:[mainWindow() selectedChannelOn:self]
				 asType:TVCLogLineTypeDebug
				command:m.command
			 receivedAt:m.receivedAt];

			break;
		}
		case RPL_ENDOFWHOIS:
		{
			self.inWhoisResponse = NO;

/*			if (printMessage) {
				[self printReply:m inChannel:[mainWindow() selectedChannelOn:self]];
			} */

			break;
		}
		case RPL_ENDOFWHOWAS:
		{
			self.inWhowasResponse = NO;

/*			if (printMessage) {
				[self printReply:m inChannel:[mainWindow() selectedChannelOn:self]];
			} */

			break;
		}
		case RPL_CHANNELMODEIS:
		{
			NSAssertReturn([m paramsCount] > 2);

			NSString *channelName = [m paramAt:1];

			NSString *modeString = [m sequence:2];

			if ([modeString isEqualToString:@"+"]) {
				return;
			}

			IRCChannel *channel = [self findChannel:channelName];

			if (channel == nil) {
				break;
			}

			if (channel.isActive) {
				[channel.modeInfo clear];

				[channel.modeInfo updateModes:modeString];
			}

			printMessage = [self postReceivedMessage:m withText:modeString destinedFor:channel];

			/* We perform this check after printMessage is defined so that
			 filters have a chance to act on the input. */
			/* IRCClient perform mode requests for channels without the user
			 asking for it so we must check that here. */
			if (channel.channelModesReceived == NO) {
				channel.channelModesReceived = YES;
			}

			if (printMessage) {
				NSString *message = channel.modeInfo.stringWithMaskedPassword;

				[self print:TXTLS(@"IRC[obp-ww]", message)
						 by:nil
				  inChannel:channel
					 asType:TVCLogLineTypeMode
					command:m.command
				 receivedAt:m.receivedAt];
			}

			break;
		}
		case RPL_TOPIC:
		{
			NSAssertReturn([m paramsCount] == 3);

			NSString *channelName = [m paramAt:1];
			NSString *topic = [m paramAt:2];

			IRCChannel *channel = [self findChannel:channelName];

			if (channel == nil) {
				break;
			}

			printMessage = [self postReceivedMessage:m withText:topic destinedFor:channel];

			channel.topic = topic;

			if (printMessage) {
				[self print:TXTLS(@"IRC[7nm-7v]", topic)
						 by:nil
				  inChannel:channel
					 asType:TVCLogLineTypeTopic
					command:m.command
				 receivedAt:m.receivedAt];
			}

			break;
		}
		case RPL_TOPICWHOTIME:
		{
			NSAssertReturn([m paramsCount] == 4);

			NSString *channelName = [m paramAt:1];

			IRCChannel *channel = [self findChannel:channelName];

			if (channel == nil) {
				break;
			}

			printMessage = [self postReceivedMessage:m withText:nil destinedFor:channel];

			if (printMessage == NO) {
				return;
			}

			NSString *topicSetter = [m paramAt:2];
			NSString *setTime = [m paramAt:3];

			topicSetter = topicSetter.nicknameFromHostmask;

			NSDate *setTimeDate = [NSDate dateWithTimeIntervalSince1970:setTime.doubleValue];

			setTime = TXFormatDateLongStyle(setTimeDate, YES);

			NSString *message = TXTLS(@"IRC[y7s-3e]", topicSetter, setTime);

			[self print:message
					 by:nil
			  inChannel:channel
				 asType:TVCLogLineTypeTopic
				command:m.command
			 receivedAt:m.receivedAt];

			break;
		}
		case RPL_CREATIONTIME:
		{
			break; // Ignore
		}
		case RPL_INVITING:
		{
			NSAssertReturn([m paramsCount] == 3);

			NSAssertReturn(printMessage);

			NSString *nickname = [m paramAt:1];
			NSString *channelName = [m paramAt:2];

			IRCChannel *channel = [self findChannel:channelName];

			if (channel == nil) {
				break;
			}

			[self print:TXTLS(@"IRC[wk4-rv]", nickname, channelName)
					 by:nil
			  inChannel:channel
				 asType:TVCLogLineTypeDebug
				command:m.command
			 receivedAt:m.receivedAt];

			break;
		}
		case RPL_ISON:
		{
			/* Present reply to the user if we have destination */
			BOOL visibleIsonRequest = self.requestedCommands.visibleIsonRequest;

			[self.requestedCommands recordIsonRequestClosed];

			if (visibleIsonRequest) {
				if (printMessage) {
					[self printReplyToHiddenCommandResponsesQuery:m];
				}

				/* It is important that we don't process logic for visible
				 requests because if user does ISON for people that aren't
				 on the tracked list and the logic below sees the response
				 missing those, then it will think everyone tracked went offline. */
				break;
			}

			/* If the ISON records were not requested by the user, then
			 treat the results as user tracking information. */
			NSString *onlineNicknamesString = m.sequence;

			NSArray *onlineNicknames = [onlineNicknamesString componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];

			/* Start going over the list of tracked nicknames */
			NSDictionary *trackedUsers = self.trackedUsers.trackedUsers;

			[trackedUsers enumerateKeysAndObjectsUsingBlock:^(NSString *trackedUser, NSNumber *trackingStatusInt, BOOL *stop) {
				IRCAddressBookUserTrackingStatus trackingStatus =
				IRCAddressBookUserTrackingStatusUnknown;

				/* Was the user on during the last check? */
				BOOL ison = trackingStatusInt.boolValue;

				if (ison) {
					/* If the user was on before, but is not in the list of ISON
					 users in this reply, then they are considered gone. Log that. */
					if ([onlineNicknames containsObjectIgnoringCase:trackedUser] == NO) {
						if (self.invokingISONCommandForFirstTime == NO) {
							trackingStatus = IRCAddressBookUserTrackingStatusSignedOff;
						}
					}
				} else {
					/* If they were not on but now are, then log that too. */
					if ([onlineNicknames containsObjectIgnoringCase:trackedUser]) {
						if (self.invokingISONCommandForFirstTime) {
							trackingStatus = IRCAddressBookUserTrackingStatusAvailable;
						} else {
							trackingStatus = IRCAddressBookUserTrackingStatusSignedOn;
						}
					}
				}

				/* If something changed (non-nil localization string), then scan 
				 the list of address book entries to report the result. */
				if (trackingStatus != IRCAddressBookUserTrackingStatusUnknown) {
					[self statusOfTrackedNickname:trackedUser changedTo:trackingStatus notify:YES];
				}
			}]; // for

			if (self.invokingISONCommandForFirstTime) { // Reset internal property
				self.invokingISONCommandForFirstTime = NO;
			}

			/* Update private messages */
			for (IRCChannel *channel in self.channelList) {
				if (channel.privateMessage == NO) {
					continue;
				}

				if (channel.isActive) {
					/* If the user is no longer on, deactivate the private message */
					if ([onlineNicknames containsObjectIgnoringCase:channel.name] == NO) {
						[channel deactivate];

						[mainWindow() reloadTreeItem:channel];
					}
				} else {
					/* Activate the private message if the user is back online */
					if ([onlineNicknames containsObjectIgnoringCase:channel.name]) {
						[channel activate];

						[mainWindow() reloadTreeItem:channel];
					}
				}
			}

			break;
		}
		case RPL_WHOREPLY:
		{
			NSAssertReturn([m paramsCount] > 6);

			/* Present reply to the user if we have destination */
			if (self.requestedCommands.visibleWhoRequest) {
				if (printMessage) {
					[self printReplyToHiddenCommandResponsesQuery:m];
				}

				/* We could remove this and be fine, but it's a lot
				 over overhead to process WHO responses so let's just
				 wait until the next automated one. */
				break;
			}

			/* Process reply */
			NSString *channelName = [m paramAt:1];

			IRCChannel *channel = [self findChannel:channelName];

			if (channel == nil) {
				break;
			}

			/* Example incoming data:
				<channel> <user> <host> <server> <nick> <H|G>[*][@|+] <hopcount> <real name>

				#freenode znc unaffiliated/namikaze kornbluth.freenode.net Namikaze G 0 Christian
				#freenode ~D unaffiliated/solprefixer kornbluth.freenode.net solprefixer H 0 solprefixer
			*/

			NSString *nickname = [m paramAt:5];
			NSString *username = [m paramAt:2];
			NSString *address = [m paramAt:3];
			NSString *flags = [m paramAt:6];
			NSString *realName = [m paramAt:7];

			BOOL isAway = NO;
			BOOL isIRCop = NO;

			// Field Syntax: <H|G>[*][@|+]
			// Strip G or H (away status).
			NSMutableString *userModes = [NSMutableString string];

			for (NSUInteger i = 0; i < flags.length; i++) {
				NSString *character = [flags stringCharacterAtIndex:i];

				if ([character isEqualToString:@"G"]) {
					isAway = self.monitorAwayStatus;

					continue;
				} else if ([character isEqualToString:@"*"]) {
					isIRCop = YES;

					continue;
				}

				NSString *modeSymbol = [self.supportInfo modeSymbolForUserPrefix:character];

				if (modeSymbol == nil) {
					continue;
				}

				[userModes appendString:modeSymbol];
			}

			/* Parameter 7 includes the hop count and real name because it begins with a :
			 Therefore, we cut after the first space to get the real, real name value. */
			NSInteger realNameFirstSpace = [realName stringPosition:@" "];

			if (realNameFirstSpace > 0 && realNameFirstSpace < realName.length) {
				realName = [realName substringAfterIndex:realNameFirstSpace];
			}

			/* Find global user and create mutable copy */
			IRCUser *user = [self findUser:nickname];

			IRCUserMutable *userMutable = nil;

			if (user == nil) {
				userMutable = [[IRCUserMutable alloc] initWithNickname:nickname onClient:self];
			} else {
				userMutable = [user mutableCopy];
			}

			userMutable.nickname = nickname;
			userMutable.username = username;
			userMutable.address = address;

			userMutable.isAway = isAway;
			userMutable.isIRCop = isIRCop;

			userMutable.realName = realName;

			/* Insert the user into the client and return the final copy that was */
			BOOL userChanged = (user != nil && [user isEqual:userMutable] == NO);

			IRCUser *userAdded = nil;

			if (user == nil || userChanged) {
				userAdded = [self addUserAndReturn:userMutable];
			} else {
				userAdded = user;
			}

			/* Find the user associated with this channel  */
			IRCChannelUser *member = [user userAssociatedWithChannel:channel];

			if (member == nil)
			{
				IRCChannelUserMutable *memberMutable = [[IRCChannelUserMutable alloc] initWithUser:userAdded];

				memberMutable.modes = userModes;

				[channel addMember:memberMutable];
			}
			else if (userChanged)
			{
				/* Determine whether the users were modified in such a way that
				 they require their cell in the user list be resorted. */
				/* We do not want to resort unless absolutely necessary because
				 sorting a channel with a few hundred users has overhead. */
				BOOL IRCopStatusChanged = (user.isIRCop != userAdded.isIRCop);
				
				BOOL resortMember = IRCopStatusChanged;

				BOOL replaceInAllChannels = (IRCopStatusChanged && [TPCPreferences memberListSortFavorsServerStaff]);

				if (resortMember) {
					[channel replaceMember:member
								withMember:member
									resort:resortMember
					  replaceInAllChannels:replaceInAllChannels];
				}
				else if (user.isAway != userAdded.isAway)
				{
					[mainWindow() updateDrawingForUserInUserList:userAdded];
				}
			}

			/* Update local cache of our hostmask */
			if ([self nicknameIsMyself:nickname]) {
				NSString *hostmask = [NSString stringWithFormat:@"%@!%@@%@", nickname, username, address];

				self.userHostmask = hostmask;
			}

			break;
		}
		case RPL_ENDOFWHO:
		{
			BOOL visibleWhoRequest = self.requestedCommands.visibleWhoRequest;

			[self.requestedCommands recordWhoRequestClosed];

			if (visibleWhoRequest && printMessage) {
				[self printReplyToHiddenCommandResponsesQuery:m];
			}

			break;
		}
		case RPL_NAMEREPLY:
		{
			NSAssertReturn([m paramsCount] > 3);

			/* Present reply to the user if we have destination */
			if (printMessage) {
				[self printReplyToHiddenCommandResponsesQuery:m];
			}

			/* Process reply */
			NSString *channelName = [m paramAt:2];

			IRCChannel *channel = [self findChannel:channelName];

			if (channel == nil || channel.channelNamesReceived) {
				break;
			}

			NSString *nicknamesString = [m paramAt:3];

			NSArray *nicknames = [nicknamesString componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];

			for (NSString *nickname in nicknames) {
				if (nickname.length == 0) {
					continue;
				}

				/* Find first character that is not a user mode */
				NSMutableString *memberModes = [NSMutableString string];

				NSUInteger characterIndex = 0;

				for (characterIndex = 0; characterIndex < nickname.length; characterIndex++) {
					NSString *prefix = [nickname stringCharacterAtIndex:characterIndex];

					NSString *modeSymbol = [self.supportInfo modeSymbolForUserPrefix:prefix];

					if (modeSymbol == nil) {
						break;
					}

					[memberModes appendString:modeSymbol];
				} // for

				/* Split away hostmask if available */
				NSString *newNickname = [nickname substringFromIndex:characterIndex];

				NSString *nicknameInt = nil;
				NSString *usernameInt = nil;
				NSString *addressInt = nil;

				if ([newNickname hostmaskComponents:&nicknameInt username:&usernameInt address:&addressInt onClient:self] == NO) {
					/* When NAMES reply is not a host, then set the nicknameInt
					 to the value of nickname and leave the rest as nil. */

					nicknameInt = newNickname;
				}

				/* Find global user */
				/* An instance of IRCUser may already exist from a NAMES
				 reply for another channel. If one already exist, then
				 we don't make an effort to change it's credentials. */
				IRCUser *userAdded = nil;

				IRCUser *user = [self findUser:nicknameInt];

				if (user == nil) {
					IRCUserMutable *userMutable = [[IRCUserMutable alloc] initWithNickname:nicknameInt onClient:self];

					userMutable.nickname = nicknameInt;
					userMutable.username = usernameInt;
					userMutable.address = addressInt;

					userAdded = [self addUserAndReturn:userMutable];
				} else {
					userAdded = user;
				}

				/* Find channel user */
				IRCChannelUser *member = [userAdded userAssociatedWithChannel:channel];

				IRCChannelUserMutable *memberMutable = nil;

				if (member == nil) {
					memberMutable = [[IRCChannelUserMutable alloc] initWithUser:userAdded];
				} else if ([self nicknameIsMyself:nicknameInt]) {
					memberMutable = [member mutableCopy];
				} else {
					/* If a user with this name already exists in the channel,
					 then we do not continue unless its us. We are added to the
					 channel when the JOIN is received, but we still need modes. */

					return;
				}

				/* Create channel user */
				memberMutable.modes = memberModes;

				/* Add user to channel */
				[channel addMember:memberMutable checkForDuplicates:YES];
			} // for

			break;
		}
		case RPL_ENDOFNAMES:
		{
			NSAssertReturn([m paramsCount] == 3);

			/* Present reply to the user if we have destination */
			if (printMessage) {
				[self printReplyToHiddenCommandResponsesQuery:m];
			}

			/* Process reply */
			NSString *channelName = [m paramAt:1];

			IRCChannel *channel = [self findChannel:channelName];

			if (channel == nil || channel.channelNamesReceived) {
				break;
			}

			channel.channelNamesReceived = YES;

			/* We have to wait until names are processed before populating
			 defaults for a channel so that we are certain there is actually
			 only one user, which is us. */
			if (channel.numberOfMembers == 1 && self.isBrokenIRCd_aka_Twitch == NO) {
				NSString *defaultModes = channel.config.defaultModes;

				if (defaultModes.length > 0) {
					[self sendModes:defaultModes withParametersString:nil inChannel:channel];
				}

				NSString *defaultTopic = channel.config.defaultTopic;

				if (defaultTopic.length > 0) {
					[self sendTopicTo:defaultTopic inChannel:channel];
				}
			}

			/* Update user count in title */
			[mainWindow() updateTitleFor:channel];

			break;
		}
		case RPL_LISTSTART:
		{
			TDCServerChannelListDialog *channelListDialog = [self channelListDialog];

			if (channelListDialog) {
				channelListDialog.contentAlreadyReceived = NO;

				[channelListDialog clear];
			}

			break;
		}
		case RPL_LIST:
		{
			NSAssertReturn([m paramsCount] > 2);

			NSString *channel = [m paramAt:1];
			NSString *userCount = [m paramAt:2];
			NSString *topic = [m sequence:3];

			if ([channel isEqualToString:@"*"]) {
				break;
			}

			TDCServerChannelListDialog *channelListDialog = [self channelListDialog];

			if (channelListDialog) {
				[channelListDialog addChannel:channel count:userCount.integerValue topic:topic];
			}

			break;
		}
		case RPL_LISTEND:
		{
			TDCServerChannelListDialog *channelListDialog = [self channelListDialog];

			if (channelListDialog) {
				channelListDialog.contentAlreadyReceived = YES;
			}

			break;
		}
		case RPL_BANLIST:
		case RPL_INVITELIST:
		case RPL_EXCEPTLIST:
		case RPL_QUIETLIST:
		{
			NSAssertReturn([m paramsCount] > 2);

			NSUInteger paramsOffset = 0;

			/* Quiet list has an extra argument which is the string "q" */
			if (numeric == RPL_QUIETLIST && m.paramsCount == 6)	{
				paramsOffset = 1;
			}

			NSString *channelName = [m paramAt:1];

			NSString *entryMask = [m paramAt:(2 + paramsOffset)];

			NSString *entryAuthor = nil;

			NSDate *entryCreationDate = nil;

			BOOL extendedLine = (m.paramsCount > (4 + paramsOffset));

			if (extendedLine) {
				entryAuthor = [m paramAt:(3 + paramsOffset)].nicknameFromHostmask;

				entryCreationDate = [NSDate dateWithTimeIntervalSince1970:[m paramAt:(4 + paramsOffset)].doubleValue];
			}

			TDCChannelBanListSheet *listSheet = [windowController() windowFromWindowList:@"TDCChannelBanListSheet"];

			if (listSheet) {
				if (listSheet.contentAlreadyReceived) {
					listSheet.contentAlreadyReceived = NO;

					[listSheet clear];
				}

				[listSheet addEntry:entryMask setBy:entryAuthor creationDate:entryCreationDate];

				return;
			}

			if (printMessage == NO) {
				return;
			}

			NSString *localization = nil;

			if (numeric == RPL_BANLIST) {
				localization = @"c04-d0";
			} else if (numeric == RPL_INVITELIST) {
				localization = @"py2-qh";
			} else if (numeric == RPL_EXCEPTLIST) {
				localization = @"ov2-ci";
			} else if (numeric == RPL_QUIETLIST) {
				localization = @"u5z-az";
			}

			if (extendedLine) {
				localization = [NSString stringWithFormat:@"IRC[%@-1]", localization];
			} else {
				localization = [NSString stringWithFormat:@"IRC[%@-2]", localization];
			}

			NSString *message = nil;

			if (extendedLine) {
				message = TXTLS(localization, channelName, entryMask, entryAuthor, entryCreationDate);
			} else {
				message = TXTLS(localization, channelName, entryMask);
			}

			[self print:message
					 by:nil
			  inChannel:nil
				 asType:TVCLogLineTypeDebug
				command:m.command
			 receivedAt:m.receivedAt];

			break;
		}
		case RPL_ENDOFBANLIST:
		case RPL_ENDOFINVITELIST:
		case RPL_ENDOFEXCEPTLIST:
		case RPL_ENDOFQUIETLIST:
		{
			TDCChannelBanListSheet *listSheet = [windowController() windowFromWindowList:@"TDCChannelBanListSheet"];

			if (listSheet) {
				listSheet.contentAlreadyReceived = YES;

				break;
			}

			if (printMessage) {
				[self printReply:m];
			}

			break;
		}
		case RPL_YOUREOPER:
		{
			if (self.userIsIRCop == NO) {
				self.userIsIRCop = YES;
			} else {
				break;
			}

			if (printMessage) {
				[self print:TXTLS(@"IRC[6bh-br]", m.senderNickname)
						 by:nil
				  inChannel:nil
					 asType:TVCLogLineTypeDebug
					command:m.command
				 receivedAt:m.receivedAt];
			}

			break;
		}
		case RPL_CHANNEL_URL:
		{
			NSAssertReturn([m paramsCount] == 3);

			NSAssertReturn(printMessage);

			NSString *channelName = [m paramAt:1];
			NSString *website = [m paramAt:2];

			IRCChannel *channel = [self findChannel:channelName];

			if (channel == nil) {
				return;
			}

			[self print:TXTLS(@"IRC[8tq-g6]", website)
					 by:nil
			  inChannel:channel
				 asType:TVCLogLineTypeWebsite
				command:m.command
			 receivedAt:m.receivedAt];

			break;
		}
		case RPL_WATCHSTAT:
		case RPL_WATCHLIST:
		{
			if (printMessage) {
				[self printReplyToHiddenCommandResponsesQuery:m];
			}

			break;
		}
		case RPL_REAWAY:
		case RPL_GONEAWAY:
		case RPL_NOTAWAY:
		{
			NSAssertReturn([m paramsCount] > 4);

			/* Present reply to the user if we have destination */
			if (printMessage) {
				[self printReplyToHiddenCommandResponsesQuery:m];
			}

			/* Process reply */
			NSString *nickname = [m paramAt:1];

			IRCAddressBookEntry *addressBookEntry =	[self findUserTrackingAddressBookEntryForNickname:nickname];

			if (addressBookEntry == nil) {
				break;
			}

			switch (numeric) {
				case RPL_REAWAY:
				case RPL_GONEAWAY: // is away
				{
					[self modifyUserWithNickname:nickname asAway:YES];

					break;
				}
				case RPL_NOTAWAY: // is no longer away
				{
					[self modifyUserWithNickname:nickname asAway:NO];

					break;
				}
				default:
				{
					break;
				}
			} // switch()

			break;
		}
		case RPL_LOGON:
		case RPL_LOGOFF:
		{
			NSAssertReturn([m paramsCount] > 4);

			/* Present reply to the user if we have destination */
			if (printMessage) {
				[self printReplyToHiddenCommandResponsesQuery:m];
			}

			 /* Process reply */
			NSString *nickname = [m paramAt:1];

			IRCAddressBookEntry *addressBookEntry =	[self findUserTrackingAddressBookEntryForNickname:nickname];

			if (addressBookEntry == nil) {
				break;
			}

			switch (numeric) {
				case RPL_LOGON: // logged online
				{
					[self statusOfTrackedNickname:nickname changedTo:IRCAddressBookUserTrackingStatusSignedOn notify:YES];

					break;
				}
				case RPL_LOGOFF: // logged offline
				{
					[self statusOfTrackedNickname:nickname changedTo:IRCAddressBookUserTrackingStatusSignedOff notify:YES];

					break;
				}
				default:
				{
					break;
				}
			} // switch()

			break;
		}
		case RPL_NOWON:
		case RPL_NOWOFF:
		{
			NSAssertReturn([m paramsCount] > 4);

			/* Present reply to the user if we have destination */
			if (printMessage) {
				[self printReplyToHiddenCommandResponsesQuery:m];
			}

			/* Process reply */
			NSString *nickname = [m paramAt:1];

			IRCAddressBookEntry *addressBookEntry =	[self findUserTrackingAddressBookEntryForNickname:nickname];

			if (addressBookEntry == nil) {
				break;
			}

			switch (numeric) {
				case RPL_NOWON: // is online
				{
					[self statusOfTrackedNickname:nickname changedTo:IRCAddressBookUserTrackingStatusAvailable notify:NO];
					
					break;
				}
				case RPL_NOWOFF: // is offline
				{
					[self statusOfTrackedNickname:nickname changedTo:IRCAddressBookUserTrackingStatusNotAvailable notify:NO];
					
					break;
				}
				default:
				{
					break;
				}
			} // switch()

			break;
		}
		case RPL_WATCHOFF:
		{
			if (printMessage) {
				[self printReplyToHiddenCommandResponsesQuery:m];
			}

			break;
		}
		case ERR_TOOMANYWATCH:
		{
			/* This message is always printed because Textual does not
			 make an effort to check the maximum allowance for this
			 command. We therefore want a user to know why tracking
			 breaks in Textual instead of blaming it on a bug. */
			if (printMessage) {
				[self printErrorReply:m];
			}

			break;
		}
//		case RPL_CLEARWATCH: /* Not implemented by any IRCd */
		case RPL_ENDOFWATCHLIST:
		{
			if (printMessage) {
				[self printReplyToHiddenCommandResponsesQuery:m];
			}

			break;
		}
		case RPL_MONLIST:
		{
			if (printMessage) {
				[self printReplyToHiddenCommandResponsesQuery:m];
			}

			break;
		}
		case RPL_MONONLINE:
		case RPL_MONOFFLINE:
		{
			NSAssertReturn([m paramsCount] == 2);

			/* Present reply to the user if we have destination */
			if (printMessage) {
				[self printReplyToHiddenCommandResponsesQuery:m];
			}

			/* Process reply */
			NSString *changedUsersString = [m paramAt:1];

			NSArray *changedUsers = [changedUsersString componentsSeparatedByString:@","];

			for (NSString *changedUser in changedUsers) {
				NSString *nickname = nil;

				if ([changedUser hostmaskComponents:&nickname username:NULL address:NULL onClient:self] == NO) {
					nickname = changedUser;
				}

				IRCAddressBookEntry *addressBookEntry =	[self findUserTrackingAddressBookEntryForNickname:nickname];

				if (addressBookEntry == nil) {
					continue;
				}

				if (numeric == RPL_MONONLINE) { // logged online
					[self statusOfTrackedNickname:nickname changedTo:IRCAddressBookUserTrackingStatusSignedOn notify:YES];
				} else {
					[self statusOfTrackedNickname:nickname changedTo:IRCAddressBookUserTrackingStatusSignedOff notify:YES];
				}
			}

			break;
		}
		case ERR_MONLISTFULL:
		{
			/* See ERR_TOOMANYWATCH for reason we always print this. */
			if (printMessage) {
				[self printErrorReply:m];
			}

			break;
		}
		case RPL_ENDOFMONLIST:
		{
			if (printMessage) {
				[self printReplyToHiddenCommandResponsesQuery:m];
			}

			break;
		}
		case RPL_TARGUMODEG:
		{
			// Ignore. 717 will take care of notification.

			break;
		}
		case RPL_TARGNOTIFY:
		{
			NSAssertReturn([m paramsCount] == 3);

			NSAssertReturn(printMessage);

			NSString *nickname = [m paramAt:1];

			[self printDebugInformation:TXTLS(@"IRC[11i-ev]", nickname)];

			break;
		}
		case RPL_UMODEGMSG:
		{
			NSAssertReturn([m paramsCount] == 4);

			NSAssertReturn(printMessage);

			NSString *nickname = [m paramAt:1];
			NSString *hostmask = [m paramAt:2];

			NSString *message = TXTLS(@"IRC[3yj-in]", nickname, hostmask);

			IRCChannel *channel = nil;

			if ([TPCPreferences locationToSendNotices] == TXNoticeSendLocationSelectedChannel) {
				channel = [mainWindow() selectedChannelOn:self];
			}

			if (channel) {
				[self printDebugInformation:message inChannel:channel];
			} else {
				[self printDebugInformationToConsole:message];
			}

			break;
		}
		case RPL_LOGGEDIN:
		{
			NSAssertReturn([m paramsCount] == 4);

			[self enableCapability:ClientIRCv3SupportedCapabilityIsIdentifiedWithSASL];

			if (printMessage) {
				[self print:[m sequence:3]
						 by:nil
				  inChannel:nil
					 asType:TVCLogLineTypeDebug
					command:m.command
				 receivedAt:m.receivedAt];
			}

			break;
		}
		case RPL_LOGGEDOUT:
		{
			NSAssertReturn([m paramsCount] == 3);

			[self resetSASLNegotiation];

			if (printMessage) {
				[self print:[m sequence:2]
						 by:nil
				  inChannel:nil
					 asType:TVCLogLineTypeDebug
					command:m.command
				 receivedAt:m.receivedAt];
			}

			break;
		}
		case RPL_SASLSUCCESS:
		case ERR_NICKLOCKED:
		case ERR_SASLFAIL:
		case ERR_SASLTOOLONG:
		case ERR_SASLABORTED:
		case ERR_SASLALREADY:
		case RPL_SASLMECHS: /* Treated as error */
		{
			if (printMessage) {
				if (numeric == RPL_SASLSUCCESS) { // success
					[self printReply:m];
				} else {
					[self printErrorReply:m];
				}
			}

			if ([self isPendingCapabilityEnabled:ClientIRCv3SupportedCapabilityIsInSASLNegotiation]) {
				[self disablePendingCapability:ClientIRCv3SupportedCapabilityIsInSASLNegotiation];

				[self resumeCapabilityNegotiation];
			}

			break;
		}
		default:
		{
			/* We will handle custom WHOIS responses here because there
			 are so many that it is impossible to cover them all above. */
			/* For those that we don't handle, give a plugin a chance first. */
			NSString *numericString = [NSString stringWithUnsignedInteger:numeric];

			if ([sharedPluginManager().supportedServerInputCommands containsObject:numericString]) {
				break;
			}

			if (printMessage) {
				/* Output custom WHOIS response to proper target */
				if (self.inWhoisResponse && m.paramsCount > 2) {
					[self printUnknownReply:m inChannel:[mainWindow() selectedChannelOn:self]];

					break;
				}

				/* Output unknown result */
				[self printUnknownReply:m];
			}

			break;
		}
	} // switch()
}

- (void)receiveErrorNumericReply:(IRCMessage *)m
{
	NSParameterAssert(m != nil);

	NSUInteger numeric = m.commandNumeric;

	BOOL printMessage = [self postReceivedMessage:m];

	switch (numeric) {
		case ERR_NOSUCHNICK:
		{
			NSAssertReturn(printMessage);

			NSString *channelName = [m paramAt:1];

			IRCChannel *channel = [self findChannel:channelName];

			if (channel) {
				[self printErrorReply:m inChannel:channel withSequence:2];
			} else {
				[self printErrorReply:m];
			}

			break;
		}
		case ERR_NOSUCHSERVER:
		case ERR_NOSUCHCHANNEL:
		{
			if (printMessage) {
				[self printErrorReply:m];
			}

			break;
		}
		case ERR_NICKNAMEINUSE:
		case ERR_ERRONEUSNICKNAME:
		{
			if (self.isLoggedIn) {
				if (printMessage) {
					[self printErrorReply:m];
				}

				break;
			}

			[self receiveNicknameCollisionError:m];

			break;
		}
		case ERR_UNAVAILRESOURCE:
		{
			NSString *target = [m paramAt:1];

			if (self.isLoggedIn || [self stringIsNickname:target] == NO) {
				if (printMessage) {
					[self printErrorReply:m];
				}

				break;
			}

			[self receiveNicknameCollisionError:m];

			break;
		}
		case ERR_CANNOTSENDTOCHAN:
		{
			NSAssertReturn(printMessage);

			NSString *channelName = [m paramAt:1];

			IRCChannel *channel = [self findChannel:channelName];

			if (channel) {
				[self printErrorReply:m inChannel:channel withSequence:2];
			} else {
				[self printErrorReply:m];
			}

			break;
		}
		case ERR_ADMONLY:
		case ERR_BADCHANMASK:
		case ERR_BADCHANNAME:
		case ERR_BADCHANNEL:
		case ERR_BADCHANNELKEY:
		case ERR_BANNEDFROMCHAN:
		case ERR_CHANNELISFULL:
		case ERR_DELAYREJOIN:
		case ERR_FORBIDDENCHANNEL:
		case ERR_INVITEONLYCHAN:
		case ERR_LINKCHANNEL:
		case ERR_NEEDREGGEDNICK:
		case ERR_NOHIDING:
		case ERR_OPERONLY:
		case ERR_OPERSPVERIFY:
		case ERR_SECUREONLYCHAN:
		case ERR_THROTTLE:
		case ERR_TOOMANYCHANNELS:
		case ERR_TOOMANYJOINS:
		{
			NSString *channelName = [m paramAt:1];

			IRCChannel *channel = [self findChannel:channelName];

			if (channel) {
				channel.errorOnLastJoinAttempt = YES;

				/* In addition to the console, print join errors in
				 the channel itself because user might check there. */
				if (printMessage) {
					[self printErrorReply:m inChannel:channel withSequence:2];
				}
			}

			/* Print to console */
			if (printMessage) {
				[self printErrorReply:m];
			}

			break;
		}
		case ERR_WHOSYNTAX:
		case ERR_WHOLIMEXCEED:
		{
			[self.requestedCommands recordWhoRequestClosed];

			if (printMessage) {
				[self printErrorReply:m];
			}

			break;
		}
		case ERR_DISABLED:
		case ERR_UNKNOWNCOMMAND:
		case ERR_NEEDMOREPARAMS:
		{
			NSString *command = [m paramAt:1];

			if ([command isEqualToString:@"ISON"]) {
				[self.requestedCommands recordIsonRequestClosed];
			} else if ([command isEqualToString:@"WHO"]) {
				[self.requestedCommands recordWhoRequestClosed];
			}

			if (printMessage) {
				[self printErrorReply:m];
			}

			break;
		}
		default:
		{
			if (printMessage) {
				[self printErrorReply:m];
			}

			break;
		}
	} // switch()
}

- (void)receiveNicknameCollisionError:(IRCMessage *)m
{
	NSParameterAssert(m != nil);

	if (self.isConnected == NO || self.isLoggedIn) {
		return;
	}

	NSArray *alternateNicknames = self.config.alternateNicknames;

	NSString *tryingNickname = self.tryingNicknameSentNickname;

	[self printDebugInformationToConsole:TXTLS(@"IRC[js3-9v]", tryingNickname)];

	NSUInteger tryingNicknameNumber = self.tryingNicknameNumber;

	if (alternateNicknames.count > tryingNicknameNumber) {
		NSString *nickname = alternateNicknames[tryingNicknameNumber];

		[self changeNickname:nickname];
	} else {
		[self tryAnotherNickname];
	}

	self.tryingNicknameNumber += 1;
}

- (void)tryAnotherNickname
{
	if (self.isConnected == NO || self.isLoggedIn) {
		return;
	}

	/* IRCISupportInfo would not be populated by now which means we cannot use a
	 server-specific maximum nickname length value at this point. */
	const NSUInteger maximumLength = IRCProtocolDefaultNicknameMaximumLength;

	NSString *tryingNickname = [self.tryingNicknameSentNickname padNicknameWithCharacter:'_' maximumLength:maximumLength];

	if (tryingNickname) {
		self.tryingNicknameSentNickname = tryingNickname;
	} else {
		self.tryingNicknameSentNickname = @"0";
	}

	[self changeNickname:self.tryingNicknameSentNickname];
}

#pragma mark -
#pragma mark Autojoin

- (void)startAutojoinTimer
{
	if (self.autojoinTimer.timerIsActive) {
		return;
	}

	NSTimeInterval interval = [TPCPreferences autojoinDelayAfterIdentification];

	if (CGFloatAreEqual(interval, 0.0)) {
		[self onAutojoinTimer];

		return;
	}

	[self.autojoinTimer start:interval onRepeat:NO];
}

- (void)stopAutojoinTimer
{
	if (self.autojoinTimer.timerIsActive == NO) {
		return;
	}

	[self.autojoinTimer stop];
}

- (void)onAutojoinTimer
{
	[self startAutojoinNextJoinTimer];
}

- (void)startAutojoinDelayedWarningTimer
{
	if (self.autojoinDelayedWarningTimer.timerIsActive) {
		return;
	}

	[self.autojoinDelayedWarningTimer start:_autojoinDelayedWarningInterval onRepeat:YES];
}

- (void)stopAutojoinDelayedWarningTimer
{
	if (self.autojoinDelayedWarningTimer.timerIsActive == NO) {
		return;
	}

	[self.autojoinDelayedWarningTimer stop];
}

- (void)onAutojoinDelayedWarningTimer
{
	if (self.isLoggedIn == NO ||
		self.config.hideAutojoinDelayedWarnings ||
		self.autojoinDelayedWarningCount >= _autojoinDelayedWarningMaxCount)
	{
		[self stopAutojoinDelayedWarningTimer];

		return;
	}

	self.autojoinDelayedWarningCount += 1;

	/* This message is posted to the server console and the 
	 front most channel if it is on this server. */
	NSString *text = TXTLS(@"IRC[r5h-fj]");

	[self printDebugInformationToConsole:text];

	IRCChannel *c = [mainWindow() selectedChannelOn:self];

	if (c != nil) {
		[self printDebugInformation:text inChannel:c];
	}
}

- (void)startAutojoinNextJoinTimer
{
	if (self.autojoinNextJoinTimer.timerIsActive) {
		return;
	}

	NSTimeInterval interval = [TPCPreferences autojoinDelayBetweenChannelJoins];

	[self.autojoinNextJoinTimer start:interval onRepeat:YES];

	/* Fake first event */
	[self onAutojoinNextJoinTimer];
}

- (void)stopAutojoinNextJoinTimer
{
	if (self.autojoinNextJoinTimer.timerIsActive == NO) {
		return;
	}

	[self.autojoinNextJoinTimer stop];

	self.channelsToAutojoin = nil;
}

- (void)onAutojoinNextJoinTimer
{
	[self autojoinNextChannel];
}

- (void)autojoinNextChannel
{
	if (self.isAutojoining == NO) {
		return;
	}

	@synchronized (self.channelsToAutojoin) {
		NSUInteger numberOfChannelsRemaining = self.channelsToAutojoin.count;

		NSUInteger maximumNumberOfJoins = [TPCPreferences autojoinMaximumChannelJoins];

		NSRange arrayRange;

		BOOL endOfArray = (numberOfChannelsRemaining <= maximumNumberOfJoins);

		if (endOfArray == NO) {
			arrayRange = NSMakeRange(0, maximumNumberOfJoins);
		} else {
			arrayRange = NSMakeRange(0, numberOfChannelsRemaining);
		}

		NSArray *channelsToJoin = [self.channelsToAutojoin subarrayWithRange:arrayRange];

		[self autojoinChannels:channelsToJoin];

		if (endOfArray == NO) {
			[self.channelsToAutojoin removeObjectsInRange:arrayRange];
		} else {
			self.isAutojoining = NO;

			self.isAutojoined = YES;

			[self stopAutojoinNextJoinTimer];
		}
	}
}

- (void)autojoinChannels:(NSArray<IRCChannel *> *)channels
{
	NSParameterAssert(channels != nil);

	[self joinChannels:channels];
}

- (void)performAutoJoin
{
	[self performAutoJoinInitiatedByUser:NO];
}

- (void)performAutoJoinInitiatedByUser:(BOOL)initiatedByUser
{
	if (self.isAutojoining) {
		return;
	}

	[self stopAutojoinDelayedWarningTimer];

	if (initiatedByUser == NO) {
		/* Ignore previous invocations of this method */
		if (self.isAutojoined) {
			return;
		}

		/* Ignore autojoin based on ZNC preferences */
		if (self.isConnectedToZNC && self.config.zncIgnoreConfiguredAutojoin) {
			self.isAutojoined = YES;

			return;
		}

		/* Do nothing unless certain conditions are met */
		if ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityIsIdentifiedWithSASL] == NO) {
			if (self.config.autojoinWaitsForNickServ) {
				if (self.serverHasNickServ && self.userIsIdentifiedWithNickServ == NO) {
					return;
				}
			}
		}
	}

	NSMutableArray<IRCChannel *> *channelsToAutojoin = [NSMutableArray array];

	for (IRCChannel *c in self.channelList) {
		if (c.isChannel && c.isActive == NO) {
			if (c.config.autoJoin) {
				[channelsToAutojoin addObject:c];
			}
		}
	}

	if (channelsToAutojoin.count == 0) {
		self.isAutojoining = YES;

		return;
	}

	self.isAutojoining = YES;

	@synchronized (self.channelsToAutojoin) {
		[channelsToAutojoin shuffle];

		self.channelsToAutojoin = channelsToAutojoin;
	}

	[self startAutojoinTimer];
}

#pragma mark -
#pragma mark Post Events

- (void)postEventToViewController:(NSString *)eventToken
{
	if (themeSettings().js_postHandleEventNotifications == NO) {
		return; // Cancel operation...
	}

	[self postEventToViewController:eventToken forItem:self];

	for (IRCChannel *channel in self.channelList) {
		[self postEventToViewController:eventToken forItem:channel];
	}
}

- (void)postEventToViewController:(NSString *)eventToken forChannel:(IRCChannel *)channel
{
	if (themeSettings().js_postHandleEventNotifications == NO) {
		return; // Cancel operation...
	}

	[self postEventToViewController:eventToken forItem:channel];
}

- (void)postEventToViewController:(NSString *)eventToken forItem:(IRCTreeItem *)item
{
	NSParameterAssert(eventToken != nil);
	NSParameterAssert(item != nil);

	if (self.isTerminating) {
		return;
	}

	[item.viewController evaluateFunction:@"Textual.handleEvent" withArguments:@[eventToken] onQueue:NO];
}

#pragma mark -
#pragma mark Timers

- (void)startPongTimer
{
	if (self.pongTimer.timerIsActive) {
		return;
	}

	[self.pongTimer start:_pongCheckInterval onRepeat:YES];
}

- (void)stopPongTimer
{
	if (self.pongTimer.timerIsActive == NO) {
		return;
	}

	[self.pongTimer stop];
}

- (void)onPongTimer
{
	if (self.isLoggedIn == NO) {
		[self stopPongTimer];

		return;
	}

	/* Instead of stopping and starting the timer every time this changes, it
	 it is easier to check if we should do it every timer iteration.
	 The ability to disable this is important on PSYBNC connection because
	 PSYBNC doesn't respond to PING commands. There are other irc daemons that
	 don't reply to PING either and they should all be shot. */
	NSTimeInterval timeSpent = [NSDate timeIntervalSinceNow:self.lastMessageReceived];

	if (timeSpent >= _timeoutInterval)
	{
		/* If EOF Received when we were not expecting it, then timeout regardless
		 of user preference once our timeout interval is reached. */
		if (self.socket.EOFReceived || self.config.performDisconnectOnPongTimer) {
			[self printDebugInformation:TXTLS(@"IRC[bps-la]", (timeSpent / 60.0)) inChannel:nil];

			XRPerformBlockSynchronouslyOnMainQueue(^{
				[self disconnect];
			});

			return;
		}

		if (self.timeoutWarningShownToUser == NO) {
			self.timeoutWarningShownToUser = YES;

			[self printDebugInformation:TXTLS(@"IRC[gzo-54]", (timeSpent / 60.0)) inChannel:nil];
		}
	}
	else if (timeSpent >= _pingInterval)
	{
		if (self.config.performPongTimer == NO) {
			return;
		}

		[self sendPing:self.serverAddress];
	}
}

- (void)startReconnectTimer
{
	if ((self.reconnectEnabledBecauseOfSleepMode		&& self.config.autoSleepModeDisconnect == NO) ||
		(self.reconnectEnabledBecauseOfSleepMode == NO  && self.config.autoReconnect == NO))
	{
		return;
	}

	if (self.reconnectTimer.timerIsActive) {
		return;
	}

	[self.reconnectTimer start:_reconnectInterval onRepeat:YES];
}

- (void)stopReconnectTimer
{
	if (self.reconnectTimer.timerIsActive == NO) {
		return;
	}

	[self.reconnectTimer stop];
}

- (void)onReconnectTimer
{
	if (self.isConnecting || self.isConnected) {
		return;
	}

	[self connect:IRCClientConnectModeReconnect];
}

- (void)startRetryTimer
{
	if (self.retryTimer.timerIsActive) {
		return;
	}

	[self.retryTimer start:_retryInterval];
}

- (void)stopRetryTimer
{
	if (self.retryTimer.timerIsActive == NO) {
		return;
	}

	[self.retryTimer stop];
}

- (void)onRetryTimer
{
	if (self.isConnected == NO) {
		return;
	}

	XRPerformBlockSynchronouslyOnMainQueue(^{
		__weak IRCClient *weakSelf = self;

		self.disconnectCallback = ^{
			[weakSelf connect:IRCClientConnectModeRetry];
		};

		[self disconnect];
	});
}

#pragma mark -
#pragma mark Requested Commands

- (void)removeRequestedCommands
{
	[self.requestedCommands removeCommands];
}

- (void)createHiddenCommandResponses
{
	if (self.isTerminating) {
		return;
	}

	if (self.rawDataLogQuery != nil) {
		return;
	}

	IRCChannel *query = [self findChannelOrCreate:@"Hidden Responses" isUtility:YES];

	self.hiddenCommandResponsesQuery = query;

	[mainWindow() select:query];

	[self printDebugInformation:TXTLS(@"IRC[yem-td]") inChannel:query];
}

- (void)printReplyToHiddenCommandResponsesQuery:(IRCMessage *)message
{
	IRCChannel *query = self.hiddenCommandResponsesQuery;

	if (query == nil) {
		return;
	}

	[self printReply:message inChannel:query];
}

#pragma mark -
#pragma mark Plugins and Scripts

- (void)outputDescriptionForError:(NSError *)error forTextualCmdScriptAtPath:(NSString *)path inputString:(NSString *)inputString
{
	NSString *filename = path.lastPathComponent;

	NSString *errorDescription = error.userInfo[NSAppleScriptErrorMessage];

	if (errorDescription == nil) {
		errorDescription = error.userInfo[NSAppleScriptErrorBriefMessage];
	}

	if (errorDescription == nil) {
		errorDescription = error.localizedFailureReason;
	}

	if (errorDescription == nil) {
		errorDescription = error.localizedDescription;
	}

	if (inputString.length == 0) {
		inputString = @"(no input)";
	}

	[self printDebugInformation:TXTLS(@"IRC[2mc-h0]", filename, inputString, errorDescription)];

	LogToConsoleError("%{public}@", TXTLS(@"IRC[ax0-mt]", errorDescription));
}

- (void)sendTextualCmdScriptResult:(NSString *)resultString toChannel:(nullable NSString *)channel
{
	NSParameterAssert(resultString != nil);

	IRCTreeItem *destination = nil;

	if (channel == nil) {
		destination = self;
	} else {
		destination = [self findChannel:channel];
	}

	if (destination == nil) {
		LogToConsoleFault("A script returned a result but its destination no longer exists");

		return;
	}

	resultString = resultString.trim;

	XRPerformBlockAsynchronouslyOnMainQueue(^{
		[self inputText:resultString destination:destination];
	});
}

- (void)executeTextualCmdScriptInContext:(NSDictionary<NSString *, NSString *> *)context
{
	XRPerformBlockAsynchronouslyOnQueue([THOPluginDispatcher dispatchQueue], ^{
		[self _executeTextualCmdScriptInContext:context];
	});
}

- (void)_executeTextualCmdScriptInContext:(NSDictionary<NSString *, NSString *> *)context
{
	NSParameterAssert(context != nil);

	NSString *inputString = context[@"inputString"];

	NSString *path = context[@"path"];

	NSString *targetChannel = context[@"targetChannel"];

	NSParameterAssert(path != nil);

	NSURL *pathURL = [NSURL fileURLWithPath:path];

	/* Is it AppleScript? */
	if ([path hasSuffix:TPCResourceManagerScriptDocumentTypeExtension]) {
		BOOL isBuiltinScript = [path hasPrefix:[TPCPathInfo applicationResources]];

		/* /////////////////////////////////////////////////////// */
		/* Event Descriptor */
		/* /////////////////////////////////////////////////////// */

		NSAppleEventDescriptor *firstParameter = [NSAppleEventDescriptor descriptorWithString:inputString];
		NSAppleEventDescriptor *secondParameter = [NSAppleEventDescriptor descriptorWithString:targetChannel];

		NSAppleEventDescriptor *parameters = [NSAppleEventDescriptor listDescriptor];

		[parameters insertDescriptor:firstParameter atIndex:1];
		[parameters insertDescriptor:secondParameter atIndex:2];

		ProcessSerialNumber process = { 0, kCurrentProcess };

		NSAppleEventDescriptor *target = [NSAppleEventDescriptor descriptorWithDescriptorType:typeProcessSerialNumber
																						bytes:&process
																					   length:sizeof(ProcessSerialNumber)];

		NSAppleEventDescriptor *handler = [NSAppleEventDescriptor descriptorWithString:@"textualcmd"];

		NSAppleEventDescriptor *event = [NSAppleEventDescriptor appleEventWithEventClass:kASAppleScriptSuite
																				 eventID:kASSubroutineEvent
																		targetDescriptor:target
																				returnID:kAutoGenerateReturnID
																		   transactionID:kAnyTransactionID];

		[event setParamDescriptor:handler forKeyword:keyASSubroutineName];

		[event setParamDescriptor:parameters forKeyword:keyDirectObject];

		/* /////////////////////////////////////////////////////// */
		/* Execute Event */
		/* /////////////////////////////////////////////////////// */
		/* NSUserAppleScriptTask expects the script to be in the Application Scripts folder 
		 which means if we want to execute scripts in the app's Resources folder, we use a
		 regular call to NSAppleScript. It's pretty safe to say that scripts we make ourselves
		 wont produce errors which means the logic for handling errors is ignored for scripts
		 that are performed in the Resources folder. */

		if (isBuiltinScript)
		{
			NSAppleScript *appleScript = [[NSAppleScript alloc] initWithContentsOfURL:pathURL error:NULL];

			if (appleScript == nil) {
				return;
			}

			NSAppleEventDescriptor *result = [appleScript executeAppleEvent:event error:NULL];

			if (result == nil) {
				return;
			}

			[self sendTextualCmdScriptResult:result.stringValue toChannel:targetChannel];
		}
		else // isBuiltinScript
		{
			NSError *appleScriptError = nil;

			NSUserAppleScriptTask *appleScript = [[NSUserAppleScriptTask alloc] initWithURL:pathURL error:&appleScriptError];

			if (appleScript == nil) {
				[self outputDescriptionForError:appleScriptError forTextualCmdScriptAtPath:path inputString:inputString];

				return;
			}

			[appleScript executeWithAppleEvent:event
							 completionHandler:^(NSAppleEventDescriptor *result, NSError *error)
			 {
				 if (result == nil) {
					 [self outputDescriptionForError:error forTextualCmdScriptAtPath:path inputString:inputString];
				 } else {
					 [self sendTextualCmdScriptResult:result.stringValue toChannel:targetChannel];
				 }
			 }];
		}

		return;
	}

	/* /////////////////////////////////////////////////////// */
	/* Execute Shell Script */
	/* /////////////////////////////////////////////////////// */

	/* Build list of arguments. */
	NSMutableArray *taskArguments = [NSMutableArray array];

	if (targetChannel) {
		[taskArguments addObject:targetChannel];
	} else {
		[taskArguments addObject:@""];
	}

	NSArray *inputStringComponents = [inputString componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];

	[taskArguments addObjectsFromArray:inputStringComponents];

	/* Create task object */
	NSError *taskError = nil;

	NSUserUnixTask *task = [[NSUserUnixTask alloc] initWithURL:pathURL error:&taskError];

	if (task == nil) {
		[self outputDescriptionForError:taskError forTextualCmdScriptAtPath:path inputString:inputString];

		return;
	}

	/* Prepare pipe */
	NSPipe *standardOutputPipe = [NSPipe pipe];

	NSFileHandle *readingPipe = standardOutputPipe.fileHandleForReading;
	NSFileHandle *writingPipe = standardOutputPipe.fileHandleForWriting;

	task.standardOutput = writingPipe;

	/* Try performing task */
	[task executeWithArguments:taskArguments completionHandler:^(NSError *error) {
		if (error) {
			[self outputDescriptionForError:error forTextualCmdScriptAtPath:path inputString:inputString];

			return;
		}

		NSData *result = [readingPipe readDataToEndOfFile];

		NSString *resultString = [NSString stringWithData:result encoding:NSUTF8StringEncoding];

		[self sendTextualCmdScriptResult:resultString toChannel:targetChannel];
	}];
}

- (void)processBundlesUserMessage:(NSString *)message command:(NSString *)command
{
	NSParameterAssert(message != nil);

	[THOPluginDispatcher userInputCommandInvokedOnClient:self commandString:command messageString:message];
}

- (void)processBundlesServerMessage:(IRCMessage *)message
{
	NSParameterAssert(message != nil);

	[THOPluginDispatcher didReceiveServerInput:message onClient:self];
}

- (BOOL)postReceivedMessage:(IRCMessage *)referenceMessage
{
	NSParameterAssert(referenceMessage != nil);

	return [self postReceivedMessage:referenceMessage
							withText:referenceMessage.sequence
						 destinedFor:nil];
}

- (BOOL)postReceivedMessage:(IRCMessage *)referenceMessage withText:(nullable NSString *)text destinedFor:(nullable IRCChannel *)textDestination
{
	NSParameterAssert(referenceMessage != nil);

	return [self postReceivedCommand:referenceMessage.command
							withText:text
						 destinedFor:textDestination
					referenceMessage:referenceMessage];
}

- (BOOL)postReceivedCommand:(NSString *)command withText:(nullable NSString *)text destinedFor:(nullable IRCChannel *)textDestination referenceMessage:(IRCMessage *)referenceMessage
{
	NSParameterAssert(command != nil);
	NSParameterAssert(referenceMessage != nil);

	return [THOPluginDispatcher receivedCommand:command
									   withText:text
									 authoredBy:referenceMessage.sender
									destinedFor:textDestination
									   onClient:self
									 receivedAt:referenceMessage.receivedAt
							   referenceMessage:referenceMessage];
}

#pragma mark -
#pragma mark Commands

- (void)connect
{
	[self connect:IRCClientConnectModeNormal];
}

- (void)connect:(IRCClientConnectMode)connectMode
{
	[self connect:connectMode bypassProxy:NO];
}

- (void)connect:(IRCClientConnectMode)connectMode bypassProxy:(BOOL)bypassProxy
{
	/* Do not allow a connect to occur until the current 
	 socket has completed disconnecting */
	if (self.isConnecting || self.isConnected || self.isQuitting || self.isDisconnecting) {
		return;
	}

	/* Check if system is sleeping. */
	if ([XRSystemInformation systemIsSleeping]) {
		LogToConsoleInfo("Refusing to connect because system is sleeping");

		return;
	}

	/* Do we have somewhere to connect to? */
	NSArray *servers = self.config.serverList;

	if (servers.count == 0) {
		[self printDebugInformationToConsole:TXTLS(@"IRC[iaa-0u]")];

		return;
	}

	/* Begin populating configuration */
	/* Temporary values take priority. When a temporary server
	 address is specified, then the temporary port is used too,
	 or 6667 without SSL is used. Nothing from the current
	 server configuration is read if there is temporary server. */
	NSString *serverAddress = nil;

	uint16_t serverPort = IRCConnectionDefaultServerPort;

	BOOL connectionPrefersSecuredConnection = NO;

	if (self.temporaryServerAddressOverride) {
		serverAddress = self.temporaryServerAddressOverride;

		if (self.temporaryServerPortOverride > 0 &&
			self.temporaryServerPortOverride <= TXMaximumTCPPort)
		{
			serverPort = self.temporaryServerPortOverride;
		}
	}

	if (serverAddress.isValidInternetAddress == NO) {
		NSUInteger serverIndex = self.lastServerSelected;

		if (serverIndex == NSNotFound) {
			serverIndex = 0;
		} else {
			serverIndex += 1;

			if (serverIndex >= servers.count) {
				serverIndex = 0;
			}
		}

		self.lastServerSelected = serverIndex;

		IRCServer *server = servers[serverIndex];

		serverAddress = server.serverAddress;
		serverPort = server.serverPort;

		connectionPrefersSecuredConnection = server.prefersSecuredConnection;

		self.server = server;
	}

	/* Do not wait for an actual connect before destroying the temporary
	 store. Once its defined, its to be nil'd out no matter what. */
	self.temporaryServerAddressOverride = nil;
	self.temporaryServerPortOverride = 0;

	/* Reset status */
	self.connectType = connectMode;

	self.disconnectType = IRCClientDisconnectModeNormal;

	self.isConnecting = YES;

	/* Disable reconnect attempt but permit more */
	[self stopReconnectTimer];

	self.reconnectEnabled = YES;

	/* Present status to user */
	[mainWindow() updateTitleFor:self];

	if (connectMode == IRCClientConnectModeReconnect) {
		[self printDebugInformationToConsole:TXTLS(@"IRC[xxb-y2]")];
	} else if (connectMode == IRCClientConnectModeRetry) {
		[self printDebugInformationToConsole:TXTLS(@"IRC[ky3-36]")];
	}

	/* To provide user with similar behavior, when migrating -connectionPrefersIPv4
	 in IRCClientConfig, we set the address type to IRCConnectionAddressTypeIPv4.
	 We check if both values are set to offer the user a warning that the preference
	 they had has changed in a way they may not want. When user changes the address
	 type in Server Properties, we unset -connectionPrefersIPv4 so that the warning
	 does not appear again, ever. */
	IRCConnectionAddressType addressType = self.config.addressType;

	if (self.config.showConnectionPrefersIPv4Warning) {
		[self printDebugInformation:TXTLS(@"IRC[w05-ph]")];
	}

	[self printDebugInformationToConsole:TXTLS(@"IRC[o77-ls]", serverAddress, serverPort)];

	[RZNotificationCenter() postNotificationName:IRCClientWillConnectNotification object:self];

	/* Create socket */
	IRCConnectionConfigMutable *socketConfig = [IRCConnectionConfigMutable new];

	socketConfig.addressType = addressType;

	socketConfig.serverAddress = serverAddress;
	socketConfig.serverPort = serverPort;

	IRCConnectionProxyType proxyType = self.config.proxyType;

	/* Network.framework cannot use a custom proxy so we can only either use none
	 or allow it to automatically configure the connection using the system proxy. */
	socketConfig.connectionPrefersModernSockets = ([TPCPreferences preferModernSockets] &&
												   (proxyType == IRCConnectionProxyTypeNone ||
													proxyType == IRCConnectionProxyTypeAutomatic));

	socketConfig.cipherSuites = self.config.cipherSuites;

	socketConfig.connectionPrefersSecuredConnection = connectionPrefersSecuredConnection;
	socketConfig.connectionShouldValidateCertificateChain = self.config.validateServerCertificateChain;

	socketConfig.identityClientSideCertificate = self.config.identityClientSideCertificate;

	if (bypassProxy == NO) {
		socketConfig.proxyType = proxyType;

		if (socketConfig.proxyType == IRCConnectionProxyTypeSocks4 ||
			socketConfig.proxyType == IRCConnectionProxyTypeSocks5 ||
			socketConfig.proxyType == IRCConnectionProxyTypeHTTP ||
			socketConfig.proxyType == IRCConnectionProxyTypeHTTPS)
		{
			socketConfig.proxyPort = self.config.proxyPort;
			socketConfig.proxyAddress = self.config.proxyAddress;
			socketConfig.proxyPassword = self.config.proxyPassword;
			socketConfig.proxyUsername = self.config.proxyUsername;
		}
	}

	socketConfig.floodControlDelayInterval = self.config.floodControlDelayTimerInterval;
	socketConfig.floodControlMaximumMessages = self.config.floodControlMaximumMessages;

	socketConfig.connectionPrefersModernCiphersOnly = [TPCPreferences preferModernCiphers];

	self.socket = [[IRCConnection alloc] initWithConfig:socketConfig onClient:self];

	[self.socket open];

	/* Pass status to view controller */
	[self postEventToViewController:@"serverConnecting"];
}

- (void)autoConnectWithDelay:(NSUInteger)delay afterWakeUp:(BOOL)afterWakeUp
{
	self.connectDelay = delay;

	if (afterWakeUp) {
		[self autoConnectAfterWakeUp];
	} else {
		[self autoConnect];
	}
}

- (void)autoConnect
{
	NSUInteger connectDelay = self.connectDelay;

	if (connectDelay == 0) {
		[self autoConnectPerformConnect];

		return;
	}

	[self performSelectorInCommonModes:@selector(autoConnectPerformConnect) withObject:nil afterDelay:connectDelay];
}

- (void)autoConnectPerformConnect
{
	if (self.isConnecting || self.isConnected) {
		return;
	}

	[self connect];
}

- (void)autoConnectAfterWakeUp
{
	NSUInteger connectDelay = self.connectDelay;

	if (connectDelay == 0) {
		[self autoConnectAfterWakeUpPerformConnect];

		return;
	}

	[self printDebugInformationToConsole:TXTLS(@"IRC[3s6-e6]", connectDelay)];

	[self performSelectorInCommonModes:@selector(autoConnectAfterWakeUpPerformConnect) withObject:nil afterDelay:connectDelay];
}

- (void)autoConnectAfterWakeUpPerformConnect
{
	if (self.isConnecting || self.isConnected) {
		return;
	}

	self.reconnectEnabledBecauseOfSleepMode = YES;

	[self connect:IRCClientConnectModeReconnect];
}

- (void)disconnect
{
	[self cancelPerformRequestsWithSelector:@selector(disconnect) object:nil];

	if (self.isConnecting == NO && self.isConnected == NO) {
		return;
	}

	if (self.socket == nil) {
		return;
	}

	self.isDisconnecting = YES;

	[RZNotificationCenter() postNotificationName:IRCClientWillDisconnectNotification object:self];

	[self.socket close];
}

- (void)quit
{
	NSString *comment = nil;

	if (self.disconnectType == IRCClientDisconnectModeComputerSleep) {
		comment = self.config.sleepModeLeavingComment;
	} else {
		comment = self.config.normalLeavingComment;
	}

	[self quitWithComment:comment];
}

- (void)quitWithComment:(NSString *)comment
{
	NSParameterAssert(comment != nil);

	if ((self.isConnecting == NO && self.isConnected == NO) || self.isQuitting || self.isDisconnecting) {
		return;
	}

	self.isQuitting	= YES;

	[self cancelReconnect];

	if (self.isTerminating == NO) {
		[self postEventToViewController:@"serverDisconnecting"];
	}

	[RZNotificationCenter() postNotificationName:IRCClientWillSendQuitNotification object:self];

	[self.socket clearSendQueue];

	/* If -isLoggedIn is NO, then the connection does not need 
	 to be closed gracefully because the user hasn't even joined
	 a channel yet, so who are we doing it for? */
	if (self.isLoggedIn == NO) {
		[self disconnect];

		return;
	}

	[self send:@"QUIT", comment, nil];

	/* We give it two seconds before forcefully breaking so that the graceful
	 quit with the quit message above can be performed. */
	[self performSelectorInCommonModes:@selector(disconnect) withObject:nil afterDelay:2.0];
}

- (void)cancelReconnect
{
	self.reconnectEnabled = NO;
	self.reconnectEnabledBecauseOfSleepMode = NO;

	[self stopReconnectTimer];

	[mainWindow() updateTitleFor:self];
}

- (void)changeNickname:(NSString *)newNickname
{
	NSParameterAssert(newNickname != nil);

	if (self.isConnected == NO) {
		return;
	}

	if (newNickname.length == 0) {
		return;
	}

	[self send:@"NICK", newNickname, nil];
}

- (void)joinKickedChannel:(IRCChannel *)channel
{
	[self joinChannel:channel];
}

- (void)joinChannel:(IRCChannel *)channel
{
	[self joinChannel:channel password:nil];
}

- (void)joinUnlistedChannel:(NSString *)channel
{
	[self joinUnlistedChannel:channel password:nil];
}

- (void)joinChannel:(IRCChannel *)channel password:(nullable NSString *)password
{
	NSParameterAssert(channel != nil);

	if (channel.isChannel == NO || channel.isActive) {
		return;
	}

	channel.status = IRCChannelStatusJoining;

	if (password == nil) {
		password = channel.secretKey;
	}

	[self forceJoinChannel:channel.name password:password];
}

- (void)joinUnlistedChannel:(NSString *)channel password:(nullable NSString *)password
{
	NSParameterAssert(channel != nil);

	if ([self stringIsChannelName:channel] == NO) {
		// Many IRCd (I don't know of any that don't) use "JOIN 0" as a
		// secret way to have the user part all channels they are in.
		if ([channel isEqualToString:@"0"]) {
			[self forceJoinChannel:channel password:password];
		}

		return;
	}

	IRCChannel *channelPointer = [self findChannel:channel];

	if (channelPointer) {
		[self joinChannel:channelPointer password:password];

		return;
	}

	[self forceJoinChannel:channel password:password];
}

- (void)forceJoinChannel:(NSString *)channel password:(nullable NSString *)password
{
	NSParameterAssert(channel != nil);

	if (self.isLoggedIn == NO) {
		return;
	}

	if (channel.length == 0) {
		return;
	}

	[self send:@"JOIN", channel, password, nil];
}

- (void)joinUnlistedChannelsWithStringAndSelectBestMatch:(NSString *)channels
{
	[self joinUnlistedChannelsWithStringAndSelectBestMatch:channels passwords:nil];
}

- (void)joinUnlistedChannelsWithStringAndSelectBestMatch:(NSString *)channels passwords:(nullable NSString *)passwords
{
	NSParameterAssert(channels != nil);

	if (channels.length == 0) {
		return;
	}

	NSArray *targets = [channels componentsSeparatedByString:@","];

	[self joinUnlistedChannelsAndSelectBestMatch:targets passwords:passwords];
}

- (void)joinUnlistedChannelsAndSelectBestMatch:(NSArray<NSString *> *)channels
{
	[self joinUnlistedChannelsAndSelectBestMatch:channels passwords:nil];
}

- (void)joinUnlistedChannelsAndSelectBestMatch:(NSArray<NSString *> *)channels passwords:(nullable NSString *)passwords
{
	NSParameterAssert(channels != nil);

	if (self.isLoggedIn == NO) {
		return;
	}

	if (channels.count == 0) {
		return;
	}

	__block BOOL performJoin = YES;

	__block IRCChannel *channelToSelect = nil;

	for (NSString *channel in channels) {
		if (channelToSelect == nil && [self stringIsChannelName:channel]) {
			channelToSelect = [self findChannelOrCreate:channel];

			performJoin = (channelToSelect.isActive == NO || channels.count > 1);

			break;
		}
	}

	if (performJoin) {
		[self send:@"JOIN", [channels componentsJoinedByString:@","], passwords, nil];
	}

	if (channelToSelect) {
		[mainWindow() select:channelToSelect];
	}
}

- (void)joinChannels:(NSArray<IRCChannel *> *)channels
{
	NSParameterAssert(channels != nil);

	if (self.isLoggedIn == NO) {
		return;
	}

	if (channels.count == 0) {
		return;
	}

	NSMutableString *joinStringWithoutKey = nil;
	NSMutableString *joinStringWithKey = nil;

	NSMutableString *keyString = nil;

	for (IRCChannel *channel in channels) {
		if (channel.isChannel == NO || channel.isActive) {
			continue;
		}

		channel.status = IRCChannelStatusJoining;

		NSString *password = nil;

		if (password == nil) {
			password = channel.secretKey;
		}

		if (password.length == 0) {
			if (joinStringWithoutKey == nil) {
				joinStringWithoutKey = [NSMutableString stringWithString:channel.name];
			} else {
				[joinStringWithoutKey appendFormat:@",%@", channel.name];
			}
		} else {
			if (joinStringWithKey == nil) {
				joinStringWithKey = [NSMutableString stringWithString:channel.name];
			} else {
				[joinStringWithKey appendFormat:@",%@", channel.name];
			}

			if (keyString == nil) {
				keyString = [NSMutableString stringWithString:password];
			} else {
				[keyString appendFormat:@",%@", password];
			}
		}
	}

	if (joinStringWithoutKey) {
		[self send:@"JOIN", joinStringWithoutKey, nil];
	}

	if (joinStringWithKey && keyString) {
		[self send:@"JOIN", joinStringWithKey, keyString, nil];
	}
}

- (void)partUnlistedChannel:(NSString *)channel
{
	[self partUnlistedChannel:channel withComment:nil];
}

- (void)partChannel:(IRCChannel *)channel
{
	[self partChannel:channel withComment:nil];
}

- (void)partUnlistedChannel:(NSString *)channel withComment:(nullable NSString *)comment
{
	NSParameterAssert(channel != nil);

	if ([self stringIsChannelName:channel] == NO) {
		return;
	}

	IRCChannel *channelPointer = [self findChannel:channel];

	if (channelPointer == nil) {
		return;
	}

	[self partChannel:channelPointer withComment:comment];
}

- (void)partChannel:(IRCChannel *)channel withComment:(nullable NSString *)comment
{
	NSParameterAssert(channel != nil);

	if (self.isLoggedIn == NO) {
		return;
	}

	if (channel.isChannel == NO || channel.isActive == NO) {
		return;
	}

	if (comment == nil) {
		comment = self.config.normalLeavingComment;
	}

	[self send:@"PART", channel.name, comment, nil];
}

- (void)sendWhoToChannel:(IRCChannel *)channel
{
	[self sendWhoToChannel:channel hideResponse:NO];
}

- (void)sendWhoToChannel:(IRCChannel *)channel hideResponse:(BOOL)hideResponse
{
	NSParameterAssert(channel != nil);

	if (channel.isChannel == NO) {
		return;
	}

	[self sendWhoToChannelNamed:channel.name hideResponse:hideResponse];
}

- (void)sendWhoToChannelNamed:(NSString *)channel
{
	[self sendWhoToChannelNamed:channel hideResponse:NO];
}

- (void)sendWhoToChannelNamed:(NSString *)channel hideResponse:(BOOL)hideResponse
{
	NSParameterAssert(channel != nil);

	if (self.isLoggedIn == NO) {
		return;
	}

	if (channel.length == 0) {
		return;
	}

	if (hideResponse == NO) {
		[self.requestedCommands recordWhoRequestOpenedAsVisible];
	} else {
		[self.requestedCommands recordWhoRequestOpened];
	}

	[self send:@"WHO", channel, nil];
}

- (void)sendWhois:(NSString *)nickname
{
	NSParameterAssert(nickname != nil);

	if (self.isLoggedIn == NO) {
		return;
	}

	if (nickname.length == 0) {
		return;
	}

	[self send:@"WHOIS", nickname, nickname, nil];
}

- (void)kick:(NSString *)nickname inChannel:(IRCChannel *)channel
{
	NSParameterAssert(nickname != nil);
	NSParameterAssert(channel != nil);

	if (self.isLoggedIn == NO) {
		return;
	}

	if (channel.isChannel == NO || channel.isActive == NO) {
		return;
	}

	if (nickname.length == 0) {
		return;
	}

	NSString *reason = [TPCPreferences defaultKickMessage];

	[self send:@"KICK", channel.name, nickname, reason, nil];
}

- (void)toggleAwayStatusWithComment:(nullable NSString *)comment
{
	if (self.userIsAway) {
		[self toggleAwayStatus:NO withComment:nil];
	} else {
		if (comment.length == 0) {
			comment = TXTLS(@"IRC[xog-in]");
		}

		[self toggleAwayStatus:YES withComment:comment];
	}
}

- (void)toggleAwayStatus:(BOOL)setAway
{
	NSString *comment = TXTLS(@"IRC[xog-in]");

	[self toggleAwayStatus:setAway withComment:comment];
}

- (void)toggleAwayStatus:(BOOL)setAway withComment:(nullable NSString *)comment
{
	NSParameterAssert(setAway == NO || comment != nil);

	if (self.isLoggedIn == NO) {
		return;
	}

	if (setAway) {
		[self send:@"AWAY", comment, nil];
	} else {
		[self send:@"AWAY", nil];
	}

	NSString *newNickname = nil;

	if (setAway) {
		newNickname = self.config.awayNickname;

		self.preAwayUserNickname = self.userNickname;
	} else {
		newNickname = self.preAwayUserNickname;

		self.preAwayUserNickname = nil;

		/* If we have an away nickname configured but no preAwayNickname set,
		 then use the configured nickname instead. User probably was on bouncer
		 and relaunched Textual, losing preAwayNickname.*/
		if (newNickname == nil && self.config.awayNickname.length > 0) {
			newNickname = self.config.nickname;
		}
	}

	if (newNickname) {
		[self changeNickname:newNickname];
	}
}

- (void)presentCertificateTrustInformation
{
	if (self.isSecured == NO) {
		return;
	}

	[self.socket openSecuredConnectionCertificateModal];
}

- (void)requestModesForChannel:(IRCChannel *)channel
{
	[self sendModes:nil withParameters:nil inChannel:channel];
}

- (void)requestModesForChannelNamed:(NSString *)channel
{
	[self sendModes:nil withParameters:nil inChannelNamed:channel];
}

- (void)sendModes:(nullable NSString *)modeSymbols withParameters:(nullable NSArray<NSString *> *)parameters inChannel:(IRCChannel *)channel
{
	NSParameterAssert(channel != nil);

	[self sendModes:modeSymbols withParameters:parameters inChannelNamed:channel.name];
}

- (void)sendModes:(nullable NSString *)modeSymbols withParametersString:(nullable NSString *)parametersString inChannel:(IRCChannel *)channel
{
	NSParameterAssert(channel != nil);

	[self sendModes:modeSymbols withParametersString:parametersString inChannelNamed:channel.name];
}

- (void)sendModes:(nullable NSString *)modeSymbols withParameters:(nullable NSArray<NSString *> *)parameters inChannelNamed:(NSString *)channel
{
	NSString *parametersString = [parameters componentsJoinedByString:@" "];

	[self sendModes:modeSymbols withParametersString:parametersString inChannelNamed:channel];
}

- (void)sendModes:(nullable NSString *)modeSymbols withParametersString:(nullable NSString *)parametersString inChannelNamed:(NSString *)channel
{
	NSParameterAssert(channel != nil);

	if (self.isLoggedIn == NO) {
		return;
	}

	if (channel.length == 0) {
		return;
	}

	[self send:@"MODE", channel, modeSymbols, parametersString, nil];
}

- (void)sendPing:(NSString *)tokenString
{
	NSParameterAssert(tokenString != nil);

	if (self.isConnected == NO) {
		return;
	}

	[self send:@"PING", tokenString, nil];
}

- (void)sendPong:(NSString *)tokenString
{
	NSParameterAssert(tokenString != nil);

	if (self.isConnected == NO) {
		return;
	}

	[self send:@"PONG", tokenString, nil];
}

- (void)sendInviteTo:(NSString *)nickname toJoinChannel:(IRCChannel *)channel
{
	NSParameterAssert(channel != nil);

	if (channel.isChannel == NO) {
		return;
	}

	[self sendInviteTo:nickname toJoinChannelNamed:channel.name];
}

- (void)sendInviteTo:(NSString *)nickname toJoinChannelNamed:(NSString *)channel
{
	NSParameterAssert(nickname != nil);
	NSParameterAssert(channel != nil);

	if (nickname.length == 0 || channel.length == 0) {
		return;
	}

	[self send:@"INVITE", nickname, channel, nil];
}

- (void)requestTopicForChannel:(IRCChannel *)channel
{
	[self sendTopicTo:nil inChannel:channel];
}

- (void)requestTopicForChannelNamed:(NSString *)channel
{
	[self sendTopicTo:nil inChannelNamed:channel];
}

- (void)sendTopicTo:(nullable NSString *)topic inChannel:(IRCChannel *)channel
{
	NSParameterAssert(channel != nil);

	if (channel.isChannel == NO || channel.isActive == NO) {
		return;
	}

	[self sendTopicTo:topic inChannelNamed:channel.name];
}

- (void)sendTopicTo:(nullable NSString *)topic inChannelNamed:(NSString *)channel
{
	NSParameterAssert(channel != nil);

	if (self.isLoggedIn == NO) {
		return;
	}

	if (channel.length == 0) {
		return;
	}

	[self send:@"TOPIC", channel, topic, nil];
}

- (void)sendCapability:(NSString *)subcommand data:(nullable NSString *)data
{
	NSParameterAssert(subcommand != nil);

	if (self.isConnected == NO) {
		return;
	}

	[self send:@"CAP", subcommand, data, nil];
}

- (void)sendCapabilityAuthenticate:(NSString *)data
{
	NSParameterAssert(data != nil);

	if (self.isConnected == NO) {
		return;
	}

	if (data.length == 0) {
		return;
	}

	[self send:@"AUTHENTICATE", data, nil];
}

- (void)sendIsonForNicknames:(NSArray<NSString *> *)nicknames
{
	[self sendIsonForNicknames:nicknames hideResponse:NO];
}

- (void)sendIsonForNicknames:(NSArray<NSString *> *)nicknames hideResponse:(BOOL)hideResponse
{
	NSParameterAssert(nicknames != nil);

	/* Split nicknames into fixed number per-command in case there are a lot or are long. */
	/* June 11, 2018: Disabled this because Textual expects nicknames to appear in ISON
	 response or they are considered offline. If we chunk out results, then user may
	 disappear in one ISON response and then appear in another. The long term solution
	 is to stop relying on ISON but no idea when that will come. */
//	[nicknames enumerateSubarraysOfSize:8 usingBlock:^(NSArray *objects, BOOL *stop) {
		[self _sendIsonForNicknames:nicknames hideResponse:hideResponse];
//	}];
}

- (void)_sendIsonForNicknames:(NSArray<NSString *> *)nicknames hideResponse:(BOOL)hideResponse
{
	NSParameterAssert(nicknames != nil);

	if (self.isLoggedIn == NO) {
		return;
	}

	if (nicknames.count == 0) {
		return;
	}

	if (hideResponse == NO) {
		[self.requestedCommands recordIsonRequestOpenedAsVisible];
	} else {
		[self.requestedCommands recordIsonRequestOpened];
	}

	NSString *nicknamesString = [nicknames componentsJoinedByString:@" "];

	[self send:@"ISON", nicknamesString, nil];
}

- (void)requestChannelList
{
	if (self.isLoggedIn == NO) {
		return;
	}

	[self send:@"LIST", nil];
}

- (void)sendPassword:(NSString *)password
{
	NSParameterAssert(password != nil);

	if (self.isConnected == NO) {
		return;
	}

	if (password.length == 0) {
		return;
	}

	[self send:@"PASS", password, nil];
}

- (void)modifyWatchListBy:(BOOL)adding nicknames:(NSArray<NSString *> *)nicknames
{
	NSParameterAssert(nicknames != nil);

	/* Split nicknames into fixed number per-command in case there are a lot or are long. */
	[nicknames enumerateSubarraysOfSize:8 usingBlock:^(NSArray *objects, BOOL *stop) {
		[self _modifyWatchListBy:adding nicknames:objects];
	}];
}

- (void)_modifyWatchListBy:(BOOL)adding nicknames:(NSArray<NSString *> *)nicknames
{
	NSParameterAssert(nicknames != nil);

	if (self.isLoggedIn == NO) {
		return;
	}

	if (nicknames.count == 0) {
		return;
	}

	NSString *modifier = nil;

	if ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityMonitorCommand])
	{
		if (adding) {
			modifier = @"+";
		} else {
			modifier = @"-";
		}

		NSString *nicknamesString = [nicknames componentsJoinedByString:@","];

		[self send:@"MONITOR", modifier, nicknamesString, nil];
	}
	else if ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityWatchCommand])
	{
		if (adding) {
			modifier = @" +";
		} else {
			modifier = @" -";
		}

		NSString *nicknamesString = [nicknames componentsJoinedByString:modifier];

		[self send:@"WATCH", [modifier stringByAppendingString:nicknamesString], nil];
	}
}

#pragma mark -
#pragma mark File Transfers

- (void)notifyFileTransfer:(TXNotificationType)type nickname:(NSString *)nickname filename:(NSString *)filename filesize:(uint64_t)totalFilesize requestIdentifier:(NSString *)identifier
{
	NSParameterAssert(nickname != nil);
	NSParameterAssert(filename != nil);
	NSParameterAssert(identifier != nil);

	NSString *description = nil;

	switch (type) {
		case TXNotificationTypeFileTransferSendSuccessful:
		{
			description = TXTLS(@"Notifications[fhn-dd]", filename, totalFilesize);

			break;
		}
		case TXNotificationTypeFileTransferReceiveSuccessful:
		{
			description = TXTLS(@"Notifications[oqh-pn]", filename, totalFilesize);

			break;
		}
		case TXNotificationTypeFileTransferSendFailed:
		{
			description = TXTLS(@"Notifications[9r4-cq]", filename);

			break;
		}
		case TXNotificationTypeFileTransferReceiveFailed:
		{
			description = TXTLS(@"Notifications[cqq-ci]", filename);

			break;
		}
		case TXNotificationTypeFileTransferReceiveRequested:
		{
			description = TXTLS(@"Notifications[wik-wq]", filename, totalFilesize);

			break;
		}
		default:
		{
			break;
		}
	}

	NSDictionary *info = @{
	   @"isFileTransferNotification" : @(YES),
	   @"fileTransferUniqueIdentifier" : identifier,
	   @"fileTransferNotificationType" : @(type)
	};

	[self notifyEvent:type lineType:TVCLogLineTypeUndefined target:nil nickname:nickname text:description userInfo:info];
}

- (void)receivedDCCQuery:(IRCMessage *)m text:(NSString *)text ignoreInfo:(nullable IRCAddressBookEntry *)ignoreInfo
{
	NSParameterAssert(m != nil);
	NSParameterAssert(text != nil);

	if (self.isLoggedIn == NO) {
		return;
	}

	/* Do not continue if the user has configured an ignore for file transfers */
	if (ignoreInfo.ignoreFileTransferRequests) {
		return;
	}

	/* Do not continue if we are not the target */
	if ([self nicknameIsMyself:[m paramAt:0]] == NO) {
		return;
	}

	/* Record information */
	NSString *sender = m.senderNickname;

	NSMutableString *textMutable = [text mutableCopy];

	NSString *subcommand = textMutable.uppercaseGetToken;

	BOOL isSendRequest = ([subcommand isEqualToString:@"SEND"]);
	BOOL isResumeRequest = ([subcommand isEqualToString:@"RESUME"]);
	BOOL isAcceptRequest = ([subcommand isEqualToString:@"ACCEPT"]);

	if (isSendRequest == NO && isResumeRequest == NO && isAcceptRequest == NO) {
		return;
	}

	NSString *section1 = nil;

	if ([textMutable hasPrefix:@"\""]) {
		section1 = textMutable.tokenInsideQuotes;
	} else {
		section1 = textMutable.token;
	}

	NSString *section2 = textMutable.token;
	NSString *section3 = textMutable.token;
	NSString *section4 = textMutable.token;
	NSString *section5 = textMutable.token;

	/* Trim whitespaces in case someone tries to send blank 
	 spaces in a quoted string for filename. */
	section1 = section1.trim;

	/* Remove T from in front of token if it is there. */
	if (isSendRequest) {
		if ([section5 hasPrefix:@"T"]) {
			section5 = [section5 substringFromIndex:1];
		}
	} else if (isAcceptRequest || isResumeRequest) {
	   if ([section4 hasPrefix:@"T"]) {
			section4 = [section4 substringFromIndex:1];
	   }
	}

	/* Valid values? */
	if ( section1.length == 0 ||
		 section2.length == 0 ||
		(section4.length == 0 && isSendRequest))
	{
		return;
	}

	/* Start data association. */
	NSString *hostAddress = nil;
	NSString *hostPort = nil;
	NSString *filename = nil;
	NSString *filesize = nil;
	NSString *transferToken = nil;

	/* Match data variables. */
	if (isSendRequest)
	{
		/* Get normal information */
		filename = section1.safeFilename;

		filesize = section4;

		hostPort = section3;

		transferToken = section5;

		/* Translate host address */
		if (section2.numericOnly) {
			long long a = section2.longLongValue;

			NSInteger w = (a & 0xff); a >>= 8;
			NSInteger x = (a & 0xff); a >>= 8;
			NSInteger y = (a & 0xff); a >>= 8;
			NSInteger z = (a & 0xff);

			hostAddress = [NSString stringWithFormat:@"%ld.%ld.%ld.%ld", (long)z, (long)y, (long)x, (long)w];
		} else {
			hostAddress = section2;
		}
	}
	else if (isResumeRequest || isAcceptRequest)
	{
		filename = section1.safeFilename;

		filesize = section3;

		hostPort = section2;

		transferToken = section4;

		hostAddress = nil;
	}

	if (transferToken && transferToken.length == 0) {
		transferToken = nil;
	}

	/* Important checks */
	if (transferToken.length > 0 && transferToken.numericOnly == NO) {
		LogToConsoleError("Fatal error: Received transfer token that is not a number");

		goto present_error;
	}

	NSInteger hostPortInt = hostPort.integerValue;

	if (hostPortInt == 0 && transferToken == nil) {
		LogToConsoleError("Fatal error: Port cannot be zero without a transfer token");

		goto present_error;
	} else if (hostPortInt < 0 || hostPortInt > TXMaximumTCPPort) {
		LogToConsoleError("Fatal error: Port cannot be less than zero or greater than 65535");

		goto present_error;
	}

	long long filesizeInt = filesize.longLongValue;

	if (filesizeInt <= 0 || filesizeInt > powl(1000, 4)) { // 1 TB
		LogToConsoleError("Fatal error: Filesize is silly");

		goto present_error;
	}

	/* Process individual commands */
	if (isSendRequest) {
		/* DCC SEND <filename> <peer-ip> <port> <filesize> [token] */

		if (transferToken) {
			TDCFileTransferDialogTransferController *e = [[self fileTransferController] fileTransferSenderMatchingToken:transferToken];

			/* 0 port indicates a new request in reverse DCC */
			if (hostPortInt == 0)
			{
				if (e != nil) {
					LogToConsoleError("Fatal error: Received reverse DCC request with token '%{public}@' but the token already exists", transferToken);

					goto present_error;
				}

				[self receivedDCCSend:sender
							 filename:filename
							  address:hostAddress
								 port:hostPortInt
							 filesize:filesizeInt
								token:transferToken];

				return;
			}
			else if (e)
			{
				if (e.transferStatus != TDCFileTransferDialogTransferStatusWaitingForReceiverToAccept) {
					LogToConsoleError("Fatal error: Unexpected request to begin transfer");

					goto present_error;
				}

				[e didReceiveSendRequest:hostAddress hostPort:hostPortInt];

				return;
			}
		}
		else // transferToken
		{
			/* Treat as normal DCC request */
			[self receivedDCCSend:sender
						 filename:filename
						  address:hostAddress
							 port:hostPort.integerValue
						 filesize:filesize.longLongValue
							token:nil];

			return;
		}
	}
	else if (isResumeRequest || isAcceptRequest)
	{
		TDCFileTransferDialogTransferController *e = nil;

		if (transferToken && hostPortInt == 0) {
			e = [[self fileTransferController] fileTransferSenderMatchingToken:transferToken];
		} else if (transferToken == nil && hostPortInt > 0) {
			e = [[self fileTransferController] fileTransferMatchingPort:hostPortInt];
		}

		if (e == nil) {
			LogToConsoleError("Fatal error: Could not locate file transfer that matches resume request");

			goto present_error;
		}

		if ((isResumeRequest && (e.transferStatus != TDCFileTransferDialogTransferStatusWaitingForReceiverToAccept &&
								 e.transferStatus != TDCFileTransferDialogTransferStatusIsListeningAsSender)) ||
			(isAcceptRequest && e.transferStatus != TDCFileTransferDialogTransferStatusWaitingForResumeAccept))
		{
			LogToConsoleError("Fatal error: Bad transfer status");

			goto present_error;
		}

		if (isResumeRequest) {
			[e didReceiveResumeRequest:filesizeInt];
		} else {
			[e didReceiveResumeAccept:filesizeInt];
		}

		return;
	}

	// Report an error
present_error:
	[self print:TXTLS(@"IRC[y3w-la]", sender) by:nil inChannel:nil asType:TVCLogLineTypeDCCFileTransfer command:TVCLogLineDefaultCommandValue];
}

- (void)receivedDCCSend:(NSString *)nickname filename:(NSString *)filename address:(NSString *)address port:(uint16_t)port filesize:(uint64_t)totalFilesize token:(nullable NSString *)transferToken
{
	NSParameterAssert(nickname != nil);
	NSParameterAssert(filename != nil);
	NSParameterAssert(address != nil);

	NSString *message = TXTLS(@"IRC[snf-45]", nickname, filename, totalFilesize);

	[self print:message by:nil inChannel:nil asType:TVCLogLineTypeDCCFileTransfer command:TVCLogLineDefaultCommandValue];

	if ([TPCPreferences fileTransferRequestReplyAction] == TXFileTransferRequestReplyIgnore) {
		return;
	}

	NSString *addedRequest = [[self fileTransferController] addReceiverForClient:self nickname:nickname address:address port:port filename:filename filesize:totalFilesize token:transferToken];

	if (addedRequest == nil) {
		return;
	}

	[self notifyFileTransfer:TXNotificationTypeFileTransferReceiveRequested nickname:nickname filename:filename filesize:totalFilesize requestIdentifier:addedRequest];
}

- (void)sendFileResume:(NSString *)nickname port:(uint16_t)port filename:(NSString *)filename filesize:(uint64_t)totalFilesize token:(nullable NSString *)transferToken
{
	NSParameterAssert(nickname != nil);
	NSParameterAssert(filename != nil);

	NSString *escapedFilename = [self DCCSendEscapeFilename:filename];

	NSString *stringToSend = nil;

	if (transferToken) {
		stringToSend = [NSString stringWithFormat:@"%@ %hu %lli %@", escapedFilename, port, totalFilesize, transferToken];
	} else {
		stringToSend = [NSString stringWithFormat:@"%@ %hu %lli", escapedFilename, port, totalFilesize];
	}

	[self sendCTCPQuery:nickname command:@"DCC RESUME" text:stringToSend];
}

- (void)sendFileResumeAccept:(NSString *)nickname port:(uint16_t)port filename:(NSString *)filename filesize:(uint64_t)totalFilesize token:(nullable NSString *)transferToken
{
	NSParameterAssert(nickname != nil);
	NSParameterAssert(filename != nil);

	NSString *escapedFilename = [self DCCSendEscapeFilename:filename];

	NSString *stringToSend = nil;

	if (transferToken) {
		stringToSend = [NSString stringWithFormat:@"%@ %hu %lli %@", escapedFilename, port, totalFilesize, transferToken];
	} else {
		stringToSend = [NSString stringWithFormat:@"%@ %hu %lli", escapedFilename, port, totalFilesize];
	}

	[self sendCTCPQuery:nickname command:@"DCC ACCEPT" text:stringToSend];
}

- (void)sendFile:(NSString *)nickname port:(uint16_t)port filename:(NSString *)filename filesize:(uint64_t)totalFilesize token:(nullable NSString *)transferToken
{
	NSParameterAssert(nickname != nil);
	NSParameterAssert(filename != nil);

	NSString *address = [self DCCTransferAddress];

	if (address == nil) {
		return;
	}

	NSString *escapedFilename = [self DCCSendEscapeFilename:filename];

	NSString *stringToSend = nil;

	if (transferToken.length > 0) {
		stringToSend = [NSString stringWithFormat:@"%@ %@ %hu %lli %@", escapedFilename, address, port, totalFilesize, transferToken];
	} else {
		stringToSend = [NSString stringWithFormat:@"%@ %@ %hu %lli", escapedFilename, address, port, totalFilesize];
	}

	[self sendCTCPQuery:nickname command:@"DCC SEND" text:stringToSend];

	NSString *message = TXTLS(@"IRC[ags-s8]", nickname, filename, totalFilesize);

	[self print:message by:nil inChannel:nil asType:TVCLogLineTypeDCCFileTransfer command:TVCLogLineDefaultCommandValue];
}

- (NSString *)DCCSendEscapeFilename:(NSString *)filename
{
	NSParameterAssert(filename != nil);

	NSString *filenameEscaped = filename.safeFilename;

	if ([filenameEscaped contains:@" "] == NO) {
		return filenameEscaped;
	}
	
	/* Escape double quotes because the filename will be wrapped.
	 February 20, 2017: Maybe we should replace the double quote
	 with another character or remove completely? Untested how other
	 clients will handle an escaped double quote. */
	filenameEscaped = [filenameEscaped stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""];

	return [NSString stringWithFormat:@"\"%@\"", filenameEscaped];
}

- (nullable NSString *)DCCTransferAddress
{
	NSString *address = [self fileTransferController].IPAddress;

	if (address == nil) {
		return nil;
	}

	if (address.IPv6Address) {
		return address;
	}

	NSArray *addressOctets = [address componentsSeparatedByString:@"."];

	if (addressOctets.count != 4) {
		LogToConsoleError("User configured a silly IP address");

		return nil;
	}

	NSInteger w = [addressOctets[0] integerValue];
	NSInteger x = [addressOctets[1] integerValue];
	NSInteger y = [addressOctets[2] integerValue];
	NSInteger z = [addressOctets[3] integerValue];

	unsigned long long a = 0;

	a |= w; a <<= 8;
	a |= x; a <<= 8;
	a |= y; a <<= 8;
	a |= z;

	return [NSString stringWithFormat:@"%llu", a];
}

#pragma mark -
#pragma mark Command Queue

- (NSString *)descriptionForTimedCommand:(IRCTimedCommand *)timedCommand
{
	NSParameterAssert(timedCommand != nil);

	NSString *timerInterval = TXHumanReadableTimeInterval(timedCommand.timerInterval, NO, 0);

	NSString *timeRemaining = TXHumanReadableTimeInterval(timedCommand.timeRemaining, NO, 0);

	NSString *timerStatus = nil;

	if (timedCommand.timerIsActive == NO) {
		timerStatus = TXTLS(@"IRC[ww4-sn]");
	} else {
		timerStatus = TXTLS(@"IRC[bhz-9e]");
	}

	if (timedCommand.repeatTimer == NO) {
		return TXTLS(@"IRC[4n6-2x]",
					 timedCommand.identifier,
					 timerStatus,
					 timerInterval,
					 timeRemaining,
					 timedCommand.command);
	} else {
		NSUInteger repeatLimit = timedCommand.iterations;

		NSString *repeatLimitDescriptor = nil;

		if (repeatLimit == 0) {
			repeatLimitDescriptor = TXTLS(@"IRC[o26-ae]");
		} else {
			repeatLimitDescriptor = [NSString stringWithUnsignedInteger:repeatLimit];
		}

		return TXTLS(@"IRC[uw0-v2]",
					 timedCommand.identifier,
					 timerStatus,
					 timerInterval,
					 timeRemaining,
					 repeatLimitDescriptor,
					 timedCommand.currentIteration,
					 timedCommand.command);
	}
}

- (nullable IRCTimedCommand *)timedCommandWithIdentifier:(NSString *)identifier
{
	NSParameterAssert(identifier != nil);

	@synchronized (self.timedCommands) {
		return self.timedCommands[identifier];
	}
}

- (NSArray<IRCTimedCommand *> *)listOfTimedCommands
{
	@synchronized (self.timedCommands) {
		return self.timedCommands.allValues;
	}
}

- (void)addTimedCommand:(IRCTimedCommand *)timedCommand
{
	NSParameterAssert(timedCommand != nil);

	@synchronized (self.timedCommands) {
		self.timedCommands[timedCommand.identifier] = timedCommand;
	}
}

- (void)removeTimedCommands
{
	@synchronized (self.timedCommands) {
		[self.timedCommands removeAllObjects];
	}
}

- (void)removeTimedCommand:(IRCTimedCommand *)timedCommand
{
	NSParameterAssert(timedCommand != nil);

	@synchronized (self.timedCommands) {
		[self.timedCommands removeObjectForKey:timedCommand.identifier];
	}
}

- (void)stopTimedCommand:(IRCTimedCommand *)timedCommand
{
	NSParameterAssert(timedCommand != nil);

	[timedCommand stop];
}

- (void)startTimedCommand:(IRCTimedCommand *)timedCommand interval:(NSUInteger)timerInterval
{
	NSParameterAssert(timedCommand != nil);

	[self startTimedCommand:timedCommand interval:timerInterval onRepeat:NO iterations:0];
}

- (void)startTimedCommand:(IRCTimedCommand *)timedCommand interval:(NSUInteger)timerInterval onRepeat:(BOOL)repeatTimer
{
	NSParameterAssert(timedCommand != nil);

	[self startTimedCommand:timedCommand interval:timerInterval onRepeat:repeatTimer iterations:0];
}

- (void)startTimedCommand:(IRCTimedCommand *)timedCommand interval:(NSUInteger)timerInterval onRepeat:(BOOL)repeatTimer iterations:(NSUInteger)iterations
{
	NSParameterAssert(timedCommand != nil);

	[timedCommand start:timerInterval onRepeat:repeatTimer iterations:iterations];
}

- (BOOL)restartTimedCommand:(IRCTimedCommand *)timedCommand
{
	NSParameterAssert(timedCommand != nil);

	return [timedCommand restart];
}

- (void)onTimedCommand:(IRCTimedCommand *)timedCommand
{
	NSParameterAssert(timedCommand != nil);

	/* Remove timer */
	if (timedCommand.timerIsActive == NO) {
		[self removeTimedCommand:timedCommand];
	}

	/* The -channelId is only a suggestion. It's okay if this returns nil.
	 The channel is what was selected at the time that the timer was created.
	 -sendCommand:completeTarget:target: may very well ignore the channel we
	 give it, even if it's non-nil, depending on format of command. */
	IRCChannel *channel = (IRCChannel *)[worldController() findItemWithId:timedCommand.channelId];

	[self sendCommand:timedCommand.command completeTarget:YES target:channel.name];
}

#pragma mark -
#pragma mark User Tracking

- (void)clearTrackedUsers
{
	[self.trackedUsers clearTrackedUsers];
}

- (void)statusOfTrackedNickname:(NSString *)nickname changedTo:(IRCAddressBookUserTrackingStatus)newStatus
{
	[self statusOfTrackedNickname:nickname changedTo:newStatus notify:NO];
}

- (void)statusOfTrackedNickname:(NSString *)nickname changedTo:(IRCAddressBookUserTrackingStatus)newStatus notify:(BOOL)notify
{
	NSParameterAssert(nickname != nil);

	[self.trackedUsers statusOfTrackedNickname:nickname changedTo:newStatus];

	if (notify) {
		[self notifyStatusOfTrackedNickname:nickname changedTo:newStatus];
	}
}

- (void)notifyStatusOfTrackedNickname:(NSString *)nickname changedTo:(IRCAddressBookUserTrackingStatus)newStatus
{
	NSParameterAssert(nickname != nil);

	NSString *message = nil;

	if (newStatus == IRCAddressBookUserTrackingStatusSignedOn) {
		message = TXTLS(@"Notifications[xk2-1l]", nickname);
	} else if (newStatus == IRCAddressBookUserTrackingStatusSignedOff) {
		message = TXTLS(@"Notifications[rif-9r]", nickname);
	} else if (newStatus == IRCAddressBookUserTrackingStatusAvailable) {
		message = TXTLS(@"Notifications[97r-0l]", nickname);
	}

	if (message == nil) {
		return;
	}

	[self notifyEvent:TXNotificationTypeAddressBookMatch lineType:TVCLogLineTypeNotice target:nil nickname:nickname text:message];
}

- (void)populateISONTrackedUsersList
{
	if (self.isLoggedIn == NO) {
		return;
	}

	/* Additions & Removals for WATCH command. ISON does not access these. */
	NSMutableArray<NSString *> *watchAdditions = [NSMutableArray array];
	NSMutableArray<NSString *> *watchRemovals = [NSMutableArray array];

	/* Compare configuration to the list of tracked nicknames.
	 * Nicknames that are new are added to watchAdditions */
	NSDictionary *trackedUsersOld = self.trackedUsers.trackedUsers;

	NSMutableArray<NSString *> *trackedUsersNew = [NSMutableArray array];

	for (IRCAddressBookEntry *g in self.config.ignoreList) {
		if (g.trackUserActivity == NO) {
			continue;
		}

		NSString *trackingNickname = g.trackingNickname;

		IRCAddressBookUserTrackingStatus trackingStatus = [self.trackedUsers statusOfUser:trackingNickname];

		if (trackingStatus != IRCAddressBookUserTrackingStatusUnknown) {
			[trackedUsersNew addObject:trackingNickname];

			continue;
		}

		[watchAdditions addObject:trackingNickname];

		[self.trackedUsers _addTrackedUser:trackingNickname];
	}

	/* Compare old list of tracked nicknames to new list to find
	 those that no longer appear. Mark those for removal. */
	for (NSString *trackedUser in trackedUsersOld) {
		if ([trackedUsersNew containsObjectIgnoringCase:trackedUser]) {
			continue;
		}

		[watchRemovals addObject:trackedUser];

		[self.trackedUsers _removeTrackedUser:trackedUser];
	}

	/* Set new entries */
	[self modifyWatchListBy:YES nicknames:watchAdditions];

	[self modifyWatchListBy:NO nicknames:watchRemovals];

	[self startISONTimer];
}

- (void)startISONTimer
{
	if (self.isonTimer.timerIsActive) {
		return;
	}

	[self.isonTimer start:_isonCheckInterval onRepeat:YES];

	[self startWhoTimer];
}

- (void)stopISONTimer
{
	if (self.isonTimer.timerIsActive == NO) {
		return;
	}

	[self.isonTimer stop];

	[self stopWhoTimer];
}

- (void)onISONTimer
{
	if (self.isLoggedIn == NO || self.isBrokenIRCd_aka_Twitch) {
		return;
	}

	NSMutableArray<NSString *> *nicknames = [NSMutableArray array];

	// Request ISON status for tracked users
	if (self.supportsAdvancedTracking == NO) {
		for (NSString *trackedUser in self.trackedUsers.trackedUsers) {
			[nicknames addObject:trackedUser];
		}
	}

	// Request ISON status for private messages
	for (IRCChannel *channel in self.channelList) {
		if (channel.privateMessage) {
			[nicknames addObject:channel.name];
		}
	}

	[self sendIsonForNicknames:nicknames hideResponse:YES];
}

- (void)startWhoTimer
{
	if (self.whoTimer.timerIsActive) {
		return;
	}

	[self.whoTimer start:_whoCheckInterval onRepeat:YES];
}

- (void)stopWhoTimer
{
	if (self.whoTimer.timerIsActive == NO) {
		return;
	}

	[self.whoTimer stop];
}

- (void)onWhoTimer
{
	if (self.isLoggedIn == NO || self.isBrokenIRCd_aka_Twitch) {
		return;
	}

	NSArray *channelList = self.channelList;

	[self sendTimedWhoRequestsToChannels:channelList];
}

- (void)sendTimedWhoRequestsToChannels:(NSArray<IRCChannel *> *)channelList
{
	NSParameterAssert(channelList != nil);

	if (self.isLoggedIn == NO || self.isBrokenIRCd_aka_Twitch) {
		return;
	}

#define _maximumChannelCountPerWhoBatchRequest			4
#define _maximumSingleChannelSizePerWhoBatchRequest		5000
#define _maximumTotalChannelSizePerWhoBatchRequest		2000

	NSUInteger channelCount = channelList.count;

	if (channelCount == 0) {
		return;
	}

	NSUInteger startingPosition = self.lastWhoRequestChannelListIndex;

	NSUInteger endingPosition = (startingPosition + _maximumChannelCountPerWhoBatchRequest);

	if (startingPosition >= channelCount) {
		startingPosition = 0;
	}

	if (endingPosition >= channelCount) {
		endingPosition = (channelCount - 1);
	}

	NSUInteger totalMemberCount = 0;

	NSMutableArray<IRCChannel *> *channelsToQuery = nil;

	for (NSUInteger channelIndex = startingPosition; channelIndex <= endingPosition; channelIndex++) {
		IRCChannel *channel = channelList[channelIndex];

		if (channel.isActive == NO || channel.isChannel == NO) {
			continue;
		}

		/* Update internal state of flag */
		BOOL sentInitialWhoRequest = channel.sentInitialWhoRequest;

		if (sentInitialWhoRequest == NO) {
			channel.sentInitialWhoRequest = YES;
		}

		/* continue to next channel and do not break so that the
		 -sentInitialWhoRequest flag of all channels can be updated. */
		if (self.config.sendWhoCommandRequestsToChannels == NO) {
			continue;
		}

		/* Perform comparisons to know whether channel is acceptable */
		NSUInteger numberOfMembers = channel.numberOfMembers;

		if (sentInitialWhoRequest == NO) {
			if (numberOfMembers > _maximumSingleChannelSizePerWhoBatchRequest) {
				continue;
			}
		} else {
			if ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityAwayNotify]) {
				continue;
			}

			if (numberOfMembers > [TPCPreferences trackUserAwayStatusMaximumChannelSize]) {
				continue;
			}
		}

		/* Add channel to list */
		if (channelsToQuery == nil) {
			channelsToQuery = [NSMutableArray new];
		}

		[channelsToQuery addObject:channel];

		/* Update total number of members and maybe break loop */
		totalMemberCount += numberOfMembers;

		if (totalMemberCount > _maximumTotalChannelSizePerWhoBatchRequest) {
			endingPosition = channelIndex;

			break;
		}
	}

	self.lastWhoRequestChannelListIndex = (endingPosition + 1);

	/* Send WHO requests */
	if (channelsToQuery == nil) {
		return;
	}

	for (IRCChannel *channel in channelsToQuery) {
		[self sendWhoToChannel:channel hideResponse:YES];
	}

#undef _maximumChannelCountPerWhoBatchRequest
#undef _maximumSingleChannelSizePerWhoBatchRequest
#undef _maximumTotalChannelSizePerWhoBatchRequest
}

- (void)updateUserTrackingStatusForEntry:(IRCAddressBookEntry *)addressBookEntry withMessage:(IRCMessage *)message
{
	NSParameterAssert(addressBookEntry != nil);
	NSParameterAssert(message != nil);

	if (self.supportsAdvancedTracking) {
		return;
	}

	IRCAddressBookUserTrackingStatus trackingStatus = [self.trackedUsers statusOfEntry:addressBookEntry];

	if (trackingStatus == IRCAddressBookUserTrackingStatusUnknown) {
		return;
	}

	BOOL ison = (trackingStatus == IRCAddressBookUserTrackingStatusAvailable);

	/* Notification Type: JOIN Command */
	if ([message.command isEqualToStringIgnoringCase:@"JOIN"]) {
		if (ison == NO) {
			[self statusOfTrackedNickname:message.senderNickname changedTo:IRCAddressBookUserTrackingStatusSignedOn notify:YES];
		}

		return;
	}

	/* Notification Type: QUIT Command */
	if ([message.command isEqualToStringIgnoringCase:@"QUIT"]) {
		if (ison) {
			[self statusOfTrackedNickname:message.senderNickname changedTo:IRCAddressBookUserTrackingStatusSignedOff notify:YES];
		}

		return;
	}

	/* Notification Type: NICK Command */
	if ([message.command isEqualToStringIgnoringCase:@"NICK"]) {
		if (ison) {
			[self statusOfTrackedNickname:message.senderNickname changedTo:IRCAddressBookUserTrackingStatusSignedOff notify:YES];
		} else {
			[self statusOfTrackedNickname:message.senderNickname changedTo:IRCAddressBookUserTrackingStatusSignedOn notify:YES];
		}

		return;
	}
}

#pragma mark -
#pragma mark Channel Ban List Dialog

- (void)createChannelInviteExceptionListSheet
{
	[self createChannelBanListSheet:TDCChannelBanListSheetEntryTypeInviteException];
}

- (void)createChannelBanExceptionListSheet
{
	[self createChannelBanListSheet:TDCChannelBanListSheetEntryTypeBanException];
}

- (void)createChannelBanListSheet
{
	[self createChannelBanListSheet:TDCChannelBanListSheetEntryTypeBan];
}

- (void)createChannelQuietListSheet
{
	[self createChannelBanListSheet:TDCChannelBanListSheetEntryTypeQuiet];
}

- (void)createChannelBanListSheet:(TDCChannelBanListSheetEntryType)entryType
{
	[windowController() popMainWindowSheetIfExists];

	IRCChannel *c = mainWindow().selectedChannel;

	if (c == nil) {
		return;
	}

	TDCChannelBanListSheet *listSheet = [[TDCChannelBanListSheet alloc] initWithEntryType:entryType inChannel:c];

	if (listSheet == nil) {
		return;
	}

	listSheet.delegate = (id)self;

	listSheet.window = mainWindow();

	[listSheet start];

	[windowController() addWindowToWindowList:listSheet];
}

- (void)channelBanListSheetOnUpdate:(TDCChannelBanListSheet *)sender
{
	IRCChannel *channel = sender.channel;

	if (channel == nil) {
		return;
	}

	NSString *modeSend = [NSString stringWithFormat:@"+%@", sender.modeSymbol];

	[self sendModes:modeSend withParametersString:nil inChannel:channel];
}

- (void)channelBanListSheetWillClose:(TDCChannelBanListSheet *)sender
{
	IRCChannel *channel = sender.channel;

	if (channel == nil) {
		return;
	}

	NSArray *listOfChanges = sender.listOfChanges;

	for (NSString *change in listOfChanges) {
		[self sendModes:change withParametersString:nil inChannel:channel];
	}

	[windowController() removeWindowFromWindowList:sender];
}

#pragma mark -
#pragma mark Network Channel List Dialog

- (NSString *)channelListDialogWindowKey
{
	return [NSString stringWithFormat:@"TDCServerChannelListDialog -> %@", self.uniqueIdentifier];
}

- (nullable TDCServerChannelListDialog *)channelListDialog
{
	return [windowController() windowFromWindowList:[self channelListDialogWindowKey]];
}

- (void)createChannelListDialog
{
	if ([windowController() maybeBringWindowForward:[self channelListDialogWindowKey]]) {
		return; // The window was brought forward already.
	}

	TDCServerChannelListDialog *channelListDialog = [[TDCServerChannelListDialog alloc] initWithClient:self];

	channelListDialog.delegate = (id)self;

	[channelListDialog show];

	[windowController() addWindowToWindowList:channelListDialog withDescription:[self channelListDialogWindowKey]];
}

- (void)serverChannelListDialogOnUpdate:(TDCServerChannelListDialog *)sender
{
	[self requestChannelList];
}

- (void)serverChannelListDialog:(TDCServerChannelListDialog *)sender joinChannels:(NSArray<NSString *> *)channels
{
	[self joinUnlistedChannelsAndSelectBestMatch:channels];
}

- (void)serverChannelDialogWillClose:(TDCServerChannelListDialog *)sender
{
	[windowController() removeWindowFromWindowList:[self channelListDialogWindowKey]];
}

@end

NS_ASSUME_NONNULL_END
