/*
 * 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/Template.h>
#include <znc/FileUtils.h>
#include <znc/ZNCDebug.h>
#include <znc/Translation.h>
#include <algorithm>

using std::stringstream;
using std::vector;
using std::list;
using std::ostream;
using std::pair;
using std::map;

void CTemplateOptions::Parse(const CString& sLine) {
    CString sName = sLine.Token(0, false, "=").Trim_n().AsUpper();
    CString sValue = sLine.Token(1, true, "=").Trim_n();

    if (sName == "ESC") {
        m_eEscapeTo = CString::ToEscape(sValue);
    } else if (sName == "ESCFROM") {
        m_eEscapeFrom = CString::ToEscape(sValue);
    }
}

CTemplate* CTemplateLoopContext::GetRow(unsigned int uIndex) {
    size_t uSize = m_pvRows->size();

    if (uIndex < uSize) {
        if (m_bReverse) {
            return (*m_pvRows)[uSize - uIndex - 1];
        } else {
            return (*m_pvRows)[uIndex];
        }
    }

    return nullptr;
}

CString CTemplateLoopContext::GetValue(const CString& sName, bool bFromIf) {
    CTemplate* pTemplate = GetCurRow();

    if (!pTemplate) {
        DEBUG("Loop [" + GetName() + "] has no row index [" +
              CString(GetRowIndex()) + "]");
        return "";
    }

    if (sName.Equals("__ID__")) {
        return CString(GetRowIndex() + 1);
    } else if (sName.Equals("__COUNT__")) {
        return CString(GetRowCount());
    } else if (sName.Equals("__ODD__")) {
        return ((GetRowIndex() % 2) ? "" : "1");
    } else if (sName.Equals("__EVEN__")) {
        return ((GetRowIndex() % 2) ? "1" : "");
    } else if (sName.Equals("__FIRST__")) {
        return ((GetRowIndex() == 0) ? "1" : "");
    } else if (sName.Equals("__LAST__")) {
        return ((GetRowIndex() == m_pvRows->size() - 1) ? "1" : "");
    } else if (sName.Equals("__OUTER__")) {
        return ((GetRowIndex() == 0 || GetRowIndex() == m_pvRows->size() - 1)
                    ? "1"
                    : "");
    } else if (sName.Equals("__INNER__")) {
        return ((GetRowIndex() == 0 || GetRowIndex() == m_pvRows->size() - 1)
                    ? ""
                    : "1");
    }

    return pTemplate->GetValue(sName, bFromIf);
}

CTemplate::~CTemplate() {
    for (const auto& it : m_mvLoops) {
        const vector<CTemplate*>& vLoop = it.second;
        for (CTemplate* pTemplate : vLoop) {
            delete pTemplate;
        }
    }

    for (CTemplateLoopContext* pContext : m_vLoopContexts) {
        delete pContext;
    }
}

void CTemplate::Init() {
    /* We have no CConfig in ZNC land
     * Hmm... Actually, we do have it now.
    CString sPath(CConfig::GetValue("WebFilesPath"));

    if (!sPath.empty()) {
        SetPath(sPath);
    }
    */

    ClearPaths();
    m_pParent = nullptr;
}

CString CTemplate::ExpandFile(const CString& sFilename, bool bFromInc) {
    /*if (sFilename.StartsWith("/") || sFilename.StartsWith("./")) {
        return sFilename;
    }*/

    CString sFile(ResolveLiteral(sFilename).TrimLeft_n("/"));

    for (auto& it : m_lsbPaths) {
        CString& sRoot = it.first;
        CString sFilePath(CDir::ChangeDir(sRoot, sFile));

        // Make sure path ends with a slash because "/foo/pub*" matches
        // "/foo/public_keep_out/" but "/foo/pub/*" doesn't
        if (!sRoot.empty() && !sRoot.EndsWith("/")) {
            sRoot += "/";
        }

        if (it.second && !bFromInc) {
            DEBUG("\t\tSkipping path (not from INC)  [" + sFilePath + "]");
            continue;
        }

        if (CFile::Exists(sFilePath)) {
            if (sRoot.empty() || sFilePath.StartsWith(sRoot)) {
                DEBUG("    Found  [" + sFilePath + "]");
                return sFilePath;
            } else {
                DEBUG("\t\tOutside of root [" + sFilePath + "] !~ [" + sRoot +
                      "]");
            }
        }
    }

    switch (m_lsbPaths.size()) {
        case 0:
            DEBUG("Unable to find [" + sFile + "] using the current directory");
            break;
        case 1:
            DEBUG("Unable to find [" + sFile + "] in the defined path [" +
                  m_lsbPaths.begin()->first + "]");
            break;
        default:
            DEBUG("Unable to find [" + sFile + "] in any of the " +
                  CString(m_lsbPaths.size()) + " defined paths");
    }

    return "";
}

void CTemplate::SetPath(const CString& sPaths) {
    VCString vsDirs;
    sPaths.Split(":", vsDirs, false);

    for (const CString& sDir : vsDirs) {
        AppendPath(sDir, false);
    }
}

CString CTemplate::MakePath(const CString& sPath) const {
    CString sRet(CDir::ChangeDir("./", sPath + "/"));

    if (!sRet.empty() && !sRet.EndsWith("/")) {
        sRet += "/";
    }

    return sRet;
}

void CTemplate::PrependPath(const CString& sPath, bool bIncludesOnly) {
    DEBUG("CTemplate::PrependPath(" + sPath + ") == [" + MakePath(sPath) + "]");
    m_lsbPaths.push_front(make_pair(MakePath(sPath), bIncludesOnly));
}

void CTemplate::AppendPath(const CString& sPath, bool bIncludesOnly) {
    DEBUG("CTemplate::AppendPath(" + sPath + ") == [" + MakePath(sPath) + "]");
    m_lsbPaths.push_back(make_pair(MakePath(sPath), bIncludesOnly));
}

void CTemplate::RemovePath(const CString& sPath) {
    DEBUG("CTemplate::RemovePath(" + sPath + ") == [" +
          CDir::ChangeDir("./", sPath + "/") + "]");

    for (const auto& it : m_lsbPaths) {
        if (it.first == sPath) {
            m_lsbPaths.remove(it);
            RemovePath(
                sPath);  // @todo probably shouldn't use recursion, being lazy
            return;
        }
    }
}

void CTemplate::ClearPaths() { m_lsbPaths.clear(); }

bool CTemplate::SetFile(const CString& sFileName) {
    m_sFileName = ExpandFile(sFileName, false);
    PrependPath(sFileName + "/..");

    if (sFileName.empty()) {
        DEBUG("CTemplate::SetFile() - Filename is empty");
        return false;
    }

    if (m_sFileName.empty()) {
        DEBUG("CTemplate::SetFile() - [" + sFileName + "] does not exist");
        return false;
    }

    DEBUG("Set template file to [" + m_sFileName + "]");

    return true;
}

class CLoopSorter {
    CString m_sType;

  public:
    CLoopSorter(const CString& sType) : m_sType(sType) {}
    bool operator()(CTemplate* pTemplate1, CTemplate* pTemplate2) {
        return (pTemplate1->GetValue(m_sType, false) <
                pTemplate2->GetValue(m_sType, false));
    }
};

CTemplate& CTemplate::AddRow(const CString& sName) {
    CTemplate* pTmpl = new CTemplate(m_spOptions, this);
    m_mvLoops[sName].push_back(pTmpl);

    return *pTmpl;
}

CTemplate* CTemplate::GetRow(const CString& sName, unsigned int uIndex) {
    vector<CTemplate*>* pvLoop = GetLoop(sName);

    if (pvLoop) {
        if (pvLoop->size() > uIndex) {
            return (*pvLoop)[uIndex];
        }
    }

    return nullptr;
}

vector<CTemplate*>* CTemplate::GetLoop(const CString& sName) {
    CTemplateLoopContext* pContext = GetCurLoopContext();

    if (pContext) {
        CTemplate* pTemplate = pContext->GetCurRow();

        if (pTemplate) {
            return pTemplate->GetLoop(sName);
        }
    }

    map<CString, vector<CTemplate*>>::iterator it = m_mvLoops.find(sName);

    if (it != m_mvLoops.end()) {
        return &(it->second);
    }

    return nullptr;
}

bool CTemplate::PrintString(CString& sRet) {
    sRet.clear();
    stringstream sStream;
    bool bRet = Print(sStream);

    sRet = sStream.str();

    return bRet;
}

bool CTemplate::Print(ostream& oOut) { return Print(m_sFileName, oOut); }

bool CTemplate::Print(const CString& sFileName, ostream& oOut) {
    if (sFileName.empty()) {
        DEBUG("Empty filename in CTemplate::Print()");
        return false;
    }

    CFile File(sFileName);

    if (!File.Open()) {
        DEBUG("Unable to open file [" + sFileName + "] in CTemplate::Print()");
        return false;
    }

    CString sLine;
    CString sSetBlockVar;
    bool bValidLastIf = false;
    bool bInSetBlock = false;
    unsigned long uFilePos = 0;
    unsigned long uCurPos = 0;
    unsigned int uLineNum = 0;
    unsigned int uNestedIfs = 0;
    unsigned int uSkip = 0;
    bool bLoopCont = false;
    bool bLoopBreak = false;
    bool bExit = false;

    // Single template works across multiple translation domains, e.g. .tmpl of
    // a module can INC'lude Footer.tmpl from core
    CString sI18N;

    while (File.ReadLine(sLine)) {
        CString sOutput;
        bool bFoundATag = false;
        bool bTmplLoopHasData = false;
        uLineNum++;
        CString::size_type iPos = 0;
        uCurPos = uFilePos;
        CString::size_type uLineSize = sLine.size();
        bool bBroke = false;

        while (1) {
            iPos = sLine.find("<?");

            if (iPos == CString::npos) {
                break;
            }

            uCurPos += iPos;
            bFoundATag = true;

            if (!uSkip) {
                sOutput += sLine.substr(0, iPos);
            }

            sLine = sLine.substr(iPos + 2);

            CString::size_type iPos2 = sLine.find("?>");

            // Make sure our tmpl tag is ended properly
            if (iPos2 == CString::npos) {
                DEBUG("Template tag not ended properly in file [" + sFileName +
                      "] [<?" + sLine + "]");
                return false;
            }

            uCurPos += iPos2 + 4;

            CString sMid = CString(sLine.substr(0, iPos2)).Trim_n();

            // Make sure we don't have a nested tag
            if (!sMid.Contains("<?")) {
                sLine = sLine.substr(iPos2 + 2);
                CString sAction = sMid.Token(0);
                CString sArgs = sMid.Token(1, true);
                bool bNotFound = false;

                // If we're breaking or continuing from within a loop, skip all
                // tags that aren't ENDLOOP
                if ((bLoopCont || bLoopBreak) && !sAction.Equals("ENDLOOP")) {
                    continue;
                }

                if (!uSkip) {
                    if (sAction.Equals("INC")) {
                        if (!Print(ExpandFile(sArgs, true), oOut)) {
                            DEBUG("Unable to print INC'd file [" + sArgs + "]");
                            return false;
                        }
                    } else if (sAction.Equals("SETOPTION")) {
                        m_spOptions->Parse(sArgs);
                    } else if (sAction.Equals("ADDROW")) {
                        CString sLoopName = sArgs.Token(0);
                        MCString msRow;

                        if (sArgs.Token(1, true, " ").OptionSplit(msRow)) {
                            CTemplate& NewRow = AddRow(sLoopName);

                            for (const auto& it : msRow) {
                                NewRow[it.first] = it.second;
                            }
                        }
                    } else if (sAction.Equals("SET")) {
                        CString sName = sArgs.Token(0);
                        CString sValue = sArgs.Token(1, true);

                        (*this)[sName] = sValue;
                    } else if (sAction.Equals("JOIN")) {
                        VCString vsArgs;
                        // sArgs.Split(" ", vsArgs, false, "\"", "\"");
                        sArgs.QuoteSplit(vsArgs);

                        if (vsArgs.size() > 1) {
                            CString sDelim = vsArgs[0];
                            bool bFoundOne = false;
                            CString::EEscape eEscape = CString::EASCII;

                            for (const CString& sArg : vsArgs) {
                                if (sArg.StartsWith("ESC=")) {
                                    eEscape =
                                        CString::ToEscape(sArg.LeftChomp_n(4));
                                } else {
                                    CString sValue = GetValue(sArg);

                                    if (!sValue.empty()) {
                                        if (bFoundOne) {
                                            sOutput += sDelim;
                                        }

                                        sOutput += sValue.Escape_n(eEscape);
                                        bFoundOne = true;
                                    }
                                }
                            }
                        }
                    } else if (sAction.Equals("SETBLOCK")) {
                        sSetBlockVar = sArgs;
                        bInSetBlock = true;
                    } else if (sAction.Equals("ENDSETBLOCK")) {
                        CString sName = sSetBlockVar.Token(0);
                        (*this)[sName] += sOutput;
                        sOutput = "";
                        bInSetBlock = false;
                        sSetBlockVar = "";
                    } else if (sAction.Equals("EXPAND")) {
                        sOutput += ExpandFile(sArgs, true);
                    } else if (sAction.Equals("VAR")) {
                        sOutput += GetValue(sArgs);
                    } else if (sAction.Equals("LT")) {
                        sOutput += "<?";
                    } else if (sAction.Equals("GT")) {
                        sOutput += "?>";
                    } else if (sAction.Equals("CONTINUE")) {
                        CTemplateLoopContext* pContext = GetCurLoopContext();

                        if (pContext) {
                            uSkip++;
                            bLoopCont = true;

                            break;
                        } else {
                            DEBUG("[" + sFileName + ":" +
                                  CString(uCurPos - iPos2 - 4) +
                                  "] <? CONTINUE ?> must be used inside of a "
                                  "loop!");
                        }
                    } else if (sAction.Equals("BREAK")) {
                        // break from loop
                        CTemplateLoopContext* pContext = GetCurLoopContext();

                        if (pContext) {
                            uSkip++;
                            bLoopBreak = true;

                            break;
                        } else {
                            DEBUG(
                                "[" + sFileName + ":" +
                                CString(uCurPos - iPos2 - 4) +
                                "] <? BREAK ?> must be used inside of a loop!");
                        }
                    } else if (sAction.Equals("EXIT")) {
                        bExit = true;
                    } else if (sAction.Equals("DEBUG")) {
                        DEBUG("CTemplate DEBUG [" + sFileName + "@" +
                              CString(uCurPos - iPos2 - 4) + "b] -> [" + sArgs +
                              "]");
                    } else if (sAction.Equals("LOOP")) {
                        CTemplateLoopContext* pContext = GetCurLoopContext();

                        if (!pContext ||
                            pContext->GetFilePosition() != uCurPos) {
                            // we are at a brand new loop (be it new or a first
                            // pass at an inner loop)

                            CString sLoopName = sArgs.Token(0);
                            bool bReverse = (sArgs.Token(1).Equals("REVERSE"));
                            bool bSort = (sArgs.Token(1).StartsWith("SORT"));
                            vector<CTemplate*>* pvLoop = GetLoop(sLoopName);

                            if (bSort && pvLoop != nullptr &&
                                pvLoop->size() > 1) {
                                CString sKey;

                                if (sArgs.Token(1)
                                        .TrimPrefix_n("SORT")
                                        .StartsWith("ASC=")) {
                                    sKey =
                                        sArgs.Token(1).TrimPrefix_n("SORTASC=");
                                } else if (sArgs.Token(1)
                                               .TrimPrefix_n("SORT")
                                               .StartsWith("DESC=")) {
                                    sKey = sArgs.Token(1)
                                               .TrimPrefix_n("SORTDESC=");
                                    bReverse = true;
                                }

                                if (!sKey.empty()) {
                                    std::sort(pvLoop->begin(), pvLoop->end(),
                                              CLoopSorter(sKey));
                                }
                            }

                            if (pvLoop) {
                                // If we found data for this loop, add it to our
                                // context vector
                                // unsigned long uBeforeLoopTag = uCurPos -
                                // iPos2 - 4;
                                unsigned long uAfterLoopTag = uCurPos;

                                for (CString::size_type t = 0; t < sLine.size();
                                     t++) {
                                    char c = sLine[t];
                                    if (c == '\r' || c == '\n') {
                                        uAfterLoopTag++;
                                    } else {
                                        break;
                                    }
                                }

                                m_vLoopContexts.push_back(
                                    new CTemplateLoopContext(uAfterLoopTag,
                                                             sLoopName,
                                                             bReverse, pvLoop));
                            } else {
                                // If we don't have data, just skip this loop
                                // and everything inside
                                uSkip++;
                            }
                        }
                    } else if (sAction.Equals("IF")) {
                        if (ValidIf(sArgs)) {
                            uNestedIfs++;
                            bValidLastIf = true;
                        } else {
                            uSkip++;
                            bValidLastIf = false;
                        }
                    } else if (sAction.Equals("REM")) {
                        uSkip++;
                    } else if (sAction.Equals("I18N")) {
                        sI18N = sArgs;
                    } else if (sAction.Equals("FORMAT") ||
                               sAction.Equals("PLURAL")) {
                        bool bHaveContext = false;
                        if (sArgs.TrimPrefix("CTX=")) {
                            bHaveContext = true;
                        }
                        VCString vsArgs;
                        sArgs.QuoteSplit(vsArgs);
                        CString sEnglish, sContext;
                        size_type idx = 0;
                        if (bHaveContext && vsArgs.size() > idx) {
                            sContext = vsArgs[idx];
                            idx++;
                        }
                        if (vsArgs.size() > idx) {
                            sEnglish = vsArgs[idx];
                            idx++;
                        }
                        CString sFormat;
                        if (sAction.Equals("PLURAL")) {
                            CString sEnglishes;
                            int iNum = 0;
                            if (vsArgs.size() > idx) {
                                sEnglishes = vsArgs[idx];
                                idx++;
                            }
                            if (vsArgs.size() > idx) {
                                iNum = GetValue(vsArgs[idx], true).ToInt();
                                idx++;
                            }
                            sFormat = CTranslation::Get().Plural(
                                sI18N, sContext, sEnglish, sEnglishes, iNum);
                        } else {
                            sFormat = CTranslation::Get().Singular(
                                sI18N, sContext, sEnglish);
                        }
                        MCString msParams;
                        for (size_type i = 0; i + idx < vsArgs.size(); ++i) {
                            msParams[CString(i + 1)] =
                                GetValue(vsArgs[i + idx], false);
                        }
                        sOutput += CString::NamedFormat(sFormat, msParams);
                    } else {
                        bNotFound = true;
                    }
                } else if (sAction.Equals("REM")) {
                    uSkip++;
                } else if (sAction.Equals("IF")) {
                    uSkip++;
                } else if (sAction.Equals("LOOP")) {
                    uSkip++;
                }

                if (sAction.Equals("ENDIF")) {
                    if (uSkip) {
                        uSkip--;
                    } else {
                        uNestedIfs--;
                    }
                } else if (sAction.Equals("ENDREM")) {
                    if (uSkip) {
                        uSkip--;
                    }
                } else if (sAction.Equals("ENDLOOP")) {
                    if (bLoopCont && uSkip == 1) {
                        uSkip--;
                        bLoopCont = false;
                    }

                    if (bLoopBreak && uSkip == 1) {
                        uSkip--;
                    }

                    if (uSkip) {
                        uSkip--;
                    } else {
                        // We are at the end of the loop so we need to inc the
                        // index
                        CTemplateLoopContext* pContext = GetCurLoopContext();

                        if (pContext) {
                            pContext->IncRowIndex();

                            // If we didn't go out of bounds we need to seek
                            // back to the top of our loop
                            if (!bLoopBreak && pContext->GetCurRow()) {
                                uCurPos = pContext->GetFilePosition();
                                uFilePos = uCurPos;
                                uLineSize = 0;

                                File.Seek(uCurPos);
                                bBroke = true;

                                if (!sOutput.Trim_n().empty()) {
                                    pContext->SetHasData();
                                }

                                break;
                            } else {
                                if (sOutput.Trim_n().empty()) {
                                    sOutput.clear();
                                }

                                bTmplLoopHasData = pContext->HasData();
                                DelCurLoopContext();
                                bLoopBreak = false;
                            }
                        }
                    }
                } else if (sAction.Equals("ELSE")) {
                    if (!bValidLastIf && uSkip == 1) {
                        CString sArg = sArgs.Token(0);

                        if (sArg.empty() || (sArg.Equals("IF") &&
                                             ValidIf(sArgs.Token(1, true)))) {
                            uSkip = 0;
                            bValidLastIf = true;
                        }
                    } else if (!uSkip) {
                        uSkip = 1;
                    }
                } else if (bNotFound) {
                    // Unknown tag that isn't being skipped...
                    vector<std::shared_ptr<CTemplateTagHandler>>&
                        vspTagHandlers = GetTagHandlers();

                    if (!vspTagHandlers.empty()) {
                        // @todo this should go up to the top to grab handlers
                        CTemplate* pTmpl = GetCurTemplate();
                        CString sCustomOutput;

                        for (const auto& spTagHandler : vspTagHandlers) {
                            if (spTagHandler->HandleTag(*pTmpl, sAction, sArgs,
                                                        sCustomOutput)) {
                                sOutput += sCustomOutput;
                                bNotFound = false;
                                break;
                            }
                        }

                        if (bNotFound) {
                            DEBUG("Unknown/Unhandled tag [" + sAction + "]");
                        }
                    }
                }

                continue;
            }

            DEBUG("Malformed tag on line " + CString(uLineNum) + " of ["
                  << File.GetLongName() + "]");
            DEBUG("--------------- [" + sLine + "]");
        }

        if (!bBroke) {
            uFilePos += uLineSize;

            if (!uSkip) {
                sOutput += sLine;
            }
        }

        if (!bFoundATag || bTmplLoopHasData ||
            sOutput.find_first_not_of(" \t\r\n") != CString::npos) {
            if (bInSetBlock) {
                CString sName = sSetBlockVar.Token(0);
                // CString sValue = sSetBlockVar.Token(1, true);
                (*this)[sName] += sOutput;
            } else {
                oOut << sOutput;
            }
        }

        if (bExit) {
            break;
        }
    }

    oOut.flush();

    return true;
}

void CTemplate::DelCurLoopContext() {
    if (m_vLoopContexts.empty()) {
        return;
    }

    delete m_vLoopContexts.back();
    m_vLoopContexts.pop_back();
}

CTemplateLoopContext* CTemplate::GetCurLoopContext() {
    if (!m_vLoopContexts.empty()) {
        return m_vLoopContexts.back();
    }

    return nullptr;
}

bool CTemplate::ValidIf(const CString& sArgs) {
    CString sArgStr = sArgs;
    // sArgStr.Replace(" ", "", "\"", "\"", true);
    sArgStr.Replace(" &&", "&&", "\"", "\"", false);
    sArgStr.Replace("&& ", "&&", "\"", "\"", false);
    sArgStr.Replace(" ||", "||", "\"", "\"", false);
    sArgStr.Replace("|| ", "||", "\"", "\"", false);

    CString::size_type uOrPos = sArgStr.find("||");
    CString::size_type uAndPos = sArgStr.find("&&");

    while (uOrPos != CString::npos || uAndPos != CString::npos ||
           !sArgStr.empty()) {
        bool bAnd = false;

        if (uAndPos < uOrPos) {
            bAnd = true;
        }

        CString sExpr = sArgStr.Token(0, false, ((bAnd) ? "&&" : "||"));
        sArgStr = sArgStr.Token(1, true, ((bAnd) ? "&&" : "||"));

        if (ValidExpr(sExpr)) {
            if (!bAnd) {
                return true;
            }
        } else {
            if (bAnd) {
                return false;
            }
        }

        uOrPos = sArgStr.find("||");
        uAndPos = sArgStr.find("&&");
    }

    return false;
}

bool CTemplate::ValidExpr(const CString& sExpression) {
    bool bNegate = false;
    CString sExpr(sExpression);
    CString sName;
    CString sValue;

    if (sExpr.TrimPrefix("!")) {
        bNegate = true;
    }

    if (sExpr.Contains("!=")) {
        sName = sExpr.Token(0, false, "!=").Trim_n();
        sValue = sExpr.Token(1, true, "!=", false, "\"", "\"", true).Trim_n();
        bNegate = !bNegate;
    } else if (sExpr.Contains("==")) {
        sName = sExpr.Token(0, false, "==").Trim_n();
        sValue = sExpr.Token(1, true, "==", false, "\"", "\"", true).Trim_n();
    } else if (sExpr.Contains(">=")) {
        sName = sExpr.Token(0, false, ">=").Trim_n();
        sValue = sExpr.Token(1, true, ">=", false, "\"", "\"", true).Trim_n();
        return (GetValue(sName, true).ToLong() >= sValue.ToLong());
    } else if (sExpr.Contains("<=")) {
        sName = sExpr.Token(0, false, "<=").Trim_n();
        sValue = sExpr.Token(1, true, "<=", false, "\"", "\"", true).Trim_n();
        return (GetValue(sName, true).ToLong() <= sValue.ToLong());
    } else if (sExpr.Contains(">")) {
        sName = sExpr.Token(0, false, ">").Trim_n();
        sValue = sExpr.Token(1, true, ">", false, "\"", "\"", true).Trim_n();
        return (GetValue(sName, true).ToLong() > sValue.ToLong());
    } else if (sExpr.Contains("<")) {
        sName = sExpr.Token(0, false, "<").Trim_n();
        sValue = sExpr.Token(1, true, "<", false, "\"", "\"", true).Trim_n();
        return (GetValue(sName, true).ToLong() < sValue.ToLong());
    } else {
        sName = sExpr.Trim_n();
    }

    if (sValue.empty()) {
        return (bNegate != IsTrue(sName));
    }

    sValue = ResolveLiteral(sValue);

    return (bNegate != GetValue(sName, true).Equals(sValue));
}

bool CTemplate::IsTrue(const CString& sName) {
    if (HasLoop(sName)) {
        return true;
    }

    return GetValue(sName, true).ToBool();
}

bool CTemplate::HasLoop(const CString& sName) {
    return (GetLoop(sName) != nullptr);
}

CTemplate* CTemplate::GetParent(bool bRoot) {
    if (!bRoot) {
        return m_pParent;
    }

    return (m_pParent) ? m_pParent->GetParent(bRoot) : this;
}

CTemplate* CTemplate::GetCurTemplate() {
    CTemplateLoopContext* pContext = GetCurLoopContext();

    if (!pContext) {
        return this;
    }

    return pContext->GetCurRow();
}

CString CTemplate::ResolveLiteral(const CString& sString) {
    if (sString.StartsWith("**")) {
        // Allow string to start with a literal * by using two in a row
        return sString.substr(1);
    } else if (sString.StartsWith("*")) {
        // If it starts with only one * then treat it as a var and do a lookup
        return GetValue(sString.substr(1));
    }

    return sString;
}

CString CTemplate::GetValue(const CString& sArgs, bool bFromIf) {
    CTemplateLoopContext* pContext = GetCurLoopContext();
    CString sName = sArgs.Token(0);
    CString sRest = sArgs.Token(1, true);
    CString sRet;

    while (sRest.Replace(" =", "=", "\"", "\"")) {
    }
    while (sRest.Replace("= ", "=", "\"", "\"")) {
    }

    VCString vArgs;
    MCString msArgs;
    // sRest.Split(" ", vArgs, false, "\"", "\"");
    sRest.QuoteSplit(vArgs);

    for (const CString& sArg : vArgs) {
        msArgs[sArg.Token(0, false, "=").AsUpper()] = sArg.Token(1, true, "=");
    }

    /* We have no CConfig in ZNC land
     * Hmm... Actually, we do have it now.
    if (msArgs.find("CONFIG") != msArgs.end()) {
        sRet = CConfig::GetValue(sName);
    } else*/ if (msArgs.find("ROWS") != msArgs.end()) {
        vector<CTemplate*>* pLoop = GetLoop(sName);
        sRet = CString((pLoop) ? pLoop->size() : 0);
    } else if (msArgs.find("TOP") == msArgs.end() && pContext) {
        sRet = pContext->GetValue(sArgs, bFromIf);

        if (!sRet.empty()) {
            return sRet;
        }
    } else {
        if (sName.TrimPrefix("*")) {
            MCString::iterator it = find(sName);
            sName = (it != end()) ? it->second : "";
        }

        MCString::iterator it = find(sName);
        sRet = (it != end()) ? it->second : "";
    }

    vector<std::shared_ptr<CTemplateTagHandler>>& vspTagHandlers =
        GetTagHandlers();

    if (!vspTagHandlers
             .empty()) {  // @todo this should go up to the top to grab handlers
        CTemplate* pTmpl = GetCurTemplate();

        if (sRet.empty()) {
            for (const auto& spTagHandler : vspTagHandlers) {
                CString sCustomOutput;

                if (!bFromIf &&
                    spTagHandler->HandleVar(*pTmpl, sArgs.Token(0),
                                            sArgs.Token(1, true),
                                            sCustomOutput)) {
                    sRet = sCustomOutput;
                    break;
                } else if (bFromIf &&
                           spTagHandler->HandleIf(*pTmpl, sArgs.Token(0),
                                                  sArgs.Token(1, true),
                                                  sCustomOutput)) {
                    sRet = sCustomOutput;
                    break;
                }
            }
        }

        for (const auto& spTagHandler : vspTagHandlers) {
            if (spTagHandler->HandleValue(*pTmpl, sRet, msArgs)) {
                break;
            }
        }
    }

    if (!bFromIf) {
        if (sRet.empty()) {
            sRet = ResolveLiteral(msArgs["DEFAULT"]);
        }

        MCString::iterator it = msArgs.find("ESC");

        if (it != msArgs.end()) {
            VCString vsEscs;
            it->second.Split(",", vsEscs, false);

            for (const CString& sEsc : vsEscs) {
                sRet.Escape(CString::ToEscape(sEsc));
            }
        } else {
            sRet.Escape(m_spOptions->GetEscapeFrom(),
                        m_spOptions->GetEscapeTo());
        }
    }

    return sRet;
}
