#include <emscripten/bind.h>
#include <emscripten/version.h>

#include <cstdint>
#include <regex>
#include <stdexcept>
#include <string>

namespace {

using emscripten::val;

constexpr int kBridgeAbiVersion = 1;
constexpr std::size_t kMaximumCaptureGroups = 1000;

std::wstring decode_utf8(const std::string& input) {
  std::wstring output;
  output.reserve(input.size());
  for (std::size_t index = 0; index < input.size();) {
    const auto first = static_cast<unsigned char>(input[index]);
    std::uint32_t code_point = 0;
    std::size_t width = 0;
    if (first <= 0x7f) {
      code_point = first;
      width = 1;
    } else if ((first & 0xe0) == 0xc0) {
      code_point = first & 0x1f;
      width = 2;
    } else if ((first & 0xf0) == 0xe0) {
      code_point = first & 0x0f;
      width = 3;
    } else if ((first & 0xf8) == 0xf0) {
      code_point = first & 0x07;
      width = 4;
    } else {
      throw std::invalid_argument("Input is not valid UTF-8.");
    }
    if (index + width > input.size()) {
      throw std::invalid_argument("Input ends inside a UTF-8 sequence.");
    }
    for (std::size_t offset = 1; offset < width; ++offset) {
      const auto continuation =
          static_cast<unsigned char>(input[index + offset]);
      if ((continuation & 0xc0) != 0x80) {
        throw std::invalid_argument("Input contains invalid UTF-8.");
      }
      code_point = (code_point << 6) | (continuation & 0x3f);
    }
    const bool overlong = (width == 2 && code_point < 0x80) ||
                          (width == 3 && code_point < 0x800) ||
                          (width == 4 && code_point < 0x10000);
    if (overlong || code_point > 0x10ffff ||
        (code_point >= 0xd800 && code_point <= 0xdfff)) {
      throw std::invalid_argument("Input contains invalid Unicode.");
    }
    output.push_back(static_cast<wchar_t>(code_point));
    index += width;
  }
  return output;
}

void append_utf8_code_point(std::string& output, std::uint32_t code_point) {
  if (code_point <= 0x7f) {
    output.push_back(static_cast<char>(code_point));
  } else if (code_point <= 0x7ff) {
    output.push_back(static_cast<char>(0xc0 | (code_point >> 6)));
    output.push_back(static_cast<char>(0x80 | (code_point & 0x3f)));
  } else if (code_point <= 0xffff) {
    output.push_back(static_cast<char>(0xe0 | (code_point >> 12)));
    output.push_back(static_cast<char>(0x80 | ((code_point >> 6) & 0x3f)));
    output.push_back(static_cast<char>(0x80 | (code_point & 0x3f)));
  } else {
    output.push_back(static_cast<char>(0xf0 | (code_point >> 18)));
    output.push_back(
        static_cast<char>(0x80 | ((code_point >> 12) & 0x3f)));
    output.push_back(static_cast<char>(0x80 | ((code_point >> 6) & 0x3f)));
    output.push_back(static_cast<char>(0x80 | (code_point & 0x3f)));
  }
}

std::string encode_utf8(const std::wstring& input) {
  std::string output;
  output.reserve(input.size());
  for (const wchar_t character : input) {
    append_utf8_code_point(output, static_cast<std::uint32_t>(character));
  }
  return output;
}

class BoundedOutput {
 public:
  explicit BoundedOutput(std::size_t maximum_bytes)
      : maximum_bytes_(maximum_bytes) {}

  void append(
      std::wstring::const_iterator first,
      std::wstring::const_iterator last) {
    if (truncated_) return;
    for (; first != last; ++first) {
      std::string encoded;
      append_utf8_code_point(
          encoded, static_cast<std::uint32_t>(*first));
      if (value_.size() + encoded.size() > maximum_bytes_) {
        truncated_ = true;
        return;
      }
      value_ += encoded;
    }
  }

  void append(const std::wstring& value) {
    append(value.cbegin(), value.cend());
  }

  const std::string& value() const { return value_; }
  bool truncated() const { return truncated_; }

 private:
  std::size_t maximum_bytes_;
  std::string value_;
  bool truncated_ = false;
};

template <typename Iterator>
void append_native_format_bounded(
    BoundedOutput& output,
    const std::match_results<Iterator>& match,
    const std::wstring& replacement) {
  for (std::size_t index = 0;
       index < replacement.size() && !output.truncated();
       ++index) {
    if (replacement[index] != L'$' || index + 1 == replacement.size()) {
      output.append(
          replacement.cbegin() + index,
          replacement.cbegin() + index + 1);
      continue;
    }
    const wchar_t next = replacement[index + 1];
    if (next == L'$') {
      output.append(
          replacement.cbegin() + index + 1,
          replacement.cbegin() + index + 2);
      ++index;
    } else if (next == L'&') {
      output.append(match[0].first, match[0].second);
      ++index;
    } else if (next == L'`') {
      output.append(match.prefix().first, match.prefix().second);
      ++index;
    } else if (next == L'\'') {
      output.append(match.suffix().first, match.suffix().second);
      ++index;
    } else if (next >= L'0' && next <= L'9') {
      std::size_t capture = static_cast<std::size_t>(next - L'0');
      ++index;
      if (index + 1 < replacement.size() &&
          replacement[index + 1] >= L'0' &&
          replacement[index + 1] <= L'9') {
        capture =
            capture * 10 +
            static_cast<std::size_t>(replacement[index + 1] - L'0');
        ++index;
      }
      output.append(match[capture].first, match[capture].second);
    } else {
      output.append(
          replacement.cbegin() + index,
          replacement.cbegin() + index + 1);
    }
  }
}

bool replacement_expansion_self_test() {
  const std::wstring subject = L"xab!";
  const std::wregex expression(L"(a)(b)?");
  std::wsmatch match;
  if (!std::regex_search(subject, match, expression)) return false;
  const std::wstring fixtures[] = {
      L"<$1:$2:$&>",
      L"$$-$`-$'",
      L"$0-$00-$01-$99",
      L"$x-$",
  };
  for (const auto& replacement : fixtures) {
    BoundedOutput bounded(4096);
    append_native_format_bounded(bounded, match, replacement);
    if (bounded.truncated() ||
        bounded.value() != encode_utf8(match.format(replacement))) {
      return false;
    }
  }

  const std::wstring large_subject(4096, L'a');
  const std::wregex large_expression(L"(a+)");
  std::wsmatch large_match;
  if (!std::regex_search(
          large_subject, large_match, large_expression)) {
    return false;
  }
  std::wstring amplifying_replacement;
  amplifying_replacement.reserve(65'536);
  for (std::size_t index = 0; index < 32'768; ++index) {
    amplifying_replacement += L"$1";
  }
  BoundedOutput adversarial(5);
  append_native_format_bounded(
      adversarial, large_match, amplifying_replacement);
  return adversarial.truncated() && adversarial.value() == "aaaaa";
}

val failure(
    const std::string& phase,
    const std::string& code,
    const std::string& message) {
  val result = val::object();
  result.set("abi", kBridgeAbiVersion);
  result.set("ok", false);
  result.set("phase", phase);
  result.set("code", code);
  result.set(
      "message",
      message.size() <= 1024 ? message : message.substr(0, 1024));
  return result;
}

std::regex_constants::syntax_option_type syntax_options(
    const val& request) {
  using namespace std::regex_constants;
  auto result = syntax_option_type{};
  const val options = request["options"];
  std::string grammar = "ECMAScript";
  const val configured_grammar = options["grammar"];
  if (!configured_grammar.isUndefined()) {
    grammar = configured_grammar.as<std::string>();
  }
  if (grammar == "ECMAScript") {
    result = ECMAScript;
  } else if (grammar == "basic") {
    result = basic;
  } else if (grammar == "extended") {
    result = extended;
  } else if (grammar == "awk") {
    result = awk;
  } else if (grammar == "grep") {
    result = grep;
  } else if (grammar == "egrep") {
    result = egrep;
  } else {
    throw std::invalid_argument("Unsupported std::regex grammar.");
  }
  const std::string flags = request["flags"].as<std::string>();
  if (flags.find('i') != std::string::npos) result |= icase;
  if (flags.find('n') != std::string::npos) result |= nosubs;
  if (flags.find('o') != std::string::npos) result |= optimize;
  if (flags.find('c') != std::string::npos) result |= collate;
  return result;
}

template <typename Iterator>
val match_record(
    const std::match_results<Iterator>& match,
    Iterator subject_begin) {
  val record = val::object();
  val span = val::array();
  span.call<void>(
      "push",
      static_cast<double>(std::distance(subject_begin, match[0].first)));
  span.call<void>(
      "push",
      static_cast<double>(std::distance(subject_begin, match[0].second)));
  record.set("span", span);
  val captures = val::array();
  for (std::size_t group = 1; group < match.size(); ++group) {
    if (!match[group].matched) {
      captures.call<void>("push", val::null());
      continue;
    }
    val capture = val::array();
    capture.call<void>(
        "push",
        static_cast<double>(
            std::distance(subject_begin, match[group].first)));
    capture.call<void>(
        "push",
        static_cast<double>(
            std::distance(subject_begin, match[group].second)));
    captures.call<void>("push", capture);
  }
  record.set("captures", captures);
  return record;
}

val cpp_regex_identity() {
  val identity = val::object();
  identity.set("implementation", "libc++ std::wregex");
  identity.set("cplusplus", static_cast<double>(__cplusplus));
#ifdef __EMSCRIPTEN_MAJOR__
  identity.set(
      "emscripten",
      std::to_string(__EMSCRIPTEN_MAJOR__) + "." +
          std::to_string(__EMSCRIPTEN_MINOR__) + "." +
          std::to_string(__EMSCRIPTEN_TINY__));
#else
  identity.set("emscripten", "unknown");
#endif
  identity.set("wcharBits", static_cast<double>(sizeof(wchar_t) * 8));
  identity.set(
      "replacementSelfTest", replacement_expansion_self_test());
  return identity;
}

val cpp_regex_run(const val& request) {
  try {
    if (request["abi"].as<int>() != kBridgeAbiVersion) {
      return failure("bridge", "unsupported-abi", "Unsupported bridge ABI.");
    }
    const std::string operation = request["operation"].as<std::string>();
    if (operation != "execute" && operation != "replace") {
      return failure(
          "bridge", "invalid-operation", "Unsupported bridge operation.");
    }
    const std::wstring pattern =
        decode_utf8(request["pattern"].as<std::string>());
    const std::wstring subject =
        decode_utf8(request["subject"].as<std::string>());
    const std::wstring replacement =
        operation == "replace"
            ? decode_utf8(request["replacement"].as<std::string>())
            : std::wstring();
    const std::string flags = request["flags"].as<std::string>();
    const bool global = flags.find('g') != std::string::npos;
    const auto maximum_matches =
        request["maximumMatches"].as<std::uint32_t>();
    const auto maximum_capture_rows =
        request["maximumCaptureRows"].as<std::uint32_t>();
    const auto maximum_output_bytes =
        operation == "replace"
            ? request["maximumOutputBytes"].as<std::uint32_t>()
            : 1U;

    std::wregex expression;
    try {
      expression.assign(pattern, syntax_options(request));
    } catch (const std::regex_error& error) {
      return failure("compile", "regex-error", error.what());
    }
    const std::size_t group_count = expression.mark_count();
    if (group_count > kMaximumCaptureGroups) {
      return failure(
          "compile",
          "capture-group-limit",
          "The pattern exceeds the 1,000 capture-group bridge limit.");
    }

    val matches = val::array();
    std::size_t match_count = 0;
    std::size_t capture_rows = 0;
    bool results_truncated = false;
    BoundedOutput output(maximum_output_bytes);
    auto next_output = subject.cbegin();
    using Iterator = std::wstring::const_iterator;
    std::regex_iterator<Iterator> current(
        subject.cbegin(), subject.cend(), expression);
    const std::regex_iterator<Iterator> end;
    for (; current != end; ++current) {
      const auto& match = *current;
      const std::size_t rows = std::max<std::size_t>(1, group_count);
      if (match_count >= maximum_matches ||
          capture_rows + rows > maximum_capture_rows) {
        results_truncated = true;
        break;
      }
      matches.call<void>(
          "push", match_record(match, subject.cbegin()));
      ++match_count;
      capture_rows += rows;

      if (operation == "replace") {
        output.append(next_output, match[0].first);
        append_native_format_bounded(output, match, replacement);
        next_output = match[0].second;
      }
      if (!global) {
        ++current;
        break;
      }
    }
    if (operation == "replace") {
      output.append(next_output, subject.cend());
    }

    val result = val::object();
    result.set("abi", kBridgeAbiVersion);
    result.set("ok", true);
    result.set("groupCount", static_cast<double>(group_count));
    result.set("groupNames", val::array());
    result.set("matches", matches);
    result.set("resultsTruncated", results_truncated);
    if (operation == "replace") {
      result.set("output", output.value());
      result.set(
          "outputBytes", static_cast<double>(output.value().size()));
      result.set("outputTruncated", output.truncated());
    }
    return result;
  } catch (const std::exception& error) {
    return failure("bridge", "bridge-error", error.what());
  } catch (...) {
    return failure(
        "bridge", "bridge-error", "Unknown C++ bridge failure.");
  }
}

}  // namespace

EMSCRIPTEN_BINDINGS(regex_tools_cpp_bridge) {
  emscripten::function("regexToolsCppIdentity", &cpp_regex_identity);
  emscripten::function("regexToolsCppRun", &cpp_regex_run);
}
