/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
   file Copyright.txt or https://cmake.org/licensing for details.  */
#include "cmListCommand.h"

#include <cassert>
#include <cstdio>
#include <functional>
#include <set>
#include <sstream>
#include <stdexcept>
#include <utility>
#include <vector>

#include <cm/memory>
#include <cm/optional>
#include <cmext/algorithm>
#include <cmext/string_view>

#include "cmExecutionStatus.h"
#include "cmList.h"
#include "cmMakefile.h"
#include "cmMessageType.h"
#include "cmPolicies.h"
#include "cmRange.h"
#include "cmStringAlgorithms.h"
#include "cmSubcommandTable.h"
#include "cmValue.h"

namespace {

bool GetIndexArg(const std::string& arg, int* idx, cmMakefile& mf)
{
  long value;
  if (!cmStrToLong(arg, &value)) {
    switch (mf.GetPolicyStatus(cmPolicies::CMP0121)) {
      case cmPolicies::WARN: {
        // Default is to warn and use old behavior OLD behavior is to allow
        // compatibility, so issue a warning and use the previous behavior.
        std::string warn =
          cmStrCat(cmPolicies::GetPolicyWarning(cmPolicies::CMP0121),
                   " Invalid list index \"", arg, "\".");
        mf.IssueMessage(MessageType::AUTHOR_WARNING, warn);
        CM_FALLTHROUGH;
      }
      case cmPolicies::OLD:
        // OLD behavior is to allow compatibility, so just ignore the
        // situation.
        break;
      case cmPolicies::NEW:
        return false;
      case cmPolicies::REQUIRED_IF_USED:
      case cmPolicies::REQUIRED_ALWAYS:
        std::string msg =
          cmStrCat(cmPolicies::GetRequiredPolicyError(cmPolicies::CMP0121),
                   " Invalid list index \"", arg, "\".");
        mf.IssueMessage(MessageType::FATAL_ERROR, msg);
        break;
    }
  }

  // Truncation is happening here, but it had always been happening here.
  *idx = static_cast<int>(value);

  return true;
}

bool GetListString(std::string& listString, const std::string& var,
                   const cmMakefile& makefile)
{
  // get the old value
  cmValue cacheValue = makefile.GetDefinition(var);
  if (!cacheValue) {
    return false;
  }
  listString = *cacheValue;
  return true;
}

cm::optional<cmList> GetList(const std::string& var,
                             const cmMakefile& makefile)
{
  cm::optional<cmList> list;

  std::string listString;
  if (!GetListString(listString, var, makefile)) {
    return list;
  }
  // if the size of the list
  if (listString.empty()) {
    list.emplace();
    return list;
  }
  // expand the variable into a list
  list.emplace(listString, cmList::EmptyElements::Yes);
  // if no empty elements then just return
  if (!cm::contains(*list, std::string())) {
    return list;
  }
  // if we have empty elements we need to check policy CMP0007
  switch (makefile.GetPolicyStatus(cmPolicies::CMP0007)) {
    case cmPolicies::WARN: {
      // Default is to warn and use old behavior
      // OLD behavior is to allow compatibility, so recall
      // ExpandListArgument without the true which will remove
      // empty values
      list->assign(listString);
      std::string warn =
        cmStrCat(cmPolicies::GetPolicyWarning(cmPolicies::CMP0007),
                 " List has value = [", listString, "].");
      makefile.IssueMessage(MessageType::AUTHOR_WARNING, warn);
      return list;
    }
    case cmPolicies::OLD:
      // OLD behavior is to allow compatibility, so recall
      // ExpandListArgument without the true which will remove
      // empty values
      list->assign(listString);
      return list;
    case cmPolicies::NEW:
      return list;
    case cmPolicies::REQUIRED_IF_USED:
    case cmPolicies::REQUIRED_ALWAYS:
      makefile.IssueMessage(
        MessageType::FATAL_ERROR,
        cmPolicies::GetRequiredPolicyError(cmPolicies::CMP0007));
      return {};
  }
  return list;
}

bool HandleLengthCommand(std::vector<std::string> const& args,
                         cmExecutionStatus& status)
{
  if (args.size() != 3) {
    status.SetError("sub-command LENGTH requires two arguments.");
    return false;
  }

  const std::string& listName = args[1];
  const std::string& variableName = args.back();

  auto list = GetList(listName, status.GetMakefile());
  status.GetMakefile().AddDefinition(variableName,
                                     std::to_string(list ? list->size() : 0));

  return true;
}

bool HandleGetCommand(std::vector<std::string> const& args,
                      cmExecutionStatus& status)
{
  if (args.size() < 4) {
    status.SetError("sub-command GET requires at least three arguments.");
    return false;
  }

  const std::string& listName = args[1];
  const std::string& variableName = args.back();
  // expand the variable
  auto list = GetList(listName, status.GetMakefile());
  if (!list) {
    status.GetMakefile().AddDefinition(variableName, "NOTFOUND");
    return true;
  }
  // FIXME: Add policy to make non-existing lists an error like empty lists.
  if (list->empty()) {
    status.SetError("GET given empty list");
    return false;
  }

  std::vector<int> indexes;
  for (std::size_t cc = 2; cc < args.size() - 1; cc++) {
    int index;
    if (!GetIndexArg(args[cc], &index, status.GetMakefile())) {
      status.SetError(cmStrCat("index: ", args[cc], " is not a valid index"));
      return false;
    }
    indexes.push_back(index);
  }

  try {
    auto values = list->get_items(indexes.begin(), indexes.end());
    status.GetMakefile().AddDefinition(variableName, values.to_string());
    return true;
  } catch (std::out_of_range& e) {
    status.SetError(e.what());
    return false;
  }
}

bool HandleAppendCommand(std::vector<std::string> const& args,
                         cmExecutionStatus& status)
{
  assert(args.size() >= 2);

  // Skip if nothing to append.
  if (args.size() < 3) {
    return true;
  }

  cmMakefile& makefile = status.GetMakefile();
  std::string const& listName = args[1];
  // expand the variable
  std::string listString;
  GetListString(listString, listName, makefile);

  makefile.AddDefinition(
    listName, cmList::append(listString, args.begin() + 2, args.end()));
  return true;
}

bool HandlePrependCommand(std::vector<std::string> const& args,
                          cmExecutionStatus& status)
{
  assert(args.size() >= 2);

  // Skip if nothing to prepend.
  if (args.size() < 3) {
    return true;
  }

  cmMakefile& makefile = status.GetMakefile();
  std::string const& listName = args[1];
  // expand the variable
  std::string listString;
  GetListString(listString, listName, makefile);

  makefile.AddDefinition(
    listName, cmList::prepend(listString, args.begin() + 2, args.end()));
  return true;
}

bool HandlePopBackCommand(std::vector<std::string> const& args,
                          cmExecutionStatus& status)
{
  assert(args.size() >= 2);

  cmMakefile& makefile = status.GetMakefile();
  auto ai = args.cbegin();
  ++ai; // Skip subcommand name
  std::string const& listName = *ai++;
  auto list = GetList(listName, makefile);

  if (!list) {
    // Can't get the list definition... undefine any vars given after.
    for (; ai != args.cend(); ++ai) {
      makefile.RemoveDefinition(*ai);
    }
    return true;
  }

  if (!list->empty()) {
    if (ai == args.cend()) {
      // No variables are given... Just remove one element.
      list->pop_back();
    } else {
      // Ok, assign elements to be removed to the given variables
      for (; !list->empty() && ai != args.cend(); ++ai) {
        assert(!ai->empty());
        makefile.AddDefinition(*ai, list->back());
        list->pop_back();
      }
      // Undefine the rest variables if the list gets empty earlier...
      for (; ai != args.cend(); ++ai) {
        makefile.RemoveDefinition(*ai);
      }
    }

    makefile.AddDefinition(listName, list->to_string());

  } else if (ai !=
             args.cend()) { // The list is empty, but some args were given
    // Need to *undefine* 'em all, cuz there are no items to assign...
    for (; ai != args.cend(); ++ai) {
      makefile.RemoveDefinition(*ai);
    }
  }

  return true;
}

bool HandlePopFrontCommand(std::vector<std::string> const& args,
                           cmExecutionStatus& status)
{
  assert(args.size() >= 2);

  cmMakefile& makefile = status.GetMakefile();
  auto ai = args.cbegin();
  ++ai; // Skip subcommand name
  std::string const& listName = *ai++;
  auto list = GetList(listName, makefile);

  if (!list) {
    // Can't get the list definition... undefine any vars given after.
    for (; ai != args.cend(); ++ai) {
      makefile.RemoveDefinition(*ai);
    }
    return true;
  }

  if (!list->empty()) {
    if (ai == args.cend()) {
      // No variables are given... Just remove one element.
      list->pop_front();
    } else {
      // Ok, assign elements to be removed to the given variables
      auto vi = list->begin();
      for (; vi != list->end() && ai != args.cend(); ++ai, ++vi) {
        assert(!ai->empty());
        makefile.AddDefinition(*ai, *vi);
      }
      list->erase(list->begin(), vi);
      // Undefine the rest variables if the list gets empty earlier...
      for (; ai != args.cend(); ++ai) {
        makefile.RemoveDefinition(*ai);
      }
    }

    makefile.AddDefinition(listName, list->to_string());

  } else if (ai !=
             args.cend()) { // The list is empty, but some args were given
    // Need to *undefine* 'em all, cuz there are no items to assign...
    for (; ai != args.cend(); ++ai) {
      makefile.RemoveDefinition(*ai);
    }
  }

  return true;
}

bool HandleFindCommand(std::vector<std::string> const& args,
                       cmExecutionStatus& status)
{
  if (args.size() != 4) {
    status.SetError("sub-command FIND requires three arguments.");
    return false;
  }

  const std::string& listName = args[1];
  const std::string& variableName = args.back();
  // expand the variable
  auto list = GetList(listName, status.GetMakefile());

  if (!list) {
    status.GetMakefile().AddDefinition(variableName, "-1");
    return true;
  }

  auto index = list->find(args[2]);
  status.GetMakefile().AddDefinition(
    variableName, index == cmList::npos ? "-1" : std::to_string(index));
  return true;
}

bool HandleInsertCommand(std::vector<std::string> const& args,
                         cmExecutionStatus& status)
{
  if (args.size() < 4) {
    status.SetError("sub-command INSERT requires at least three arguments.");
    return false;
  }

  const std::string& listName = args[1];

  // expand the variable
  int index;
  if (!GetIndexArg(args[2], &index, status.GetMakefile())) {
    status.SetError(cmStrCat("index: ", args[2], " is not a valid index"));
    return false;
  }
  auto list = GetList(listName, status.GetMakefile());
  if (!list) {
    list = cmList{};
  }

  try {
    list->insert_items(index, args.begin() + 3, args.end(),
                       cmList::ExpandElements::No, cmList::EmptyElements::Yes);
    status.GetMakefile().AddDefinition(listName, list->to_string());
    return true;
  } catch (std::out_of_range& e) {
    status.SetError(e.what());
    return false;
  }
}

bool HandleJoinCommand(std::vector<std::string> const& args,
                       cmExecutionStatus& status)
{
  if (args.size() != 4) {
    status.SetError(cmStrCat("sub-command JOIN requires three arguments (",
                             args.size() - 1, " found)."));
    return false;
  }

  const std::string& listName = args[1];
  const std::string& glue = args[2];
  const std::string& variableName = args[3];

  // expand the variable
  auto list = GetList(listName, status.GetMakefile());

  if (!list) {
    status.GetMakefile().AddDefinition(variableName, "");
    return true;
  }

  status.GetMakefile().AddDefinition(variableName, list->join(glue));
  return true;
}

bool HandleRemoveItemCommand(std::vector<std::string> const& args,
                             cmExecutionStatus& status)
{
  assert(args.size() >= 2);

  if (args.size() == 2) {
    return true;
  }

  const std::string& listName = args[1];
  // expand the variable
  auto list = GetList(listName, status.GetMakefile());

  if (!list) {
    return true;
  }

  status.GetMakefile().AddDefinition(
    listName, list->remove_items(args.begin() + 2, args.end()).to_string());
  return true;
}

bool HandleReverseCommand(std::vector<std::string> const& args,
                          cmExecutionStatus& status)
{
  assert(args.size() >= 2);
  if (args.size() > 2) {
    status.SetError("sub-command REVERSE only takes one argument.");
    return false;
  }

  const std::string& listName = args[1];
  // expand the variable
  auto list = GetList(listName, status.GetMakefile());

  if (!list) {
    return true;
  }

  status.GetMakefile().AddDefinition(listName, list->reverse().to_string());
  return true;
}

bool HandleRemoveDuplicatesCommand(std::vector<std::string> const& args,
                                   cmExecutionStatus& status)
{
  assert(args.size() >= 2);
  if (args.size() > 2) {
    status.SetError("sub-command REMOVE_DUPLICATES only takes one argument.");
    return false;
  }

  const std::string& listName = args[1];
  // expand the variable
  auto list = GetList(listName, status.GetMakefile());

  if (!list) {
    return true;
  }

  status.GetMakefile().AddDefinition(listName,
                                     list->remove_duplicates().to_string());
  return true;
}

bool HandleTransformCommand(std::vector<std::string> const& args,
                            cmExecutionStatus& status)
{
  if (args.size() < 3) {
    status.SetError(
      "sub-command TRANSFORM requires an action to be specified.");
    return false;
  }

  // Descriptor of action
  // Action: enum value identifying action
  // Arity: number of arguments required for the action
  struct ActionDescriptor
  {
    ActionDescriptor(std::string name)
      : Name(std::move(name))
    {
    }
    ActionDescriptor(std::string name, cmList::TransformAction action,
                     int arity)
      : Name(std::move(name))
      , Action(action)
      , Arity(arity)
    {
    }

    operator const std::string&() const { return this->Name; }

    std::string Name;
    cmList::TransformAction Action;
    int Arity = 0;
  };

  // Build a set of supported actions.
  std::set<ActionDescriptor,
           std::function<bool(const std::string&, const std::string&)>>
    descriptors{ { { "APPEND", cmList::TransformAction::APPEND, 1 },
                   { "PREPEND", cmList::TransformAction::PREPEND, 1 },
                   { "TOUPPER", cmList::TransformAction::TOUPPER, 0 },
                   { "TOLOWER", cmList::TransformAction::TOLOWER, 0 },
                   { "STRIP", cmList::TransformAction::STRIP, 0 },
                   { "GENEX_STRIP", cmList::TransformAction::GENEX_STRIP, 0 },
                   { "REPLACE", cmList::TransformAction::REPLACE, 2 } },
                 [](const std::string& x, const std::string& y) {
                   return x < y;
                 } };

  const std::string& listName = args[1];

  // Parse all possible function parameters
  using size_type = std::vector<std::string>::size_type;
  size_type index = 2;

  auto descriptor = descriptors.find(args[index]);

  if (descriptor == descriptors.end()) {
    status.SetError(
      cmStrCat(" sub-command TRANSFORM, ", args[index], " invalid action."));
    return false;
  }

  // Action arguments
  index += 1;
  if (args.size() < index + descriptor->Arity) {
    status.SetError(cmStrCat("sub-command TRANSFORM, action ",
                             descriptor->Name, " expects ", descriptor->Arity,
                             " argument(s)."));
    return false;
  }

  std::vector<std::string> arguments;
  index += descriptor->Arity;
  if (descriptor->Arity > 0) {
    arguments =
      std::vector<std::string>(args.begin() + 3, args.begin() + index);
  }

  const std::string REGEX{ "REGEX" };
  const std::string AT{ "AT" };
  const std::string FOR{ "FOR" };
  const std::string OUTPUT_VARIABLE{ "OUTPUT_VARIABLE" };
  std::unique_ptr<cmList::TransformSelector> selector;
  std::string outputName = listName;

  try {
    // handle optional arguments
    while (args.size() > index) {
      if ((args[index] == REGEX || args[index] == AT || args[index] == FOR) &&
          selector) {
        status.SetError(
          cmStrCat("sub-command TRANSFORM, selector already specified (",
                   selector->GetTag(), ")."));

        return false;
      }

      // REGEX selector
      if (args[index] == REGEX) {
        if (args.size() == ++index) {
          status.SetError("sub-command TRANSFORM, selector REGEX expects "
                          "'regular expression' argument.");
          return false;
        }

        selector =
          cmList::TransformSelector::New<cmList::TransformSelector::REGEX>(
            args[index]);

        index += 1;
        continue;
      }

      // AT selector
      if (args[index] == AT) {
        // get all specified indexes
        std::vector<cmList::index_type> indexes;
        while (args.size() > ++index) {
          std::size_t pos;
          int value;

          try {
            value = std::stoi(args[index], &pos);
            if (pos != args[index].length()) {
              // this is not a number, stop processing
              break;
            }
            indexes.push_back(value);
          } catch (const std::invalid_argument&) {
            // this is not a number, stop processing
            break;
          }
        }

        if (indexes.empty()) {
          status.SetError(
            "sub-command TRANSFORM, selector AT expects at least one "
            "numeric value.");
          return false;
        }

        selector =
          cmList::TransformSelector::New<cmList::TransformSelector::AT>(
            std::move(indexes));

        continue;
      }

      // FOR selector
      if (args[index] == FOR) {
        if (args.size() <= ++index + 1) {
          status.SetError(
            "sub-command TRANSFORM, selector FOR expects, at least,"
            " two arguments.");
          return false;
        }

        cmList::index_type start = 0;
        cmList::index_type stop = 0;
        cmList::index_type step = 1;
        bool valid = true;
        try {
          std::size_t pos;

          start = std::stoi(args[index], &pos);
          if (pos != args[index].length()) {
            // this is not a number
            valid = false;
          } else {
            stop = std::stoi(args[++index], &pos);
            if (pos != args[index].length()) {
              // this is not a number
              valid = false;
            }
          }
        } catch (const std::invalid_argument&) {
          // this is not numbers
          valid = false;
        }
        if (!valid) {
          status.SetError("sub-command TRANSFORM, selector FOR expects, "
                          "at least, two numeric values.");
          return false;
        }
        // try to read a third numeric value for step
        if (args.size() > ++index) {
          try {
            std::size_t pos;

            step = std::stoi(args[index], &pos);
            if (pos != args[index].length()) {
              // this is not a number
              step = 1;
            } else {
              index += 1;
            }
          } catch (const std::invalid_argument&) {
            // this is not number, ignore exception
          }
        }

        if (step <= 0) {
          status.SetError("sub-command TRANSFORM, selector FOR expects "
                          "positive numeric value for <step>.");
          return false;
        }

        selector =
          cmList::TransformSelector::New<cmList::TransformSelector::FOR>(
            { start, stop, step });

        continue;
      }

      // output variable
      if (args[index] == OUTPUT_VARIABLE) {
        if (args.size() == ++index) {
          status.SetError("sub-command TRANSFORM, OUTPUT_VARIABLE "
                          "expects variable name argument.");
          return false;
        }

        outputName = args[index++];
        continue;
      }

      status.SetError(cmStrCat("sub-command TRANSFORM, '",
                               cmJoin(cmMakeRange(args).advance(index), " "),
                               "': unexpected argument(s)."));
      return false;
    }

    // expand the list variable
    auto list = GetList(listName, status.GetMakefile());

    if (!list) {
      status.GetMakefile().AddDefinition(outputName, "");
      return true;
    }

    list->transform(descriptor->Action, arguments, std::move(selector));
    status.GetMakefile().AddDefinition(outputName, list->to_string());
    return true;
  } catch (cmList::transform_error& e) {
    status.SetError(e.what());
    return false;
  }
}

bool HandleSortCommand(std::vector<std::string> const& args,
                       cmExecutionStatus& status)
{
  assert(args.size() >= 2);
  if (args.size() > 8) {
    status.SetError("sub-command SORT only takes up to six arguments.");
    return false;
  }

  using SortConfig = cmList::SortConfiguration;
  SortConfig sortConfig;

  size_t argumentIndex = 2;
  const std::string messageHint = "sub-command SORT ";

  while (argumentIndex < args.size()) {
    std::string const& option = args[argumentIndex++];
    if (option == "COMPARE") {
      if (sortConfig.Compare != SortConfig::CompareMethod::DEFAULT) {
        std::string error = cmStrCat(messageHint, "option \"", option,
                                     "\" has been specified multiple times.");
        status.SetError(error);
        return false;
      }
      if (argumentIndex < args.size()) {
        std::string const& argument = args[argumentIndex++];
        if (argument == "STRING") {
          sortConfig.Compare = SortConfig::CompareMethod::STRING;
        } else if (argument == "FILE_BASENAME") {
          sortConfig.Compare = SortConfig::CompareMethod::FILE_BASENAME;
        } else if (argument == "NATURAL") {
          sortConfig.Compare = SortConfig::CompareMethod::NATURAL;
        } else {
          std::string error =
            cmStrCat(messageHint, "value \"", argument, "\" for option \"",
                     option, "\" is invalid.");
          status.SetError(error);
          return false;
        }
      } else {
        status.SetError(cmStrCat(messageHint, "missing argument for option \"",
                                 option, "\"."));
        return false;
      }
    } else if (option == "CASE") {
      if (sortConfig.Case != SortConfig::CaseSensitivity::DEFAULT) {
        status.SetError(cmStrCat(messageHint, "option \"", option,
                                 "\" has been specified multiple times."));
        return false;
      }
      if (argumentIndex < args.size()) {
        std::string const& argument = args[argumentIndex++];
        if (argument == "SENSITIVE") {
          sortConfig.Case = SortConfig::CaseSensitivity::SENSITIVE;
        } else if (argument == "INSENSITIVE") {
          sortConfig.Case = SortConfig::CaseSensitivity::INSENSITIVE;
        } else {
          status.SetError(cmStrCat(messageHint, "value \"", argument,
                                   "\" for option \"", option,
                                   "\" is invalid."));
          return false;
        }
      } else {
        status.SetError(cmStrCat(messageHint, "missing argument for option \"",
                                 option, "\"."));
        return false;
      }
    } else if (option == "ORDER") {

      if (sortConfig.Order != SortConfig::OrderMode::DEFAULT) {
        status.SetError(cmStrCat(messageHint, "option \"", option,
                                 "\" has been specified multiple times."));
        return false;
      }
      if (argumentIndex < args.size()) {
        std::string const& argument = args[argumentIndex++];
        if (argument == "ASCENDING") {
          sortConfig.Order = SortConfig::OrderMode::ASCENDING;
        } else if (argument == "DESCENDING") {
          sortConfig.Order = SortConfig::OrderMode::DESCENDING;
        } else {
          status.SetError(cmStrCat(messageHint, "value \"", argument,
                                   "\" for option \"", option,
                                   "\" is invalid."));
          return false;
        }
      } else {
        status.SetError(cmStrCat(messageHint, "missing argument for option \"",
                                 option, "\"."));
        return false;
      }
    } else {
      status.SetError(
        cmStrCat(messageHint, "option \"", option, "\" is unknown."));
      return false;
    }
  }

  const std::string& listName = args[1];
  // expand the variable
  auto list = GetList(listName, status.GetMakefile());

  if (!list) {
    return true;
  }

  status.GetMakefile().AddDefinition(listName,
                                     list->sort(sortConfig).to_string());
  return true;
}

bool HandleSublistCommand(std::vector<std::string> const& args,
                          cmExecutionStatus& status)
{
  if (args.size() != 5) {
    status.SetError(cmStrCat("sub-command SUBLIST requires four arguments (",
                             args.size() - 1, " found)."));
    return false;
  }

  const std::string& listName = args[1];
  const std::string& variableName = args.back();

  // expand the variable
  auto list = GetList(listName, status.GetMakefile());

  if (!list || list->empty()) {
    status.GetMakefile().AddDefinition(variableName, "");
    return true;
  }

  int start;
  int length;
  if (!GetIndexArg(args[2], &start, status.GetMakefile())) {
    status.SetError(cmStrCat("index: ", args[2], " is not a valid index"));
    return false;
  }
  if (!GetIndexArg(args[3], &length, status.GetMakefile())) {
    status.SetError(cmStrCat("index: ", args[3], " is not a valid index"));
    return false;
  }

  if (start < 0) {
    status.SetError(cmStrCat("begin index: ", start, " is out of range 0 - ",
                             list->size() - 1));
    return false;
  }
  if (length < -1) {
    status.SetError(cmStrCat("length: ", length, " should be -1 or greater"));
    return false;
  }

  using size_type = cmList::size_type;

  try {
    auto sublist = list->sublist(static_cast<size_type>(start),
                                 static_cast<size_type>(length));
    status.GetMakefile().AddDefinition(variableName, sublist.to_string());
    return true;
  } catch (std::out_of_range& e) {
    status.SetError(e.what());
    return false;
  }
}

bool HandleRemoveAtCommand(std::vector<std::string> const& args,
                           cmExecutionStatus& status)
{
  if (args.size() < 3) {
    status.SetError("sub-command REMOVE_AT requires at least "
                    "two arguments.");
    return false;
  }

  const std::string& listName = args[1];
  // expand the variable
  auto list = GetList(listName, status.GetMakefile());

  if (!list || list->empty()) {
    std::ostringstream str;
    str << "index: ";
    for (size_t i = 1; i < args.size(); ++i) {
      str << args[i];
      if (i != args.size() - 1) {
        str << ", ";
      }
    }
    str << " out of range (0, 0)";
    status.SetError(str.str());
    return false;
  }

  size_t cc;
  std::vector<cmList::index_type> removed;
  for (cc = 2; cc < args.size(); ++cc) {
    int index;
    if (!GetIndexArg(args[cc], &index, status.GetMakefile())) {
      status.SetError(cmStrCat("index: ", args[cc], " is not a valid index"));
      return false;
    }
    removed.push_back(index);
  }

  try {
    status.GetMakefile().AddDefinition(
      listName,
      list->remove_items(removed.begin(), removed.end()).to_string());
    return true;
  } catch (std::out_of_range& e) {
    status.SetError(e.what());
    return false;
  }
}

bool HandleFilterCommand(std::vector<std::string> const& args,
                         cmExecutionStatus& status)
{
  if (args.size() < 2) {
    status.SetError("sub-command FILTER requires a list to be specified.");
    return false;
  }

  if (args.size() < 3) {
    status.SetError(
      "sub-command FILTER requires an operator to be specified.");
    return false;
  }

  if (args.size() < 4) {
    status.SetError("sub-command FILTER requires a mode to be specified.");
    return false;
  }

  const std::string& op = args[2];
  cmList::FilterMode filterMode;
  if (op == "INCLUDE") {
    filterMode = cmList::FilterMode::INCLUDE;
  } else if (op == "EXCLUDE") {
    filterMode = cmList::FilterMode::EXCLUDE;
  } else {
    status.SetError("sub-command FILTER does not recognize operator " + op);
    return false;
  }

  const std::string& listName = args[1];
  // expand the variable
  auto list = GetList(listName, status.GetMakefile());

  if (!list) {
    return true;
  }

  const std::string& mode = args[3];
  if (mode != "REGEX") {
    status.SetError("sub-command FILTER does not recognize mode " + mode);
    return false;
  }
  if (args.size() != 5) {
    status.SetError("sub-command FILTER, mode REGEX "
                    "requires five arguments.");
    return false;
  }
  const std::string& pattern = args[4];

  try {
    status.GetMakefile().AddDefinition(
      listName, list->filter(pattern, filterMode).to_string());
    return true;
  } catch (std::invalid_argument& e) {
    status.SetError(e.what());
    return false;
  }
}
} // namespace

bool cmListCommand(std::vector<std::string> const& args,
                   cmExecutionStatus& status)
{
  if (args.size() < 2) {
    status.SetError("must be called with at least two arguments.");
    return false;
  }

  static cmSubcommandTable const subcommand{
    { "LENGTH"_s, HandleLengthCommand },
    { "GET"_s, HandleGetCommand },
    { "APPEND"_s, HandleAppendCommand },
    { "PREPEND"_s, HandlePrependCommand },
    { "POP_BACK"_s, HandlePopBackCommand },
    { "POP_FRONT"_s, HandlePopFrontCommand },
    { "FIND"_s, HandleFindCommand },
    { "INSERT"_s, HandleInsertCommand },
    { "JOIN"_s, HandleJoinCommand },
    { "REMOVE_AT"_s, HandleRemoveAtCommand },
    { "REMOVE_ITEM"_s, HandleRemoveItemCommand },
    { "REMOVE_DUPLICATES"_s, HandleRemoveDuplicatesCommand },
    { "TRANSFORM"_s, HandleTransformCommand },
    { "SORT"_s, HandleSortCommand },
    { "SUBLIST"_s, HandleSublistCommand },
    { "REVERSE"_s, HandleReverseCommand },
    { "FILTER"_s, HandleFilterCommand },
  };

  return subcommand(args[0], args, status);
}
