/* bzflag
 * Copyright (c) 1993-2025 Tim Riker
 *
 * This package is free software;  you can redistribute it and/or
 * modify it under the terms of the license found in the file
 * named COPYING that should have accompanied this file.
 *
 * THIS PACKAGE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 */

// interface header
#include "ServerListFilter.h"

// system headers
#include <vector>
#include <string>
#include <string.h>
#include <ctype.h>

// common implementation headers
#include "AnsiCodes.h"
#include "ServerItem.h"
#include "TextUtils.h"
#include "bzglob.h"
#include "global.h"

// local headers
#include "playing.h"


std::map<std::string, size_t> ServerListFilter::boolMap;
std::map<std::string, size_t> ServerListFilter::rangeMap;
std::map<std::string, size_t> ServerListFilter::patternMap;


//============================================================================//

static void errorMessage(const std::string& msg)
{
    addMessage(NULL, ANSI_STR_FG_RED "ServerListFilter: " + msg);
}


//============================================================================//

ServerListFilter::ServerListFilter()
    : orFilter(NULL)
{
    fflush(stdout);
    reset();
}


ServerListFilter::ServerListFilter(const std::string& filter)
    : orFilter(NULL)
{
    fflush(stdout);
    parse(filter);
}


//============================================================================//

void ServerListFilter::reset()
{
    source = "";

    // OR clauses
    if (orFilter)
    {
        delete orFilter;
        orFilter = NULL;
    }

    // pattern filters
    addrPat   .reset();
    descPat   .reset();
    addrDescPat   .reset();

    // boolean filters
    ffa       .reset();
    offa      .reset();
    ctf       .reset();
    rabbit    .reset();

    jump      .reset();
    rico      .reset();
    handi     .reset();
    replay    .reset();
    inertia   .reset();
    antidote  .reset();
    favorite  .reset();

    // range filters
    shots     .reset();
    players   .reset();
    freeSlots .reset();
    validTeams    .reset();

    maxTime   .reset();
    maxPlayers    .reset();
    maxTeamScore  .reset();
    maxPlayerScore.reset();
    shakeWins .reset();
    shakeTime .reset();

    rogueCount    .reset();
    redCount  .reset();
    greenCount    .reset();
    blueCount .reset();
    purpleCount   .reset();
    observerCount .reset();

    rogueMax  .reset();
    redMax    .reset();
    greenMax  .reset();
    blueMax   .reset();
    purpleMax .reset();
    observerMax   .reset();

    rogueFree .reset();
    redFree   .reset();
    greenFree .reset();
    blueFree  .reset();
    purpleFree    .reset();
    observerFree  .reset();
}


//============================================================================//

static int countPlayers(const ServerItem& item)
{
    const PingPacket& ping = item.ping;
    int players = 0;
    players += ping.rogueCount;
    players += ping.redCount;
    players += ping.greenCount;
    players += ping.blueCount;
    players += ping.purpleCount;
    return players;
}


static int countFreeSlots(const ServerItem& item)
{
    const PingPacket& ping = item.ping;
    int freeSlots = 0;
    freeSlots += (ping.rogueMax  - ping.rogueCount);
    freeSlots += (ping.redMax    - ping.redCount);
    freeSlots += (ping.greenMax  - ping.greenCount);
    freeSlots += (ping.blueMax   - ping.blueCount);
    freeSlots += (ping.purpleMax - ping.purpleCount);

    // also check against the 'maxPlayers' limit
    const int totalPlayers = countPlayers(item) + ping.observerCount;
    const int totalFreeSlots = ping.maxPlayers - totalPlayers;
    if (freeSlots > totalFreeSlots)
        freeSlots = totalFreeSlots;

    return freeSlots;
}


static int countValidTeams(const ServerItem& item)
{
    const PingPacket& ping = item.ping;
    int count = 0;
    if (ping.rogueMax  > 0)
        count++;
    if (ping.redMax    > 0)
        count++;
    if (ping.greenMax  > 0)
        count++;
    if (ping.blueMax   > 0)
        count++;
    if (ping.purpleMax > 0)
        count++;
    return count;
}


static bool isReplay(const ServerItem& item)
{
    return ((item.ping.observerMax == 16) &&
            (item.ping.maxPlayers == 200));
}


//============================================================================//

bool ServerListFilter::check(const ServerItem& item) const
{
    // OR clauses
    if (orFilter && orFilter->check(item))
        return true;

    // pattern filters
    std::string addr, desc;
    item.splitAddrTitle(addr, desc);
    if (!addrPat.check(addr))
        return false;
    if (!descPat.check(desc))
        return false;
    if (!addrDescPat.check(addr) &&
            !addrDescPat.check(desc))
        return false;

    const PingPacket& p = item.ping;

    // boolean filters
    const uint16_t type = p.gameType;
    if (!ffa      .check(type == TeamFFA))
        return false;
    if (!offa     .check(type == OpenFFA))
        return false;
    if (!ctf      .check(type == ClassicCTF))
        return false;
    if (!rabbit       .check(type == RabbitChase))
        return false;

    const uint16_t options = p.gameOptions;
    if (!jump .check((options & JumpingGameStyle)!=0))
        return false;
    if (!rico .check((options & RicochetGameStyle)!=0))
        return false;
    if (!handi    .check((options & HandicapGameStyle)!=0))
        return false;
    if (!inertia  .check((options & InertiaGameStyle)!=0))
        return false;
    if (!antidote .check((options & AntidoteGameStyle)!=0))
        return false;

    if (!replay       .check(isReplay(item)))
        return false;
    if (!favorite     .check(item.favorite))
        return false;

    // range filters
    if (!shots        .check(p.maxShots))
        return false;
    if (!players      .check(countPlayers(item)))
        return false;
    if (!freeSlots    .check(countFreeSlots(item)))
        return false;
    if (!validTeams   .check(countValidTeams(item)))
        return false;

    if (!maxTime      .check(p.maxTime))
        return false;
    if (!maxPlayers   .check(p.maxPlayers))
        return false;
    if (!maxTeamScore .check(p.maxTeamScore))
        return false;
    if (!maxPlayerScore   .check(p.maxPlayerScore))
        return false;
    if (!shakeWins    .check(p.shakeWins))
        return false;
    if (!shakeTime    .check(p.shakeTimeout))
        return false;

    if (!rogueCount   .check(p.rogueCount))
        return false;
    if (!redCount     .check(p.redCount))
        return false;
    if (!greenCount   .check(p.greenCount))
        return false;
    if (!blueCount    .check(p.blueCount))
        return false;
    if (!purpleCount  .check(p.purpleCount))
        return false;
    if (!observerCount    .check(p.observerCount))
        return false;

    if (!rogueMax     .check(p.rogueMax))
        return false;
    if (!redMax       .check(p.redMax))
        return false;
    if (!greenMax     .check(p.greenMax))
        return false;
    if (!blueMax      .check(p.blueMax))
        return false;
    if (!purpleMax    .check(p.purpleMax))
        return false;
    if (!observerMax  .check(p.observerMax))
        return false;

    const int totalFree = p.maxPlayers - (countPlayers(item) + p.observerCount);
#define FREE_SLOTS(T) std::min(totalFree, p.T ## Max - p.T ## Count)
    if (!rogueFree    .check(FREE_SLOTS(rogue)))
        return false;
    if (!redFree      .check(FREE_SLOTS(red)))
        return false;
    if (!greenFree    .check(FREE_SLOTS(green)))
        return false;
    if (!blueFree     .check(FREE_SLOTS(blue)))
        return false;
    if (!purpleFree   .check(FREE_SLOTS(purple)))
        return false;
    if (!observerFree .check(FREE_SLOTS(observer)))
        return false;
#undef FREE_SLOTS

    return true;
}


//============================================================================//

static std::string stripLeadingWhite(const std::string& s)
{
    if (s.empty())
        return s;
    const std::string::size_type pos = s.find_first_not_of(" \t");
    if (pos == std::string::npos)
        return s;
    return s.substr(pos);
}


bool ServerListFilter::parse(const std::string& filter)
{
    setupBoolMap();
    setupRangeMap();
    setupPatternMap();

    reset();

    source = filter;

    std::string adGlobStr;
    std::string filterStr;
    const std::string::size_type optPos = source.find_first_of('/');
    if (optPos == std::string::npos)
        adGlobStr = source;
    else
    {
        adGlobStr = source.substr(0, optPos);
        filterStr = source.substr(optPos + 1);
        const std::string::size_type orPos = filterStr.find_first_of('/');
        if (orPos != std::string::npos)
        {
            orFilter = new ServerListFilter;
            orFilter->parse("/" + filterStr.substr(orPos + 1));
            filterStr.resize(orPos);
        }
    }

    if (!adGlobStr.empty())
        addrDescPat.setupGlob(adGlobStr, true);

    const std::vector<std::string> filters = TextUtils::tokenize(filterStr, ",");
    for (size_t i = 0; i < filters.size(); i++)
        parseFilter(filters[i]);

    return true;
}


//============================================================================//

bool ServerListFilter::parseFilter(const std::string& f)
{
    char op;
    std::string label, param;
    switch (parseFilterType(f, op, label, param))
    {
    case '#':
    {
        return true;
    }
    case 'b':
    {
        return parseBoolFilter(label, op);
    }
    case 'r':
    {
        return parseRangeFilter(label, op, param);
    }
    case 'p':
    {
        return parsePatternFilter(label, op, param);
    }
    default:
    {
        errorMessage("invalid filter, '" + f + "'");
        return false;
    }
    }
}


char ServerListFilter::parseFilterType(const std::string& _f, char& op,
                                       std::string& label, std::string& param)
{
    const std::string f = stripLeadingWhite(_f);

    if (f.empty() || (f[0] == '#'))
    {
        return '#'; // comment
    }

    if ((f[0] == '+') || (f[0] == '-'))
    {
        label = f.substr(1);
        param = "";
        op = f[0];
        return 'b'; // boolean
    }

    const char* s = f.c_str();
    const char* c = s;
    while ((*c != 0) && (isalnum(*c) || (*c == '_')))
        c++;

    switch (*c)
    {
    case '=':
    case '>':
    case '<':
    {
        label = std::string(s, c - s);
        if ((*c == '=') || (*(c + 1) != '='))
        {
            param = std::string(c + 1);
            op = *c;
        }
        else
        {
            param = std::string(c + 2);
            op = *c + 0x3f; // map [<>] to [{}]
        }
        return 'r'; // range
    }
    case ')':
    case ']':
    {
        label = std::string(s, c - s);
        param = std::string(c + 1);
        op = *c;
        return 'p'; // pattern
    }
    }

    return 0; // invalid
}

//============================================================================//

bool ServerListFilter::parseBoolFilter(const std::string& label, char op)
{
    std::map<std::string, size_t>::const_iterator it = boolMap.find(label);
    if (it == boolMap.end())
    {
        errorMessage("unknown boolean label, '" + label + "'");
        return false;
    }

    BoolFilter* bf = (BoolFilter*)((char*)this + it->second);
    bf->active = true;
    bf->value  = (op == '+');

    return true;
}


bool ServerListFilter::parseRangeFilter(const std::string& label, char op,
                                        const std::string& param)
{
    std::map<std::string, size_t>::const_iterator it = rangeMap.find(label);
    if (it == rangeMap.end())
    {
        errorMessage("unknown range label, '" + label + "'");
        return false;
    }

    const char* s = param.c_str();
    char* e;
    float value = (float)strtod(s, &e);
    if ((e == s) || (param.size() != size_t(e - s)))
    {
        errorMessage("bad range value, '" + param + "'");
        return false;
    }

    RangeFilter* rf = (RangeFilter*)((char*)this + it->second);
    switch (op)
    {
    case '<':
    {
        rf->maxActive = true;
        rf->maxValue = value;
        break;
    }
    case '{':
    {
        rf->maxActive = true;
        rf->maxValue = value + 1.0f;
        break;
    }
    case '>':
    {
        rf->minActive = true;
        rf->minValue = value;
        break;
    }
    case '}':
    {
        rf->minActive = true;
        rf->minValue = value - 1.0f;
        break;
    }
    case '=':
    {
        rf->minActive = true;
        rf->maxActive = true;
        rf->minValue = value - 1.0f;
        rf->maxValue = value + 1.0f;
        break;
    }
    default:
    {
        return false;
    }
    }

    return true;
}


bool ServerListFilter::parsePatternFilter(const std::string& label, char op,
        const std::string& param)
{
    const std::string lower = TextUtils::tolower(label);
    std::map<std::string, size_t>::const_iterator it = patternMap.find(lower);
    if (it == patternMap.end())
    {
        errorMessage("unknown pattern label, '" + label + "'");
        return false;
    }

    bool useGlob;
    switch (op)
    {
    case ')':
    {
        useGlob = true;
        break;
    }
    case ']':
    {
        useGlob = false;
        break;
    }
    default:
    {
        return false;
    }
    }

    bool noCase = islower(label[0]) != 0;

    PatternFilter* pf = (PatternFilter*)((char*)this + it->second);

    return useGlob ? pf->setupGlob(param, noCase)
           : pf->setupRegex(param, noCase);
}


//============================================================================//
//
//  Filter name maps
//

#undef  OFFSETOF
#define OFFSETOF(x) (size_t)((char*)&this->x - (char*)this)


void ServerListFilter::setupBoolMap()
{
    if (!boolMap.empty())
        return;

    std::map<std::string, size_t>& m = boolMap;

    m["F"] = m["ffa"]     = OFFSETOF(ffa);
    m["O"] = m["offa"]        = OFFSETOF(offa);
    m["C"] = m["ctf"]     = OFFSETOF(ctf);
    m["R"] = m["rabbit"]      = OFFSETOF(rabbit);

    m["j"] = m["jump"]        = OFFSETOF(jump);
    m["r"] = m["rico"]        = OFFSETOF(rico);
    m["h"] = m["handicap"]    = OFFSETOF(handi);
    m["P"] = m["replay"]      = OFFSETOF(replay);
    m["i"] = m["inertia"]     = OFFSETOF(inertia);
    m["a"] = m["antidote"]    = OFFSETOF(antidote);
    m["F"] = m["favorite"]    = OFFSETOF(favorite);
}


void ServerListFilter::setupRangeMap()
{
    if (!rangeMap.empty())
        return;

    std::map<std::string, size_t>& m = rangeMap;

    m["s"]   = m["shots"]     = OFFSETOF(shots);
    m["p"]   = m["players"]   = OFFSETOF(players);
    m["f"]   = m["freeSlots"] = OFFSETOF(freeSlots);
    m["vt"]  = m["validTeams"]    = OFFSETOF(validTeams);

    m["mt"]  = m["maxTime"]   = OFFSETOF(maxTime);
    m["mp"]  = m["maxPlayers"]    = OFFSETOF(maxPlayers);
    m["mts"] = m["maxTeamScore"]  = OFFSETOF(maxTeamScore);
    m["mps"] = m["maxPlayerScore"] = OFFSETOF(maxPlayerScore);

    m["sw"]  = m["shakeWins"] = OFFSETOF(shakeWins);
    m["st"]  = m["shakeTime"] = OFFSETOF(shakeTime);

    m["Rm"]  = m["rogueMax"]  = OFFSETOF(rogueMax);
    m["rm"]  = m["redMax"]    = OFFSETOF(redMax);
    m["gm"]  = m["greenMax"]  = OFFSETOF(greenMax);
    m["bm"]  = m["blueMax"]   = OFFSETOF(blueMax);
    m["pm"]  = m["purpleMax"] = OFFSETOF(purpleMax);
    m["om"]  = m["observerMax"]   = OFFSETOF(observerMax);

    m["Rp"]  = m["roguePlayers"]  = OFFSETOF(rogueCount);
    m["rp"]  = m["redPlayers"]    = OFFSETOF(redCount);
    m["gp"]  = m["greenPlayers"]  = OFFSETOF(greenCount);
    m["bp"]  = m["bluePlayers"]   = OFFSETOF(blueCount);
    m["pp"]  = m["purplePlayers"] = OFFSETOF(purpleCount);
    m["op"]  = m["observerPlayers"] = OFFSETOF(observerCount);

    m["Rf"]  = m["rogueFree"] = OFFSETOF(rogueFree);
    m["rf"]  = m["redFree"]   = OFFSETOF(redFree);
    m["gf"]  = m["greenFree"] = OFFSETOF(greenFree);
    m["bf"]  = m["blueFree"]  = OFFSETOF(blueFree);
    m["pf"]  = m["purpleFree"]    = OFFSETOF(purpleFree);
    m["of"]  = m["observerFree"]  = OFFSETOF(observerFree);
}


void ServerListFilter::setupPatternMap()
{
    if (!patternMap.empty())
        return;

    std::map<std::string, size_t>& m = patternMap;

    m["a"] = m["addr"] = m["address"] = OFFSETOF(addrPat);
    m["d"] = m["desc"] = m["description"] = OFFSETOF(descPat);
    m["ad"] = m["addrdesc"]       = OFFSETOF(addrDescPat);
}


bool ServerListFilter::isBoolLabel(const std::string& label)
{
    return boolMap.find(label) != boolMap.end();
}


bool ServerListFilter::isRangeLabel(const std::string& label)
{
    return rangeMap.find(label) != rangeMap.end();
}


bool ServerListFilter::isPatternLabel(const std::string& label)
{
    return patternMap.find(TextUtils::tolower(label)) != patternMap.end();
}


//============================================================================//

void ServerListFilter::print(const std::string& origIndent) const
{
    printf("%sServerListFilter <'%s'>\n", origIndent.c_str(), source.c_str());

    const std::string indent = origIndent + "  ";

    // pattern filters
    addrPat   .print("addrPat",   indent);
    descPat   .print("descPat",   indent);
    addrDescPat   .print("addrDescPat",   indent);

    // boolean filters
    ffa       .print("ffa",       indent);
    offa      .print("offa",      indent);
    ctf       .print("ctf",       indent);
    rabbit    .print("rabbit",    indent);

    jump      .print("jump",      indent);
    rico      .print("rico",      indent);
    handi     .print("handi",     indent);
    replay    .print("replay",    indent);
    inertia   .print("inertia",   indent);
    antidote  .print("antidote",  indent);
    favorite  .print("favorite",  indent);

    // range filters
    shots     .print("shots",     indent);
    players   .print("players",   indent);
    freeSlots .print("freeSlots", indent);
    validTeams    .print("validTeams",    indent);

    maxTime   .print("maxTime",   indent);
    maxPlayers    .print("maxPlayers",    indent);
    maxTeamScore  .print("maxTeamScore",  indent);
    maxPlayerScore.print("maxPlayerScore",indent);
    shakeWins .print("shakeWins", indent);
    shakeTime .print("shakeTime", indent);

    rogueCount    .print("rogueCount",    indent);
    redCount  .print("redCount",  indent);
    greenCount    .print("greenCount",    indent);
    blueCount .print("blueCount", indent);
    purpleCount   .print("purpleCount",   indent);
    observerCount .print("observerCount", indent);

    rogueMax  .print("rogueMax",  indent);
    redMax    .print("redMax",    indent);
    greenMax  .print("greenMax",  indent);
    blueMax   .print("blueMax",   indent);
    purpleMax .print("purpleMax", indent);
    observerMax   .print("observerMax",   indent);

    rogueFree .print("rogueFree", indent);
    redFree   .print("redFree",   indent);
    greenFree .print("greenFree", indent);
    blueFree  .print("blueFree",  indent);
    purpleFree    .print("purpleFree",    indent);
    observerFree  .print("observerFree",  indent);

    // OR clauses
    if (orFilter)
        orFilter->print(indent);
}


//============================================================================//
//
//  ServerListFilter::PatternFilter
//

void ServerListFilter::PatternFilter::reset()
{
    type = NoPattern;
    noCase = true;
    pattern = "";
    if (re)
    {
        regfree(re);
        delete re;
        re = NULL;
    }
}


bool ServerListFilter::PatternFilter::setupGlob(const std::string& _pattern,
        bool _noCase)
{
    reset();

    noCase = _noCase;
    pattern = _pattern;

    if (pattern.empty())
    {
        reset();
        return false;
    }

    if (noCase)
        pattern = TextUtils::tolower(pattern);

    if ((pattern.find("*") == std::string::npos) &&
            (pattern.find("?") == std::string::npos))
        pattern = "*" + pattern + "*";

    type = GlobPattern;

    return true;
}


bool ServerListFilter::PatternFilter::setupRegex(const std::string& _pattern,
        bool _noCase)
{
    reset();

    noCase = _noCase;
    pattern = _pattern;

    int opts = REG_EXTENDED | REG_NOSUB;
    if (noCase)
        opts |= REG_ICASE;

    re = new regex_t;

    const int errcode = regcomp(re, pattern.c_str(), opts);
    if (errcode != 0)
    {
        char errbuf[256];
        const size_t len = regerror(errcode, re, errbuf, sizeof(errbuf));
        const std::string errstr(errbuf, len);
        errorMessage("bad regex, " + errstr + " '" + pattern + "'");
        reset();
        return false;
    }

    type = RegexPattern;

    return true;
}


bool ServerListFilter::PatternFilter::check(const std::string& s) const
{
    switch (type)
    {
    case NoPattern:
    {
        break;
    }
    case RegexPattern:
    {
        return (regexec(re, s.c_str(), 0, NULL, 0) == 0);
    }
    case GlobPattern:
    {
        if (noCase)
            return glob_match(pattern, TextUtils::tolower(s));
        else
            return glob_match(pattern, s);
    }
    }
    return true;
}


//============================================================================//
//
//  ServerListFilter::*Filter::print()
//

void ServerListFilter::BoolFilter::print(const std::string& name,
        const std::string& indent) const
{
    if (active)
    {
        printf("%s%s = %s\n", indent.c_str(),
               name.c_str(), value ? "true" : "false");
    }
}


void ServerListFilter::RangeFilter::print(const std::string& name,
        const std::string& indent) const
{
    if (!minActive && !maxActive)
        return;
    else if (minActive && maxActive)
    {
        printf("%s%.3g < %s < %.3g\n", indent.c_str(),
               minValue, name.c_str(), maxValue);
    }
    else if (minActive)
    {
        printf("%s%s > %.3g\n", indent.c_str(),
               name.c_str(), minValue);
    }
    else if (maxActive)
    {
        printf("%s%s < %.3g\n", indent.c_str(),
               name.c_str(), maxValue);
    }
}


void ServerListFilter::PatternFilter::print(const std::string& name,
        const std::string& indent) const
{
    if (type == NoPattern)
        return;
    const char* typeStr = (type == GlobPattern) ? "glob" : "regex";
    const char* caseStr = noCase ? "nocase" : "case";
    printf("%s%s = '%s' <%s|%s>\n", indent.c_str(),
           name.c_str(), pattern.c_str(), typeStr, caseStr);
}

std::string ServerListFilter::colorizeSearch(const std::string& in)
{
    std::string out;
    std::vector<std::string> filters;
    std::vector<char> separators;
    const char *c, *s, *s0 = in.c_str();
    for (c = s = s0; true; c++)
    {
        if (*c == 0)
        {
            filters.push_back(std::string(s, c - s));
            s = c + 1;
            break;
        }
        else if ((*c == '/') || (*c == ','))
        {
            filters.push_back(std::string(s, c - s));
            separators.push_back(*c);
            s = c + 1;
        }
    }

    static const std::string controlColor = ANSI_STR_FG_WHITE;
    static const std::string badColor     = ANSI_STR_FG_ORANGE;
    static const std::string unknownColor = ANSI_STR_FG_RED;
    static const std::string commentColor = ANSI_STR_FG_BLACK;
    static const std::string boolColor    = ANSI_STR_FG_YELLOW;
    static const std::string rangeColor   = ANSI_STR_FG_GREEN;
    static const std::string globColor    = ANSI_STR_FG_CYAN;
    static const std::string regexColor   = ANSI_STR_FG_MAGENTA;

    out += filters[0];
    for (size_t i = 1; i < filters.size(); i++)
    {
        out += controlColor;
        out += separators[i - 1];
        char op;
        std::string lbl, param;
        const char type =
            ServerListFilter::parseFilterType(filters[i], op, lbl, param);
        switch (type)
        {
        case 'p':
        {
            if (ServerListFilter::isPatternLabel(lbl))
                out += (op == ')') ? globColor : regexColor;
            else
                out += unknownColor;
            break;
        }
        case 'b':
        {
            out += ServerListFilter::isBoolLabel(lbl) ? boolColor : unknownColor;
            break;
        }
        case 'r':
        {
            out += ServerListFilter::isRangeLabel(lbl) ? rangeColor : unknownColor;
            break;
        }
        case '#':
        {
            out += commentColor;
            break;
        }
        default:
        {
            out += badColor;
            break;
        }
        }
        out += filters[i];
    }

    return out;
}

//============================================================================//

// Local Variables: ***
// mode: C++ ***
// tab-width: 4 ***
// c-basic-offset: 4 ***
// indent-tabs-mode: nil ***
// End: ***
// ex: shiftwidth=4 tabstop=4
