#  Copyright (c) Meta Platforms, Inc. and affiliates.
#  This source code is licensed under both the GPLv2 (found in the COPYING file in the root directory)
#  and the Apache 2.0 License (found in the LICENSE.Apache file in the root directory).

#!/usr/bin/env python3
"""Regenerate all checked-in RocksDB C API generated fragments."""

from __future__ import annotations

import argparse
import shutil
import subprocess
import sys
from pathlib import Path


ROOT = Path(__file__).resolve().parents[2]
GEN = ROOT / "tools/c_api_gen/generate_c_api.py"
AUTO_GEN = ROOT / "tools/c_api_gen/auto_simple_bindings.py"
ROUNDTRIP_GEN = ROOT / "tools/c_api_gen/gen_roundtrip_tests.py"
SPEC = ROOT / "tools/c_api_gen/spec.json"
GENERATED_ROOT = ROOT / "c_api_gen"
CLANG_FORMAT_STYLE_FILE = ROOT / ".clang-format"


JOBS = [
    (
        ["DB data operations", "WriteBatch", "Transaction", "TransactionDB simple"],
        GENERATED_ROOT / "c_generated_subset.h.inc",
        GENERATED_ROOT / "c_generated_subset.cc.inc",
    ),
    (
        ["DB data operations"],
        GENERATED_ROOT / "c_generated_db_simple_subset.h.inc",
        GENERATED_ROOT / "c_generated_db_simple_subset.cc.inc",
    ),
    (
        ["WriteBatch"],
        GENERATED_ROOT / "c_generated_writebatch_subset.h.inc",
        GENERATED_ROOT / "c_generated_writebatch_subset.cc.inc",
    ),
    (
        ["Transaction"],
        GENERATED_ROOT / "c_generated_transaction_subset.h.inc",
        GENERATED_ROOT / "c_generated_transaction_subset.cc.inc",
    ),
    (
        ["TransactionDB simple"],
        GENERATED_ROOT / "c_generated_transactiondb_subset.h.inc",
        GENERATED_ROOT / "c_generated_transactiondb_subset.cc.inc",
    ),
    (
        ["BlockBasedOptions simple"],
        GENERATED_ROOT / "c_generated_block_based_options_subset.h.inc",
        GENERATED_ROOT / "c_generated_block_based_options_subset.cc.inc",
    ),
    (
        ["JobInfo metadata simple"],
        GENERATED_ROOT / "c_generated_jobinfo_metadata_subset.h.inc",
        GENERATED_ROOT / "c_generated_jobinfo_metadata_subset.cc.inc",
    ),
    (
        ["CuckooOptions simple"],
        GENERATED_ROOT / "c_generated_cuckoo_options_subset.h.inc",
        GENERATED_ROOT / "c_generated_cuckoo_options_subset.cc.inc",
    ),
    (
        ["Options simple"],
        GENERATED_ROOT / "c_generated_options_subset.h.inc",
        GENERATED_ROOT / "c_generated_options_subset.cc.inc",
    ),
]


def run_job(sections: list[str], header_out: Path, source_out: Path) -> None:
    cmd = [
        sys.executable,
        str(GEN),
        "--spec",
        str(SPEC),
    ]
    for section in sections:
        cmd.extend(["--section", section])
    cmd.extend(
        [
            "--header-out",
            str(header_out),
            "--source-out",
            str(source_out),
        ]
    )
    subprocess.run(cmd, cwd=ROOT, check=True)


def format_generated_outputs(clang_format: str | None) -> None:
    # The checked-in .inc fragments (and therefore the inlined c.h/c.cc) are
    # clang-format-canonicalized. Pin the formatter (e.g. clang-format-21) to
    # match CI so regeneration is byte-reproducible across environments; see
    # tools/c_api_gen/README.md for the required version.
    resolved = shutil.which(clang_format) if clang_format else None
    if resolved is None:
        print(
            f"warning: clang-format binary '{clang_format}' not found in PATH; "
            "skipping formatting (generated output may not be byte-reproducible)",
            file=sys.stderr,
        )
        return

    generated_paths = sorted(GENERATED_ROOT.glob("*.inc"))
    subprocess.run(
        [
            resolved,
            f"--style=file:{CLANG_FORMAT_STYLE_FILE}",
            "-i",
            *(str(path) for path in generated_paths),
        ],
        cwd=ROOT,
        check=True,
    )


C_BASE_H = ROOT / "tools/c_api_gen/c_base.h"
C_H = ROOT / "include/rocksdb/c.h"
C_BASE_CC = ROOT / "tools/c_api_gen/c_base.cc"
C_CC = ROOT / "db/c.cc"


def _generated_banner(edit_target: str) -> str:
    return (
        "// @generated\n"
        "// -----------------------------------------------------------------------------\n"
        "// Auto-generated by tools/c_api_gen/regen_all.py.\n"
        "// DO NOT EDIT THIS FILE DIRECTLY.\n"
        f"// Edit {edit_target} or the inputs under tools/c_api_gen/,\n"
        "// then rerun: python3 tools/c_api_gen/regen_all.py\n"
        "// -----------------------------------------------------------------------------\n\n"
    )


def _strip_source_template_notice(base: str) -> str:
    import re

    return re.sub(
        r"(?ms)^// SOURCE TEMPLATE -- DO NOT .*?^//\s*$\n?",
        "",
        base,
        count=1,
    )


def _split_leading_line_comment_header(base: str) -> tuple[str, str]:
    lines = base.splitlines(keepends=True)
    idx = 0

    while idx < len(lines):
        stripped = lines[idx].lstrip()
        if stripped.startswith("//"):
            idx += 1
            continue
        break

    return "".join(lines[:idx]), "".join(lines[idx:])


def _inline_fragments(
    base_path: Path,
    out_path: Path,
    inc_pattern: str,
    search_root: Path,
    main_include: str | None = None,
    generated_banner: str | None = None,
) -> None:
    """Inline generated .inc fragments into out_path from base_path."""
    import re
    from pathlib import Path as _Path

    base = _strip_source_template_notice(base_path.read_text())
    if generated_banner is not None:
        header, rest = _split_leading_line_comment_header(base)
        if header:
            base = header.rstrip("\n") + "\n" + generated_banner + rest.lstrip("\n")
        else:
            base = generated_banner + base.lstrip("\n")
    # Prepend main include as a separate group (own line + blank line)
    if main_include is not None:
        first_include = re.search(r'^#include ', base, re.MULTILINE)
        if first_include:
            pos = first_include.start()
            base = base[:pos] + main_include + "\n\n" + base[pos:]

    def inline_include(m: re.Match) -> str:
        inc_path = search_root / m.group(1)
        inc_content = inc_path.read_text().strip("\n")
        filename = _Path(m.group(1)).name
        return (
            f"// BEGIN generated: {filename}\n"
            + inc_content
            + f"\n// END generated: {filename}"
        )

    result = re.sub(inc_pattern, inline_include, base)
    out_path.write_text(result)
    print(f"Generated {out_path.relative_to(ROOT)}")


def generate_c_h() -> None:
    """Inline all generated .h.inc fragments into c.h from c_base.h."""
    _inline_fragments(
        C_BASE_H,
        C_H,
        r'#include ["<](c_api_gen/[^">]+)[">]',
        ROOT,
        generated_banner=_generated_banner("tools/c_api_gen/c_base.h"),
    )


def generate_c_cc() -> None:
    """Inline all generated .cc.inc fragments into c.cc from c_base.cc."""
    _inline_fragments(
        C_BASE_CC,
        C_CC,
        r'#include "(c_api_gen/[^"]+)"',
        ROOT,
        main_include='#include "rocksdb/c.h"',
        generated_banner=_generated_banner("tools/c_api_gen/c_base.cc"),
    )


def main() -> int:
    parser = argparse.ArgumentParser(
        description="Regenerate all checked-in RocksDB C API generated fragments."
    )
    parser.add_argument(
        "--clang-format",
        default="clang-format",
        help=(
            "clang-format binary used to canonicalize the generated .inc "
            "fragments (default: clang-format). Pin to the version CI uses "
            "(e.g. clang-format-21) for byte-reproducible output."
        ),
    )
    args = parser.parse_args()

    for sections, header_out, source_out in JOBS:
        run_job(sections, header_out, source_out)
    subprocess.run([sys.executable, str(AUTO_GEN)], cwd=ROOT, check=True)
    format_generated_outputs(args.clang_format)
    generate_c_h()
    generate_c_cc()
    # c.h and c.cc are assembled from already-formatted .inc fragments.
    # We do NOT re-run clang-format on them after inlining -- the .inc content
    # is formatted above, and reformatting the inlined result would produce
    # different line-wrapping since clang-format sees more context.
    #
    # Generate the round-trip C tests last: they read the (now generated)
    # c.h to discover which option objects have a parameterless create/destroy
    # and the .h.inc fragments to pair setters with getters. Re-run formatting
    # so the new test fragment is canonicalized like the rest. This does not
    # affect c.h/c.cc, which are not assembled from the test fragment.
    subprocess.run([sys.executable, str(ROUNDTRIP_GEN)], cwd=ROOT, check=True)
    format_generated_outputs(args.clang_format)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
