/*
 * Copyright (C) 2004-2025 ZNC, see the NOTICE file for details.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <znc/IRCSock.h>
#include <znc/Chan.h>
#include <znc/User.h>
#include <znc/IRCNetwork.h>
#include <znc/Server.h>
#include <znc/Query.h>
#include <znc/ZNCDebug.h>
#include <time.h>

using std::set;
using std::vector;
using std::map;

#define IRCSOCKMODULECALL(macFUNC, macEXITER)                              \
    NETWORKMODULECALL(macFUNC, m_pNetwork->GetUser(), m_pNetwork, nullptr, \
                      macEXITER)
// These are used in OnGeneralCTCP()
const unsigned long long CIRCSock::m_uCTCPFloodTime = 5000;
const unsigned int CIRCSock::m_uCTCPFloodCount = 5;

// It will be bad if user sets it to 0.00000000000001
// If you want no flood protection, set network's flood rate to -1
// TODO move this constant to CIRCNetwork?
static const double FLOOD_MINIMAL_RATE = 0.3;

class CIRCFloodTimer : public CCron {
    CIRCSock* m_pSock;

  public:
    CIRCFloodTimer(CIRCSock* pSock) : m_pSock(pSock) {
        StartMaxCycles(m_pSock->m_fFloodRate, 0);
    }
    CIRCFloodTimer(const CIRCFloodTimer&) = delete;
    CIRCFloodTimer& operator=(const CIRCFloodTimer&) = delete;
    void RunJob() override {
        if (m_pSock->m_iSendsAllowed < m_pSock->m_uFloodBurst) {
            m_pSock->m_iSendsAllowed++;
        }
        m_pSock->TrySend();
    }
};

bool CIRCSock::IsFloodProtected(double fRate) {
    return fRate > FLOOD_MINIMAL_RATE;
}

CIRCSock::CIRCSock(CIRCNetwork* pNetwork)
    : CIRCSocket(),
      m_bAuthed(false),
      m_bNamesx(false),
      m_bUHNames(false),
      m_bAwayNotify(false),
      m_bAccountNotify(false),
      m_bExtendedJoin(false),
      m_bServerTime(false),
      m_bMessageTagCap(false),
      m_sPerms("*!@%+"),
      m_sPermModes("qaohv"),
      m_scUserModes(),
      m_mceChanModes(),
      m_pNetwork(pNetwork),
      m_Nick(),
      m_sPass(""),
      m_msChans(),
      m_uMaxNickLen(9),
      m_uCapPaused(0),
      m_ssAcceptedCaps(),
      m_ssPendingCaps(),
      m_lastCTCP(0),
      m_uNumCTCP(0),
      m_mISupport(),
      m_vSendQueue(),
      m_iSendsAllowed(pNetwork->GetFloodBurst()),
      m_uFloodBurst(pNetwork->GetFloodBurst()),
      m_fFloodRate(pNetwork->GetFloodRate()),
      m_bFloodProtection(IsFloodProtected(pNetwork->GetFloodRate())),
      m_lastFloodWarned(0) {
    EnableReadLine();
    m_Nick.SetIdent(m_pNetwork->GetIdent());
    m_Nick.SetHost(m_pNetwork->GetBindHost());
    SetEncoding(m_pNetwork->GetEncoding());

    m_mceChanModes['b'] = ListArg;
    m_mceChanModes['e'] = ListArg;
    m_mceChanModes['I'] = ListArg;
    m_mceChanModes['k'] = HasArg;
    m_mceChanModes['l'] = ArgWhenSet;
    m_mceChanModes['p'] = NoArg;
    m_mceChanModes['s'] = NoArg;
    m_mceChanModes['t'] = NoArg;
    m_mceChanModes['i'] = NoArg;
    m_mceChanModes['n'] = NoArg;

    pNetwork->SetIRCSocket(this);

    // RFC says a line can have 512 chars max + 512 chars for message tags, but
    // we don't care ;)
    SetMaxBufferThreshold(2048);
    if (m_bFloodProtection) {
        AddCron(new CIRCFloodTimer(this));
    }
}

CIRCSock::~CIRCSock() {
    if (!m_bAuthed) {
        IRCSOCKMODULECALL(OnIRCConnectionError(this), NOTHING);
    }

    const vector<CChan*>& vChans = m_pNetwork->GetChans();
    for (CChan* pChan : vChans) {
        pChan->Reset();
    }

    m_pNetwork->IRCDisconnected();

    for (const auto& it : m_msChans) {
        delete it.second;
    }

    Quit();
    m_msChans.clear();
    m_pNetwork->AddBytesRead(GetBytesRead());
    m_pNetwork->AddBytesWritten(GetBytesWritten());
}

void CIRCSock::Quit(const CString& sQuitMsg) {
    if (IsClosed()) {
        return;
    }
    if (!m_bAuthed) {
        Close(CLT_NOW);
        return;
    }
    if (!sQuitMsg.empty()) {
        PutIRC("QUIT :" + sQuitMsg);
    } else {
        PutIRC("QUIT :" + m_pNetwork->ExpandString(m_pNetwork->GetQuitMsg()));
    }
    Close(CLT_AFTERWRITE);
}

void CIRCSock::ReadLine(const CString& sData) {
    CString sLine = sData;

    sLine.Replace("\n", "");
    sLine.Replace("\r", "");

    DEBUG("(" << m_pNetwork->GetUser()->GetUsername() << "/"
              << m_pNetwork->GetName() << ") IRC -> ZNC [" << sLine << "]");

    bool bReturn = false;
    IRCSOCKMODULECALL(OnRaw(sLine), &bReturn);
    if (bReturn) return;

    CMessage Message(sLine);
    Message.SetNetwork(m_pNetwork);

    IRCSOCKMODULECALL(OnRawMessage(Message), &bReturn);
    if (bReturn) return;

    switch (Message.GetType()) {
        case CMessage::Type::Account:
            bReturn = OnAccountMessage(Message);
            break;
        case CMessage::Type::Action:
            bReturn = OnActionMessage(Message);
            break;
        case CMessage::Type::Away:
            bReturn = OnAwayMessage(Message);
            break;
        case CMessage::Type::Capability:
            bReturn = OnCapabilityMessage(Message);
            break;
        case CMessage::Type::ChgHost:
            bReturn = OnChgHostMessage(Message);
            break;
        case CMessage::Type::CTCP:
            bReturn = OnCTCPMessage(Message);
            break;
        case CMessage::Type::Error:
            bReturn = OnErrorMessage(Message);
            break;
        case CMessage::Type::Invite:
            bReturn = OnInviteMessage(Message);
            break;
        case CMessage::Type::Join:
            bReturn = OnJoinMessage(Message);
            break;
        case CMessage::Type::Kick:
            bReturn = OnKickMessage(Message);
            break;
        case CMessage::Type::Mode:
            bReturn = OnModeMessage(Message);
            break;
        case CMessage::Type::Nick:
            bReturn = OnNickMessage(Message);
            break;
        case CMessage::Type::Notice:
            bReturn = OnNoticeMessage(Message);
            break;
        case CMessage::Type::Numeric:
            bReturn = OnNumericMessage(Message);
            break;
        case CMessage::Type::Part:
            bReturn = OnPartMessage(Message);
            break;
        case CMessage::Type::Ping:
            bReturn = OnPingMessage(Message);
            break;
        case CMessage::Type::Pong:
            bReturn = OnPongMessage(Message);
            break;
        case CMessage::Type::Quit:
            bReturn = OnQuitMessage(Message);
            break;
        case CMessage::Type::TagMsg:
            bReturn = OnTagMessage(Message);
            break;
        case CMessage::Type::Text:
            bReturn = OnTextMessage(Message);
            break;
        case CMessage::Type::Topic:
            bReturn = OnTopicMessage(Message);
            break;
        case CMessage::Type::Wallops:
            bReturn = OnWallopsMessage(Message);
            break;
        default:
            break;
    }
    if (bReturn) return;

    m_pNetwork->PutUser(Message);
}

void CIRCSock::SendNextCap() {
    if (m_uCapPaused) {
        return;
    }

    if (!m_ssPendingCaps.empty()) {
        CString sCaps = std::move(*m_ssPendingCaps.begin());
        m_ssPendingCaps.erase(m_ssPendingCaps.begin());
        while (!m_ssPendingCaps.empty()) {
            const CString& sNext = *m_ssPendingCaps.begin();
            // Old version of cap spec allowed NAK to only contain first 100
            // symbols of the REQ message.
            // Alternatively, instead of parsing NAK we could remember the last
            // REQ sent, but this is simpler, as we need the splitting logic anyway.
            // TODO: lift this limit to something more reasonable, e.g. 400
            if (sCaps.length() + sNext.length() > 98) {
                break;
            }
            sCaps += " " + sNext;
            m_ssPendingCaps.erase(m_ssPendingCaps.begin());
        }
        PutIRC("CAP REQ :" + sCaps);
        return;
    }

    if (!m_ssPendingCapsPhase2.empty()) {
        // Those which NAKed in first phase, try them again, one by one,
        // to know which of them failed.
        CString sCap = std::move(*m_ssPendingCapsPhase2.begin());
        m_ssPendingCapsPhase2.erase(m_ssPendingCapsPhase2.begin());
        PutIRC("CAP REQ :" + sCap);
        return;
    }

    // We already got all needed ACK/NAK replies.
    if (!m_bAuthed) {
        PutIRC("CAP END");
    }
}

void CIRCSock::PauseCap() { ++m_uCapPaused; }

void CIRCSock::ResumeCap() {
    --m_uCapPaused;
    SendNextCap();
}

bool CIRCSock::OnServerCapAvailable(const CString& sCap, const CString& sValue) {
    bool bResult = false;
    IRCSOCKMODULECALL(OnServerCapAvailable(sCap, sValue), &bResult);
    return bResult;
}

// #124: OnChanMsg(): nick doesn't have perms
static void FixupChanNick(CNick& Nick, CChan* pChan) {
    // A channel nick has up-to-date channel perms, but might be
    // lacking (usernames-in-host) the associated ident & host.
    // An incoming message, on the other hand, has normally a full
    // nick!ident@host prefix. Sync the two so that channel nicks
    // get the potentially missing piece of info and module hooks
    // get the perms.
    CNick* pChanNick = pChan->FindNick(Nick.GetNick());
    if (pChanNick) {
        if (!Nick.GetIdent().empty()) {
            pChanNick->SetIdent(Nick.GetIdent());
        }
        if (!Nick.GetHost().empty()) {
            pChanNick->SetHost(Nick.GetHost());
        }
        Nick.Clone(*pChanNick);
    }
}

bool CIRCSock::OnAccountMessage(CMessage& Message) {
    // TODO: IRCSOCKMODULECALL(OnAccountMessage(Message)) ?
    return false;
}

bool CIRCSock::OnActionMessage(CActionMessage& Message) {
    bool bResult = false;
    CChan* pChan = nullptr;
    CString sTarget = Message.GetTarget();
    if (sTarget.Equals(GetNick())) {
        IRCSOCKMODULECALL(OnPrivCTCPMessage(Message), &bResult);
        if (bResult) return true;
        IRCSOCKMODULECALL(OnPrivActionMessage(Message), &bResult);
        if (bResult) return true;

        if (!m_pNetwork->IsUserOnline() ||
            !m_pNetwork->GetUser()->AutoClearQueryBuffer()) {
            const CNick& Nick = Message.GetNick();
            CQuery* pQuery = m_pNetwork->AddQuery(Nick.GetNick());
            if (pQuery) {
                CActionMessage Format;
                Format.Clone(Message);
                Format.SetNick(_NAMEDFMT(Nick.GetNickMask()));
                Format.SetTarget("{target}");
                Format.SetText("{text}");
                pQuery->AddBuffer(Format, Message.GetText());
            }
        }
    } else {
        pChan = m_pNetwork->FindChan(sTarget);
        if (pChan) {
            Message.SetChan(pChan);
            FixupChanNick(Message.GetNick(), pChan);
            IRCSOCKMODULECALL(OnChanCTCPMessage(Message), &bResult);
            if (bResult) return true;
            IRCSOCKMODULECALL(OnChanActionMessage(Message), &bResult);
            if (bResult) return true;

            if (!pChan->AutoClearChanBuffer() || !m_pNetwork->IsUserOnline() ||
                pChan->IsDetached()) {
                CActionMessage Format;
                Format.Clone(Message);
                Format.SetNick(_NAMEDFMT(Message.GetNick().GetNickMask()));
                Format.SetTarget(_NAMEDFMT(Message.GetTarget()));
                Format.SetText("{text}");
                pChan->AddBuffer(Format, Message.GetText());
            }
        }
    }

    return (pChan && pChan->IsDetached());
}

bool CIRCSock::OnAwayMessage(CMessage& Message) {
    // TODO: IRCSOCKMODULECALL(OnAwayMessage(Message)) ?
    return false;
}

bool CIRCSock::OnCapabilityMessage(CMessage& Message) {
    // The first parameter is most likely "*". No idea why, the
    // CAP spec don't mention this, but all implementations
    // I've seen add this extra asterisk
    CString sSubCmd = Message.GetParam(1);

    // If the caplist of a reply is too long, it's split
    // into multiple replies. A "*" is prepended to show
    // that the list was split into multiple replies.
    // This is useful mainly for LS. For ACK and NAK
    // replies, there's no real need for this, because
    // we request only 1 capability per line.
    // If we will need to support broken servers or will
    // send several requests per line, need to delay ACK
    // actions until all ACK lines are received and
    // to recognize past request of NAK by 100 chars
    // of this reply.
    // As for LS, we shouldn't don't send END after receiving first line,
    // because interesting caps can be on next line.
    CString sArgs;
    bool bSendNext = true;
    if (Message.GetParam(2) == "*") {
        bSendNext = false;
        sArgs = Message.GetParam(3);
    } else {
        sArgs = Message.GetParam(2);
    }

    static std::map<CString, std::function<void(CIRCSock * pSock, bool bVal)>>
        mSupportedCaps = {
            {"multi-prefix",
             [](CIRCSock* pSock, bool bVal) { pSock->m_bNamesx = bVal; }},
            {"userhost-in-names",
             [](CIRCSock* pSock, bool bVal) { pSock->m_bUHNames = bVal; }},
            {"cap-notify", [](CIRCSock* pSock, bool bVal) {}},
            {"invite-notify", [](CIRCSock* pSock, bool bVal) {}},
            {"server-time",
             [](CIRCSock* pSock, bool bVal) { pSock->m_bServerTime = bVal; }},
            {"znc.in/server-time-iso",
             [](CIRCSock* pSock, bool bVal) { pSock->m_bServerTime = bVal; }},
            {"chghost", [](CIRCSock* pSock, bool) {}},
            {"message-tags", [](CIRCSock* pSock,
                                bool bVal) { pSock->m_bMessageTagCap = bVal; }},
        };

    auto RemoveCap = [&](const CString& sCap) {
        IRCSOCKMODULECALL(OnServerCapResult(sCap, false), NOTHING);
        auto it = mSupportedCaps.find(sCap);
        if (it != mSupportedCaps.end()) {
            it->second(this, false);
        }
        m_ssAcceptedCaps.erase(sCap);
        m_ssPendingCaps.erase(sCap);
    };

    if (sSubCmd == "LS" || sSubCmd == "NEW") {
        VCString vsTokens;
        sArgs.Split(" ", vsTokens, false);

        for (const CString& sToken : vsTokens) {
            CString sCap, sValue;
            int eq = sToken.find('=');
            if (eq == std::string::npos) {
                sCap = sToken;
            } else {
                sCap = sToken.substr(0, eq);
                sValue = sToken.substr(eq + 1);
            }
            if (CZNC::Get().GetServerCapBlacklist().count(sCap)) {
                continue;
            }
            m_msCapLsValues[sCap] = sValue;
            if (OnServerCapAvailable(sCap, sValue) || mSupportedCaps.count(sCap)) {
                m_ssPendingCaps.insert(sCap);
            }
        }
    } else if (sSubCmd == "ACK") {
        VCString vsCaps;
        sArgs.Split(" ", vsCaps, false);
        for (CString& sCap : vsCaps) {
            IRCSOCKMODULECALL(OnServerCapResult(sCap, true), NOTHING);
            auto it = mSupportedCaps.find(sCap);
            if (it != mSupportedCaps.end()) {
                it->second(this, true);
            }
            m_ssAcceptedCaps.insert(std::move(sCap));
        }
    } else if (sSubCmd == "NAK") {
        // This should work because there's no [known]
        // capability with length of name more than 100 characters.
        VCString vsCaps;
        sArgs.Split(" ", vsCaps, false);
        if (vsCaps.size() == 1) {
            RemoveCap(sArgs);
        } else {
            // Retry them one by one
            for (CString& sCap : vsCaps) {
                m_ssPendingCapsPhase2.insert(std::move(sCap));
            }
        }
    } else if (sSubCmd == "DEL") {
        VCString vsTokens;
        sArgs.Split(" ", vsTokens, false);

        for (const CString& sCap : vsTokens) {
            RemoveCap(sCap);
            m_msCapLsValues.erase(sCap);
        }
    }

    if (bSendNext) {
        SendNextCap();
    }
    // Don't forward any CAP stuff to the client
    return true;
}

bool CIRCSock::OnCTCPMessage(CCTCPMessage& Message) {
    bool bResult = false;
    CChan* pChan = nullptr;
    CString sTarget = Message.GetTarget();
    if (sTarget.Equals(GetNick())) {
        if (Message.IsReply()) {
            IRCSOCKMODULECALL(OnCTCPReplyMessage(Message), &bResult);
            return bResult;
        } else {
            IRCSOCKMODULECALL(OnPrivCTCPMessage(Message), &bResult);
            if (bResult) return true;
        }
    } else {
        pChan = m_pNetwork->FindChan(sTarget);
        if (pChan) {
            Message.SetChan(pChan);
            FixupChanNick(Message.GetNick(), pChan);
            if (Message.IsReply()) {
                IRCSOCKMODULECALL(OnCTCPReplyMessage(Message), &bResult);
                return bResult;
            } else {
                IRCSOCKMODULECALL(OnChanCTCPMessage(Message), &bResult);
            }
            if (bResult) return true;
        }
    }

    const CNick& Nick = Message.GetNick();
    const CString& sMessage = Message.GetText();
    const MCString& mssCTCPReplies = m_pNetwork->GetUser()->GetCTCPReplies();
    CString sQuery = sMessage.Token(0).AsUpper();
    MCString::const_iterator it = mssCTCPReplies.find(sQuery);
    bool bHaveReply = false;
    CString sReply;

    if (it != mssCTCPReplies.end()) {
        sReply = m_pNetwork->ExpandString(it->second);
        bHaveReply = true;

        if (sReply.empty()) {
            return true;
        }
    }

    if (!bHaveReply && !m_pNetwork->IsUserAttached()) {
        if (sQuery == "VERSION") {
            sReply = CZNC::GetTag(false);
        } else if (sQuery == "PING") {
            sReply = sMessage.Token(1, true);
        }
    }

    if (!sReply.empty()) {
        unsigned long long now = CUtils::GetMillTime();
        // If the last CTCP is older than m_uCTCPFloodTime, reset the counter
        if (m_lastCTCP + m_uCTCPFloodTime < now) m_uNumCTCP = 0;
        m_lastCTCP = now;
        // If we are over the limit, don't reply to this CTCP
        if (m_uNumCTCP >= m_uCTCPFloodCount) {
            DEBUG("CTCP flood detected - not replying to query");
            return true;
        }
        m_uNumCTCP++;

        PutIRC("NOTICE " + Nick.GetNick() + " :\001" + sQuery + " " + sReply +
               "\001");
        return true;
    }

    return (pChan && pChan->IsDetached());
}

bool CIRCSock::OnChgHostMessage(CChgHostMessage& Message) {
    // The emulation of QUIT+JOIN would be cleaner inside CClient::PutClient()
    // but computation of new modes is difficult enough so that I don't want to
    // repeat it for every client
    //
    // TODO: make CNick store modes (v, o) instead of perm chars (+, @), that
    // would simplify this
    bool bNeedEmulate = false;
    for (CClient* pClient : m_pNetwork->GetClients()) {
        if (pClient->HasChgHost()) {
            pClient->PutClient(Message);
        } else {
            bNeedEmulate = true;
            pClient->PutClient(CMessage(Message.GetNick(), "QUIT",
                                        {"Changing hostname"},
                                        Message.GetTags()));
        }
    }

    CNick NewNick = Message.GetNick();
    NewNick.SetIdent(Message.GetNewIdent());
    NewNick.SetHost(Message.GetNewHost());

    for (CChan* pChan : m_pNetwork->GetChans()) {
        if (CNick* pNick = pChan->FindNick(Message.GetNick().GetNick())) {
            pNick->SetIdent(Message.GetNewIdent());
            pNick->SetHost(Message.GetNewHost());
        }

        if (!bNeedEmulate) continue;
        if (pChan->IsDisabled()) continue;
        if (pChan->IsDetached()) continue;

        if (CNick* pNick = pChan->FindNick(NewNick.GetNick())) {
            VCString vsModeParams = {pChan->GetName(), "+"};
            for (char cPerm : pNick->GetPermStr()) {
                char cMode = GetModeFromPerm(cPerm);
                if (cMode) {
                    vsModeParams[1].append(1, cMode);
                    vsModeParams.push_back(NewNick.GetNick());
                }
            }

            CTargetMessage ModeMsg;
            ModeMsg.SetNick(CNick(":irc.znc.in"));
            ModeMsg.SetTags(Message.GetTags());
            ModeMsg.SetCommand("MODE");
            ModeMsg.SetParams(std::move(vsModeParams));

            for (CClient* pClient : m_pNetwork->GetClients()) {
                if (!pClient->HasChgHost()) {
                    // TODO: send account name and real name too, for
                    // extended-join
                    pClient->PutClient(CMessage(NewNick, "JOIN",
                                                {pChan->GetName()},
                                                Message.GetTags()));
                    if (ModeMsg.GetParams().size() > 2) {
                        pClient->PutClient(ModeMsg);
                    }
                }
            }
        }
    }

    return true;
}

bool CIRCSock::OnErrorMessage(CMessage& Message) {
    // ERROR :Closing Link: nick[24.24.24.24] (Excess Flood)
    CString sError = Message.GetParam(0);
    m_pNetwork->PutStatus(t_f("Error from server: {1}")(sError));
    return true;
}

bool CIRCSock::OnInviteMessage(CInviteMessage& Message) {
    Message.SetChan(GetNetwork()->FindChan(Message.GetChannel()));
    bool bResult = false;
    IRCSOCKMODULECALL(OnInviteMessage(Message), &bResult);
    if (bResult) return true;
    CNick InvitedNick = Message.GetInvitedNick();
    if (InvitedNick.NickEquals(GetNick())) {
        IRCSOCKMODULECALL(OnInvite(Message.GetNick(), Message.GetParam(1)),
                          &bResult);
    }
    return bResult;
}

bool CIRCSock::OnJoinMessage(CJoinMessage& Message) {
    const CNick& Nick = Message.GetNick();
    CString sChan = Message.GetParam(0);
    CChan* pChan = nullptr;

    if (Nick.NickEquals(GetNick())) {
        m_pNetwork->AddChan(sChan, false);
        pChan = m_pNetwork->FindChan(sChan);
        if (pChan) {
            pChan->Enable();
            pChan->SetIsOn(true);
            PutIRC("MODE " + sChan);
        }
    } else {
        pChan = m_pNetwork->FindChan(sChan);
    }

    if (pChan) {
        pChan->AddNick(Nick.GetNickMask());
        Message.SetChan(pChan);
        IRCSOCKMODULECALL(OnJoinMessage(Message), NOTHING);

        if (pChan->IsDetached()) {
            return true;
        }
    }

    return false;
}

bool CIRCSock::OnKickMessage(CKickMessage& Message) {
    CString sChan = Message.GetParam(0);
    CString sKickedNick = Message.GetKickedNick();

    CChan* pChan = m_pNetwork->FindChan(sChan);

    if (pChan) {
        Message.SetChan(pChan);
        IRCSOCKMODULECALL(OnKickMessage(Message), NOTHING);
        // do not remove the nick till after the OnKick call, so modules
        // can do Chan.FindNick or something to get more info.
        pChan->RemNick(sKickedNick);
    }

    if (GetNick().Equals(sKickedNick) && pChan) {
        pChan->SetIsOn(false);

        // Don't try to rejoin!
        pChan->Disable();
    }

    return (pChan && pChan->IsDetached());
}

bool CIRCSock::OnModeMessage(CModeMessage& Message) {
    const CNick& Nick = Message.GetNick();
    CString sTarget = Message.GetTarget();
    VCString vsModes = Message.GetModeParams();
    CString sModes = Message.GetModeList();

    CChan* pChan = m_pNetwork->FindChan(sTarget);
    if (pChan) {
        pChan->ModeChange(sModes, vsModes, &Nick);

        if (pChan->IsDetached()) {
            return true;
        }
    } else if (sTarget == m_Nick.GetNick()) {
        bool bAdd = true;
        /* no module call defined (yet?)
                MODULECALL(OnRawUserMode(*pOpNick, *this, sModeArg, sArgs),
           m_pNetwork->GetUser(), nullptr, );
        */
        for (unsigned int a = 0; a < sModes.size(); a++) {
            const char& cMode = sModes[a];

            if (cMode == '+') {
                bAdd = true;
            } else if (cMode == '-') {
                bAdd = false;
            } else {
                if (bAdd) {
                    m_scUserModes.insert(cMode);
                } else {
                    m_scUserModes.erase(cMode);
                }
            }
        }
    }
    return false;
}

bool CIRCSock::OnNickMessage(CNickMessage& Message) {
    const CNick& Nick = Message.GetNick();
    CString sNewNick = Message.GetNewNick();
    bool bIsVisible = false;

    vector<CChan*> vFoundChans;
    const vector<CChan*>& vChans = m_pNetwork->GetChans();

    for (CChan* pChan : vChans) {
        if (pChan->ChangeNick(Nick.GetNick(), sNewNick)) {
            vFoundChans.push_back(pChan);

            if (!pChan->IsDetached()) {
                bIsVisible = true;
            }
        }
    }

    if (Nick.NickEquals(GetNick())) {
        // We are changing our own nick, the clients always must see this!
        bIsVisible = false;
        SetNick(sNewNick);
        m_pNetwork->PutUser(Message);
    }

    IRCSOCKMODULECALL(OnNickMessage(Message, vFoundChans), NOTHING);

    return !bIsVisible;
}

bool CIRCSock::OnNoticeMessage(CNoticeMessage& Message) {
    CString sTarget = Message.GetTarget();
    bool bResult = false;

    if (sTarget.Equals(GetNick())) {
        IRCSOCKMODULECALL(OnPrivNoticeMessage(Message), &bResult);
        if (bResult) return true;

        if (!m_pNetwork->IsUserOnline()) {
            // If the user is detached, add to the buffer
            CNoticeMessage Format;
            Format.Clone(Message);
            Format.SetNick(CNick(_NAMEDFMT(Message.GetNick().GetNickMask())));
            Format.SetTarget("{target}");
            Format.SetText("{text}");
            m_pNetwork->AddNoticeBuffer(Format, Message.GetText());
        }

        return false;
    } else {
        CChan* pChan = m_pNetwork->FindChan(sTarget);
        if (pChan) {
            Message.SetChan(pChan);
            FixupChanNick(Message.GetNick(), pChan);
            IRCSOCKMODULECALL(OnChanNoticeMessage(Message), &bResult);
            if (bResult) return true;

            if (!pChan->AutoClearChanBuffer() || !m_pNetwork->IsUserOnline() ||
                pChan->IsDetached()) {
                CNoticeMessage Format;
                Format.Clone(Message);
                Format.SetNick(_NAMEDFMT(Message.GetNick().GetNickMask()));
                Format.SetTarget(_NAMEDFMT(Message.GetTarget()));
                Format.SetText("{text}");
                pChan->AddBuffer(Format, Message.GetText());
            }
        }

        return (pChan && pChan->IsDetached());
    }
}

static CMessage BufferMessage(const CNumericMessage& Message) {
    CMessage Format(Message);
    Format.SetNick(CNick(_NAMEDFMT(Message.GetNick().GetHostMask())));
    Format.SetParam(0, "{target}");
    unsigned uParams = Format.GetParams().size();
    for (unsigned int i = 1; i < uParams; ++i) {
        Format.SetParam(i, _NAMEDFMT(Format.GetParam(i)));
    }
    return Format;
}

bool CIRCSock::OnNumericMessage(CNumericMessage& Message) {
    const CString& sCmd = Message.GetCommand();
    CString sServer = Message.GetNick().GetHostMask();
    unsigned int uRaw = Message.GetCode();
    CString sNick = Message.GetParam(0);

    bool bResult = false;
    IRCSOCKMODULECALL(OnNumericMessage(Message), &bResult);
    if (bResult) return true;

    switch (uRaw) {
        case 1: {  // :irc.server.com 001 nick :Welcome to the Internet Relay
            if (m_bAuthed && sServer == "irc.znc.in") {
                // m_bAuthed == true => we already received another 001 => we
                // might be in a traffic loop
                m_pNetwork->PutStatus(t_s(
                    "ZNC seems to be connected to itself, disconnecting..."));
                Quit();
                return true;
            }

            m_pNetwork->SetIRCServer(sServer);
            // Now that we are connected, let nature take its course
            SetTimeout(m_pNetwork->GetUser()->GetNoTrafficTimeout(), TMO_READ);
            PutIRC("WHO " + sNick);

            m_bAuthed = true;

            const vector<CClient*>& vClients = m_pNetwork->GetClients();

            for (CClient* pClient : vClients) {
                CString sClientNick = pClient->GetNick(false);

                if (!sClientNick.Equals(sNick)) {
                    // If they connected with a nick that doesn't match the one
                    // we got on irc, then we need to update them
                    pClient->PutClient(":" + sClientNick + "!" +
                                       m_Nick.GetIdent() + "@" +
                                       m_Nick.GetHost() + " NICK :" + sNick);
                }
            }

            SetNick(sNick);

            m_pNetwork->PutStatus("Connected!");
            IRCSOCKMODULECALL(OnIRCConnected(), NOTHING);

            m_pNetwork->ClearRawBuffer();
            m_pNetwork->AddRawBuffer(BufferMessage(Message));

            m_pNetwork->IRCConnected();

            break;
        }
        case 5:
            ParseISupport(Message);
            m_pNetwork->UpdateExactRawBuffer(BufferMessage(Message));
            break;
        case 10: {  // :irc.server.com 010 nick <hostname> <port> :<info>
            CString sHost = Message.GetParam(1);
            CString sPort = Message.GetParam(2);
            CString sInfo = Message.GetParam(3);
            m_pNetwork->PutStatus(
                t_f("Server {1} redirects us to {2}:{3} with reason: {4}")(
                    m_pNetwork->GetCurrentServer()->GetString(false), sHost,
                    sPort, sInfo));
            m_pNetwork->PutStatus(
                t_s("Perhaps you want to add it as a new server."));
            // Don't send server redirects to the client
            return true;
        }
        case 2:
        case 3:
        case 4:
        case 250:  // highest connection count
        case 251:  // user count
        case 252:  // oper count
        case 254:  // channel count
        case 255:  // client count
        case 265:  // local users
        case 266:  // global users
            m_pNetwork->UpdateRawBuffer(sCmd, BufferMessage(Message));
            break;
        case 305:
            m_pNetwork->SetIRCAway(false);
            break;
        case 306:
            m_pNetwork->SetIRCAway(true);
            break;
        case 324: {  // MODE
            // :irc.server.com 324 nick #chan +nstk key
            CChan* pChan = m_pNetwork->FindChan(Message.GetParam(1));

            if (pChan) {
                pChan->SetModes(Message.GetParam(2), Message.GetParamsSplit(3));

                // We don't SetModeKnown(true) here,
                // because a 329 will follow
                if (!pChan->IsModeKnown()) {
                    // When we JOIN, we send a MODE
                    // request. This makes sure the
                    // reply isn't forwarded.
                    return true;
                }
                if (pChan->IsDetached()) {
                    return true;
                }
            }
        } break;
        case 329: {
            // :irc.server.com 329 nick #chan 1234567890
            CChan* pChan = m_pNetwork->FindChan(Message.GetParam(1));

            if (pChan) {
                unsigned long ulDate = Message.GetParam(2).ToULong();
                pChan->SetCreationDate(ulDate);

                if (!pChan->IsModeKnown()) {
                    pChan->SetModeKnown(true);
                    // When we JOIN, we send a MODE
                    // request. This makes sure the
                    // reply isn't forwarded.
                    return true;
                }
                if (pChan->IsDetached()) {
                    return true;
                }
            }
        } break;
        case 331: {
            // :irc.server.com 331 yournick #chan :No topic is set.
            CChan* pChan = m_pNetwork->FindChan(Message.GetParam(1));

            if (pChan) {
                pChan->SetTopic("");
                if (pChan->IsDetached()) {
                    return true;
                }
            }

            break;
        }
        case 332: {
            // :irc.server.com 332 yournick #chan :This is a topic
            CChan* pChan = m_pNetwork->FindChan(Message.GetParam(1));

            if (pChan) {
                CString sTopic = Message.GetParam(2);
                pChan->SetTopic(sTopic);
                if (pChan->IsDetached()) {
                    return true;
                }
            }

            break;
        }
        case 333: {
            // :irc.server.com 333 yournick #chan setternick 1112320796
            CChan* pChan = m_pNetwork->FindChan(Message.GetParam(1));

            if (pChan) {
                sNick = Message.GetParam(2);
                unsigned long ulDate = Message.GetParam(3).ToULong();

                pChan->SetTopicOwner(sNick);
                pChan->SetTopicDate(ulDate);

                if (pChan->IsDetached()) {
                    return true;
                }
            }

            break;
        }
        case 352: {  // WHO
            // :irc.yourserver.com 352 yournick #chan ident theirhost.com irc.theirserver.com theirnick H :0 Real Name
            sNick = Message.GetParam(5);
            CString sChan = Message.GetParam(1);
            CString sIdent = Message.GetParam(2);
            CString sHost = Message.GetParam(3);

            if (sNick.Equals(GetNick())) {
                m_Nick.SetIdent(sIdent);
                m_Nick.SetHost(sHost);
            }

            m_pNetwork->SetIRCNick(m_Nick);
            m_pNetwork->SetIRCServer(sServer);

            // A nick can only have one ident and hostname. Yes, you can query
            // this information per-channel, but it is still global. For
            // example, if the client supports UHNAMES, but the IRC server does
            // not, then AFAIR "passive snooping of WHO replies" is the only way
            // that ZNC can figure out the ident and host for the UHNAMES
            // replies.
            const vector<CChan*>& vChans = m_pNetwork->GetChans();

            for (CChan* pChan : vChans) {
                pChan->OnWho(sNick, sIdent, sHost);
            }

            CChan* pChan = m_pNetwork->FindChan(sChan);
            if (pChan && pChan->IsDetached()) {
                return true;
            }

            break;
        }
        case 353: {  // NAMES
            // :irc.server.com 353 nick @ #chan :nick1 nick2
            // Todo: allow for non @+= server msgs
            CChan* pChan = m_pNetwork->FindChan(Message.GetParam(2));
            // If we don't know that channel, some client might have
            // requested a /names for it and we really should forward this.
            if (pChan) {
                CString sNicks = Message.GetParam(3);
                pChan->AddNicks(sNicks);
                if (pChan->IsDetached()) {
                    return true;
                }
            }

            break;
        }
        case 366: {  // end of names list
            // :irc.server.com 366 nick #chan :End of /NAMES list.
            CChan* pChan = m_pNetwork->FindChan(Message.GetParam(1));

            if (pChan) {
                if (pChan->IsOn()) {
                    // If we are the only one in the chan, set our default modes
                    if (pChan->GetNickCount() == 1) {
                        CString sModes = pChan->GetDefaultModes();

                        if (sModes.empty()) {
                            sModes =
                                m_pNetwork->GetUser()->GetDefaultChanModes();
                        }

                        if (!sModes.empty()) {
                            PutIRC("MODE " + pChan->GetName() + " " + sModes);
                        }
                    }
                }
                if (pChan->IsDetached()) {
                    // don't put it to clients
                    return true;
                }
            }

            break;
        }
        case 375:  // begin motd
        case 422:  // MOTD File is missing
            if (m_pNetwork->GetIRCServer().Equals(sServer)) {
                m_pNetwork->ClearMotdBuffer();
            }
        case 372:  // motd
        case 376:  // end motd
            if (m_pNetwork->GetIRCServer().Equals(sServer)) {
                m_pNetwork->AddMotdBuffer(BufferMessage(Message));
            }
            break;
        case 437:
            // :irc.server.net 437 * badnick :Nick/channel is temporarily unavailable
            // :irc.server.net 437 mynick badnick :Nick/channel is temporarily unavailable
            // :irc.server.net 437 mynick badnick :Cannot change nickname while banned on channel
            if (m_pNetwork->IsChan(Message.GetParam(1)) || sNick != "*") break;
        case 432:
        // :irc.server.com 432 * nick :Erroneous Nickname: Illegal chars
        case 433: {
            CString sBadNick = Message.GetParam(1);

            if (!m_bAuthed) {
                SendAltNick(sBadNick);
                return true;
            }
            break;
        }
        case 451:
            // :irc.server.com 451 CAP :You have not registered
            // Servers that don't support CAP will give us this error, don't send
            // it to the client
            if (sNick.Equals("CAP")) return true;
        case 470: {
            // :irc.unreal.net 470 mynick [Link] #chan1 has become full, so you are automatically being transferred to the linked channel #chan2
            // :mccaffrey.freenode.net 470 mynick #electronics ##electronics :Forwarding to another channel

            // freenode style numeric
            CChan* pChan = m_pNetwork->FindChan(Message.GetParam(1));
            if (!pChan) {
                // unreal style numeric
                pChan = m_pNetwork->FindChan(Message.GetParam(2));
            }
            if (pChan) {
                pChan->Disable();
                m_pNetwork->PutStatus(
                    t_f("Channel {1} is linked to another channel and was thus "
                        "disabled.")(pChan->GetName()));
            }
            break;
        }
        case 670:
            // :hydra.sector5d.org 670 kylef :STARTTLS successful, go ahead with TLS handshake
            //
            // 670 is a response to `STARTTLS` telling the client to switch to
            // TLS
            if (!GetSSL()) {
                StartTLS();
                m_pNetwork->PutStatus(t_s("Switched to SSL (STARTTLS)"));
            }

            return true;
    }

    return false;
}

bool CIRCSock::OnPartMessage(CPartMessage& Message) {
    const CNick& Nick = Message.GetNick();
    CString sChan = Message.GetTarget();

    CChan* pChan = m_pNetwork->FindChan(sChan);
    bool bDetached = false;
    if (pChan) {
        pChan->RemNick(Nick.GetNick());
        Message.SetChan(pChan);
        IRCSOCKMODULECALL(OnPartMessage(Message), NOTHING);

        if (pChan->IsDetached()) bDetached = true;
    }

    if (Nick.NickEquals(GetNick())) {
        m_pNetwork->DelChan(sChan);
    }

    /*
     * We use this boolean because
     * m_pNetwork->DelChan() will delete this channel
     * and thus we would dereference an
     * already-freed pointer!
     */
    return bDetached;
}

bool CIRCSock::OnPingMessage(CMessage& Message) {
    // Generate a reply and don't forward this to any user,
    // we don't want any PING forwarded
    PutIRCQuick("PONG " + Message.GetParam(0));
    return true;
}

bool CIRCSock::OnPongMessage(CMessage& Message) {
    // Block PONGs, we already responded to the pings
    return true;
}

bool CIRCSock::OnQuitMessage(CQuitMessage& Message) {
    const CNick& Nick = Message.GetNick();
    bool bIsVisible = false;

    if (Nick.NickEquals(GetNick())) {
        m_pNetwork->PutStatus(t_f("You quit: {1}")(Message.GetReason()));
        // We don't call module hooks and we don't
        // forward this quit to clients (Some clients
        // disconnect if they receive such a QUIT)
        return true;
    }

    vector<CChan*> vFoundChans;
    const vector<CChan*>& vChans = m_pNetwork->GetChans();

    for (CChan* pChan : vChans) {
        if (pChan->RemNick(Nick.GetNick())) {
            vFoundChans.push_back(pChan);

            if (!pChan->IsDetached()) {
                bIsVisible = true;
            }
        }
    }

    IRCSOCKMODULECALL(OnQuitMessage(Message, vFoundChans), NOTHING);

    return !bIsVisible;
}

bool CIRCSock::OnTagMessage(CTargetMessage& Message) {
    bool bResult = false;
    CChan* pChan = nullptr;
    CString sTarget = Message.GetTarget();

    if (sTarget.Equals(GetNick())) {
        IRCSOCKMODULECALL(OnPrivTagMessage(Message), &bResult);
        if (bResult) return true;

        if (!m_pNetwork->IsUserOnline() ||
            !m_pNetwork->GetUser()->AutoClearQueryBuffer()) {
            const CNick& Nick = Message.GetNick();
            CQuery* pQuery = m_pNetwork->AddQuery(Nick.GetNick());
            if (pQuery) {
                CTargetMessage Format;
                Format.Clone(Message);
                Format.SetNick(_NAMEDFMT(Nick.GetNickMask()));
                Format.SetTarget("{target}");
                pQuery->AddBuffer(Format);
            }
        }
    } else {
        pChan = m_pNetwork->FindChan(sTarget);
        if (pChan) {
            Message.SetChan(pChan);
            FixupChanNick(Message.GetNick(), pChan);
            IRCSOCKMODULECALL(OnChanTagMessage(Message), &bResult);
            if (bResult) return true;

            if (!pChan->AutoClearChanBuffer() || !m_pNetwork->IsUserOnline() ||
                pChan->IsDetached()) {
                CTargetMessage Format;
                Format.Clone(Message);
                Format.SetNick(_NAMEDFMT(Message.GetNick().GetNickMask()));
                Format.SetTarget(_NAMEDFMT(Message.GetTarget()));
                pChan->AddBuffer(Format);
            }
        }
    }

    return (pChan && pChan->IsDetached());
}

bool CIRCSock::OnTextMessage(CTextMessage& Message) {
    bool bResult = false;
    CChan* pChan = nullptr;
    CString sTarget = Message.GetTarget();

    if (sTarget.Equals(GetNick())) {
        IRCSOCKMODULECALL(OnPrivTextMessage(Message), &bResult);
        if (bResult) return true;

        if (!m_pNetwork->IsUserOnline() ||
            !m_pNetwork->GetUser()->AutoClearQueryBuffer()) {
            const CNick& Nick = Message.GetNick();
            CQuery* pQuery = m_pNetwork->AddQuery(Nick.GetNick());
            if (pQuery) {
                CTextMessage Format;
                Format.Clone(Message);
                Format.SetNick(_NAMEDFMT(Nick.GetNickMask()));
                Format.SetTarget("{target}");
                Format.SetText("{text}");
                pQuery->AddBuffer(Format, Message.GetText());
            }
        }
    } else {
        pChan = m_pNetwork->FindChan(sTarget);
        if (pChan) {
            Message.SetChan(pChan);
            FixupChanNick(Message.GetNick(), pChan);
            IRCSOCKMODULECALL(OnChanTextMessage(Message), &bResult);
            if (bResult) return true;

            if (!pChan->AutoClearChanBuffer() || !m_pNetwork->IsUserOnline() ||
                pChan->IsDetached()) {
                CTextMessage Format;
                Format.Clone(Message);
                Format.SetNick(_NAMEDFMT(Message.GetNick().GetNickMask()));
                Format.SetTarget(_NAMEDFMT(Message.GetTarget()));
                Format.SetText("{text}");
                pChan->AddBuffer(Format, Message.GetText());
            }
        }
    }

    return (pChan && pChan->IsDetached());
}

bool CIRCSock::OnTopicMessage(CTopicMessage& Message) {
    const CNick& Nick = Message.GetNick();
    CChan* pChan = m_pNetwork->FindChan(Message.GetParam(0));

    if (pChan) {
        Message.SetChan(pChan);
        bool bReturn = false;
        IRCSOCKMODULECALL(OnTopicMessage(Message), &bReturn);
        if (bReturn) return true;

        pChan->SetTopicOwner(Nick.GetNick());
        pChan->SetTopicDate((unsigned long)time(nullptr));
        pChan->SetTopic(Message.GetTopic());
    }

    return (pChan && pChan->IsDetached());
}

bool CIRCSock::OnWallopsMessage(CMessage& Message) {
    // :blub!dummy@rox-8DBEFE92 WALLOPS :this is a test
    CString sMsg = Message.GetParam(0);

    if (!m_pNetwork->IsUserOnline()) {
        CMessage Format(Message);
        Format.SetNick(CNick(_NAMEDFMT(Message.GetNick().GetHostMask())));
        Format.SetParam(0, "{text}");
        m_pNetwork->AddNoticeBuffer(Format, sMsg);
    }
    return false;
}

void CIRCSock::PutIRC(const CString& sLine) {
    PutIRC(CMessage(sLine));
}

void CIRCSock::PutIRC(const CMessage& Message) {
    // Only print if the line won't get sent immediately (same condition as in
    // TrySend()!)
    if (m_bFloodProtection && m_iSendsAllowed <= 0) {
        DEBUG("(" << m_pNetwork->GetUser()->GetUsername() << "/"
                  << m_pNetwork->GetName() << ") ZNC -> IRC ["
                  << CDebug::Filter(Message.ToString()) << "] (queued)");
    }
    m_vSendQueue.push_back(Message);
    TrySend();
}

void CIRCSock::PutIRCQuick(const CString& sLine) {
    // Only print if the line won't get sent immediately (same condition as in
    // TrySend()!)
    if (m_bFloodProtection && m_iSendsAllowed <= 0) {
        DEBUG("(" << m_pNetwork->GetUser()->GetUsername() << "/"
                  << m_pNetwork->GetName() << ") ZNC -> IRC ["
                  << CDebug::Filter(sLine) << "] (queued to front)");
    }
    m_vSendQueue.emplace_front(sLine);
    TrySend();
}

void CIRCSock::TrySend() {
    // This condition must be the same as in PutIRC() and PutIRCQuick()!
    while (!m_vSendQueue.empty() &&
           (!m_bFloodProtection || m_iSendsAllowed > 0)) {
        m_iSendsAllowed--;
        CMessage& Message = m_vSendQueue.front();

        if (!m_bMessageTagCap) {
            MCString mssTags;
            for (const auto& it : Message.GetTags()) {
                if (IsTagEnabled(it.first)) {
                    mssTags[it.first] = it.second;
                }
            }
            Message.SetTags(mssTags);
        }
        Message.SetNetwork(m_pNetwork);

        bool bSkip = false;
        IRCSOCKMODULECALL(OnSendToIRCMessage(Message), &bSkip);

        if (!bSkip) {
            PutIRCRaw(Message.ToString());
        }
        m_vSendQueue.pop_front();

        if (m_vSendQueue.size() * m_fFloodRate > 600) {
            unsigned long long now = CUtils::GetMillTime();
            // Warn no more often than once every 2 minutes
            if (now > m_lastFloodWarned + 2 * 60'000) {
                m_lastFloodWarned = now;
                this->GetNetwork()->PutStatus(
                    t_f("Warning: flood protection is delaying your messages "
                        "by {1} seconds")(m_vSendQueue.size() * m_fFloodRate));
            }
        }
    }
}

void CIRCSock::PutIRCRaw(const CString& sLine) {
    CString sCopy = sLine;
    bool bSkip = false;
    IRCSOCKMODULECALL(OnSendToIRC(sCopy), &bSkip);
    if (!bSkip) {
        DEBUG("(" << m_pNetwork->GetUser()->GetUsername() << "/"
                  << m_pNetwork->GetName() << ") ZNC -> IRC ["
                  << CDebug::Filter(sCopy) << "]");
        Write(sCopy + "\r\n");
    }
}

void CIRCSock::SetNick(const CString& sNick) {
    m_Nick.SetNick(sNick);
    m_pNetwork->SetIRCNick(m_Nick);
}

void CIRCSock::Connected() {
    DEBUG(GetSockName() << " == Connected()");

    CString sPass = m_sPass;
    CString sNick = m_pNetwork->GetNick();
    CString sIdent = m_pNetwork->GetIdent();
    CString sRealName = m_pNetwork->GetRealName();

    bool bReturn = false;
    IRCSOCKMODULECALL(OnIRCRegistration(sPass, sNick, sIdent, sRealName),
                      &bReturn);
    if (bReturn) return;

    PutIRC("CAP LS 302");

    if (!sPass.empty()) {
        PutIRC(CMessage(CNick(), "PASS", {sPass}));
    }

    PutIRC("NICK " + sNick);
    PutIRC("USER " + sIdent + " \"" + sIdent + "\" \"" + sIdent + "\" :" +
           sRealName);

    // SendAltNick() needs this
    m_Nick.SetNick(sNick);
}

void CIRCSock::Disconnected() {
    IRCSOCKMODULECALL(OnIRCDisconnected(), NOTHING);

    DEBUG(GetSockName() << " == Disconnected()");
    if (!m_pNetwork->GetUser()->IsBeingDeleted() &&
        m_pNetwork->GetIRCConnectEnabled() &&
        m_pNetwork->GetServers().size() != 0) {
        m_pNetwork->PutStatus(t_s("Disconnected from IRC. Reconnecting..."));
    }
    m_pNetwork->ClearRawBuffer();
    m_pNetwork->ClearMotdBuffer();

    ResetChans();

    // send a "reset user modes" cmd to the client.
    // otherwise, on reconnect, it might think it still
    // had user modes that it actually doesn't have.
    CString sUserMode;
    for (char cMode : m_scUserModes) {
        sUserMode += cMode;
    }
    if (!sUserMode.empty()) {
        m_pNetwork->PutUser(":" + m_pNetwork->GetIRCNick().GetNickMask() +
                            " MODE " + m_pNetwork->GetIRCNick().GetNick() +
                            " :-" + sUserMode);
    }

    // also clear the user modes in our space:
    m_scUserModes.clear();
}

void CIRCSock::SockError(int iErrno, const CString& sDescription) {
    CString sError = sDescription;

    DEBUG(GetSockName() << " == SockError(" << iErrno << " " << sError << ")");
    if (!m_pNetwork->GetUser()->IsBeingDeleted()) {
        if (GetConState() != CST_OK) {
            m_pNetwork->PutStatus(
                t_f("Cannot connect to IRC ({1}). Retrying...")(sError));
        } else {
            m_pNetwork->PutStatus(
                t_f("Disconnected from IRC ({1}). Reconnecting...")(sError));
        }
    }
    for (const CString& s : m_vsSSLError) {
        m_pNetwork->PutStatus(s);
    }
    m_pNetwork->ClearRawBuffer();
    m_pNetwork->ClearMotdBuffer();

    ResetChans();
    m_scUserModes.clear();
}

#ifdef HAVE_LIBSSL
void CIRCSock::SSLCertError(X509* pCert) {
    BIO* mem = BIO_new(BIO_s_mem());
    X509_print(mem, pCert);
    char* pCertStr = nullptr;
    long iLen = BIO_get_mem_data(mem, &pCertStr);
    CString sCert(pCertStr, iLen);
    BIO_free(mem);

    VCString vsCert;
    sCert.Split("\n", vsCert);
    for (const CString& s : vsCert) {
        // It shouldn't contain any bad characters, but let's be
        // safe...
        m_vsSSLError.push_back("|" + s.Escape_n(CString::EDEBUG));
    }
    CString sSHA1;
    if (GetPeerFingerprint(sSHA1))
        m_vsSSLError.push_back(
            "SHA1: " + sSHA1.Escape_n(CString::EHEXCOLON, CString::EHEXCOLON));
    CString sSHA256 = GetSSLPeerFingerprint(pCert);
    m_vsSSLError.push_back("SHA-256: " + sSHA256);
    m_vsSSLError.push_back(
        t_f("If you trust this certificate, do /znc "
            "AddTrustedServerFingerprint {1}")(sSHA256));
}
#endif

void CIRCSock::Timeout() {
    DEBUG(GetSockName() << " == Timeout()");
    if (!m_pNetwork->GetUser()->IsBeingDeleted()) {
        m_pNetwork->PutStatus(
            t_s("IRC connection timed out.  Reconnecting..."));
    }
    m_pNetwork->ClearRawBuffer();
    m_pNetwork->ClearMotdBuffer();

    ResetChans();
    m_scUserModes.clear();
}

void CIRCSock::ConnectionRefused() {
    DEBUG(GetSockName() << " == ConnectionRefused()");
    if (!m_pNetwork->GetUser()->IsBeingDeleted()) {
        m_pNetwork->PutStatus(t_s("Connection Refused.  Reconnecting..."));
    }
    m_pNetwork->ClearRawBuffer();
    m_pNetwork->ClearMotdBuffer();
}

void CIRCSock::ReachedMaxBuffer() {
    DEBUG(GetSockName() << " == ReachedMaxBuffer()");
    m_pNetwork->PutStatus(t_s("Received a too long line from the IRC server!"));
    Quit();
}

void CIRCSock::ParseISupport(const CMessage& Message) {
    const VCString vsParams = Message.GetParams();

    for (size_t i = 1; i + 1 < vsParams.size(); ++i) {
        const CString& sParam = vsParams[i];
        CString sName = sParam.Token(0, false, "=");
        CString sValue = sParam.Token(1, true, "=");

        if (0 < sName.length() && ':' == sName[0]) {
            break;
        }

        m_mISupport[sName] = sValue;

        if (sName.Equals("PREFIX")) {
            CString sPrefixes = sValue.Token(1, false, ")");
            CString sPermModes = sValue.Token(0, false, ")");
            sPermModes.TrimLeft("(");

            if (!sPrefixes.empty() && sPermModes.size() == sPrefixes.size()) {
                m_sPerms = sPrefixes;
                m_sPermModes = sPermModes;
            }
        } else if (sName.Equals("CHANTYPES")) {
            m_pNetwork->SetChanPrefixes(sValue);
        } else if (sName.Equals("NICKLEN")) {
            unsigned int uMax = sValue.ToUInt();

            if (uMax) {
                m_uMaxNickLen = uMax;
            }
        } else if (sName.Equals("CHANMODES")) {
            if (!sValue.empty()) {
                m_mceChanModes.clear();

                for (unsigned int a = 0; a < 4; a++) {
                    CString sModes = sValue.Token(a, false, ",");

                    for (unsigned int b = 0; b < sModes.size(); b++) {
                        m_mceChanModes[sModes[b]] = (EChanModeArgs)a;
                    }
                }
            }
        } else if (sName.Equals("NAMESX")) {
            if (m_bNamesx) continue;
            m_bNamesx = true;
            PutIRC("PROTOCTL NAMESX");
        } else if (sName.Equals("UHNAMES")) {
            if (m_bUHNames) continue;
            m_bUHNames = true;
            PutIRC("PROTOCTL UHNAMES");
        }
    }
}

CString CIRCSock::GetISupport(const CString& sKey,
                              const CString& sDefault) const {
    MCString::const_iterator i = m_mISupport.find(sKey.AsUpper());
    if (i == m_mISupport.end()) {
        return sDefault;
    } else {
        return i->second;
    }
}

CString CIRCSock::GetCapLsValue(const CString& sKey,
                                const CString& sDefault) const {
    MCString::const_iterator i = m_msCapLsValues.find(sKey);
    if (i == m_msCapLsValues.end()) {
        return sDefault;
    } else {
        return i->second;
    }
}

void CIRCSock::SendAltNick(const CString& sBadNick) {
    const CString& sLastNick = m_Nick.GetNick();

    // We don't know the maximum allowed nick length yet, but we know which
    // nick we sent last. If sBadNick is shorter than that, we assume the
    // server truncated our nick.
    if (sBadNick.length() < sLastNick.length())
        m_uMaxNickLen = (unsigned int)sBadNick.length();

    unsigned int uMax = m_uMaxNickLen;

    const CString& sConfNick = m_pNetwork->GetNick();
    const CString& sAltNick = m_pNetwork->GetAltNick();
    CString sNewNick = sConfNick.Left(uMax - 1);

    if (sLastNick.Equals(sConfNick)) {
        if ((!sAltNick.empty()) && (!sConfNick.Equals(sAltNick))) {
            sNewNick = sAltNick;
        } else {
            sNewNick += "-";
        }
    } else if (sLastNick.Equals(sAltNick) && !sAltNick.Equals(sNewNick + "-")) {
        sNewNick += "-";
    } else if (sLastNick.Equals(sNewNick + "-") &&
               !sAltNick.Equals(sNewNick + "|")) {
        sNewNick += "|";
    } else if (sLastNick.Equals(sNewNick + "|") &&
               !sAltNick.Equals(sNewNick + "^")) {
        sNewNick += "^";
    } else if (sLastNick.Equals(sNewNick + "^") &&
               !sAltNick.Equals(sNewNick + "a")) {
        sNewNick += "a";
    } else {
        char cLetter = 0;
        if (sBadNick.empty()) {
            m_pNetwork->PutUser(t_s("No free nick available"));
            Quit();
            return;
        }

        cLetter = sBadNick.back();

        if (cLetter == 'z') {
            m_pNetwork->PutUser(t_s("No free nick found"));
            Quit();
            return;
        }

        sNewNick = sConfNick.Left(uMax - 1) + ++cLetter;
        if (sNewNick.Equals(sAltNick))
            sNewNick = sConfNick.Left(uMax - 1) + ++cLetter;
    }
    PutIRC("NICK " + sNewNick);
    m_Nick.SetNick(sNewNick);
}

char CIRCSock::GetPermFromMode(char cMode) const {
    if (m_sPermModes.size() == m_sPerms.size()) {
        for (unsigned int a = 0; a < m_sPermModes.size(); a++) {
            if (m_sPermModes[a] == cMode) {
                return m_sPerms[a];
            }
        }
    }

    return 0;
}

char CIRCSock::GetModeFromPerm(char cPerm) const {
    if (m_sPermModes.size() == m_sPerms.size()) {
        for (unsigned int a = 0; a < m_sPermModes.size(); a++) {
            if (m_sPerms[a] == cPerm) {
                return m_sPermModes[a];
            }
        }
    }

    return 0;
}

CIRCSock::EChanModeArgs CIRCSock::GetModeType(char cMode) const {
    map<char, EChanModeArgs>::const_iterator it =
        m_mceChanModes.find(cMode);

    if (it == m_mceChanModes.end()) {
        return NoArg;
    }

    return it->second;
}

void CIRCSock::ResetChans() {
    for (const auto& it : m_msChans) {
        it.second->Reset();
    }
}

void CIRCSock::SetTagSupport(const CString& sTag, bool bState) {
    if (bState) {
        m_ssSupportedTags.insert(sTag);
    } else {
        m_ssSupportedTags.erase(sTag);
    }
}

