/*
  Licensed to the Apache Software Foundation (ASF) under one
  or more contributor license agreements.  See the NOTICE file
  distributed with this work for additional information
  regarding copyright ownership.  The ASF licenses this file
  to you under the Apache License, Version 2.0 (the
  "License"); you may not use this file except in compliance
  with the License.  You may obtain a copy of the License at

  http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
*/

/**
 * @file pattern.cc
 * @brief PRCE related classes.
 * @see pattern.h
 */

#include "pattern.h"
#include "tsutil/Regex.h"

static void
replaceString(String &str, const String &from, const String &to)
{
  if (from.empty()) {
    return;
  }

  String::size_type start_pos = 0;
  while ((start_pos = str.find(from, start_pos)) != String::npos) {
    str.replace(start_pos, from.length(), to);
    start_pos += to.length();
  }
}

Pattern::Pattern() : _pattern(""), _replacement("") {}

/**
 * @brief Initializes PCRE2 pattern by providing the subject and replacement strings.
 * @param pattern PCRE2 pattern, a string containing PCRE2 patterns, capturing groups.
 * @param replacement PCRE2 replacement, a string where $0 ... $9 will be replaced with the corresponding capturing groups
 * @return true if successful, false if failure
 */
bool
Pattern::init(const String &pattern, const String &replacement)
{
  _pattern.assign(pattern);
  _replacement.assign(replacement);

  _tokenCount = 0;

  if (!compile()) {
    PrefetchDebug("failed to initialize pattern:'%s', replacement:'%s'", pattern.c_str(), replacement.c_str());
    return false;
  }

  return true;
}

/**
 * @brief Initializes PCRE2 pattern by providing the pattern only or pattern+replacement in a single configuration string.
 * @see init()
 * @param config PCRE2 pattern <pattern> or PCRE2 pattern + replacement in format /<pattern>/<replacement>/
 * @return true if successful, false if failure
 */
bool
Pattern::init(const String &config)
{
  if (config[0] == '/') {
    /* This is a config in format /regex/replacement/ */
    String pattern;
    String replacement;

    size_t start   = 1;
    size_t current = 0;
    size_t next    = 1;
    do {
      current = next + 1;
      next    = config.find_first_of('/', current);
    } while (next != String::npos && '\\' == config[next - 1]);

    if (next != String::npos) {
      pattern = config.substr(start, next - start);
    } else {
      /* Error, no closing '/' */
      PrefetchError("failed to parse the pattern in '%s'", config.c_str());
      return false;
    }

    start = next + 1;
    do {
      current = next + 1;
      next    = config.find_first_of('/', current);
    } while (next != String::npos && '\\' == config[next - 1]);

    if (next != String::npos) {
      replacement = config.substr(start, next - start);
    } else {
      /* Error, no closing '/' */
      PrefetchError("failed to parse the replacement in '%s'", config.c_str());
      return false;
    }

    // Remove '\' which escaped '/' inside the pattern and replacement strings.
    ::replaceString(pattern, "\\/", "/");
    ::replaceString(replacement, "\\/", "/");

    return this->init(pattern, replacement);
  } else {
    return this->init(config, "");
  }

  /* Should never get here. */
  return false;
}

/**
 * @brief Checks if the pattern object was initialized with a meaningful regex pattern.
 * @return true if initialized, false if not.
 */
bool
Pattern::empty() const
{
  return _pattern.empty() || _regex.empty();
}

/**
 * @brief PCRE2 matches a subject string against the regex pattern.
 * @param subject PCRE2 subject
 * @return true - matched, false - did not.
 */
bool
Pattern::match(const String &subject)
{
  PrefetchDebug("matching '%s' to '%s'", _pattern.c_str(), subject.c_str());

  if (_regex.empty()) {
    return false;
  }

  RegexMatches matches;
  int          matchCount = _regex.exec(subject, matches, RE_NOTEMPTY);
  if (matchCount < 0) {
    if (matchCount != RE_ERROR_NOMATCH) {
      PrefetchError("matching error %d", matchCount);
    }
    return false;
  }

  return true;
}

/**
 * @brief Replaces all replacements found in the replacement string with what matched in the PCRE2 capturing groups.
 * @param subject PCRE2 subject string
 * @param result reference to A string where the result of the replacement will be stored
 * @return true - success, false - nothing matched or failure.
 */
bool
Pattern::replace(const String &subject, String &result)
{
  PrefetchDebug("matching '%s' to '%s'", _pattern.c_str(), subject.c_str());

  if (_regex.empty()) {
    return false;
  }

  RegexMatches matches;
  int          matchCount = _regex.exec(subject, matches, RE_NOTEMPTY);

  if (matchCount <= 0) {
    if (matchCount != RE_ERROR_NOMATCH) {
      PrefetchError("matching error %d", matchCount);
    }
    return false;
  }

  int previous = 0;
  for (int i = 0; i < _tokenCount; i++) {
    int replIndex = _tokens[i];

    /* $replIndex was validated at config-load time against the number of groups the pattern defines, but
     * the group may still not have participated in *this* match (e.g. a trailing optional group such as
     * "(\?.*)?" when the subject has no query string).  pcre2_match() returns one past the highest
     * participating group, so substitute an empty string for a group at or beyond that -- the documented
     * PCRE2 semantics for an unmatched group -- rather than failing the whole replacement.  Use ""
     * rather than a default-constructed view so data() is never null, which "%.*s" requires. */
    std::string_view dst = (replIndex < matchCount) ? matches[replIndex] : std::string_view{""};

    PrefetchDebug("replacing '$%d' with '%.*s'", replIndex, static_cast<int>(dst.length()), dst.data());

    result.append(_replacement, previous, _tokenOffset[i] - previous);
    result.append(dst);

    previous = _tokenOffset[i] + 2; /* 2 is the size of $0 or $1 or $2, ... or $9 */
  }

  result.append(_replacement, previous, _replacement.length() - previous);

  PrefetchDebug("replacing '%s' resulted in '%s'", _replacement.c_str(), result.c_str());

  return true;
}

/**
 * @brief PCRE2 compiles the regex, called only during initialization.
 * @return true if successful, false if not.
 */
bool
Pattern::compile()
{
  PrefetchDebug("compiling pattern:'%s', replacement:'%s'", _pattern.c_str(), _replacement.c_str());

  std::string error;
  int         erroffset;
  if (!_regex.compile(_pattern, error, erroffset)) {
    PrefetchError("compile of regex '%s' at char %d: %s", _pattern.c_str(), erroffset, error.c_str());
    return false;
  }

  if (_replacement.empty()) {
    /* No replacement necessary - we are done. */
    return true;
  }

  _tokenCount  = 0;
  bool success = true;

  for (unsigned i = 0; i < _replacement.length(); i++) {
    if (_replacement[i] == '$') {
      if (_tokenCount >= TOKENCOUNT) {
        PrefetchError("too many tokens in replacement string: %s", _replacement.c_str());

        success = false;
        break;
      } else if (_replacement[i + 1] < '0' || _replacement[i + 1] > '9') {
        PrefetchError("invalid replacement token $%c in %s: should be $0 - $9", _replacement[i + 1], _replacement.c_str());

        success = false;
        break;
      } else {
        /* Store the location of the replacement */
        /* Convert '0' to 0 */
        _tokens[_tokenCount]      = _replacement[i + 1] - '0';
        _tokenOffset[_tokenCount] = i;
        _tokenCount++;
        /* Skip the next char */
        i++;
      }
    }
  }

  /* Validate replacement references against the number of capture groups the pattern actually defines
   * (not how many happen to participate in any given match) at config-load time.  This catches a
   * genuinely out-of-range reference such as $5 against a 3-group pattern, and a pattern that defines
   * more groups than can be captured -- RegexMatches holds the whole match plus TOKENCOUNT-1 groups. */
  if (success) {
    int32_t captureCount = _regex.get_capture_count();
    if (captureCount < 0) {
      PrefetchError("failed to get capture count for regex '%s'", _pattern.c_str());
      success = false;
    } else if (captureCount > TOKENCOUNT - 1) {
      PrefetchError("regex '%s' defines %d capture groups; the prefetch plugin supports at most %d (references $0..$%d)",
                    _pattern.c_str(), captureCount, TOKENCOUNT - 1, TOKENCOUNT - 1);
      success = false;
    } else {
      for (int i = 0; i < _tokenCount; i++) {
        if (_tokens[i] > captureCount) {
          PrefetchError("invalid reference $%d in replacement '%s': pattern defines only %d group(s)", _tokens[i],
                        _replacement.c_str(), captureCount);
          success = false;
          break;
        }
      }
    }
  }

  return success;
}

/**
 * @brief Destructor, deletes all patterns.
 */
MultiPattern::~MultiPattern() {}

/**
 * @brief Check if empty.
 * @return true if the classification contains any patterns, false otherwise
 */
bool
MultiPattern::empty() const
{
  return _list.empty();
}

/**
 * @brief Adds a pattern to the multi-pattern
 *
 * The order of addition matters during the classification
 * @param pattern pattern pointer
 */
void
MultiPattern::add(std::unique_ptr<Pattern> pattern)
{
  this->_list.push_back(std::move(pattern));
}

/**
 * @brief Matches the subject string against all patterns.
 * @param subject subject string.
 * @return true if any matches, false if nothing matches.
 */
bool
MultiPattern::match(const String &subject) const
{
  for (auto &p : this->_list) {
    if (nullptr != p && p->match(subject)) {
      return true;
    }
  }
  return false;
}

/**
 * @brief Calls Pattern::replace() on all patterns in the multi-pattern one by one until the first match.
 * @param subject subject string.
 * @param result vector of the result.
 * @return true if any matches, false if nothing matches.
 */
bool
MultiPattern::replace(const String &subject, String &result) const
{
  for (auto &p : this->_list) {
    if (nullptr != p && p->replace(subject, result)) {
      return true;
    }
  }
  return false;
}

/**
 * @brief Returns the name of the multi-pattern (set during the instantiation only).
 */
const String &
MultiPattern::name() const
{
  return _name;
}
