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

#include <cm/optional>

#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"

template <typename FunctionSignature>
struct cmCommandLineArgument
{
  enum class Values
  {
    Zero,
    One,
    Two,
    ZeroOrOne,
    OneOrMore
  };

  enum class RequiresSeparator
  {
    Yes,
    No
  };

  enum class ParseMode
  {
    Valid,
    Invalid,
    SyntaxError,
    ValueError
  };

  std::string InvalidSyntaxMessage;
  std::string InvalidValueMessage;
  std::string Name;
  Values Type;
  RequiresSeparator SeparatorNeeded;
  std::function<FunctionSignature> StoreCall;

  template <typename FunctionType>
  cmCommandLineArgument(std::string n, Values t, FunctionType&& func)
    : InvalidSyntaxMessage(cmStrCat(" is invalid syntax for ", n))
    , InvalidValueMessage(cmStrCat("Invalid value used with ", n))
    , Name(std::move(n))
    , Type(t)
    , SeparatorNeeded(RequiresSeparator::Yes)
    , StoreCall(std::forward<FunctionType>(func))
  {
  }

  template <typename FunctionType>
  cmCommandLineArgument(std::string n, Values t, RequiresSeparator s,
                        FunctionType&& func)
    : InvalidSyntaxMessage(cmStrCat(" is invalid syntax for ", n))
    , InvalidValueMessage(cmStrCat("Invalid value used with ", n))
    , Name(std::move(n))
    , Type(t)
    , SeparatorNeeded(s)
    , StoreCall(std::forward<FunctionType>(func))
  {
  }

  template <typename FunctionType>
  cmCommandLineArgument(std::string n, std::string failedMsg, Values t,
                        FunctionType&& func)
    : InvalidSyntaxMessage(cmStrCat(" is invalid syntax for ", n))
    , InvalidValueMessage(std::move(failedMsg))
    , Name(std::move(n))
    , Type(t)
    , SeparatorNeeded(RequiresSeparator::Yes)
    , StoreCall(std::forward<FunctionType>(func))
  {
  }

  template <typename FunctionType>
  cmCommandLineArgument(std::string n, std::string failedMsg, Values t,
                        RequiresSeparator s, FunctionType&& func)
    : InvalidSyntaxMessage(cmStrCat(" is invalid syntax for ", n))
    , InvalidValueMessage(std::move(failedMsg))
    , Name(std::move(n))
    , Type(t)
    , SeparatorNeeded(s)
    , StoreCall(std::forward<FunctionType>(func))
  {
  }

  bool matches(std::string const& input) const
  {
    bool matched = false;
    if (this->Type == Values::Zero) {
      matched = (input == this->Name);
    } else if (this->SeparatorNeeded == RequiresSeparator::No) {
      matched = cmHasPrefix(input, this->Name);
    } else if (cmHasPrefix(input, this->Name)) {
      if (input.size() == this->Name.size()) {
        matched = true;
      } else {
        matched =
          (input[this->Name.size()] == '=' || input[this->Name.size()] == ' ');
      }
    }
    return matched;
  }

  template <typename T, typename... CallState>
  bool parse(std::string const& input, T& index,
             std::vector<std::string> const& allArgs,
             CallState&&... state) const
  {
    ParseMode parseState = ParseMode::Valid;

    if (this->Type == Values::Zero) {
      if (input.size() == this->Name.size()) {
        parseState =
          this->StoreCall(std::string{}, std::forward<CallState>(state)...)
          ? ParseMode::Valid
          : ParseMode::Invalid;
      } else {
        parseState = ParseMode::SyntaxError;
      }

    } else if (this->Type == Values::One || this->Type == Values::ZeroOrOne) {
      if (input.size() == this->Name.size()) {
        auto nextValueIndex = index + 1;
        if (nextValueIndex >= allArgs.size() ||
            allArgs[nextValueIndex][0] == '-') {
          if (this->Type == Values::ZeroOrOne) {
            parseState =
              this->StoreCall(std::string{}, std::forward<CallState>(state)...)
              ? ParseMode::Valid
              : ParseMode::Invalid;
          } else {
            parseState = ParseMode::ValueError;
          }
        } else {
          parseState = this->StoreCall(allArgs[nextValueIndex],
                                       std::forward<CallState>(state)...)
            ? ParseMode::Valid
            : ParseMode::Invalid;
          index = nextValueIndex;
        }
      } else {
        auto value = this->extract_single_value(input, parseState);
        if (parseState == ParseMode::Valid) {
          parseState =
            this->StoreCall(value, std::forward<CallState>(state)...)
            ? ParseMode::Valid
            : ParseMode::Invalid;
        }
      }
    } else if (this->Type == Values::Two) {
      if (input.size() == this->Name.size()) {
        if (index + 2 >= allArgs.size() || allArgs[index + 1][0] == '-' ||
            allArgs[index + 2][0] == '-') {
          parseState = ParseMode::ValueError;
        } else {
          index += 2;
          parseState =
            this->StoreCall(cmStrCat(allArgs[index - 1], ";", allArgs[index]),
                            std::forward<CallState>(state)...)
            ? ParseMode::Valid
            : ParseMode::Invalid;
        }
      }
    } else if (this->Type == Values::OneOrMore) {
      if (input.size() == this->Name.size()) {
        auto nextValueIndex = index + 1;
        if (nextValueIndex >= allArgs.size() ||
            allArgs[nextValueIndex][0] == '-') {
          parseState = ParseMode::ValueError;
        } else {
          std::string buffer = allArgs[nextValueIndex++];
          while (nextValueIndex < allArgs.size() &&
                 allArgs[nextValueIndex][0] != '-') {
            buffer = cmStrCat(buffer, ";", allArgs[nextValueIndex++]);
          }
          parseState =
            this->StoreCall(buffer, std::forward<CallState>(state)...)
            ? ParseMode::Valid
            : ParseMode::Invalid;
          index = (nextValueIndex - 1);
        }
      } else {
        auto value = this->extract_single_value(input, parseState);
        if (parseState == ParseMode::Valid) {
          parseState =
            this->StoreCall(value, std::forward<CallState>(state)...)
            ? ParseMode::Valid
            : ParseMode::Invalid;
        }
      }
    }

    if (parseState == ParseMode::SyntaxError) {
      cmSystemTools::Error(
        cmStrCat("'", input, "'", this->InvalidSyntaxMessage));
    } else if (parseState == ParseMode::ValueError) {
      cmSystemTools::Error(this->InvalidValueMessage);
    }
    return (parseState == ParseMode::Valid);
  }

  template <typename... Values>
  static std::function<FunctionSignature> setToTrue(Values&&... values)
  {
    return ArgumentLambdaHelper<FunctionSignature>::generateSetToTrue(
      std::forward<Values>(values)...);
  }

  template <typename... Values>
  static std::function<FunctionSignature> setToValue(Values&&... values)
  {
    return ArgumentLambdaHelper<FunctionSignature>::generateSetToValue(
      std::forward<Values>(values)...);
  }

private:
  template <typename T>
  class ArgumentLambdaHelper;

  template <typename... CallState>
  class ArgumentLambdaHelper<bool(const std::string&, CallState...)>
  {
  public:
    static std::function<bool(const std::string&, CallState...)>
    generateSetToTrue(bool& value1)
    {
      return [&value1](const std::string&, CallState&&...) -> bool {
        value1 = true;
        return true;
      };
    }

    static std::function<bool(const std::string&, CallState...)>
    generateSetToTrue(bool& value1, bool& value2)
    {
      return [&value1, &value2](const std::string&, CallState&&...) -> bool {
        value1 = true;
        value2 = true;
        return true;
      };
    }

    static std::function<bool(const std::string&, CallState...)>
    generateSetToValue(std::string& value1)
    {
      return [&value1](const std::string& arg, CallState&&...) -> bool {
        value1 = arg;
        return true;
      };
    }

    static std::function<bool(const std::string&, CallState...)>
    generateSetToValue(cm::optional<std::string>& value1)
    {
      return [&value1](const std::string& arg, CallState&&...) -> bool {
        value1 = arg;
        return true;
      };
    }
  };

  std::string extract_single_value(std::string const& input,
                                   ParseMode& parseState) const
  {
    // parse the string to get the value
    auto possible_value = cm::string_view(input).substr(this->Name.size());
    if (possible_value.empty()) {
      parseState = ParseMode::ValueError;
    } else if (possible_value[0] == '=') {
      possible_value.remove_prefix(1);
      if (possible_value.empty()) {
        parseState = ParseMode::ValueError;
      }
    }
    if (parseState == ParseMode::Valid && possible_value[0] == ' ') {
      possible_value.remove_prefix(1);
    }
    return std::string(possible_value);
  }
};
