////////////////////////////////////////////////////////////////////////////////
//
// Copyright 2006 - 2021, Tomas Babej, Paul Beckingham, Federico Hernandez.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// https://www.opensource.org/licenses/mit-license.php
//
////////////////////////////////////////////////////////////////////////////////

#include <cmake.h>
// cmake.h include header must come first

#include <CLI2.h>
#include <CmdCustom.h>
#include <CmdTimesheet.h>
#include <Color.h>
#include <Context.h>
#include <Lexer.h>
#include <format.h>
#include <shared.h>
#include <stdlib.h>
#include <utf8.h>

#include <algorithm>
#include <sstream>

// Overridden by rc.abbreviation.minimum.
int CLI2::minimumMatchLength = 3;

// Alias expansion limit. Any more indicates some kind of error.
static int safetyValveDefault = 10;

////////////////////////////////////////////////////////////////////////////////
A2::A2(const std::string& raw, Lexer::Type lextype) {
  _lextype = lextype;
  attribute("raw", raw);
}

////////////////////////////////////////////////////////////////////////////////
A2::A2(const A2& other) = default;

////////////////////////////////////////////////////////////////////////////////
A2& A2::operator=(const A2& other) = default;

////////////////////////////////////////////////////////////////////////////////
bool A2::hasTag(const std::string& tag) const {
  return std::find(_tags.begin(), _tags.end(), tag) != _tags.end();
}

////////////////////////////////////////////////////////////////////////////////
void A2::tag(const std::string& tag) {
  if (!hasTag(tag)) _tags.push_back(tag);
}

////////////////////////////////////////////////////////////////////////////////
void A2::unTag(const std::string& tag) {
  for (auto i = _tags.begin(); i != _tags.end(); ++i)
    if (*i == tag) {
      _tags.erase(i);
      break;
    }
}

////////////////////////////////////////////////////////////////////////////////
// Accessor for attributes.
void A2::attribute(const std::string& name, const std::string& value) {
  _attributes[name] = value;

  if (name == "raw") decompose();
}

////////////////////////////////////////////////////////////////////////////////
// Accessor for attributes.
const std::string A2::attribute(const std::string& name) const {
  // Prevent autovivification.
  auto i = _attributes.find(name);
  if (i != _attributes.end()) return i->second;

  return "";
}

////////////////////////////////////////////////////////////////////////////////
const std::string A2::getToken() const {
  auto i = _attributes.find("canonical");
  if (i == _attributes.end()) i = _attributes.find("raw");

  return i->second;
}

////////////////////////////////////////////////////////////////////////////////
void A2::decompose() {
  if (_lextype == Lexer::Type::tag) {
    std::string raw = _attributes["raw"];
    attribute("name", raw.substr(1));
    attribute("sign", raw.substr(0, 1));
  }

  else if (_lextype == Lexer::Type::substitution) {
    // if (Directory (raw).exists ())
    //   return;

    std::string from;
    std::string to;
    std::string flags;
    if (Lexer::decomposeSubstitution(_attributes["raw"], from, to, flags)) {
      attribute("from", from);
      attribute("to", to);
      attribute("flags", flags);
    }
  }

  else if (_lextype == Lexer::Type::pair) {
    std::string name;
    std::string mod;
    std::string sep;
    std::string value;
    if (Lexer::decomposePair(_attributes["raw"], name, mod, sep, value)) {
      attribute("name", name);
      attribute("modifier", mod);
      attribute("separator", sep);
      attribute("value", value);

      if (name == "rc") {
        if (mod != "")
          tag("CONFIG");
        else
          tag("RC");
      }
    }
  }

  else if (_lextype == Lexer::Type::pattern) {
    // if (Directory (raw).exists ())
    //   return;

    std::string pattern;
    std::string flags;
    if (Lexer::decomposePattern(_attributes["raw"], pattern, flags)) {
      attribute("pattern", pattern);
      attribute("flags", flags);
    }
  }
}

////////////////////////////////////////////////////////////////////////////////
const std::string A2::dump() const {
  auto output = Lexer::typeToString(_lextype);

  // Dump attributes.
  std::string atts;
  for (const auto& a : _attributes) atts += a.first + "='\033[33m" + a.second + "\033[0m' ";

  // Dump tags.
  std::string tags;
  for (const auto& tag : _tags) {
    if (tag == "BINARY")
      tags += "\033[1;37;44m" + tag + "\033[0m ";
    else if (tag == "CMD")
      tags += "\033[1;37;46m" + tag + "\033[0m ";
    else if (tag == "FILTER")
      tags += "\033[1;37;42m" + tag + "\033[0m ";
    else if (tag == "MODIFICATION")
      tags += "\033[1;37;43m" + tag + "\033[0m ";
    else if (tag == "MISCELLANEOUS")
      tags += "\033[1;37;45m" + tag + "\033[0m ";
    else if (tag == "RC")
      tags += "\033[1;37;41m" + tag + "\033[0m ";
    else if (tag == "CONFIG")
      tags += "\033[1;37;101m" + tag + "\033[0m ";
    else if (tag == "?")
      tags += "\033[38;5;255;48;5;232m" + tag + "\033[0m ";
    else
      tags += "\033[32m" + tag + "\033[0m ";
  }

  return output + ' ' + atts + tags;
}

////////////////////////////////////////////////////////////////////////////////
static const char* getValue(int argc, const char** argv, std::string arg) {
  const auto is_arg = [&](std::string s) {
    return s.size() > arg.size() + 1 && (s[arg.size()] == ':' || s[arg.size()] == '=') &&
           s.compare(0, arg.size(), arg) == 0;
  };
  // find last argument before --
  auto last = std::make_reverse_iterator(argv);
  auto first = std::make_reverse_iterator(std::find(argv, argv + argc, std::string("--")));
  auto it = std::find_if(first, last, is_arg);
  if (it == last) return nullptr;
  // return the string after : or =
  return *it + arg.size() + 1;
}

////////////////////////////////////////////////////////////////////////////////
// Static method.
bool CLI2::getOverride(int argc, const char** argv, File& rc) {
  const char* value = getValue(argc, argv, "rc");
  if (value == nullptr) return false;
  rc = File(value);
  return true;
}

////////////////////////////////////////////////////////////////////////////////
// Look for CONFIG data.location and initialize a Path object.
// Static method.
bool CLI2::getDataLocation(int argc, const char** argv, Path& data) {
  const char* value = getValue(argc, argv, "rc.data.location");
  if (value == nullptr) {
    std::string location = Context::getContext().config.get("data.location");
    if (location != "") data = location;
    return false;
  }
  data = Directory(value);
  return true;
}

////////////////////////////////////////////////////////////////////////////////
// Static method.
void CLI2::applyOverrides(int argc, const char** argv) {
  auto& context = Context::getContext();
  auto last = std::find(argv, argv + argc, std::string("--"));
  auto is_override = [](const std::string& s) { return s.compare(0, 3, "rc.") == 0; };
  auto get_sep = [&](const std::string& s) {
    if (is_override(s)) return s.find_first_of(":=", 3);
    return std::string::npos;
  };
  auto override_settings = [&](std::string raw) {
    auto sep = get_sep(raw);
    if (sep == std::string::npos) return;
    std::string name = raw.substr(3, sep - 3);
    std::string value = raw.substr(sep + 1);
    context.config.set(name, value);
  };
  auto display_overrides = [&](std::string raw) {
    if (is_override(raw)) context.footnote(format("Configuration override {1}", raw));
  };
  std::for_each(argv, last, override_settings);
  if (context.verbose("override")) std::for_each(argv, last, display_overrides);
}

////////////////////////////////////////////////////////////////////////////////
void CLI2::alias(const std::string& name, const std::string& value) { _aliases[name] = value; }

////////////////////////////////////////////////////////////////////////////////
void CLI2::entity(const std::string& category, const std::string& name) {
  // Walk the list of entities for category.
  auto c = _entities.equal_range(category);
  for (auto e = c.first; e != c.second; ++e)
    if (e->second == name) return;

  // The category/name pair was not found, therefore add it.
  _entities.emplace(category, name);
}

////////////////////////////////////////////////////////////////////////////////
// Capture a single argument.
void CLI2::add(const std::string& argument) {
  A2 arg(Lexer::trim(argument), Lexer::Type::word);
  arg.tag("ORIGINAL");
  _original_args.push_back(arg);

  // Adding a new argument invalidates prior analysis.
  _args.clear();
}

////////////////////////////////////////////////////////////////////////////////
// Capture a set of arguments, inserted immediately after <offset> arguments
// after the binary..
void CLI2::add(const std::vector<std::string>& arguments, int offset /* = 0 */) {
  std::vector<A2> replacement{_original_args.begin(), _original_args.begin() + offset + 1};

  for (const auto& arg : arguments) replacement.emplace_back(arg, Lexer::Type::word);

  for (unsigned int i = 1 + offset; i < _original_args.size(); ++i)
    replacement.push_back(_original_args[i]);

  _original_args = replacement;

  // Adding a new argument invalidates prior analysis.
  _args.clear();
}

////////////////////////////////////////////////////////////////////////////////
// Arg0 is the first argument, which is the name and potentially a relative or
// absolute path to the invoked binary.
//
// The binary name is 'task', but if the binary is reported as 'cal' or
// 'calendar' then it was invoked via symbolic link, in which case capture the
// first argument as 'calendar'.
void CLI2::handleArg0() {
  // Capture arg0 separately, because it is the command that was run, and could
  // need special handling.
  auto raw = _original_args[0].attribute("raw");
  A2 a(raw, Lexer::Type::word);
  a.tag("BINARY");

  std::string basename = "task";
  auto slash = raw.rfind('/');
  if (slash != std::string::npos) basename = raw.substr(slash + 1);

  a.attribute("basename", basename);
  if (basename == "cal" || basename == "calendar") {
    _args.push_back(a);

    A2 cal("calendar", Lexer::Type::word);
    _args.push_back(cal);
  } else {
    _args.push_back(a);
  }
}

////////////////////////////////////////////////////////////////////////////////
// All arguments must be individually and wholly recognized by the Lexer. Any
// argument not recognized is considered a Lexer::Type::word.
//
// As a side effect, tags all arguments after a terminator ('--') with
// TERMINATED.
void CLI2::lexArguments() {
  // Note: Starts iterating at index 1, because ::handleArg0 has already
  //       processed it.
  bool terminated = false;
  for (unsigned int i = 1; i < _original_args.size(); ++i) {
    bool quoted = Lexer::wasQuoted(_original_args[i].attribute("raw"));

    // Process single-token arguments.
    std::string lexeme;
    Lexer::Type type;
    Lexer lex(_original_args[i].attribute("raw"));
    if (lex.token(lexeme, type) &&
        (lex.isEOS() ||                           // Token goes to EOS
         (quoted && type == Lexer::Type::pair)))  // Quoted pairs automatically go to EOS
    {
      if (!terminated && type == Lexer::Type::separator)
        terminated = true;
      else if (terminated)
        type = Lexer::Type::word;

      A2 a(_original_args[i].attribute("raw"), type);
      if (terminated) a.tag("TERMINATED");
      if (quoted) a.tag("QUOTED");

      if (_original_args[i].hasTag("ORIGINAL")) a.tag("ORIGINAL");

      _args.push_back(a);
    }

    // Process multiple-token arguments.
    else {
      const std::string quote = "'";

      // Escape unescaped single quotes
      std::string escaped = "";

      // For performance reasons. The escaped string is as long as the original.
      escaped.reserve(_original_args[i].attribute("raw").size());

      std::string::size_type cursor = 0;
      bool nextEscaped = false;
      while (int num = utf8_next_char(_original_args[i].attribute("raw"), cursor)) {
        std::string character = utf8_character(num);
        if (!nextEscaped && (character == "\\"))
          nextEscaped = true;
        else {
          if (character == quote && !nextEscaped) escaped += "\\";
          nextEscaped = false;
        }
        escaped += character;
      }

      cursor = 0;
      std::string word;
      if (Lexer::readWord(quote + escaped + quote, quote, cursor, word)) {
        Lexer::dequote(word);
        A2 unknown(word, Lexer::Type::word);
        if (lex.wasQuoted(_original_args[i].attribute("raw"))) unknown.tag("QUOTED");

        if (_original_args[i].hasTag("ORIGINAL")) unknown.tag("ORIGINAL");

        _args.push_back(unknown);
      }

      // This branch may have no use-case.
      else {
        A2 unknown(_original_args[i].attribute("raw"), Lexer::Type::word);
        unknown.tag("UNKNOWN");

        if (lex.wasQuoted(_original_args[i].attribute("raw"))) unknown.tag("QUOTED");

        if (_original_args[i].hasTag("ORIGINAL")) unknown.tag("ORIGINAL");

        _args.push_back(unknown);
      }
    }
  }

  if (Context::getContext().config.getInteger("debug.parser") >= 2)
    Context::getContext().debug(dump("CLI2::analyze lexArguments"));
}

////////////////////////////////////////////////////////////////////////////////
// [1] Scan all args for the 'add' and 'log' commands, and demote any
//     Lexer::Type::Tag args with sign '-' to Lexer::Type::word.
// [2] Convert any pseudo args name:value into config settings, and erase.
void CLI2::demotion() {
  bool changes = false;
  std::vector<A2> replacement;

  std::string canonical;
  for (auto& a : _args) {
    if (a._lextype == Lexer::Type::tag && a.attribute("sign") == "-") {
      std::string command = getCommand();
      if (command == "add" || command == "log") {
        a._lextype = Lexer::Type::word;
        changes = true;
      }
    }

    else if (a._lextype == Lexer::Type::pair &&
             canonicalize(canonical, "pseudo", a.attribute("name"))) {
      Context::getContext().config.set(canonical, a.attribute("value"));
      changes = true;

      // Equivalent to erasing 'a'.
      continue;
    }

    replacement.push_back(a);
  }

  if (changes && Context::getContext().config.getInteger("debug.parser") >= 2)
    Context::getContext().debug(dump("CLI2::analyze demotion"));
}

////////////////////////////////////////////////////////////////////////////////
// Intended to be called after ::add() to perform the final analysis.
void CLI2::analyze() {
  if (Context::getContext().config.getInteger("debug.parser") >= 2)
    Context::getContext().debug(dump("CLI2::analyze"));

  // Process _original_args.
  _args.clear();
  handleArg0();
  lexArguments();

  // Process _args.
  aliasExpansion();
  if (!findCommand()) {
    defaultCommand();
    if (!findCommand()) throw std::string("You must specify a command or a task to modify.");
  }

  demotion();
  canonicalizeNames();

  // Determine arg types: FILTER, MODIFICATION, MISCELLANEOUS.
  categorizeArgs();
  parenthesizeOriginalFilter();

  // Cache frequently looked up items
  _command = getCommand();
}

////////////////////////////////////////////////////////////////////////////////
// Process raw filter string.
// Insert filter arguments (wrapped in parentheses) immediatelly after the binary.
void CLI2::addFilter(const std::string& arg) {
  if (arg.length()) {
    std::vector<std::string> filter;
    filter.push_back("(");

    std::string lexeme;
    Lexer::Type type;
    Lexer lex(arg);

    while (lex.token(lexeme, type)) filter.push_back(lexeme);

    filter.push_back(")");
    add(filter);
    analyze();
  }
}

////////////////////////////////////////////////////////////////////////////////
// Process raw modification string.
// Insert modification arguments immediatelly after the command (i.e. 'add')
void CLI2::addModifications(const std::string& arg) {
  if (arg.length()) {
    std::vector<std::string> mods;

    std::string lexeme;
    Lexer::Type type;
    Lexer lex(arg);

    while (lex.token(lexeme, type)) mods.push_back(lexeme);

    // Determine at which argument index does the task modification command
    // reside
    unsigned int cmdIndex = 0;
    for (; cmdIndex < _args.size(); ++cmdIndex) {
      // Command found, stop iterating.
      if (_args[cmdIndex].hasTag("CMD")) break;
    }

    // Insert modifications after the command.
    add(mods, cmdIndex);
    analyze();
  }
}

////////////////////////////////////////////////////////////////////////////////
// There are situations where a context filter is applied. This method
// determines whether one applies, and if so, applies it. Disqualifiers include:
//   - filter contains ID or UUID
void CLI2::addContext(bool readable, bool writeable) {
  // Recursion block.
  if (_context_added) return;

  // Detect if any context is set, and bail out if not
  std::string contextString;
  if (readable)
    // Empty string is treated as "currently selected context"
    contextString = Context::getContext().getTaskContext("read", "");
  else if (writeable)
    contextString = Context::getContext().getTaskContext("write", "");
  else
    return;

  // If context is empty, bail out too
  if (contextString.empty()) return;

  // For readable contexts: Detect if UUID or ID is set, and bail out
  if (readable)
    for (auto& a : _args) {
      if (a._lextype == Lexer::Type::uuid || a._lextype == Lexer::Type::number ||
          a._lextype == Lexer::Type::set) {
        Context::getContext().debug(
            format("UUID/ID argument found '{1}', not applying context.", a.attribute("raw")));
        return;
      }
    }

  // Apply the context. Readable (filtering) takes precedence. Also set the
  // block now, since addFilter calls analyze(), which calls addContext().
  _context_added = true;
  if (readable)
    addFilter(contextString);
  else if (writeable)
    addModifications(contextString);

  // Inform the user about the application of context
  if (Context::getContext().verbose("context"))
    Context::getContext().footnote(format("Context '{1}' set. Use 'task context none' to remove.",
                                          Context::getContext().config.get("context")));
}

////////////////////////////////////////////////////////////////////////////////
// Parse the command line, identifiying filter components, expanding syntactic
// sugar as necessary.
void CLI2::prepareFilter() {
  // Clear and re-populate.
  _id_ranges.clear();
  _uuid_list.clear();
  _context_added = false;

  // Remove all the syntactic sugar for FILTERs.
  lexFilterArgs();
  findIDs();
  findUUIDs();
  insertIDExpr();
  desugarFilterPlainArgs();
  findStrayModifications();
  desugarFilterTags();
  desugarFilterAttributes();
  desugarFilterPatterns();
  insertJunctions();  // Deliberately after all desugar calls.

  if (Context::getContext().verbose("filter")) {
    std::string combined;
    for (const auto& a : _args) {
      if (a.hasTag("FILTER")) {
        if (combined != "") combined += ' ';

        combined += a.attribute("raw");
      }
    }

    if (combined.size()) Context::getContext().footnote(std::string("Filter: ") + combined);
  }
}

////////////////////////////////////////////////////////////////////////////////
// Return all the MISCELLANEOUS args as strings.
const std::vector<std::string> CLI2::getWords() {
  std::vector<std::string> words;
  for (const auto& a : _args)
    if (a.hasTag("MISCELLANEOUS")) words.push_back(a.attribute("raw"));

  if (Context::getContext().config.getInteger("debug.parser") >= 2) {
    Color colorOrigArgs("gray10 on gray4");
    std::string message = " ";
    for (const auto& word : words) message += colorOrigArgs.colorize(word) + ' ';
    Context::getContext().debug("CLI2::getWords" + message);
  }

  return words;
}

////////////////////////////////////////////////////////////////////////////////
// Return all the MISCELLANEOUS args.
const std::vector<A2> CLI2::getMiscellaneous() {
  std::vector<A2> misc;
  for (const auto& a : _args)
    if (a.hasTag("MISCELLANEOUS")) misc.push_back(a);

  return misc;
}

////////////////////////////////////////////////////////////////////////////////
// Search for 'value' in _entities category, return canonicalized value.
bool CLI2::canonicalize(std::string& canonicalized, const std::string& category,
                        const std::string& value) {
  // Utilize a cache mapping of (category, value) -> canonicalized value.
  // This cache does not need to be invalidated, because entities are defined
  // only once per initialization of the Context object.
  int cache_key = 31 * std::hash<std::string>{}(category) + std::hash<std::string>{}(value);
  auto cache_result = _canonical_cache.find(cache_key);
  if (cache_result != _canonical_cache.end()) {
    canonicalized = cache_result->second;
    return true;
  }

  // Extract a list of entities for category.
  std::vector<std::string> options;
  auto c = _entities.equal_range(category);
  for (auto e = c.first; e != c.second; ++e) {
    // Shortcut: if an exact match is found, success.
    if (value == e->second) {
      canonicalized = value;
      _canonical_cache[cache_key] = value;
      return true;
    }

    options.push_back(e->second);
  }

  // Match against the options, throw away results.
  std::vector<std::string> matches;
  if (autoComplete(value, options, matches, minimumMatchLength) == 1) {
    canonicalized = matches[0];
    _canonical_cache[cache_key] = matches[0];
    return true;
  }

  return false;
}

////////////////////////////////////////////////////////////////////////////////
std::string CLI2::getBinary() const {
  if (_args.size()) return _args[0].attribute("raw");

  return "";
}

////////////////////////////////////////////////////////////////////////////////
std::string CLI2::getCommand(bool canonical) const {
  // Shortcut if analysis has been finalized
  if (_command != "") return _command;

  for (const auto& a : _args)
    if (a.hasTag("CMD")) return a.attribute(canonical ? "canonical" : "raw");

  return "";
}

////////////////////////////////////////////////////////////////////////////////
const std::string CLI2::dump(const std::string& title) const {
  std::stringstream out;

  out << "\033[1m" << title << "\033[0m\n"
      << "  _original_args\n    ";

  Color colorArgs("gray10 on gray4");
  Color colorFilter("black on rgb311");
  for (auto i = _original_args.begin(); i != _original_args.end(); ++i) {
    if (i != _original_args.begin()) out << ' ';

    if (i->hasTag("ORIGINAL"))
      out << colorArgs.colorize(i->attribute("raw"));
    else
      out << colorFilter.colorize(i->attribute("raw"));
  }

  out << '\n';

  if (_args.size()) {
    out << "  _args\n";
    for (const auto& a : _args) out << "    " << a.dump() << '\n';
  }

  if (_id_ranges.size()) {
    out << "  _id_ranges\n    ";
    for (const auto& range : _id_ranges) {
      if (range.first != range.second)
        out << colorArgs.colorize(range.first + "-" + range.second) << ' ';
      else
        out << colorArgs.colorize(range.first) << ' ';
    }

    out << '\n';
  }

  if (_uuid_list.size()) {
    out << "  _uuid_list\n    ";
    for (const auto& uuid : _uuid_list) out << colorArgs.colorize(uuid) << ' ';

    out << '\n';
  }

  return out.str();
}

////////////////////////////////////////////////////////////////////////////////
// If any aliases are found in un-TERMINATED arguments, replace the alias with
// a set of Lexed tokens from the configuration.
void CLI2::aliasExpansion() {
  bool changes = false;
  bool action;
  int counter = 0;
  do {
    action = false;
    std::vector<A2> reconstructed;

    std::string raw;
    for (const auto& i : _args) {
      raw = i.attribute("raw");
      if (i.hasTag("TERMINATED")) {
        reconstructed.push_back(i);
      } else if (_aliases.find(raw) != _aliases.end()) {
        std::string lexeme;
        Lexer::Type type;
        Lexer lex(_aliases[raw]);
        while (lex.token(lexeme, type)) reconstructed.emplace_back(lexeme, type);

        action = true;
        changes = true;
      } else {
        reconstructed.push_back(i);
      }
    }

    _args = reconstructed;

    std::vector<A2> reconstructedOriginals;
    bool terminated = false;
    for (const auto& i : _original_args) {
      if (i.attribute("raw") == "--") terminated = true;

      if (terminated) {
        reconstructedOriginals.push_back(i);
      } else if (_aliases.find(i.attribute("raw")) != _aliases.end()) {
        std::string lexeme;
        Lexer::Type type;
        Lexer lex(_aliases[i.attribute("raw")]);
        while (lex.token(lexeme, type)) reconstructedOriginals.emplace_back(lexeme, type);

        action = true;
        changes = true;
      } else {
        reconstructedOriginals.push_back(i);
      }
    }

    _original_args = reconstructedOriginals;
  } while (action && counter++ < safetyValveDefault);

  if (counter >= safetyValveDefault)
    Context::getContext().debug(format("Nested alias limit of {1} reached.", safetyValveDefault));

  if (changes && Context::getContext().config.getInteger("debug.parser") >= 2)
    Context::getContext().debug(dump("CLI2::analyze aliasExpansion"));
}

////////////////////////////////////////////////////////////////////////////////
// Scan all arguments and canonicalize names that need it.
void CLI2::canonicalizeNames() {
  bool changes = false;
  for (auto& a : _args) {
    if (a._lextype == Lexer::Type::pair) {
      std::string raw = a.attribute("raw");
      if (raw.substr(0, 3) != "rc:" && raw.substr(0, 3) != "rc.") {
        std::string name = a.attribute("name");
        std::string canonical;
        if (canonicalize(canonical, "pseudo", name) || canonicalize(canonical, "attribute", name)) {
          a.attribute("canonical", canonical);
        } else {
          a._lextype = Lexer::Type::word;
        }

        changes = true;
      }
    }
  }

  if (changes && Context::getContext().config.getInteger("debug.parser") >= 2)
    Context::getContext().debug(dump("CLI2::analyze canonicalizeNames"));
}

////////////////////////////////////////////////////////////////////////////////
// Categorize FILTER, MODIFICATION and MISCELLANEOUS args, based on CMD DNA.
void CLI2::categorizeArgs() {
  // Context is only applied for commands that request it.
  std::string command = getCommand();
  Command* cmd = Context::getContext().commands[command];

  // Determine if the command uses Context. CmdCustom and CmdTimesheet need to
  // be handled separately, as they override the parent Command::use_context
  // method, and this is a pointer to Command class.
  //
  // All Command classes overriding uses_context () getter need to be specified
  // here.
  bool uses_context;
  if (dynamic_cast<CmdCustom*>(cmd))
    uses_context = (dynamic_cast<CmdCustom*>(cmd))->uses_context();
  else if (dynamic_cast<CmdTimesheet*>(cmd))
    uses_context = (dynamic_cast<CmdTimesheet*>(cmd))->uses_context();
  else if (cmd)
    uses_context = cmd->uses_context();

  // Apply the context, if applicable
  if (cmd && uses_context) addContext(cmd->accepts_filter(), cmd->accepts_modifications());

  bool changes = false;
  bool afterCommand = false;
  for (auto& a : _args) {
    if (a._lextype == Lexer::Type::separator) continue;

    // Record that the command has been found, it affects behavior.
    if (a.hasTag("CMD")) {
      afterCommand = true;
    }

    // Skip admin args.
    else if (a.hasTag("BINARY") || a.hasTag("RC") || a.hasTag("CONFIG")) {
      // NOP.
    }

    // All combinations, with all 8 cases handled below.:
    //
    //   -- -- --   Error: found an arg, but none expected
    //   -- -- Mi   task [Mi] <cmd> [Mi]
    //   -- Mo --   task [Mo] <cmd> [Mo]
    //   -- Mo Mi   Internally inconsistent
    //   Fi -- --   task [Fi] <cmd> [Fi]
    //   Fi -- Mi   task [Fi] <cmd> [Mi]
    //   Fi Mo --   task [Fi] <cmd> [Mo]
    //   Fi Mo Mi   Internally inconsistent
    //
    else if (cmd && !cmd->accepts_filter() && !cmd->accepts_modifications() &&
             !cmd->accepts_miscellaneous()) {
      // No commands were expected --> error.
      throw format("The '{1}' command does not allow '{2}'.", command, a.attribute("raw"));
    } else if (cmd && !cmd->accepts_filter() && !cmd->accepts_modifications() &&
               cmd->accepts_miscellaneous()) {
      a.tag("MISCELLANEOUS");
      changes = true;
    } else if (cmd && !cmd->accepts_filter() && cmd->accepts_modifications() &&
               !cmd->accepts_miscellaneous()) {
      a.tag("MODIFICATION");
      changes = true;
    } else if (cmd && !cmd->accepts_filter() && cmd->accepts_modifications() &&
               cmd->accepts_miscellaneous()) {
      // Error: internally inconsistent.
      throw std::string("Unknown error. Please report.");
    } else if (cmd && cmd->accepts_filter() && !cmd->accepts_modifications() &&
               !cmd->accepts_miscellaneous()) {
      a.tag("FILTER");
      changes = true;
    } else if (cmd && cmd->accepts_filter() && !cmd->accepts_modifications() &&
               cmd->accepts_miscellaneous()) {
      if (!afterCommand)
        a.tag("FILTER");
      else
        a.tag("MISCELLANEOUS");

      changes = true;
    } else if (cmd && cmd->accepts_filter() && cmd->accepts_modifications() &&
               !cmd->accepts_miscellaneous()) {
      if (!afterCommand)
        a.tag("FILTER");
      else
        a.tag("MODIFICATION");

      changes = true;
    } else if (cmd && cmd->accepts_filter() && cmd->accepts_modifications() &&
               cmd->accepts_miscellaneous()) {
      // Error: internally inconsistent.
      throw std::string("Unknown error. Please report.");
    }
  }

  if (changes && Context::getContext().config.getInteger("debug.parser") >= 2)
    Context::getContext().debug(dump("CLI2::analyze categorizeArgs"));
}

////////////////////////////////////////////////////////////////////////////////
// The following command:
//
//    task +home or +work list
//
// Is reasonable, and does not work unless the filter is parenthesized. Ignoring
// context, the 'list' report has a filter, which is inserted at the beginning
// like this:
//
//   task ( status:pending ) +home or +work list
//
// Parenthesizing the user-provided (original) filter yields this:
//
//   task ( status:pending ) ( +home or +work ) list
//
// And when the conjunction is added:
//
//   task ( status:pending ) and ( +home or +work ) list
//
// the query is correct.
void CLI2::parenthesizeOriginalFilter() {
  // Locate the first and last ORIGINAL FILTER args.
  unsigned int firstOriginalFilter = 0;
  unsigned int lastOriginalFilter = 0;
  for (unsigned int i = 1; i < _args.size(); ++i) {
    if (_args[i].hasTag("FILTER") && _args[i].hasTag("ORIGINAL")) {
      if (firstOriginalFilter == 0) firstOriginalFilter = i;

      lastOriginalFilter = i;
    }
  }

  // If found, parenthesize the arg list accordingly.
  if (firstOriginalFilter && lastOriginalFilter) {
    std::vector<A2> reconstructed;
    for (unsigned int i = 0; i < _args.size(); ++i) {
      if (i == firstOriginalFilter) {
        A2 openParen("(", Lexer::Type::op);
        openParen.tag("ORIGINAL");
        openParen.tag("FILTER");
        reconstructed.push_back(openParen);
      }

      reconstructed.push_back(_args[i]);

      if (i == lastOriginalFilter) {
        A2 closeParen(")", Lexer::Type::op);
        closeParen.tag("ORIGINAL");
        closeParen.tag("FILTER");
        reconstructed.push_back(closeParen);
      }
    }

    _args = reconstructed;

    if (Context::getContext().config.getInteger("debug.parser") >= 2)
      Context::getContext().debug(dump("CLI2::analyze parenthesizeOriginalFilter"));
  }
}

////////////////////////////////////////////////////////////////////////////////
// Scan all arguments and if any are an exact match for a command name, then
// tag as CMD. If an argument is an exact match for an attribute, despite being
// an inexact match for a command, then it is not a command.
bool CLI2::findCommand() {
  for (auto& a : _args) {
    std::string raw = a.attribute("raw");
    std::string canonical;

    // If the arg canonicalized to a 'cmd', but is also not an exact match
    // for an 'attribute', proceed. Example:
    //   task project=foo list
    //        ^cmd        ^cmd
    //        ^attribute
    if (exactMatch("cmd", raw))
      canonical = raw;
    else if (exactMatch("attribute", raw))
      continue;
    else if (!canonicalize(canonical, "cmd", raw))
      continue;

    a.attribute("canonical", canonical);
    a.tag("CMD");

    // Apply command DNA as tags.
    Command* command = Context::getContext().commands[canonical];
    if (command->read_only()) a.tag("READONLY");
    if (command->displays_id()) a.tag("SHOWSID");
    if (command->needs_gc()) a.tag("RUNSGC");
    if (command->uses_context()) a.tag("USESCONTEXT");
    if (command->accepts_filter()) a.tag("ALLOWSFILTER");
    if (command->accepts_modifications()) a.tag("ALLOWSMODIFICATIONS");
    if (command->accepts_miscellaneous()) a.tag("ALLOWSMISC");

    if (Context::getContext().config.getInteger("debug.parser") >= 2)
      Context::getContext().debug(dump("CLI2::analyze findCommand"));

    // Stop and indicate command found.
    return true;
  }

  // Indicate command not found.
  return false;
}

////////////////////////////////////////////////////////////////////////////////
// Search for exact 'value' in _entities category.
bool CLI2::exactMatch(const std::string& category, const std::string& value) const {
  // Extract a list of entities for category.
  auto c = _entities.equal_range(category);
  for (auto e = c.first; e != c.second; ++e)
    if (value == e->second) return true;

  return false;
}

////////////////////////////////////////////////////////////////////////////////
// +tag --> tags _hastag_ tag
// -tag --> tags _notag_ tag
void CLI2::desugarFilterTags() {
  bool changes = false;
  std::vector<A2> reconstructed;
  for (const auto& a : _args) {
    if (a._lextype == Lexer::Type::tag && a.hasTag("FILTER")) {
      changes = true;

      A2 left("tags", Lexer::Type::dom);
      left.tag("FILTER");
      reconstructed.push_back(left);

      std::string raw = a.attribute("raw");

      A2 op(raw[0] == '+' ? "_hastag_" : "_notag_", Lexer::Type::op);
      op.tag("FILTER");
      reconstructed.push_back(op);

      A2 right("" + raw.substr(1) + "", Lexer::Type::string);
      right.tag("FILTER");
      reconstructed.push_back(right);
    } else
      reconstructed.push_back(a);
  }

  if (changes) {
    _args = reconstructed;

    if (Context::getContext().config.getInteger("debug.parser") >= 2)
      Context::getContext().debug(dump("CLI2::prepareFilter desugarFilterTags"));
  }
}

////////////////////////////////////////////////////////////////////////////////
void CLI2::findStrayModifications() {
  bool changes = false;

  auto command = getCommand();
  if (command == "add" || command == "log") {
    for (auto& a : _args) {
      if (a.hasTag("FILTER")) {
        a.unTag("FILTER");
        a.tag("MODIFICATION");
        changes = true;
      }
    }
  }

  if (changes)
    if (Context::getContext().config.getInteger("debug.parser") >= 2)
      Context::getContext().debug(dump("CLI2::prepareFilter findStrayModifications"));
}

////////////////////////////////////////////////////////////////////////////////
// <name>[.<mod>]:['"][<value>]['"] --> name <op> value
void CLI2::desugarFilterAttributes() {
  bool changes = false;
  std::vector<A2> reconstructed;
  for (auto& a : _args) {
    if (a._lextype == Lexer::Type::pair && a.hasTag("FILTER")) {
      std::string raw = a.attribute("raw");
      std::string name = a.attribute("name");
      std::string mod = a.attribute("modifier");
      std::string sep = a.attribute("separator");
      std::string value = a.attribute("value");

      // An unquoted string, while equivalent to an empty string, doesn't cause
      // an operand shortage in eval.
      if (value == "") value = "''";

      // Some values are expressions, which need to be lexed. The best way to
      // determine whether an expression is either a single value, or needs to
      // be lexed, is to lex it and count the tokens. For example:
      //    now+1d
      // This should be lexed and surrounded by parentheses:
      //    (
      //    now
      //    +
      //    1d
      //    )
      // Use this sequence in place of a single value.
      std::vector<A2> values = lexExpression(value);
      if (Context::getContext().config.getInteger("debug.parser") >= 2) {
        Context::getContext().debug("CLI2::lexExpression " + name + ':' + value);
        for (auto& v : values) Context::getContext().debug("  " + v.dump());
        Context::getContext().debug(" ");
      }

      bool found = false;
      std::string canonical;
      if (canonicalize(canonical, "attribute", name)) {
        // Certain attribute types do not suport math.
        //   string   --> no
        //   numeric  --> yes
        //   date     --> yes
        //   duration --> yes
        bool evalSupported = true;
        Column* col = Context::getContext().columns[canonical];
        if (col && col->type() == "string") evalSupported = false;

        A2 lhs(name, Lexer::Type::dom);
        lhs.tag("FILTER");
        lhs.attribute("canonical", canonical);
        lhs.attribute("modifier", mod);

        A2 op("", Lexer::Type::op);
        op.tag("FILTER");

        // Attribute types that do not support evaluation should be interpreted
        // as strings (currently this means that string attributes are not evaluated)
        A2 rhs("", evalSupported ? values[0]._lextype : Lexer::Type::string);
        rhs.tag("FILTER");

        // Special case for '<name>:<value>'.
        if (mod == "") {
          op.attribute("raw", "=");
          rhs.attribute("raw", value);
        } else if (mod == "before" || mod == "under" || mod == "below") {
          op.attribute("raw", "<");
          rhs.attribute("raw", value);
        } else if (mod == "after" || mod == "over" || mod == "above") {
          op.attribute("raw", ">");
          rhs.attribute("raw", value);
        } else if (mod == "by") {
          op.attribute("raw", "<=");
          rhs.attribute("raw", value);
        } else if (mod == "none") {
          op.attribute("raw", "==");
          rhs.attribute("raw", "''");
        } else if (mod == "any") {
          op.attribute("raw", "!==");
          rhs.attribute("raw", "''");
        } else if (mod == "is" || mod == "equals") {
          op.attribute("raw", "==");
          rhs.attribute("raw", value);
        } else if (mod == "not") {
          op.attribute("raw", "!=");
          rhs.attribute("raw", value);
        } else if (mod == "isnt") {
          op.attribute("raw", "!==");
          rhs.attribute("raw", value);
        } else if (mod == "has" || mod == "contains") {
          op.attribute("raw", "~");
          rhs.attribute("raw", value);
        } else if (mod == "hasnt") {
          op.attribute("raw", "!~");
          rhs.attribute("raw", value);
        } else if (mod == "startswith" || mod == "left") {
          op.attribute("raw", "~");
          rhs.attribute("raw", "^" + value);
        } else if (mod == "endswith" || mod == "right") {
          op.attribute("raw", "~");
          rhs.attribute("raw", value + "$");
        } else if (mod == "word") {
          op.attribute("raw", "~");
#if defined(DARWIN)
          rhs.attribute("raw", value);
#elif defined(SOLARIS)
          rhs.attribute("raw", "\\<" + value + "\\>");
#else
          rhs.attribute("raw", "\\b" + value + "\\b");
#endif
        } else if (mod == "noword") {
          op.attribute("raw", "!~");
#if defined(DARWIN)
          rhs.attribute("raw", value);
#elif defined(SOLARIS)
          rhs.attribute("raw", "\\<" + value + "\\>");
#else
          rhs.attribute("raw", "\\b" + value + "\\b");
#endif
        } else
          throw format("Error: unrecognized attribute modifier '{1}'.", mod);

        reconstructed.push_back(lhs);
        reconstructed.push_back(op);

        // Do not modify this construct without full understanding.
        // Getting this wrong breaks a whole lot of filtering tests.
        if (evalSupported) {
          for (auto& v : values) reconstructed.push_back(v);
        } else if (Lexer::isDOM(rhs.attribute("raw"))) {
          rhs._lextype = Lexer::Type::dom;
          reconstructed.push_back(rhs);
        } else {
          reconstructed.push_back(rhs);
        }

        found = true;
      }

      // If the name does not canonicalize to either an attribute or a UDA
      // then it is not a recognized Lexer::Type::pair, so downgrade it to
      // Lexer::Type::word.
      else {
        a._lextype = Lexer::Type::word;
      }

      if (found)
        changes = true;
      else
        reconstructed.push_back(a);
    }
    // Not a FILTER pair.
    else
      reconstructed.push_back(a);
  }

  if (changes) {
    _args = reconstructed;

    if (Context::getContext().config.getInteger("debug.parser") >= 2)
      Context::getContext().debug(dump("CLI2::prepareFilter desugarFilterAttributes"));
  }
}

////////////////////////////////////////////////////////////////////////////////
// /pattern/ --> description ~ 'pattern'
void CLI2::desugarFilterPatterns() {
  bool changes = false;
  std::vector<A2> reconstructed;
  for (const auto& a : _args) {
    if (a._lextype == Lexer::Type::pattern && a.hasTag("FILTER")) {
      changes = true;

      A2 lhs("description", Lexer::Type::dom);
      lhs.tag("FILTER");
      reconstructed.push_back(lhs);

      A2 op("~", Lexer::Type::op);
      op.tag("FILTER");
      reconstructed.push_back(op);

      A2 rhs(a.attribute("pattern"), Lexer::Type::string);
      rhs.attribute("flags", a.attribute("flags"));
      rhs.tag("FILTER");
      reconstructed.push_back(rhs);
    } else
      reconstructed.push_back(a);
  }

  if (changes) {
    _args = reconstructed;

    if (Context::getContext().config.getInteger("debug.parser") >= 2)
      Context::getContext().debug(dump("CLI2::prepareFilter desugarFilterPatterns"));
  }
}

////////////////////////////////////////////////////////////////////////////////
// An ID sequence can be:
//
//   a single ID:          1
//   a list of IDs:        1,3,5
//   a list of IDs:        1 3 5
//   a range:              5-10
//   or a combination:     1,3,5-10 12
//
void CLI2::findIDs() {
  bool changes = false;

  if (Context::getContext().config.getBoolean("sugar")) {
    bool previousFilterArgWasAnOperator = false;
    int filterCount = 0;

    for (const auto& a : _args) {
      if (a.hasTag("FILTER")) {
        ++filterCount;

        if (a._lextype == Lexer::Type::number) {
          // Skip any number that was preceded by an operator.
          if (!previousFilterArgWasAnOperator) {
            changes = true;
            std::string number = a.attribute("raw");
            _id_ranges.emplace_back(number, number);
          }
        } else if (a._lextype == Lexer::Type::set) {
          // Split the ID list into elements.
          auto elements = split(a.attribute("raw"), ',');

          for (auto& element : elements) {
            changes = true;
            auto hyphen = element.find('-');
            if (hyphen != std::string::npos)
              _id_ranges.emplace_back(element.substr(0, hyphen), element.substr(hyphen + 1));
            else
              _id_ranges.emplace_back(element, element);
          }
        }

        std::string raw = a.attribute("raw");
        previousFilterArgWasAnOperator =
            (a._lextype == Lexer::Type::op && raw != "(" && raw != ")") ? true : false;
      }
    }

    // If no IDs were found, and no filter was specified, look for number/set
    // listed as a MODIFICATION.
    std::string command = getCommand();

    if (!_id_ranges.size() && filterCount == 0 && command != "add" && command != "log") {
      for (auto& a : _args) {
        if (a.hasTag("MODIFICATION")) {
          std::string raw = a.attribute("raw");

          // For a number to be an ID, it must not contain any sign or floating
          // point elements.
          if (a._lextype == Lexer::Type::number && raw.find('.') == std::string::npos &&
              raw.find('e') == std::string::npos && raw.find('-') == std::string::npos) {
            changes = true;
            a.unTag("MODIFICATION");
            a.tag("FILTER");
            _id_ranges.emplace_back(raw, raw);
          } else if (a._lextype == Lexer::Type::set) {
            a.unTag("MODIFICATION");
            a.tag("FILTER");

            // Split the ID list into elements.
            auto elements = split(raw, ',');

            for (const auto& element : elements) {
              changes = true;
              auto hyphen = element.find('-');
              if (hyphen != std::string::npos)
                _id_ranges.emplace_back(element.substr(0, hyphen), element.substr(hyphen + 1));
              else
                _id_ranges.emplace_back(element, element);
            }
          }
        }
      }
    }
  }

  // Sugar-free.
  else {
    std::vector<A2> reconstructed;
    for (const auto& a : _args) {
      if (a.hasTag("FILTER") && a._lextype == Lexer::Type::number) {
        changes = true;
        A2 pair("id:" + a.attribute("raw"), Lexer::Type::pair);
        pair.tag("FILTER");
        pair.decompose();
        reconstructed.push_back(pair);
      } else
        reconstructed.push_back(a);
    }

    if (changes) _args = reconstructed;
  }

  if (changes)
    if (Context::getContext().config.getInteger("debug.parser") >= 2)
      Context::getContext().debug(dump("CLI2::prepareFilter findIDs"));
}

////////////////////////////////////////////////////////////////////////////////
void CLI2::findUUIDs() {
  bool changes = false;

  if (Context::getContext().config.getBoolean("sugar")) {
    for (const auto& a : _args) {
      if (a._lextype == Lexer::Type::uuid && a.hasTag("FILTER")) {
        changes = true;
        _uuid_list.push_back(a.attribute("raw"));
      }
    }

    if (!_uuid_list.size()) {
      for (auto& a : _args) {
        if (a._lextype == Lexer::Type::uuid && a.hasTag("MODIFICATION")) {
          changes = true;
          a.unTag("MODIFICATION");
          a.tag("FILTER");
          _uuid_list.push_back(a.attribute("raw"));
        }
      }
    }
  }

  // Sugar-free.
  else {
    std::vector<A2> reconstructed;
    for (const auto& a : _args) {
      if (a.hasTag("FILTER") && a._lextype == Lexer::Type::uuid) {
        changes = true;
        A2 pair("uuid:" + a.attribute("raw"), Lexer::Type::pair);
        pair.tag("FILTER");
        pair.decompose();
        reconstructed.push_back(pair);
      } else
        reconstructed.push_back(a);
    }

    if (changes) _args = reconstructed;
  }

  if (changes)
    if (Context::getContext().config.getInteger("debug.parser") >= 2)
      Context::getContext().debug(dump("CLI2::prepareFilter findUUIDs"));
}

////////////////////////////////////////////////////////////////////////////////
void CLI2::insertIDExpr() {
  // Skip completely if no ID/UUID was found. This is because below, '(' and ')'
  // are inserted regardless of list size.
  if (!_id_ranges.size() && !_uuid_list.size()) return;

  // Find the *first* occurence of lexer type set/number/uuid, and replace it
  // with a synthesized expression. All other occurences are eaten.
  bool changes = false;
  bool foundID = false;
  std::vector<A2> reconstructed;
  for (const auto& a : _args) {
    if ((a._lextype == Lexer::Type::set || a._lextype == Lexer::Type::number ||
         a._lextype == Lexer::Type::uuid) &&
        a.hasTag("FILTER")) {
      if (!foundID) {
        foundID = true;
        changes = true;

        // Construct a single sequence that represents all _id_ranges and
        // _uuid_list in one clause. This is essentially converting this:
        //
        //   1,2-3 uuid,uuid uuid 4
        //
        // into:
        //
        //   (
        //        ( id == 1 )
        //     or ( ( id >= 2 ) and ( id <= 3 ) )
        //     or ( id == 4 )
        //     or ( uuid = $UUID )
        //     or ( uuid = $UUID )
        //   )

        // Building block operators.
        A2 openParen("(", Lexer::Type::op);
        openParen.tag("FILTER");
        A2 closeParen(")", Lexer::Type::op);
        closeParen.tag("FILTER");
        A2 opOr("or", Lexer::Type::op);
        opOr.tag("FILTER");
        A2 opAnd("and", Lexer::Type::op);
        opAnd.tag("FILTER");
        A2 opSimilar("=", Lexer::Type::op);
        opSimilar.tag("FILTER");
        A2 opEqual("==", Lexer::Type::op);
        opEqual.tag("FILTER");
        A2 opGTE(">=", Lexer::Type::op);
        opGTE.tag("FILTER");
        A2 opLTE("<=", Lexer::Type::op);
        opLTE.tag("FILTER");

        // Building block attributes.
        A2 argID("id", Lexer::Type::dom);
        argID.tag("FILTER");

        A2 argUUID("uuid", Lexer::Type::dom);
        argUUID.tag("FILTER");

        reconstructed.push_back(openParen);

        // Add all ID ranges.
        for (auto r = _id_ranges.begin(); r != _id_ranges.end(); ++r) {
          if (r != _id_ranges.begin()) reconstructed.push_back(opOr);

          if (r->first == r->second) {
            reconstructed.push_back(openParen);
            reconstructed.push_back(argID);
            reconstructed.push_back(opEqual);

            A2 value(r->first, Lexer::Type::number);
            value.tag("FILTER");
            reconstructed.push_back(value);

            reconstructed.push_back(closeParen);
          } else {
            bool ascending = true;
            int low = strtol(r->first.c_str(), nullptr, 10);
            int high = strtol(r->second.c_str(), nullptr, 10);
            if (low <= high)
              ascending = true;
            else
              ascending = false;

            reconstructed.push_back(openParen);
            reconstructed.push_back(argID);
            reconstructed.push_back(opGTE);

            A2 startValue((ascending ? r->first : r->second), Lexer::Type::number);
            startValue.tag("FILTER");
            reconstructed.push_back(startValue);

            reconstructed.push_back(opAnd);
            reconstructed.push_back(argID);
            reconstructed.push_back(opLTE);

            A2 endValue((ascending ? r->second : r->first), Lexer::Type::number);
            endValue.tag("FILTER");
            reconstructed.push_back(endValue);

            reconstructed.push_back(closeParen);
          }
        }

        // Combine the ID and UUID sections with 'or'.
        if (_id_ranges.size() && _uuid_list.size()) reconstructed.push_back(opOr);

        // Add all UUID list items.
        for (auto u = _uuid_list.begin(); u != _uuid_list.end(); ++u) {
          if (u != _uuid_list.begin()) reconstructed.push_back(opOr);

          reconstructed.push_back(openParen);
          reconstructed.push_back(argUUID);
          reconstructed.push_back(opSimilar);

          A2 value(*u, Lexer::Type::string);
          value.tag("FILTER");
          reconstructed.push_back(value);

          reconstructed.push_back(closeParen);
        }

        reconstructed.push_back(closeParen);
      }

      // No 'else' because all set/number/uuid args but the first are removed.
    } else
      reconstructed.push_back(a);
  }

  if (changes) {
    _args = reconstructed;

    if (Context::getContext().config.getInteger("debug.parser") >= 2)
      Context::getContext().debug(dump("CLI2::prepareFilter insertIDExpr"));
  }
}

////////////////////////////////////////////////////////////////////////////////
// FILTER Lexer::Type::word args will become part of an expression, and so they
// need to be Lexed.
void CLI2::lexFilterArgs() {
  bool changes = false;
  std::vector<A2> reconstructed;
  for (const auto& a : _args) {
    if (a._lextype == Lexer::Type::word && a.hasTag("FILTER")) {
      changes = true;

      std::string lexeme;
      Lexer::Type type;
      Lexer lex(a.attribute("raw"));
      while (lex.token(lexeme, type)) {
        A2 extra(lexeme, type);
        extra.tag("FILTER");
        reconstructed.push_back(extra);
      }
    } else
      reconstructed.push_back(a);
  }

  if (changes) {
    _args = reconstructed;

    if (Context::getContext().config.getInteger("debug.parser") >= 2)
      Context::getContext().debug(dump("CLI2::prepareFilter lexFilterArgs"));
  }
}

////////////////////////////////////////////////////////////////////////////////
// FILTER, Lexer::Type::word args are treated as search terms.
//
// Algorithm:
//   Given:
//     - task ... argX candidate argY
//   Where:
//     - neither argX nor argY are an operator, except (, ), and, or, xor
//     - candidate is one of: Lexer::Type::word
//                            Lexer::Type::identifier
//                            Lexer::Type::date
//
void CLI2::desugarFilterPlainArgs() {
  // First walk the arg list looking for plain words that are not part of an
  // existing expression.
  auto prevprev = &_args[0];
  auto prev = &_args[0];
  for (auto& a : _args) {
    auto raw = a.attribute("raw");
    auto praw = prev->attribute("raw");
    auto ppraw = prevprev->attribute("raw");

    if ((prevprev->_lextype != Lexer::Type::op ||  // argX
         ppraw == "(" || ppraw == ")" || ppraw == "and" || ppraw == "or" || ppraw == "xor") &&

        (prev->_lextype == Lexer::Type::identifier ||  // candidate
         prev->_lextype == Lexer::Type::date ||        // candidate
         prev->_lextype == Lexer::Type::word) &&       // candidate

        prev->hasTag("FILTER") &&  // candidate

        (a._lextype != Lexer::Type::op ||  // argY
         raw == "(" || raw == ")" || raw == "and" || raw == "or" || raw == "xor")) {
      prev->tag("PLAIN");
    }

    prevprev = prev;
    prev = &a;
  }

  // Cover the case where the *last* argument is a plain arg.
  auto& penultimate = _args[_args.size() - 2];
  auto praw = penultimate.attribute("raw");
  auto& last = _args[_args.size() - 1];
  if ((penultimate._lextype != Lexer::Type::op ||  // argX
       praw == "(" || praw == ")" || praw == "and" || praw == "or" || praw == "xor") &&

      (last._lextype == Lexer::Type::identifier ||  // candidate
       last._lextype == Lexer::Type::word) &&       // candidate

      last.hasTag("FILTER"))  // candidate
  {
    last.tag("PLAIN");
  }

  // Walk the list again, upgrading PLAIN args.
  bool changes = false;
  std::vector<A2> reconstructed;
  for (const auto& a : _args) {
    if (a.hasTag("PLAIN")) {
      changes = true;

      A2 lhs("description", Lexer::Type::dom);
      lhs.attribute("canonical", "description");
      lhs.tag("FILTER");
      lhs.tag("PLAIN");
      reconstructed.push_back(lhs);

      A2 op("~", Lexer::Type::op);
      op.tag("FILTER");
      op.tag("PLAIN");
      reconstructed.push_back(op);

      std::string word = a.attribute("raw");
      Lexer::dequote(word);
      A2 rhs(word, Lexer::Type::string);
      rhs.tag("FILTER");
      rhs.tag("PLAIN");
      reconstructed.push_back(rhs);
    } else
      reconstructed.push_back(a);
  }

  if (changes) {
    _args = reconstructed;

    if (Context::getContext().config.getInteger("debug.parser") >= 2)
      Context::getContext().debug(dump("CLI2::prepareFilter desugarFilterPlainArgs"));
  }
}

////////////////////////////////////////////////////////////////////////////////
// Detects if the bracket at iterator it is a start or end of an empty paren expression
// Examples:
// ( status = pending ) ( )
//                      ^
//              it -----|         => true
//
// ( status = pending ) ( project = Home )
//                      ^
//              it -----|         => false
bool CLI2::isEmptyParenExpression(std::vector<A2>::iterator it, bool forward /* = true */) const {
  int open = 0;
  int closed = 0;

  for (auto a = it; a != (forward ? _args.end() : _args.begin()); (forward ? ++a : --a)) {
    if (a->attribute("raw") == "(")
      open++;
    else if (a->attribute("raw") == ")")
      closed++;
    else
      // Encountering a non-paren token means there is something between parenthees
      return false;

    // Getting balanced parentheses means we have an empty paren expression
    if (open == closed && open != 0) return true;
  }

  // Should not end here.
  return false;
}

////////////////////////////////////////////////////////////////////////////////
// Two consecutive FILTER, non-OP arguments that are not "(" or ")" need an
// "and" operator inserted between them.
//
//   ) <non-op>         -->  ) and <non-op>
//   <non-op> (         -->  <non-op> <and> (
//   ) (                -->  ) and (
//   <non-op> <non-op>  -->  <non-op> and <non-op>
//
void CLI2::insertJunctions() {
  bool changes = false;
  std::vector<A2> reconstructed;
  auto prev = _args.begin();

  for (auto a = _args.begin(); a != _args.end(); ++a) {
    if (a->hasTag("FILTER")) {
      // The prev iterator should be the first FILTER arg.
      if (prev == _args.begin()) prev = a;

      // Insert AND between terms.
      else if (a != prev) {
        if ((prev->_lextype != Lexer::Type::op && a->attribute("raw") == "(" &&
             !isEmptyParenExpression(a, true)) ||
            (prev->attribute("raw") == ")" && a->_lextype != Lexer::Type::op &&
             !isEmptyParenExpression(prev, false)) ||
            (prev->attribute("raw") == ")" && a->attribute("raw") == "(" &&
             !isEmptyParenExpression(a, true) && !isEmptyParenExpression(prev, false)) ||
            (prev->_lextype != Lexer::Type::op && a->_lextype != Lexer::Type::op)) {
          A2 opOr("and", Lexer::Type::op);
          opOr.tag("FILTER");
          reconstructed.push_back(opOr);
          changes = true;
        }
      }

      // Previous FILTER arg.
      prev = a;
    }

    reconstructed.push_back(*a);
  }

  if (changes) {
    _args = reconstructed;

    if (Context::getContext().config.getInteger("debug.parser") >= 2)
      Context::getContext().debug(dump("CLI2::prepareFilter insertJunctions"));
  }
}

////////////////////////////////////////////////////////////////////////////////
// Look for situations that require defaults:
//
// 1. If no command was found, and no ID/UUID, and if rc.default.command is
//    configured, inject the lexed tokens from rc.default.command.
//
// 2. If no command was found, but an ID/UUID was found, then assume a command
//    of 'information'.
//
void CLI2::defaultCommand() {
  // Scan the top-level branches for evidence of ID, UUID, overrides and other
  // arguments.
  bool changes = false;
  bool found_command = false;
  bool found_sequence = false;

  for (const auto& a : _args) {
    std::string raw = a.attribute("raw");

    if (a.hasTag("CMD")) found_command = true;

    if (a._lextype == Lexer::Type::uuid || a._lextype == Lexer::Type::number) found_sequence = true;
  }

  // If no command was specified, then a command will be inserted.
  if (!found_command) {
    // Default command.
    if (!found_sequence) {
      // Apply overrides, if any.
      std::string defaultCommand = Context::getContext().config.get("default.command");
      if (defaultCommand != "") {
        // Modify _args, _original_args to be:
        //   <args0> [<def0> ...] <args1> [...]

        std::vector<A2> reconstructedOriginals{_original_args[0]};
        std::vector<A2> reconstructed{_args[0]};

        std::string lexeme;
        Lexer::Type type;
        Lexer lex(defaultCommand);

        while (lex.token(lexeme, type)) {
          reconstructedOriginals.emplace_back(lexeme, type);

          A2 cmd(lexeme, type);
          cmd.tag("DEFAULT");
          reconstructed.push_back(cmd);
        }

        for (unsigned int i = 1; i < _original_args.size(); ++i)
          reconstructedOriginals.push_back(_original_args[i]);

        for (unsigned int i = 1; i < _args.size(); ++i) reconstructed.push_back(_args[i]);

        _original_args = reconstructedOriginals;
        _args = reconstructed;
        changes = true;
      }
    } else {
      A2 info("information", Lexer::Type::word);
      info.tag("ASSUMED");
      _args.push_back(info);
      changes = true;
    }
  }

  if (changes && Context::getContext().config.getInteger("debug.parser") >= 2)
    Context::getContext().debug(dump("CLI2::analyze defaultCommand"));
}

////////////////////////////////////////////////////////////////////////////////
// Some values are expressions, which need to be lexed. The best way to
// determine whether an expression is either a single value, or needs to be
// lexed, is to lex it and count the tokens. For example:
//    now+1d
// This should be lexed and surrounded by parentheses:
//    (
//    now
//    +
//    1d
//    )
std::vector<A2> CLI2::lexExpression(const std::string& expression) {
  std::vector<A2> lexed;
  std::string lexeme;
  Lexer::Type type;
  Lexer lex(expression);
  while (lex.token(lexeme, type)) {
    A2 token(lexeme, type);
    token.tag("FILTER");
    lexed.push_back(token);
  }

  // If there were multiple tokens, parenthesize, because this expression will
  // be used as a value.
  if (lexed.size() > 1) {
    A2 openParen("(", Lexer::Type::op);
    openParen.tag("FILTER");
    A2 closeParen(")", Lexer::Type::op);
    closeParen.tag("FILTER");

    lexed.insert(lexed.begin(), openParen);
    lexed.push_back(closeParen);
  }

  return lexed;
}

////////////////////////////////////////////////////////////////////////////////

// vim: ts=2:sw=2
