#!/bin/sh
set -eu

: "${PRE15_DUT:?PRE15_DUT is required}"
: "${PRE15_ROOT:?PRE15_ROOT is required}"
: "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}"
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
: "${PRE15_FREEBSD_SRC:?PRE15_FREEBSD_SRC is required}"
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
. "$PRE15_LIB_DIR/runner.sh"

probe=$PRE15_DUT/tests/pre15/probes/ondisk_layout.c
freebsd_header=$PRE15_DUT/src/erofs_fs.h
internal_header=$PRE15_DUT/src/internal.h
linux_header=$PRE15_ROOT/src-linux/erofs_fs.h
sys=$PRE15_FREEBSD_SRC/sys
target=x86_64-unknown-freebsd15.0
artifacts=$PRE15_RUN_DIR/artifacts
include_root=$PRE15_CASE_TMP/include

for tool in clang python3 sha256sum; do
	command -v "$tool" >/dev/null 2>&1 || \
		pre15_infra_blocked "missing B02 host tool: $tool"
done
test -d "$sys/amd64/include" || \
	pre15_infra_blocked "FreeBSD amd64 headers are absent: $sys"

pre15_record_fixture b02-layout-probe "$probe"
pre15_record_fixture b02-freebsd-ondisk-header "$freebsd_header"
pre15_record_fixture b02-freebsd-internal-header "$internal_header"
pre15_record_fixture b02-linux-ondisk-header "$linux_header"

mkdir -p "$artifacts" "$include_root"
ln -s "$sys/amd64/include" "$include_root/machine"
ln -s "$sys/x86/include" "$include_root/x86"
resource_include=$(clang -print-resource-dir)/include
pre15_target_reached

if clang --target="$target" -fsyntax-only -std=gnu17 -Wall -Wextra -Werror \
	-nostdinc -isystem "$resource_include" \
	-isystem "$PRE15_FREEBSD_SRC/include" -isystem "$include_root" \
	-isystem "$sys" -Xclang -fdump-record-layouts "$probe" \
	> "$artifacts/freebsd-record-layouts.txt" \
	2> "$artifacts/freebsd-layout.stderr"; then
	:
else
	pre15_dut_fail 'FreeBSD ondisk layout/type/macro oracle failed'
fi
if clang -DB02_LINUX_REFERENCE -fsyntax-only -std=gnu17 \
	-Wall -Wextra -Werror -Xclang -fdump-record-layouts "$probe" \
	> "$artifacts/linux-record-layouts.txt" \
	2> "$artifacts/linux-layout.stderr"; then
	:
else
	pre15_dut_fail 'Linux reference layout/type/macro oracle failed'
fi

clang --target="$target" -E -dM -std=gnu17 -nostdinc \
	-isystem "$resource_include" -isystem "$PRE15_FREEBSD_SRC/include" \
	-isystem "$include_root" -isystem "$sys" "$probe" | \
	awk '$2 ~ /^(EROFS_|Z_EROFS_)/ { print }' | LC_ALL=C sort \
	> "$artifacts/freebsd-macros.txt"
clang -DB02_LINUX_REFERENCE -E -dM -std=gnu17 "$probe" | \
	awk '$2 ~ /^(EROFS_|Z_EROFS_)/ { print }' | LC_ALL=C sort \
	> "$artifacts/linux-macros.txt"

if python3 - "$PRE15_ROOT" "$artifacts/static-check.json" <<'PY'
from __future__ import annotations

import json
from pathlib import Path
import re
import sys


root = Path(sys.argv[1])
output = Path(sys.argv[2])
dut = root / "repo-pre-15"
src = dut / "src"
ondisk = (src / "erofs_fs.h").read_text(encoding="utf-8")
internal = (src / "internal.h").read_text(encoding="utf-8")
linux = (root / "src-linux/erofs_fs.h").read_text(encoding="utf-8")


def require_once(source: str, token: str) -> None:
    count = source.count(token)
    if count != 1:
        raise SystemExit(f"expected one occurrence of {token!r}, found {count}")


def require_order(source: str, tokens: tuple[str, ...], label: str) -> None:
    positions = []
    for token in tokens:
        require_once(source, token)
        positions.append(source.index(token))
    if positions != sorted(positions):
        raise SystemExit(f"{label} section order mismatch: {tokens!r}")


ondisk_markers = (
    "struct erofs_deviceslot {",
    "struct erofs_super_block {",
    "EROFS_INODE_CHUNK_BASED",
    "#define EROFS_CHUNK_FORMAT_BLKBITS_MASK",
    "#define EROFS_INODE_LAYOUT_COMPACT",
    "struct erofs_inode_chunk_info {",
    "struct erofs_inode_compact {",
    "struct erofs_inode_extended {",
    "struct erofs_xattr_ibody_header {",
    "struct erofs_xattr_entry {",
    "struct erofs_xattr_long_prefix {",
    "#define EROFS_NULL_ADDR",
    "struct erofs_inode_chunk_index {",
    "#define EROFS_DIRENT_NID_METABOX_BIT",
    "struct erofs_dirent {",
    "#define EROFS_NAME_LEN",
    "struct z_erofs_lz4_cfgs {",
    "struct z_erofs_map_header {",
    "Z_EROFS_LCLUSTER_TYPE_PLAIN",
    "struct z_erofs_lcluster_index {",
    "struct z_erofs_extent {",
)
require_order(ondisk, ondisk_markers, "FreeBSD ondisk")
require_order(linux, ondisk_markers, "Linux ondisk")

aliases = {
    "EROFS_INODE_LAYOUT_COMPACT": "0",
    "EROFS_INODE_LAYOUT_EXTENDED": "1",
    "EROFS_INODE_LAYOUT_PLAIN": "EROFS_INODE_FLAT_PLAIN",
}
for name, value in aliases.items():
    pattern = rf"^#define[ \t]+{name}[ \t]+{value}$"
    if len(re.findall(pattern, ondisk, re.MULTILINE)) != 1:
        raise SystemExit(f"layout alias mismatch: {name}")

if not re.search(
    r"^#define[ \t]+EROFS_DEVT_SLOT_SIZE[ \t]+"
    r"sizeof\(struct erofs_deviceslot\)$",
    ondisk,
    re.MULTILINE,
):
    raise SystemExit("EROFS_DEVT_SLOT_SIZE is not derived from its record")
if re.search(r"^#if.*EROFS_DEVT_SLOT_SIZE", ondisk, re.MULTILINE):
    raise SystemExit("sizeof-based EROFS_DEVT_SLOT_SIZE is used in #if")

internal_markers = (
    "struct erofs_mount_opts {",
    "struct erofs_sb_lz4_info {",
    "struct erofs_device_info {",
    "struct erofs_xattr_prefix_item {",
    "struct erofs_zextent_cache {",
    "struct erofs_mount {",
    "#define EROFS_FEATURE_FUNCS",
    "EROFS_FEATURE_FUNCS(lz4_0padding",
    "struct erofs_node {",
    "struct erofs_fid {",
    "erofs_inode_version(unsigned int ifmt)",
    "#define EROFS_MAP_MAPPED",
    "struct erofs_map_blocks {",
    "struct erofs_map_dev {",
    "/* Buffer and device I/O. */",
    "/* Logical mapping and file data. */",
    "/* Inode and vnode lifecycle. */",
    "/* Directory operations. */",
    "/* Compressed mapping and data. */",
    "/* Compression configuration. */",
    "/* VOP vectors. */",
)
require_order(internal, internal_markers, "FreeBSD internal")
if re.search(r"return[ \t]+-E[A-Z0-9_]+", internal):
    raise SystemExit("Linux negative-errno convention entered FreeBSD internal.h")

vnops = (src / "erofs_vnops.c").read_text(encoding="utf-8")
super_source = (src / "super.c").read_text(encoding="utf-8")
inode = (src / "inode.c").read_text(encoding="utf-8")


def initializer(source: str, declaration: str, prefix: str) -> list[tuple[str, str]]:
    match = re.search(re.escape(declaration) + r"\s*=\s*\{(.*?)\n\};", source, re.DOTALL)
    if not match:
        raise SystemExit(f"initializer absent: {declaration}")
    return re.findall(
        rf"^\s*\.({prefix}_[A-Za-z0-9_]+)\s*=\s*([^,]+),",
        match.group(1),
        re.MULTILINE,
    )


vnode = initializer(vnops, "struct vop_vector erofs_vnodeops", "vop")
fifo = initializer(vnops, "struct vop_vector erofs_fifoops", "vop")
vfs = initializer(super_source, "static struct vfsops erofs_vfsops", "vfs")
expected_vnode = [
    ("vop_default", "&default_vnodeops"),
    ("vop_inactive", "erofs_inactive"),
    ("vop_reclaim", "erofs_reclaim"),
    ("vop_lookup", "vfs_cache_lookup"),
    ("vop_cachedlookup", "erofs_lookup"),
    ("vop_readdir", "erofs_readdir"),
    ("vop_readlink", "erofs_readlink"),
    ("vop_open", "erofs_open"),
    ("vop_read", "erofs_read"),
    ("vop_bmap", "erofs_bmap"),
    ("vop_getpages", "vnode_pager_local_getpages"),
    ("vop_getpages_async", "vnode_pager_local_getpages_async"),
    ("vop_getattr", "erofs_getattr"),
    ("vop_setattr", "erofs_setattr"),
    ("vop_access", "erofs_access"),
    ("vop_pathconf", "erofs_pathconf"),
    ("vop_getextattr", "erofs_getextattr"),
    ("vop_listextattr", "erofs_listextattr"),
    ("vop_deleteextattr", "erofs_deleteextattr"),
    ("vop_setextattr", "erofs_setextattr"),
    ("vop_getacl", "erofs_vop_getacl"),
    ("vop_aclcheck", "erofs_aclcheck"),
    ("vop_setacl", "erofs_setacl"),
    ("vop_vptofh", "erofs_vptofh"),
]
expected_fifo = [
    ("vop_default", "&fifo_specops"),
    ("vop_access", "erofs_access"),
    ("vop_aclcheck", "erofs_aclcheck"),
    ("vop_deleteextattr", "erofs_deleteextattr"),
    ("vop_getacl", "erofs_vop_getacl"),
    ("vop_getextattr", "erofs_getextattr"),
    ("vop_getattr", "erofs_getattr"),
    ("vop_listextattr", "erofs_listextattr"),
    ("vop_pathconf", "erofs_pathconf"),
    ("vop_reclaim", "erofs_reclaim"),
    ("vop_setacl", "erofs_setacl"),
    ("vop_setattr", "erofs_setattr"),
    ("vop_setextattr", "erofs_setextattr"),
    ("vop_vptofh", "erofs_vptofh"),
]
expected_vfs = [
    ("vfs_fhtovp", "erofs_fhtovp"),
    ("vfs_mount", "erofs_mount"),
    ("vfs_root", "erofs_root"),
    ("vfs_statfs", "erofs_statfs"),
    ("vfs_unmount", "erofs_unmount"),
    ("vfs_vget", "erofs_vgetf"),
]
if vnode != expected_vnode or fifo != expected_fifo or vfs != expected_vfs:
    raise SystemExit("VOP/VFS callback targets changed")

hash_calls = re.findall(
    r"vfs_hash_(?:get|insert)\s*\([^;]*?erofs_vfs_hash_cmp", inode, re.DOTALL
)
if len(hash_calls) != 2:
    raise SystemExit(f"hash comparator callsites changed: {len(hash_calls)}")

decompressors = []
for path in sorted(src.glob("decompressor*.c")):
    for line in path.read_text(encoding="utf-8").splitlines():
        match = re.match(r"\s*\.(config|decompress)\s*=\s*([^,]+),", line)
        if match:
            decompressors.append((path.name, match.group(1), match.group(2)))
expected_decompressors = [
    ("decompressor.c", "decompress", "z_erofs_transform_plain"),
    ("decompressor.c", "decompress", "z_erofs_transform_plain"),
    ("decompressor.c", "config", "z_erofs_load_lz4_config"),
    ("decompressor.c", "decompress", "z_erofs_lz4_decompress"),
    ("decompressor_deflate.c", "config", "z_erofs_load_deflate_config"),
    ("decompressor_deflate.c", "decompress", "z_erofs_deflate_decompress"),
    ("decompressor_lzma.c", "config", "z_erofs_load_lzma_config"),
    ("decompressor_lzma.c", "decompress", "z_erofs_lzma_decompress"),
    ("decompressor_zstd.c", "config", "z_erofs_load_zstd_config"),
    ("decompressor_zstd.c", "decompress", "z_erofs_zstd_decompress"),
]
if decompressors != expected_decompressors:
    raise SystemExit("decompressor callback targets changed")

result = {
    "status": "PASS",
    "source_symbol_denominator": {
        "baseline_physical": 464,
        "baseline_unique": 389,
        "b02_macro_delta": 3,
        "expected_physical": 467,
        "expected_unique": 392,
        "added_names": sorted(aliases),
        "kld_symbol_delta": 0,
    },
    "callbacks": {
        "vnode_slots": len(vnode),
        "fifo_slots": len(fifo),
        "vfs_slots": len(vfs),
        "hash_comparator_callsites": len(hash_calls),
        "decompressor_fields": len(decompressors),
    },
    "freebsd_adaptations": [
        "packed ondisk records",
        "little-endian typedefs",
        "explicit supported incompat mask",
        "FreeBSD VOP/VFS types",
        "positive errno convention",
    ],
}
with output.open("x", encoding="ascii") as stream:
    json.dump(result, stream, ensure_ascii=True, indent=2, sort_keys=True)
    stream.write("\n")
print(
    "static PASS source-symbols=464+3 callbacks=24+14/6 hash=2 decompress=10"
)
PY
then
	:
else
	pre15_dut_fail 'header order, symbol denominator, or callback oracle failed'
fi

sha256sum \
	"$artifacts/freebsd-record-layouts.txt" \
	"$artifacts/linux-record-layouts.txt" \
	"$artifacts/freebsd-macros.txt" \
	"$artifacts/linux-macros.txt" \
	"$artifacts/static-check.json" \
	> "$artifacts/SHA256SUMS"

printf 'layout PASS FreeBSD and Linux size/offset/alignment/type/macro oracles\n'
