/* bzflag
 * Copyright (c) 1993-2017 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: 8 ***
// c-basic-offset: 2 ***
// indent-tabs-mode: t ***
// End: ***
// ex: shiftwidth=2 tabstop=8
