#!/usr/bin/env php
<?php
declare(strict_types=1);

function arg_value(array $argv, string $name, ?string $default = null): ?string {
    foreach ($argv as $i => $arg) {
        if ($arg === $name && isset($argv[$i + 1])) {
            return $argv[$i + 1];
        }
        if (str_starts_with($arg, $name . '=')) {
            return substr($arg, strlen($name) + 1);
        }
    }
    return $default;
}

function arg_list(array $argv, string $name): ?array {
    $value = arg_value($argv, $name);
    if ($value === null || trim($value) === '') {
        return null;
    }
    return array_values(array_filter(
        array_map('trim', explode(',', $value)),
        static fn($part) => $part !== ''
    ));
}

function filter_enabled(array $filter, string $value): bool {
    return in_array($value, $filter, true);
}

function project_root(): string {
    return dirname(__DIR__);
}

function absolute_path(string $path, string $base): string {
    if (str_starts_with($path, '/')) {
        return $path;
    }
    return rtrim($base, '/') . '/' . $path;
}

function load_engines(string $root): array {
    $local = $root . '/engines.local.json';
    $toolList = $root . '/tools/enginelist.json';
    $example = $root . '/config/engines.example.json';
    $path = match (true) {
        is_file($local) => $local,
        is_file($toolList) => $toolList,
        default => $example,
    };
    $data = json_decode((string)file_get_contents($path), true, flags: JSON_THROW_ON_ERROR);
    if (!is_array($data)) {
        throw new RuntimeException("Invalid engine config: {$path}");
    }
    return $data;
}

function subst(string $value, array $vars): string {
    foreach ($vars as $key => $replacement) {
        $value = str_replace('{' . $key . '}', $replacement, $value);
    }
    return $value;
}

function subst_cmd(array $cmd, array $vars): array {
    return array_map(static fn($part) => subst((string)$part, $vars), $cmd);
}

function shell_join(array $cmd): string {
    return implode(' ', array_map('escapeshellarg', $cmd));
}

function run_cmd(array $cmd, string $cwd, bool $check = true, ?array $env = null): array {
    $desc = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
    $procEnv = null;
    if ($env !== null) {
        $baseEnv = getenv();
        $procEnv = array_replace(is_array($baseEnv) ? $baseEnv : [], $env);
    }
    $proc = proc_open($cmd, $desc, $pipes, $cwd, $procEnv);
    if (!is_resource($proc)) {
        throw new RuntimeException('Cannot start command: ' . shell_join($cmd));
    }
    $stdout = stream_get_contents($pipes[1]);
    $stderr = stream_get_contents($pipes[2]);
    foreach ($pipes as $pipe) {
        fclose($pipe);
    }
    $code = proc_close($proc);
    $result = [
        'command' => $cmd,
        'cwd' => $cwd,
        'exit_code' => $code,
        'stdout' => $stdout,
        'stderr' => $stderr,
    ];
    if ($check && $code !== 0) {
        throw new RuntimeException(
            "Command failed ({$code}): " . shell_join($cmd) . "\nSTDOUT:\n{$stdout}\nSTDERR:\n{$stderr}"
        );
    }
    return $result;
}

function meson_compile_aborted(array $result): bool {
    return $result['exit_code'] === 134 || str_contains($result['stdout'], '[code=134]');
}

function run_meson_compile(string $buildDir, string $cwd, array $env): array {
    $compile = run_cmd(['meson', 'compile', '-C', $buildDir], $cwd, false, $env);
    if (!meson_compile_aborted($compile)) {
        return $compile;
    }

    $retry = run_cmd(['/bin/sh', '-lc', 'sleep 1; meson compile -C ' . escapeshellarg($buildDir)], $cwd, false, $env);
    if ($retry['exit_code'] === 0) {
        $retry['stdout'] = "Retry after meson compile exit 134 succeeded.\n" . $retry['stdout'];
        return $retry;
    }

    if (meson_compile_aborted($retry)) {
        $third = run_cmd(['/bin/sh', '-lc', 'sleep 2; meson compile -C ' . escapeshellarg($buildDir)], $cwd, false, $env);
        if ($third['exit_code'] === 0) {
            $third['stdout'] = "Second retry after meson compile exit 134 succeeded.\n" . $third['stdout'];
            return $third;
        }
        return $third;
    }
    return $retry;
}

function mkdirp(string $dir): void {
    if (!is_dir($dir) && !mkdir($dir, 0775, true) && !is_dir($dir)) {
        throw new RuntimeException("Cannot create directory: {$dir}");
    }
}

function rrmdir(string $dir): void {
    if (!is_dir($dir)) {
        return;
    }
    $it = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS),
        RecursiveIteratorIterator::CHILD_FIRST
    );
    foreach ($it as $path) {
        if ($path->isDir()) {
            rmdir($path->getPathname());
        } else {
            unlink($path->getPathname());
        }
    }
    rmdir($dir);
}

function copy_tree(string $src, string $dst): void {
    if (!is_dir($src)) {
        return;
    }
    mkdirp($dst);
    $it = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($src, FilesystemIterator::SKIP_DOTS),
        RecursiveIteratorIterator::SELF_FIRST
    );
    foreach ($it as $path) {
        $target = $dst . '/' . $it->getSubPathName();
        if ($path->isDir()) {
            mkdirp($target);
        } else {
            copy($path->getPathname(), $target);
        }
    }
}

function write_file(string $path, string $data): void {
    mkdirp(dirname($path));
    file_put_contents($path, $data);
}

function php2vala_runtime_arg(string $mode): string {
    if ($mode === 'native') {
        return '--native-runtime';
    }
    if ($mode === 'both') {
        return '--both-runtimes';
    }
    return '--embed-runtime';
}

function php2vala_optimizer_arg(int $level): string {
    return match ($level) {
        1 => '--no-optimize-local-vars',
        2 => '--optimize-local-vars',
        3 => '--optimize-file-func-vars',
        4 => '--optimize-cross-file-func-vars',
        default => throw new RuntimeException("Invalid php2vala optimizer level: {$level}"),
    };
}

function php2vala_rule_name(string $kind, string $mode): string {
    if ($mode === 'both') {
        return $kind;
    }
    return $kind . '-' . $mode;
}

function build_env_with_prefix(string $prefix, array $extra = []): array {
    $pkgPaths = [
        $prefix . '/lib64/pkgconfig',
        $prefix . '/lib/pkgconfig',
        $prefix . '/share/pkgconfig',
    ];
    $ldPaths = [
        $prefix . '/lib64',
        $prefix . '/lib',
    ];
    $env = [
        'CCACHE_DISABLE' => '1',
        'PKG_CONFIG_PATH' => implode(':', $pkgPaths) . ':' . (string)getenv('PKG_CONFIG_PATH'),
        'LD_LIBRARY_PATH' => implode(':', $ldPaths) . ':' . (string)getenv('LD_LIBRARY_PATH'),
        'LIBRARY_PATH' => implode(':', $ldPaths) . ':' . (string)getenv('LIBRARY_PATH'),
        'XDG_DATA_HOME' => $prefix . '/share',
        'VALAFLAGS' => '--vapidir=' . $prefix . '/share/vala/vapi ' . (string)getenv('VALAFLAGS'),
    ];
    return array_replace($env, $extra);
}

function meson_dependency_expr(string $dep, bool $forBinary, string $phpSlot, string $abiver): string {
    $dep = trim($dep);
    if ($forBinary && str_contains($dep, ':')) {
        [$libName] = explode(':', $dep, 2);
        if (preg_match('/^pw[A-Za-z0-9_]*$/', $libName)) {
            return "cc.find_library(" . meson_quote($libName . '-' . $phpSlot . '-' . $abiver) . ", required: true)";
        }
    }
    if ($forBinary && preg_match('/^pw[A-Za-z0-9_]*$/', $dep)) {
        return "cc.find_library(" . meson_quote($dep . '-' . $phpSlot . '-' . $abiver) . ", required: true)";
    }
    if ($forBinary && str_starts_with($dep, 'lib')) {
        $dep = 'valaphp-' . $dep;
    }
    return "dependency(" . meson_quote($dep) . ")";
}

function meson_dependency_array(array $deps, bool $forBinary, string $phpSlot, string $abiver): string {
    $exprs = [];
    foreach ($deps as $dep) {
        $exprs[] = meson_dependency_expr((string)$dep, $forBinary, $phpSlot, $abiver);
    }
    return '[' . implode(', ', $exprs) . ']';
}

function meson_quote(string $value): string {
    return "'" . str_replace("'", "\\'", $value) . "'";
}

function safe_id(string $value): string {
    $value = preg_replace('/[^A-Za-z0-9_]+/', '_', $value) ?? $value;
    $value = trim($value, '_');
    if ($value === '' || preg_match('/^[0-9]/', $value)) {
        $value = 'case_' . $value;
    }
    return $value;
}

function concise_failure(array $result): array {
    return [
        'command' => $result['command'],
        'cwd' => $result['cwd'],
        'exit_code' => $result['exit_code'],
        'stdout_tail' => substr($result['stdout'], -4000),
        'stderr_tail' => substr($result['stderr'], -4000),
    ];
}

function write_command_wrapper(string $wrapper, array $command, string $cwd): void {
    write_file(
        $wrapper,
        "#!/bin/sh\ncd " . escapeshellarg($cwd) . " || exit 127\nexec " . shell_join($command) . " \"\$@\"\n"
    );
    chmod($wrapper, 0775);
}

function ensure_per_case_symlink(string $buildDir, string $caseId, string $engineId): void {
    $linkDir = $buildDir . '/per_case/' . $caseId;
    mkdirp($linkDir);
    $link = $linkDir . '/' . $engineId;
    $target = '../../' . $engineId . '/' . $caseId;

    if (is_link($link)) {
        if (readlink($link) === $target) {
            return;
        }
        unlink($link);
    } elseif (file_exists($link)) {
        throw new RuntimeException("Cannot create per-case symlink, path already exists: {$link}");
    }

    if (!symlink($target, $link)) {
        throw new RuntimeException("Cannot create per-case symlink: {$link} -> {$target}");
    }
}

function count_physical_lines(string $source): int {
    $source = str_replace(["\r\n", "\r"], "\n", $source);
    $source = rtrim($source, "\n");
    if ($source === '') {
        return 0;
    }
    return substr_count($source, "\n") + 1;
}

function strip_vala_leading_comment(string $source): string {
    $source = preg_replace('/^\xEF\xBB\xBF/', '', $source) ?? $source;
    $source = ltrim($source);
    if (str_starts_with($source, '/*')) {
        $end = strpos($source, '*/');
        if ($end !== false) {
            $source = substr($source, $end + 2);
            return ltrim($source);
        }
    }
    $lines = preg_split('/\R/', $source);
    if ($lines === false) {
        return $source;
    }
    $i = 0;
    while ($i < count($lines) && trim($lines[$i]) === '') {
        $i++;
    }
    while ($i < count($lines) && preg_match('/^\s*\/\//', $lines[$i])) {
        $i++;
    }
    return implode("\n", array_slice($lines, $i));
}

function should_compare_php_vala_lines(array $engine): bool {
    $mode = (string)($engine['mode'] ?? '');
    $opt = (int)($engine['optimizer'] ?? 4);
    return $opt === 1 && in_array($mode, ['native', 'embed', 'both'], true);
}

function md_escape(string $value): string {
    return str_replace(["\r", "\n", '|'], ['\\r', ' ', '\\|'], $value);
}

function markdown_fixed_row(array $values, array $widths): string {
    $cells = [];
    foreach ($widths as $key => $width) {
        $cells[] = str_pad(md_escape((string)($values[$key] ?? '')), $width, ' ', STR_PAD_RIGHT);
    }
    return '| ' . implode(' | ', $cells) . " |\n";
}

function markdown_fixed_separator(array $widths): string {
    return '| ' . implode(' | ', array_map(static fn($width) => str_repeat('-', $width), $widths)) . " |\n";
}

function compare_php_vala_lines(array $engine, string $case, string $caseId, array $buildResult): ?array {
    if (!should_compare_php_vala_lines($engine)) {
        return null;
    }
    $sourcePhp = (string)($buildResult['source_php'] ?? $case);
    $generatedVala = (string)($buildResult['generated_vala'] ?? '');
    if (!is_file($sourcePhp) || !is_file($generatedVala)) {
        return null;
    }
    $phpLines = count_physical_lines((string)file_get_contents($sourcePhp));
    $valaRaw = (string)file_get_contents($generatedVala);
    $valaLinesRaw = count_physical_lines($valaRaw);
    $valaComparable = strip_vala_leading_comment($valaRaw);
    $valaLinesComparable = count_physical_lines($valaComparable);
    $delta = $valaLinesComparable - $phpLines;
    return [
        'case_id' => $caseId,
        'engine_id' => (string)$engine['id'],
        'mode' => (string)($engine['mode'] ?? ''),
        'optimizer' => (int)($engine['optimizer'] ?? 4),
        'source_php' => $sourcePhp,
        'generated_vala' => $generatedVala,
        'php_lines' => $phpLines,
        'vala_lines_raw' => $valaLinesRaw,
        'vala_lines_without_leading_comment' => $valaLinesComparable,
        'line_delta' => $delta,
        'line_delta_percent' => $phpLines > 0 ? ($delta / $phpLines) * 100.0 : null,
    ];
}

function write_php_vala_line_report(string $buildDir, array $rows): void {
    $pathJson = $buildDir . '/php-vala-line-report.json';
    $pathMd = $buildDir . '/php-vala-line-report.md';
    file_put_contents($pathJson, json_encode([
        'generated_at' => date(DATE_ATOM),
        'description' => 'PHP source line count versus generated Vala line count for php2vala opt=1 native/embed/both engines. Leading generated Vala comment block or line comments are excluded from comparable Vala lines.',
        'rows' => $rows,
    ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n");

    $widths = [
        'case' => 32,
        'engine' => 28,
        'mode' => 6,
        'php' => 9,
        'vala' => 10,
        'delta' => 10,
        'percent' => 9,
    ];
    $md = "# PHP/Vala Line Report\n\n";
    $md .= "Compares source PHP files against generated Vala files for `opt=1` php2vala `native`, `embed`, and `both` engines only. The comparable Vala line count excludes a generated leading comment block or leading `//` comment lines.\n\n";
    $md .= markdown_fixed_row([
        'case' => 'Case',
        'engine' => 'Engine',
        'mode' => 'Mode',
        'php' => 'PHP lines',
        'vala' => 'Vala lines',
        'delta' => 'Delta',
        'percent' => 'Delta %',
    ], $widths);
    $md .= markdown_fixed_separator($widths);
    foreach ($rows as $row) {
        $md .= markdown_fixed_row([
            'case' => (string)$row['case_id'],
            'engine' => (string)$row['engine_id'],
            'mode' => (string)$row['mode'],
            'php' => (string)$row['php_lines'],
            'vala' => (string)$row['vala_lines_without_leading_comment'],
            'delta' => (string)$row['line_delta'],
            'percent' => $row['line_delta_percent'] === null ? 'n/a' : number_format((float)$row['line_delta_percent'], 1, '.', '') . '%',
        ], $widths);
    }
    file_put_contents($pathMd, $md);
}

function pw_build_meson_supports_optimizer(string $mode): bool {
    return in_array($mode, ['native', 'embed', 'both'], true);
}

function ensure_php2vala_case_project(array $engine, string $case, string $caseId, string $projectDir): string {
    $srcDir = $projectDir . '/src';
    mkdirp($srcDir);
    copy($case, $srcDir . '/' . basename($case));
    $caseSidecarDir = dirname($case) . '/' . $caseId . '_parts';
    if (is_dir($caseSidecarDir)) {
        copy_tree($caseSidecarDir, $srcDir . '/' . basename($caseSidecarDir));
    }

    $mode = (string)($engine['mode'] ?? 'both');
    $rule = match ($mode) {
        'native' => 'vbin-native',
        'embed' => 'vbin-embed',
        'pwphp' => 'bin',
        'pwphp-jit' => 'bin-jit',
        'both' => 'vbin',
        default => throw new RuntimeException("Unknown php2vala mode '{$mode}' for engine {$engine['id']}"),
    };
    $deps = $engine['dependencies'] ?? [];
    if (!is_array($deps)) {
        throw new RuntimeException("dependencies must be an array for engine {$engine['id']}");
    }
    $depsText = $deps ? ' (' . implode(',', array_map('strval', $deps)) . ')' : '';
    $opt = (int)($engine['optimizer'] ?? 4);
    $optText = $opt >= 1 && $opt <= 4 && pw_build_meson_supports_optimizer($mode) ? " [opt={$opt}]" : '';
    $binary = safe_id($caseId . '_' . $engine['id']);

    write_file($srcDir . '/meson.build.pw', "{$rule} = " . basename($case) . " -> {$binary}{$depsText}{$optText}\n");
    write_file($projectDir . '/meson_options.txt', "option('phpslot', type: 'string', value: '', description: 'PHP slot/API version')\noption('abiver', type: 'string', value: '', description: 'pwphpbytecode ABI version')\n");
    write_file($projectDir . '/meson.build', "project('{$binary}', ['vala', 'c'], version: '0.1.0', meson_version: '>=1.0.0')\n\nphpslot = get_option('phpslot')\nabiver = get_option('abiver')\n\nrun_command(find_program('pw-build-meson'), 'src', check: true)\nsubdir('src')\n");
    return $binary;
}

function build_php2vala_engine(array $engine, string $case, string $caseId, string $artifactBase, string $phpSlot, string $abiver): array {
    if ((string)($engine['mode'] ?? '') === 'php2vala-pwphp' && $caseId !== 'php2vala_embed_pwphpbytecode_lib') {
        return ['ok' => true, 'skip' => true, 'reason' => "engine {$engine['id']} only builds php2vala/pwphpbytecode library bridge cases"];
    }
    if ($caseId === 'real_valaphp_libpwcommon') {
        return build_valaphp_library_case($engine, $case, $caseId, $artifactBase, $phpSlot, $abiver);
    }
    if (in_array($caseId, ['pwphpbytecode_library', 'pwcomposer_library'], true)) {
        return build_pwphpbytecode_library_case($engine, $case, $caseId, $artifactBase, $phpSlot, $abiver);
    }
    if ($caseId === 'php2vala_embed_pwphpbytecode_lib') {
        return build_php2vala_embed_pwphpbytecode_library_case($engine, $case, $caseId, $artifactBase, $phpSlot, $abiver);
    }
    if (in_array($caseId, ['over_shared_lib', 'api_stub_boundary'], true)) {
        return build_php2vala_shared_lib_engine($engine, $case, $caseId, $artifactBase, $phpSlot, $abiver);
    }

    $projectDir = $artifactBase . '-project';
    $buildDir = $artifactBase . '-build';
    rrmdir($projectDir);
    rrmdir($buildDir);
    $binary = ensure_php2vala_case_project($engine, $case, $caseId, $projectDir);
    $buildEnv = [
        'CCACHE_DISABLE' => '1',
        'CCACHE_DIR' => $projectDir . '/.ccache',
    ];

    $setup = run_cmd([
        'meson', 'setup', $buildDir, $projectDir,
        '--buildtype=release',
        '-Db_lto=' . (!empty($engine['lto']) ? 'true' : 'false'),
        '-Dphpslot=' . $phpSlot,
        '-Dabiver=' . $abiver,
    ], dirname($projectDir), false, $buildEnv);
    if ($setup['exit_code'] !== 0) {
        return ['ok' => false, 'stage' => 'meson_setup', 'failure' => concise_failure($setup)];
    }

    $compile = run_meson_compile($buildDir, dirname($projectDir), $buildEnv);
    if ($compile['exit_code'] !== 0) {
        return ['ok' => false, 'stage' => 'meson_compile', 'failure' => concise_failure($compile)];
    }

    $binaryPath = $buildDir . '/src/' . $binary;
    if (!is_file($binaryPath) || !is_executable($binaryPath)) {
        return [
            'ok' => false,
            'stage' => 'artifact_missing',
            'failure' => ['message' => "Expected executable not found: {$binaryPath}"],
        ];
    }
    return [
        'ok' => true,
        'command' => [$binaryPath],
        'project_dir' => $projectDir,
        'build_dir' => $buildDir,
        'source_php' => $case,
        'generated_vala' => $buildDir . '/src/' . $binary . '.vala',
    ];
}

function build_valaphp_library_case(array $engine, string $case, string $caseId, string $artifactBase, string $phpSlot, string $abiver): array {
    $mode = (string)($engine['mode'] ?? 'both');
    if (!in_array($mode, ['native', 'embed', 'both'], true)) {
        return ['ok' => true, 'skip' => true, 'reason' => "case {$caseId} is only built for php2vala runtime engines"];
    }

    $partsDir = dirname($case) . '/' . $caseId . '_parts';
    $libPhp = $partsDir . '/' . $caseId . '_lib.php';
    $mainPhp = $partsDir . '/' . $caseId . '_native.php';
    foreach ([$libPhp, $mainPhp] as $required) {
        if (!is_file($required)) {
            throw new RuntimeException("Missing {$caseId} sidecar: {$required}");
        }
    }

    $opt = (int)($engine['optimizer'] ?? 4);
    $libName = 'valaphp-libpwcommon-test';
    $libDep = 'libpwcommon-test';
    $prefix = $artifactBase . '-prefix';
    $libProjectDir = $artifactBase . '-lib-project';
    $libBuildDir = $artifactBase . '-lib-build';
    $binProjectDir = $artifactBase . '-project';
    $binBuildDir = $artifactBase . '-build';
    foreach ([$prefix, $libProjectDir, $libBuildDir, $binProjectDir, $binBuildDir] as $dir) {
        rrmdir($dir);
    }

    mkdirp($libProjectDir . '/src');
    copy($libPhp, $libProjectDir . '/src/' . basename($libPhp));
    copy_tree($partsDir, $libProjectDir . '/src/' . basename($partsDir));
    foreach (glob($partsDir . '/*.txt') ?: [] as $sidecarDataFile) {
        copy($sidecarDataFile, $libProjectDir . '/src/' . basename($sidecarDataFile));
    }
    write_file($libProjectDir . '/meson_options.txt', "option('phpslot', type: 'string', value: '', description: 'PHP slot/API version')\noption('abiver', type: 'string', value: '', description: 'pwphpbytecode ABI version')\n");
    write_file($libProjectDir . '/meson.build', "project('{$libName}', ['vala', 'c'], version: '0.1.0', meson_version: '>=1.0.0')\n\nphpslot = get_option('phpslot')\nabiver = get_option('abiver')\n\nrun_command(find_program('pw-build-meson'), 'src', check: true)\nsubdir('src')\n");
    $vlibRule = php2vala_rule_name('vlib', $mode);
    write_file($libProjectDir . '/src/meson.build.pw', "{$vlibRule} = " . basename($libPhp) . " -> {$libName} (valaphp-runtime-core,valaphp-runtime-std,valaphp-runtime-embed) [opt={$opt}]\n");

    $buildEnv = build_env_with_prefix($prefix, ['CCACHE_DIR' => $libProjectDir . '/.ccache']);
    $setup = run_cmd(['meson', 'setup', $libBuildDir, $libProjectDir, '--buildtype=release', '--prefix=' . $prefix, '-Dphpslot=' . $phpSlot, '-Dabiver=' . $abiver], dirname($libProjectDir), false, $buildEnv);
    if ($setup['exit_code'] !== 0) {
        return ['ok' => false, 'stage' => 'library_meson_setup', 'failure' => concise_failure($setup)];
    }
    $compile = run_meson_compile($libBuildDir, dirname($libProjectDir), $buildEnv);
    if ($compile['exit_code'] !== 0) {
        return ['ok' => false, 'stage' => 'library_meson_compile', 'failure' => concise_failure($compile)];
    }
    $install = run_cmd(['meson', 'install', '-C', $libBuildDir, '--no-rebuild'], dirname($libProjectDir), false, $buildEnv);
    if ($install['exit_code'] !== 0) {
        return ['ok' => false, 'stage' => 'library_meson_install', 'failure' => concise_failure($install)];
    }

    mkdirp($binProjectDir . '/src');
    copy($mainPhp, $binProjectDir . '/src/' . basename($mainPhp));
    copy_tree($partsDir, $binProjectDir . '/src/' . basename($partsDir));
    write_file($binProjectDir . '/meson_options.txt', "option('phpslot', type: 'string', value: '', description: 'PHP slot/API version')\noption('abiver', type: 'string', value: '', description: 'pwphpbytecode ABI version')\n");
    write_file($binProjectDir . '/meson.build', "project('{$caseId}', ['vala', 'c'], version: '0.1.0', meson_version: '>=1.0.0')\n\nphpslot = get_option('phpslot')\nabiver = get_option('abiver')\n\nrun_command(find_program('pw-build-meson'), 'src', check: true)\nsubdir('src')\n");
    $vbinRule = php2vala_rule_name('vbin', $mode);
    $binary = safe_id($caseId . '_' . $engine['id']);
    write_file($binProjectDir . '/src/meson.build.pw', "{$vbinRule} = " . basename($mainPhp) . " -> {$binary} ({$libDep},valaphp-runtime-core,valaphp-runtime-std,valaphp-runtime-embed) [opt={$opt}]\n");

    $buildEnv = build_env_with_prefix($prefix, ['CCACHE_DIR' => $binProjectDir . '/.ccache']);
    $setup = run_cmd(['meson', 'setup', $binBuildDir, $binProjectDir, '--buildtype=release', '-Dphpslot=' . $phpSlot, '-Dabiver=' . $abiver], dirname($binProjectDir), false, $buildEnv);
    if ($setup['exit_code'] !== 0) {
        return ['ok' => false, 'stage' => 'consumer_meson_setup', 'failure' => concise_failure($setup)];
    }
    $compile = run_meson_compile($binBuildDir, dirname($binProjectDir), $buildEnv);
    if ($compile['exit_code'] !== 0) {
        return ['ok' => false, 'stage' => 'consumer_meson_compile', 'failure' => concise_failure($compile)];
    }
    $binaryPath = $binBuildDir . '/src/' . $binary;
    if (!is_file($binaryPath) || !is_executable($binaryPath)) {
        return ['ok' => false, 'stage' => 'artifact_missing', 'failure' => ['message' => "Expected executable not found: {$binaryPath}"]];
    }
    $wrapper = $artifactBase . '-run.sh';
    write_file($wrapper, "#!/bin/sh\nexport LD_LIBRARY_PATH=" . escapeshellarg($prefix . '/lib64:' . $prefix . '/lib') . '":${LD_LIBRARY_PATH:-}"' . "\nexec " . escapeshellarg($binaryPath) . " \"\$@\"\n");
    chmod($wrapper, 0775);
    return [
        'ok' => true,
        'command' => [$wrapper],
        'project_dir' => $binProjectDir,
        'build_dir' => $binBuildDir,
        'source_php' => $mainPhp,
        'generated_vala' => $binBuildDir . '/src/' . $binary . '.vala',
    ];
}

function build_pwphpbytecode_library_case(array $engine, string $case, string $caseId, string $artifactBase, string $phpSlot, string $abiver): array {
    $mode = (string)($engine['mode'] ?? '');
    if (!in_array($mode, ['pwphp', 'pwphp-jit'], true)) {
        return ['ok' => true, 'skip' => true, 'reason' => "case {$caseId} is only built for pwphpbytecode engines"];
    }
    $className = $caseId === 'pwcomposer_library' ? 'PwComposerLibraryCase' : 'PwBytecodeLibraryCase';
    $classFile = $className . '.php';
    $libName = $caseId === 'pwcomposer_library' ? 'pwcomposerlibcase' : 'pwbytecodelibcase';
    return build_pwphpbytecode_class_library_consumer($engine, $case, $caseId, $artifactBase, $phpSlot, $abiver, $className, $classFile, $libName, 'bin');
}

function build_php2vala_embed_pwphpbytecode_library_case(array $engine, string $case, string $caseId, string $artifactBase, string $phpSlot, string $abiver): array {
    $mode = (string)($engine['mode'] ?? '');
    if ($mode !== 'php2vala-pwphp') {
        return ['ok' => true, 'skip' => true, 'reason' => "case {$caseId} is only built for php2vala/pwphpbytecode bridge engines"];
    }
    return build_pwphpbytecode_class_library_consumer($engine, $case, $caseId, $artifactBase, $phpSlot, $abiver, 'PwEmbedBytecodeCase', 'PwEmbedBytecodeCase.php', 'pwembedbytecodelibcase', 'vbin-pwphp');
}

function build_pwphpbytecode_class_library_consumer(array $engine, string $case, string $caseId, string $artifactBase, string $phpSlot, string $abiver, string $className, string $classFile, string $libName, string $consumerKind): array {
    $partsDir = dirname($case) . '/' . $caseId . '_parts';
    $classPhp = $partsDir . '/' . $classFile;
    $mainPhp = $partsDir . '/' . $caseId . '_native.php';
    foreach ([$classPhp, $mainPhp] as $required) {
        if (!is_file($required)) {
            throw new RuntimeException("Missing {$caseId} sidecar: {$required}");
        }
    }

    $prefix = $artifactBase . '-prefix';
    $libProjectDir = $artifactBase . '-lib-project';
    $libBuildDir = $artifactBase . '-lib-build';
    $binProjectDir = $artifactBase . '-project';
    $binBuildDir = $artifactBase . '-build';
    foreach ([$prefix, $libProjectDir, $libBuildDir, $binProjectDir, $binBuildDir] as $dir) {
        rrmdir($dir);
    }

    mkdirp($libProjectDir . '/src');
    copy($classPhp, $libProjectDir . '/src/' . $classFile);
    write_file($libProjectDir . '/meson_options.txt', "option('phpslot', type: 'string', value: '', description: 'PHP slot/API version')\noption('abiver', type: 'string', value: '', description: 'pwphpbytecode ABI version')\n");
    write_file($libProjectDir . '/meson.build', "project('{$libName}', 'c', version: '0.1.0', meson_version: '>=1.0.0')\n\nphpslot = get_option('phpslot')\nabiver = get_option('abiver')\n\nrun_command(find_program('pw-build-meson'), 'src', phpslot, abiver, '--no-doxygen', check: true)\nsubdir('src')\n");
    write_file($libProjectDir . '/src/meson.build.pw', "class = {$classFile} -> {$className}\nlib = {$className} -> {$libName}\n");

    $buildEnv = build_env_with_prefix($prefix, ['CCACHE_DIR' => $libProjectDir . '/.ccache']);
    $setup = run_cmd(['meson', 'setup', $libBuildDir, $libProjectDir, '--buildtype=release', '--prefix=' . $prefix, '-Dphpslot=' . $phpSlot, '-Dabiver=' . $abiver], dirname($libProjectDir), false, $buildEnv);
    if ($setup['exit_code'] !== 0) {
        return ['ok' => false, 'stage' => 'pwphp_library_meson_setup', 'failure' => concise_failure($setup)];
    }
    $compile = run_meson_compile($libBuildDir, dirname($libProjectDir), $buildEnv);
    if ($compile['exit_code'] !== 0) {
        return ['ok' => false, 'stage' => 'pwphp_library_meson_compile', 'failure' => concise_failure($compile)];
    }
    $install = run_cmd(['meson', 'install', '-C', $libBuildDir, '--no-rebuild'], dirname($libProjectDir), false, $buildEnv);
    if ($install['exit_code'] !== 0) {
        return ['ok' => false, 'stage' => 'pwphp_library_meson_install', 'failure' => concise_failure($install)];
    }

    mkdirp($binProjectDir . '/src');
    copy($mainPhp, $binProjectDir . '/src/' . basename($mainPhp));
    $binary = safe_id($caseId . '_' . $engine['id']);
    write_file($binProjectDir . '/meson_options.txt', "option('phpslot', type: 'string', value: '', description: 'PHP slot/API version')\noption('abiver', type: 'string', value: '', description: 'pwphpbytecode ABI version')\n");
    $projectLang = $consumerKind === 'vbin-pwphp' ? "['vala', 'c']" : "'c'";
    write_file($binProjectDir . '/meson.build', "project('{$caseId}', {$projectLang}, version: '0.1.0', meson_version: '>=1.0.0')\n\nphpslot = get_option('phpslot')\nabiver = get_option('abiver')\n\nrun_command(find_program('pw-build-meson'), 'src', phpslot, abiver, '--no-doxygen', check: true)\nsubdir('src')\n");
    $rule = $consumerKind;
    if ($consumerKind === 'bin' && (string)($engine['mode'] ?? '') === 'pwphp-jit') {
        $rule = 'bin-jit';
    }
    $dep = $consumerKind === 'vbin-pwphp' ? $libName : "{$libName}:{$className}";
    write_file($binProjectDir . '/src/meson.build.pw', "{$rule} = " . basename($mainPhp) . " -> {$binary} ({$dep})\n");

    $buildEnv = build_env_with_prefix($prefix, ['CCACHE_DIR' => $binProjectDir . '/.ccache']);
    $setup = run_cmd(['meson', 'setup', $binBuildDir, $binProjectDir, '--buildtype=release', '-Dphpslot=' . $phpSlot, '-Dabiver=' . $abiver], dirname($binProjectDir), false, $buildEnv);
    if ($setup['exit_code'] !== 0) {
        return ['ok' => false, 'stage' => 'pwphp_consumer_meson_setup', 'failure' => concise_failure($setup)];
    }
    $compile = run_meson_compile($binBuildDir, dirname($binProjectDir), $buildEnv);
    if ($compile['exit_code'] !== 0) {
        return ['ok' => false, 'stage' => 'pwphp_consumer_meson_compile', 'failure' => concise_failure($compile)];
    }
    $binaryPath = $binBuildDir . '/src/' . $binary;
    if (!is_file($binaryPath) || !is_executable($binaryPath)) {
        return ['ok' => false, 'stage' => 'artifact_missing', 'failure' => ['message' => "Expected executable not found: {$binaryPath}"]];
    }
    $wrapper = $artifactBase . '-run.sh';
    write_file($wrapper, "#!/bin/sh\nexport LD_LIBRARY_PATH=" . escapeshellarg($prefix . '/lib64:' . $prefix . '/lib') . '":${LD_LIBRARY_PATH:-}"' . "\nexport XDG_DATA_HOME=" . escapeshellarg($prefix . '/share') . "\nexec " . escapeshellarg($binaryPath) . " \"\$@\"\n");
    chmod($wrapper, 0775);
    return [
        'ok' => true,
        'command' => [$wrapper],
        'project_dir' => $binProjectDir,
        'build_dir' => $binBuildDir,
        'source_php' => $mainPhp,
        'generated_vala' => $consumerKind === 'vbin-pwphp' ? $binBuildDir . '/src/' . $binary . '.vala' : '',
    ];
}

function build_php2vala_shared_lib_engine(array $engine, string $case, string $caseId, string $artifactBase, string $phpSlot, string $abiver): array {
    $projectDir = $artifactBase . '-project';
    $buildDir = $artifactBase . '-build';
    rrmdir($projectDir);
    rrmdir($buildDir);

    $srcDir = $projectDir . '/src';
    mkdirp($srcDir);
    $partsDir = dirname($case) . '/' . $caseId . '_parts';
    $mainPhp = $partsDir . '/' . $caseId . '_native.php';
    $stubPhp = $partsDir . '/' . $caseId . '.stub.php';
    $libVala = $partsDir . '/' . $caseId . '.vala';
    foreach ([$mainPhp, $stubPhp, $libVala] as $required) {
        if (!is_file($required)) {
            throw new RuntimeException("Missing {$caseId} sidecar: {$required}");
        }
    }
    copy($mainPhp, $srcDir . '/' . $caseId . '.php');
    copy($stubPhp, $srcDir . '/' . $caseId . '.stub.php');
    copy($libVala, $srcDir . '/' . $caseId . '.vala');

    $mode = (string)($engine['mode'] ?? 'both');
    if (!in_array($mode, ['native', 'embed', 'both', 'pwphp', 'pwphp-jit'], true)) {
        throw new RuntimeException("Unknown php2vala mode '{$mode}' for engine {$engine['id']}");
    }
    $opt = (int)($engine['optimizer'] ?? 4);
    $runtimeArg = php2vala_runtime_arg($mode);
    $optimizerArg = php2vala_optimizer_arg($opt);
    $binary = safe_id($caseId . '_' . $engine['id']);

    $deps = $engine['dependencies'] ?? [];
    if (!is_array($deps)) {
        throw new RuntimeException("dependencies must be an array for engine {$engine['id']}");
    }
    $deps[] = 'valaphp-runtime-core';
    if ($mode === 'native' || $mode === 'both') {
        $deps[] = 'valaphp-runtime-std';
    }
    if ($mode === 'embed' || $mode === 'both' || $mode === 'pwphp' || $mode === 'pwphp-jit') {
        $deps[] = 'valaphp-runtime-embed';
    }
    if ($mode === 'pwphp' || $mode === 'pwphp-jit') {
        $deps[] = 'valaphp-runtime-embed-pwphpbytecode';
    }
    $deps = array_values(array_unique(array_map('strval', $deps)));
    $depArray = meson_dependency_array($deps, true, $phpSlot, $abiver);

    write_file($projectDir . '/meson.build', "project('{$binary}', ['vala', 'c'], version: '0.1.0', meson_version: '>=1.0.0')\n\nsubdir('src')\n");
    write_file($srcDir . '/meson.build', <<<MESON
php2vala = find_program('php2vala')
php = find_program('php')
cc = meson.get_compiler('c')
valaphp_c_args = cc.get_supported_arguments([
  '-Wno-discarded-qualifiers',
  '-Wno-unused-but-set-variable',
  '-Wno-unused-parameter',
  '-Wno-unused-variable',
])
bench_deps = {$depArray}
core_dep = dependency('valaphp-runtime-core')

bench_shared = shared_library(
  '{$caseId}_bench',
  '{$caseId}.vala',
  dependencies: [core_dep],
  c_args: valaphp_c_args,
  vala_header: '{$caseId}_bench.h',
  install: false,
)

generated_main = custom_target(
  '{$caseId}-main-vala',
  input: '{$caseId}.php',
  output: '{$caseId}_main.vala',
  command: [
    php2vala,
    '--php-stub',
    meson.current_source_dir() / '{$caseId}.stub.php',
    '{$runtimeArg}',
    '{$optimizerArg}',
    '@INPUT@',
    '@OUTPUT@',
  ],
)

executable(
  '{$binary}',
  generated_main,
  dependencies: bench_deps,
  link_with: bench_shared,
  include_directories: include_directories('.'),
  c_args: valaphp_c_args,
  install: false,
)
MESON);

    $buildEnv = [
        'CCACHE_DISABLE' => '1',
        'CCACHE_DIR' => $projectDir . '/.ccache',
    ];
    $setup = run_cmd(['meson', 'setup', $buildDir, $projectDir, '--buildtype=release'], dirname($projectDir), false, $buildEnv);
    if ($setup['exit_code'] !== 0) {
        return ['ok' => false, 'stage' => 'meson_setup', 'failure' => concise_failure($setup)];
    }
    $compile = run_meson_compile($buildDir, dirname($projectDir), $buildEnv);
    if ($compile['exit_code'] !== 0) {
        return ['ok' => false, 'stage' => 'meson_compile', 'failure' => concise_failure($compile)];
    }

    $binaryPath = $buildDir . '/src/' . $binary;
    $libDir = $buildDir . '/src';
    if (!is_file($binaryPath) || !is_executable($binaryPath)) {
        return [
            'ok' => false,
            'stage' => 'artifact_missing',
            'failure' => ['message' => "Expected executable not found: {$binaryPath}"],
        ];
    }

    $wrapper = $artifactBase . '-run.sh';
    write_file($wrapper, "#!/bin/sh\nLD_LIBRARY_PATH=" . escapeshellarg($libDir) . '":${LD_LIBRARY_PATH:-}" exec ' . escapeshellarg($binaryPath) . " \"\$@\"\n");
    chmod($wrapper, 0775);
    return [
        'ok' => true,
        'command' => [$wrapper],
        'project_dir' => $projectDir,
        'build_dir' => $buildDir,
        'source_php' => $mainPhp,
        'generated_vala' => $buildDir . '/src/' . $caseId . '_main.vala',
    ];
}

$root = project_root();
$casesDir = absolute_path(arg_value($argv, '--cases', $root . '/generated/cases'), $root);
$buildDir = absolute_path(arg_value($argv, '--build-dir', $root . '/build/engine-build'), $root);
$phpSlot = arg_value($argv, '--php-slot', '8.3');
$abiver = arg_value($argv, '--abiver', '3');
$stamp = arg_value($argv, '--touch-stamp');
$caseFilter = arg_list($argv, '--case-ids');
$engineFilter = arg_list($argv, '--engine-ids');

mkdirp($buildDir);
$cases = glob($casesDir . '/*.php') ?: [];
sort($cases);
if ($caseFilter !== null) {
    $cases = array_values(array_filter(
        $cases,
        static fn($case) => filter_enabled($caseFilter, basename($case, '.php'))
    ));
}

$commands = [];
$phpValaLineRows = [];
$buildReport = [
    'generated_at' => date(DATE_ATOM),
    'cases_dir' => $casesDir,
    'build_dir' => $buildDir,
    'php_slot' => $phpSlot,
    'abiver' => $abiver,
    'engines' => [],
    'failures' => [],
    'skips' => [],
];

$plannedEngines = array_values(array_filter(load_engines($root), static function (array $engine) use ($engineFilter): bool {
    if (empty($engine['enabled'])) {
        return false;
    }
    if ($engineFilter !== null && !filter_enabled($engineFilter, (string)$engine['id'])) {
        return false;
    }
    return true;
}));
$totalBuildSteps = count($plannedEngines) * count($cases);
$currentBuildStep = 0;
fwrite(STDERR, sprintf(
    "Build target: %d engines x %d cases = %d engine/case artifacts in %s\n",
    count($plannedEngines),
    count($cases),
    $totalBuildSteps,
    $buildDir
));

foreach (load_engines($root) as $engine) {
    if (empty($engine['enabled'])) {
        continue;
    }
    $id = (string)$engine['id'];
    if ($engineFilter !== null && !filter_enabled($engineFilter, $id)) {
        continue;
    }
    $buildReport['engines'][$id] = ['ok' => 0, 'failed' => 0, 'skipped' => 0];
    foreach ($cases as $case) {
        $caseId = basename($case, '.php');
        $currentBuildStep++;
        fwrite(STDERR, sprintf(
            "[build %d/%d] %s/%s\n",
            $currentBuildStep,
            $totalBuildSteps,
            $id,
            $caseId
        ));
        $artifact = $buildDir . '/' . $id . '/' . $caseId . '/' . $caseId;
        mkdirp(dirname($artifact));
        $vars = [
            'root' => $root,
            'case' => $case,
            'case_id' => $caseId,
            'build_dir' => dirname($artifact),
            'artifact' => $artifact,
            'php_slot' => $phpSlot,
            'abiver' => $abiver,
        ];

        $command = null;
        $commandMetadata = [];
        $type = (string)($engine['type'] ?? 'command');
        if ($type === 'pw-build-meson') {
            $result = build_php2vala_engine($engine, $case, $caseId, $artifact, $phpSlot, $abiver);
            if (!empty($result['skip'])) {
                $buildReport['engines'][$id]['skipped']++;
                $buildReport['skips'][] = [
                    'engine_id' => $id,
                    'case_id' => $caseId,
                    'reason' => (string)($result['reason'] ?? 'not applicable'),
                ];
                continue;
            }
            if (!$result['ok']) {
                $buildReport['engines'][$id]['failed']++;
                $buildReport['failures'][] = [
                    'engine_id' => $id,
                    'case_id' => $caseId,
                    'stage' => $result['stage'],
                    'failure' => $result['failure'],
                ];
                fwrite(STDERR, "COMPILE ERROR {$id}/{$caseId} at {$result['stage']}\n");
                continue;
            }
            $command = $result['command'];
            foreach (['project_dir', 'build_dir', 'source_php', 'generated_vala'] as $metadataKey) {
                if (isset($result[$metadataKey]) && (string)$result[$metadataKey] !== '') {
                    $commandMetadata[$metadataKey] = (string)$result[$metadataKey];
                }
            }
            $lineRow = compare_php_vala_lines($engine, $case, $caseId, $result);
            if ($lineRow !== null) {
                $phpValaLineRows[] = $lineRow;
            }
            $buildReport['engines'][$id]['ok']++;
        } else {
            foreach (($engine['build'] ?? []) as $buildCmd) {
                run_cmd(subst_cmd($buildCmd, $vars), $root);
            }
            $rawCommand = subst_cmd($engine['command'], $vars);
            $wrapper = $artifact . '-run.sh';
            write_command_wrapper($wrapper, $rawCommand, $root);
            $command = [$wrapper];
            $buildReport['engines'][$id]['ok']++;
        }

        ensure_per_case_symlink($buildDir, $caseId, $id);
        $commands[] = [
            'engine_id' => $id,
            'engine_label' => $engine['label'] ?? $id,
            'case_id' => $caseId,
            'case' => $case,
            'command' => $command,
        ] + $commandMetadata;
    }
}

file_put_contents(
    $buildDir . '/engine-commands.json',
    json_encode($commands, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"
);
file_put_contents(
    $buildDir . '/build-report.json',
    json_encode($buildReport, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"
);
write_php_vala_line_report($buildDir, $phpValaLineRows);

if ($stamp !== null) {
    file_put_contents($stamp, "built " . date(DATE_ATOM) . "\n");
}

fwrite(STDOUT, "Prepared " . count($commands) . " engine commands in {$buildDir}\n");
fwrite(STDOUT, "Build report: {$buildDir}/build-report.json\n");
