import {
  AVAILABLE_REGEX_FLAVOURS,
  parseRegexFlags,
  parseRegexOptions,
  resolveRegexFlavourVersion,
} from "../flavours/flavour-registry";
import type { RegexEngineOptions } from "../model/flavour";
import {
  DEFAULT_REGEX_LIMITS,
  utf8ByteLength,
} from "../execution/request-limits";

export type Pcre2COperation = "match" | "replace";

export interface Pcre2CGenerationRequest {
  readonly flavour: "pcre2";
  readonly flavourVersion: string;
  readonly operation: Pcre2COperation;
  readonly pattern: string;
  readonly flags: readonly string[];
  readonly options: RegexEngineOptions;
  readonly subject: string;
  readonly scanAll: boolean;
  readonly replacement?: string;
  readonly maximumMatches: number;
  readonly maximumCaptureRows: number;
  readonly maximumOutputBytes: number;
}

export interface GeneratedPcre2CProgram {
  readonly target: "c17-pcre2-8";
  readonly fileName: string;
  readonly source: string;
  readonly compileCommand: string;
  readonly engineIdentity: string;
  readonly represents: "first-match" | "all-matches" | "replacement";
  readonly caveats: readonly string[];
}

const TEXT_ENCODER = new TextEncoder();
const MAXIMUM_GENERATED_TEXT_BYTES = 1024 * 1024;
const PCRE2_VERSION = "PCRE2 10.47 8-bit";

function integer(
  value: number,
  label: string,
  minimum: number,
  maximum: number,
): number {
  if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
    throw new RangeError(
      `${label} must be an integer from ${minimum.toLocaleString()} to ${maximum.toLocaleString()}.`,
    );
  }
  return value;
}

function assertUnicodeScalarText(value: string, label: string): void {
  for (let index = 0; index < value.length; index += 1) {
    const first = value.charCodeAt(index);
    if (first >= 0xd800 && first <= 0xdbff) {
      const second = value.charCodeAt(index + 1);
      if (second >= 0xdc00 && second <= 0xdfff) {
        index += 1;
        continue;
      }
      throw new Error(
        `${label} contains an unpaired UTF-16 surrogate and cannot be emitted as exact UTF-8.`,
      );
    }
    if (first >= 0xdc00 && first <= 0xdfff) {
      throw new Error(
        `${label} contains an unpaired UTF-16 surrogate and cannot be emitted as exact UTF-8.`,
      );
    }
  }
}

function byteArray(name: string, value: string): string {
  const bytes = [...TEXT_ENCODER.encode(value), 0];
  const rows: string[] = [];
  for (let index = 0; index < bytes.length; index += 12) {
    rows.push(
      `  ${bytes
        .slice(index, index + 12)
        .map((byte) => `0x${byte.toString(16).padStart(2, "0")}`)
        .join(", ")},`,
    );
  }
  return [
    `static const PCRE2_UCHAR ${name}[] = {`,
    ...rows,
    "};",
    `static const PCRE2_SIZE ${name}_length = sizeof(${name}) - 1;`,
  ].join("\n");
}

function compileOptions(flags: readonly string[]): string {
  const options = [
    "PCRE2_UTF",
    "PCRE2_UCP",
    ...(flags.includes("i") ? ["PCRE2_CASELESS"] : []),
    ...(flags.includes("m") ? ["PCRE2_MULTILINE"] : []),
    ...(flags.includes("s") ? ["PCRE2_DOTALL"] : []),
    ...(flags.includes("x") ? ["PCRE2_EXTENDED"] : []),
    ...(flags.includes("U") ? ["PCRE2_UNGREEDY"] : []),
    ...(flags.includes("J") ? ["PCRE2_DUPNAMES"] : []),
  ];
  return options.join(" | ");
}

function commonSource(
  request: Pcre2CGenerationRequest,
  options: Readonly<{
    matchLimit: number;
    depthLimit: number;
    heapLimitKib: number;
  }>,
): string {
  return `/*
 * Generated by add·ideas Regex Tools for the PCRE2 10.47 8-bit C API.
 * Pattern, subject and replacement are independent exact UTF-8 byte arrays.
 * PCRE2_UTF and PCRE2_UCP are always enabled to match the browser runtime.
 *
 * Build:
 *   cc -std=c17 -Wall -Wextra -Werror regex-tools-pcre2.c \\
 *      $(pkg-config --cflags --libs libpcre2-8) -o regex-tools-pcre2
 *
 * There is no native PCRE2 wall-clock timeout. Run this program in a process
 * that your application can terminate if a hard elapsed-time limit is needed.
 */
#define PCRE2_CODE_UNIT_WIDTH 8
#include <pcre2.h>

#include <inttypes.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#if PCRE2_MAJOR != 10 || PCRE2_MINOR != 47
#error "This generated program requires PCRE2 10.47 exactly."
#endif

${byteArray("pattern", request.pattern)}

${byteArray("subject", request.subject)}

static const uint32_t compile_options = ${compileOptions(request.flags)};
static const uint32_t match_limit = ${options.matchLimit}u;
static const uint32_t depth_limit = ${options.depthLimit}u;
static const uint32_t heap_limit_kib = ${options.heapLimitKib}u;
static const size_t maximum_matches = ${request.maximumMatches}u;
static const size_t maximum_capture_rows = ${request.maximumCaptureRows}u;

static void print_pcre2_error(const char *phase, int code) {
  PCRE2_UCHAR message[256];
  int length = pcre2_get_error_message(code, message, sizeof(message));
  if (length < 0) {
    fprintf(stderr, "%s failed with PCRE2 error %d.\\n", phase, code);
    return;
  }
  fprintf(stderr, "%s failed: %.*s (PCRE2 error %d).\\n",
          phase, length, (const char *)message, code);
}

static bool verify_runtime_version(void) {
  char version[64] = {0};
  uint32_t unicode = 0;
  int status = pcre2_config(PCRE2_CONFIG_VERSION, version);
  if (status < 0) {
    print_pcre2_error("pcre2_config(PCRE2_CONFIG_VERSION)", status);
    return false;
  }
  if (strncmp(version, "10.47 ", 6) != 0) {
    fprintf(stderr, "Expected PCRE2 10.47, loaded %s.\\n", version);
    return false;
  }
  status = pcre2_config(PCRE2_CONFIG_UNICODE, &unicode);
  if (status != 0 || unicode != 1) {
    fputs("The loaded PCRE2 10.47 library has no Unicode support.\\n", stderr);
    return false;
  }
  return true;
}

static bool configure_limits(pcre2_match_context *context) {
  int status = pcre2_set_match_limit(context, match_limit);
  if (status != 0) {
    print_pcre2_error("pcre2_set_match_limit", status);
    return false;
  }
  status = pcre2_set_depth_limit(context, depth_limit);
  if (status != 0) {
    print_pcre2_error("pcre2_set_depth_limit", status);
    return false;
  }
  status = pcre2_set_heap_limit(context, heap_limit_kib);
  if (status != 0) {
    print_pcre2_error("pcre2_set_heap_limit", status);
    return false;
  }
  return true;
}`;
}

function matchSource(
  request: Pcre2CGenerationRequest,
  global: boolean,
): string {
  return `${commonSource(request, {
    matchLimit: Number(request.options.matchLimit),
    depthLimit: Number(request.options.depthLimit),
    heapLimitKib: Number(request.options.heapLimitKib),
  })}

/*
 * PCRE2's "global" flag is an application loop, not a compile option.
 * This generated loop implements the empty-match anchored retry described by
 * the PCRE2 API and advances by one UTF-8 character only after that retry
 * reports no match.
 */
static PCRE2_SIZE advance_utf8(PCRE2_SIZE offset, bool crlf_is_newline) {
  if (offset >= subject_length) {
    return subject_length + 1;
  }
  if (crlf_is_newline && subject[offset] == 0x0d &&
      offset + 1 < subject_length &&
      subject[offset + 1] == 0x0a) {
    return offset + 2;
  }
  PCRE2_SIZE width =
      (subject[offset] & 0x80u) == 0 ? 1 :
      (subject[offset] & 0xe0u) == 0xc0u ? 2 :
      (subject[offset] & 0xf0u) == 0xe0u ? 3 : 4;
  return offset + width <= subject_length ? offset + width : subject_length + 1;
}

static const char *capture_name(pcre2_code *code, uint32_t group) {
  uint32_t name_count = 0;
  uint32_t entry_size = 0;
  PCRE2_SPTR table = NULL;
  if (pcre2_pattern_info(code, PCRE2_INFO_NAMECOUNT, &name_count) != 0 ||
      name_count == 0 ||
      pcre2_pattern_info(code, PCRE2_INFO_NAMEENTRYSIZE, &entry_size) != 0 ||
      pcre2_pattern_info(code, PCRE2_INFO_NAMETABLE, &table) != 0) {
    return NULL;
  }
  for (uint32_t index = 0; index < name_count; index += 1) {
    PCRE2_SPTR entry = table + index * entry_size;
    uint32_t number = ((uint32_t)entry[0] << 8) | entry[1];
    if (number == group) {
      return (const char *)(entry + 2);
    }
  }
  return NULL;
}

int main(void) {
  int exit_code = 1;
  int error_code = 0;
  PCRE2_SIZE error_offset = 0;
  pcre2_code *code = NULL;
  pcre2_match_data *match_data = NULL;
  pcre2_match_context *match_context = NULL;
  size_t match_count = 0;
  size_t capture_rows = 0;
  PCRE2_SIZE search_offset = 0;
  uint32_t match_options = 0;
  bool crlf_is_newline = false;

  if (!verify_runtime_version()) {
    return 1;
  }
  code = pcre2_compile(pattern, pattern_length, compile_options,
                       &error_code, &error_offset, NULL);
  if (code == NULL) {
    fprintf(stderr, "Compile error at pattern UTF-8 byte %" PRIuPTR ": ",
            (uintptr_t)error_offset);
    print_pcre2_error("pcre2_compile", error_code);
    goto cleanup;
  }
  match_data = pcre2_match_data_create_from_pattern(code, NULL);
  match_context = pcre2_match_context_create(NULL);
  if (match_data == NULL || match_context == NULL) {
    fputs("Could not allocate PCRE2 match state.\\n", stderr);
    goto cleanup;
  }
  if (!configure_limits(match_context)) {
    goto cleanup;
  }
  uint32_t newline = 0;
  if (pcre2_pattern_info(code, PCRE2_INFO_NEWLINE, &newline) != 0) {
    fputs("Could not read the PCRE2 newline convention.\\n", stderr);
    goto cleanup;
  }
  crlf_is_newline =
      newline == PCRE2_NEWLINE_CRLF ||
      newline == PCRE2_NEWLINE_ANY ||
      newline == PCRE2_NEWLINE_ANYCRLF;

  while (search_offset <= subject_length) {
    int status = pcre2_match(code, subject, subject_length, search_offset,
                             match_options, match_data, match_context);
    if (status == PCRE2_ERROR_NOMATCH && match_options != 0) {
      search_offset = advance_utf8(search_offset, crlf_is_newline);
      match_options = 0;
      continue;
    }
    if (status == PCRE2_ERROR_NOMATCH) {
      break;
    }
    if (status < 0) {
      print_pcre2_error("pcre2_match", status);
      goto cleanup;
    }
    if (status == 0) {
      fputs("The match vector was unexpectedly too small.\\n", stderr);
      goto cleanup;
    }

    uint32_t capture_count = 0;
    if (pcre2_pattern_info(code, PCRE2_INFO_CAPTURECOUNT, &capture_count) != 0) {
      fputs("Could not read the PCRE2 capture count.\\n", stderr);
      goto cleanup;
    }
    size_t required_rows = capture_count == 0 ? 1u : capture_count;
    if (match_count >= maximum_matches ||
        required_rows > maximum_capture_rows - capture_rows) {
      fputs("Regex Tools host-side match/capture result cap reached.\\n", stderr);
      exit_code = 3;
      goto cleanup;
    }

    PCRE2_SIZE *ovector = pcre2_get_ovector_pointer(match_data);
    printf("match %zu: UTF-8 bytes %" PRIuPTR "..%" PRIuPTR "\\n",
           match_count + 1, (uintptr_t)ovector[0], (uintptr_t)ovector[1]);
    for (uint32_t group = 0; group <= capture_count; group += 1) {
      PCRE2_SIZE start = ovector[group * 2];
      PCRE2_SIZE end = ovector[group * 2 + 1];
      const char *name = capture_name(code, group);
      if (start == PCRE2_UNSET || end == PCRE2_UNSET) {
        printf("  group %u%s%s: did not participate\\n",
               group, name == NULL ? "" : " / ", name == NULL ? "" : name);
        continue;
      }
      printf("  group %u%s%s: UTF-8 bytes %" PRIuPTR "..%" PRIuPTR " = ",
             group, name == NULL ? "" : " / ", name == NULL ? "" : name,
             (uintptr_t)start, (uintptr_t)end);
      (void)fwrite(subject + start, 1, (size_t)(end - start), stdout);
      fputc('\\n', stdout);
    }
    match_count += 1;
    capture_rows += required_rows;

    if (!${global ? "true" : "false"}) {
      break;
    }
    search_offset = ovector[1];
    match_options =
        ovector[0] == ovector[1] ? PCRE2_NOTEMPTY_ATSTART | PCRE2_ANCHORED : 0;
  }

  printf("completed: %zu match(es), %zu capture row(s)\\n",
         match_count, capture_rows);
  exit_code = 0;

cleanup:
  pcre2_match_context_free(match_context);
  pcre2_match_data_free(match_data);
  pcre2_code_free(code);
  return exit_code;
}
`;
}

function replacementSource(
  request: Pcre2CGenerationRequest,
  global: boolean,
): string {
  const replacement = request.replacement;
  if (replacement === undefined) {
    throw new Error("PCRE2 replacement generation requires a replacement.");
  }
  return `${commonSource(request, {
    matchLimit: Number(request.options.matchLimit),
    depthLimit: Number(request.options.depthLimit),
    heapLimitKib: Number(request.options.heapLimitKib),
  })}

${byteArray("replacement", replacement)}

static const size_t maximum_output_bytes = ${request.maximumOutputBytes}u;

int main(void) {
  int exit_code = 1;
  int error_code = 0;
  PCRE2_SIZE error_offset = 0;
  pcre2_code *code = NULL;
  pcre2_match_data *match_data = NULL;
  pcre2_match_context *match_context = NULL;
  PCRE2_UCHAR *output = NULL;

  if (!verify_runtime_version()) {
    return 1;
  }
  code = pcre2_compile(pattern, pattern_length, compile_options,
                       &error_code, &error_offset, NULL);
  if (code == NULL) {
    fprintf(stderr, "Compile error at pattern UTF-8 byte %" PRIuPTR ": ",
            (uintptr_t)error_offset);
    print_pcre2_error("pcre2_compile", error_code);
    goto cleanup;
  }
  match_data = pcre2_match_data_create_from_pattern(code, NULL);
  match_context = pcre2_match_context_create(NULL);
  output = malloc(maximum_output_bytes + 1);
  if (match_data == NULL || match_context == NULL || output == NULL) {
    fputs("Could not allocate PCRE2 replacement state.\\n", stderr);
    goto cleanup;
  }
  if (!configure_limits(match_context)) {
    goto cleanup;
  }

  PCRE2_SIZE output_length = maximum_output_bytes;
  uint32_t substitute_options =
      PCRE2_SUBSTITUTE_UNSET_EMPTY${global ? " | PCRE2_SUBSTITUTE_GLOBAL" : ""};
  int substitutions = pcre2_substitute(
      code, subject, subject_length, 0, substitute_options,
      match_data, match_context, replacement, replacement_length,
      output, &output_length);
  if (substitutions == PCRE2_ERROR_NOMEMORY) {
    fprintf(stderr,
            "Replacement exceeds the %zu-byte Regex Tools output cap.\\n",
            maximum_output_bytes);
    exit_code = 3;
    goto cleanup;
  }
  if (substitutions < 0) {
    print_pcre2_error("pcre2_substitute", substitutions);
    goto cleanup;
  }

  uint32_t capture_count = 0;
  if (pcre2_pattern_info(code, PCRE2_INFO_CAPTURECOUNT, &capture_count) != 0) {
    fputs("Could not read the PCRE2 capture count.\\n", stderr);
    goto cleanup;
  }
  size_t rows_per_match = capture_count == 0 ? 1u : capture_count;
  if ((size_t)substitutions > maximum_matches ||
      ((size_t)substitutions != 0 &&
       rows_per_match > maximum_capture_rows / (size_t)substitutions)) {
    fputs("Replacement completed but exceeded the Regex Tools result cap; "
          "output is withheld.\\n", stderr);
    exit_code = 3;
    goto cleanup;
  }

  (void)fwrite(output, 1, (size_t)output_length, stdout);
  fprintf(stderr, "\\ncompleted: %d substitution(s), %zu output bytes\\n",
          substitutions, (size_t)output_length);
  exit_code = 0;

cleanup:
  free(output);
  pcre2_match_context_free(match_context);
  pcre2_match_data_free(match_data);
  pcre2_code_free(code);
  return exit_code;
}
`;
}

function validate(request: Pcre2CGenerationRequest): Pcre2CGenerationRequest {
  if (request.flavour !== "pcre2") {
    throw new Error("The PCRE2 C generator accepts only PCRE2 snapshots.");
  }
  const definition = AVAILABLE_REGEX_FLAVOURS.require("pcre2");
  const version = resolveRegexFlavourVersion(
    request.flavourVersion,
    definition,
    "PCRE2 code-generation version",
  ).definition;
  if (version.value !== "PCRE2 10.47 8-bit WebAssembly") {
    throw new Error("The C generator is pinned to PCRE2 10.47 8-bit.");
  }
  const flags = parseRegexFlags(
    request.flags,
    definition,
    "PCRE2 code-generation flags",
  );
  const options = parseRegexOptions(
    request.options,
    definition,
    "PCRE2 code-generation options",
  );
  assertUnicodeScalarText(request.pattern, "Pattern");
  assertUnicodeScalarText(request.subject, "Subject");
  if (request.replacement !== undefined) {
    assertUnicodeScalarText(request.replacement, "Replacement");
  }
  if (utf8ByteLength(request.pattern) > 1024 * 1024) {
    throw new RangeError("PCRE2 generated patterns are limited to 1 MiB.");
  }
  if (utf8ByteLength(request.subject) > MAXIMUM_GENERATED_TEXT_BYTES) {
    throw new RangeError(
      "Generated C embeds at most 1 MiB of subject text; reduce the fixture without changing the pattern.",
    );
  }
  if (
    request.replacement !== undefined &&
    utf8ByteLength(request.replacement) > 256 * 1024
  ) {
    throw new RangeError(
      "PCRE2 generated replacements are limited to 256 KiB.",
    );
  }
  if (request.operation !== "match" && request.operation !== "replace") {
    throw new Error("Unsupported PCRE2 C generation operation.");
  }
  if (request.operation === "replace" && request.replacement === undefined) {
    throw new Error("Replacement generation requires an exact replacement.");
  }
  return {
    ...request,
    flavourVersion: version.value,
    flags,
    options,
    scanAll: request.scanAll === true,
    maximumMatches: integer(
      request.maximumMatches,
      "Generated match limit",
      1,
      DEFAULT_REGEX_LIMITS.maximumMatches,
    ),
    maximumCaptureRows: integer(
      request.maximumCaptureRows,
      "Generated capture-row limit",
      1,
      DEFAULT_REGEX_LIMITS.maximumCaptureRows,
    ),
    maximumOutputBytes: integer(
      request.maximumOutputBytes,
      "Generated output limit",
      1,
      DEFAULT_REGEX_LIMITS.maximumReplacementOutputBytes,
    ),
  };
}

export function generatePcre2C(
  input: Pcre2CGenerationRequest,
): GeneratedPcre2CProgram {
  const request = validate(input);
  const global = request.scanAll || request.flags.includes("g");
  const represents =
    request.operation === "replace"
      ? "replacement"
      : global
        ? "all-matches"
        : "first-match";
  const source =
    request.operation === "replace"
      ? replacementSource(request, global)
      : matchSource(request, global);
  return {
    target: "c17-pcre2-8",
    fileName:
      request.operation === "replace"
        ? "regex-tools-pcre2-replace.c"
        : "regex-tools-pcre2-match.c",
    source,
    compileCommand:
      "cc -std=c17 -Wall -Wextra -Werror regex-tools-pcre2.c $(pkg-config --cflags --libs libpcre2-8) -o regex-tools-pcre2",
    engineIdentity: PCRE2_VERSION,
    represents,
    caveats: [
      "PCRE2_UTF and PCRE2_UCP are mandatory, matching the Regex Tools WebAssembly runtime.",
      global
        ? "The selected g/scan-all behavior is implemented by host iteration (or PCRE2_SUBSTITUTE_GLOBAL), not by a PCRE2 compile flag."
        : "The program requests only the first match.",
      request.operation === "replace"
        ? "The output byte cap is a host buffer; PCRE2 has no Regex Tools result-row cap, so replacement row limits are checked only after substitution."
        : "Match and capture-row caps are host-loop limits, not native PCRE2 options.",
      "PCRE2 match, depth and heap limits are configured; PCRE2 exposes no native wall-clock timeout, so hard cancellation belongs to the containing process.",
      "Native ranges printed by the C API are UTF-8 byte offsets; browser editor ranges are normalized separately to UTF-16.",
    ],
  };
}
