// Copyright 2017 The Effcee Authors.
//
// Licensed 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.

#include "check.h"

#include <algorithm>
#include <cassert>
#include <memory>
#include <sstream>
#include <string>
#include <utility>

#include "cursor.h"
#include "effcee.h"
#include "make_unique.h"
#include "to_string.h"

using Status = effcee::Result::Status;
using StringPiece = effcee::StringPiece;
using Type = effcee::Check::Type;

namespace {

// Returns a table of suffix to type mappings.
const std::vector<std::pair<StringPiece, Type>>& TypeStringTable() {
  static std::vector<std::pair<StringPiece, Type>> type_str_table{
      {"", Type::Simple},  {"-NEXT", Type::Next},   {"-SAME", Type::Same},
      {"-DAG", Type::DAG}, {"-LABEL", Type::Label}, {"-NOT", Type::Not}};
  return type_str_table;
}

// Returns the Check::Type value matching the suffix part of a check rule
// prefix.  Assumes |suffix| is valid.
Type TypeForSuffix(StringPiece suffix) {
  const auto& type_str_table = TypeStringTable();
  const auto pair_iter =
      std::find_if(type_str_table.begin(), type_str_table.end(),
                   [suffix](const std::pair<StringPiece, Type>& elem) {
                     return suffix == elem.first;
                   });
  assert(pair_iter != type_str_table.end());
  return pair_iter->second;
}
}  // namespace

namespace effcee {

int Check::Part::CountCapturingGroups() {
  if (type_ == Type::Regex) return RE2(param_).NumberOfCapturingGroups();
  if (type_ == Type::VarDef) return RE2(expression_).NumberOfCapturingGroups();
  return 0;
}

Check::Check(Type type, StringPiece param) : type_(type), param_(param) {
  parts_.push_back(effcee::make_unique<Check::Part>(Part::Type::Fixed, param));
}

bool Check::Part::MightMatch(const VarMapping& vars) const {
  return type_ != Type::VarUse ||
         vars.find(ToString(VarUseName())) != vars.end();
}

std::string Check::Part::Regex(const VarMapping& vars) const {
  switch (type_) {
    case Type::Fixed:
      return RE2::QuoteMeta(param_);
    case Type::Regex:
      return ToString(param_);
    case Type::VarDef:
      return std::string("(") + ToString(expression_) + ")";
    case Type::VarUse: {
      auto where = vars.find(ToString(VarUseName()));
      if (where != vars.end()) {
        // Return the escaped form of the current value of the variable.
        return RE2::QuoteMeta((*where).second);
      } else {
        // The variable is not yet set.  Should not get here.
        return "";
      }
    }
  }
  return "";  // Unreachable.  But we need to satisfy GCC.
}

bool Check::Matches(StringPiece* input, StringPiece* captured,
                    VarMapping* vars) const {
  if (parts_.empty()) return false;
  for (auto& part : parts_) {
    if (!part->MightMatch(*vars)) return false;
  }

  std::unordered_map<int, std::string> var_def_indices;

  std::ostringstream consume_regex;
  int num_captures = 1;  // The outer capture.
  for (auto& part : parts_) {
    consume_regex << part->Regex(*vars);
    const auto var_def_name = part->VarDefName();
    if (!var_def_name.empty()) {
      var_def_indices[num_captures++] = ToString(var_def_name);
    }
    num_captures += part->NumCapturingGroups();
  }
  std::unique_ptr<StringPiece[]> captures(new StringPiece[num_captures]);
  const bool matched = RE2(consume_regex.str())
                           .Match(*input, 0, input->size(), RE2::UNANCHORED,
                                  captures.get(), num_captures);
  if (matched) {
    *captured = captures[0];
    input->remove_prefix(captured->end() - input->begin());
    // Update the variable mapping.
    for (auto& var_def_index : var_def_indices) {
      const int index = var_def_index.first;
      (*vars)[var_def_index.second] = ToString(captures[index]);
    }
  }

  return matched;
}

namespace {
// Returns a Result and a parts list for the given pattern.  This splits out
// regular expressions as delimited by {{ and }}, and also variable uses and
// definitions.  This can fail when a regular expression is invalid.
std::pair<Result, Check::Parts> PartsForPattern(StringPiece pattern) {
  Check::Parts parts;
  StringPiece fixed, regex, var;

  using Type = Check::Part::Type;

  while (!pattern.empty()) {
    const auto regex_start = pattern.find("{{");
    const auto regex_end = pattern.find("}}");
    const auto var_start = pattern.find("[[");
    const auto var_end = pattern.find("]]");
    const bool regex_exists =
        regex_start < regex_end && regex_end < StringPiece::npos;
    const bool var_exists = var_start < var_end && var_end < StringPiece::npos;

    if (regex_exists && (!var_exists || regex_start < var_start)) {
      const auto consumed =
          RE2::Consume(&pattern, "(.*?){{(.*?)}}", &fixed, &regex);
      if (!consumed) {
        assert(consumed &&
               "Did not make forward progress for regex in check rule");
      }
      if (!fixed.empty()) {
        parts.emplace_back(
            effcee::make_unique<Check::Part>(Type::Fixed, fixed));
      }
      if (!regex.empty()) {
        parts.emplace_back(
            effcee::make_unique<Check::Part>(Type::Regex, regex));
        if (parts.back()->NumCapturingGroups() < 0) {
          return std::make_pair(
              Result(Result::Status::BadRule,
                     std::string("invalid regex: ") + ToString(regex)),
              Check::Parts());
        }
      }
    } else if (var_exists && (!regex_exists || var_start < regex_start)) {
      const auto consumed =
          RE2::Consume(&pattern, "(.*?)\\[\\[(.*?)\\]\\]", &fixed, &var);
      if (!consumed) {
        assert(consumed &&
               "Did not make forward progress for var in check rule");
      }
      if (!fixed.empty()) {
        parts.emplace_back(
            effcee::make_unique<Check::Part>(Type::Fixed, fixed));
      }
      if (!var.empty()) {
        auto colon = var.find(":");
        // A colon at the end is useless anyway, so just make it a variable
        // use.
        if (colon == StringPiece::npos || colon == var.size() - 1) {
          parts.emplace_back(
              effcee::make_unique<Check::Part>(Type::VarUse, var));
        } else {
          StringPiece name = var.substr(0, colon);
          StringPiece expression = var.substr(colon + 1, StringPiece::npos);
          parts.emplace_back(effcee::make_unique<Check::Part>(
              Type::VarDef, var, name, expression));
          if (parts.back()->NumCapturingGroups() < 0) {
            return std::make_pair(
                Result(
                    Result::Status::BadRule,
                    std::string("invalid regex in variable definition for ") +
                        ToString(name) + ": " + ToString(expression)),
                Check::Parts());
          }
        }
      }
    } else {
      // There is no regex, no var def, no var use.  Must be a fixed string.
      parts.push_back(effcee::make_unique<Check::Part>(Type::Fixed, pattern));
      break;
    }
  }

  return std::make_pair(Result(Result::Status::Ok), std::move(parts));
}

}  // namespace

std::pair<Result, CheckList> ParseChecks(StringPiece str,
                                         const Options& options) {
  // Returns a pair whose first member is a result constructed from the
  // given status and message, and the second member is an empy pattern.
  auto failure = [](Status status, StringPiece message) {
    return std::make_pair(Result(status, message), CheckList{});
  };

  if (options.prefix().size() == 0)
    return failure(Status::BadOption, "Rule prefix is empty");
  if (RE2::FullMatch(options.prefix(), "\\s+"))
    return failure(Status::BadOption,
                   "Rule prefix is whitespace.  That's silly.");

  CheckList check_list;

  const auto quoted_prefix = RE2::QuoteMeta(options.prefix());
  // Match the following parts:
  //    .*?               - Text that is not the rule prefix
  //    quoted_prefix     - A Simple Check prefix
  //    (-NEXT|-SAME)?    - An optional check type suffix. Two shown here.
  //    :                 - Colon
  //    \s*               - Whitespace
  //    (.*?)             - Captured parameter
  //    \s*               - Whitespace
  //    $                 - End of line

  const RE2 regexp(std::string(".*?") + quoted_prefix +
                   "(-NEXT|-SAME|-DAG|-LABEL|-NOT)?"
                   ":\\s*(.*?)\\s*$");
  Cursor cursor(str);
  while (!cursor.Exhausted()) {
    const auto line = cursor.RestOfLine();

    StringPiece matched_param;
    StringPiece suffix;
    if (RE2::PartialMatch(line, regexp, &suffix, &matched_param)) {
      const Type type = TypeForSuffix(suffix);
      auto parts = PartsForPattern(matched_param);
      if (!parts.first) return std::make_pair(parts.first, CheckList());
      check_list.push_back(Check(type, matched_param, std::move(parts.second)));
    }
    cursor.AdvanceLine();
  }

  if (check_list.empty()) {
    return failure(
        Status::NoRules,
        std::string("No check rules specified. Looking for prefix ") +
            options.prefix());
  }

  if (check_list[0].type() == Type::Same) {
    return failure(Status::BadRule, std::string(options.prefix()) +
                                        "-SAME can't be the first check rule");
  }

  return std::make_pair(Result(Result::Status::Ok), check_list);
}
}  // namespace effcee
